ACTF2020新生赛的Upload 1,算是我入门Web安全时印象很深的一道题。题目本身不复杂,但把“文件上传漏洞”这条链路上最常见的三个环节全串起来了:找到上传点、绕过文件校验、用一句话木马配合蚁剑拿到执行权限。很多新手卡在这类题上,不是不知道要传马,而是搞不清楚每一步到底在过什么检测、为什么这么改就能过、蚁剑连不上的时候又该查哪里。这篇文章就按我当时做题的思路,从拿到题目的第一眼开始,一步步拆到最终拿到Flag,顺手把文件上传题最常见的考点和排查技巧都整理出来。适合刚接触CTF Web方向、或者正在刷upload题型的同学参考,老手也可以跳着看后面的避坑部分。
1. 拿到题目先做什么:信息收集与功能定位
很多人上手就是传一个PHP文件,被拦了就开始乱试后缀,这种“盲试法”效率很低。我习惯先花两分钟把题目的“功能边界”摸清楚:这个页面到底提供了什么功能,它接受什么、拒绝什么,报错信息长什么样。把这三件事搞清楚,后面的绕过路径基本就清晰了。
1.1 确认入口和功能边界
打开靶场页面,通常就是一个文件上传表单,可能还附带一个“上传成功”之后的提示或跳转。第一步不是急着选文件,而是先做三件简单的事:
- 上传一张正常的图片(JPG/PNG/GIF都行),观察返回结果
- 上传一个扩展名很离谱的文件(比如 .txt、.html),看提示是否一致
- 用浏览器开发者工具看Network面板,确认请求发到了哪个接口、用了什么HTTP方法
这一步的价值在于建立“基准线”。正常图片是能传的,那就说明上传功能本身是通的;不同的失败提示,能帮你判断前后端过滤的类型。比如前端直接弹窗提示“只允许上传图片”,说明校验在浏览器里就做了;如果页面刷新后显示“上传失败,文件类型不允许”,那基本是后端在拦。
这个阶段不要急着想绕过的姿势,先记住三个信息:上传接口的URL、上传后文件存放的路径、失败提示的文案。这三个信息后面每一步都需要用。
1.2 文件上传题的常见考点清单
做多了会发现,文件上传题翻来覆去就考那么几个点。我把常见的检测方式和对应的绕过思路整理成一个表,方便对照:
| 检测类型 | 常见实现方式 | 绕过思路 |
|---|---|---|
| 前端JS校验 | 在浏览器里判断扩展名 | Burp抓包改包,直接绕过前端 |
| 后端扩展名过滤 | 黑名单或白名单判断后缀 | 尝试可用后缀、大小写、空格、.htaccess等 |
| MIME类型校验 | 检查Content-Type | 抓包改成image/jpeg等图片类型 |
| 文件头校验 | 读取文件前几个字节 | 在脚本前加上GIF89a等图片头 |
| 文件内容检测 | 过滤<?php等关键字 | 用短标签、编码、拼接等方式变形 |
| 解析规则限制 | 服务器对不同后缀解析策略不同 | 换Apache/IIS/PHP支持的解析后缀 |
新生赛的题一般不会把上面所有检测都叠在一起,通常组合两三种,比如“前端JS校验 + Content-Type校验 + 文件头校验”,或者“后端黑名单扩展名 + MIME校验”。所以当你发现一种绕过方式无效时,别慌,按这个清单逐项排除,很快能找到卡点。
1.3 这道题的初步判断
回到ACTF2020新生赛的Upload 1,我记得当时最直观的感受是:这是一个典型的新手向上传题,没有特别偏门的绕过姿势,重点就是把“上传校验”这关走通。从题目名称里也能猜到,它没有考解析漏洞之类的进阶内容,核心就是文件上传 + 一句话木马 + 蚁剑连接三件套。
所以当时的思路很明确:先用Burp Suite把请求拦下来,观察上传的POST数据长什么样;接着试探服务器对扩展名的过滤规则;再确认上传文件的路径;最后用蚁剑连上去找Flag。整个过程不需要写特别复杂的脚本,手工就能完成。下面每个环节我都会拆开细讲,包括我踩过的坑。
2. 一句话木马:先搞懂你的武器
蚁剑连接的前提是服务器上已经有一个“能让你执行代码”的东西,这就是一句话木马。很多新手会把它想得很玄乎,其实原理一句话就能讲清楚:服务器在解析PHP文件时,如果遇到eval()这类函数,就会执行括号里的字符串参数。我们上传一个内容极简的PHP文件,里面只有一个eval(),再通过POST参数传入我们想执行的命令,就实现了对服务器的控制。
2.1 一句话木马的执行原理
以最经典的PHP一句话为例:
<?php @eval($_POST['cmd']); ?>这行代码干了什么呢?$_POST['cmd']是从HTTP请求中读取一个名为cmd的参数值,eval()把这个值当成PHP代码执行,开头的@是用来抑制错误输出的。当攻击者用蚁剑这类工具连接时,蚁剑会在POST数据里带上cmd=...,参数值是一段精心构造的PHP代码,用来实现文件列表、读取文件、执行命令等功能。
这里面有个容易忽略的细节:eval执行的不是系统命令,而是PHP代码。所以蚁剑并不是直接把ls、cat这样的Shell命令塞进cmd,而是先通过PHP代码在服务器端拼出一个小型“执行框架”,再在这个框架之上执行具体操作。不理解这一点,后续排查问题时容易走弯路。
2.2 新生赛写什么样的马最合适
在新生赛场景下,最稳妥的就是经典原版一句话,简单直接:
<?php @eval($_POST['cmd']); ?>为什么推荐用这个而不是各种免杀变体?因为新生赛的过滤规则通常不查内容,只要后缀能解析成PHP,这个马就能用。过度加密反而可能带来两个问题:一是有些加密写法依赖特定PHP版本,靶机环境未必支持;二是蚁剑连接时对某些变形马的兼容性也不一定好,调试起来浪费时间。
不过有一点要注意:参数名、加密器和连接密码必须和蚁剑里配置的一致。如果一句话里写的是$_POST['cmd'],蚁剑的连接密码就要填cmd,填成shell或者x那就连不上。这个看似低级的错误,实际做题时出现频率非常高。
2.3 怎么验证上传是否成功
上传操作本身成功,不代表一句话木马就能用。中间还隔着“服务器是否把该文件当PHP执行”这一关。我通常分两步验证:
- 上传成功后,在浏览器里直接访问这个文件的URL
- 如果页面是空白,大概率说明PHP已经执行了(因为eval在等待POST参数,没有输出内容);如果浏览器直接显示文件源码,说明服务器没有解析PHP,只是把这个文件当普通文本返回了
这里有一个容易误导新手的点:很多同学看到“上传成功”四个字就兴冲冲去开蚁剑,结果连不上,回头才发现上传后的文件名被服务器改了,或者路径和预想的不一样。所以拿到上传成功的响应后,先仔细看它返回了什么信息,很多题目会在响应里直接告诉你文件保存的路径和文件名。如果响应里没有路径,那就需要去常见目录里探一下(这个我在后面实操部分细讲)。
3. 蚁剑:连接配置与避坑手册
蚁剑(AntSword)是国内安全圈使用很广的一款开源跨平台WebShell管理工具,界面友好,功能也比老牌的“菜刀”完善不少。它本质上是一个桌面客户端,通过加载不同的“解码器”和“编码器”来与不同形式的一句话木马通信。CTF里绝大多数PHP一句话场景,蚁剑默认配置就能直接连上。
3.1 为什么选蚁剑
我接触过的WebShell管理工具有好几款,蚁剑是我在CTF和实战测试里用得最多的。原因有三:
- 跨平台:Windows、macOS、Linux都能跑,不挑系统
- 插件化:编码器、解码器、插件机制都很完善,遇到特殊变形的一句话可以临时加载对应模块
- 生态好:文档和社区资料多,遇到问题搜索解决方案方便
相比之下,菜刀已经很多年不更新,对HTTPS和PHP 7+的兼容性也一般;冰蝎的流量特征和加密方式更适合实战场景,在CTF新生赛里反而属于“杀鸡用牛刀”。所以我的建议是:先把蚁剑用熟练,它的默认功能足以覆盖绝大多数题目需求。
3.2 安装和初始化的坑
蚁剑的安装本身不复杂,但从GitHub Releases页面下载对应系统的压缩包,解压后需要先初始化。这里有两个常见的坑:
- 初始化过程需要联网加载一些依赖,如果网络环境不好,初始化界面会一直转圈,看似“卡死”。解决方法是多试几次,或者手动下载依赖包放到指定目录
- 运行时如果提示缺
node或npm相关组件,说明系统环境缺依赖,按提示安装即可。新版蚁剑是Electron封装,对系统版本有一定要求,太老的系统可能跑不起来
装好之后先不要急着连题目,可以本地搭一个PHP环境,放一个一句话木马文件,先用蚁剑连本地测试一下,确认工具本身没问题。这样等真正做题时,出现的任何连接失败都能确定是题目原因,而不是工具没装好。
3.3 连接配置的关键参数
蚁剑的“添加数据”里,有几个参数需要特别注意:
- URL:必须精确到可访问的脚本文件,比如
http://target.com/uploads/shell.php,而不是只写到目录 - 连接密码:要和一句话木马里的POST参数名一一对应。比如
$_POST['cmd'],密码就填cmd - 连接类型:选
PHP,蚁剑会根据这个类型选择对应的解码器 - 编码器/解码器:默认配置即可,后面遇到特殊过滤再调整
我自己踩过的一个坑是URL结尾带了多余的空格或者斜杠,蚁剑会报“404”或者“URL格式错误”,排查半天结果就是多了个字符。后来我习惯先复制URL到浏览器里访问一遍,确认能打开再粘贴到蚁剑里。
3.4 连接失败最常见的原因
蚁剑连接失败,90%以上是下面几个原因:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 404 | URL路径不对,文件不存在 | 确认上传路径和文件名,浏览器先访问试试 |
| 403 | 目录禁止列目录,或文件无权限 | 检查是否有其他路径,考虑上传到可访问目录 |
| 500 | PHP语法错误或组件缺失 | 检查一句话木马是否被截断、编码是否正确 |
| 连接成功但列不了目录 | 密码不对,或者解析器类型不对 | 核对密码,尝试更换编解码器 |
| 响应为空 | eval被禁用或过滤 | 换assert等方式,或用其他变形一句话 |
遇到连接失败,我的习惯是先回到浏览器,用URL直接访问马文件,把页面返回的内容作为判断依据。很多时候“蚁剑连不上”的真相是文件压根没被解析成PHP,那就不是蚁剑的配置问题,而是上传环节还没有完全打通。
4. 完整实操:绕校验、传马、连蚁剑、拿Flag
前面把三个核心概念都铺垫完了,现在进入实战。我按当时做题的时间线,把从第一次提交文件到最终拿到Flag的全过程写出来。这里我不会虚构题目的具体返回文案,因为每套靶场可能略有差异,但操作路径和判断逻辑是完全通用的。
4.1 用Burp确认拦截点位置
先用Burp Suite打开代理,浏览器访问靶场,随便选一张图片上传,抓到上传请求。请求里最关键的部分是multipart/form-data格式下的文件内容,格式大致如下:
POST /upload.php HTTP/1.1 Host: target.com Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ------WebKitFormBoundary7MA4YWxkTrZu0gW Content-Disposition: form-data; name="file"; filename="test.png" Content-Type: image/png PNG文件二进制内容... ------WebKitFormBoundary7MA4YWxkTrZu0gW--这里有两个字段是后面绕校验的核心:一个是filename="test.png",它传递的是文件名,后端如果做扩展名过滤,看的通常是这里;另一个是Content-Type: image/png,它传递的是MIME类型,后端如果做MIME过滤,看的是这里。
正常图片上传成功后,再上传一个PHP文件,如果页面直接弹窗提示文件类型不允许,说明有前端校验。前端校验的时候数据包根本不会发到服务器,所以用Burp把请求拦下来,把filename改成test.png、Content-Type改成image/png,再放行,就能绕过前端限制。这一步几乎所有文件上传题都会用到,务必熟练掌握。
4.2 后缀黑名单和白名单的绕过尝试
绕过前端之后,真正的考验是后端过滤。以新生赛常见套路为例,后端很可能维护了一个黑名单,禁掉了php、php3、php5、phtml这些常见PHP后缀,但对其他后缀没做严格限制。
我当时把一句话木马分别改成shell.php3、shell.php5、shell.phtml挨个试,其中一个成功上传了。这里要注意,不是后缀看着像PHP就一定能解析,具体能解析哪些后缀取决于服务器用的中间件和PHP配置。Apache通常是AddType或AddHandler指令决定哪些后缀会交给PHP处理;Nginx则看fastcgi_param的配置。CTF靶场一般会特意开放其中一个可执行后缀作为通关路径,所以逐个尝试是最高效的办法。
如果黑名单拦得比较死,还可以试试大小写绕过,比如shell.Php、shell.pHp。有些后端过滤用的是in_array(strtolower($ext), blacklist),那大小写就没用;如果过滤逻辑是str_replace('php', '', $ext),那shell.pphphp反而能变成shell.php。命名虽绕,但本质都是猜它过滤逻辑的写法。
4.3 文件头与MIME双伪装
如果后端除了扩展名,还检查了文件内容和MIME类型,就要把木马伪装成图片。常见的做法是在PHP代码前加上图片文件头,比如:
GIF89a <?php @eval($_POST['cmd']); ?>同时把Content-Type改成image/gif。这样对于只读取文件前几个字节的校验,这个文件看起来就是一张GIF图;对于检查MIME类型的校验,数据包里的Content-Type也足够以假乱真。
但这里要注意一个进阶坑:如果后端采用了“图片二次渲染”,也就是用imagecreatefromjpeg()之类的函数重新生成图片,那么你塞在文件末尾的PHP代码会被直接丢弃,因为渲染后的图片只保留了图像数据。解决办法是用工具把一句话木马写入图片的EXIF信息,或者用脚本把PHP代码嵌入到图片的像素数据中,保证渲染后代码依然存在。新生赛很少考到二次渲染,但如果后续在upload-labs里刷题会经常遇到,这里先有个概念。
4.4 路径定位与连接
上传成功只是第一步,接下来得知道文件被保存到了哪里。常见的位置有upload/、uploads/、files/,文件名可能保持原名,也可能被重命名为时间戳或随机字符串。如果上传接口的响应里直接给出了路径,那最省事;如果没有,有几种办法:
- 用浏览器开发者工具看Network面板,上传成功后有没有额外的跳转或接口返回
- 尝试常见目录名拼接,比如
uploads/shell.php3、upload/shell.php3 - 如果存在目录遍历或文件列表,直接浏览上传目录
我当时是直接试了几个常见路径,最后在uploads/目录下找到了文件。打开浏览器访问http://target.com/uploads/shell.php3,页面是空白的,这说明PHP已经执行了,当前文件具备被当作PHP解析的条件。到这个节点,蚁剑才有连接的基础。
在蚁剑里添加数据,URL填完整路径,密码填cmd,类型选PHP,点击测试连接。如果返回成功,文件管理的界面里就能看到服务器的目录结构了。
4.5 获取Flag
连接成功后,Flag的位置一般逃不过这几个地方:
- 当前目录下,直接叫
flag.php、flag.txt - 网站根目录(
/var/www/html/)下 - 服务器临时目录或者上一级目录
用蚁剑的文件管理器逐个目录看过去,找到可疑文件后用自带的编辑器打开,或者右键“终端”执行cat /path/to/flag。如果Flag不在上述位置,还可以用终端命令全盘搜索特定文件名,比如:
find / -name "*flag*" 2>/dev/null这里有个小技巧:很多题目把Flag放在PHP文件里,直接双击打开会看到一堆源码,别被绕晕了,找flag{...}格式的字符串就行。蚁剑的文件编辑器右上角可以直接搜索文件内容,非常方便。
5. 卡住了怎么办:问题排查与考点复盘
做题的时候,卡住才是常态。这一节我把自己刷文件上传题时最常遇到的卡点和排查思路整理出来,同时也总结一下这道题如果变了皮,应该往哪个方向想。
5.1 报错速查表
| 现象 | 可能的卡点 | 优先排查项 |
|---|---|---|
| 上传时前端弹窗拦截 | 前端JS校验 | 用Burp改包绕过,别在前端纠缠 |
| 上传后提示“类型不允许” | 后端MIME校验 | 改Content-Type为图片类型 |
| 上传成功但访问404 | 路径/文件名变了 | 找上传响应或扫描上传目录 |
| 访问文件显示源码 | 服务器未解析该后缀 | 换可执行后缀,如php3、phtml |
| 蚁剑连接成功但列目录失败 | 密码或编码器不匹配 | 核对密码,切换默认解码器重新连接 |
| 提示“密码错误” | 连接配置不一致 | 确认一句话里的参数名和蚁剑密码一致 |
5.2 从“上传成功”到“蚁剑连接成功”之间还有几道关
很多新手以为“上传成功 = 成功getshell”,其实中间还隔着两关。第一关是文件是否被当成PHP解析,验证方法就是浏览器访问,看返回的是空白还是源码。第二关是一句话木马能否被蚁剑正确识别执行,验证方法是把一句话内容单独拿出来在本地PHP环境测试一次,确认代码本身没毛病。
我见过不少同学卡在“上传成功但蚁剑报404”这个问题上,最后发现是访问路径写错了。记住一点:蚁剑的URL必须指向具体文件,不是目录。服务器如果对目录启用了默认索引页或者伪静态,你写目录也可能返回200,但连接的并不是你的木马文件,自然无法执行。
5.3 如果题目改了:常见变体提醒
同样是文件上传题,很多平台会做变体,多知道几种思路会从容很多:
- 过滤了
php关键字但保留.htaccess可用:可以上传一个.htaccess文件,把某个图片后缀的解析方式改成PHP - 白名单只允许
.jpg且扩展名严格校验:配合.htaccess或者服务器解析漏洞,让图片文件被当作脚本执行 - 上传后文件名随机化、不可预测:这时候需要对目录做一次简单的路径探测
- 上传点不止一个,有的是头像上传、有的是附件上传,校验强度可能不同
这些变体在upload-labs里都有对应关卡,刷题时遇到可以回头对照。
6. 题外话:这套手法在真实防御里的对应关系
写了这么多绕过姿势,最后想从另一个角度聊聊:这些攻击手法对应的防御措施到底是什么。理解了防守方会怎么做,反过来也能帮你更深刻地理解攻击链路。
6.1 攻击链总结
一次完整的文件上传攻击,链路其实是这样:
- 找到上传入口
- 构造一个能在目标服务器上执行的脚本文件(一句话木马)
- 绕过各类文件校验,让这个文件成功上传
- 保证服务器会把该文件解析成脚本(而不是下载或直接显示源码)
- 用WebShell管理工具连接,获取文件管理和命令执行能力
- 在服务器上翻找敏感文件
防守方只要在任意一个环节卡死,攻击就断了。所以你在做题时遇到的每一个校验,所对应的就是真实系统里的一道防线。
6.2 防御建议
从防守方视角,我总结了几条最实用的加固建议:
- 上传目录和脚本执行目录分开,上传目录禁止执行PHP
- 文件名统一重命名为随机字符串,不保留用户原始文件名
- 后缀校验用白名单,而不是黑名单;黑名单永远堵不干净
- 同时校验MIME类型、文件头、文件大小,多层过滤
- 对图片使用二次渲染,丢弃非图像数据
- 上传目录权限设置为不可写、不可执行
你会发现,真实防御往往不是靠某一个点扛住攻击,而是“纵深防御”。这也是为什么题目里经常叠加多种过滤,每一项都过,才能走到最后一步。
6.3 学习路线的建议
如果你刚接触这类题,我建议按这个顺序刷:先刷upload-labs的前五关,把前端校验、MIME校验、黑名单扩展名、白名单扩展名、.htaccess这几类基础绕过全部过一遍;再回头做ACTF2020这类新生赛综合题,体会组合校验的感觉;最后再研究图片马和二次渲染这类进阶姿势。这样循序渐进,比一上来就研究免杀Shell要扎实很多。
最后的一点体会
训练的时候,最值得关注的其实不是“最终有没有打通”,而是每一步里你对“为什么要这样做”的理解。比如:为什么改一下Content-Type能过?因为后端只信了这个头。为什么换php3能过?因为服务器把这些后缀都交给了PHP解析。把这些“为什么”串起来,你会发现文件上传题翻来覆去就那几件事:让文件传上去,让文件被解析,让代码能执行。想通了这条线,后面遇到再复杂的题目,心里都不会慌。