常见的文件上传漏洞包括任意文件上传、任意文件下载、任意文件删除以及任意文件读取和写入,本篇内容主要介绍常见的文件上传漏洞涉及到的 PHP 代码的相关部分,旨在帮助我们在阅读 PHP 代码的时候,能够对可能出现漏洞的地方有所了解。
文件上传代码
1.1.1 文件上传的前端代码
文件上传的前端代码较为简单,只要提供基本的文件上传功能,将数据传到 PHP 文件中即可,代码如下:
<!DOCTYPE html>
<meta charset="UTF-8">
<html>
<head>
<title>文件上传示例</title>
</head>
<body>
<h1>文件上传</h1>
<form action="upload.php" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="file">
<input type="submit" value="上传">
</form>
</body>
</html>
注意,此处的<form action="upload.php"
指向的即为文件上传的接口,在渗透测试的过程中,如果发现了目标网站的 PHP 接口,其实是可以自己本地尝试构建文件上传的前端代码,将文件传送到目标端口中去,类似的格式为:<form action="testurl/upload.php" method="post" enctype="multipart/form-data">
前端代码整体显示如下:
1.2 文件上传的后端代码
1.2.1本地开发处理函数
首先我们来看一段构造好的后端 PHP 处理代码,这段代码是根据 MIME 进行文件上传的判断:
<?php
// 检查是否有文件上传
if (isset($_FILES['file'])) {
// 获取文件信息
$file =$_FILES['file'];
$fileName =$file['name'];
$fileTmp =$file['tmp_name'];
$fileError =$file['error'];
// 允许的文件类型(根据 MIME 类型)
$allowMimeTypes = array(
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel',
'text/plain',
// 在这里添加更多的允许 MIME 类型
);
// 获取文件的 MIME 类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $fileTmp);
finfo_close($finfo);
// 检查 MIME 类型
if (in_array($mimeType,$allowMimeTypes)) {
// 如果没有错误,则上传文件
if ($fileError === 0) {
// 设定上传文件的路径
$uploadPath = 'uploads/'.$fileName;
// 保存文件
if (move_uploaded_file($fileTmp,$uploadPath)) {
echo "文件上传成功";
} else {
echo "文件上传失败";
}
} else {
echo "文件上传过程中出现错误";
}
} else {
echo "不允许的文件类型";
}
} else {
echo "没有文件被上传";
}
?>
这段后端代码的处理逻辑为:首先检查是否有文件上传,如果有文件上传就获取文件信息,并且根据文件信息去判断是否属于允许的文件类型,如果属于就上传,如果不属于就不上传。
其中需要关注的有两个问题,一个是 FILE 函数,一个是 MIME 校验
FILE 函数 数据结构如下:
$_FILES['filename'] = array(
'name' => array(string), // 文件名
'type' => array(string), // 文件类型
'size' => array(int), // 文件大小
'tmp_name' => array(string), // 文件的临时存储路径
'error' => array(int) // 错误代码
);
可以注意到我们根据 File函数处理上传的数据包,来判断文件的类型,在编辑的过程中,我们设置的过滤标准是根据 MIME 类型进行过滤,相关代码如下:
// 允许的文件类型(根据 MIME 类型)
$allowMimeTypes = array(
'image/jpeg',
'image/png',
'image/gif',
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel',
'text/plain',
// 在这里添加更多的允许 MIME 类型
);
实际在传输数据包时,我们首先上传一个名为test.php
的文件,其发送数据包和返回数据包如下:
实际上数据包会显示给我们服务器端可以接受的文件 MIME 类型,大体可以知道是根据 MIME 过滤的,此时的绕过方法有将Content-Type
删除,不用做其他任何的修改,这样在服务器端对 MIME 函数的接收为NULL
,由于 in_array() 函数使用宽松比较,如果数组中存在 NULL 值,那么 in_array(NULL, $array) 将返回 true,从而达成了绕过的效果。
再来看一种常见的过滤方法,根据白名单后缀名过滤后端处理代码
<?php
// 定义允许的文件扩展名
$allowed_extensions = array('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg');
// 检查是否有文件上传
if ($_FILES) {
$file =$_FILES['file'];
$file_name =$file['name'];
$file_tmp_name =$file['tmp_name'];
$file_error =$file['error'];
$file_size =$file['size'];
// 获取文件后缀名
$file_extension = strrchr($file_name, '.');
// 检查后缀名是否允许
if (!in_array($file_extension,$allowed_extensions)) {
echo "请上传以下类型的文件:jpg, jpeg, png, gif, bmp, svg";
} elseif ($file_error !== UPLOAD_ERR_OK) {
// 如果文件上传失败
echo "文件上传失败。";
} else {
// 如果文件上传成功
// 确保临时文件存在并且可读
if (is_uploaded_file($file_tmp_name)) {
// 文件上传的处理...
$upload_success = move_uploaded_file($file['tmp_name'], 'uploads/' . $file['name']);
// 检查文件是否成功上传
if ($upload_success) {
echo "文件上传成功。";
} else {
echo "文件上传失败,移动文件时出错。";
}
} else {
echo "文件上传失败,临时文件不存在或不可读。";
}
}
} else {
echo "没有文件被上传。";
}
?>
这里的核心逻辑就是先检查扩展名是否是允许的,如果不是允许的或者上传错误就进行上传失败,然后再进行上传的操作。
我们对比上传test.jpg
和test.php
后缀会发现这种是根据白名单后缀去验证的,实际绕过很困难。
在这种情况下,可行的思路是通过SVG-XSS
去进行漏洞赏金的挖掘或者窃取管理员 Cookie,笔者曾经靠此种思路轻易获取了很不错的赏金漏洞。
其次通过黑名单处理文件上传也是常见的一种处理逻辑(实际现在随着安全意识的提升,不再变多),相关代码如下:
<?php
// 定义允许的文件扩展名
$allowed_extensions = array('.php');
// 检查是否有文件上传
if ($_FILES) {
$file =$_FILES['file'];
$file_name =$file['name'];
$file_tmp_name =$file['tmp_name'];
$file_error =$file['error'];
$file_size =$file['size'];
// 获取文件后缀名
$file_extension = strrchr($file_name, '.');
// 检查后缀名是否允许
if (in_array($file_extension,$allowed_extensions)) {
echo "请不要上传php文件类型";
} elseif ($file_error !== UPLOAD_ERR_OK) {
// 如果文件上传失败
echo "文件上传失败。";
} else {
// 如果文件上传成功
// 确保临时文件存在并且可读
if (is_uploaded_file($file_tmp_name)) {
// 文件上传的处理...
$upload_success = move_uploaded_file($file['tmp_name'], 'uploads/' . $file['name']);
// 检查文件是否成功上传
if ($upload_success) {
echo "文件上传成功。";
} else {
echo "文件上传失败,移动文件时出错。";
}
} else {
echo "文件上传失败,临时文件不存在或不可读。";
}
}
} else {
echo "没有文件被上传。";
}
?>
实际传输数据包时,很容易达成绕过的效果,比如进行大小写绕过即可:
1.2.2 富文本编辑器处理上传
UEditor 是一款由百度前端技术部开源的在线富文本编辑器,它支持所见即所得的编辑模式,并且提供了丰富的功能,如图片上传、表格、代码高亮、多语言支持等。UEditor 设计简洁、功能强大,可以轻松集成到各种Web应用中,特别是PHP开发的Web应用。
在实际的网页中,我们常常会看到某些投稿网站使用 UEditor,如果 UEditor 版本过低,我们可以通过 UEditor 拿到网页的 Webshell,本次实验环境使用的是v1.4.3.3
版本的富文本编辑器。
部署 UEdior 方法如下:
- 下载UEditor: 访问UEditor的GitHub页面(GitHub - fex-team/ueditor: rich text 富文本编辑器)或者官网(GitHub - fex-team/ueditor: rich text 富文本编辑器)下载最新版本的UEditor。下载后,你将得到一个包含ueditor.config.js、ueditor.all.min.js等文件的压缩包。
- 解压UEditor: 将下载的压缩包解压,并将解压后的文件夹上传到你的PHP服务器上的一个合适目录。
- 配置UEditor: 在你的HTML页面中,引入UEditor的JS文件和配置文件。例如:
<script type="text/javascript" src="path/to/ueditor.config.js"></script>
<script type="text/javascript" src="path/to/ueditor.all.min.js"></script>
- 初始化UEditor: 在你的HTML页面中,添加一个<textarea>元素,然后使用JavaScript初始化UEditor。例如:
<textarea name="content" id="content" rows="100" cols="100"></textarea>
<script type="text/javascript">
UE.getEditor('content');
</script>
- 进行文件上传操作,UEdior 编辑器内部的
config.json
文件规定了可以上传的内容和版本,
进行数据发包后会发现一切都正常运行,安装编辑器的处理逻辑执行。当 Ueditor 版本落后时,我们可以成功上传 Webs hell,曾经笔者为一家公司做渗透测试时,这家公司唯一的入口点便是 Ueditor,进入服务器后发现被很多黑客入侵过,可见此种漏洞是中小型公司开发人员最常忽略的漏洞之一。
1.2.3 开发框架进行文件上传
在Web开发中,PHP 的 Laravel框架提供了多种方式来处理文件上传,其中最常用的是使用Storage
facade和UploadedFile
类。
Laravel的Storage
facade提供了简单易用的文件系统操作。在Laravel中,默认的文件存储后端是本地文件系统,但你可以配置其他存储后端,如Amazon S3。
文件上传基本示例
下面是一个简单的文件上传示例:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
Route::post('/upload', function (Request $request) {
if ($request->hasFile('file')) {
$file = $request->file('file');
$filename = $file->getClientOriginalName();
$path = $file->storeAs('public/files', $filename);
// 或者使用Storage facade
$path = Storage::disk('public')->put($filename, file_get_contents($file));
return response()->json(['path' => $path]);
}
});
在这个例子中,我们使用Request
对象的hasFile
方法来检查请求中是否包含文件。如果包含文件,我们使用file
方法获取文件实例,然后使用storeAs
方法将文件保存到公共存储区。
此外 Laravel还提供了UploadedFile
类,它代表了上传的文件。这个类提供了许多有用的方法来处理文件。
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
Route::post('/upload', function (Request $request) {
if ($request->file('file')) {
$file = $request->file('file');
$filename = $file->getClientOriginalName();
$path = $file->storeAs('public/files', $filename);
// 使用UploadedFile的方法
$fileSize = $file->getSize();
$fileMimeType = $file->getMimeType();
// 保存到数据库或其他处理
// ...
return response()->json(['path' => $path]);
}
});
曾经历史上爆出来的 Laravel 漏洞有https://www.ddosi.org/laravel-upload-bypass/,因此开发框架上也会存在文件上传的安全问题,并非使用框架便可以高枕无忧,还是需要及时更新和打补丁去防止落后版本框架的公开漏洞被黑客利用攻击。
总结
文件上传是常见的网络服务提供点,我们面对文件上传的时候,其实思路不只是去拿到Webshell,也可以根据其服务提供的特点,尝试去挖掘XSS或者其他方面的漏洞,不失为一种好的选择,本篇文章对文件上传的PHP代码的基础做了详细的介绍,旨在告诉初学者常见的文件上传类型及原理,开发者是如何部署文件上传服务的,以及如何常见的过滤手法是什么,希望这篇文章能够对初学者有所帮助