模板引擎注入也算是通用的漏洞类型了,不论是python/java,出现过的cve例子也不少。JSP/FreeMarker/Velocity 作为Struts2 的默认模板引擎,分析分析其实现原理,为以后的漏洞分析作铺垫。
0x01 背景知识
Struts2 Tag
Tag是struts 模板元素的基础单位。建议先阅读参考【6】中《Tag Developers Guide》。
Tag 有以下几类
- 通用tag
- 控制流 tag, 例如 if、else
- 数据tag, 例如include、set、url
- UI tag,例如 from、text、div
- Form tag,例如 checkbox、checkbox、head
- 非Form tag,例如actonerror、actionmessage
模板的逻辑是如何实现的呢?
0x02 JSP
2.1 JSP Demo
1 | <%@ page contentType="text/html;charset=UTF-8" language="java" %> |
2.2 原理解析
2.2.1 编译
众所周知,jsp 运行时会被编译成class。细节如何
- 何时进行的编译?
- 如何进行的编译?
通常情况,是在第一次请求的时候,jsp 预编译成java代码,在编译成java字节码,也即class文件,由jsp引擎实现,目录在CATALINA_BASE 目录下。
具体是通过JDTCompiler,这里不不深入研究。
上文的中Demo 最终编译成的class 代码截取如下:
1 | public final class index_jsp extends HttpJspBase implements JspSourceDependent, JspSourceImports { |
2.2.2 Tag 实现
以_jspx_meth_s_005ftextfield_005f0 为例,最终是通过TextFieldTag class 实现的
- setxxx():初始化,设置一些参数
- doStartTag():获取一些组件信息和属性赋值,总之是些初始化的工作
- doEndTag():在标签解析结束后需要做的事,如调用组件的 end() 方法
类似的Tag 还有很多,如上面一节中提到的
ComponentTagSupport
- BodyTagSupport 是servlet Tag 基础类
- StrutsBodyTagSupport 是struts Tag 基础类,有几个直接子类,ComponentTagSupport/ iterators
- ComponentTagSupport,其中几乎所有的struts tag类都继承ComponentTagSupport
ComponentTagSupport 实现了doStartTag、doEndTag 接口
1 | protected Component component; |
Component
ComponentTagSupport 有个属性Component。什么是Component?可以理解为Tag 到html 的实现。Component 和Tag 基本是一一对应的,是ComponentTagSupport#getBean 接口定义的对应关系。例如TextFieldTag
1 | public class TextFieldTag extends AbstractUITag { |
UIBean
UIBean 继承基础类Component,它的evaluateParams 方法实现了对html 元素属性进行赋值。这里是S2-001 漏洞产生的原因,本文不过多扩展,后文再详细跟踪。
1 | public boolean end(Writer writer, String body) { |
0x03 Velocity
与JSP 编译成class 不一样,Velocity 是实时通过ConfluenceVelocityServlet 进行处理的。大致过程是解析成AST,再计算每个AST节点。
3.1 Velocity Demo
背景知识不详细展开,具体可以参考官方文档【5】,或者文档【4】。直接看个最基础的例子:
1 | # Person.java |
1 | ## hello.vm |
1 | # HelloVelocity.java |
其中需要重点关注的几个概念:
- VelocityEngine
- Template 模板
- Template.merge 接口
- VelocityContext 上下文
- StringWriter
3.2 原理解析
Velocity 也可以算是一种代码语言,也是通过AST 树进行翻译和执行。
3.2.1 生成AST树
还是以上文的hello.vm 为例
涉及到的AST 节点
- ASTText :文本
- ASTReference :引用
- ASTSetDirective set :Set指令
- ASTDirective :指令
- ASTWord :关键字?
- ASTBlock :代码块?
- ASTStringLiteral :字符串
- ASTExpression :表达式
当然,还有其他更多的节点
参考【3】中提到4 大类,这里直接引用
Velocity的语法相对简单,所以它的语法节点并不是很多,总共有50几个,它们可以划分为如下几种类型。
块节点类型:主要用来表示一个代码块,它们本身并不表示某个具体的语法节点,也不会有什么渲染规则。这种类型的节点主要由ASTReference、ASTBlock和ASTExpression等组成。
扩展节点类型:这些节点可以被扩展,可以自己去实现,如我们上面提到的#foreach,它就是一个扩展类型的ASTDirective节点,我们同样可以自己再扩展一个ASTDirective类型的节点。
中间节点类型:位于树的中间,它的下面有子节点,它的渲染依赖于子节点才能完成,如ASTIfStatement和ASTSetDirective等。
叶子节点:它位于树的叶子上,没有子节点,这种类型的节点要么直接输出值,要么写到writer中,如ASTText和ASTTrue等。
Directive
这里展开讲讲扩展节点,velocity默认内置的Directive有以下
在Struts中,有更多的Directive
在Confluence中,有BodyTag/Tag/Param 指令,Tag 的实现是CVE-2021-26084 Ognl注入漏洞的原因。
3.2.2 执行AST
与Ognl 的SimpleNode.getValueBody 接口类似,Velocity 的接口为SimpleNode.render/value
1 | # SimpleNode.java |
这里就个区别,与Ognl不同,result不会作为前一个的参数。
ASTSetDirective
1 | public boolean render( InternalContextAdapter context, Writer writer) |
ASTReference
引用,几个重要的步骤
1 | # ASTReference#render |
ASTStringLiteral
需要注意单双引号的区别
1 | #set( $foo = "bar") |
1 | bar |
可以看到,默认情况单引号不解析Velocity中的可用变量。可以通过改变velocity.properties 中的stringliterals.interpolate=false配置来改变这种默认设置。
1 | private boolean interpolate = true; |
ASTDirective
ASTSetDirective 名称上可能想当然继承ASTDirective,但是并非如此。ASTDirective 有两个属性
- directive 为Directive类型,也就是前文讲到的各种扩展节点
- directiveName
1 | public class ASTDirective extends SimpleNode |
ASTDirective 的render 接口实际调用的是directive 的render,具体的逻辑在每个Directive 实现类里面。
3.3 Struts2 下的Velocity
几个关键的Velocity包
velocity 核心
velocity-tools 和web相关的一些支持
struts-core 有内置velocity的一些支持
在org.apache.struts2.views.velocity 目录下
- components 各种Directive 指令支持,例如form
- StrutsResourceLoader
- StrutsVelocityContext
- VelocityManager
VelocityManager
负责Velocity 的环境准备
1 | public class VelocityManager { |
StrutsVelocityContext
StrutsVelocityContext 继承VelocityContext,且重写了internalGet 借口
1 | public class StrutsVelocityContext extends VelocityContext { |
0x04 总结与思考
本文简要分析了JSP 的执行逻辑,首先编译成class,其中会将jsp 语句分解成各种Tag,Tag 交由Component 进行转化成实际的html 字符。
接下来跟踪了Velocity AST解析与执行的流程,选取了几个常见的AST Node 跟踪。没有什么新东西,只是为以后漏洞分析做准备。
这些都是struts2 系列漏洞分析的基础。精力有限,没有再分析FreeMarker,应该和Velocity 类似,以后遇到相关的在做分析吧。