服务器端模板注入(SSTI)漏洞与防护
字数 2469 2025-11-08 10:03:34
服务器端模板注入(SSTI)漏洞与防护
描述
服务器端模板注入(SSTI)是一种发生在Web应用服务器端的安全漏洞。当应用在渲染用户输入时,未对用户输入进行严格过滤,直接将包含恶意模板指令的用户输入拼接到模板中并执行时,就会产生SSTI漏洞。攻击者可以利用此漏洞在服务器上执行任意代码,读取敏感文件,甚至完全控制服务器。常见的模板引擎有Jinja2(Python)、Freemarker/Thymeleaf(Java)、Twig(PHP)、Smarty(PHP)等。
解题过程
-
理解模板引擎的工作原理
- 目标:模板引擎旨在将动态数据(如用户名、文章内容)嵌入到静态的页面结构(HTML模板)中。它通过特殊的语法(如
{{ 变量 }}、{% 逻辑控制 %})来标记动态内容的位置和控制逻辑流。 - 正常流程:
- 开发者编写一个模板文件(如
welcome.html),其中包含模板语法:<h1>Welcome, {{ username }}!</h1>。 - 后端代码(如一个Python Flask视图函数)接收到用户请求,获取数据(如从数据库读取
username = "Alice")。 - 后端代码调用模板引擎的渲染函数,将模板文件和数据(上下文)传入:
render_template("welcome.html", username=username)。 - 模板引擎执行渲染,将
{{ username }}替换为具体的值"Alice",生成最终的HTML:<h1>Welcome, Alice!</h1>。 - 生成的HTML被发送给用户浏览器。
- 开发者编写一个模板文件(如
- 关键点:模板引擎需要“解析”和“执行”模板中的特殊语法。如果用户能够控制这些被“执行”的内容,漏洞就产生了。
- 目标:模板引擎旨在将动态数据(如用户名、文章内容)嵌入到静态的页面结构(HTML模板)中。它通过特殊的语法(如
-
识别SSTI漏洞点
- 目标:找到应用中哪些地方将用户输入直接用于模板渲染。
- 漏洞成因:当开发者错误地将用户输入直接拼接进模板字符串,而不是作为数据传递给模板时。
- 错误示例(Python Flask/Jinja2):
# 危险!直接将用户输入作为模板的一部分进行渲染 from flask import Flask, request app = Flask(__name__) @app.route('/greet') def greet(): name = request.args.get('name', 'Guest') # 错误:使用字符串格式化或拼接来“构造”模板内容 template = f"<h1>Hello, {name}!</h1>" # 或 "<h1>Hello, " + name + "!</h1>" return render_template_string(template) # 直接渲染字符串模板 - 正确示例:
# 安全:将用户输入作为数据传递给预定义的模板 @app.route('/greet_safe') def greet_safe(): name = request.args.get('name', 'Guest') # 正确:使用单独的模板文件,用户输入是上下文中的一个变量 return render_template("greet.html", username=name) # 在 greet.html 中: <h1>Hello, {{ username }}!</h1>
- 错误示例(Python Flask/Jinja2):
- 探测方法:在可能存在用户输入渲染的地方(如搜索框、个人信息页),尝试输入简单的模板表达式。
- 输入:
{{ 7*7 }} - 观察:如果页面返回内容中包含了
49,而不是原始的{{ 7*7 }},则极有可能存在SSTI漏洞。因为模板引擎执行了乘法运算。
- 输入:
-
利用SSTI漏洞
- 目标:在确认存在SSTI后,根据模板引擎的类型,构造恶意载荷来执行系统命令或读取文件。
- 步骤:
- 识别模板引擎类型:不同引擎的语法和内置对象/函数不同。通过输入一些探测载荷来观察错误信息或行为差异。
- 输入
{{ 7*'7' }}:Jinja2会返回7777777,Twig会返回49。 - 输入
${7*7}:可能是Freemarker/Velocity。 - 输入
<%= 7*7 %>:可能是ERB(Ruby)/JSP。
- 输入
- 寻找可用的类和方法:模板引擎通常提供一个上下文环境,其中包含一些内置对象(如Python中的
__builtins__、os模块,Java中的Runtime类)。攻击者的目标是调用这些对象的方法。 - 构造恶意载荷(以Jinja2为例):
- 读取敏感文件:尝试访问文件系统。
{{ ''.__class__.__mro__[1].__subclasses__() }}:这条载荷的目的是列出所有Python内建类。''是一个字符串对象,它的__class__是str类,__mro__(方法解析顺序)会显示其继承链(如str,object),[1]通常是基类object,__subclasses__()会返回所有继承自object的类。从输出的大量类列表中,需要找到一个可以用于执行命令或读文件的类(如<class 'os._wrap_close'>、<class 'subprocess.Popen'>),并记下其在列表中的索引。
- 执行系统命令:
- 假设从上面的列表中找到
<class 'subprocess.Popen'>在第400个位置。 - 载荷:
{{ ''.__class__.__mro__[1].__subclasses__()[400]('whoami', stdout=-1).communicate() }} - 解释:通过索引400获取到
Popen类,然后实例化它,传入命令whoami,最后调用communicate()方法执行命令并获取输出。
- 假设从上面的列表中找到
- 读取敏感文件:尝试访问文件系统。
- 识别模板引擎类型:不同引擎的语法和内置对象/函数不同。通过输入一些探测载荷来观察错误信息或行为差异。
-
防护SSTI漏洞
- 目标:从根本上杜绝用户输入被当作模板指令执行的可能性。
- 核心原则:严格分离代码(模板语法)和数据(用户输入)。
- 具体措施:
- 避免动态模板渲染:绝对不要使用
render_template_string这类函数直接渲染包含用户输入的字符串。始终坚持使用预定义的、静态的模板文件(如.html、.j2文件),并将用户输入作为数据(变量)传递给模板引擎。 - 对用户输入进行严格的过滤和转义:如果业务场景确实需要一定的动态性(应尽量避免),必须对用户输入进行白名单过滤,只允许安全的字符。对于所有输出到页面的数据,确保模板引擎的自动转义功能是开启的(现代模板引擎通常默认开启),这样
<,>等字符会被转义成<,>,从而防止其被当作HTML或脚本执行。但注意,自动转义是针对HTML上下文的,对于模板注入本身,仅仅转义是不够的,因为攻击载荷(如{{...}})本身是合法的模板语法。 - 在沙箱环境中运行模板引擎:某些模板引擎支持沙箱模式,它会限制可访问的类和函数,从而降低漏洞利用的成功率。但这并非绝对安全,有经验的攻击者可能找到沙箱逃逸的方法。
- 代码审计和安全测试:在开发过程中,定期进行代码审查,特别关注所有模板渲染相关的代码。在测试阶段,使用SAST/DAST工具并手动进行SSTI漏洞测试。
- 避免动态模板渲染:绝对不要使用