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⽂件中,此时我们假设访问
即可触发我们写好的⽊马。
第⼀个防护:验证⽂件类型
我们来动⼿改下上⾯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攻击