Solr-RCE-via-Velocity-template

0x01 环境搭建

solr通过启动的时候加上-a参数,就可以使用额外的 JVM 参数(例如以 -X 开头的参数)启动 Solr,下面开启一下jdwp的远程调试。


solr start -p 8988 -f -a "-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8988"

0x02 漏洞分析

可以看到这个 filter 的注释说的,也就说相关路径都会先发送到这个 SolrRequestFilter 进行处理。

image-20191101110359541

选择在 SolrRequestFilterdofilter 处下个断点,可以看到请求过来了。

dofilter 方法中,这里会先获取我们 web.xml 中的 excludePatterns 的路径进行正则比对。

代码继续下行,来到这里,跟进 getHttpSolrCall 方法。

image-20191101111809469

getHttpSolrCall 方法中最后的处理结果是返回一个 HttpSolrCall 对象。

紧接着调用call方法,来到了 HttpSolrCall#call 中。

跟进 HttpSolrCall#call 中,一直往下走,代码来到了这里,根据 switch 进行选择,这里的 actionPROCESS ,所以自然进入 PROCESScase 中进行处理,然后调用this.execute方法来处理 solrRsp 对象。

跟进 HttpSolrCall#execute 中,首先调用 getCore 方法获取 SolrCore 对象,然后调用 SolrCore#execute 方法。

image-20191101130025369

跟进 SolrCore#execute 方法选择在 handler.handleRequest 处下一个断点,为什么会在这里下断点,因为从方法名称来看,这个名称大概会处理我们传入的 request 请求,这里由于我们请求的路径是/solr/test/config实际上是针对这个 config 进行操作,所以这里的handler对象是 SolrConfigHandler

代码继续下行,来到 RequestHandlerBase 类中,这个类调用this.handleRequestBody来处理。

而这个 handlerRequestBody 实际上是一个抽象类,也就是solr实际通过自己的路由分发,将不同url请求,转发到不同的 handler 进行处理,这是我的理解,可能存在偏差。

然后在 SolrConfigHandler#handleRequestBody 中就可以看到一些处理了,由于我们请求的数据类型是json,请求方式POST,所以自然会进入command.handlerPOST进行处理。

跟进command.handlerPOST,实际上可以看到this.handleCommands方法进行处理的时候overlay对象的值正是我们已经传入的。

继续跟进 SolrConfigHandler$commandhandleCommands 方法,这里通过 SolrResourceLoader 类加载资源配置,然后调用 SolrResourceLoader#persistConfLocally 方法针对文件进行操作。

继续 SolrResourceLoader#persistConfLocally 方法可以看到,获取配置文件路径,写入内容,而写入部分的正是我们通过POST方式上传上来的数据。

第一阶段修改配置文件的调用栈如下所示:


persistConfLocally:900, SolrResourceLoader (org.apache.solr.core)

handleCommands:504, SolrConfigHandler$Command (org.apache.solr.handler)

handlePOST:345, SolrConfigHandler$Command (org.apache.solr.handler)

access$100:158, SolrConfigHandler$Command (org.apache.solr.handler)

handleRequestBody:136, SolrConfigHandler (org.apache.solr.handler)

handleRequest:199, RequestHandlerBase (org.apache.solr.handler)

execute:2551, SolrCore (org.apache.solr.core)

execute:711, HttpSolrCall (org.apache.solr.servlet)

call:516, HttpSolrCall (org.apache.solr.servlet)

doFilter:395, SolrDispatchFilter (org.apache.solr.servlet)

doFilter:341, SolrDispatchFilter (org.apache.solr.servlet)

当然第二阶段就是通过模版注入,远程代码执行,代码断点还是下到call.call位置。

跟进 HttpSolrCall#call 中,跟进下图代码中的this.getResponseWriter

HttpSolrCall#getResponseWriter,可以看到实际上循环来到了下图位置,调用的是 SolrCore#getQueryResponseWriter

image-20191101151858441

SolrCore#getQueryResponseWriter 实际上是根据请求参数重的 wt 字段的值去获取reponseWriter 对象,payload中的参数是 velocity ,所以这里最后的返回对象是 VelocityResponseWriter

紧接着代码下行来到下图位置,我们看到 writeResponse 方法处理了我们刚刚的 VelocityResponseWriter对象。

继续跟进 HttpSolrCall#writeResponse ,代码调用了 writeQueryResponse 方法。

代码一路下行,会来到 VelocityResponseWriter#write 当中,然而刚开始时候,我只导入了solr-webapp目录下 WEB-INF 中的jar文件,然后就会出现下图中的情况,明明debug断点到了,但是无法打开查看源代码。后面深入看看才发现少导入了两个文件的jar,一个是 dist 目录中的 jar 文件,另一个是contrib/velocity/lib目录下的 Jar 文件。

继续愉快的debug,跟进 VelocityResponseWriter#write ,首先调用 createEngine 函数处理我们传入的 request 对象。

跟进 createEngine 函数,这里实例化 SolrParamResourceLoader 类来处理 request 对象。

这里有个疑问为啥 paramsResourceLoaderEnabled 是true,本质原因就在这之前HttpSolrCall#call方法中通过 getResponseWriter 方法,进一步来到 VelocityResponseWriter#init 方法获取到我们之前第一步修改配置文件的时候写入配置文件中的params.resource.loader.enabledsolr.resource.loader.enabled的结果,所以这里自然是true。

跟进 SolrParamResourceLoader 类,这里遍历循环我们payload中的数据,当 namev.template ,我们在payload中的 v.template 的值是 custom ,所以这里实际上是生成了 custom.vm 的恶意 engine

代码回到 createEngine 方法中,这里自然 paramsResourceLoaderEnabled 是true,原因不在细说,上面所过了。

执行完这两个if之中的 SolrParamResourceLoaderSolrVelocityResourceLoader 操作之后,代码回到 VelocityResponseWriter#write 中,调用 VelocityResponseWriter#getTemplate 方法处理刚刚的engine对象,以及我们的request请求对象。

VelocityResponseWriter#getTemplate 方法中回根据我们提交的 v.template 参数获取我们构造的恶意 template 对象。

最后在调用我们构造好的恶意 templatemerge 方法,达到命令模版注入的效果。

实际上模版进入到 templatemerge 方法,然后及时一些AST解析过程。最后补上核心调用栈:


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:506, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection)

invoke:494, UberspectImpl$VelMethodImpl (org.apache.velocity.util.introspection)

execute:198, ASTMethod (org.apache.velocity.runtime.parser.node)

execute:304, ASTReference (org.apache.velocity.runtime.parser.node)

value:605, ASTReference (org.apache.velocity.runtime.parser.node)

value:72, ASTExpression (org.apache.velocity.runtime.parser.node)

render:235, ASTSetDirective (org.apache.velocity.runtime.parser.node)

render:377, SimpleNode (org.apache.velocity.runtime.parser.node)

merge:359, Template (org.apache.velocity)

merge:264, Template (org.apache.velocity)

write:166, 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)

0x03 流程图

简单绘制一个流程图,方便自己后记

0x04 后话

从solr官方网站可以看出,这个插件实际上是一个可选项,这个漏洞本质还是配合solr未授权来达到rce的目的,所以实际上临时解决这个漏洞最优雅的方式应该是加上鉴权。

加上鉴权啥问题都没得。

Reference

用Intellij idea搭建solr调试环境

http://lucene.apache.org/solr/guide/6_6/velocity-response-writer.html

2 个赞