PHP文件上传漏洞原理以及如何防御详解 【转】

PHP文件上传漏洞
一、漏洞描述 
在本人开发过程中文件上传的功能是很常见的,比如一个游戏平台:

①用户可以上传自己的头像图片,

②用户论坛发表文章时又需要上传图片来丰富自己的文章,

③更有甚者游戏开发用户需要上传APK文件等。文件上传功能是十分重要的,

所以针对这个功能的漏洞就由下面来讨论研究,经过实战例子和跟大家探讨如何防护。

​ 为了方便统一讨论,目前我们就以上传图片功能来做讲解,会由浅到深的进行。

注意:代码中有详细的注释,请新手读者认真阅读。为方便新手读者,代码可能会写的比较原始,有一定基础的读者请自行尝试封装。如有疑问请在评论区留言。谢谢。

第一种漏洞使用:
我们先看一个前端HTML代码上传图片的简易版代码:upload.html

<!DOCTYPE html>
<html>
<meta charset="utf-8">
<title>测试文件上传漏洞</title>
<body>
<!-- 下面是一个上传文件的form表单 -->
<form enctype="multipart/form-data" action="upload_file.php" method="POST" />
选择你要上传的图片:<br>
<input name="upload_file" type="file" /><br><br>
<input type="submit" name="upload" value="上传" />
</form>
</body>
</html>
上面代码的页面效果:

一般开发为了快速实现效果就会写出类似下面的代码:upload_file.php

<?php
// 防止没有任何文件上传报错
if (!isset($_FILES['file01'])) {
echo "Nothing Upload";
return ;
}
// 用一个变量来存储上传文件的object,减少代码。
$file = $_FILES['file'];
// 设定好文件的保存路径
$save_upload_path = "uploads/" . basename($file['name']);
// 开始上传文件
if(!move_uploaded_file($file['tmp_name'], $save_upload_path)) {
echo '您的图片上传失败.';
return;
} else {
echo $save_upload_path . '文件已经成功上传!';
return;
}
 上面的代码就是简单的把上传的文件保存到服务器上。然而并没有做任何的过滤或者防护措施。这是很致命的操作。如果此时我们写一个简单的一句话木马入侵就会很容易操控系统。例如:cmd.php

// 简单的一句话木马
<?php eval($_GET['cmd']); ?>
然后我们利用上面html代码上传到服务器的uploads文件中,此时我们假设访问

http://www.域名.com/uploads/cmd.php?cmd=system('whoami') 

即可触发我们写好的木马。

第一个防护:验证文件类型
我们来动手改下上面uplaod_file.php的文件(下面是新版)

<?php
// 防止没有任何文件上传报错
if (!isset($_FILES['file01'])) {
echo "Nothing Upload";
return ;
}
// 用一个变量来存储上传文件的object,减少代码。
$file = $_FILES['file'];
// 设定好文件的保存路径
$save_upload_path = "uploads/" . basename($file['name']);


$uploaded_name = $file[ 'name' ];//文件名称
$uploaded_type = $file[ 'type' ];//文件类型
$uploaded_size = $file[ 'size' ];//文件大小
// 新增步骤:识别文件类型
if( $uploaded_type !== "image/jpeg" ||
$uploaded_type !== "image/png" )
{
// 这里的图片类型出了png和jpeg外还有很多,例如gif,
// 但是除非你的网站很open,不然建议不要开放gif的上传控制。
echo "Upload false, the file type is not allowed";
return;
}
// 识别文件大小,1MB = 1024*1024B,限制2MB
if($uploaded_size > 1024*1024*2)
{
echo "Upload false, the file size is too large";
return ;
}

// 开始上传文件
if(!move_uploaded_file($file['tmp_name'], $save_upload_path)) {
echo '您的图片上传失败.';
return;
} else {
echo $save_upload_path . '文件已经成功上传!';
return;
}
以上就是就基本的防御,通过判断文件的类型以及文件的大小来达到限制其他文件的上传。但是已经说了这是最简单的,所以是很容易被破解的,下面来介绍第二种情况。

第二种漏洞使用:
有一定基础的PHPer都会做一定基础防御,但是道高一尺魔高一丈。下面我们来介绍一套组合拳:抓包工具:Burpsuite; 中国菜刀:Cknife(Burp Suite 是用于攻击web 应用程序的集成平台,包含了许多工具。)(希望大家能 "善" 用此类工具,心无杂念。)具体如何用这些工具我就不在这里赘述了。

使用上面的工具能帮助入侵者修改文件的类型甚至是文件的大小,以此来骗过系统程序的判断。有的小伙伴可能会想到控制文件的后缀,其实通过Burpsuite抓包依然是可以将.png的文件变成.php文件运行的。

所以我们需要全面防护,但是要记住一句话:安全是相对的,没有任何绝对的安全!

全面防护:最终版upload_file.php
<?php
// 防止没有任何文件上传报错
if (!isset($_FILES['file01'])) {
echo "Nothing Upload";
return ;
}

// 增加请求头token验证,token的生成方法建议大家可以参考JWT做法。
if(!checkToken($_REQUEST[ 'user_token' ]))
{
echo "Nothing Upload";
return;
}

// 用一个变量来存储上传文件的object,减少代码。
$file = $_FILES['file'];
// 设定好文件的保存路径
$save_upload_path = "uploads/";
$uploaded_name = $file[ 'name' ]; // 文件名称
$uploaded_type = $file[ 'type' ]; // 文件类型
$uploaded_size = $file[ 'size' ]; // 文件大小
$uploaded_ext = substr( $uploaded_name, strrpos( $uploaded_name, '.' ) + 1); // 文件后缀名
$end_upload_ext = strtolower( $uploaded_ext );
$uploaded_tmp = $file['tmp_name']; // 临时文件数据
$save_upload_file = md5( uniqid().$uploaded_name ) . '.' . $uploaded_ext;
$temp_path = ( ( ini_get( 'upload_tmp_dir' ) == '' ) ? ( sys_get_temp_dir() ) : ( ini_get( 'upload_tmp_dir' ) ) );
$temp_path .= DIRECTORY_SEPARATOR . md5( uniqid() . $uploaded_name ) . '.' . $uploaded_ext;
// 识别文件类型
if( $uploaded_type !== "image/jpeg" ||
$uploaded_type !== "image/png" )
{
// 这里的图片类型出了png和jpeg外还有很多,例如gif,
// 但是除非你的网站很open,不然建议不要开放gif的上传控制。
echo "Upload false, the file type is not allowed";
return;
}
// 识别文件大小,1MB = 1024*1024B,限制2MB
if($uploaded_size > 1024*1024*2)
{
echo "Upload false, the file size is too large";
return ;
}
// 识别文件后缀名
if( $end_upload_ext !== 'jpg' || $end_upload_ext !== 'jpeg' || $end_upload_ext !== 'png' )
{
echo "Upload false, the file size is too large";
return ;
}

// 开始上传文件
if(getimagesize($uploaded_tmp))
{
// 将图片重新复制一遍,防止有害数据保留下来
if( $uploaded_type == 'image/jpeg' ) {
$img = imagecreatefromjpeg( $uploaded_tmp );
imagejpeg( $img, $temp_path, 100);
}
else if($uploaded_type == 'image/png') {
$img = imagecreatefrompng( $uploaded_tmp );
imagepng( $img, $temp_path);
}
else
{
return ;
}
// 释放图片资源
imagedestroy( $img );
// 临时保存的图片转存到真实路径下
if( rename( $temp_path, __dir__ . DIRECTORY_SEPARATOR . $save_upload_path . $target_file ) ) {
echo "upload file successfully";
}
else {
echo "upload false";
}
// 删除临时图片文件(为防止未清理临时文件前程序崩溃等,建议加多一个守护进程清除,以及增加报错记录)
if( file_exists( $temp_path ) )
{
unlink( $temp_path );
}

return;
}

// 增加csrf防御
runCsrfToken();
upload_file.php文件代码详解:

确保文件数据真实存在
增加请求token,防止跨域攻击
然后通过限制文件的类型、文件的后缀名,以及文件大小来控制上传白名单
uniqid()函数能帮助我们获取微秒级的13位数的时间戳,减少在同一时间上传相同文件名而造成误差的可能。
通过getimagesize()函数判断该文件是否是真实的图片资源。
最后通过将图片重新复制成一个新的图片,去除掉有害的数据,留下真实的图片资源。
最后增加一步防止CSRF攻击

原文地址:https://www.cnblogs.com/11a22b33c/p/13354453.html