Java代码审计汇总系列(五)——文件相关

一、概述

由于部分web系统可能存在隐藏的、仅在后端执行的如上传下载等文件操作接口,仅通过黑盒测试方法对界面可视的文件功能点进行测试,无法全面覆盖,从代码层面可以更快更全地审计出文件类漏洞。

文件操作类的漏洞一般有:文件上传、任意文件查看/下载/删除、文件导出(DDE)、文件解压(zip炸弹、目录穿越)等,黑盒系列主要汇总了上传相关的漏洞:文件上传漏洞另类绕过技巧及挖掘案例全汇总

二、挖掘过程

挖掘文件类漏洞需要熟悉File类的相关函数和常见操作,以下列代码为例讲解相关类函数:

public void deleteFile(String sourcePath,String fileName)
    {
       File file = new File(sourcePath);
       if (file.isDirectory())
       {
           File[] files = file.listFiles();
           for (int i = 0; i < files.length; i++)
           {
                if(files[i].getName().endsWith(".zip")
                        &&!fileName.equals(files[i].getName()))
                {
                    if(!FileUtil.deleteFile(files[i]))
                    {
                       DEBUGGER.debug("faile to delete file with fileName "
                                + file.getPath());

一个目录其实就是一个 File 对象,包含其他文件和文件夹。这里创建一个File对象,调用isDirectory() 方法判断是否是一个目录,然后调用list() 方法循环提取其中的文件和文件夹,最后经过两个嵌套的判断来执行删除操作。

综上,这个deleteFile方法的功能就是指定一个路径和文件名,删除该路径目录下不同于给定文件名的所有其他 .zip 压缩包文件,且这个是public 方法,所以能直接通过 http 请求调用此接口进行删除操作。

三、挖掘技巧

挖掘文件类漏洞同样有两个思路,正向:通过页面功能找对应代码实现,通常是抓包路由的方式;逆向:以文件处理的特征类和函数作关键字定位代码,常见文件处理的类有:

FileInputStream
FileOutputStream
File
FileUtils
IOUtils
BufferedReader
ServletFileUpload
MultipartFile
CommonsMultipartFile
PrintWriter
ZipInputStream
ZipEntry.getSize

挖掘此类漏洞的关键在于判断文件名、文件路径和文件内容是否为用户可控,且是否经过过滤处理过程。具体漏洞挖掘过程及修复见案例章节。

四、实战案例

1、 文件上传

文件上传功能是最常见,利用最简单同时也是最容易防御的文件操作漏洞之一,一个典型的上传操作代码如:

@RequestMapping(value= "/upload", method = RequestMethod.POST)
 public String uploadFileHandler(ModelMap model,@RequestParam("file") MultipartFile file,@RequestParam("name") String name) {
   if (!file.isEmpty()) {
     try {
         byte[]bytes = file.getBytes();
 
         //Creating the directory to store the uploaded files
         StringrootPath = System.getProperty("catalina.home");
         Filedir = new File(rootPath + File.separator + "tmpFiles");
         if(!dir.exists())
           dir.mkdirs();
 
         //Create the file on server
         FileserverFile = new File(dir.getAbsolutePath() + File.separator + name +"." + FilenameUtils.getExtension(file.getOriginalFilename()));
                                    
         BufferedOutputStreamstream = new BufferedOutputStream(new FileOutputStream(serverFile));
         stream.write(bytes);
         stream.close();
 
         Stringpath = "File has been uploaded to <b>" + serverFile;
         model.addAttribute("path",path);
         return"upload";
     } catch (Exception e) {
         return"You failed to upload " + "" + " => " +e.getMessage();
     }
    }else {
         return"You failed to upload " + name+ " because the file wasempty.";
    }
  }

这段代码调用MultipartFile类来实现上传功能,其中file和name传自前端,文件file只进行了非空判断,所以存在任意文件上传;同时在使用File的新方法来创建新文件的时候使用拼接name来形成文件名:

// Create the file on server
File serverFile = newFile(dir.getAbsolutePath() + File.separator + name + "." +FilenameUtils.getExtension(file.getOriginalFilename()));

所以可以使用路径跨越符../../,name参数设为../../evil.jsp实现任意目录文件上传:

针对任意文件上传漏洞的修复,建议使用表名单方式检测文件类型和后缀:

if(!mime.equalsIgnoreCase("application/vnd.openxmlformats-officedocument.wordprocessingml.document")|| !"docx".equals(ext)){
String error = "File notsupported!";
model.addAttribute("path",error);
return "upload-soln";
}...

或者在配置文件中进行限制:

针对目录穿越,可取后缀名前的字符串作文件名,如:

不同开发的防御方法都不尽相同,实际情况中可以对防御机制进行针对性分析,看是否可以绕过或转化为其他漏洞。

2、 任意文件操作(读取、删除、下载)

如下的任意文件下载漏洞,downLoad方法的实现调用了downLoadUserFile方法,url用户可控:

关注传参及判断过程,文件名若以/share/url则进行下载文件操作,因此这里可构造数据包https://1.1.1.1:8080/rest/report/downLoad?url=share/bfmshare.tar.gz进行有限的任意文件读取:

3、 文件解压目录穿越

系统的文件解压功能定位到代码,跟踪到uncompressOneFile函数的实现,发现没有对文件名进行校验,导致可使用 ../跳出上一级从而达到任意路径写入。

使用十六进制工具打开zip包,修改文件目录,达到解压文件目录穿越。在黑盒测试中遇到压缩包上传的功能也可使用此进行测试:

关于文件解压的另一个漏洞“解压炸弹”:

public static final int BUFFER = 512;
public static final int TOOBIG = 0x6400000;// 100MB
public final void unzip(String filename)throws java.io.IOException
{
 
FileInputStream fis = newFileInputStream(filename);
ZipInputStream zis = new ZipInputStream(newBufferedInputStream(fis));
ZipEntry entry;
try
{
while ((entry = zis.getNextEntry()) !=null)
{
System.out.println("Extracting: "+ entry);
int count;
byte data[] = new byte[BUFFER];
// Write the files to the disk, but only ifthe file is not insanely
big
if (entry.getSize() > TOOBIG)
{
throw new IllegalStateException(
"File to be unzipped is huge.");
}
if (entry.getSize() == -1)
{
throw new IllegalStateException(
"File to be unzipped might behuge.");
}
 
FileOutputStream fos = new FileOutputStream(entry.getName());
BufferedOutputStream dest = newBufferedOutputStream(fos,
BUFFER);
while ((count = zis.read(data, 0, BUFFER))!= -1)
{
dest.write(data, 0, count);
}
dest.flush();
dest.close();
zis.closeEntry();
}
}
finally
{
zis.close();
}
}

这个错误示例调用ZipEntry.getSize()方法在解压提取一个条目之前判断其大小,但恶意攻击者可以伪造ZIP文件中用来描述解压条目大小的字段,因此,getSize()方法的返回值是不可靠的,本地资源实际可能被过度消耗而造成DOS攻击。

对解压炸弹防御的正确方法是对文件名、解压文件大小和个数都进行检测:除了在解压每个条目之前对其文件名进行校验;while循环代码检查从zip存档文件中解压出来的每个文件条目的大小是否大于100MB;最后计算从存档文件中解压出来的文件条目总数,如果超过1024个,则抛出异常,如:

static final int BUFFER = 512;
static final int TOOBIG = 0x6400000; // max size of unzipped data, 100MB
static final int TOOMANY = 1024; // max number of files
// ...
private String sanitzeFileName(String entryName, String intendedDir)throws
IOException
{
File f = new File(intendedDir, entryName);
String canonicalPath = f.getCanonicalPath();
File iD = new File(intendedDir);
String canonicalID = iD.getCanonicalPath();
if (canonicalPath.startsWith(canonicalID))
{
return canonicalPath;
}
else
{
throw new IllegalStateException(
"File is outside extraction target directory.");
}
}
// ...
public final void unzip(String fileName) throws java.io.IOException
{
FileInputStream fis = new FileInputStream(fileName);
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(fis));
ZipEntry entry;
int entries = 0;
int total = 0;
byte[] data = new byte[BUFFER];
try
{
while ((entry = zis.getNextEntry()) != null)
{
System.out.println("Extracting: " + entry);
int count;
// Write the files to the disk, but ensure that the entryName is valid,
// and that the file is not insanely big
String name = sanitzeFileName(entry.getName(), ".");
FileOutputStream fos = new FileOutputStream(name);
BufferedOutputStream dest = new BufferedOutputStream(fos, BUFFER);
while (total + BUFFER <= TOOBIG && (count = zis.read(data, 0,BUFFER)) !=
-1)
{
dest.write(data, 0, count);
total += count;
}
dest.flush();
dest.close();
zis.closeEntry();
entries++;
if (entries > TOOMANY)
{
throw new IllegalStateException("Too many files to unzip.");
}
if (total > TOOBIG)
{
throw new IllegalStateException(
"File being unzipped is too big.");
}
}
}
finally
{
zis.close();
}
}

文件类的漏洞远不限于上述所列,实际审计中通过页面功能正向审计或逆向跟踪的方法定位到文件处理的代码,研究任何类型的文件处理和每一步操作过程,看是否可以进行文件覆盖、移动、增删改等操作。

本文分享自微信公众号 - 卓文见识(zhuowenjianshi)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Nicky's blog

MySQL基础之Natural Join用法

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

8820
来自专栏小詹同学

OpenCV实现年龄与性别预测

前面我写了很多篇关于OpenCV DNN应用相关的文章,这里再来一篇文章,用OpenCV DNN实现一个很有趣好玩的例子,基于Caffe的预训练模型实现年龄与性...

5430
来自专栏机器人课程与技术

机器人控制器编程课程-教案05-秘籍

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

8710
来自专栏code秘密花园

前端技术观察第七期 - 为什么 Progressive Web Apps 是移动端 web 的未来

7620
来自专栏飞总聊IT

笑死人不偿命的知乎沙雕问题排行榜

作者:徐麟,某互联网公司数据分析狮,个人公众号数据森麟(id:shujusenlin)

13220
来自专栏无道编程

前端SweetAlter弹窗js的使用

SweetAlter是一个可以美化的alter,他可以是适应所有设备【电脑,手机,平板】,自动全屏覆盖,位置在屏幕最中间。

13330
来自专栏前端资源

Z-Blog PHP百度熊掌号搜索结果出图页面改造

熊掌号为优质图文内容生产者提供结搜索结果出图权益,帮助站点获取更好的搜索结果展现样式,为搜索用户提供更好的浏览体验。

7020
来自专栏code秘密花园

推荐6款Vue管理后台框架,收藏好,留备用

来源 | https://www.jianshu.com/p/0f41bfe211a8

17640
来自专栏小詹同学

100 行 python 代码告诉你国庆哪些景点爆满

举国欢庆的国庆节马上就要到来了,你想好去哪里看人山人海了吗?还是窝在家里充电学习呢?说起国庆,塞车与爆满这两个词必不可少,去年国庆我在想要是我能提前知道哪些景点...

6430
来自专栏前端资源

超越Cookie,当今的客户端数据存储技术有哪些

当 cookie 被首次引入时,它是浏览器保存数据的唯一方式。之后又有了很多新的选择:Web Storage API、IndexedDB 和 Cache API...

7930

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励