本节摘要:本节拆解 SQL 注入的成因与最小利用:字符串拼接如何让用户输入改变语句结构,一个引号怎么闭合出"永真条件"绕过登录,联合查询怎么把任意表读出来。演示全部在本地靶场完成,payload 做了最小化裁剪,只求讲清机理。
注入回合开打。红队没有碰登录口,而是盯着商品搜索框随手输入了一个单引号。页面报错了——不是"没有找到商品"的业务错误,而是数据库语法错误。这个报错本身就是情报:用户输入被原样拼进了 SQL 语句,引号打断了语句结构,数据库把剩下的输入当成了残缺的语法。红队据此知道,这个框不是搜索框,是数据库的遥控器。
这背后的哲学问题值得先想清楚:程序里有两种东西——指令(程序想执行的逻辑)与数据(用户提供的值)。安全的前提是两者边界清晰。注入的本质,就是边界崩塌:数据里夹带的指令碎片,被数据库当真执行了。
看靶场里那段出问题的代码(反面教材):
# 反面教材:字符串拼接(靶场复现) def search_products(keyword: str): sql = f"SELECT id, name, price FROM products WHERE name LIKE '%{keyword}%'" return db.execute(sql).fetchall()
正常输入"咖啡",语句是自洽的。输入一个引号,语句变成 ... LIKE '%'%'——引号提前闭合,后面的 % 成了语法错误。若输入构造为 ' OR '1'='1,拼出来的语句变成:
-- 拼接结果:条件恒真,返回全表 SELECT id, name, price FROM products WHERE name LIKE '%' OR '1'='1%'
条件从"名字像某关键词"变成"名字像空串 或 永真",搜索框直接变成全表导出。登录场景同理,经典的闭合绕过(靶场演示,payload 已最小化):
输入用户名:admin'-- 输入密码:随便填 拼接后:SELECT ... FROM users WHERE name='admin'--' AND pwd_hash='...' 效果: -- 把密码校验整段注释掉,直接以 admin 身份登录
两行输入,认证旁路。注意这里的分野:口令回合红队在"猜值",注入回合红队在"改逻辑"——后者不关心你的密码是什么。

红队确认注入点后,标准推进是"问数据库自己要地图":
-- 靶场演示:先探当前库与版本(报错注入样式,最小化) ' AND EXTRACTVALUE(1, CONCAT(0x7e, DATABASE())) -- -- 页面报错信息回显:XPATH syntax error: '~shopdb' -- 同法可问出版本、当前用户、表清单,随后逐表读取
联合查询则把结果"贴"进正常页面:
-- 靶场演示:联合查询读取用户表(列数先用排序子句对齐) ' UNION SELECT name, pwd_hash, NULL FROM users -- -- 搜索结果区直接列出全部账号与口令哈希——上一章的离线破解素材就此到手
这两个演示串起了科目之间的接力:注入拖出哈希列,离线暴破还原口令,横向移动再扩张——这正是第章开篇攻击链的具象化。安全写作边界在此重申:演示止步于"读数据",写文件、执行系统命令这类后渗透动作只讲原理与条件(数据库账号有写权限、目录可写、配置允许),不给落地代码。
注入的危害上限由三件事决定:数据库账号权限、数据库进程的文件与命令权限、应用对错误的处理方式。权限收得紧,注入只能读到几张无关紧要的表;权限放得开(应用直连管理员账号),注入可以越库、读写文件、在数据库服务器上执行命令,战火从数据层烧到系统层。这也是蓝队"最小权限"策略的定价依据——它不能防注入发生,但能决定注入发生后的天花板。
注入在日志与流量里留下指纹:含引号、注释符、数据库关键字的异常参数;突然出现的高频同形态请求(自动化扫描器特征);数据库错误日志的语法错误聚集;单请求响应时间异常(时间盲注的前兆)。一条最小检测规则:
-- WAF/日志层:疑似注入参数的聚合(命中关键字 + 引号组合) SELECT ts, path, param_snip, count(*) AS hits FROM waf_log WHERE ts > now() - interval '15 minute' AND (param_snip ~* '(union.*select|or.?''1''=|extractvalue|--|#|;)') GROUP BY ts, path, param_snip HAVING count(*) >= 3 ORDER BY hits DESC;
应急上,确认注入后第一动作是止血与取证并行:临时以虚拟补丁(WAF 精准规则或参数校验)封住注入点,导出访问与数据库日志评估被读取的范围,轮换数据库凭证(防止注入拖走的连接串被复用),再排期根治改造。跳过取证直接修复,会永远丢失"对手拿到了什么"的答案。
读完这节做个自测:把自家系统所有"接收输入并进入数据库"的入口列出来,逐个问一句——它进语句时,走的是拼接还是占位符?答案决定你是已经安全,还是只是还没被扫到。
自测说到"列出所有入口",具体怎么列?按数据流向逆推,一张表就能盘完注入面:
| 输入来源 | 典型例子 | 容易忽略的变体 |
|---|---|---|
| 查询参数 | 搜索词、筛选值、排序字段 | 隐藏字段、分页游标 |
| 请求体 | 表单、接口字段 | 数组下标、批量操作的 id 列表 |
| 请求头 | 排序语言、渠道标识 | 用户代理、自定义追踪头入库场景 |
| 路径段 | 资源标识、租户名 | 伪静态规则里的参数化段 |
| 二级来源 | 数据库里存的"上一次输入" | 二阶注入:入库时无害,出库拼接时发作 |
最后一行值得专门强调:二阶注入是盘点时最容易漏的形态——数据入库时经过了转义,看似安全;但被读出后拼进了另一条语句,转义语境已经消失,攻击载荷原地复活。这也是为什么根治必须落在"输出到 SQL 的那一刻走参数化",而不是"输入进系统的那一刻做过滤"——入口千千万,出口只有一类。把这张表交给开发自查,比空喊"注意注入"有效得多。