
От патча к RCE: собственноручно собранный эксплойт для CVE-2010-4221 (переполнение стека TELNET IAC в ProFTPD), с полным описанием пути, продиктованного ошибками
Полностью воспроизводимый лабораторный стенд и написанный вручную эксплойт на сырых сокетах для
CVE-2010-4221 — переполнение стекового буфера до аутентификации в
pr_netio_telnet_gets() ProFTPD — созданный как учебное упражнение по
исследованию уязвимостей и разработке эксплойтов.
Каждая неудача задокументирована. Прямой путь — ложь; обходные пути — и есть урок.
Этот репозиторий — учебный артефакт. Он существует, чтобы люди, которые не могут позволить себе наставника или обучение, могли узнать, как на самом деле рождается эксплойт повреждения памяти: от патча, через неудачи, до работающего подтверждения концепции в лаборатории, которой вы владеете.
Работа Red Team — настоящая, профессиональная наступательная безопасность — определяется одним словом: авторизация. Всё, что делает профессионал, происходит в рамках письменного соглашения: подписанного документа о правилах взаимодействия, в котором указаны объём, цели, разрешённые техники, временное окно и люди, которые это одобрили. Без этого документа те же самые нажатия клавиш — это не профессия, а преступление практически в любой юрисдикции на Земле.
Итак, вот контракт для этого репозитория, без права обсуждения:
Ремесло стоит того, чтобы его изучать. Ремесло чего-то стоит только с дисциплиной, которая приходит вместе с ним.
ProFTPD обрабатывает TELNET escape-последовательности на управляющем канале FTP. В
TELNET байт 0xFF (IAC, «Interpret As Command») является escape-байтом;
литеральный 0xFF передаётся как 0xFF 0xFF.
pr_netio_telnet_gets() копирует байты клиента в стековый буфер
(char buf[PR_DEFAULT_CMD_BUFSZ+1] из pr_cmd_read, 4104 байта при
MAXPATHLEN=4096 в glibc), отслеживая оставшееся место в buflen — переменной
типа size_t, беззнаковой.
Уязвимый путь (1.3.3a, netio.c):
case TELNET_IAC:
switch (cp) {
...
default:
*bp++ = TELNET_IAC; // запись №1
buflen--; // уменьшение №1
telnet_mode = 0;
break;
}
break;
...
*bp++ = cp; // запись №2
buflen--; // уменьшение №2 <-- нет проверки между ними
Две записи, два уменьшения, без проверки нуля между ними. Когда buflen
равно ровно 1, пара уменьшает его до 0, а затем происходит переполнение до
SIZE_MAX (18 квинтиллионов). Теперь цикл считает, что буфер бесконечен,
и продолжает записывать управляемые атакующим байты вверх по стеку — поверх сохранённых
регистров, сохранённого RBP и адреса возврата.
До аутентификации. Функция выполняется до того, как USER/PASS вообще обрабатываются.
Исправление (коммит
3cc69b8388,
«Bug#3521 - Telnet IAC processing stack overflow», выпущено в 1.3.3c)
состоит из двенадцати строк. Вся граница безопасности — это:
if (buflen == 0) {
break;
}
См. patch.diff. Чтение патча показывает, где была рана — это и есть навык.
Dockerfile компилирует ProFTPD 1.3.3a из исходников исторического снимка Debian, намеренно небезопасно (так выглядел 2010 год):
-fno-stack-protector — без канарейки-z execstack — исполняемый стек (без NX)-no-pie — фиксированные адреса бинарникаgdb, который отключает ASLR по умолчанию → детерминированный стекdocker build -t proftpd-133a .
docker rm -f lab133 2>/dev/null
docker run -d --name lab133 --cap-add SYS_PTRACE \
--security-opt seccomp=unconfined -p 127.0.0.1:2122:21 \
proftpd-133a sh -c 'gdb -batch -ex "set follow-fork-mode child" \
-ex "run" -ex "continue" --args /usr/local/sbin/proftpd -n -d1 \
> /tmp/gdb.txt 2>&1; sleep 600'
python3 exploit.py
Ожидаемый вывод:
[S] 220 ProFTPD 1.3.3a Server (lab-iac) ...
[S] THE SERVER SAID: b'PWNED!!PWNED!!'
(Шеллкод пишет в файловые дескрипторы 0, 1 и 2, потому что мы не хотели зависеть от знания того, какой из них несёт управляющий канал — два из них отвечают.)
"SITE " + NOP-слайд + шеллкод + [\xff\xff флуд] + паддинг + [ret] + "\n"
^^^^^^^^^^^^^^^^^^^^ ^^^^
шеллкод живёт ВНУТРИ буфера хвост переполнения доставляет
команды — область, которую никто только ОДИН адрес
не трогает
buf[4102]
(завершающий NUL). Всё ниже живых локальных переменных фрейма спокойно.buflen до переполнения (см. «Путь»
о проблеме чётности).pr_cmd_read достигает return 0 после разбора,
CPU попадает в слайд и соскальзывает в шеллкод.Эта инвертированная архитектура — сначала полезная нагрузка, затем флуд, адрес в конце —
была проверена против канонического модуля Metasploit
(proftp_telnet_iac), который использует ту же компоновку. Их цели имели NX,
поэтому им требовалась ROP-цепочка с «четверным разыменованием» указателя res;
в нашей лаборатории исполняемый стек, поэтому достаточно одного прямого возврата.
Финальный эксплойт — 60 строк. Во что это обошлось:
Слепой \xff флуд → ничего. Сервер вежливо закрыл
сессию. Корневая причина: buflen начинается с 4102 (ЧЁТНОЕ), и каждая
пара IAC уменьшает на 2 — он попадает ровно на 0, никогда на 1. Для
переполнения нужна нарушенная чётность. Урок: чтение конечного автомата лучше, чем распыление.
Неверный размер буфера. Первая калиброванная попытка предполагала буфер в 1024 байта.
Реальный — MAXPATHLEN+8 = 4104 в Linux/glibc. Флуд
остановился на 3 КБ раньше цели. Урок: измеряйте цель, не предполагайте.
Первый SIGSEGV. Циклический узор де Брёйна (Aa0Aa1...) поместил
слот возврата на buf+4152, перекрёстно проверенный дважды (математика фрейма + смещение узора). Урок: циклический узор — это измерительная лента, а не эксплойт.
Контроль RIP. Установка слота в 0x4141414141414141 вызвала сбой
самой инструкции ret — x86-64 отвергает неканонические адреса, и
ошибка происходит на ret, а наше значение ожидает в обратной трассировке.
Урок: сбой на ret с вашим значением во фрейме = контроль.
Шеллкод выше слота ret → затёрт. 8 байт перезаписаны
указателем кучи (0x4d7838 — позже идентифицирован как выделение пула cmd_rec). Стек-фреймы выше слота принадлежат функциям, которые всё ещё
работают между приземлением и перехватом. Урок: переполнение — не последняя запись; программа продолжает жить в стеке, который вы только что испортили.
«Мёртвая зона» ниже слота → тоже исписана. Собственные локальные
переменные pr_cmd_read (cmd, buflen, cp) живут прямо там и продолжают
сохраняться во время разбора.
Криминалистика аппаратной точкой останова. watch *(long*)ADDR в gdb превратил
загадку в камеру: каждая запись в затёртый адрес, с
обратной трассировкой, по порядку. Урок: когда вопрос «кто записал эту память?»,
ответ — в одной команде gdb.
Прочтите эталон, затем поймите его. Канонический модуль подтвердил инвертированную архитектуру. Чтение чужого эксплойта после построения собственной мысленной модели — это изучение; до — это копирование.
size_t никогда не становится отрицательным — он становится гигантским. Целочисленное
переполнение в счётчике оставшегося места — это переполнение стека с дополнительными шагами.\x0a
(завершает чтение) и переживает \xff (TELNET escape) по замыслу.argv[0] (бинарник из дерева сборки против установленного) сдвинула весь
стек на 0x40 и молча аннулировала идеальный эксплойт.modules/exploits/linux/ftp/proftp_telnet_iac.rb
(rapid7/metasploit-framework)Создано diegslva, обучение на публике — от «никогда не писал эксплойты» до RCE до аутентификации с шеллкодом ручной работы за один задокументированный день. Если этот репозиторий чему-то вас научил, передайте это дальше.