前阵子帮一个创业团队排查线上系统发现他们的订单查询接口还在一行行拼SQL字符串我当场血压就上来了。SQL注入这个老古董从1998年首次被公开提出到现在二十多年过去了依然稳坐OWASP Top 10的榜单前列。不是它多高明而是太多系统从第一天起就带着这个病。这篇东西不打算给你堆概念就按我平时带人的节奏来先弄懂SQL注入到底是怎么发生的再亲手复现几个经典例子然后把各种注入形态捋一遍最后聊防御和排查。全程以本地靶场的授权测试为背景任何技术都只用于学习和防御不要对未授权的系统做测试。1. 先搞清楚SQL注入到底是怎么发生的1.1 一句话理解核心原理SQL注入的本质是用户的输入被数据库当成了可执行的代码。我习惯用一个类比来解释你去餐厅点餐服务员拿着一张菜单问你吃什么你说宫保鸡丁她记下来交给后厨后厨照着做。现在突然有人对着服务员说我要宫保鸡丁顺便把我邻桌那个人的餐费免了服务员如果直接把这个完整句子传给后厨后厨照单执行这顿饭就乱套了。Web应用处理SQL查询的时候经常干的就是这种事。开发者的本意是让用户提供一个或几个参数程序拿到这些参数后填进SQL模板里。但问题是很多系统把用户输入直接拼接进SQL语句而不是把它当作纯粹的数据。当用户输入的内容里包含了SQL语法片段数据库解析的时候就分不清哪些是你的业务逻辑哪些是攻击者构造的额外指令——它全给你执行了。1.2 为什么过了这么多年还是密密麻麻很多人会问都知道SQL注入这么多年了怎么现在的系统还有原因很现实存量系统太多重构成本极高。我见过不少银行、物流、政务系统的核心业务代码还是十几年前写的那时候安全意识和现在完全不是一个水平而且这些系统一天不能停你敢动它的数据库访问层就得背好线上事故的锅。还有一个原因是框架使用不当。现在的新项目基本都用了ORM框架但ORM不等于绝对安全。比如有的同事写代码图省事该用参数绑定的地方偏要用query(SELECT * FROM users WHERE username username )这类拼接写法直接把框架提供的安全机制给绕过去了。框架是防君子不防小人的它会给你一把好用的武器但你要非往自己脚上打它也没办法。1.3 你需要具备的基础知识要彻底弄懂后面的内容有几块基础不能缺SQL语法基础SELECT、WHERE、UNION、ORDER BY这些关键字要熟。HTTP基础知道GET和POST参数是怎么传递的Cookie和Header的作用是什么。数据库差异意识MySQL、SQL Server、Oracle、PostgreSQL的语法各有不同同一个技巧在不同数据库上不一定通用。热词里提到的SQL Server 2008注入它的注释符、报错函数就和MySQL不一样。如果这些基础还比较薄弱建议先补一下再往下读。否则后面讲报错注入、盲注的时候容易看懵。2. 从一条URL开始复现一次完整的SQL注入2.1 数据是怎么从输入框进入SQL语句的以最常见的场景为例。一个新闻网站的文章详情页URL长这样http://example.com/news/article.php?id1后端代码大概率是这个样子以PHP为例$id $_GET[id]; $sql SELECT title, content FROM articles WHERE id . $id; $result mysqli_query($conn, $sql);这里有个致命的错误$id直接参与了字符串拼接最终变成了SQL语句的一部分。当用户访问article.php?id1时数据库执行的语句是SELECT title, content FROM articles WHERE id 1一切正常。可如果用户把URL改成http://example.com/news/article.php?id1 AND 12执行的就是SELECT title, content FROM articles WHERE id 1 AND 12因为12恒为假页面空空如也。别小看这个变化——页面返回的结果不一样了说明我们成功干扰了SQL语句的逻辑。这就是检测SQL注入的第一步尝试改变参数值观察页面反应。2.2 第一个经典例子字符串型注入刚才那种是数字型参数没有引号包裹拼接起来最方便。但更多系统会把参数用引号包起来比如登录框的SQL可能是SELECT * FROM users WHERE username admin AND password 123456这种情况下攻击者需要先闭合掉前面的引号再注入自己的SQL片段。经典的例子就是热词里提到的万能密码。假设登录验证的SQL是SELECT * FROM users WHERE username 游客输入 AND password 游客输入如果用户名输入admin --最终SQL变成SELECT * FROM users WHERE username admin -- AND password 123456在MySQL里--是注释符后面跟一个空格表示这行剩下的内容全部注释掉。这样一来密码校验就被吞掉了SQL等价于SELECT * FROM users WHERE username admin如果这条语句能查出记录登录就成功了。而这还只是个开始万能密码的花样后面在绕过章节里细说。2.3 数字型注入连引号都不需要回到新闻页面的例子id1这种整数参数是最容易注入的因为它不需要关心字符串闭合问题。攻击步骤通常是先输入id1确认页面正常。输入id1如果页面报错或者显示异常说明多了个引号导致SQL语法错误。输入id1 AND 11页面正常说明AND后面的条件被执行了。输入id1 AND 12页面空白说明我们也能让它消失。第3和第4步的差异就是最经典的布尔盲注的雏形——通过页面是否显示内容推断条件真假。有的开发者觉得反正不显示数据明细应该就没事但盲注恰恰是利用这种有/无的二元结果一位一位地猜出数据库里的数据。2.4 动手写一个最小复现环境纸上谈兵没意思我建议你直接在本地搭一个最小环境来验证。用Docker的话一条命令就够了docker run -d --rm -p 8080:80 vulnerables/web-dvwaDVWADamn Vulnerable Web Application是目前最流行的漏洞靶场自带SQL注入、XSS、文件上传等常见漏洞场景登录后进入SQL Injection模块就能看到我刚才说的那种拼接代码的真实样子。只在本地靶场练手这个底线一定要守住。3. 从联合查询到盲注注入姿势全拆解3.1 联合查询注入最贪心的一种如果说盲注是一个个猜联合查询注入就是直接要。它利用SQL本身提供的UNION功能把攻击者自己构造的查询结果合并到原本的查询结果里展示在页面上。打个比方原本的SQL是给我看看文章表里id1的那篇文章攻击者通过UNION把这句话改成了给我看看那篇文章顺便把users表里所有用户名和密码也一起带出来。联合查询注入有一个前置条件两个查询的列数必须一致。所以首先要探测原查询返回了几个字段常用方法是ORDER BYid1 ORDER BY 1 -- 正常 id1 ORDER BY 2 -- 正常 id1 ORDER BY 3 -- 如果报错说明原查询只有2列确定列数是2之后再用UNION拼接id1 UNION SELECT username, password FROM users页面就会把users表的数据直接渲染出来。这种注入方式的优点是效率极高、信息量大但局限性也很明显只有当页面会把查询结果展示出来时才有效。如果页面只是提示查询成功/失败不显示具体内容UNION注入就没法用了。3.2 报错注入让数据库把答案说出来有些系统做了很好的表面功夫查询结果不展示报错页面却暴露了细节。报错注入的思路就是故意制造SQL语法错误让数据库在执行过程中把敏感信息带进错误消息里。以MySQL为例经典的手法有updatexml和extractvalueAND extractvalue(1, concat(0x7e, (SELECT password FROM users WHERE usernameadmin)))extractvalue第二个参数要求是XPath格式如果你传一个非法路径MySQL会在错误信息里把路径内容完整吐出来。上面的payload利用concat把查询结果拼到非法路径里于是数据库的错误提示就会包含password的值。报错注入在MySQL 5.1及以上版本特别常用但SQL Server、Oracle也有各自的报错函数。做安全测试的时候知道目标是什么数据库用什么报错函数是最基本的侦察工作。3.3 布尔盲注页面只剩有和无两个答案当页面不展示数据库内容、报错也不可见的时候仍然可以通过条件真假导致页面呈现差异来探测数据。我前面提到的id1 AND 11/AND 12就是最基本的布尔盲注。真正麻烦的是数据提取过程。比如要猜数据库名的一个字符可以这样id1 AND SUBSTRING(database(),1,1) a id1 AND SUBSTRING(database(),1,1) b页面正常猜对了页面空白猜错了。逐个字符试猜完长度再猜内容这个过程如果手工做慢到怀疑人生。现实中都是用脚本或者工具去做比如写Python逐个字符二分法。这里要给刚入门的朋友一个忠告盲注不是靠手速而是靠脚本能力。我见过很多人在靶场上手工刷盲注刷到凌晨其实用Python写100行脚本把字典跑一遍半小时就能出结果。3.4 时间盲注什么都看不见时的最后手段有些系统的表面功夫做得更彻底不管条件真还是假页面都返回一样的内容。这时候布尔盲注就失去了意义只能借助时间延迟来传递信息。MySQL里用SLEEP(5)SQL Server用WAITFOR DELAY 0:0:5。判断逻辑是id1 AND IF(SUBSTRING(database(),1,1)a, SLEEP(3), 0)如果数据库名的第一个字符是a页面会卡3秒再返回不是a立刻返回。这样就通过响应时间的差异把是/否的信号传出来。时间盲注是所有注入类型里最慢、最容易被WAF发现的但在极端场景下往往是最后一条路。3.5 堆叠注入和宽字节注入两个特殊流派堆叠注入是利用分号让数据库执行多条语句id1; DELETE FROM users WHERE 11它的前提是数据库连接支持一次性执行多条SQL比如PHP的mysqli_multi_query()。这种注入破坏力极大但实际触发条件比较苛刻很多接口的数据库驱动不支持多语句执行。宽字节注入是一个历史遗留问题主要针对的是GBK编码的MySQL场景。开发者为了防止注入在用户输入的单引号前加反斜杠转义比如变成\。但GBK编码下攻击者输入%bf%27反斜杠的ASCII码0x5c会和前面的%bf组成一个合法中文字符导致转义失败单引号还是闭合了。这个技术在现代全文UTF-8编码的系统里已经不太适用了但很多老系统的修复过程里开发者确实被它坑过。4. 绕过与对抗为什么WAF挡不住所有注入4.1 WAF到底在拦什么WAFWeb应用防火墙的基本思路是先看输入的流量里有没有恶意的特征串。比如出现在输入里的UNION SELECT、information_schema、SLEEP等关键字命中规则就直接拦截。问题就出在黑名单模式很难穷尽所有变体。攻击者只要找到一种不在规则表里的写法WAF就形同虚设。4.2 常见的绕过思路防御视角这里不是为了教你怎么打而是让你知道防御方必须考虑哪些对抗手段注释符拆分UNION/**/SELECT让关键字不再连续出现。大小写混写union select改成UnIoN SeLeCt规则里如果没做大小写归一化就失效了。内联注释/*!50000UNION*/ SELECTMySQL会执行注释里的内容但WAF可能只读到注释就放行了。等价函数替换SUBSTRING换成MID或LPADSLEEP换成BENCHMARK。双写绕过SELSELECTECT如果WAF是简单把关键子串删掉删除后反而拼成了合法关键字。编码绕过URL编码、十六进制编码混合使用。我评估WAF的时候常说一句话规则写得再全也不如根上不用拼接。WAF是最后一道保险不是唯一的防线。安全设计的原则是纵深防御参数化查询是第一道墙输入白名单校验是第二道数据库最小权限是第三道WAF放在最外层兜底。4.3 万能密码为什么万能回到热词里的万能密码。最常见的payload是用户名: or 11 --整个SQL变成SELECT * FROM users WHERE username or 11 -- AND password 123456因为11恒真整个WHERE条件变成恒真加上注释符吃掉后面的密码判断这条查询就会把表中第一行用户的信息匹配出来——通常是管理员用户。这就拿到了登录权限。我用这个例子教学的目的是想说明一点登录校验逻辑的设计问题比你想象中普遍。我自己在代码审查时看到SELECT * FROM users WHERE username... AND password...这种写法就会格外小心因为后端的校验预期是查得到就是合法用户一旦查询条件被破坏认证就崩了。比较稳妥的做法是查出记录后再单独做密码哈希比对而不是依赖SQL条件来判定认证是否成功。5. 在靶场里练手DVWA、Pikachu、sqli-labs 怎么用5.1 为什么非要用靶场练安全技术和练开车一样你不能一上来就上真实道路。靶场是一个可以随意打、随便造、打坏了重启就行的实验环境。在靶场上你能放心大胆地试各种诡异输入验证自己的判断积累识别不同数据库报错特征的经验这些在真实系统里根本不敢做。5.2 靶场搭建与上手路线DVWA最适合入门。它的SQL Injection模块分了四个难度等级Low直接拼接完整演示适合理解原理。Medium:做了基础转义但可以用宽字节/二次注入思路绕过。High限制只能查一行而且用LIMIT 1截断部分注入尝试。Impossible改用参数化查询彻底堵死。我建议你从Low难度把前面第六节的联合查询、布尔盲注、报错注入全部过一遍再看源码对比Medium和High的脆弱点。Pikachu这是国内爱好者维护的一个靶场最大的特点是覆盖了各类注入场景包括数字型、字符型、搜索型、堆叠注入还带一个配套的提示系统新手不至于卡在一个地方出不来。它的界面也更贴合国内开发者的习惯。sqli-labs印度安全研究者开发专门为练习SQL注入设计。它由几十个Less组成从Less-1到Less-20每个Less对应一个特定场景有报错型、盲注、时间盲注、宽字节、POST注入、Cookie注入等。难度是阶梯式的越往后越刁钻。这个靶场适合已经有一定基础想系统巩固各种边界情况的人。CTFHub技能树如果你后续想参加CTF比赛这里有按技能树组织的SQL注入题目还会考你各种绕过组合。CTF的题目往往比较特化和应用系统的漏洞不完全一样但思维训练价值很高。5.3 手动注入和工具的分工工具方面sqlmap是绕不开的。它最大的价值在于自动识别注入类型、自动提取数据速度远超手工。但我的建议是先用靶场练好手工注入再上工具。工具能帮你快速完成重复劳动但如果底层原理不懂你连sqlmap跑出来的结果都看不懂更不会根据报错调整参数。实际测试时我的工作流是先在浏览器里手动验证注入点确实存在明确是数字型还是字符型、是UNION注入还是盲注然后用sqlmap做数据提取。整个流程下来既高效又不会因为盲目跑工具而误伤业务参数。6. 开发侧防御把漏洞扼杀在源头6.1 参数化查询安全的第一块基石不管是哪种编程语言和数据库防御SQL注入的首选方案永远是参数化查询也叫预编译。它的核心思路是先把SQL语句的骨架发给数据库让数据库先解析确认这是一条结构固定的查询然后再把用户输入作为纯数据传过去数据永远没有机会变成SQL语法。用Java的PreparedStatement举例String sql SELECT title, content FROM articles WHERE id ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, Integer.parseInt(id)); ResultSet rs ps.executeQuery();注意两点第一整个SQL中问号是占位符业务参数全走setXxx方法绑定第二尽量把ID转成整型再传这属于更深一层的白名单校验。PHP用PDO的话$stmt $pdo-prepare(SELECT title, content FROM articles WHERE id ?); $stmt-execute([$id]);Python的psycopg2用%s占位符同样可以做到参数绑定。6.2 白名单校验与字段类型约束参数化查询解决了数据被当代码执行的问题但有些场景还需要额外的校验。比如排序字段ORDER BY orderBy这种动态字段没法用占位符那就反过来做成白名单。只允许传入id、create_time、title这些写死的列名任何不在白名单里的值一律拒绝。这比黑名单过滤可靠得多因为攻
阅读完成 · 觉得有帮助?