CTF实战:利用.phtml绕过文件上传黑名单漏洞
1. 项目概述:从一道CTF题看文件上传漏洞的本质
最近在带新人打CTF靶场,发现很多朋友对文件上传漏洞的理解还停留在“传个一句话木马”的层面,遇到稍微有点防护的题目就无从下手。正好,BUUCTF平台上的ACTF2020新生赛Upload1这道题,就是一个非常经典且教学意义十足的文件上传漏洞实战案例。它不像那些“裸奔”的靶场,而是设置了一道简单的前端验证,逼着你去思考“绕过”这件事。这道题的核心通关点,就在于利用.phtml后缀来绕过常见的黑名单过滤机制,最终拿到服务器权限。
如果你正在学习Web安全,尤其是对文件上传漏洞的绕过技巧感兴趣,那么通过这道题的详细拆解,你不仅能学会如何通关,更能深刻理解黑名单过滤的局限性、.phtml文件在特定环境下的执行原理,以及一套完整的“发现-分析-绕过-利用”实战思路。这远比死记硬背几个Payload有用得多。接下来,我就以这道题为蓝本,带你完整走一遍渗透测试流程,并深入聊聊背后的那些“门道”。
2. 题目环境与初步信息收集
2.1 靶场启动与界面初探
首先,我们需要在BUUCTF平台上找到并启动[ACTF2020新生赛]Upload1这道题目。启动后,你会获得一个临时的靶机地址,通常格式为http://xxx.node4.buuoj.cn:port/。用浏览器访问这个地址,映入眼帘的是一个非常简洁的文件上传界面。
这个界面通常只包含一个文件选择框、一个上传按钮,以及可能的一些提示文字(比如“上传图片”)。从用户体验上看,它和一个普通的头像上传功能没什么区别。但作为安全测试者,我们的第一反应不应该是直接上传文件,而是查看前端代码。按下F12打开开发者工具,查看网页的HTML源码和JavaScript代码。
为什么这一步至关重要?很多入门级的文件上传漏洞防护,仅仅依赖于前端JavaScript进行文件后缀名验证。这种验证对用户是透明的,但对我们来说,绕过它可能就像禁用浏览器JS一样简单。通过查看前端代码,我们可以快速判断验证逻辑是放在前端(客户端)还是后端(服务器端),这直接决定了我们后续的绕过策略。
2.2 前端代码分析与绕过尝试
在本题的HTML源码中,我们很快就能发现端倪。通常,代码中会包含一段类似下面的JavaScript验证函数:
function checkFile() { var file = document.getElementById("file").value; if (file == null || file == "") { alert("请选择要上传的文件!"); return false; } // 定义允许的后缀名 var allow_ext = ".jpg|.png|.gif|.jpeg"; // 提取文件后缀 var ext_name = file.substring(file.lastIndexOf(".")); if (allow_ext.indexOf(ext_name) == -1) { alert("仅允许上传图片文件!"); return false; } }这段代码的逻辑非常清晰:它定义了一个白名单allow_ext(包含.jpg, .png, .gif, .jpeg),然后获取用户选择文件的后缀名进行比对。如果不在白名单内,就弹出警告并阻止表单提交。
绕过方法一:直接禁用JavaScript这是最粗暴有效的方法。在浏览器设置中禁用JavaScript,或者使用Burp Suite这类代理工具拦截请求并直接修改,前端验证就形同虚设了。你可以直接上传一个.php文件,然后通过Burp将请求转发出去。
绕过方法二:修改前端代码在开发者工具的“元素”(Elements)选项卡中,直接找到这个检查函数并将其删除,或者修改allow_ext变量,加上.php,然后正常上传。
注意:在实际渗透测试中,前端验证绕过通常只是第一步,甚至可能是一个“烟雾弹”。出题人故意留下这么明显的前端漏洞,往往意味着后端还有更严格的检查在等着你。所以,即使前端绕过成功,也不要高兴得太早。
2.3 使用Burp Suite进行请求拦截与重放
为了更专业地进行测试,我们通常会使用Burp Suite。配置好浏览器代理后,我们尝试上传一个名为shell.php的文件,内容为一句话木马<?php @eval($_POST['cmd']);?>。
- 拦截请求:点击上传后,Burp Suite的Proxy模块会拦截到这个HTTP POST请求。
- 观察请求:你会看到请求体中包含了文件内容,
Content-Disposition头部中指明了文件名filename="shell.php"。 - 尝试直接放行:如果我们直接Forward这个被前端JS拦截前就被Burp捕获的原始请求(此时还未经过JS验证),会发生什么?很多时候,后端会返回一个错误,比如“文件类型不允许”。这证明了后端也存在验证。
- 重放与测试:我们将这个请求发送到Burp的Repeater模块。在Repeater中,我们可以随意修改
filename参数,比如改为shell.jpg,然后发送请求,观察响应。通过对比修改前后端的不同响应,我们可以一步步摸清后端过滤器的规则。
在本题目中,当你将.php改为.jpg发送后,可能会发现上传成功了,但访问上传后的文件却显示的是图片的二进制乱码,或者直接提示“图片无法显示”。这说明服务器虽然允许.jpg文件上传,但并没有把它当成PHP脚本来执行。我们的任务,就是找到一个既能通过后端验证,又能被服务器当作PHP代码来执行的文件名。
3. 核心漏洞原理:黑名单过滤与.phtml的妙用
3.1 什么是黑名单与白名单?
文件上传的防护逻辑,主要分为两种:黑名单(Blacklist)和白名单(Whitelist)。
- 黑名单:明确禁止某些危险的后缀名上传,比如
.php,.asp,.jsp,.exe等。它的思想是“除了已知危险的,其他都允许”。- 优点:配置相对简单。
- 缺点:防不住未知的危险后缀,或者名单遗漏。
.phtml绕过就是利用了这个缺点。
- 白名单:只允许某些安全的、预期的后缀名上传,比如
.jpg,.png,.pdf等。它的思想是“只允许已知安全的,其他都禁止”。- 优点:安全性远高于黑名单。只要名单收得紧,几乎无懈可击。
- 缺点:业务逻辑可能更复杂,需要严格校验文件内容。
ACTF2020新生赛Upload1这道题,从后端的行为来看,采用的是一种不完善的黑名单机制。它可能禁止了.php,但遗漏了其他一些也能执行PHP代码的后缀。
3.2 为什么.phtml可以执行PHP代码?
这才是本题的技术核心。.phtml后缀并不常见,但它确实在某些特定的Web服务器配置下,可以被解析为PHP脚本。
- 历史渊源:在早期,
.phtml(有时是.php3,.php4,.php5)是PHP文件的一种可选扩展名。有些管理员或老旧系统为了区分纯PHP文件和混合了大量HTML的PHP文件,会使用.phtml。 - 服务器配置:Apache服务器通过
AddType指令或<FilesMatch>在配置文件中关联特定后缀与PHP解析引擎。一个典型的配置可能如下:
这行配置告诉Apache,凡是遇到AddType application/x-httpd-php .php .phtml .php3.php,.phtml,.php3结尾的文件,都交给application/x-httpd-php(即PHP解析模块)来处理,而不是直接将其文本内容返回给浏览器。 - 本题环境:出题人显然搭建了一个特意配置了解析
.phtml的Apache+PHP环境。当黑名单只过滤了.php时,上传.phtml文件就能同时满足两个条件:绕过黑名单检查+被服务器当作PHP执行。
3.3 其他可能被遗漏的后缀
除了.phtml,不完善的黑名单还可能遗漏很多“变种”后缀,这取决于服务器配置。了解这些有助于你在其他题目或真实场景中拓宽思路:
- 大小写绕过:
Php,PHP,pHp(在Windows服务器上,文件名通常不区分大小写)。 - 双写绕过:
p.phphp(如果过滤逻辑是简单删除字符串”php”,删除后剩下p.hp,可能结合其他漏洞)。 - 点号、空格绕过:
shell.php.或shell.php(在某些系统处理文件名时,末尾的点或空格会被自动去除)。 - 特殊解析绕过:
.php5,.php4,.php3,.pht(这些是PHP历史版本或变种后缀)。 - 嵌套后缀:
shell.jpg.php(如果只检查最后一次点号后的后缀,可能被绕过;但现代防护通常会检查整个文件名)。 - 利用服务器解析特性:
shell.php.jpg(配合Apache的AddHandler错误配置,可能导致文件被解析为PHP)。这在本题不适用,但属于重要知识点。
实操心得:在测试时,不要只盯着一个.phtml。我会习惯用一个Burp Intruder(入侵者)模块,加载一个包含几十种常见绕过后缀的字典(如php, php3, php4, php5, phtml, pht, jsp, asp, aspx...以及它们的大小写变体),对filename参数进行模糊测试,快速探测目标系统的黑名单范围。
4. 完整通关步骤详解
掌握了原理,我们来一步步完成这道题。
4.1 准备攻击载荷
我们需要的不是一个简单的.phtml文件,而是一个包含能够执行系统命令或读取文件代码的Webshell。
最简单的PHP Webshell:
<?php @eval($_POST['cmd']);?>这是一句话木马,通过POST传递cmd参数执行任意PHP代码。但它过于敏感,很多WAF或简单的关键词过滤会拦截eval、$_POST等函数。
更隐蔽的测试载荷:对于CTF题目,我们通常需要读取服务器上的特定文件(比如flag)。因此,可以使用更直接的文件操作函数:
<?php echo system('cat /flag'); ?>或者,为了更通用地测试命令执行:
<?php if(isset($_GET['c'])){ system($_GET['c']);} ?>这样,我们上传后访问shell.phtml?c=ls就能列出目录。
本题专用载荷:由于题目目标是获取flag,我们可以直接将读取flag的命令写入:
GIF89a <?php echo file_get_contents('/flag'); ?>这里开头的GIF89a是GIF图片的文件头。这是一种简单的文件内容欺骗技巧。如果后端不仅检查后缀,还通过exif_imagetype()或getimagesize()函数检查文件内容是否为图片,添加这个文件头有可能绕过内容检查。虽然本题可能不需要,但这是一个好习惯。
我们将上述代码保存为一个文本文件,然后将其重命名为shell.phtml。
4.2 实施上传绕过
- 打开Burp Suite,配置代理,浏览器设置好代理。
- 访问靶场地址,在文件上传界面选择我们制作好的
shell.phtml文件。 - 点击上传,此时Burp会拦截到请求。
- 在Burp Proxy中观察请求。关键的部位是请求体(Body)部分,你会看到:
------WebKitFormBoundaryxxx Content-Disposition: form-data; name="file"; filename="shell.phtml" Content-Type: text/html GIF89a <?php echo file_get_contents('/flag'); ?> - 直接Forward这个请求。因为前端JS可能只检查了
.php,对.phtml放行。如果前端JS白名单里没有.phtml,我们可能需要先按2.2节的方法禁用或绕过JS。 - 观察服务器响应。如果上传成功,页面通常会返回上传文件的存储路径,比如:
务必记录下这个完整路径,它是我们访问Webshell的URL。文件上传成功!路径:uploads/shell.phtml
4.3 访问Webshell并获取Flag
假设上传成功后的路径是http://xxx.node4.buuoj.cn:port/uploads/shell.phtml。
- 直接在浏览器中访问这个URL。
- 如果我们的代码是
<?php echo file_get_contents('/flag'); ?>,那么页面应该会直接显示出flag的内容,格式可能为actf{xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}。 - 如果使用的是带参数的一句话木马,则需要使用中国蚁剑、冰蝎这类Webshell管理工具连接,或者手动构造POST请求。对于本题,直接输出flag的代码是最快捷的。
常见问题排查:
- 访问后显示源代码或下载文件:这说明服务器没有将
.phtml配置为PHP解析。请检查你是否正确使用了.phtml后缀,或者尝试其他备用后缀(如.php5)。 - 页面空白或报错:检查你的PHP代码语法是否正确。在本地PHP环境中测试无误后再上传。
- 上传路径找不到:仔细查看上传成功后的提示信息,路径可能有哈希或时间戳前缀。使用Burp Repeater多次测试并观察响应。
4.4 利用中国蚁剑进行可视化连接(备选方案)
如果题目需要进一步的目录浏览或交互,使用工具更高效。
- 假设我们上传的是一句话木马:
shell.phtml,内容为<?php @eval($_POST['pass']);?>。 - 打开中国蚁剑,右键点击“添加数据”。
- URL地址栏填写完整的Webshell地址:
http://xxx.node4.buuoj.cn:port/uploads/shell.phtml。 - 连接密码填写
pass(与POST参数名一致)。 - 编码器选择
default(默认)。 - 点击“添加”。如果一切正常,左侧会列出服务器目录结构。
- 通常flag文件位于根目录
/、/var/www/html或当前目录下,文件名可能是flag、flag.txt、flag.php等。直接双击打开即可查看。
5. 深度防御绕过技巧与实战扩展
通关一道题是基础,更重要的是举一反三。文件上传漏洞的防御和绕过是“矛与盾”的持续对抗。
5.1 进阶绕过技巧:组合拳
现实中,防御措施往往是多层的。我们需要思考如何组合使用技巧。
后缀名绕过 + 内容欺骗:
- 场景:后端同时检查后缀名(黑名单)和文件内容(要求是图片)。
- 方法:制作一个图片马。用
copy命令(Windows)或cat命令(Linux)将一个真实图片和一个PHP Webshell合并。- Windows:
copy normal.jpg /b + shell.php /a webshell.jpg - Linux:
cat normal.jpg shell.php > webshell.jpg
- Windows:
- 上传这个
webshell.jpg。然后利用文件包含漏洞或解析漏洞(如Apache的test.jpg.php错误解析),让图片中的PHP代码得以执行。这在没有.phtml这样的解析后缀时是常用思路。
修改HTTP请求头:
- 场景:后端通过
Content-Type字段验证文件类型。 - 方法:Burp拦截上传请求,将
Content-Type: application/octet-stream修改为Content-Type: image/jpeg。
- 场景:后端通过
利用WAF/过滤器的逻辑缺陷:
- 场景:过滤函数递归删除或替换“php”字符串。
- 方法:双写绕过。上传文件名为
shell.pphphp,如果过滤器将php替换为空,则第一次替换后变为shell.ph,可能无法识别;或者设计为递归删除直到没有php为止,最终变为shell.。这需要根据响应判断过滤逻辑。
5.2 防御视角:如何构建更安全的上传功能
作为开发者,从这道题里应该学到如何避免此类漏洞:
- 强制使用白名单:这是最重要的原则。只允许
.jpg,.png,.gif等业务必需的后缀。名单要短且明确。 - 文件内容校验:不仅检查后缀,还要用可靠的服务端库(如GD库、ImageMagick)验证文件内容确实是所声称的格式。防止图片马。
- 重命名文件:上传后,使用随机算法(如时间戳+MD5)生成新的文件名,并彻底丢弃用户上传时的原始文件名。这可以防止所有基于文件名的绕过。
- 控制文件权限:上传目录设置为不可执行脚本。在Apache中,可以通过
.htaccess文件设置php_flag engine off。在Nginx中,确保上传目录的location块不传递给PHP-FPM。 - 文件存储位置:尽量不要让用户能直接通过Web URL访问上传的文件。如果必须访问,可以通过一个单独的脚本(如
download.php?id=xxx)来读取和输出文件,并在脚本中做严格的权限和类型检查。 - 使用云存储或独立服务:将文件上传到OSS、AWS S3等对象存储服务,这些服务通常有更完善的安全机制。
5.3 在BUUCTF及其他靶场中的实战联想
做完这道题,你可以顺藤摸瓜,在BUUCTF上挑战其他文件上传题目,巩固技能:
- [极客大挑战 2019]Upload:可能需要结合前端绕过、内容欺骗和
.user.ini文件利用。 - [SUCTF 2019]CheckIn:经典的
.user.ini与图片马组合利用。 - Upload-Labs:这是一个专门的文件上传漏洞靶场项目,包含了从简单到困难的20关,涵盖了前端验证、MIME类型验证、黑名单、白名单、条件竞争、图片马、解析漏洞等几乎所有常见考点,是系统学习的最佳选择。
我个人在实际操作中的体会是,文件上传漏洞的利用过程就像一场“猜谜游戏”。你需要根据服务器的每一次响应(错误信息、成功信息、返回路径)来不断调整你的Payload。保持耐心,系统性地测试每一种可能(后缀、内容、头部),并深刻理解每一层防御措施背后的原理,才能真正做到“以不变应万变”。这道Upload1题目就是一个完美的起点,它用最简单的设置,引出了黑名单过滤这个最核心的弱点。记住.phtml,更要记住这种“发现过滤规则-寻找规则遗漏-构造有效载荷”的通用渗透测试思维。