java 模板引擎_Apache Solr Velocity模板注入漏洞深度分析

34f0d0a6d935bfe1230140dd9609942d.png

1. 漏洞分析环境搭建

需要工具

IDEA

Apache Ant

Apache Solr8.2.0源码

Apache Solr8.2.0服务端

Chrome

Burp

2. Apache solr 简介和漏洞复现

首先先简单介绍一下Apache Solr

Apache Solr是一个强大的搜索服务器,它支持像API一样的REST。Solr由Lucene提供支持,可以实现强大的匹配功能,例如短语,通配符,连接,分组和更多的各种数据类型。 它是高度优化的高流量使用Apache Zookeeper。

介绍完Apache Solr之后我们就来复现一下这次的 Apache Solr Velocity

服务端模板注入漏洞

我们首先从Apache Solr官网上下载Apache Solr 8.2.0的服务端

https://mirrors.tuna.tsinghua.edu.cn/apache/lucene/solr/8.2.0/solr-8.2.0.tgz

下载完成之后解压

3fc5b807750279c9778beca8b542e1fa.png

我们通过命令行终端进入bin目录然后输入“./solr start”命令

032bbb1008406c119eba4a904b288a37.png

Apache Solr就会默认在本地的8983端口启动服务,

我们访问一下地址 http://127.0.0.1:8983/solr/#/

查看左侧的Core Selector的集合名称

3f881f1e6981aed5f49ad36978f37976.png

使用burp Repeater模块像服务端发包修改指定集合的配置

7351f9b6ffd479ecf0b2464aa6a165ff.png

修改配置成功

然后发送事先构造好的playload

2f253494660b06b2911ebbcf5f3206c8.png

漏洞复现成功

3. 模板引擎简介

3.1 JSP简介

漏洞复现完成,但是分析漏洞我们还需要一些前置知识,比如什么是模板注入漏洞,以及Velocity究竟是什么,

我们都知道,现在web开发讲究的是一个前后端分离的方式,MVC模式就是其经典的代表。如果抛弃前后端分离,仅仅开发一个能用的网站,只需要一个JSP其实就够了,但是这样很明显会导致开发时逻辑及其混乱,以及后期维护起来成本极高的问题,这样的开发完全违背的我们java这么一个面向对象语言优雅的编程思维。

我们在开发一个程序时希望的就是一个模块尽量是独立完成某一个功能而不依赖别的模块的,也就是我们的高内聚,低耦合的思想。

这种思想用到我们的web开发的架构时,就有了我们的MVC模式,即 Mode,

View,Controller。和我们的web三层架构,即表示层,业务逻辑层,和数据接口层。尽量保证每一层都是独立可用的,在这里特别提示一下,web三层架构是java独有的概念,而MVC架构则是通用的。

在这种情况下,每一层都出现了其相对应开源组件。

首先不得不提的两个使用量最高的MVC框架,Struts2,和SpringMVC。

表现层有我们的JSP和Thymeleaf,Velocity,Freemarker等模板引擎

业务层由我们最火热的开源组件Spring

数据层就有我们最常见的Mybaits和Hibernate两个Dao层框架

而这次我们要重点注意的就是位于我们的表现层,也就是我们的Velocity模板引擎。

对于web不太熟悉的同学可能暂时还不能理解什么是模板引擎,或者说模板引擎是做什么用的。但是相信大家都听过JSP,

JSP的全称是Java Server Package,与普通的静态html页面相比,区别在于我们可以在JSP页面上书写java代码,以实现和用户进行交互,从而达到动态的这么一个效果。

JSP一开始出现的时候是同时兼具前端和后端的作用,也就是说如果只是开发一个勉强能用的java动态网站,jsp其实就足够了。

在JSP出现之前,实现动态页面的效果用的是Servlet的技术,Servlet可以很好的实现接受用户传来的参数并进行处理。但是把数据返回到前端并输出html页面时却异常的麻烦和痛苦。通常需要一行一行的输出html代码,像下面这样

4d0ea621964ef2c2d0f47c5f51f85a45.png

后来JSP出现了,如果说Servlet是java代码中写HTML的话,那Jsp就是HTML中穿插写java代码了,jsp相比于Servlet来说并不是一个新的技术,jsp是Servlet的一个扩展,其本质仍是Servlet,

我们看一个最简单的JSP页面

3c513fe11e34a135f749cc59269894a3.png

看起来就是一个普通的HTML页面,为什么我会说jsp的本质是Servlet呢?

当我们将项目编译打成war包部署在Tomcat下时,会放在Tomcat的WebApp目录下,里面有我们的项目后台的java文件编译成的.class文件。同时也有我们的jsp文件。

但是我们的jsp文件是不能直接被解析的,Jsp不像HTML拿来就能直接返返回给客户,因为jsp文件中是包含有java代码的,浏览器又不能解析我们jsp页面上的java代码,所以将jsp编译成浏览器能解析的html页面的工作就交由了我们的Tomcat来做

当我们启动Tomcat时第一次访问我们的这个jsp页面,往往速度都会稍微慢一些,往后在访问时速度就会很快。这是因为,第一访问时,Tomcat会在他的根目录的work/Catalina/localhos目录下生成我们对应项目名称的一个文件夹。

并生成一个名称为org.apache.jsp的一个package,我们去观察一下

8b15ac419d8ddf1bcb5870e073024cd6.png

我们可以看到一个java文件和一个.class文件。还记得我刚刚才说过jsp的本质其实就是Servlet么?我们点开这个java文件来一探究竟。

d96fe7f08c9db9d0c90b201d80a8495a.png

我们从中观察到这这么几个重点

4d1c3fa78aaeb0380e329205359ec910.png

首先这是一个java类,它继承了HttpJspBase类同时实现了两个接口

第二个重点在这里

d834fd0768fc0d1131dfbf80057a6457.png

这是一个静态代码块,静态代码块在类进行加载时就会执行,先于构造代码块和构造方法,是一个java类中最先被执行的代码。

我们根据其代码内容不难看出这静态代码块的作用是用来import Java类的。

接下来是一个名叫_jspService的函数,是不是特别像servlet的doGET和doPost方法?

02937852a502131398ee49604f21ca6d.png

最后我们再看这里

99a7000ce571b9f098c9981143f145f9.png

我们发现我们之前看到的jsp文件中的html内容,在这里被替换成了通过

JspWriter对象一句一句的写出的。

此时是不是理解了我之前说的,Jsp的本质就是servlet。表面上上我们是在一堆HTML标签中插入了一个又一个的java代码,本质上Tmocat在接收到客户端对我们这个jsp的请求后,会将我们的整个jsp文件编译成java文件在编译成.class文件。将HTML一句一句通过JspWriter对象的write方法一行一行的输出。

3.2 Velocity模板引擎介绍

讲解了JSP的基础知识后不知道大家有没有发现一个问题就是,Jsp虽然说是模板引擎的一种,但是如果只做为一个为前端服务的模板引擎来说,它的功能过于强大了,导致它不光可以书写前端页面,因为JSP可以毫无阻碍地访问底层的 Servlet API 和 Java 编程语言,所以同时也可以无缝书写后端的逻辑代码,在展示数据的同时也可以对数据进行处理。

这样就导致前端和后端完全就纠缠在了一起。完全违背了我们MVC的设计思想,你能想象一个前端页面是用Servlet输出,而后端代码使用Jsp来写的网站该怎么去维护么?

面向对象的优雅思想在这一刻荡然无存。

面向对象的核心思想就是,低耦合,高内聚。每一个模块的功能尽可能单一,尽可能的降低和别的模块和功能之间的耦合度。

所以Thymeleaf,Velocity,Freemarker等优秀模板引擎就一个接一个的出现了。

Velocity为主我们来了解,这个在MVC设计模式中,为View层服务的优秀模板引擎。

刚才通过对Jsp的介绍,我们理解了,一个模板引擎他的主要功能就是负责将后端代码也就是servlet处理完成的数据,提取并按照之前写好的样式展示出来。

Velocity是一个基于java的模板引擎(template engine)。它允许任何人仅仅使用简单的模板语言(templatelanguage)来引用由java代码定义的对象。

当Velocity应用于web开发时,界面设计人员可以和java程序开发人员同步开发一个遵循MVC架构的web站点,也就是说,页面设计人员可以只关注页面的显示效果,而由java程序开发人员关注业务逻辑编码。Velocity将java代码从web页面中分离出来,这样为web站点的长期维护提供了便利,同时也为我们在JSP和PHP之外又提供了一种可选的方案。

前面说了这么多,现在我们在这里简单演示下Velocity这个模板引擎,给大家一个更直观的概念。

首先导入以下的包

98bd30641f783c702ca042309ffe6959.png

然后我们创建一个演示类

25b7f24c3f15a453c6f86ec0fae702a1.png

这里我们首先实例化了一个VelocityEngine,并设置加载加载classpath目录下的vm文件

然后初始化VelocityEngine,接着就是加载一个模板,这里模板的名字叫“Hellovelocity.vm”接下来的操作就是我们向模板的上下文中添加我们要传递的参数和值了。

最后的t.merget就会开始循环遍历生成的Velocity AST语法书的各个节点,执行每个节点的渲染方法。

我们看一下我们加载的这个模板的具体实现

44d57838e45cbac9b8319ff1ce8d45d3.png

和最终的执行结果

6c8f5349cca2531f280d75052a3b789b.png

我们看到这里可以将我们之前后端代码中传输的值直接取出也可以循环取出。

这样我们就可以提前将静态部分用HTML和JavaScript写好,然后需要动态交互的部分就可以使用Velocity语法来进行编写。

4. 漏洞和POC构造分析

4.1 漏洞分析环境搭建

首先我们下载Apache Slor 8.2.0源码

https://mirrors.tuna.tsinghua.edu.cn/apache/lucene/solr/8.2.0/solr-8.2.0-src.tgz

下载完成后

我们进入Solr源码根目录

执行命令

ant ivy-bootstrap

bbdafa31aa18c76349b55140276abf46.png

然后再执行ant idea命令将源码转化成idea可以导入的模式

38bf3b4c4f8052b87436d6edecee24ff.png

然后我们打开idea,选择open

d058ee6fb888a220e7dc3896669c4798.png

最后导入完成后的样子

c3a78c1b901f7a3ddb164f3ca9268ae1.png

为了可以调试源码,我们需要再做一些配置

点开左上角的Edit Configuration

5518753e49c04d32dd18728772a6ed28.png

然后新增Remote

92575f2898be3ba6a887cde7fff0bac2.png

并按照如下配置

7088e3413de72938212f8eae218af3ca.png

配置完成后我们进入solr的服务端的bin目录,并执行如下命令

8094ba74121ca8503c21c8f5bca484e4.png

5ef18eaac9d19e12e1c197568c31bcb9.png

然后我们在idea中点击debug按钮,当有如下显示时代表调试环境搭建成功

7a4f919acfe9ef4eb4868386ff54f251.png

接下来我们就可以在自己想下断点的地方下断点了。

4.2 POC第一部分执行和构造分析

首先我们就来一步一步分析这个漏洞吧,审计一个web项目我们首先先看有没有web.xml这个文件

5a5d9b359358e916ea4050887ba54925.png

我们找到了web.xml这个文件,位置在solr/webapp/WEB-INF/目录下

我们打开看一下内容

首先这个web.xml文件一开始就是一个filter过滤器,这个过滤器类路径是

org.apache.solr.servlet.SolrDispatchFilter,拦截的范围是所有请求

5cb7ae9b981d10046d90646876e49997.png

所以我们首先就需要去这个SolrDispatchFilter这个类去观察

此时我们有两条分析接下来漏洞走向的方式,我们通过查阅网上的资料得知

905cd4d35950791c7debf96ab2d8c9f7.png

我们去目录下查看一下

40aa8637137e522ff4b68b3167b1409b.png

果然有这两个文件

然后我们看下两个文件的部分内容,先看下solrconfig.xml

a42fa25566ba3290d7815c8284d7e194.png

可以看到velocity.params.resource.loader.enabled参数默认是flase,也就是说是默认是不开启的。

我们在看一看configoverlay.json文件

0b51f01d812776e4262a5eae00ea0aea.png

看到这里存储着我们上传上来的参数,这里我们将params.resource.loader.enabled制为true

我们可以通过观察该文件何时被修改来判断,是否该跟进代码中。

然后我们观察poc的时候不难发现请求的API为“/config”

我们通过查阅资料发现

2d6f2d6e8d705ac09611607741a648e2.png

Solr中有很多的RequestHandler,默认配置在solrconfig.xml中,同时也有很多没有配置在solrconfig.xml,称为隐式RequestHandler。而“/config”就是其中之一,我们可以看到SolrConfigHandler便是处理提交我们提交poc的API之一

但是为了,讲的更加清晰,我们还是从SolrDispatchFilter.doFilter方法来一步一步的跟踪。

首先SolrDispatchFilter.doFilter方法执行到第 423行的时候,

会调用HttpSolrCall.call方法

198a6d731f41d69a6d00ac50137359be.png

我们跟进这个方法

然后代码执行到execute()方法时configoverlay.json文件更新了所以我们跟进这个函数

0d4c720922fb36cee0f1263fa3fc2d99.png

继续跟进

2ca78c7f22124092c3f9bd081a9c3ca5.png

按照上面的思路,执行到handler.handleRequest()继续跟进

335f10795da85427279fef42b3f75622.png

335f10795da85427279fef42b3f75622.png

此时就进入到了一开始我们从资料中所看到的“/config”所对应的类SolrConfigHandler,

b9602415e2d0928b7a0a14f124bde731.png

由于此时进入这个函数是为了调用它的handleRequestBody方法,所以我们接着向下执行

这里POST用来修改数据。GET用来查询数据,所以我们执行到

command.handlePOST()方法然后跟进

9b02d5129e70a8598dbaafd5ea53e568.png

执行到handleCommands()方法 此时传入的opsCopy就是我们从前端传入的配置信息,而overlay时当前的配置信息

94467063b4f8d2e9d19a2f1daaafe0a8.png

继续跟进,当执行到SolrResourceLoader.persistConfLocally()方法时

configoverlay.json,文件更新了,

071676ea3692bf94e68b00a4af628069.png

此时我们看到,关键参数是overlay.toButeArray()

db1e2134adf9c06f916c97783921e9de.png

而overlay参数最近的一次赋值动作是在这行代码里进行的,我们先跟进updateNamedPlugin()方法看一看

89e2c3caf65f23e9a3cd1bc71591b0da.png

updateNamedPlugin方法中将op 和overlay参数都传入了进去

当执行到这个if判断时,判断为真,返回overlay,所以关键在于

Verifyclass()这个函数。

这里op仍然为我们 post传入的配置参数 clz的值为“solr.VelocityResponseWriter”

继续跟进

63e56d83cbd75c95542423f1389803c4.png

跟进函数之后我们看到这样一行代码

773096d115184bd45b4f1d93878b3585.png

根据执行逻辑首先执行getCore方法,返回一个SolrCore对象

a0861e2862a24696a3c8ad0fe50b119b.png

af7f68827d2ef315b36fd7a3ba8f15a1.png

然后执行op.getDataMap()方法,返回一个Map对像

1d472113fe39a54feaa920ff6c209320.png

f683835540c3830475bd28141d7b28bb.png

然后new 一个PluginInfo对象,构造方法里的主要操作就是向一个 NameList类型的对象中存值,存入的是我们POST传入的配置参数。

360334e7f9c9497c12fcf393af643bc3.png

9d0ccddaabf66569ddc340f4a1fcafe4.png

5827fe3360215b4d1e99b57f38ffcf77.png

createInitInstance()方法

9e56df004bed516459d7aa5bfb9d3caf.png

泛型变量o是根据我们传入的参数PulginInfo对象的className属性“solr.VelocityResponseWriter”然后通过createInstance()方法反射获得的VelocityResponseWriter对象

因为VelocityResponseWriter对象实现了NamedListInitializedPlugin接口

1b633b27c5d073a089e1e73d1a2526fc.png

所以执行

        4360ec97e9bcde49cf19138978cccdc6.png                 

跟进

然后我们进入了VelocityResponseWriter对象的init方法,在这里有这么几行代码

127ee3742ea3fb67e2437e94467a6776.png

可以看到在这里我们将VelocityResponseWriter对像的两个重要属性

paramsResourceLoaderEnabled,

solrResourceLoaderEnabled

设置为了true,也就是允许我们上传自定义模板了

紧接着init方法执行结束后,就会将VelocityResponseWriter对象按原路返回到SolrConfigHandler并赋值给overly属性

f9a69832386b813532e4cf0110cf538d.png

紧接着执行到第504行代码时configoverlay.json文件更新了,我们跟进这个方法

70a3bbfd481ab5c2d5f4d6677fb52251.png

在调用SolrResourceLoader.persistConfLocally()方法时,可以看到我们将

overly作为参数传递了进去

5a476cb6814b4eee32054a9e165ebecb.png

此时观察代码我们就明白了,真正将我们post传递的配置参数写入文件的操作是在这一步进行的。至此 poc的第一部分追踪完毕。

4.3 POC第二部分执行和构造分析

接下来是poc执行的第二阶段

老规矩先从SolrDispatchFilter类看起

执行到HttpSolrCall.call步入

b6dc3651eef60c2cda8a798d01207459.png

紧接着执行到HttpSolrCall.writeResponse方法

1d71ba070ac9430ef3ac537599955eea.png

观察此刻传入的三个参数,solrRsp参数是一个SolrQueryResponse对象,我们GET传入的playload存储在该对象的value属性中

678553222b6bf9bd21e877aca74e78a0.png

这个responseWriter对象相当重要,这里我们看到了两个参数

paramsResourceLoaderEnabled和solrResourceLoaderEnabled

这是我们poc第一步中修改的两个配置属性,只有这两个属性为true我们才可以上传自定义模板成功

271291963b0e9cd1a4c0405520d0640d.png

responseWriter参数指向的是一个VelocityResponseWriter对象,responseWriter最近一次被赋值是在,下面这行代码中

6499695658255d5fd698b0c832773ea9.png

本着刨根问底,以及锻炼我们分析代码执行逻辑能力的目的,我们深入了解一下

我们跟踪进HttpSolrCall.getResponseWriter方法

b9aae62535fa58f7e361a633bfb51a5b.png

可以看到,这里将我们GET传入的key的值为wt的属性里面的值velocity取出并作为参数传给了core.getQueryResponseWriter方法,core参数指向的是一个SolrCore对象

4f6194f980d548dc6b6d9d0702c2459c.png

跟入SolrCore.getQueryResponseWriter方法

2db3a9e85927bb366a740e8f7704ec51.png

跟入responseWriters.get方法,

此时我们来到了一个PluginBag对象的get方法 

b183935f31ce38fa9e855137b72038ad.png

在执行完T result = get(name)方法后 result的结果中是一个VelocityResponseWriter对象且

paramsResourceLoaderEnabled和solrResourceLoaderEnabled属性都已被置为true,就是说给这两个属性赋值的操作就在get(name)这个方法里。继续跟进

f33740da0d236cc4e3ba0c1e7d00cade.png

还是继续跟进result的无参get方法

1879acea0a49f465b849bb58602436cd.png

到这里,就出现问题了

这里会判断一个名字叫lazyInst的属性是否为空,如果不为空,则返回这个属性。

5dac264ae7be15fd050ae958b25453e2.png

我们来看看此时这个lazyInst属性是什么

d285d30a8b240aa9af2a208740f09519.png

可以看到就是我们最终返回的VelocityResponseWriter对象。

那么问题就来了,我们这执行过程中并没有看到lazyInst对象被赋值,那么lazyInst属性指向的VelocityResponseWriter对象是哪来的呢?

我们会退一步,观察这行代码

PluginHolder result = registry.get(name);

6690732aa497429205c3dd98ce96a1f0.png

registry是一个hashmap类型,有final标识符

f84448728b72acb4ea8c96809403d48b.png

观察此时registry里面的内容

2e213de1a7429eb7124e21dbfabf1997.png

又因为registry.get(name)传入的name参数的值为velocity

我们打开这里的velocity

d60ea7ac7be29db4e85801582a0d0842.png

赫然看到那个lazyInst就在里面,我们知道标示的final的属性就是常量了,在对像生成被赋值了一次以后就不会再更改了。我通过多次发送poc请求测试发现每次到这个断点时当前的对象ID都是相同的,所以每次执行调用的都是同一个对像。

我们重新发送poc的第一部分。Poc第一部分请求完成后再在此处下断点

25086e00509a143e37ea4dd1a7517426.png

此时lazyInst属性就为空了

我们继续执行

8f30e150cec422550658df8ea9642a3c.png

此时由于lazyInst为空了,所以不会直接返回,我们跟进createInst方法,

看到在createInst方法的最后lazyInst属性被赋值,我们向上寻找这个localInst变量

d9e6dc94ff32790165d02b17c486c00e.png

在下面这行代码中localInst第一次被赋值

10e1a73c3b90d84f28386e303fdd1da9.png

此时localInst中的内容为

30e575c05e6d4a996bce11639a6f0ea9.png

也就是说此时程序只是从solrconfig.xml中读取了默认的配置,还并没有读取

configoverlay.json中我们更新的配置。

所以这行就不跟进了。

当执行到initInstance(localInst, pluginInfo)这行代码时

73e9986660115c3256fec8921631fd14.png

我们清楚的看到参数被更新了,那我们就跟入这行代码

2dcf215a7ac65bdc8695ebef2b3dc2a3.png

跟入((NamedListInitializedPlugin)inst).init(info.initArgs)

9f509c237ed0a644b5611cf3b8541e90.png

然后就又看到了我们执行poc第一部分时所碰到的代码了,至此获取

configoverlay.json中我们更新的配置信息的执行逻辑我们已经分析完毕

4d649c6f8be077b248e05a2132dff42b.png

接下来继续原路返回到我们调用HttpSolrCall.getResponseWriter的位置

35104ffd139e97f4661500f7b06a15a8.png

继续跟进writeResponse(solrRsp, responseWriter,reqMethod);

此时solrRsp中存放的是我们Get传入的poc,responseWriter中存放的是我们configoverlay.json文件中存放的更新配置。

跟入QueryResponseWriterUtil.writeQueryResponse方法。

c37329aa72f735bc3df63b8c703f0f46.png

跟入responseWriter.write方法

1eff096d604b36abcbb54d2b68748bae.png

我们执行createEngine()方法时生成了一个VelocityEngine对象

68bbcb3b3cd06c8c0c90f1c28c59ea8d.png

我们进入createEngine()方法后可以看到方法内的第一行代码就是new一个VelocityEngine对象

96457cc3870877f6fcd325579a34e9cc.png

关键点在以下这几行代码,这里对

paramsResourceLoaderEnabled

solrResourceLoaderEnabled两个参数进行了判断,当

paramsResourceLoaderEnabled参数为true时执行

loaders.add("params");

engine.setProperty("params.resource.loader.instance",new SolrParamResourceLoader(request));

30f47ae60531d3d9be9fbeefbfd9141c.png

根据网上查到的资料我们可以看到params.resource.loader.instance这个属性的含义

bf79a144133a1d60b5ec672cd67c8a76.png

也就是说当开启这个属性的时候,我们就可以通过Solr来上传我们自定义的模板了。

最后返回VelocityEngine对象

5abc5cbfb39ee805528f5feee671b3d9.png

返回到responseWriter.write方法,继续执行到

Template template = getTemplate(engine,request);

29025d8f749f4fcc040889fc31393ff4.png

这里我们生成了一个template

跟进去后我们看到

c30a314785770332c6419d89d28b3042.png

从我们Get传入的参数中获取V.template作为模板的名字

bf74728b8747a60b47df3ee9a435a36c.png

fbed04bb7492ca7dac170306b612ddf9.png

同时将我们传入的Poc也就是Velocity模板语句解析成AST抽象语法树

这里就要对velocity的AST抽象语法树做一下简单的介绍了

在计算机科学中,抽象语法树(abstractsyntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。

之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

Velocity是通过JavaCC和JJTree生成抽象语法树的,

javaCC 是一个能生成语法和词法分析器的生成程序。语法和词法分析器是字符串处理软件的重要组件,javacc是类似lex/yacc的parser生成器,可以把一段文本转换为抽象语法树(AST)。

JJTree是javaCC的预处理器,用于在JavaCC生成的源代码中的各个地方插入表示语义动作的分析树

用网上的一张图来介绍一下AST的一些节点

495535e8a52e51af6483e0e4a3a85afd.png

Velocity的语法相对简单,所以它的语法节点并不是很多,总共有50几个,它们可以划分为如下几种类型。

1.    块节点类型:主要用来表示一个代码块,它们本身并不表示某个具体的语法节点,也不会有什么渲染规则。这种类型的节点主要由ASTReference、ASTBlock和ASTExpression等组成。

2.    扩展节点类型:这些节点可以被扩展,可以自己去实现,如我们上面提到的#foreach,它就是一个扩展类型的ASTDirective节点,我们同样可以自己再扩展一个ASTDirective类型的节点。

3.    中间节点类型:位于树的中间,它的下面有子节点,它的渲染依赖于子节点才能完成,如ASTIfStatement和ASTSetDirective等。

4.    叶子节点:它位于树的叶子上,没有子节点,这种类型的节点要么直接输出值,要么写到writer中,如ASTText和ASTTrue等。

我们再来看一下poc中的Velocity语句,和children中的节点信息

#set($x='')

#set($rt=$x.class.forName('java.lang.Runtime'))

#set($ex=$rt.getRuntime().exec('open/Applications/Calculator.app/'))

41e447a3c51f112115f37281c319b850.png

#set最终被解析为Velocity AST语法树中的ASTSetDirective类,根据上面的Velocity AST语法树的图我们看到ASTSetDirective节点有两个字节点

分别是ASTReference,和ASTExpression,

cc209dd4c334e171101d629a5bcef0f7.png

我们看到下标为0的ASTSetDirective类中有两个属性。right和left

分别代表了$x=''中“=”号的两边,左边的ASTReference有两种可能,

一就是用来进行赋值操作的变量名

例:#set( $iAmVariable = 'good!')将字面量“good”赋值给名字为iAmVariable的变量

第二种也是赋值操作,但是赋值操作的目标是一个对象的某个属性

例:#set($Persion.name = 'kkk')

这种赋值方式的本质其实是调用Persion的setName方法。

区分这两种赋值方式我们可以通过观察此时的ASTReference这个节点是否有子节点来判断

譬如第一种#set( $iAmVariable = 'good!') 我们观察一下

5e8d4fd4ac1dd004ab0f29a70e1044ef.png

可以看到最后的children属性为空

再观察第二种#set($Persion.name = 'kkk')

7b9823419defd4cfcc9d86e48773424a.png

可以看到children属性中,是有子节点的。

Velocity通过ASTReference类来表示一个变量和变量的方法调用,ASTReference类如果有子节点,就表示这个变量有方法调用,方法调用同样是通过“.”来区分的,每一个点后面会对应一个方法调用。ASTReference有两种类型的子节点,分别是ASTIdentifier和ASTMethod。它们分别代表两种类型的方法调用,其中ASTIdentifier主要表示隐式的“get”和“set”类型的方法调用。而ASTMethod表示所有其他类型的方法调用,如所有带括号的方法调用都会被解析成ASTMethod类型的节点。

所谓隐式方法调用在Velocity中通常有如下几种。

1.Set类型,如#set($person.name=”junshan”),如下:

·      person.setName(“junshan”)

·      person.setname(“junshan”)

·      person.put(“name”,”junshan”)

2.Get类型,如#set($name=$person.name)中的$person.name,如下:

·      person.getName()

·      person.getname()

·      person.get(“name”)

·      person.isname()

·      person.isName()

接下来我们来看ASTText节点,我们从节点图中看到ASTText没有任何子节点了,它是一个叶子结点,所以这种类型的节点要么直接输出值,要么写到writer中。

60ffccffdc43511a8bc6a58252bf3789.png

到这里我们简单介绍了下Velocity AST语法树的一些基础知识。接下来我们回归我们程序的执行逻辑。

接下来的velocity模板引擎的执行逻辑现在这里简单说明一下,其实也很简单,其实就是会不停的遍历和执行各个子节点中的render方法

首先根据Velocity AST语法树的那张图,我们看到总的根节点是ASTprocess

所以会首先调用ASTprocess的render方法,具体在哪里调用呢,我们来看代码

8352d1d793899fcb74d38384e5ca2c44.png

继续跟入

2da727a379eba3d8f05545b9648f8b83.png

当执行到((SimpleNode)data).render(ica,writer);

这行代码是,我们可以看到此时的data就是ASTprocess节点,所以Template.merge方法中调用了AST的根节点(ASTprocess)的render方法((SimpleNode)data).render(ica,writer);。此调用将迭代处理各个子节点render方法。如果是ASTReference类型的节点则在render方法中会调用execute方法执行反射替换相关处理。

d8684e5e197e2588ba8c272a43d4a2cc.png

当进入到ASTprocess节点的render方法后会根据深度优先遍历算法开始遍历整棵树,遍历算法如下

99be5f520de8a6539d63d89d7db887ce.png

即依次执行当前节点中的所有子节点的render方法,而每个节点的具体渲染规则都在其对应节点的render方法中实现。

这里我们可以打印一下我们poc所生成的语法树的详细结构

71b0aa07ee2434a6e2552989b4f4ca2f.png

有了这个语法树结构后,程序的执行顺序就相当清晰了。

我们首先调用了ASTSetDirective类的render方法,看到该方法中首先调用了ASTExpression类value方法。

c579417d02f365459214953e63a9cd1e.png

而ASTExpression类value方法中又调用了它的子节点ASTStringLiteral

节点的value方法

38177e18762ec5870ccf0b810b6b4a05.png

最后ASTStringLiteral类的value方法返回一个字面量

bb8009085f388e593c5860c38bf77a13.png

接着返回到ASTSetDirective类执行它的第二个子节点也就是等号左边的$x

这里对应的是ASTReference类,这里是调用了ASTReference类的setValue方法

7d9987f7597c17126a0ac2e0eb3f7f0a.png

跟入方法后可以看到,由于该ASTReference节点没有子节点了,所以

直接执行

context.put(rootString, value);这里的value就是我们刚刚获得的“=”号右边的字面量

2216a8af135e470a917251fb98084767.png

我们跟进去看一眼,能看得出后续就是赋值操作了,就不继续深入了

92b3c58e50b16bf727be59bd66e0b791.png

Poc第一行#set($x='')执行完毕

然后开始遍历第二个节点

第二个节点是ASTText节点,这个没什么好说的,就只是直接输出或者写到write中

e87a06c5563b8f791ec6d7756a8d6b4a.png

然后开始遍历第三个节点

第三个节点仍然是ASTSetDirective类,它的render方法中仍然是先执行“=”号右边的子节点ASTExpression类的value方法

当执行到该方法时我们可以看到,此时的ASTExpression节点还有一个子节点,但是不是ASTStringLiteral节点了,而是ASTReference节点

0f92d3f4f86ee71f28b7b45cb13f9b39.png

所以此次执行的将会是ASTReference类的value方法

b97f645ee02be55430667c5e88730267.png

执行execute方法

我们重点看execute中的这行代码

Object result = getVariableValue(context,rootString);

这里返回的是我们给$x所赋的值“”然后程序会判断该值是否为空

如果一开始我们没有执行#set($x='')为$x赋一个值的话,此时会执行下面的

EventHandlerUtil.invalidGetMethod()方法,该方法会因为$x的值为空而不会向下继续执行。

所以我们poc的第一步就需要先为一个变量赋值,赋任何值都可以。

c88848e4721a8fe8f465577148490795.png

接下来执行到下面这些代码时,就开始遍历当前ASTReference的两个子节点

14a5b6b17881a5755adab0737cc16e71.png

e095ee7d9be3307cc1134ad94c6e749b.png

执行完ASTIdentifier类的execute返回一个Class对象

640eee57a3dba88b7fcf00651fe827b1.png

接下来就是遍历第二个节点也就是ASTMethod节点,

执行ASTMethod节点的execute方法。

Execute方法中执行了method的invoke方法跟入

c01033e8f657606ca568ea376269e50e.png

最后调用doInvoke方法

d01009b6fac228d290010651ff393e15.png

我们看一下doInvoke方法的内容

c941d0d5fcf0773a6546f76431a25663.png

这一路下来的反射调用到最终获取Runtime类的class对象我用更直观的方式重写了一下方便理解

148ddda8d941b0c88d4c145a8179086f.png

这一系列的操作等同于Class.forName("java.lang.Runtime")

后面的poc的第三行

#set($ex=$rt.getRuntime().exec('open/Applications/Calculator.app/'))

执行逻辑和上面的如出一辙,就不再深入分析了,感兴趣的朋友可以自己跟踪代码分析一下。

最后放一下最终的一个调用链

6f719a54a8fd2cd073205b62fe36f840.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值