ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

青少年CTF ez-sqli题解:手工SQL注入从原理到拿flag完整链路

青少年CTF ez-sqli题解:手工SQL注入从原理到拿flag完整链路 拿到这道“青少年CTF”的ez-sqli题目时我刚结束一场线上赛的Web方向复盘。说句实在话现在的入门赛题越来越“卷”但SQL注入SQL Injection简称SQLi永远是Web安全里最经典、也最值得手把手啃下来的硬功夫。这道题能出现在青少年CTF练习平台定位很明确就是给刚接触安全的新手准备的难度不高但足够让你把“手工注入”这条完整链路走一遍。这篇文章我就用解这道题的过程把SQL注入从原理、探测、利用到拿flag的完整思路串起来。适合刚入门CTF、想系统搞懂Web安全基础或者刷题总卡在“不知道下一步干嘛”的朋友阅读。1. 题目理解与信息收集先别急着上工具1.1 拿到题目第一件事看接口、看参数、看反应我打开题目环境后页面是典型的一个查询类业务界面带一个输入框要求输入用户ID来获取信息。这种场景在真实的业务系统里非常常见比如订单查询、会员信息查询、成绩查询。Web题目设计成这种形式目的就是模拟一个最典型的“用户输入 → 后端拼SQL → 数据库返回 → 页面回显”的流程。先做一个最基础的探路动作输入一个正常值比如数字1观察返回。如果页面返回了一条用户信息说明ID参数确实被后端查询使用了。接下来输入一个不可能存在的值比如0如果返回空大概率是“无记录”而不是报错这说明程序是用mysqli或PDO的fetch_assoc这类函数取结果查询失败并不会暴露SQL语句错误。我觉得这反而是好事说明这道题既不是报错注入的套路也不会直接给你“SQL语法错误”的提示你只能靠自己判断注入点。这一步的要点是信息收集阶段的每一条观察都是在给后面选择注入手法做准备。很多新手上来就挂sqlmap一把梭虽然也能出结果但完全失去了这道题的意义而且一旦遇到sqlmap被过滤的进阶题就不会玩了。我建议所有人在入门阶段都要先把人工探测的肌肉记忆练出来。1.2 判断注入点的两个经典动作判断一个参数是否存在SQL注入我常用的两个动作是单引号探测和逻辑判断探测。先输入1如果页面报错说明单引号被拼进了SQL语句并破坏了原有语法这是最直观的注入信号。不过这道题的页面非常克制输入1后没有报错信息只是返回空白。这就有点意思了排除报错注入继续用逻辑判断。再输入1 and 11和1 and 12分别观察。如果前者返回正常后者返回空说明我们输入的and条件确实进入了SQL语句的WHERE子句并参与了运算这就是可注入的确凿信号。这里有个容易被忽略的细节有返回和没返回的页面状态差异是什么。有的程序会渲染出“查无此人”的提示有的只是表格消失这两种页面在写自动化判断脚本时的检测规则完全不同。说个经验之谈逻辑判断探测时一定要保证其他变量一致只改变and后面的恒真恒假条件。我见过有新手在同一轮探测中同时改变URL、参数值、甚至User-Agent导致结果对比完全失真。做安全测试和做物理实验一样控制变量是最基本的方法论。1.3 字段数量探测ORDER BY的妙用确认存在注入点后下一步要搞清楚目标查询语句查出多少列。这决定了后面UNION SELECT语句能不能拼得进去是整个手工注入流程里非常关键的一步。操作方法是输入1 ORDER BY 1、1 ORDER BY 2、1 ORDER BY 3逐次递增页面正常就一直加直到出现异常。这里的原理是ORDER BY后面跟数字时数据库按照第N列进行排序如果N大于实际字段数数据库会报“Unknown column”错误。如果页面没有报错机制那就观察返回内容是否变化。我在这题里实测到ORDER BY 4时页面出现异常也就是说查询字段数是3个。记住这个数字它就是UNION查询时SELECT后面需要列出的字段个数。这个探测过程看起来很机械但它在整套注入流程中的位置非常关键后面UNION构造就是从这里接力的。2. 核心原理拆解SQL注入为何能成立2.1 一个老旧但必须懂的代码陷阱SQL注入之所以存在且屡禁不绝根源在于程序把用户输入的数据直接拼接进了SQL语句。看一段典型的漏洞代码思路$id $_GET[id]; $sql SELECT id,username,email FROM users WHERE id $id; $result mysqli_query($conn, $sql);当用户输入1时执行的是SELECT id,username,email FROM users WHERE id 1当用户输入1 UNION SELECT 1,2,3时执行的却是SELECT id,username,email FROM users WHERE id 1 UNION SELECT 1,2,3问题就出在用户的输入被当作SQL代码执行了。开发者想要的是用户输入一个“值”但程序却给了用户一个“任意代码片段”的执行权限。这个本质一定要透彻理解因为参数化查询PreparedStatement为什么能防注入说到底就是因为它把数据和代码彻底分开了——SQL结构固定不变用户输入只作为参数传入数据库不会把参数当代码执行。从开发者的角度看修复SQL注入并不难难的是改变所有开发者的编码习惯。尤其是一些老项目的历史包袱全库几千条拼字符串的SQL不是一朝一夕可以改完的这也是SQL注入在真实漏洞榜单上存活至今的客观原因。2.2 从查询结果看“回显位”和“数据位”理解UNION注入前要先分清两个概念查询要取的列数和页面会显示的列数。两者经常不是一回事。比如底层SQL查询的是id,username,email三列但页面模板只把前两列渲染出来了。这时候你做UNION SELECT就要让SELECT返回的列数为3但真正能让你看到数据的是页面显示的那几列。所以在UNION SELECT中我们通常会在各个位置放置数字标记如1,2,3通过观察页面上哪个数字“弹”出来了来确定哪个位置是可利用的回显位。这一步太关键了很多人卡在这里。他们知道要UNION SELECT 1,2,3但不知道为什么要用数字也不理解回显位的意义。你再想一下如果页面一个回显都没有UNION注入的路就走不通只能换报错注入、盲注或者时间盲注。这也是我在刚开始解这道题时把字段数摸清楚就迫不及待做UNION测试的原因——这个页面大概率是有回显的否则题目难度就要上升一个档次。2.3 闭合符号SQL语句的“括号配平”在做SQL注入时只关注参数本身是不够的你还得知道参数在SQL语句中是如何被包裹的。很多题目会在参数前后加上引号或括号比如SELECT * FROM users WHERE id $id SELECT * FROM users WHERE id ($id)这种情况下直接输入1 UNION SELECT 1,2,3是不行的因为拼接后的SQL变成SELECT * FROM users WHERE id 1 UNION SELECT 1,2,3单引号没有闭合后面的UNION被当成字符串内容的一部分。所以要先输入1来闭合前面的引号再用注释符--或者#把后面的内容注释掉使整条SQL语句语法正确。这道题用的就是单引号闭合因为输入1后页面返回空说明SQL语法没有崩溃大概率是闭合成功且查询为假。确定闭合方式后我构造的注入语句形态就是1 UNION SELECT 1,2,3 --这里--是MySQL中的注释符把SQL语句后面的内容全部注释掉。我习惯用--而不是#是因为在某些场景中#需要编码成%23才能正常传参而--在URL中可以直接使用兼容性好一些。3. 实操过程从探测到取flag的完整链条3.1 确定回显位在确认单引号闭合和字段数为3后我构造了第一轮UNION测试1 UNION SELECT 1,2,3 --页面返回了三列数据其中第二列和第三列的位置上数字2和3被直接显示在页面上说明这两个位置是回显位。这里有个细节我虽然知道了回显位是2和3但2和3这种数字本身没有业务含义所以后面需要把具体的SQL查询结果替换到这两个位置上去。各位新手朋友练习到这里一定要把环境记录下来最好用Burp Suite的Repeater保存请求因为后续你可能要来回切换好几种payload。反复在浏览器地址栏里编辑URL不仅容易复制错而且也不便于观察每次请求的差异。3.2 获取当前数据库名既然有了回显位就可以开始让数据库“开口说话”了。我用以下payload获取当前数据库的名称1 UNION SELECT 1,database(),3 --页面第二个回显位显示了数据库名是个标准的security库名也可能根据题目设置不同。不管它叫什么这个信息标志着我们的注入真正进入“获取数据”阶段了。获取当前数据库名的意义在于后面查询表名和列名时可以用table_schema 数据库名来做精确定位避免从所有库中捞数据减少干扰和判断误差。理论上也可以不查database()直接通过information_schema去遍历所有库的所有表。但那样数据量太大而且容易把自己绕晕。先锁定当前操作的库是效率最高的做法。3.3 查看库中有哪些表拿到了数据库名接下来就要获取这个库下面的所有表名。在MySQL的元数据库information_schema中tables表记录了所有数据库的数据表信息。查询表名的payload如下1 UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema数据库名 --这里用了group_concat()函数它的作用是把多行的查询结果拼接到一行每条结果用逗号分隔。为什么要拼接因为UNION查询要求前后SELECT返回的行数和列数一致如果直接查询出多行回显很难展示完整而且页面可能只显示第一行。group_concat能把所有表名压缩在一条记录里正好适合回显位有限的情况。结果返回了一堆表名其中有一张表的名称非常扎眼比如flag表或者users之类的常见命名。这道题里我在表名列表里看到了flag表目标就已经非常明确了。3.4 获取目标表的列名既然确定了目标表就需要查看这张表有哪些列。这一步用到的是information_schema中的columns表它记录了数据库中每一张表的每个字段。查询列名的payload1 UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_schema数据库名 AND table_nameflag --这一步执行后回显位展示了flag表的列名比如flag列说明只要查出这个字段的值就完成了。这里我有个建议在真实项目或比赛中如果列名不明确可以用group_concat(column_name)把所有列都带出来。但如果列名很多可以用limit分批查。这次只有一列就不需要那么折腾了。3.5 最终取flag终于到了最后一步。确定flag表下有flag列后查询语句就直接了1 UNION SELECT 1,flag,3 FROM flag --回显位直接输出了完整的flag字符串。拿到flag后整道题就算解完了。下一步就是去平台提交拿分数。到这里整个手工SQL注入的流程就闭环了发现注入点 → 确定闭合方式 → 确定字段数 → 定位回显位 → 查询库名 → 查询表名 → 查询列名 → 查询数据。这套思维模式不仅适用于这一道ez-sqli几乎所有的UNION注入题都是这个套路。后面遇到盲注、报错注入大框架不变只是获取数据的方式变了而已。4. 常见问题与排查技巧实录4.1 为什么我输入了UNION SELECT但没有反应这是新手最常踩的坑。大多是因为字段数不对导致UNION前后查询列数不一致数据库直接报错或者是闭合方式不对整个UNION被当成了字符串。排查顺序是先确认几个“数”字段数是几、回显位是几、闭包是什么。再推荐一个调试技巧在UNION SELECT中放置不同的数字或字符组合比如111,222,333而不是1,2,3。这样即使页面显示顺序有变化你也可以根据特征数字准确分辨哪个是哪个。我在做更复杂的题目时还会放md5(1)、version()、user()等函数一次请求拿多个信息。4.2 注释符到底用哪一个MySQL常用的注释符有三种#、--后面带空格、/* */。在实际传参时#在URL中要编码成%23--后面要跟一个空格由于URL会自动把空格去掉所以我习惯用--这里的在URL编码中代表空格。如果是POST请求那么--或#都可以直接使用没什么编码问题。这里有个让新人崩溃的场景明明在数据库命令行里测试相同的SQL语句是能跑的放到URL里就失效。原因往往是注释符后面的空格问题、#没有编码、或者请求方式是GET但用了带空格的payload。逐步排查请求格式就够了。4.3 为什么回显位只能显示数字很多模板渲染程序只取出查询结果的第一行数据但UNION查询如果没有指定排序数据库可能返回第一张表的查询结果也就是说你前面输入的条件要保证前方SELECT查不到数据才能让UNION部分的结果成为第一行。这就是为什么很多payload前面要带一个恒假条件如id-1。用-1就是为了让前半段查询无返回从而让UNION部分的数据顶上来显示。比如我再强调一下标准的UNION注入payload习惯-1 UNION SELECT 1,2,3 --用-1而不用1就是为了确保前半段不返回数据让UNION出来的结果成为唯一结果集。4.4 信息收集不彻底导致卡壳我在带新人的时候发现很多人卡住不是不知道SQL注入怎么做而是没有把页面特征观察到位。比如有的题目隐藏了报错信息有的题目存在输入长度限制有的题目对所有输入都加了一层转义这些都会改变payload的构造方式。做题的第一步永远是看接口、看参数、看返回而不是看到输入框就盲打一套标准payload。还有一点是关于“用什么工具”Burp Suite的Repeater是我做Web题最顺手的工具它能把整个请求过程结构化地保存下来也方便对URL参数做修改和重放。有不少题目在浏览器里因为转义、编码等原因payload不生效但在Burp里手动发请求就一切正常。对刚入门的朋友来说学用Burp放包收包比学任何注入技巧都重要。4.5 关于sqlmap什么时候用什么时候不用我并不是反对用sqlmap它确实能大幅提高效率。但sqlmap只是一个熟练工具替代不了对注入原理的理解。入门阶段我不建议立刻用sqlmap原因很简单如果一道题用了sqlmap你拿到了flag但你根本不清楚中间发生了什么后面遇到闭包变换、WAF拦截、参数多次编码的题目就会完全无从下手。等手工注入的整个流程做熟了再上sqlmap你才能读懂它的日志在干什么也才能在处理特殊情况时手动添加--tamper脚本绕过过滤。我自己的习惯是比赛中如果手工注入能够在5分钟内完成就直接手工打。如果遇到大量数据的盲注或者时间比较紧就会上sqlmap自动化。手工和工具的结合点掌握好了效率会非常高。5. 总结与扩展思考这道ez-sqli难度确实不大但它把SQL注入的完整链路非常清晰地走了一遍。我觉得它的价值在于让新手用最顺畅的方式理解“输入是如何变成代码的”同时也把MySQL的information_schema这张元数据表的基础用法练熟了。很多人在这个环节跳过了手工过程直接用工具结果后面学到报错注入和盲注时脑子里的概念就是一团浆糊。从这道题向后延伸SQL注入还有几个常见分支可以去系统学习字符型注入引号闭合、数字型注入无引号闭合、POST登录框注入绕过认证、报错注入updatexml/extractvalue、布尔盲注、时间盲注、堆叠注入以及宽字节注入老版本数据库。这些分支的核心思路都差不多差异在于获取数据的方式不同以及数值型/字符型的编码细节。再往后走后端防御手段也值得研究比如参数化查询、WAF的规则绕过、输入过滤的缺陷点。这些内容在CTF的进阶题中经常会碰撞到一起。所以我经常跟准备打CTF的朋友建议Web方向别急于打高难度的题先把基础打扎实。所谓“基础”主要就包括SQL注入的几种经典手法、XSS、CSRF、文件包含、文件上传、命令执行这几个大类其中SQL注入又是最值得反复训练的一项因为它的变化最多也最能训练人的逻辑思维。我现在做这道题的经验也让我在回头看真实业务系统的时候更加敏感。在某些老系统里一个不起眼的查询参数后面可能就藏着一个拖库的入口。安全意识不是背出来的是一次一次亲手“打进”数据库之后长在手上的条件反射。希望大家都能在合法授权、CTF比赛和靶场练习的前提下把这些实验做扎实后面遇到再花哨的题目底子都不会虚。
返回列表