На прошлой неделе обнаружили RCE React2Shell (CVE-2025-55182), коммьюнити ИБ пестрит этой новостью. А тем временем уже начали её использовать “in the wild”. Однако, нам с вами куда интереснее какие следы оставляет эксплуатация данной уязвимости на стороне сервера.
Для начала разберемся какие есть способы эксплуатации. В ходе эксплуатации используется легитимный протокол React Server Components (RSC). Атакующий может доставить пейлоад тремя разными способами: через JSON, Multipart-форму или GET-параметры.
Атака через POST-запрос. Ключевой маркер — специфический заголовок Next-Action или rsc-action-id в логах реверс прокси:
grep -rE "next-action|rsc-action-id" /var/log/nginx/access.log
также можно чекнуть наличие бинарей:
grep -E 'wget|curl|bash|sh|python|nc' /var/log/nginx/access.log
В стандартных логах выглядит как легитимный POST. Для детекта требуется включать расширенные логи, с телом запроса client_body_in_file_only.
В случае, если расширенное логгирование включено, ищем Content-Type: multipart/form-data, в JSONе будет содержаться нагрузка:
grep -rE '\$@|"status":"resolved_model"|"\$L"|"\$B"' /var/log/
Тогда можно поискать артефакты выполнения эксплойта в директории с телом запроса. Параметр client_body_buffer_size (если оно было больше 8kb-16kb).
Важный нюанс: когда Nginx получает multipart запрос, который больше определенного размера (параметр client_body_buffer_size, обычно 8kb-16kb), он сбрасывает тело запроса на диск во временный файл по пути: /var/lib/nginx/body/ или /tmp/nginx_client_body/. Там можно найти:
Content-Disposition, Content-Type );execSync('command'))ls -lart /var/lib/nginx/body/ | tail -20
strings /var/lib/nginx/body/* | grep -E "execSync|require.*child"
⏰ Критическое окно: Nginx удаляет временный файл сразу после обработки запроса (~100-200ms). Требуется быстрая реакция!
Если файл уже удален, остается только попытка восстановления:
sudo photorec /dev/sda1
# или
sudo extundelete /dev/sda1 --restore-all
После восстановления, пробуем найти в RECOVERED_FILES:
grep -rE "process.mainModule|execSync" ./RECOVERED_FILES/
Если Next.js Server Action обрабатывает загруженные файлы (например, загрузка аватарки), он парсит multipart форму через FormData API. Когда Node.js парсит большой multipart, некоторые библиотеки (например, busboy, formidable) сбрасывают части файлов во временную директорию.
# Стандартная временная папка Node.js
ls -lart /tmp/ | grep -E "form|upload|tmp|node"
# Или в более специфичной папке
ls -lart /tmp/busboy* 2>/dev/null
ls -lart /tmp/formidable* 2>/dev/null
# Также проверяем /var/tmp
ls -lart /var/tmp/ | grep -E "form|node"
# Смотрим по времени доступа (когда были созданы)
find /tmp -type f -mmin -2 -size +100c # Файлы измененные в последние 2 минуты
# Чекаем строки, похожие на JavaScript код (признак пейлоада)
strings /tmp/* | grep -E "execSync|require.*child_process|then.*proto"
Вместо multipart/form-data атакующий может отправить данные как URL-encoded:
POST / HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Next-Action: [ACTION_ID]
cmd=touch%20/tmp/pwned.txt&other=data
В таком случае, в расширенных логах ищем:
grep "application/x-www-form-urlencoded" /var/log/nginx/access.log
grep -E "cmd=|bash|sh|python|nc|curl|wget" /var/log/nginx/access.log
grep -E "%20|%26|%7C|%60" /var/log/nginx/access.log | grep -E "bash|id|whoami"
Нюанс: URL-encoded часто более скрыта, чем multipart, потому что WAF’ы редко ловят этот Content-Type. Но в памяти процесса следы остаются одинаковые.
Даже если логирование выключено или payload доставлен скрытно, если RCE произошла, процесс Node.js ВСЕГДА спавнит bash/sh/python. Это первое, на что нужно смотреть.
RCE = node спавнил bash. Это видно всегда, независимо от логирования:
# Визуализация дерева (лучший вариант)
ps -ef --forest | grep -A 2 "node" | grep -E "bash|sh|python"
# Или через pstree
pstree -ap | grep node -A 5
Ищем ESTABLISHED соединения от процесса node на нестандартные порты:
# Через ss (быстро)
ss -tunap | grep node | grep ESTABLISHED
# Или через lsof
lsof -i -P -n | grep node | grep ESTABLISHED
Что ищем:
node 12345 www-data 45u IPv4 98765 0t0 TCP 192.168.1.100:45678->attacker.com:4444 (ESTABLISHED)
Если сервер был выключен/crashed во время атаки, payload может остаться в swap или можно сделать дамп памяти живого процесса.
# Через gdb
sudo gdb -p $(pgrep -f "node") -batch -ex "dump memory /tmp/mem.bin 0 0xffffffff" -ex quit
# Ищем паттерны
strings /tmp/mem.bin | grep -E "execSync|process.mainModule|cmd="
# Или через /proc напрямую
sudo dd if=/proc/$(pgrep -f "node")/mem of=/tmp/node_mem.bin
strings /tmp/node_mem.bin | grep "execSync"
# Дампим swap
sudo dd if=/dev/sda2 of=/tmp/swap_dump.img # если sda2 это swap
# Ищем следы
strings /tmp/swap_dump.img | grep -E "execSync|process.mainModule|resolved_model"
# Если у вас есть дамп памяти
volatility -f memory.dump linux_pslist # Список процессов в момент дампа
volatility -f memory.dump linux_strings | grep "execSync"
Если приложение в контейнере:
# Ошибки протокола React Flight (признак попыток эксплуатации)
docker logs <container_id> 2>&1 | grep -E "ReactServerComponentsError|Digest|resolved_model"
# Процессы внутри контейнера
docker exec <container_id> ps aux | grep -E "sh|bash|nc|curl|wget"
# Анализ памяти контейнера
docker exec <container_id> strings /proc/self/mem | grep "execSync"
#DFIR #BlueTeam #React2Shell #CVE202555182 #Forensics #IncidentResponse