有研究人员在安全测试时,绕过了 Cloudflare WAF 的 SQLi 过滤器,这意味着Cloudflare 安装没有正确配置,但Cloudflare目前还不认为这是个漏洞。虽如此,安全人员在为其应用程序部署安全保护时需要注意有关问题。对于试图绕过 WAF 的人来说,发现的些许漏洞也可能会派上用场。攻击者可以利用一种或多种技术(具体取决于最终应用程序)绕过某些 WAF 并通过滥用 SQLi 漏洞窃取数据。
测试时使用的Web应用程序详细信息
测试时使用的Web 应用程序的目的对于实际测试来说并不重要。除了在服务器上公开的 /graphql 端点和运行查询的 PostgreSQL 服务之外,写入查询的堆栈也并不重要。 GraphQL 是一种查询语言,它处理查询,在测试中,在后端运行适当的 SQL 查询并返回请求的数据。之前由于测试人员从未使用或阅读过有关 GraphQL 的信息,因此遵循的是官方教程https://graphql.org/learn/,如果感兴趣你也可以了解一下。
查询的形成类似于 JSON 对象。应用程序向 /graphql 端点发送 POST 请求。请求正文包含 GraphQL 查询,如下所示:
X-MARK 中未过滤的参数从 GraphQL 传递,并在 PostgreSQL 存储过程中被替换,从而允许我们注入在后端服务器上执行的 SQL 查询。
这几乎是我们目前必须了解的关于应用程序的内容。在接下来的部分中,我将介绍我在测试 SQL 注入应用程序时观察的一些现象,这些观察使我成功地利用了SQL注入,包括绕过Cloudflare SQLi过滤器。
观察 1
第一个重要的观察结果是服务器对有效的电子邮件输入(即当 SQL 查询返回数据时)的响应与对无效的电子邮件输入的响应不同。返回的 HTTP 代码始终为 200,但响应正文不同:
这将返回“OK”:
这将返回"NOT OK":
起初这可能看起来不太像,但它实际上是允许我验证数据库中特定数据是否存在的机制。它让我想到了利用这种机制执行SQL盲注入攻击并使用脚本自动化它的想法。该脚本构造一个有效负载,并将其与POST请求一起发送,POST请求反过来修改在后端服务器上运行的SQL查询。
步骤如下:
- 我们有一组字符,称为字母表。这组字符包括所有可能的字符,这些字符可以是我们试图从数据库中提取的数据的一部分。
- 假设资源是需要检索的 SQL 资源。它可以是数据库名称、用户名、任何表格中的单元格值等。
- 将逐字符检索资源。我们试图检索的字符称为 char。
- 我们遍历字母表,并将每个字母与 char 进行比较。
- 如果有匹配,我们记录结果并移动到下一个字符。
- 最后,我们通过连接所有字符获得了完整的资源。
观察 2
第二个重要的观察结果是,在执行的SQL查询出现错误时,会显示错误消息,这对我来说容易得多。这个错误信息不允许我执行一个基于错误的SQL注入,而是显示了由PostgreSQL在SQL服务器上执行的完整SQL查询。这一点为什么如此重要,我们很快就会明白。
从错误消息中提取的SQL查询看起来像这样:
作为“email”变量提交的用户输入反映在X-MARK上。这就是为什么拥有完整的SQL查询对于开发过程很重要的两个原因:
- 可以看到用户输入被括在括号中,这条信息使有效负载的生成过程变得更加容易,因为人们知道输入必须包含与左括号匹配的右括号,以便结束 SQL 查询有效。
- 用户输入反映在查询中的多个位置(第 5、10、11 行),给我带来麻烦的是第 5 行。如果是 X-MARK 仅反映在 WHERE 子句中的情况,事情会更容易。但在这种情况下,我必须确保我的输入不会弄乱表 JOIN。这是必要的,以确保生成和查询正确的表行,以便我可以获得所需的数据。
注意:第 8 行的 $1 符号是 SQL 准备查询的位置参数,并由 HTTP 请求的“名称”变量参数替换。但它不容易受到 SQL 注入的影响。
第一次尝试
我首先尝试查找当前数据库的名称。在 PostgreSQL 中执行此操作的 SQL 查询是:
有很多事情需要考虑,而且从一开始就很疯狂,我必须从一开始就想出绕过 Cloudflare 的方法。
WAF 绕过的第一种办法
首先,我必须首先将 current_database() 的第一个字符与字符“a”进行比较。PostgreSQL的方法是:
Cloudflare会阻塞'substr'函数,所以诀窍是要么使用'left',要么使用'right'函数。我使用'right'函数,因为'left'给了我一些麻烦,当我试图找出我已经找到所有的数据库名称的字符。新的查询(将“a”与最后一个资源的字符进行比较,如下所示:
并且未被 WAF 检测到。
注意:函数 right(current_database(), N) 返回数据库名称最右边的 N 个字符。因此,当找到最后一个字符时,例如X,下一次调用该函数应该是:
由于我们已经知道我们必须关闭查询中的左括号(来自观察2),POST 请求的正文如下所示(此处仅显示'email'变量):
但是,记住后端 SQL 查询如何包含 JOIN 子句(来自观察2),我还在查询中添加了一些额外的内容,以确保 SQL 连接在后台正确执行。 POST 请求的正文如下所示:
服务器上的后续 SQL 查询如下所示:
这很复杂,但想法是一样的:如果“a”是数据库名称的最右边的字符,我们将从服务器获得一个“OK”响应。
无论如何,在向服务器提交这个请求后,我看到了Cloudflare WAF(配置错误)的可怕页面,告诉我我的请求被阻止了。
第二次尝试
在我再次尝试之前,我必须了解 Cloudflare 对我的查询有什么帮助。
经过反复测试,我发现问题出在生成的服务器 SQL 查询中的 FROM 子句中的空格。这导致我进入第二个 WAF 绕过。
WAF 绕过的第二种办法
此处使用的第二种 WAF 绕过技术消除了 SQL 查询中的空格,并将 SQL FROM 子句的部分括在括号中。
变成了:
因此 POST 请求的结果正文变为:
在服务器上的后续SQL查询是这样的:
这样,整个过程就可以实现自动化了,以找到数据库名称的整个值。相同的过程还检索了用户名(通过使用 user 函数)和数据库版本(通过使用 version() 函数)。但是存储在数据库表中的数据呢?检索这些数据的通用查询,以及我在上一篇文章中使用的绕过方法的查询都不起作用。两者都被阻止:
为什么我的查询被阻止了?问题是紧跟在 SELECT 子句之后的 FROM 子句。以下查询将很好地通过(错误配置的)Cloudflare WAF SQLi 过滤器:
一旦在查询结束时引入 WHERE 子句,WAF 就会启动并阻止请求。我的最终目标是从任何表中检索数据。是时候深入挖掘兔子洞了。
第三次尝试
我在这里给出 SQLi 的早期失败尝试,只是因为我希望这篇文章向人们展示在渗透测试期间思维过程是如何展开的。
在我尝试使事情复杂化(即脱离所有 JOIN 和 FROM 子句)时,我使用了一个简单的分号和注释技巧 (;--)。计划是首先检索数据库名称,然后在此基础上检索表中的数据:
服务器上生成的 SQL 查询如下:
无论如何,这当然行不通,原因有两个:
- 第5行之后的所有内容都会因为注释而被忽略,这不一定是限制性的,但我宁愿在 FROM 子句中执行我的 SQLi。
- 我收到以下错误:“绑定消息提供 1 个参数,但准备好的语句需要 0”。这是因为 name 变量被传递给准备好的语句,但第 8 行被忽略了,因此新的准备好的语句不需要该变量。
第四次尝试
我在这里给出了从表中检索数据的另一个早期尝试,它使我更接近我的目标。在此之前我所知道的是,以下负载将被WAF 阻止:
注意:我添加了 LIMIT 和 OFFSET 关键字,以便从 table1 中仅检索一行。 LIMIT 表示我们只想要检索一行,OFFSET 表示在开始检索数据之前我们想要跳过多少行。在这种情况下,OFFSET 0 表示数据库应该跳过 0 行并返回 table1 中的第一行。这对于逐一检索表的所有行很有用。
WAF 绕过的第三种办法
回顾从数据库服务器产生的错误中检索到的 SQL 查询,我注意到可能不需要使用 FROM 子句。 table1 表在使用 AS 关键字的查询中别名为 t1,并且可以基于 t1 引用它的任何列。这样就可以这样查询 table1 的 column1 列:
这可以很好地通过(错误配置的)Cloudflare WAF,因此 POST 请求正文中的有效负载可以这样转换:
服务器上的后续 SQL 查询如下所示:
这可以正常工作,但限制是只能提取 table1(或 table2)中的数据,因为这些是服务器 SQL 查询中唯一的别名表,继续进行最后的成功尝试。
最后一次尝试
好吧,如果我想从数据库中检索任何我想要的数据,我不得不放弃WAF 绕过的第三种方法。此时,似乎没有办法避免使用FROM子句。而且,在不被Cloudflare的WAF检测到的情况下,似乎也没有办法成功地将FROM子句隐藏到有效载荷中。似乎我正在寻找的答案不在SQL查询中。我不得不后退一步。
进入 GraphQL
我们已经看到发送的请求的正文是一个 GraphQL 查询,然后它被翻译成一个 SQL 查询。所以我的下一个尝试是改变 GraphQL 查询并设法隐藏其中的 FROM 子句,这将有望转换为在服务器上工作的SQL查询。
如上所述,GraphQL 查询的结构类似于 JSON 对象。 JSON 中的数据以名称/值对存储在字典中,它们都是字符串。 GraphQL 查询需要字符串键,但允许使用任意参数。这些规则适用于 GraphQL:
- 数据用逗号分隔;
- 花括号容纳对象;
- 方括号包含数组;
因此一个GraphQL查询参数可以看起来像以下任何一种方式:
这让我想到:如果我将对象的值作为数组而不是字符串传递,后端服务器上的 SQL 查询会发生什么。简而言之,我想打破 SQL 注入查询并将 FROM 子句移动到不同的对象以欺骗 Cloudflare。
进入
该请求绕过了WAF,我从数据库中得到了一个错误,报错是一个格式不正确的SQL查询,并向我显示了完整的SQL查询结果。在注入点的查询是这样的:
注意 SELECT column1 后面的逗号 (,) 吗?那是我绕过 SQLi 过滤的凭证。将 GraphQL 查询参数的值作为数组传递会在后端 SQL 服务器中转换为字符串。字符串只是由逗号和空格字符分隔的数组项的串联!此时,SQL查询是错误的,但我可以注释掉逗号,并获得一个有效的、绕过waff的请求,该请求从我选择的任何数据库表中检索我想要的任何数据。
这是最终 POST 请求的正文:
以及在 SQL 服务器上生成的有效 SQL 查询:
成功!
为了简化表检索过程,我用Python编写了一个脚本来自动化这个过程。脚本的伪代码如下所示:
这就是我如何利用 GraphQL 制作 SQL 注入,绕过配置错误的 Cloudflare WAF 实例,并能够在后端检索整个数据库。正如我在开头提到的,这种绕过技术的组合不适用于正确配置的 Cloudflare WAF。
缓解措施
缓解数据库上 SQL 注入的最安全方法是准备好的语句。