
前言在SQL注入讲解的前期我们先使用最经典的sqli-lab靶场进行分析虽然说这个靶场模拟不了现代前后端分离的场景但目的是挑选部分关卡进行快速过渡一、源码分析Less-11 的注入点与闭合点1. 页面功能与请求链路Less-11 是一个 POST 登录表单参数为uname、passwd。后端接收 POST 参数后直接拼接 SQL无过滤、无预处理。成功时回显username和password失败时输出mysql_error()。2. 关键源码解读核心代码要点$uname、$passwd直接进入 SQL。没有mysql_real_escape_string对于新手来说这里需要解释一下这是php中对字符串中特殊字符进行转义的函数其他语言也有提供类似函数但需要注意这不是完美的解决方法可能能通过宽字节注入绕过但也能在很大程度上避免SQL注入、没有 PDO 预处理。print_r(mysql_error())会输出 MySQL 错误因此可做报错注入。代码还把用户名和密码写入result.txt存在日志泄露和日志注入风险当然这里是靶场为了方便学习者判断注入特意设计的如果真实攻击场景中出现确实是个严重的漏洞。如果你想实时查看发送到数据库的查询语句但又嫌翻阅result.txt麻烦可以在源码生成sql语句后加上echo $sql在你访问的页面中显示当然在实战黑盒测试中肯定不会有这样的机会这种操作只是方便我们学习。3. SQL 模板拆解实际 SQL 结构SELECT username, password FROM users WHERE username$uname and password$passwd LIMIT 0,1$uname、$passwd被单引号包裹由and连接。注入时通常要闭合前面的单引号构造自己的 SQL 逻辑注释掉后面的 and password... LIMIT 0,1。4. 判断闭合方式输入unameadmin会触发 SQL 语法错误You have an error in your SQL syntax … near ‘’ and password‘’ LIMIT 0,1对比一下只输入admin大家会发现输入的单引号字符居然能够使MySQL程序报错证明单引号由于会改变SQL语句的语法而不能作为用户名的输入即$uname在SQL语句中通过单引号引用传入的字符串。这时候大家思考一下如果是单引号引用的话我们多输入的单引号是不是相当于提前完成了闭合此时再加一个注释符会怎么样呢大家手动进行测试后会发现无论密码输入什么用户都能够进行登录原理也显而易见这个注释符刚好把后面密码的判断语句给注释掉了导致select语句查询的标准只有用户名5. 源码缺陷总结直接拼接 SQL。无类型校验、无白名单。错误信息回显。日志记录敏感输入。没有预处理。二、其他闭合手法从 Less-11 扩展到通用场景1. 闭合的本质SQL 注入闭合的本质是先让用户输入“跳出”原本的字符串或括号上下文再插入 SQL 语法最后注释掉剩余部分。原 SQLWHERE username$uname and password$passwd输入admin#变成WHERE usernameadmin# and password#注释掉后面内容实际条件变成WHERE usernameadmin2. 常见闭合矩阵上下文SQL 示例闭合 payload 示例单引号字符串name$x or 11 --双引号字符串name$x or 11 --单引号括号name($x)) or 11 --双引号括号name($x)) or 11 --多层括号id((($x)))))) or 11 --数值型id$x1 or 11 --数值括号id($x)1) or 11 --LIKE 单引号name like %$x%% or 11 --IN 单引号id in ($x)) or 11 --ORDER BY 动态order by $x通常无引号直接id,(select ...)LIMIT 动态limit $x数值型1 procedure analyse()等由于这份教程面向新手所以这里简单提一下后面四个比较特殊的闭合类型在LIKE的语句中常出现%是为了配合进行模糊查询%是SQL语句中的通配符代表任意内容在IN的语句中常出现是固定语法括号内为列表在ORDER BY语句后使用的语句含义为先执行该语句再将执行结果返还给ORDER BY进行排序如果页面存在任何形式的错误回显我们就可以借助这个特性构造成可能出现报错的语句结合报错进行判断在LIMIT语句后的特殊语句是将LIMIT查询后的结果输出给后面的特殊子句执行但LIMIT后本身不能再加上其他非规定范围内的子句3. 注释语法对比数据库注释方式注意MySQL--、#、/* */--后必须有空格SQL Server--、/* */不支持#Oracle--、/* */常需from dualPostgreSQL--、/* */支持$$等编码注意URL 中#要写成%23这样在服务器收到URL后才能解码为#否则可能会和URL中将#设计为锚点的本意冲突。POST body 中可表示空格所以 Burp 中常用--。浏览器输入框直接输入--不一定等于--。换行也可作为--后的空白--%0a。这里大家可能会觉得奇怪为什么在数据包中我用可以代表空格但是输入框里输入的却不一定能被解析为空格呢原因是这样的在 HTTP 协议中如果请求头是Content-Type: application/x-www-form-urlencoded表单提交最常用的类型那么***请求体POST body***中的空格会被编码为或者%20加号本身会被编码为%2B。这种奇妙的错位就导致了你在输入框中输入的会被浏览器编码为%2B并在服务器被成功解码为相当于你发送的就单纯是这个符号但是如果你是在浏览器发出后的数据包进行修改那么你修改的内容既是编码后的内容此时你使用或者%20就都能在服务器被正确解码为空格了。需要注意这是针对请求体的说法在url中输入是能被正常解析为空格的。4. 与 sqli-labs 其他关卡映射分析Less-12、Less-13、Less-14源码、尝试使用闭合手法直接登录该关卡默认已知admin是数据库中已经有记录的用户三、现代网站应用场景闭合手法还存在吗1. 现代开发的变化现代应用通常使用PDO / MySQLi 预处理。ORMLaravel Eloquent、ThinkPHP ORM、Hibernate、MyBatis。参数化查询WHERE username ? AND password ?。在标准参数化查询下用户输入admin#只是普通字符串值。数据库不会把它当作 SQL 语法。闭合手法基本失效。2. 预编译为什么能防闭合预编译把 SQL 模板和参数分开传输SELECT * FROM users WHERE username ? AND password ?数据库先解析模板知道?是数据位置。即使用户输入admin#也只是参数值不会改变 SQL 结构。3. 预编译覆盖不到的地方以下位置不能简单参数化仍可能拼接表名SELECT * FROM $table列名ORDER BY $column排序方向ASC/DESCORDER BY、GROUP BYLIMIT某些驱动IN列表动态拼接LIKE通配符处理动态 SQL、存储过程、报表系统旧系统、CMS 插件、第三方 SDK4. ORM / 框架误用常见危险写法LaravelwhereRaw(username$name)ThinkPHPwhere(username$name)MyBatis${name}而不是#{name}HibernateHQL 字符串拼接Node.jsknex.raw、sequelize.query拼接Pythoncursor.execute(... %s % name)这些场景下闭合手法仍然适用。5. 二次注入与宽字节二次注入注册时存入admin#后续修改密码时取出拼接触发注入。宽字节注入GBK 编码下%df可能吃掉反斜杠使单引号逃逸。API/JSON后端若把 JSON 字段直接拼 SQL同样存在闭合问题。后记由于该教程面向新手板块三的内容我们就暂时不做深入后续会循序渐进地进行讲解。SQL注入闭合手法虽然在现代网站中较难生效但是这个思路是共通的了解后方便我们后续深入其他SQL注入手法也可以向外拓展到诸如XSS、代码执行、命令执行等攻击手段。希望各位业内的老师傅能纠正文章的错误非商业用途仅供交流学习禁止转载