1 文件上传简介
1.1 什么是任意文件上传漏洞
任意文件上传漏洞常发生在文件上传功能中,由于后端代码中没有严格限制用户上传的文件,导致攻击者可以上传带有恶意攻击代码的JSP 脚本到目标服务器,进而执行脚本,以达到控制操纵目标服务器等目的。
1.2 任意文件上传漏洞危害
如果目标服务器存在任意文件上传漏洞,服务将会面临巨大风险,包括但不限于:服务器的网页被篡改,网站被上传木马,服务器被远程控制,被安装后门,执行挖矿程序等。
2 任意文件上传漏洞代码审计
在对文件上传功能进行代码审计时我们主要分析整个上传流程对所上传文件做了什么样的操作。进而分析是否能够造成任意文件上传漏洞。
我们比较关注的几点:
①、SpringBoot对JSP的限制。
②、文件后缀名是否存在白名单。
③、文件类型是否存在白名单。
④、所保存的路径是否能够解析JSP。
⑤、文件头检测。
但是Springboot相关官方禁止使用一些JSP文件,并做了一些限制,若想使用,需要添加如下依赖:
<!--用于编译jsp-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
2.1 会遇见的一些限制
2.1.1 文件后缀名校验
主要关注后端是否对上传的文件后缀名进行了检查判断。如果在后端对后缀名没有限制。那么就极有可能存在任意文件上传漏洞。很多项目是通过前端限制文件上传类型。但是我们都可以通过抓包的方式修改后缀为webshll文件(php,jsp),以达到上传Webshell的目的。下图就是一个案例,后端获取文件名(26行),然后进行"目录"+"文件"进行拼接,无过滤情况。这时候就可以上传webshell了。
2.1.2文件后缀名校验黑白名单
如果后端使用了黑白名单限制后缀名已经初步起到了一定的防护作用。但具体还需要根据实际情况分析。如果是黑名单是否存在遗漏的情况,或者一些解析漏洞,例如添加%00,或者Windows对大小写并不敏感,甚至利用windows的特性DATA,可能就会绕过黑名单检测。
2.1.3 文件名操作
在这里我们关注上传的文件名是否有所改动。常见的情况是后端直接接受保存我们上传的文件名。但也常后端自定义不可预测的文件名,比如使用UUID。比如以下代码:
String originalFileName = file.getOriginalFilename();
String extension = originalFileName.substring(originalFileName.lastIndexOf('.'));
String fileName = UUID.randomUUID() + extension;
将文件名随机命名可以增加一些攻击利用难度。但并没有直接修复任意文件上传漏洞。很多时候由于代码编写不规范会将上传后的路径,文件名也会显示在前端。
除此之外,可能还会存在一种情况,就是可能会对图片进行操作,也就是存在二次渲染的情况,这时候就要判断未改变的字节位置,插入webshell。
2.1.4 保存路径
在这里我们关注是否保存在本地,保存文件路径是否是在非解析路径,保存路径是否可控。
文件保存在云存储服务器:
存在以下一种情况,可能将文件保存在云存储服务器,例如七牛云OSS,阿里OSS等,假若保存在了OSS上,尽管成功上传了Webshell,但是无法执行,也没有任何作用。如果允许上传html文件,也可以尝试上传html文件,进而造成xss漏洞获取cookie。
文件保存在本地,但是无法执行:
可能我们幸幸苦苦找到了一个文件上传点,绕过了层层限制,上传成功后,却发现无法解析,这时候就得想想文件上传后,目录是否可以穿越了,在获取文件名后,大多会进行路径拼接操作。如果保存图片的地址是非解析目录,我们可以配合目录穿越漏洞操作WebShell存储到其他地方,尝试执行。例如下列代码,没有进行校验,可以拼接相关文件名。
String fileName = file.getOriginalFilename();
String filePath = path + fileName;
2.2 文件上传功能关键字
大家在对完整项目进行代码审计时,尽量整理收集尽可能多的信息,确定系统功能点,进而可以针对性的进行代码审计。
在面对一个完整的项目中有我常用以下方式定位上传功能,比如:查看需求文档,查看Controller层,部署后通过前端定位功能点,全局搜索关键字等等。
下面给出一些文件上传关键字,帮助你快速定位是否存在文件上传功能。
File
FileUpload
FileUploadBase
FileItemIteratorImpl
FileItemStreamImpl
FileUtils
UploadHandleServlet
FileLoadServlet
FileOutputStream
DiskFileItemFactory
MultipartRequestEntity
MultipartFile
com.oreilly.servlet.MultipartRequest
......
3 演示案例
在实际代码审计过程中,我们可以结合白+黑的模式来进行审计,首先找到上传功能点,然后通过上传功能点进一步审计,找到绕过的方法。
3.1 寻找上传点
以下为一个演示案例
这里找到了一个上传的前端页面,我们先上传一个测试文件试试。发现接口为"/image/gok4"。
进入源码中,全局搜索相关关键字,但是发现并没有找到这个接口。
有可能在引入的jar包中,在jar包中进行搜索,找到了相关接口。
3.2 代码审计
源代码如下
public String gok4(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "uploadfile", required = true) MultipartFile uploadfile,
@RequestParam(value = "param", required = false) String param,
@RequestParam(value = "fileType", required = true) String fileType,
@RequestParam(value = "pressText", required = false) String pressText) {
try {
long maxSize = 4096000L;
System.out.println(uploadfile.getSize());
if (uploadfile.getSize() > maxSize) {
return this.responseErrorData(response, 1, "上传的图片大小不能超过4M。");
} else {
String[] type = fileType.split(",");
this.setFileTypeList(type);
String ext = FileUploadUtils.getSuffix(uploadfile.getOriginalFilename());
if (fileType.contains(ext) && !"jsp".equals(ext)) {
String filePath = this.getPath(request, ext, param);
File file = new File(this.getProjectRootDirPath(request) + filePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
uploadfile.transferTo(file);
return this.responseData(filePath, 0, "上传成功", response);
} else {
return this.responseErrorData(response, 1, "文件格式错误,上传失败。");
}
}
} catch (Exception var13) {
logger.error("gok4()--error", var13);
return this.responseErrorData(response, 2, "系统繁忙,上传失败");
}
}
首先是2-5行的代码,这几行代码含义是需要添加的参数,true为必须携带的参数,false为可以选择携带或者不携带的参数。uploadfile和fileType必须携带。
接着是7-10行的代码,这几行是判断文件大小的参数,文件需要小于4mb,否则上传失败
long maxSize = 4096000L;
System.out.println(uploadfile.getSize());
if (uploadfile.getSize() > maxSize) {
return this.responseErrorData(response, 1, "上传的图片大小不能超过4M。");
再看12-14行代码,首先是12行的代码,获取filetype参数的内容,并以","进行分割,获取每条数据,最后,将上传的文件名赋值给ext
String[] type = fileType.split(",");
this.setFileTypeList(type);
String ext = FileUploadUtils.getSuffix(uploadfile.getOriginalFilename());
最后就是15-20行的内容了,这里首先是判断上传文件的扩展名并检查是否在允许的文件类型列表中,且不为JSP文件。 如果满足以上两个条件,即可直接保存该文件。
if (fileType.contains(ext) && !"jsp".equals(ext)) {
String filePath = this.getPath(request, ext, param);
File file = new File(this.getProjectRootDirPath(request) + filePath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
这里我们回到原来的数据包,原来filename的文件后缀需要满足filetype的参数,只要存在filetype里的参数,即可上传。
我们尝试在fileType上添加一个jsp文件,并将文件后缀改为jsp进行尝试,发现上传失败,因为在第15行代码已经明确提示,不能上传jsp文件。
3.3 绕过分析
有什么绕过方法呢,这里有两种绕过方法,一种是上传jspx格式的文件,还有一种就说利用windows特性了,利用DATA方式进行绕过。
3.3.1 利用jspx进行绕过
首先介绍以下jspx,jspx实际上是一种JavaServer Pages(JSP)文件的格式,它扩展了标准的JSP技术。JSP是一种用于构建动态网页的技术,允许开发者在HTML中嵌入Java代码。jspx文件继承了JSP的这些特性,并添加了额外的功能。
修改fileType并添加jspx,发现可以上传成功。
3.3.2 利用windows特性DATA绕过
其次是DATA绕过,注:DATA绕过只能是目标主机是windows系统。
通过修改fileType,添加一个jsp::$DATA
访问上传地址,发现能够正常解析,相关webshell就不一一演示啦。
4 修复方案
列出允许的扩展。只允许业务功能的安全和关键扩展
确保在验证扩展名之前应用输入验证。
验证文件类型,不要相信Content-Type头,因为它可以被欺骗。
将文件名改为由应用程序生成的文件名
设置一个文件名的长度限制。如果可能的话,限制允许的字符
设置一个文件大小限制
只允许授权用户上传文件
将文件存储在不同的服务器上。如果不可能,就把它们存放在webroot之外。
在公众访问文件的情况下,使用一个处理程序,在应用程序中被映射到文件名(someid -> file.ext)。
通过杀毒软件或沙盒(如果有的话)运行文件,以验证它不包含恶意数据。
确保任何使用的库都是安全配置的,并保持最新。
保护文件上传免受CSRF攻击