java防止文件上传_文件上传漏洞:getshell的最好方式,我们如何防御?

我相信,你在开发Web应用时,后端一定会提供文件的上传功能,比如前端页面肯定有图片的展示,后端必定会提供图片的上传入口。但是,你在做文件上传功能时,是否考虑过它的安全性问题呢?

请看下面的代码:

@PostMapping("upload")

public String upload(@RequestParam("file")MultipartFile file,HttpServletRequest request)throws Exception{

String filename = file.getOriginalFilename();

//上传文件到服务器

String url = uploadService.upload(filename);

return url;

}

上述代码存在非常严重的安全漏洞,可以看到,该代码没有对文件做任何的限制,只要传入的是文件流,它就能接收它,并且将其上传到服务器。

假设你的服务器运行的是 Tomcat 容器,那么它能执行后缀为 .jsp 的文件,攻击者就可以上传 .jsp 文件,并且文件内容为可执行的 java 代码。当 .jsp 文件被上传上去后,攻击就可以利用菜刀、蚁剑等工具连接该 .jsp 文件,连接成功后,攻击者就可以控制你的服务器,俗称 getshell。如图所示:

java防止文件上传_文件上传漏洞:getshell的最好方式,我们如何防御?_第1张图片

我用蚁剑成功连接上目标服务器,并且可以对服务器上的文件做删除修改操作,如果你的服务器还存在漏洞(如缓冲区溢出漏洞)的话,攻击者还可以提权,以获取更高的操作权限,可以说如果你的应用存在文件上传漏洞的话,威胁是相当大的。

那么,我们该如何防御呢?

通过前面的演示,要修复上述文件上传漏洞,最关键的就是不能让攻击上传带有木马特性代码的可执行文件。

我们假设该上传功能,只允许上传图片格式的文件。那么,第一步就是要判断文件类型,判断文件类型的方式主要有两种:

根据HTTP请求头的“Content-Type”来判断。

截断文件名,判断文件后缀名。

我们先来说第一种方式,“Content-Type”记录的就是当前请求内容的类型,它有固定的值,如果是图片类型一般以“image/**”来表示,其中“**”为图片格式,如:image/png、image/jpeg等。

那么,判断文件类型为图片格式就可以使用下列代码:

String contentType = request.getHeader("Content-Type");

if(!"image/png".equals(contentType) || !"image/jpeg".equals(contentType) || !"image/gif".equals(contentType)){

throw new Exception("图片格式不正确!");

}

上述代码的安全性不够,还会存在问题,因为“Content-Type”是客户端传给服务端的,攻击者可以捕获HTTP请求,手动修改“Content-Type”的值为image/jpeg,但是文件后缀依然是 .jsp,也可以骗过服务端上传到服务器。

因此,一般采用第二种方式更为保险。其代码如下:

String filename = file.getOriginalFilename();

String suffix = filename.substring(file.lastIndexOf('.'));

if(!".jpg".equals(suffix) || !".jpeg".equals(suffix)|| !"png".equals(suffix) || !"gif".equals(suffix)){

throw new Exception("图片格式不正确!");

}

仅判断文件后缀,只是提高了攻击的门槛,但是对于有经验的攻击者来说,同样有方法可以绕过,攻击者可以采用截断的方式来绕过,例如将攻击脚本命名为:shell.jsp%00.jpg,在这个命名中多了一个“%00”字符,该字符属于截断字符,当上传到服务器后,服务器发现“%00”字符后,它会截断后面的内容,从而上传到服务器后,文件名变成了 shell.jsp。

因此,我们还应修改上述代码,使之变得不可绕过。有一个比较好的方法就是强制改变上传到服务器的文件名,并且后面带上截取的文件后缀名。

String newFilename = UUID.randomUUID().toString() + suffix;

//上传文件到服务器

String url = uploadService.upload(newFilename);

return url;

上述代码大大地提高了文件上传的安全性,但也不是绝对的安全,它存在一个问题:如果上传后的文件是放到可执行脚本的容器下面,则攻击者可以将攻击脚本隐藏到图片中,使图片可以骗过容器变成可执行文件,并实施攻击。

要彻底修复文件上传漏洞,你可以采取下面两个方法:

有条件的话,你可以将文件上传到专门的文件服务器,该文件服务器只存放文件,不启动任何容器,这样一来,访问文件服务器的任何文件,它都会按照普通文本/二进制文件来处理。

如果只能放到 Tomcat 等容器下,需要判断图片内容,不同的图片,它的二进制流内容是有严格规定的,如果图片的二进制按照这种严格的规定输入的话,则 Tomcat 只能按照图片方式来处理,不会执行里面的任何攻击脚本。

上述的第2种方法涉及到对图片二进制的理解,我们不需要深入研究,只要知道图片二进制的文件头是如何定义的就行了。

用 WinHex 分别打开 JPG/JPEG、PNG、GIF格式的图片,如图所示:

386040a75a0bdc24f4be3b9b4ec480e0.png

java防止文件上传_文件上传漏洞:getshell的最好方式,我们如何防御?_第2张图片

java防止文件上传_文件上传漏洞:getshell的最好方式,我们如何防御?_第3张图片

文件头都是在二进制的最开始定义的,JPG/JPEG 的文件头标识为:FFD8FFE0,PNG 的文件头标识为:89504E47,GIF 的文件头标识为:47494638。因此,在判断文件类型时,只需要读取文件流并转成字节数组,判断所取得的字节数组前几位是否为图片的文件头即可。示例代码如下:

InputStream inputStream = file.getInputStream();

int len = -1;

StringBuilder builder = new StringBuilder(4);

for(int i = 0;i < 4 && -1 != (len = inputStream.read());i++){

builder.append(Integer.toHexString(len));

}

if(!"ffd8ffe0".equals(builder.toString()) || !"89504e47".equals(builder.toString()) || !"47494638".equals(builder.toString())){

throw new Exception("文件类型必须是JPG、PNG或GIF");

}

inputStream.close();

至此,我们可以完全防止攻击者上传攻击脚本。但是,上述代码并不完美,虽然可以防止攻击者上传攻击脚本,但是我们并没有限制文件大小,如果攻击者上传足够大的文件,比如100G,会导致服务器带宽始终被攻击者占用,如果攻击者采取分布式上传策略,很可能导致DDoS(分布式拒绝服务)攻击或者磁盘被攻击者上传的文件占满。因此,我们还应对文件大小做限制,如:

int size = file.getSize();

if(size > 1000000){

throw new Exception("图片太大,请重新上传!");

}

这样,我们既防止了攻击者非法上传攻击脚本,还保证了服务器带宽和磁盘资源不被非法占用。

上述以图片为例讲解了如果防御或修复文件上传漏洞,在实际场景中,可能还有其他的一些文件上传,比如excel、word、txt等文件,这些文件的的处理方法同图片上传方法一致,均按照文件后缀、格式、内容、大小等多维度来判断,就不会导致文件上传漏洞。

这里,我需要着重提一个应用场景,比如在 CMS 系统中,有这样一个功能:后端可以动态新增 jsp 页面,这时避免不了 jsp 动态页面的上传,我们既要允许客户端上传 jsp 文件,又要防止非法的 jsp 文件被上传。这时,就需要对 jsp 文件内容进行判断,校验其是否存在特征代码。

攻击脚本的代码,一般都会通过内置函数调用服务器的相关命令,以达到控制服务器的目的,如 java 语言可以通过 Runtime.getRuntime().exec(cmd) 来执行命令。那么,我们就可以校验 jsp 文件是否包含这类关键词,如下列代码:

StringBuilder content = StringBuilder();

BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream(),"UTF-8"));

String line = null;

while(null != (line = reader.readLine())){

content.append(line);

}

reader.close();

if(content.toString().contains("exec") || content.toString().contains("Runtime")){

throw new Exception("当前文件不合法!");

}

所谓上有政策,下有对策,有经验的攻击者不会明目张胆的编写带有明显特征码的攻击脚本,这就是我们常说的免杀脚本。免杀脚本,通过后端代码不容易被查出来,针对这种情况,我的建议是:后端尽可能不要提供直接上传 jsp 等动态页面的入口,如果不可避免,将其上传到另外的可以执行 jsp 文件的服务器上,使其与主服务器独立开来,防止主服务器被攻击,同时再备份一份 jsp 文件。

你可能感兴趣的:(java防止文件上传)