文件上传漏洞:getshell的最好方式,我们如何防御?
发布日期:2021-07-01 00:02:03 浏览次数:2 分类:技术文章

本文共 4121 字,大约阅读时间需要 13 分钟。

我相信,你在开发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。如图所示:

 

             

 

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

 

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

 

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

 

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

 

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

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

 

我们先来说第一种方式,“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;

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

 

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

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

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

 

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

 

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

 

             

 

             

 

             

 

文件头都是在二进制的最开始定义的,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 文件。

转载地址:https://lynnlovemin.blog.csdn.net/article/details/108486153 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:搭建漏洞环境,学习常见漏洞的利用手段与防御方法
下一篇:古典密码学原理和Base64算法原理

发表评论

最新留言

路过,博主的博客真漂亮。。
[***.116.15.85]2024年05月04日 06时22分49秒