文件上传漏洞是我们平时渗透过程中经常利用的漏洞,利用文件上传我们可以直接得到 webshell,是非常直接的攻击方式。写这篇文章主要是想对常见的文件上传检测和绕过进行总结,同时练习文件上传 php 代码的编写。
以下上传测试使用的 HTML 表单的代码为:
<html>
<head>
<title>File Upload</title>
<meta charset="utf-8">
</head>
<body>
<form action="upload.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="1000000">
选择文件: <input type="file" name="myfile">
<input type="submit" value="Upload">
</form>
</body>
</html>
1.1 前端 JavaScript 检测
前端一般都是使用 js 来限制我们的上传类型和文件大小,这里以 upload-labs Pass-01 的源码为例:
function checkFile() {
var file = document.getElementsByName('upload_file')[0].value;
if (file == null || file == "") {
alert("请选择要上传的文件!");
return false;
}
//定义允许上传的文件类型
var allow_ext = ".jpg|.png|.gif";
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf("."));
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name + "|") == -1) {
var errMsg = "该文件不允许上传,请上传" + allow_ext + "类型的文件,当前文件类型为:" + ext_name;
alert(errMsg);
return false;
}}
对于前端的检测我们可以抓包来修改文件类型,也可以禁用掉 JavaScript。总之,只有前端的限制是非常不安全的,非常容易被绕过。
2.1 后端检测文件类型
2.1.1 检测 content-type
后端代码大致为:
<?php
$allow_content_type = array("image/gif", "image/png", "image/jpeg");$path = "./uploads";$type = $_FILES["myfile"]["type"];if (!in_array($type, $allow_content_type)) {
die("File type error!<br>");} else {
$file = $path . '/' . $_FILES["myfile"]["name"];
if (move_uploaded_file($_FILES["myfile"]["tmp_name"], $file)) {
echo 'Success!<br>';
} else {
echo 'Error!<br>';
}}
?>
绕过方法:
抓包将 content-type 改为图片形式(即 ‘image/png’ 等),即可成功上传
2.1.2 检测文件头判断文件类型
后端代码大致为:
<?php
$allow_mime = array("image/gif", "image/png", "image/jpeg");$imageinfo = getimagesize($_FILES["myfile"]["tmp_name"]);$path = "./uploads";if (!in_array($imageinfo['mime'], $allow_mime)) {
die("File type error!<br>");} else {
$file = $path . '/' . $_FILES["myfile"]["name"];
if (move_uploaded_file($_FILES["myfile"]["tmp_name"], $file)) {
echo 'Success!<br>';
} else {
echo 'Error!<br>';
}}
?>
此时虽然检查的也是文件类型,但是是使用 getimagesize () 函数来获取文件的 MIME 类型,此时检测的不是数据包中的 content-type,而是图片的文件头,常见的图片文件头如下:
gif(GIF89a) : 47 49 46 38 39 61
jpg、jpeg : FF D8 FF
png : 89 50 4E 47 0D 0A
绕过方法:
当上传 php 文件时,可以使用 winhex、010editor 等十六进制处理工具,在数据最前面添加图片的文件头,从而绕过检测
2.2 后端检测文件扩展名
2.2.1 黑名单检测
后端代码大致为:
<?php
// 实际情况中黑名单内数据会更多更全面$blacklist = array('php', 'asp', 'aspx', 'jsp');$path = "./uploads";$type = array_pop(explode('.', $_FILES['myfile']['name']));if (in_array(strtolower($type), $blacklist)) {
die("File type errer!<br>");} else {
$file = $path . '/' . $_FILES['myfile']['name'];
if (move_uploaded_file($_FILES['myfile']['tmp_name'], $file)) {
echo 'Success!<br>';
} else {
echo 'Error!<br>';
}}
?>
众所周知使用黑名单是非常不安全的,很多网站会使用扩展名黑名单来限制上传文件类型,有些甚至在判断时都不用 strtolower () 来处理,因此造成漏洞
绕过方法:
- 使用一些特殊扩展名来绕过(如 php 可以使用 php3、php4、php5 等来代替)
- 在后端比较没有转换大小写处理时,使用大小写混淆(如将 php 改为 pHp 等)来绕过
2.2.2 白名单检测
大致代码如下,与黑名单检测没有太大差别:
<?php
$whitelist = array('png', 'jpg', 'jpeg', 'gif');$path = "./uploads";$type = array_pop(explode('.', $_FILES['myfile']['name']));if (!in_array(strtolower($type), $whitelist)) {
die("File type errer!<br>");} else {
$file = $path . '/' . $_FILES['myfile']['name'];
if (move_uploaded_file($_FILES['myfile']['tmp_name'], $file)) {
echo 'Success!<br>';
} else {
echo 'Error!<br>';
}}
白名单相对与黑名单就安全许多,要求只能是特定扩展名的文件才能上传,虽然我们无法从代码层面来绕过,但这样也不是绝对的安全,可以利用其他漏洞来绕过
绕过方法:
- 使用 %00 截断文件名来上传(后面会讲)
- 如果目标还存在文件包含漏洞,那么就可以上传图片马再文件包含来拿 shell
2.3 后端检测文件内容
2.3.1 文件内容替换
这种主要是将文件中的敏感字符替换掉,大致代码类似于下面这样:
<?php
$path = "./uploads";$content = file_get_contents($_FILES['myfile']['tmp_name']);$content = str_replace('?', '!', $content);$file = $path . '/' . $_FILES['myfile']['name'];if (move_uploaded_file($_FILES['myfile']['tmp_name'], $file)) {
file_put_contents($file, $content);
echo 'Success!<br>';} else {
echo 'Error!<br>';}
?>
此时如果我们要上传 php 的一句话 <?php @eval($_POST['shell']);?>
时,php 的语言标记中的?会被替换为!,这样一句话就不能被执行了
绕过方法:
主要还是要根据实际过滤的字符来判断,如果写死的话可能是没办法的(一般不会,因为还要兼顾图片上传)
比如过滤掉问号,我们就可以使用 <script language='php'>system('ls');</script>
这样的一句话。具体方法要看实际代码过滤了哪些字符。
2.3.2 图片二次渲染
我们以 upload-labs Pass-16 的源码为例:
$is_upload = false;
$msg = null;
if (isset($_POST['submit'])){
// 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
$filename = $_FILES['upload_file']['name'];
$filetype = $_FILES['upload_file']['type'];
$tmpname = $_FILES['upload_file']['tmp_name'];
$target_path=UPLOAD_PATH.'/'.basename($filename);
// 获得上传文件的扩展名
$fileext= substr(strrchr($filename,"."),1);
//判断文件后缀与类型,合法才进行上传操作
if(($fileext == "jpg") && ($filetype=="image/jpeg")){
if(move_uploaded_file($tmpname,$target_path)){
//使用上传的图片生成新的图片
$im = imagecreatefromjpeg($target_path);
if($im == false){
$msg = "该文件不是 jpg 格式的图片!";
@unlink($target_path);
}else{
//给新图片指定文件名
srand(time());
$newfilename = strval(rand()).".jpg";
//显示二次渲染后的图片(使用用户上传图片生成的新图片)
$img_path = UPLOAD_PATH.'/'.$newfilename;
imagejpeg($im,$img_path);
@unlink($target_path);
$is_upload = true;
}
} else {
$msg = "上传出错!";
}
...
大致意思是后端调用了 php 的 GD 库,提取了文件中的图片数据,然后再重新渲染,这样图片中插入的恶意代码就会被过滤掉了
我自己在测试时发现不管是直接修改文件头来制作的图片马,还是利用 copy 命令制作的图片马,都无法避免其中的一句话被过滤掉。
而看了一篇文章发现其实要把一句话插入到图片数据中,这样经过渲染后这部分数据还是会保留下来。大家可以看一下作为参考:https://secgeek.net/bookfresh-vulnerability/
3.1 解析漏洞及其他漏洞
3.1.1 IIS 解析漏洞
IIS6.0
在 IIS6.0 中有两个很重要的 asp 解析漏洞:
- 假设当前有一个名为 “xxx.asp” 的目录,那么该目录下的所有文件都将被作为 asp 文件解析
- 假设上传一个名为 “test.asp;xxx.jpg” 时,该文件会被当做 asp 文件解析
IIS7.5
这个其实不能算 IIS 的洞,它其实是 php 的解析漏洞,这个漏洞利用条件是服务器在 php.ini
中将 cgi.fix_pathinfo
的值设置为 1
然后当我们访问服务器上任意一个文件时(如:http://test.com/a.jpg)
,当我们在 URL 后面添加.php
(即:http://test.com/a.jpg/.php),那么文件 a.jpg
就将被作为 php
文件来解析
3.1.2 Apache 解析漏洞
利用低版本 apache
扩展名解析特性
在了解这个解析漏洞之前,我们要首先了解 apache
和 php
的三种结合方式:
Apache 和 php 三种结合方式:
1.CGI
2.Module
3.FastCGI
该解析漏洞只有在 apache
和 php
以 Module
方式结合时才存在,而且 Apache
还有一个特性:
Apache 在解析文件时会以文件名从右向左解析,当最后一个扩展名无法识别时,就会向左查看是否有可以识别的文件名,如果没有的话就以配置中的默认文件类型来解析
例如:
a.php.xxx 因为 xxx 无法识别,而左边的 php 可识别,就会被解析为 php 文件
CVE-2017-15715
还有一个 apache
的解析漏洞就是 CVE-2017-15715
,这个漏洞利用方式就是上传一个文件名最后带有换行符 (只能是\x0A,如上传 a.php,然后在 burp 中修改文件名为 a.php\x0A)
,以此来绕过一些黑名单过滤
具体的漏洞分析可以看 p 牛:https://www.leavesongs.com/PENETRATION/apache-cve-2017-15715-vulnerability.html
3.1.3 %00 截断
这个多数被利用在截断路径,利用的条件是:
PHP < 5.3.4
magic_quotes_gpc 关闭
因为 0x00 是字符串的结束标志符,所以 php 在读取到 0x00 时就不会再往后读取,我们可以利用这些截断字符后面不需要的内容
以 upload-labs
的 Pass-12
为例,源码如下:
$is_upload = false;
$msg = null;
if(isset($_POST['submit'])){
$ext_arr = array('jpg','png','gif');
$file_ext = substr($_FILES['upload_file']['name'],strrpos($_FILES['upload_file']['name'],".")+1);
if(in_array($file_ext,$ext_arr)){
$temp_file = $_FILES['upload_file']['tmp_name'];
$img_path = $_POST['save_path']."/".rand(10, 99).date("YmdHis").".".$file_ext;
if(move_uploaded_file($temp_file,$img_path)){
$is_upload = true;
} else {
$msg = '上传出错!';
}
} else{
$msg = "只允许上传.jpg|.png|.gif 类型文件!";
}
}
由于是白名单限制了上传文件类型,因此我们无法在文件名处做文章。但最终 move_uploaded_file()
的目标目录是我们可控的,我们可以将 POST
传入的 save_path
改为../upload/shell.php%00
,这样后面的内容就会被截断掉,这就导致了任意文件上传
还要注意的是 %00 是 url 编码,在以 POST
传参时应该使用 burpsuite
对其进行 url
decode
,或者修改 hex 值为 00
;而当 GET 传参时因为浏览器会做一遍 url decode
,所以直接传 %00
即可。
3.1.4 利用.htaccess 解析
.htaccess 文件 (或者 “分布式配置文件”), 全称是 Hypertext Access (超文本入口)。提供了针对目录改变配置的方法,
即,在一个特定的文档目录中放置一个包含一个或多个指令的文件,
以作用于此目录及其所有子目录。作为用户,所能使用的命令受到限制。管理员可以通过 Apache 的 AllowOverride 指令来设置。
利用.htaccess 的条件:Apache 中配置 AllowOverride All
.htaccess 文件可以配置将特定的文件按规定的文件类型进行解析,可以用以下两种方式来配置:
<FilesMatch "test">
SetHandler application/x-httpd-php
</FilesMatch>
这一种采用正则匹配,只要文件名为 test 的文件都将被作为 php 文件解析
AddType application/x-httpd-php .jpg
第二种是将.jpg 文件都作为 php 文件解析
这样我们如果能将.htaccess
上传到服务器的话,就可以再根据我们自己设定的规则来解析上传的文件,以此来绕过上传过滤
总结
常见的文件上传的检测和绕过方式基本是以上几种。在实战或 CTF 比赛中往往是几种类型的结合,因此绕过也需要几种方式的结合。
首先我认为最重要的是前期的信息收集,服务器的类型、版本,使用的脚本语言、版本,只要做到对这些很清楚后才能考虑之后能否利用一些如 %00 截断、服务器解析漏洞来进行文件上传。
在测试时,一般我们都先要 fuzz 看一下检测是哪种类型,是前端还是后端?黑名单还是白名单?上传后的 shell 能否被成功执行?是否有文件内容的检测?
如果是黑名单的话,就要尝试各种特殊文件名(php、Php、PHP、pht、php5、phtml),或者在扩展名后添加空格、::$DATA、. 等字符,再或者是尝试上传.htaccess
如果是白名单,就要看是否可以使用 %00 截断,或者利用服务器的解析漏洞。如果真的过滤很死的话,不妨再找一下目标的文件包含漏洞,尝试利用文件包含来解析图片马。
个人认为现在文件上传在代码层除了逻辑问题外已经很少有漏洞了,大多数情况下都是利用服务器解析漏洞等来 getshell,这篇文章也只是作为自己入门文件上传的总结。如果有错误,请各位师傅指正。
暂无评论内容