news 2026/10/19 5:41:15

CTF实战复盘:Easyweb从源码泄露到SQL注入、二次渲染与文件包含

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CTF实战复盘:Easyweb从源码泄露到SQL注入、二次渲染与文件包含

在某个CTF练习平台上刷到《Easyweb》这道题时,我的第一反应是:这名字也太有迷惑性了。题名顶着“Easy”,实际却是某年全国性大学生信息安全竞赛总决赛Day2的Web1,BUUCTF上给出了复现环境。我原以为半小时能收工,结果从信息收集、源码审计到SQL注入绕过,再到文件上传的二次渲染,前前后后折腾了一个晚上。把整条链路走完再回头看,这道题其实没有一个环节是真正“难”的,但每一个环节都埋着让人翻车的细节,非常典型。

这篇复盘写给两类人:一是准备打CTF的选手,尤其是Web方向还在积累经验的新人;二是想了解真实Web攻击链路如何串起来的开发者。我会按照我当时的做题顺序来写,信息收集、登录注入、后台上传、包含触发、拿flag,每一步都讲清楚判断依据和踩坑点。

1. “Easy”这个前缀,在CTF里往往是反义词

先说一个CTF圈子的共识:出题人给人题目标上“Easy”只有两种可能,要么真的简单到一眼看穿,要么就是让你以为简单,然后在某个不起眼的角落埋雷。这道总决赛Day2的Web1明显属于后者。Day2的题目虽然不再承担区分度最高的任务,但它要保证大多数队伍能推进、又拿不全分,所以漏洞链往往设计成“两段式”甚至“三段式”。

1.1 CTF出题人的命名习惯

我刷过不少总决赛和分区赛的题,发现出题人命名有几条规律。叫“Easy”的题,入口通常会做得非常干净,干净到你第一眼甚至找不到任何可疑参数。目的就是考验基本功:目录扫得够不够细,源码审得够不够认真,拿到权限后有没有意识到后面还有一层。

相比之下,名字叫“hard”或者带生僻单词的题,反而会把攻击面铺得很大,让你不知道该往哪打。所以“Easyweb”这五个字母代表的是“入口简单、链路完整、后手隐蔽”,它考的不是某个独门绝技,而是你能不能沉住气把一个基础链路走完整。

1.2 拿到题目后,我建议你先做三个减法

说实话,我一开始也想直接上扫描器猛扫,但上次在类似题上吃过亏之后,我养成了一个习惯,先做减法。第一个减法:把题目可能涉及的考点列出来,总决赛的Web题逃不出注入、上传、包含、反序列化、命令执行这几条主线,Easyweb大概率不会跳脱这个框架。第二个减法:确认入口形态,登录页、功能页、还是直接给你一个查询框,入口形态决定了信息收集的优先级。第三个减法:提醒自己不要轻敌,“简单”的题一旦卡住,多半是信息收集做得不够,而不是漏洞本身有多难。

这个“先建模再动手”的习惯帮我省了很多无用功,后面也证明了它的价值。

2. 看似普通的登录页,第一步是翻家底

打开复现环境,页面是个再普通不过的登录框,用户名、密码、验证码,外加一个“忘记密码”的链接。我当时先看了响应头、HTML源码和robots.txt,都没有异常。然后做了目录扫描,这正是后面所有事情的起点。

2.1 源文件的几种常见存在姿势

CTF里源码泄露的方式翻来覆去就那么几种,但每道题藏的地方不一样。常见的有:备份文件直接放在网站根目录(/www.zip、/web.tar.gz、/site.zip),这种压缩包解出来直接就是整套源码;单个文件的备份(/index.php.bak、/.index.php.swp),这种往往只泄露一个关键文件;还有版本管理工具目录(.git、.svn),能完整还原历史版本,甚至能恢复被删除的敏感文件。

我的经验是,如果目标站点是一个相对完整的业务系统,先找www.zip这类根目录备份;如果只是一个简单的功能页,就盯.bak和.swp。另外字典一定要覆盖.git目录,很多队伍就栽在“只扫了常见后缀”上面。当时我用的字典里正好包含了这几种常见情况,也算是踩过的坑总结出来的经验。

2.2 从备份源码里挖出的第一处不对劲

扫描结果很快出来了,根目录下躺着一个www.zip。下载解压,里面是一个典型的MVC结构,控制器、模型、视图分得清清楚楚。我先没急着看业务代码,而是用了三分钟过了一遍目录结构,重点关注两个地方:路由入口和公共函数库。路由入口能告诉我每个请求最终会被分发到哪个方法;公共函数库则决定了过滤逻辑长什么样。

然后就看到了一段让我眼前一亮的代码,登录验证的逻辑大概是这样的:

$username = $_POST['username']; $sql = "SELECT * FROM users WHERE username = '{$username}' AND password = '" . md5($_POST['password']) . "'"; $result = mysqli_query($conn, $sql); if (mysqli_num_rows($result) > 0) { // 登录成功 }

这种把用户输入直接拼进SQL的写法,放在CTF里简直是把“注入”两个字写在了脸上。但结合我过去做题的经验,这里通常会有一个变数,不是直接就能注进去的。我继续往下读代码,发现用户名参数在拼进SQL之前,确实经过了一个自定义的过滤函数,而这个过滤函数恰恰只处理了其中一种情况,留下了我后面要用的空间。

3. 登录绕过的窄门:SQL注入被一层“类型转换”挡了路

拿到源码之后,我本地起了一套环境来复现这个登录逻辑。确认漏洞点之后,我开始尝试手工注入。但我构造常规的' or 1=1 --这类payload时,发现单引号被过滤了。仔细看了过滤函数,str_replace把单引号替换成了空字符,但只替换了一次。

于是第一反应就是经典的“双写绕过”思路,把payload里的单引号换成两个单引号,这样替换之后还能剩一个。但试完之后发现登录还是失败。再仔细读代码,发现用户名经过了intval()处理。

3.1 为什么一个md5会让注入变得不顺手

这是这道题第一个有意思的地方。用户名和密码在进入SQL拼接之前,分别被处理过:用户名被intval()强制转成了整数,密码被md5()之后直接拼进SQL。intval('admin')的结果是0,等于说你输入任何字符串,最后拼进去的都是0,这就导致你在用户名位置无论怎么构造SQL,都绕不开整数类型的限制。

那怎么办?这时候需要换个思路:既然用户名被转成了整数,那我能不能找一个数值型的注入点?但看来看去,登录这个动作本身并没有基于数字参数的查询,它就是一个简单的相等判断。

3.2 报错注入在实战里的识别与利用

我重新审了一遍源码,发现“忘记密码”这个功能里有一个隐藏接口,通过一个数字参数来查询用户密保问题。这个参数没有经过过滤,直接拼进了SQL。于是问题就变成了:找到一个数字型注入点,然后利用报错注出来数据。

报错注入的利用逻辑是:让SQL语句在执行时故意触发错误,并且让错误信息里带上我们要查询的数据。最常用的是updatexml()函数:

AND updatexml(1, concat(0x7e, database()), 1)

concat(0x7e, database())会把当前数据库名拼在一个波浪号后面,然后updatexml在解析第二个参数时发现格式不对,就把整段内容原样丢进报错信息里回显出来。这个技巧在CTF里非常实用,因为不需要像盲注那样逐字符猜,一条请求就能看到一段数据。

构造出来的最终payload是这样的逻辑:

AND updatexml(1, concat(0x7e, (SELECT table_name FROM information_schema.tables WHERE table_schema=database() LIMIT 1)), 1)

页面返回的报错信息里直接出现了表名。顺着这条路,我拿到了后台管理员账号的密码哈希。这个哈希还是明文拼接的md5,解出来之后,登录成功。

3.3 拿后台权限只是这题的起点

到了这一步,我其实已经花了整个解题流程的三分之一时间。登录进去之后,后台界面比我预想的要简单得多,左边一个菜单,有文章管理、图片管理、系统设置,看起来就是一个标准的CMS。但我心里清楚,真正的坑大概率还没出来,因为总决赛的Web题不可能会让你在登录这里就画上句号。

后台图片管理界面有一个上传头像的功能,我把注意力放到了那里。这个功能点在后来的复现中,几乎让所有队伍都卡了一轮。

4. 后台图片上传:二次渲染把一句话木马压成了花边

上传功能放在“个人设置”里,支持JPG、PNG、GIF三种格式。我按惯性操作:先抓包看接口返回,确认服务端是否校验了Content-Type、文件头、文件后缀这三个维度。实测下来,前端限制和后端限制都有,改了后缀、伪造了文件头都过不了,判断是白名单校验。

4.1 上传校验被层层包裹时,哪些姿势无效

面对这种情况,很多新手的第一反应是“改后缀绕过”或者“双文件上传”,但在这道题里都无效。出题人用的是严格的白名单,图片扩展名只能是那三种,并且会把文件内容交给一个图像处理库重新生成一遍。这就引出了另一个经典问题:二次渲染。

所谓二次渲染,简单理解就是:你上传一张图片上去,服务器会用GD库重新创建一个图片,带尺寸调整、格式转换,然后把新生成的图片保存下来。这个过程会打碎你原始文件里所有非图片结构的数据。你在图片尾巴上拼接的PHP代码,会在重新生成的过程中被当作无效数据丢掉。

4.2 二次渲染绕过的关键:找一块“带不走的像素”

绕过二次渲染的核心思路是:找到图片被二次处理之后依然保留不变的区块,把你的payload藏在这些区块里。因为GD库在处理PNG时,会对每个像素进行解码再重新编码,但图片的颜色数据本身在渲染前后大概率是一致的,这些数据块就是我们可以利用的容器。

我当时测试时上传了一张最简单的PNG。先上传一次,拿到服务器返回的渲染后图片,再对比本地原图和服务器返回图之间的差异。用Python写了个脚本,找出了两者二进制内容中完全一致的一段数据。那段数据落在图片的中间偏后的位置,正好是纯色区域,二次渲染时这块区域被原样保留了下来。

4.3 用对比脚本站稳payload

思路确认之后,接下来的操作是:保留原图前部的合法PNG头和数据块,在中间一致区块插入payload,然后重新封装成一张PNG。关键点是插入的位置必须处在GD库不会重新计算的数据块中,至少要让payload嵌入后,GD库处理时依然认为这是一张合法的PNG,不至于直接报错拒绝生成。

我当时写了一个很小的Python脚本,核心逻辑就是对原图和二次渲染图做逐字节对比,找出可嵌入区间,再把payload塞进去。这种脚本在CTF圈子里的写法人人不同,但原理都是一样的:二进制级别的差异比对。整个过程最需要耐心的是不断调整payload位置,因为有的位置虽然渲染前后代码一致,但插入payload后图片就损坏了,被识别成非PNG文件,直接被服务端拦下来。

这里要提醒一点,上传绕过类的题目,payload写得再漂亮,如果图片本身合法性过不了校验,一切都是零。所以测试初期,先上传一张最朴素的图片,确认二次渲染前后文件头和数据块布局,再动手改图。

5. 临门一脚:图片马只有当它被“包含”时才叫马

到这一步,我手上的资源是:后台账号、一个上传了带payload图片的路径。但图片路径是/uploads/xxx.png,PNG后缀决定了它不会被当作PHP执行,直接访问它只会看到一张图。这时候就需要找一个能把文件内容当作PHP代码解析的入口,也就是本地文件包含点。

5.1 文件包含点怎么找:动作、参数名与路径特征

我重新看了一遍后台功能,重点观察那些带参数跳转的URL。文件包含点的特征其实很明确:URL里出现file、page、include、template这种参数名,后面的值以前是文件名、路径或模块名,现在大概率被做了路径限制。看到了一个文件下载功能,参数名就叫file,传的是相对路径。一试就知道是包含点,因为路径穿越到其他地方就能读取系统文件,说明参数没做严格过滤。

这个包含点恰好没有限制伪协议,也没有强制加/uploads/这种前缀,所以我可以通过相对路径,把刚才上传的图片包含进来。

这里涉及到一个非常常见的原理:PHP里include语句的本质是“读文件内容并当作PHP代码执行”,所以无论被包含的文件后缀是什么,只要文件内容里存在<?php ?>标签,就会被执行。这个特性让包含和上传两个漏洞产生了化学反应。

5.2 webshell入口校验

包含参数构造好之后,我先做了一次无害验证:把图片里payload设为<?php phpinfo();?>,请求包含URL,页面成功输出了PHP的配置信息,说明这条路通了。

接下来才是真正拿flag的操作顺序。我先通过包含点执行了一个简单的目录列取命令,确认当前绝对路径;再逐个目录找flag文件。在这个阶段,我给自己定的原则是:不做任何破坏性操作,只读取目标明确的内容,这样既不影响复现环境的稳定性,也能快速定位答案。

5.3 最后的收尾:查环境、找flag、清理痕迹的思路

找到flag之后,我个人习惯把服务器上相关的上传测试文件清理掉,不让后面来复现的人看到一堆残留。当然在真实比赛里这一步更简单,你只需要把flag写进writeup,服务器环境自然有人回收。但作为练习,尽量保持环境整洁是一个好习惯。

这条完整的链路是:源码泄露 → 代码审计发现隐藏接口 → 报错注入拿管理员账密 → 后台文件上传 → 二次渲染绕过落一句话 → 文件包含触发payload。每一环单拎出来都不算难,但串联起来之后,任何一个环节卡住都无法到达末尾,这也是这道题真正的价值所在。

6. 把Easyweb从零走一遍后,我调整了三个做题习惯

做完这道题,我又花了点时间回头审视整个过程,发现真正让我耗时最久的并不是某个技术难点,而是几个心智层面的问题。

6.1 别被“套题”思维绑架

我一开始默认“登录框就应该是SQL注入的入口”,结果用户名被intval处理之后,我还在那死磕了半天。后来跳出来重新看源码,才发现注入点藏在“忘记密码”的隐藏参数里。这道题设计得很巧妙,它让显性的漏洞点做得非常“干净”,把真正的漏洞放在次级路径上。做题时不能抱着“第一眼在哪看到漏洞,漏洞就应该在哪”的惯性,还是要老老实实把整个应用的攻击面梳理完整。

6.2 工具、手工与盲猜的配比

目录扫描和源码审计的阶段,自动化工具能帮上大忙。但进入SQL注入环节之后,我建议尽量手工验证。sqlmap在总决赛题上经常会被过滤逻辑卡到无语,不是因为工具弱,而是出题人就是了解工具的默认逻辑,刻意做了针对性的变型。手工构造payload的节奏固然慢,但你能精确控制每一个参数,也更容易理解过滤规则的边界在哪里。

6.3 做记录的粒度决定复盘价值

我之前做题经常是把关键payload随手写在临时文件里,过几天再看只剩一团乱麻。这次做题我换了个方式,当时就把每一步的请求包、响应包、关键截图、失败原因,按时间顺序记在文档里。后面复盘时,我能精确还原自己是在哪一步开始跑偏的。这个记录习惯已经被我带到了所有题目练习里,对积累经验帮助非常大。

如果你现在准备做类似的题目,我的建议是:先从信息收集开始练,把备份文件扫描做到“下意识”;然后练手工报错注入,把常见函数和报错回显看熟;最后专门花时间研究二次渲染绕过,这是如今Web题最爱藏坑的点。把这三件事练扎实,再遇到叫“Easy”的题,你就不会像我一开始那样心里发怵了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 5:41:10

PostgreSQL WindowAgg Run Condition判断逻辑优化与修复解析

优化 WindowAgg Run Condition 判断逻辑&#xff0c;乍一听是个非常细碎的内核改动&#xff0c;但它背后牵扯的其实是 PostgreSQL 执行器对窗口函数框架语义的理解。我在跟进这个问题的过程中&#xff0c;把相关源码翻了好几遍&#xff0c;也动手跑了不少复现场景&#xff0c;这…

作者头像 李华
网站建设 2026/10/19 5:40:49

MTCNN人脸检测完整实现:三网络级联、训练数据生成与部署要点

简介&#xff1a;一份面向人脸检测与深度学习入门者的MTCNN完整实现&#xff0c;涵盖模型定义、数据准备、训练、验证与推理全流程&#xff0c;适合算法复现、课程设计及项目实战。压缩包共238个文件&#xff0c;以Python源码、PyTorch权重、图片样本与XML标注为核心&#xff0…

作者头像 李华
网站建设 2026/10/19 5:39:25

MySQL 5.7定时任务实战:用事件调度器自动清理表记录

很多做开发和运维的朋友都遇到过这类需求&#xff1a;业务跑着跑着&#xff0c;某张表的体积肉眼可见地往上涨&#xff0c;查了一下发现全是日志数据、临时缓存或者历史流水&#xff0c;留着碍事&#xff0c;删了又怕以后要查。尤其是 MySQL 5.7 环境里&#xff0c;表动不动几个…

作者头像 李华
网站建设 2026/10/19 5:39:22

纯Python手写CNN:230行跑通MNIST,前向反向全透明

简介&#xff1a;本资源是一份面向深度学习初学者与原理探究者的纯Python卷积神经网络&#xff08;CNN&#xff09;实现方案&#xff0c;不依赖TensorFlow、PyTorch等框架&#xff0c;聚焦底层计算逻辑与反向传播机制的理解。适合高校学生、转行学习者及希望夯实CNN数学基础的开…

作者头像 李华
网站建设 2026/10/19 5:38:46

市场恐慌时如何逆向布局?情绪信号与加仓纪律实战解析

"别人恐惧时我贪婪"这句话流传了几十年&#xff0c;几乎成了价值投资最上口的格言。但这些年我观察到一个很普遍的现象&#xff1a;多数人对它的理解只是停留在"大跌时买进"这个层面。结果要么是接飞刀接到半山腰&#xff0c;要么是在恐慌初期就急着满仓&a…

作者头像 李华
网站建设 2026/10/19 5:38:43

基于YALMIP的微电网日前经济调度MILP建模与求解实践

做微电网优化调度这几年&#xff0c;我打交道最多的工具就是MATLAB里的YALMIP工具箱。它把“建模”和“求解”彻底分开&#xff0c;用接近数学公式的语法描述目标函数和约束&#xff0c;再交给底层求解器去算。这套基于YALMIP的微电网优化调度模型&#xff0c;核心是完成日前经…

作者头像 李华