0x01 介绍
前些日子在看googlectf 2019的时候,其中一道web题涉及order by上的注入点,觉得很有趣。第一次思考如何通过一次SQL查询,尽可能得到更多有用的信息。
这里我们先给出一个小的demo: 1
2
3
4
5$db = new mysqli(null, $dbuser, $dbpass, $dbname, null, $socket);
$inject = $db->escape_string($_GET['order']);
$sql = "select * from user order by $inject";
$result = $db->query($sql);
show_fileds($result); // 依次输出每一条数据
很显然,注入点在order by
后面,而order by
后面是不能带union select
,所以这里很显然是一个盲注。并且,我们确保了这里不存在"报错注入"的可能。这里唯一输出是user
表的所有行的内容。每次输出的唯一差异性在于user
每一行的输出顺序。
因此,这里有个一个非常自然的想法: 是否可以通过输出顺序去间接地泄露一些信息呢?
如果按照这个思路去考虑问题。我们首先需要将输出顺序和特定数据对应起来。简单来说,我们需要建一张表: 1
2
3
4
5
6
7
8
9table = [
order0 => '0',
order1 => '1',
...
order9 => '9',
ordera => 'a',
...
orderz => 'z',
]c
, 就需要让输出顺序对应上orderc
。
0x02 利用rand()
如果在order by
后面加上rand()
,我们可以让输出顺序呈现一种随机状态。这是因为,mysql
在这里进行了额外的filesort
。具体来说,在每次从表里取出一行数据的时候,就会执行一下rand()
得到一个随机数。这样每一行数据都对应上了一个数字,最后我们根据这个数字对所以数据行进行重排。
1 | MariaDB [test]> select * from user; |
这个利用方式似乎离我们的想法更近了一些。至少我们可以得到不同的输出顺序了,但是坏处是我们并不能去控制它。幸运地是我们可以通过rand(1)
来固定产生随机数的种子。这样会使得在filesort
阶段产生的随机数序列是稳定的。
1 | MariaDB [test]> select * from user order by rand(1); |
这样输出顺序我们可以通过固定rand()
的种子来控制。这里我们假设需要泄露数据只包含出现在0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
的字符。
如果我们需要泄露的数据为database name,那么我们可以通过构造下面的order by
来泄露它的第1个字符: 1
rand(conv(substr(database(),1,1),36,10))
0x03 一次查询泄露多个字符
通过前面的手法我们可以通过每次查询泄露单个字符。这里单个字符的可能性有36种,因此我们只需要用到36个不一样的输出顺序。但是实际可能的输出顺序可能有n!
种,其中n
表示表中有多少条数据。比如n = 9
时,这里就有362880
种输出顺序。理论上,我们可以通过一次查询泄露362880
种不同数据中的一个。前面的做法,似乎严重浪费了这里的理论性能。
所以我们可以考虑一次泄露多位字符,m
位字符对应了36^m
种可能。那么我们一次最多可以字符串位数为int(log_36 (n!))
。比如n = 9
时,我们可以一次性泄露3位字符(log36(9!)=3.572417978
)。
那么接下来的问题就是,我们如何利用输出顺序来编码这些多位字符。同样地,我们可以利用 1
rand(conv(substr(database(),1,3),36,10))
0x04 自定义encode/decode
考虑一个比较hard的环境:
假设rand()
也无法使用。
为了方便讨论,这里我们假设表里面有9
条数据,即n = 9
。我们用数字表示[0, 9!-1]
表示可能的输出顺序。基于前面讨论,我们考虑一次性泄露3位字符,我们将其对应到[0, 36^3 - 1]
上。
对[0, 9!]
中的任意一个数字c
,我们可以将其表示为R = (r1, r2, r3, r4, r5, r6, r7, r8, r9)
,其中 1
2
3
4
5
6
7
8
9c = d9 * 9 + r9 // r9 in [0, 8]
d9 = d8 * 8 + r8 // r8 in [0, 7]
d8 = d7 * 7 + r7 // r7 in [0, 6]
d7 = d6 * 6 + r6 // r6 in [0, 5]
d6 = d5 * 5 + r5 // r5 in [0, 4]
d5 = d4 * 4 + r4 // r4 in [0, 3]
d4 = d3 * 3 + r3 // r3 in [0, 2]
d3 = d2 * 2 + r2 // r2 in [0, 1]
d2 = 1 * r1 // r1 in [0, 1]R
还原为c
。具体可以看看康托展开。
假设9个条数据分布为data1, data2, ... , data9
。初步想法,我们将data_i
的位置,表示r_i
的大小。举个例子,若输出顺序为: 1
data1, data2, data3, ... , data9
r1 = 0,
, r2 = 1
, r3 = 2
, ... r9 = 8
。但这有一个问题: 如果存在两个r_i == r_j
,输出顺序应该是什么? 幸运地是,在实际操作中我们可以避开这个问题。
对于任意一个3位字符串对应的数字表示@secret
,我们可以通过下述查询将其转换为R
并输出: 1
2
3
4
5
6
7
8
9select *
from user
order by
(select concat(
(select 1 from(select @l:=0x303132333435363738,@d:=9,@b:=@secret)x),
substr(@l,1+mod(@b,@d),1),
@l:=concat(substr(@l,1,mod(@b,@d)),
substr(@l,2+mod(@b,@d))),
@b:=@b div @d,@d:=@d-1));
- 第5行,主要作用是初始化3个变量:
@l
实际为字符串012345678
的16进制表示。@d
为9, 对应前面第一个等式中的除数。@b
为@secret
from (...)x
是derived table的一个用法,保证这里不会出现syntax error。 - 第6行,计算得到
r_i
,注意它通过当前@l
的第r_i + 1
个字符来表示的。而不是实际的r_i
。例如这里@l
可能为"12467"
,当r_i=3
时,它对应结果为字符6
。这里完美地,绕开我们前面提到的问题。对于decode过程,我们只需要还原每一步的@l
即可以得到正确的r_i
。 - 第7-8行,更新
@l
, 去掉r_i
对应的字符。 - 第9行,更新
@b = d_i
和@r = i - 1
注意: 前面提到过,order by
后面的语句会对每一行都执行一次,除了上面我们提到的subquery。所以每一行都会拿到对应的r_i
。
0x05 其他
对于超长字符的猜解过程( > 90),我们可以考虑使用compress()
来压缩。但是,我认为这有待进一步的探索,因为压缩之后可能会引入新的字符,随便会减少字符串的长度,但一定会增加猜解的范围。
总结
在本文中,我们讨论了几种关于注入点出现在order by
子句时的可能的利用情况。我们重点讨论了,如何尽可能多的通过一次查询泄露更多的信息。同时涵盖了一种带特殊encode/decode的泄露方法。