
Secure-by-default 데모 랩으로, 컨테이너 하드닝(distroless 이미지, non-root, 읽기 전용 파일시스템, 런타임 주입 시크릿)이 치명적인 Next.js/React Server Actions RCE(CVE-2025-55182 “React2Shell”)를 무력화할 수 있음을 보여주며, 안전한 배포와 안전하지 않은 배포 및 익스플로잇 로그를 나란히 비교합니다.
이 프로젝트는 Next.js 애플리케이션(특히 Server Actions)의 심각한 원격 코드 실행(RCE) 취약점과 코드 취약점이 남아 있더라도 인프라 강화가 어떻게 공격을 효과적으로 무력화하는지 보여줍니다.
표준 "안전하지 않은" 배포와 Distroless 이미지 및 읽기 전용 파일 시스템을 사용한 강화된 "안전한" 배포를 대조합니다.
소프트웨어 취약점은 피할 수 없습니다. 코드가 실패할 때, 인프라는 공격자가 발판을 넓히는 것을 막아야 합니다.
Next.js에서 사용하는 React Server Components(RSC) 구현에 **심각한 RCE(CVE-2025-55182, 일명 React2Shell)**가 존재합니다.
spawnSync 등)이 가능합니다.curl, wget, ls, cat)를 사용하여 비밀을 탈취하거나 악성코드를 다운로드합니다.
child_process.spawnSync()를 사용합니다. 이는 셸(/bin/sh) 없이 직접 바이너리를 실행합니다.chmod +x) 실행합니다.다음 로그는 애플리케이션 관점에서 공격 시도가 어떻게 보이는지 보여줍니다. 이 대비는 보안 조치의 효과를 생생하게 강조합니다.
logs/server.safe.log)로그는 반복적인 실패(ENOENT)를 보여줍니다.
spawnSync가 ls, id, curl을 실행하려고 시도합니다. Distroless 이미지에는 이러한 바이너리가 없습니다. 단순히 셸이 없는 것이 아니라 도구 자체가 없습니다.[Instrumentation] Logging initialized. Writing to: /app/logs/server.safe.log
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"1. Write Initial Chunk to /tmp/hello_test","success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"2. Verify Binary was Written","verification":{...},"success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"4. Execute Binary","stdout":"","stderr":"","error_obj":{"message":"spawnSync /tmp/hello_test EACCES","code":"EACCES"},"success":true}`'
logs/server.unsafe.log)로그는 명령 실행 성공과 파일 시스템 조작을 확인합니다.
[Instrumentation] Logging initialized. Writing to: /app/logs/server.unsafe.log
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"1. Write Initial Chunk to /tmp/hello_test","success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"4. Execute Binary","stdout":"Hello from Go binary!\\n","stderr":"","error_obj":null,"success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"command":"id","args":[],"stdout":"uid=0(root) gid=0(root) ...","stderr":"","status":0,"signal":null}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"command":"cat","args":["/app/.env"],"stdout":"","stderr":"cat: can\'t open \'/app/.env\': No such file or directory\n","status":1,"signal":null}`'
(참고: 안전하지 않은 로그에서 cat /app/.env가 위에서 실패하는 이유는 파일 이름이 루트에 .env이기 때문입니다. 전체 로그의 ls -la는 디렉터리 구조를 드러냅니다.)
표준 셸 명령 실행 시도.
id, ls, cat .env를 실행하고 민감한 데이터에 접근할 수 있습니다.spawnSync /bin/sh ENOENT. 명령을 실행할 셸이 없습니다."도구 부족"을 우회하기 위해 사용자 정의 바이너리 업로드 시도.
/tmp/malware에 씁니다.chmod +x를 실행합니다.EROFS: read-only file system.공격자가 바이너리를 변수에 로드하여 메모리에서 직접 실행할 수 있을까요?
global.payload = "...")에 연결한 후 실행합니다.child_process 함수(spawn, exec)는 파일 경로가 필요합니다. 버퍼나 문자열을 직접 실행할 수 없습니다.memfd_create(RAM에 익명 파일을 생성하는 syscall)가 필요합니다.memfd_create를 노출하지 않습니다. 접근하려면 C++ 애드온(예: ffi-napi)이 node_modules에 미리 설치되어 있어야 합니다.gcc, make)가 없으므로 공격자는 이 애드온을 즉석에서 빌드할 수 없습니다."Distroless" 이미지에는 오직 애플리케이션과 그 런타임 종속성만 포함됩니다. 패키지 관리자, 셸, 표준 UNIX 도구가 없습니다.
ls), 파일을 다운로드할 수 없으며(curl), 권한을 쉽게 상승시킬 수 없습니다.컨테이너 런타임이 루트 파일 시스템을 읽기 전용으로 마운트하도록 구성합니다.
docker-compose.yml에서:
read_only: true
tmpfs:
- /tmp:noexec # 중요: 실행을 명시적으로 차단!
/tmp에 바이너리를 쓸 수 있지만(쓰기 성공), noexec 플래그로 인해 실행은 EACCES(권한 거부)로 실패함을 보여줍니다. 이는 기능(쓰기 가능한 tmp)과 보안 사이의 균형을 유지합니다.컨테이너 이미지에 .env 파일을 절대 포함하지 마십시오. 공격자가 파일을 읽을 수 있으면(예: cat .env) 비밀이 손상됩니다.
environment 키).환경 시작:
안전한 앱과 안전하지 않은 앱이 모두 단일 docker-compose.yml 파일에 정의되어 있습니다.
docker compose up --build -d
익스플로잇 실행: 특정 포트에 대해 익스플로잇을 실행하여 차이를 확인할 수 있습니다.
안전하지 않은 앱 대상 (포트 3001):
# 1. 표준 RCE (LotL) - 성공
python exploit/poc.py http://localhost:3001
# 2. 고급 공격 (BYOL) - 성공
python exploit/poc_advanced.py http://localhost:3001
안전한 앱 대상 (포트 3000):
# 1. 표준 RCE (LotL) - 실패 (ENOENT)
python exploit/poc.py http://localhost:3000
# 2. 고급 공격 (BYOL) - 실패 (EACCES/EROFS)
python exploit/poc_advanced.py http://localhost:3000
정리:
docker compose down
| 기능 | ❌ 안전하지 않은 환경 (포트 3001) | ✅ 안전한 환경 (포트 3000) |
|---|
| 베이스 이미지 | node:20-alpine (ls, curl, wget 등 포함) | gcr.io/distroless/nodejs20-debian12 (셸, 도구 없음) |
| 파일 시스템 | 쓰기 가능 (기본 Docker 기본값) | 읽기 전용 (read_only: true) |
| 비밀 | 디스크의 .env 파일 (cat .env에 취약) | 환경 변수 (런타임에 주입) |
| 사용자 | root (기본값) | 비-root (Distroless가 강제) |