SQL注入变形记:数字型、字符型与搜索型注入原理与防御 SQL注入这个话题我在不同的安全社区里聊过很多次每次都能遇到新人问同一个问题明明学了各种注入技巧但真拿到一个目标时还是不知道该怎么下手。多数教程喜欢列一堆payload让人背却很少讲清楚一个关键点——SQL注入的本质是“拼接”而拼接的方式取决于后端代码怎么写。你看到的数字型、字符型、搜索型说到底就是开发者把用户输入塞进SQL语句的不同姿势。这篇文章我会把三种最常见的注入类型掰开揉碎讲一遍包括它们的判断依据、构造思路、利用细节和修复方案还会穿插一些我在模拟环境里踩过的坑。如果你正在系统学习Web安全或者在做代码审计时想搞清楚“这里为什么能注入、那里为什么不行”这篇文章应该能帮你把零散的知识串成一条线。文章里的所有例子都基于某本地靶场环境某模拟项目X使用的数据表和SQL语句我会尽量贴近真实业务场景方便你照着复现。1. 先把底层的逻辑讲透注入为什么会“变形”1.1 一切注入的根源动态拼接SQLSQL注入发生的前提是后端代码把用户输入直接拼进了SQL语句。用个生活化的类比你让室友帮你带杯奶茶如果把“加珍珠”这三个字原封不动转告给店员店员会照做但如果室友在便签上写的是“加珍珠另外再帮我带包烟”而你整个把便签递给店员那店员就会一脸懵——他既看到了奶茶需求也看到了买烟指令。SQL注入就是那个“整个递便签”的过程你以为用户只会输入普通内容但攻击者输入的内容里夹带了数据库能执行的“额外指令”。我们来看一个典型的动态拼接场景。假设后端代码是这样写的?php $id $_GET[id]; $sql SELECT * FROM products WHERE id . $id; $result mysqli_query($conn, $sql); ?这段代码接收URL参数id直接拼进SQL。这里的严重性在于$id完全可控。用户输入1SQL是WHERE id 1用户输入1 OR 11SQL就变成WHERE id 1 OR 11——此时条件恒真整张表的数据全被查出来。而这种注入效果好不好、怎么构造完全取决于那个id出现在SQL语句里的什么位置、以什么形态存在。所以才会出现数字型、字符型、搜索型这些不同“变形”。1.2 三种注入类型的核心差异闭合与拼接很多人初学时会背“数字型不用引号、字符型要单引号、搜索型用LIKE”但光背结论不够你得理解背后的逻辑。数字型注入的场景是SQL语句打算把参数当数字处理代码里直接拼接不加任何引号包裹。此时攻击者输入的内容不需要“突破引号包围”直接参与到SQL语法里即可。字符型注入的场景是开发者用引号把参数包起来了比如WHERE username $name。此时用户输入被当成字符串的一部分。攻击者要先输入一个单引号把SQL语句里预设的引号“闭合”掉这样才能逃出字符串字面量的限制让后面的内容变成真正的SQL语法。搜索型注入则更隐蔽。它往往出现在搜索框中SQL语句形如WHERE title LIKE %$kw%。这里的参数不仅被引号包裹还被百分号包裹。攻击者输入的内容需要同时考虑引号闭合和百分号闭合而且因为LIKE的特殊性还可能利用通配符做模糊匹配的绕过。这三种类型本质上是同一个漏洞在不同拼接方式下的表现这就是“变形记”的含义。下面我按类型逐一拆解。2. 数字型注入最基础的“裸奔”场景2.1 判断数字型注入的两个探测动作拿到一个参数该怎么确认它是不是数字型注入记住一个核心思路让真值和假值分别出现观察页面反应是否不同。原文里提到了对URL参数直接尝试and 11和and 12这确实是数字型注入最常见的验证手法。假如一个商品详情页的URL是http://example.com/product.php?id1第一次构造http://example.com/product.php?id1 and 11因为11恒真原来的条件不会受影响页面正常显示商品1的信息。第二次构造http://example.com/product.php?id1 and 12因为12恒假整个WHERE条件变成WHERE id 1 AND FALSE查询结果为空页面可能显示“商品不存在”或直接空白。两次结果不同基本可以确定存在数字型注入。这里有个细节页面差异不明显时怎么办我习惯在后面跟一个-- -把后续内容注释掉或者用and配合耗时函数如sleep(5)来判断但最稳妥的方式还是先做“真值/假值对比”因为延迟判断容易受网络抖动影响。2.2 从判断到利用逐个猜解字段与数据确认注入点后常规操作是猜字段数。这一步的目的是为后面的UNION SELECT做准备因为UNION要求前后两个查询的列数一致。猜字段数可以用ORDER BYhttp://example.com/product.php?id1 ORDER BY 1 http://example.com/product.php?id1 ORDER BY 2 ...当ORDER BY n报错或页面异常说明当前查询的字段数小于n。比如ORDER BY 5正常、ORDER BY 6报错就说明原查询有5个字段。确认字段数后用UNION SELECT回显数据http://example.com/product.php?id-1 UNION SELECT 1,2,3,4,5-- -注意这里我把id改成了-1目的是让前面的查询结果为“空”这样UNION后面的数据才会出现在页面上。如果保持id1前面的查询本来就有数据UNION会把结果合并在一起可能只看到第一行我们就看不到注入的数据了。这个细节特别容易被新手忽略。然后看页面回显了哪个数字比如页面显示3和5说明这两个位置可以替换成我们想要的查询函数或字段。于是构造http://example.com/product.php?id-1 UNION SELECT 1,2,database(),4,version()-- -就能把当前数据库名和数据库版本都显示出来。2.3 实战中数字型注入的防护心得数字型注入在真实代码中相对少见了因为稍微有点安全意识的开发者都会给参数做类型判断或参数化查询。但我在一些遗留系统、工控系统、内部管理后台的代码里还是经常见到它的影子。最有效的修复方式是把输入强制转为整数?php $id (int)$_GET[id]; $sql SELECT * FROM products WHERE id . $id; ?或者直接用参数化查询?php $stmt $pdo-prepare(SELECT * FROM products WHERE id ?); $stmt-execute([$_GET[id]]); ?注意单纯用intval()或(int)强转会丢失精度吗在PHP里如果传入的是超大整数强转后可能变成浮点数或溢出但那不影响安全。你要关注的是强转之后任何非数字字符都会被截断所以1 OR 11这样的输入会变成1注入自然失效。3. 字符型注入突破引号包围的攻防较量3.1 为什么多了一个引号难度就上了一个台阶字符型注入的典型代码长这样?php $name $_GET[name]; $sql SELECT * FROM users WHERE username $name; ?此时SQL语句预先把单引号写死了用户输入的任何内容都被视作字符串字面量。如果用户输入adminSQL是WHERE username admin没问题。但如果用户输入admin OR 11拼接后的SQL变成SELECT * FROM users WHERE username admin OR 11注意看用户输入里的第一个单引号把前面那个预设的左引号“闭合”了随后的OR 11变成了真正的SQL逻辑而末尾的右引号则由用户输入里的最后一个1来配对。最终条件恒真查询返回全部用户。这里的核心逻辑是“闭合与配平”。你把每个单引号看作一对括号开发者预留了一个左括号和一个右括号攻击者要做的是输入一个额外的左括号即单引号然后用自己的内容替代原本右括号的位置让整个语句保持语法完整。3.2 注释符号让后半段“消失”的魔法在真实注入中我们经常不想让语句末尾那部分继续参与解析。比如原始SQL可能是SELECT * FROM articles WHERE author $author AND status 1如果注入点在$author用户输入xxx OR 11-- -拼接后SELECT * FROM articles WHERE author xxx OR 11-- - AND status 1-- -后面加一个空格MySQL要求在--后至少跟一个空白字符会把 AND status 1整体注释掉这样就不用管原来的业务限制了。-- -不是唯一选择#在MySQL里也可以注释。不同数据库的注释符有差异MySQL支持--和#SQL Server支持--Oracle只支持--。这点在实战中容易被忽视用错注释符会直接导致语句语法报错页面报数据库异常信息不过这反而暴露了数据库类型也不算全无收获。3.3 字符型注入的常见绕过思路对字符型注入我补充几种值得了解的绕过思路。单引号被过滤时可以看看开发者是怎么过滤的。如果只是简单把单引号替换成空字符串那么可以尝试宽字节注入——利用数据库字符集不一致让替代转义符的\被“吃掉”从而使单引号生效。经典的构造是在%df或%bf原理是老版本MySQL中GBK多字节字符的尾部字节与反斜杠形成合法字符导致转义失效。新版本MySQL默认utf8mb4基本不受影响但我在某些老旧教育系统里确实见过这种漏洞。引号完全被转义时如果存在数字型参数入口可以考虑用0x...十六进制形式代替字符串。比如WHERE name admin可以写成WHERE name 0x61646D696E这样不需要引号也能表示字符串。同理很多WAF对UNION SELECT的关键字敏感时可以用内联注释/*!50000UNION*/或者大小写混合来绕过但这属于WAF绕过范畴这里不展开。3.4 修复字符型注入关键是参数化不是加引号很多人第一反应是“在输入里再加一个引号把它转义”。这种思路有历史渊源addslashes()函数就是为了转义单引号、双引号、反斜杠而存在的。但问题在于转义方案依赖数据库字符集和上下文一旦遇到宽字节或编码不一致的情况就可能被击穿。我在实际项目中见过一个典型案例某开发团队自己封装了一个过滤函数把替换成\同时把所有用户输入用该函数过滤。表面上看字符串被转义了但如果后台数据库使用GBK编码攻击者输入%bf%27时%bf和\会组成一个合法的GBK字符后面的单引号就“独立”出来了过滤直接失效。所以最稳妥的修复方式是放弃手工拼接改用参数化查询?php $stmt $pdo-prepare(SELECT * FROM users WHERE username ?); $stmt-execute([$_GET[name]]); ?参数化查询的核心原理是SQL语句的结构和参数数据分通道传输数据库先解析SQL结构再把参数单纯当作数据值绑定进去。用户输入永远无法改变SQL语句的结构注入自然无效。这个机制才是真正的“免疫”而不是“防御”。4. 搜索型注入藏在LIKE里的特殊“变形”4.1 搜索框用LIKE注入思路要跟着变搜索型注入通常出现在站内搜索功能里。典型代码?php $kw $_GET[keyword]; $sql SELECT * FROM news WHERE title LIKE %$kw% ORDER BY id DESC; ?这种查询的特点有两个参数被单引号包围的同时还被百分号包围。百分号是LIKE子句里的通配符能匹配任意长度字符串。所以注入时除了要闭合引号还要考虑怎么处理百分号。构造一个基础测试http://example.com/search.php?keyword1 and 11拼出来的SQL是SELECT * FROM news WHERE title LIKE %1 and 11% ORDER BY id DESC解析一下拼接结果%1是LIKE的匹配字符串搜索包含“1”的标题紧接着的and 11是恒真条件而末尾的1%实际上是原SQL里右引号后面的%加上新的配对出来的字符串——如果你仔细配平引号的话会发现整个语句语法竟然是合法的而且条件恒真。这里的关键在于攻击者输入的内容中单引号的配对点被巧妙地转移了。如果换成1 and 12页面就没有搜索结果了说明注入判断生效。与数字型不同的是搜索型注入用两种结果判断时你要观察的是“有无结果”而不是“是否报错或正常显示”。4.2 搜索型注入的盲注思路搜索功能往往不直接回显数据库内容只显示搜索结果列表。此时直接UNION SELECT可能看不到具体值适合用盲注思路处理。原文标题里提到了“25”估计是一个系列教程的第25篇前面应该已经讲过盲注的基础这里我就针对搜索场景提几个关键点。布尔盲注的判断方式在搜索关键词后面拼接一个子查询让它返回真或假观察搜索结果是否存在。http://example.com/search.php?keyword1 AND (SELECT SUBSTRING(database(),1,1))t-- -如果当前数据库名第一个字符是t搜索结果正常否则结果为空。用二分法或者逐个字符枚举就能把数据库名、表名、字段名、数据一步步拖出来。时间盲注也是常用的备选方案特别是页面没有明显差异时http://example.com/search.php?keyword1 AND SLEEP(3)-- -如果页面明显卡了3秒说明条件成立。这里有个实操细节SLEEP()能否执行还取决于当前数据账号的权限但绝大多数情况下注入点既然能执行查询执行SLEEP()也没问题。注意盲注虽是“慢工出细活”但效率并不像想象中那么低。我在模拟环境里用脚本批量枚举时一个8位的库名大约一两分钟就能跑出来。手工测盲注时建议先确定字符集和大小写规则否则枚举的效率会差很多。实际业务中很多人会用现成的自动化工具跑盲注但我始终建议你至少手写一次脚本这样才能真正理解盲注的请求-响应-判断循环。4.3 搜索型注入的“通配符陷阱”搜索型注入还有一个其他类型没有的特性通配符本身的利用。既然LIKE里支持%和_攻击者在输入里直接塞一个%就能让搜索范围扩大。比如输入%SQL变成LIKE %%%此时查询可能返回所有数据——这不算注入但会造成信息泄露面扩大。更有意思的是如果开发者过滤了单引号但没过滤%输入% OR 11很可能被过滤单引号的操作挡掉但输入%admin%却能旁路匹配到包含“admin”的所有内容。这类通配符滥用对搜索功能来说是一个容易被忽略的信息泄露路径。我在一次代码审计中发现某系统的搜索框没有注入漏洞因为用了参数化但业务方完全没限制通配符攻击者输入一个%就拖走了整个商品表这就是典型的“没有注入漏洞但有逻辑漏洞”。修复搜索型注入时除了参数化查询还要考虑对%、_这两个通配符做转义处理。在参数化查询中使用LIKE时可以这样写?php $kw % . addcslashes($_GET[keyword], %_) . %; $stmt $pdo-prepare(SELECT * FROM news WHERE title LIKE ?); $stmt-execute([$kw]); ?这里addcslashes会把%转义成\%把_转义成\_让它们不再具备通配符语义。这个细节很多开发不知道他们把参数化一加就完事结果通配符注入仍然存在。5. 实操演练三种注入在一个模拟环境里完整走一遍5.1 本地靶场环境怎么搭前面讲了很多原理现在我们把三种注入放到同一个模拟项目X里跑一遍。我用的环境是PHP 7.4 MySQL 5.7跑在本地容器里数据库里建了一张商品表products和一张用户表users。搭建时我用了一个简化版的PHP站点模拟了三个页面product.php?id1数字型注入点login.php?nameadmin字符型注入点模拟登录处传参search.php?keyword手机搜索型注入点你如果想知道具体的建表语句和页面代码可以参考我前面给出的代码框架自己组装一套就行。关键是要在可控环境里反复试不要拿真实站点练手这是红线。5.2 数字型注入的完整操作记录我先访问product.php?id1页面正常显示“商品手机价格1999”。然后尝试id1 and 11页面仍然正常改为id1 and 12页面空白——这里没有报错而是没有查询结果说明条件为假时无数据返回。我判定这是数字型注入。确认注入点后用ORDER BY猜字段数。按顺序试到ORDER BY 6时页面报错说明原查询有5个字段。接着用UNION SELECT探回显位置product.php?id-1 UNION SELECT 1,2,3,4,5-- -页面显示了2和5也就是说回显位是第2和第5列。于是product.php?id-1 UNION SELECT 1,database(),3,4,user()-- -页面直接显示数据库名是shopdb数据库用户是rootlocalhost。这说明当前连接数据库使用的是高权限账号。利用这个权限我可以继续读文件或者直接into outfile写webshell但在本地靶场我是点到为止。5.3 字符型注入的完整操作记录接着测试login.php?name1 and 11页面返回“登录成功”的假象——因为条件恒真查询返回了第一行用户记录。改为1 and 12页面提示“用户不存在”。判断为字符型注入。字符型注入在这里的回显不太直观因为登录成功与否只是布尔状态。我改用盲注思路验证数据库名长度login.php?name1 AND (SELECT LENGTH(database()))6-- -如果页面显示“用户不存在”说明长度不等于6试到7时页面显示“登录成功”注意这里的登录成功是注入构造出来的不是真正的登录成功说明数据库名长度是7。逐个猜字符后得到shopdb。这个例子也说明很多情况下字符型注入点在业务页面上没有直接数据回显盲注是必经之路。平时练习时不要老想着一次UNION SELECT搞定一切要把布尔盲注的手法练熟。5.4 搜索型注入的完整操作记录最后测试search.php?keyword1 and 11页面正常显示搜索结果列表改为1 and 12页面显示“没有找到相关内容”。判定为搜索型注入。我尝试直接UNION SELECTsearch.php?keyword1 UNION SELECT 1,2,3,4,5-- -等等这里要注意一个问题搜索型注入的SQL语句结构是LIKE %$kw%如果我在输入末尾写-- -被注释掉的部分包括原SQL的%和后面的半截语句。但如果输入末尾写且没有注释符就需要保证单引号配平。我的经验是直接拼1 UNION SELECT 1,2,3,4,5-- -并不总能成功因为UNION的列数必须与原查询一致而这取决于原SELECT里到底查了几个字段。先跑ORDER BY来判断列数比较靠谱。实测结果搜索表有5个字段回显位在第3列和第4列。于是我构造search.php?keyword1 UNION SELECT 1,2,database(),4,5-- -页面在搜索结果列表里多了一条带数据库名的记录注入成功。这里还有个小坑因为LIKE里带了%当UNION SELECT的数据与前面的查询结果合并时数据库名可能会被当作标题展示但有时会被截断或转义。页面编码如果不一致中文内容还会乱码。遇到乱码不用慌检查一下页面charset和数据库连接字符集设置通常是character_set_client的问题。6. 常见问题与排查技巧实录6.1 为什么页面没有报错但注入还是不成功这种情况最常见的两个原因一是开发者开启了display_errorsOffSQL报错被吞了二是查询结果为空时页面模板渲染逻辑不同导致你以为注入失败。判断方法是先看正常的请求返回什么再分别构造and 11和and 12如果两种请求返回不同就说明“真/假”可控即使不报错也能走盲注。另一个容易被忽略的问题是参数在传进SQL前被做了二次编码。某些框架会自动对参数做addslashes或mysqli_real_escape_string处理。这时候你看到的注入点是“假注入点”单引号被转义了直接构造会变成\。要学会看后端代码的过滤逻辑不能只盯着前端参数名。6.2 三种注入类型判断混淆怎么办初学者最容易把字符型和搜索型搞混因为它们都需要闭合引号。我的判断口诀是参数值直接跟在等号后面没有引号包裹 → 优先考虑数字型参数值被引号包裹但搜索框里没有模糊匹配需求 → 字符型参数值被引号和百分号双重包裹通常出现在搜索场景 → 搜索型具体操作时先用and 11/and 12测布尔差异如果差异成立再尝试闭合。字符型闭合用就够搜索型还需要考虑%的位置。这里有一个辅助判断的技巧搜索型注入里如果你输入一个不存在的词比如zzz不存在xyz看页面的搜索结果提示。如果提示“没有找到相关内容”说明后端逻辑是“有结果才显示列表、无结果显示空状态”。这种页面反馈本身就对应了布尔盲注里的False状态。6.3 参数化查询后就一定安全了吗不一定。我在实际项目中遇到过几种“参数化后仍然被注入”的情况部分拼接片段SQL主体用了参数化但排序字段、表名、列名仍用字符串拼接。排序字段注入ORDER BY注入就是一个经典变种因为列名不能被参数化。此时只能用白名单校验。存储过程内部拼接存储过程接收参数后内部用动态SQL重新拼接绕过参数化保护。框架查询构造器的误用某些ORM框架允许传入“裸条件”如果你把用户输入直接传给whereRaw或havingRaw实际上还是拼接。所以代码审计时不能只看有没有用prepare要追整个SQL语句的所有组成部分。安全不是加个函数就完事而是要看数据流和控制流。6.4 注入点确认了但拿不到数据怎么办这种情况多出现在权限不足的场景。比如当前数据库账号只对某几张表有查询权限information_schema可能被禁用UNION可能因为列类型不匹配报错。我的排查顺序是确认数据库账号权限用current_user()看实际用户确认注入点所在查询的列数用ORDER BY确认回显位置用无害的UNION SELECT 1,2,3...如果UNION不能回显改走盲注或报错注入比如extractvalue、updatexml最后考虑是否可以通过into outfile写文件但这需要FILE权限且知道网站绝对路径有时候就算有UNION注入的表和原表的列数一致但列的数据类型不匹配比如原查询第1列是整数我UNION SELECT的第1列是字符串数据库会报数据类型错误。这时要调整UNION里各列的类型或者用null占位。7. 排查速查表与防御修复对照我把三种注入类型的关键信息整理成一张对照表方便你快速查阅。对比维度数字型注入字符型注入搜索型注入SQL特征WHERE id $idWHERE name $nameWHERE title LIKE %$kw%判断关键参数直接参与数字运算需闭合单引号需同时处理引号和百分号典型探测and 11/and 12 and 111 and 11主要利用方式UNION、报错、盲注UNION、布尔盲注、时间盲注UNION、布尔盲注、通配符滥用常见修复强制类型转换、参数化参数化查询参数化 通配符转义防御层面我再强调几个实战中容易漏掉的修复点输入校验不能只做黑名单。过滤select、union这些词很容易被大小写、注释符、十六进制编码绕过。更靠谱的是“类型白名单 参数化”。数据库账号权限要收缩。经常看到站库配置用root连库这种高权限账号一旦注入成功攻击者能做的事情就太多了。正确的做法是给Web应用单独建账号只授权SELECT/INSERT/UPDATE/DELETE并限制可操作的库表范围。SQL语句的结构和参数分离。凡是属于SQL语法结构的部分如表名、列名、排序方向必须用白名单校验属于数据值的部分用参数化绑定。日志和监控要跟上。注入攻击的请求往往有明显特征比如URL里带、UNION SELECT、sleep(等关键词。WAF和数据库审计日志结合使用能提前发现问题。8. 写这篇文章的实操体会把三种注入类型放在一起对比着讲是我在实际学习和测试过程中逐渐形成的思路。早期我学SQL注入时总想找一套“万能payload”后来发现不同场景下的构造方法差异极大真正有用的不是记住某个payload而是理解“用户输入如何在SQL语句中参与拼接”这个底层机制。在这个模拟项目X里反复测试时我印象最深的是搜索型注入的单引号配平问题。第一次构造payload时我自作聪明地在后面多加了一个百分号结果语法一直报错。后来我把拼接后的完整SQL语句打印出来看才发现问题出在引号和百分号的配对关系上。这个经验让我养成一个习惯遇到注入不成功先把拼接后的SQL语句完整还原出来而不是盲目换payload。另外一个建议是练习时多用不同的输出方式。你可以在本地代码里临时加一条var_dump($sql)来查看真实拼接结果这样能更快地理解漏洞成因。生产环境里看不到SQL语句但本地练习时一定要把SQL打印出来看这是建立直觉最有效的办法。SQL注入的“变形”并不神秘。数字型、字符型、搜索型只是同一漏洞在不同代码写法下的外在表现一旦你看懂了拼接点在哪里就能自如地构造payload也能更稳妥地写出修复代码。希望这篇文章能对你的学习有点帮助接下来你可以去靶场环境里亲手复现一遍把每个步骤跑通。等到自己独立发现一个注入点并成功拿出数据时你会觉得之前的钻研都值得。