0x00 TL; DR
本文将通过分析今年几道通过手写Pickle opcode实现bypass的题目总结手撕Pickle的一些tips
以及通过遍历Python AST自动化生成Pickle opcode
repo地址https://github.com/eddieivan01/pker
0x01 Pickle简述
网上资料已经很多了,我就不再向互联网填充冗余信息了(可学习文末链接)
只总结Pickle的几个特点:
- 非图灵完备的栈语言,没有运算、循环、条件分支等结构
- 可以实现的操作
- 构造Python内置基础类型(
str
,int
,float
,list
,tuple
,dict
) dict
和list
成员的赋值(无法直接取值)- 对象成员的赋值(无法直接取值)
callable
对象的调用- 通过
_Pickler.find_class
导入模块中的某对象,find_class
的第一个参数可以是模块或包,本质是getattr(__import__(module), name)
- 版本保持向下兼容,通过opcode头解析版本
- 0号protocol使用
\n
作操作数的分割
- 构造Python内置基础类型(
0x02 Opcode简述
Pickle常见opcode,完整的可在$PYTHON/Lib/pickle.py
查看
name | 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 |
对应的实现都可以在pickle._Unpickler
的load_*
成员函数中查看,选取两个常见的:
pop_mark
为将MARK(
上的所有元素弹出为一个list,然后push回栈。所以需要这样构造(I0\nI1\nl
读取到INST指令后,往后读两个操作数,调用find_class
,然后弹出栈上MARK以上的参数,调用callable对象实例化,所以这样构造(S'ls'\nios\nsystem\n.
0x03 官方的Demo限制了什么
官方给出的安全反序列化是继承了pickle.Pickler
类,并重载了find_class
方法
父类原本的操作是把module导入sys.module
缓存中(并未导入全局或局部作用域),然后getattr取值,所以重载该方法后即可对module和name进行限制
哪些操作符会调用find_class
?
GLOBAL:c
INST :i
点击收藏 | 3
关注 | 3