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 进行处理。
选择在 SolrRequestFilter 的 dofilter 处下个断点,可以看到请求过来了。
这 dofilter 方法中,这里会先获取我们 web.xml 中的 excludePatterns 的路径进行正则比对。
代码继续下行,来到这里,跟进 getHttpSolrCall 方法。
在 getHttpSolrCall 方法中最后的处理结果是返回一个 HttpSolrCall 对象。
紧接着调用call方法,来到了 HttpSolrCall#call 中。
跟进 HttpSolrCall#call 中,一直往下走,代码来到了这里,根据 switch 进行选择,这里的 action 是 PROCESS ,所以自然进入 PROCESS 的 case 中进行处理,然后调用this.execute
方法来处理 solrRsp 对象。
跟进 HttpSolrCall#execute 中,首先调用 getCore 方法获取 SolrCore 对象,然后调用 SolrCore#execute 方法。
跟进 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$command 的 handleCommands 方法,这里通过 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
而 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.enabled
和solr.resource.loader.enabled
的结果,所以这里自然是true。
跟进 SolrParamResourceLoader 类,这里遍历循环我们payload中的数据,当 name 为 v.template ,我们在payload中的 v.template 的值是 custom ,所以这里实际上是生成了 custom.vm 的恶意 engine 。
代码回到 createEngine 方法中,这里自然 paramsResourceLoaderEnabled 是true,原因不在细说,上面所过了。
执行完这两个if之中的 SolrParamResourceLoader 和 SolrVelocityResourceLoader 操作之后,代码回到 VelocityResponseWriter#write 中,调用 VelocityResponseWriter#getTemplate 方法处理刚刚的engine对象,以及我们的request请求对象。
在 VelocityResponseWriter#getTemplate 方法中回根据我们提交的 v.template 参数获取我们构造的恶意 template 对象。
最后在调用我们构造好的恶意 template 的 merge 方法,达到命令模版注入的效果。
实际上模版进入到 template 的 merge 方法,然后及时一些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
http://lucene.apache.org/solr/guide/6_6/velocity-response-writer.html