利用rand(),group by,count()的SQL报错注入

我们来看下sqlmap利用这种方法注入的payload
gid=1 AND (SELECT 1842 FROM(SELECT COUNT(*),CONCAT(0x7162627671,(SELECT (ELT(1842=1842,1))),0x7176626a71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)

函数解释

SELECT (ELT(1842=1842,1))
我们看下官方文档上的解释

ELT(N,str1,str2,str3,…)
ELT() returns the Nth element of the list of strings: str1 if N = 1, str2 if N = 2, and so on. Returns NULL if N is less than 1 or greater than the number of arguments. ELT() is the complement of FIELD().

在MySQL中 1824=1824 会返回1 所以语句就是 SELECT (ELT(1,1)) 结果就是 1
FLOOR(RAND(0)*2)
先看下官方文档

RAND([N])
Returns a random floating-point value v in the range 0 <= v < 1.0.
If an integer argument N is specified, it is used as the seed value:

那么RAND(0)就是生成以0为种子的随机数,这样就意味着每次RAND(0)出来的值都是相同的。
mark

FLOOR(X)
Returns the largest integer value not greater than X.

那么 FLOOR(RAND(0)*2) 的结果显而易见,永远都是下面这种顺序
mark
CONCAT(0x7162627671,(SELECT (ELT(1842=1842,1))),0x7176626a71,FLOOR(RAND(0)*2)

CONCAT(str1,str2,…)
Returns the string that results from concatenating the arguments.
COUNT(expr)
Returns a count of the number of non-NULL values of expr in the rows retrieved by a SELECT statement. The result is a BIGINT value.

COUNT(*)

COUNT(*) is somewhat different in that it returns a count of the number of rows retrieved, whether or not they contain NULL values.
COUNT(*)有点不同,它返回的是检索的行数,不管它们是否包含NULL值。

除此之外之外还用到了派生表

派生表介绍

派生表是从SELECT语句返回的虚拟表。派生表类似于临时表,但是在SELECT语句中使用派生表比临时表简单得多,因为它不需要创建临时表的步骤。派生表和子查询通常可互换使用。当SELECT语句的FROM子句中使用独立子查询时,我们将其称为派生表。

派生表的结构

mark

派生表必须具有别名,以便稍后在查询中引用其名称。如果派生表没有别名就会报错

报错原因

我们可以从官方文档中找到这样一句话

Use of a column with RAND() values in an ORDER BY or GROUP BY clause may yield unexpected results because for either clause a RAND() expression can be evaluated multiple times for the same row, each time returning a different result.

啥意思呢,就是在存在 ORDER BYGROUP BY 的子句中使用 RAND() ,RAND()的值会被计算多次,并且每次返回不一样的值。实际上报错就是因为多次计算导致的。我们来看具体的过程。
1.先建立一张派生表。
2.然后select第一条数据,执行了一次floor(rand(0)*2),结果为0,查询派生表发现不存在键值0,再进行一次计算,结果为1,将1作为主键插入派生表。我们看下这时执行payload的结果(payload稍微修改了一下,可以更直观的显示,并且此时表里只有一条数据正好模拟第一次查询)
mark
3.再查询第二条数据,执行一次floor(rand(0)*2),结果为1,查询派生表发现存在键值1,直接count(*)+1,结束查询。
mark
4.再查询第三条记录,执行一次floor(rand(0)*2),结果为0,派生表里没有为0的键值,再计算一次,结果为1,将1作为主键插入派生表。那么问题来了,主键必须唯一,所以这一次1无法插入派生表,同时还会报错。
mark
几个注意点

1.floor(rand(0)*2)的值是可以预测的,上面已经提过了。
2.整个查询过程floor(rand(0)*2)被计算了5次,查询原数据表3次。
3.rand(),group by,count(),这三个条件缺一不可,否则就无法报错

总结一下

利用条件

要使用这种报错一定要满足下面的条件

1.rand(),group by,count(),这三个条件缺一不可
2.查询的数据表中有至少三条数据

那么不固定种子呢

在这里我们可以提出一个新问题,我不选择0作为种子,或者干脆不要种子,结果会怎么样呢?
从上面的过程我们可以知道这种报错注入利用的是主键的唯一性。所以一定要制造一个冲突,而 floor(rand(0)*2) 有着可以预测的顺序使报错成功触发。如果直接使用 floor(rand()*2) ,我们就不能保证在前几次的查询中不让派生表同时存在主键 0、1,一旦同时存在那就无法触发报错了。但是不固定为0能在一定几率上使需要的数据条数减少为2,这时要满足这样一个条件 floor(rand()*2) 第二次计算的值==第四次计算的值!=第三次计算的值。