在本节中,我们将讨论什么是服务器端模板注入,并概述利用服务器端模板注入漏洞的基本方法。 我们还将建议一些方法,以确保您自己对模板的使用不会使您遭受服务器端模板的注入。
如果您已经熟悉服务器端模板注入的核心概念,并且想直接进入我们的交互式实验室,请按照下面的链接进行测试。
该技术最早由PortSwigger在我们2015年关于该主题的研究论文中进行了记录。 如果您对我们如何利用实时网站上的某些漏洞感兴趣,请在我们的研究页面上找到完整的文章。
服务器端模板注入是指攻击者能够使用本机模板语法将恶意有效负载注入到模板中,然后在服务器端执行该模板。
模板引擎旨在通过将固定模板与易失性数据结合来生成网页。 当用户输入直接连接到模板而不是作为数据传递时,可能会发生服务器端模板注入攻击。 这使攻击者可以注入任意模板指令以操纵模板引擎,从而经常使攻击者能够完全控制服务器。 顾名思义,服务器端模板注入有效负载是在服务器端交付和评估的,这可能使它们比典型的客户端模板注入更加危险。
在严重的情况下,攻击者可能会实现远程代码执行,完全控制后端服务器,并使用它对内部基础结构执行其他攻击。
即使在无法完全执行远程代码的情况下,攻击者仍然经常可以使用服务器端模板注入作为其他众多攻击的基础,从而有可能获得对服务器上敏感数据和任意文件的读取权限。
当用户输入连接到模板中而不是作为数据传递时,服务器端模板注入漏洞就会出现。
仅提供占位符并在其中呈现动态内容的静态模板通常不容易受到服务器端模板注入的攻击。 经典示例是一封电子邮件,该电子邮件以每个用户的名字打招呼,例如Twig模板中的以下摘录:
$output = $twig->render("Dear {first_name},", array("first_name" => $user.first_name) );
这不易受到服务器端模板注入的影响,因为用户的名字仅作为数据传递到模板中。
但是,由于模板只是字符串,因此web开发人员有时会在呈现之前将用户输入直接连接到模板中。 让我们以与上述示例类似的示例为例,但是这次,用户可以在发送电子邮件之前自定义部分电子邮件。 例如,他们也许可以选择使用的名称:
$output = $twig->render("Dear " . $_GET['name']);
在此示例中,不是将静态值传递到模板中,而是使用GET参数名称动态生成模板本身的一部分。 在服务器端评估模板语法时,这可能使攻击者可以将服务器端模板注入有效负载放置在name参数内,如下所示:
http://vulnerable-website.com/?name={{bad-stuff-here}}
诸如此类的漏洞有时是由于不熟悉安全隐患的人对模板的不良设计而导致的。 就像上面的示例一样,您可能会看到不同的组件,其中一些包含用户输入,这些用户输入已串联并嵌入到模板中。 在某些方面,这类似于编写不当的准备好的语句中发生的SQL注入漏洞。
但是,有时此行为实际上是有意实施的。 例如,某些网站故意允许某些特权用户(例如内容编辑器)通过设计来编辑或提交自定义模板。 如果攻击者能够使用这种特权来破坏帐户,则显然会带来巨大的安全风险。
通常不会注意到服务器端模板注入漏洞,这不是因为它们很复杂,而是因为它们只对明确寻找它们的审计人员真正明显。 如果您能够检测到存在漏洞,则利用它可能非常容易。 在非沙盒环境中尤其如此。
与任何漏洞一样,利用漏洞的第一步就是能够找到它。 也许最简单的初始方法是通过注入模板表达式中常用的特殊字符序列来使模板模糊,例如$ {{<%[%'“}}%\。如果引发异常,则表明 服务器可能会以某种方式解释注入的模板语法,这表明服务器端模板注入可能存在漏洞。
服务器端模板注入漏洞发生在两个不同的上下文中,每个上下文都需要使用自己的检测方法。 无论您进行模糊测试的结果如何,都必须尝试以下特定于上下文的方法,这一点很重要。 如果模糊测试尚无定论,则漏洞可能仍会使用这些方法之一显示出来。 即使模糊测试确实暗示了模板注入漏洞,您仍然需要确定其上下文才能加以利用。
大多数模板语言都允许您直接使用HTML标签或使用模板的本机语法自由输入内容,这些模板将在发送HTTP响应之前在后端呈现为HTML。 例如,在Freemarker中,render('Hello'+ username)行将呈现为Hello Carlos之类的东西。
有时可以将其用于XSS,实际上常常被误认为是简单的XSS漏洞。 但是,通过将数学运算设置为参数的值,我们可以测试这是否也是服务器端模板注入攻击的潜在入口点。
例如,考虑一个包含以下易受攻击的代码的模板:
render('Hello ' + username)
在审核期间,我们可能会通过请求URL来测试服务器端模板的注入,例如:
http://vulnerable-website.com/?username=${7*7}
如果结果输出包含Hello 49,则表明该数学运算正在服务器端进行评估。 这是服务器端模板注入漏洞的良好概念证明。
请注意,成功评估数学运算所需的特定语法将根据所使用的模板引擎而有所不同。 我们将在“识别”步骤中对此进行更详细的讨论。
在其他情况下,该漏洞是通过将用户输入放置在模板表达式中来暴露的,就像我们之前在电子邮件示例中看到的那样。 这可以采用用户可控制的变量名称的形式,该变量名称放置在参数中,例如:
greeting = getQueryParameter('greeting')
engine.render("Hello {{"+greeting+"}}", data)
在网站上,产生的URL将类似于:
http://vulnerable-website.com/?greeting=data.username
例如,这将在输出到Hello Carlos中呈现。
在评估期间很容易错过此上下文,因为它不会导致明显的XSS,并且与简单的哈希映射查找几乎没有区别。 在这种情况下,测试服务器端模板注入的一种方法是,通过将任意HTML注入到值中,首先确定该参数不包含直接XSS漏洞:
http://vulnerable-website.com/?greeting=data.username<tag>
在没有XSS的情况下,这通常会导致输出中出现空白条目(只是Hello,没有用户名),编码标签或错误消息。 下一步是尝试使用通用模板语法突破该语句,并尝试在其后注入任意HTML:
http://vulnerable-website.com/?greeting=data.username}}<tag>
如果这再次导致错误或空白输出,则说明您使用了错误的模板语言提供的语法,或者,如果没有模板样式的语法似乎有效,则无法进行服务器端模板注入。 或者,如果输出和任意HTML一起正确呈现,则表明存在服务器端模板注入漏洞:
Hello Carlos<tag>
一旦检测到模板注入潜力,下一步就是确定模板引擎。
尽管有大量的模板语言,但其中许多模板使用非常相似的语法,这些语法是为避免与HTML字符冲突而专门选择的。 结果,创建探测有效载荷以测试正在使用哪个模板引擎可能相对简单。
通常只需提交无效的语法就足够了,因为产生的错误消息将准确告诉您模板引擎是什么,有时甚至是哪个版本。 例如,无效的表达式<%= foobar%>会触发来自基于Ruby的ERB引擎的以下响应:
(erb):1:in `<main>': undefined local variable or method `foobar' for main:Object (NameError)
from /usr/lib/ruby/2.5.0/erb.rb:876:in `eval'
from /usr/lib/ruby/2.5.0/erb.rb:876:in `result'
from -e:4:in `<main>'
否则,您将需要手动测试特定于语言的不同有效负载,并研究模板引擎如何解释它们。 使用基于哪个语法似乎有效或无效的消除方法,可以比您想象的更快地缩小选项的范围。 常用的方法是使用来自不同模板引擎的语法注入任意数学运算。 然后,您可以观察它们是否被成功评估。 为了帮助完成此过程,您可以使用类似于以下内容的决策树:
您应该意识到,相同的有效负载有时可能会以一种以上的模板语言返回成功的响应。 例如,有效载荷{{7 *'7'}}在Twig中返回49,在Jinja2中返回7777777。 因此,重要的是不要单凭成功的回答就得出结论。
在检测到存在潜在漏洞并成功识别模板引擎之后,您可以开始尝试找到利用它的方法。
防止服务器端模板注入的最佳方法是不允许任何用户修改或提交新模板。 但是,由于业务需求,有时这是不可避免的。
避免引入服务器端模板注入漏洞的最简单方法之一是,除非绝对必要,否则始终使用“无逻辑”模板引擎,例如Mustache。 尽可能将逻辑与表示分离开来,可以大大减少您遭受最危险的基于模板的攻击的风险。
另一措施是仅在已完全删除潜在危险模块和功能的沙盒环境中执行用户的代码。 不幸的是,沙盒化不受信任的代码本来就很困难并且容易被绕过。
最后,另一种补充方法是接受几乎不可避免的任意代码执行,并通过在例如锁定的Docker容器中部署模板环境来应用自己的沙盒。