[🥉 Bronze 3] cat 대신 실행 — DreamHack File Vulnerability Advanced 풀이 — ZINO
2026-07-06·1분 읽기·
[🥉 Bronze 3] cat 대신 실행 — DreamHack File Vulnerability Advanced 풀이
경로검증 없는 /file 엔드포인트로 path traversal, /admin의 API_KEY는 Dockerfile 더미값이라 무의미해 보이지만 /proc/self/environ을 읽으면 런타임 실제 키가 그대로 나온다. 그 키로 연 /admin RCE로 flag를 찾아도 cat은 500만 뜬다 — flag가 ---x--x--x(실행 전용) ELF라 읽지 말고 실행해야 한다.
data = open('./files/' + path).read() # (1) 경로 검증 없음
return data
return 'Error !'
@app.route('/admin', methods=['GET'])
@key_required
def admin():
cmd = request.args.get('cmd', None)
if cmd:
result = subprocess.getoutput(cmd) # (2) API_KEY만 맞으면 명령 실행
return result
return 'Error !'
/file은 사용자 입력 path를 './files/' + path로 그냥 이어 붙여 연다. ../를 걸러내는 코드가 한 줄도 없다 — 전형적인 path traversal이다. /admin은 쿼리로 받은 API_KEY가 서버가 아는 값과 같으면 subprocess.getoutput(cmd)로 사용자가 준 명령을 그대로 실행한다. RCE인데 열쇠(API_KEY)가 필요하다.
Dockerfile을 보면 힌트인 듯 함정인 듯한 줄이 있다.
ENV API_KEY API_KEY
문자 그대로 API_KEY라는 값. 이걸 그대로 믿고 ?API_KEY=API_KEY로 넣어보면 안 통한다 — 실제 배포 서버는 이 자리에 별도의 런타임 값을 넣어 띄운다. 즉 이 파일만 봐서는 진짜 키를 알 수 없다. 그런데 서버에는 이미 그 키를 읽을 수 있는 구멍이 하나 더 있다 — /file의 path traversal이다.
🔬 정찰 — 소스부터 다시, 이번엔 traversal로
굳이 로컬 zip을 안 열어봐도, /file의 취약점 자체로 서버에 떠 있는 소스를 다시 읽을 수 있다. 앱의 작업 디렉토리가 /app이므로 ../main.py면 충분하다.
브라우저로 /file?path=../main.py — 서버에 떠 있는 소스가 그대로 리턴된다
로컬에서 받은 파일과 내용이 같다는 걸 확인했으니, 이제 이 서버가 정말 그 취약점 그대로 살아있다는 게 증명된 셈이다. 다음은 /admin의 열쇠 차례다.
💣 핵심 — Dockerfile의 더미값이 아니라 /proc/self/environ
API_KEY는 프로세스 환경변수다. Linux는 실행 중인 모든 프로세스의 환경변수를 /proc/<pid>/environ으로 노출하고, 자기 자신은 /proc/self/environ으로 가리킨다. /file의 path traversal로 이 파일까지 거슬러 올라갈 수 있다면, Dockerfile을 볼 필요 없이 지금 이 순간 서버가 실제로 갖고 있는 키를 그대로 읽어올 수 있다.
GET /file?path=../../../../proc/self/environ
브라우저로 /proc/self/environ 열람 — 런타임 실제 API_KEY가 그대로 노출된다
API_KEY=d22cb18e86fc9e23996650150461c9f794ad3a4f. Dockerfile의 API_KEY라는 더미와는 전혀 다른, 이 컨테이너만의 실제 값이다. path traversal 하나로 인증 우회에 필요한 비밀을 통째로 얻었다.
키 없이 /admin을 먼저 두드려 보면 이 상태다.
키 없이 /admin?cmd=whoami — Access Denied !
🎯 익스플로잇 — RCE는 얻었는데, cat이 안 먹힌다
유출된 키를 붙여 다시 두드리면 명령이 그대로 실행된다.
GET /admin?cmd=whoami; find / -maxdepth 3 -iname '*flag*'&API_KEY=d22cb18e...
root 권한 확인 + /flag 위치 특정
root로 실행 중이고, /flag가 있다. 그런데 cmd=cat /flag를 넣으면 결과 대신 Flask 500 에러가 뜬다. 이유는 권한을 보면 바로 나온다.
---x--x--x 1 root root 16608 Aug 18 2023 /flag
읽기 권한이 아무에게도 없다 — 실행 권한만 있다.cat은 파일을 열어 바이트를 읽으려 하고, Linux는 root라도 이 파일을 "읽기"로 열 수는 있지만(파일 자체가 실행 전용 ELF 바이너리라 텍스트가 아님) subprocess.getoutput은 결과를 텍스트로 디코딩하려다 ELF의 바이너리 바이트에서 깨진다 — 그래서 500. file /flag로 확인해 보면 정체가 드러난다: 이건 읽을 파일이 아니라 실행할 프로그램이다.
/flag: ELF 64-bit LSB pie executable, x86-64, ... not stripped
cat 대신 그냥 실행하면 된다.
GET /admin?cmd=/flag&API_KEY=d22cb18e...
/flag를 cat이 아니라 직접 실행 — stdout으로 flag가 그대로 출력된다
🚀 Full Exploit
세 단계(소스 확인은 생략) — 키 유출, 위치 확인, 실행 — 를 하나로 묶으면 이렇다.
이 문제는 취약점 하나가 아니라 두 취약점의 연결이 핵심이다. /file의 path traversal 단독으로는 "파일 몇 개 더 읽는" 수준이지만, 그 대상이 /proc/self/environ이 되는 순간 프로세스의 모든 비밀(환경변수로 주입된 API 키, 토큰, DB 비밀번호)이 그대로 넘어온다. 환경변수는 설정 파일보다 안전해 보이지만, 프로세스가 살아있는 한 /proc/<pid>/environ을 통해 누구나(그 프로세스를 읽을 권한이 있다면) 통째로 열람 가능한 값이다.
그리고 마지막 단계는 권한 모델을 다시 생각하게 한다. ---x--x--x로 읽기를 막아도 실행은 열려 있으면, RCE를 쥔 공격자에게는 차이가 없다 — 어차피 실행시켜서 결과를 받아내면 그만이다. 진짜 방어는 두 군데다: 사용자 입력으로 만든 경로는 반드시 정규화(os.path.realpath)해서 허용 디렉토리를 벗어나지 않는지 검사하고, 애초에 임의 명령 실행 엔드포인트를 인증 하나로 열어두지 않는 것.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…