概述
Beetl 是一款高性能、开源的 Java 模板引擎,它提供了简洁而强大的模板语法,使开发者能够方便地生成动态的文本输出。Beetl 模板引擎具有灵活的配置选项和丰富的功能,可以广泛应用于 Web 开发、邮件生成、代码生成等领域。
安装和配置
-
下载 Beetl 模板引擎的 JAR 文件,可以从官方网站或 Maven 仓库获取。
-
将 JAR 文件添加到项目的依赖中。
-
根据项目的需求,配置 Beetl 模板引擎的相关参数,如模板目录、缓存配置、字符编码等。
模板语法
Beetl 模板引擎使用一种简洁而直观的模板语法,让开发者能够轻松地构建动态的文本输出。
输出变量
使用 ${}
语法可以在模板中输出变量的值。例如,${name}
表示输出变量 name
的值。
<p>欢迎您,${name}!</p>
条件判断
使用 if
和 else
来进行条件判断。示例:
<if(user.isAdmin)>
<p>您是管理员。</p>
<else>
<p>您不是管理员。</p>
</if>
循环遍历
使用 for
循环语句来遍历集合或数组中的元素。示例:
<ul>
<for(item in itemList)>
<li>${item}</li>
</for>
</ul>
定义变量
使用 #set
来定义变量并赋值。示例:
#set(username = "John")
<p>您好,${username}!</p>
调用方法
使用 ${}
语法调用对象的方法,并输出方法的返回值。示例:
<p>当前时间:${new java.util.Date().toString()}</p>
高级特性
Beetl 模板引擎还提供了一些高级特性,用于处理更复杂的场景。
模板继承
模板继承允许开发者定义一个基础模板,并在其基础上创建其他模板。子模板可以重写父模板中的区块内容,实现模板的复用和扩展。示例:
父模板 layout.btl
:
<!DOCTYPE html>
<html>
<head>
<title>${title}</title>
</head>
<body>
<div id="content">
<!-- 子模板可重写的区块 -->
#block("content")
<p>这是默认内容。</p>
#end
</div>
</body>
</html>
子模板 index.btl
:
#extends("layout.btl")
#block("content")
<h1>欢迎使用 Beetl 模板引擎!</h1>
#end
自定义函数
Beetl 允许开发者定义自己的函数,并在模板中调用。示例:
定义自定义函数:
public class MyFunctions {
public static String reverse(String str) {
return new StringBuilder(str).reverse().toString();
}
}
在模板中调用自定义函数:
<p>逆序输出:${reverse("Hello")}</p>
使用 Beetl 模板引擎
使用 Beetl 模板引擎的基本步骤如下:
-
创建一个
GroupTemplate
对象,作为模板引擎的入口点。 -
根据需要,配置
GroupTemplate
对象的相关参数,如模板目录、缓存配置等。 -
使用
GroupTemplate
对象的getTemplate()
方法获取模板对象。 -
将数据填充到模板中,可以通过设置模板对象的变量或传入数据模型。
-
调用模板对象的
render()
方法,将模板渲染为最终的文本输出。
以下是一个使用 Beetl 模板引擎的示例代码:
import org.beetl.core.Configuration;
import org.beetl.core.GroupTemplate;
import org.beetl.core.Template;
public class BeetlDemo {
public static void main(String[] args) throws IOException {
// 创建配置对象
Configuration configuration = Configuration.defaultConfiguration();
// 创建模板引擎
GroupTemplate groupTemplate = new GroupTemplate(configuration);
// 获取模板对象
Template template = groupTemplate.getTemplate("/path/to/template.btl");
// 设置模板变量
template.binding("name", "John");
// 渲染模板并输出结果
String result = template.render();
System.out.println(result);
}
}
漏洞演示
模板注入分析
自建一个demo,用来进行演示
跟踪getTemplate
/** 获取指定模板。
* 注意,不能根据Template为空来判断模板是否存在,请调用ResourceLoader来判断
* @param key
* @return
*/
public Template getTemplate(String key)
{
return getTemplateByLoader(key, this.resourceLoader,true);
}
调用getTemplateByLoader
使用gpt进行解读
-
将参数
key
转换为内部字符串常量,以便在后续使用中进行比较和同步。 -
从缓存中根据
key
获取Program
对象。 -
如果
program
为空,则进入同步块。 -
在同步块内部重新检查
program
是否为空,以防止在获取锁之前其他线程已经加载了模板。 -
使用提供的
loader
对象获取资源resource
。 -
调用
loadTemplate
方法加载模板,传递resource
和isTextTemplate
参数。 -
将加载后的
program
对象存储在programCache
中,使用key
作为键。 -
创建一个新的
Template
对象,传递当前对象、program
和conf
参数,并将其返回。
如果 program
不为空,则执行以下操作:
-
检查
resourceLoader
是否修改了program.rs
(可能是资源的时间戳或版本号等)。 -
如果
resourceLoader
修改了program.rs
,则再次进入同步块。 -
使用提供的
loader
对象获取资源resource
。 -
调用
loadTemplate
方法加载模板,传递resource
和isTextTemplate
参数。 -
将加载后的
program
对象存储在programCache
中,使用key
作为键。
最后,无论是从缓存中获取的 program
还是重新加载的 program
,都会创建一个新的 Template
对象,并将其返回。
总体来说,这段代码的目的是根据给定的 key
使用提供的 loader
加载模板资源,并将加载的模板缓存起来。如果缓存中已经存在相应的模板且未被修改,则直接使用缓存的模板,否则重新加载模板并更新缓存。最终返回一个包装了模板的 Template
对象。
if判断key是否为空,这里的key不为空,所以进入下面的代码
if (resourceLoader.isModified(program.rs))
{
synchronized (key)
{
Resource resource = loader.getResource(key);
program = this.loadTemplate(resource,isTextTemplate);
this.programCache.set(key, program);
}
}
-
首先声明一个变量
sf
,类型为Transformator
,并将其初始化为null
。 -
在
try
块中进行模板加载的逻辑处理。 -
调用
res.openReader()
方法打开模板资源的读取器,获取模板内容的Reader
对象。 -
根据isTextTemplate参数的值,决定创建不同类型的Transformator对象。
-
如果
isTextTemplate
为true
,则创建一个Transformator
对象,使用配置中的占位符和语句定界符。 -
如果
isTextTemplate
为false
,则根据配置中是否具有函数限制符来决定创建哪种类型的Transformator
对象。 -
如果配置中启用了 HTML 标签支持,则调用
sf.enableHtmlTagSupport()
方法,传入配置中的 HTML 标签定界符和绑定属性信息。 -
声明一个变量
scriptReader
,类型为Reader
,并将其初始化为sf.transform(reader)
的结果。这将对模板内容进行转换,并返回转换后的脚本内容的Reader
对象。 -
使用
engine.createProgram()
方法创建一个Program
对象,传入资源res
、转换后的脚本内容的scriptReader
、sf
的文本映射和行分隔符信息,以及当前对象。 -
返回创建的
Program
对象。
在 try
块中,如果发生以下异常,则会执行相应的异常处理逻辑并返回特定的 ErrorGrammarProgram
对象:
-
如果发生
HTMLTagParserException
异常,则创建一个ErrorGrammarProgram
对象,传入资源res
、当前对象和sf
的行分隔符信息,并将异常设置为其属性。然后将异常关联到资源,并返回ErrorGrammarProgram
对象。 -
如果发生
IOException
异常,则创建一个ErrorGrammarProgram
对象,传入资源res
、当前对象和sf
的行分隔符信息。然后创建一个BeetlException
对象,并将其关联到资源。将BeetlException
设置为ErrorGrammarProgram
的异常,并返回ErrorGrammarProgram
对象。 -
如果发生
BeetlException
异常,则创建一个ErrorGrammarProgram
对象,传入资源res
、当前对象和sf
的行分隔符信息。将异常关联到资源,并将异常设置为ErrorGrammarProgram
的异常。最后返回ErrorGrammarProgram
对象。
总体来说,这段代码的目的是根据给定的资源 res
和 isTextTemplate
参数加载模板,并将加载的模板转换为脚本内容,创建一个 Program
对象并返回。如果加载或转换过程中发生异常,将会返回一个特定的 ErrorGrammarProgram
对象,其中包含异常信息。
查看programCache
Cache programCache = ProgramCacheFactory.defaulCache();
跟踪defaulCache
-
声明了一个公共静态常量
CACHE
,类型为字符串,值为"org.beetl.core.cache.LocalCache"
。该常量表示缓存实现类的类名。 -
声明了一个公共静态方法
defaultCache()
,返回类型为Cache
。该方法用于获取默认的缓存实例。 -
在方法内部,首先获取当前线程的上下文类加载器,并将其赋值给
loader
变量。 -
如果
loader
为空,即当前线程没有上下文类加载器,那么将loader
设置为ProgramCacheFactory
类的类加载器。 -
调用
ObjectUtil.instance(CACHE, loader)
方法,使用loader
加载CACHE
所指定的缓存实现类,并实例化为一个Cache
对象。 -
返回实例化后的
Cache
对象。
总体来说,这段代码的目的是提供一个工厂方法 defaultCache()
,用于获取默认的缓存实例。该工厂方法通过反射加载指定的缓存实现类,并返回实例化后的缓存对象。默认情况下,缓存实现类的类名为 "org.beetl.core.cache.LocalCache"
。
instance 万物皆可序列化
最后一步就是instance,通过反射加载代码
在最新版源码中,存在黑名单
public boolean permit(Object resourceId, Class c, Object target, String method) {
if (c.isArray()) {
// 允许调用,但实际上会在在其后调用中报错。不归此处管理
return true;
}
String name = c.getName();
String className = null;
String pkgName = null;
int i = name.lastIndexOf('.');
if (i != -1) {
pkgName = name.substring(0, i);
className = name.substring(i + 1);
} else {
// 无包名,允许调用
return true;
}
if (pkgName.startsWith("java.lang")) {
return !className.equals("Runtime") && !className.equals("Process") && !className.equals(
"ProcessBuilder") && !className.equals("Thread")
// https://gitee.com/xiandafu/beetl/issues/I6RUIP
&& !className.equals("Class") //https://gitee.com/xiandafu/beetl/issues/I6RUIP#note_17223442
&& !className.equals("System");
}
if (pkgName.startsWith("javax.")) {
return false;
}
if (pkgName.startsWith("sun.")) {
return false;
}
return true;
}
但可以通过js进行绕过,从而实现rce。
${@Class.forName(%22javax.script.ScriptEngineManager%22).newInstance().getEngineByName(%22js%22).eval(%22s=%27open%20-a%20Calculator%27;java.lang.Runtime.getRuntime().exec(s);%22)}
原文始发于微信公众号(轩公子谈技术):利用GPT进行代码审计-Beelt模板注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论