作者:天融信阿爾法實驗室
公眾號:https://mp.weixin.qq.com/s/h9bhovkDoVdq6HHJcoREvg
1 漏洞分析環境搭建
- 漏洞分析環境搭建
- 需要工具
- IDEA
- Apache Ant
- Apache Solr8.2.0源碼
- Apache Solr8.2.0服務端
- Chrome
- Burp
2 Apache solr簡介和漏洞復現
首先先簡單介紹一下Apache Solr
Apache Solr是一個強大的搜索服務器,它支持像API一樣的REST。 Solr由Lucene提供支持,可以實現強大的匹配功能,例如短語,通配符,連接,分組和更多的各種數據類型。 它是高度優化的高流量使用Apache Zookeeper。
介紹完Apache Solr之后我們就來復現一下這次的 Apache Solr Velocity服務端模板注入漏洞
我們首先從Apache Solr官網上下載Apache Solr 8.2.0的服務端https://mirrors.tuna.tsinghua.edu.cn/apache/lucene/solr/8.2.0/solr-8.2.0.tgz下載完成之后解壓

我們通過命令行終端進入bin目錄然后輸入“./solr start”命令!

Apache Solr就會默認在本地的8983端口啟動服務,
我們訪問一下地址 http://127.0.0.1:8983/solr/#/
查看左側的Core Selector的集合名稱!

使用burp Repeater模塊像服務端發包修改指定集合的配置!

修改配置成功
然后發送事先構造好的payload!

3 模板引擎簡介
3.1 JSP簡介
漏洞復現完成,但是分析漏洞我們還需要一些前置知識,比如什么是模板注入漏洞,以及Velocity究竟是什么,
我們都知道,現在web開發講究的是一個前后端分離的方式,MVC模式就是其經典的代表。如果拋棄前后端分離,僅僅開發一個能用的網站,只需要一個JSP其實就夠了,但是這樣很明顯會導致開發時邏輯及其混亂,以及后期維護起來成本極高的問題,這樣的開發完全違背的我們java這么一個面向對象語言優雅的編程思維。
我們在開發一個程序時希望的就是一個模塊盡量是獨立完成某一個功能而不依賴別的模塊的,也就是我們的高內聚,低耦合的思想。
這種思想用到我們的web開發的架構時,就有了我們的MVC模式,即 Mode,
View,Controller。和我們的web三層架構,即表示層,業務邏輯層,和數據接口層。盡量保證每一層都是獨立可用的,在這里特別提示一下,web三層架構是java獨有的概念,而MVC架構則是通用的。
在這種情況下,每一層都出現了其相對應開源組件。
首先不得不提的兩個使用量最高的MVC框架,Struts2,和SpringMVC。
表現層有我們的JSP和Thymeleaf,Velocity,Freemarker等模板引擎
業務層由我們最火熱的開源組件Spring
數據層就有我們最常見的Mybaits和Hibernate兩個Dao層框架
而這次我們要重點注意的就是位于我們的表現層,也就是我們的Velocity模板引擎。
對于web不太熟悉的同學可能暫時還不能理解什么是模板引擎,或者說模板引擎是做什么用的。但是相信大家都聽過JSP,
JSP的全稱是Java Server Package,與普通的靜態html頁面相比,區別在于我們可以在JSP頁面上書寫java代碼,以實現和用戶進行交互,從而達到動態的這么一個效果。
JSP一開始出現的時候是同時兼具前端和后端的作用,也就是說如果只是開發一個勉強能用的java動態網站,jsp其實就足夠了。
在JSP出現之前,實現動態頁面的效果用的是Servlet的技術,Servlet可以很好的實現接受用戶傳來的參數并進行處理。但是把數據返回到前端并輸出html頁面時確異常的麻煩和痛苦。同常需要一行一行的輸出html代碼,像下面這樣

后來JSP出現了,如果說Servlet是java代碼中寫HTML的話,那Jsp就是HTML中穿插寫java代碼了,jsp相比于Servlet來說并不是一個新的技術,jsp是Servlet的一個擴展,其本質仍是Servlet,
我們看一個最簡單的JSP頁面

看起來就是一個普通的HTML頁面,為什么我會說jsp的本質是Servlet呢?
當我們將項目編譯打成war包部署在Tomcat下時,會放在Tomcat的WebApp目錄下,里面有我們的項目后臺的java文件編譯成的.class文件。同時也有我們的jsp文件。
但是我們的jsp文件是不能直接被解析的,Jsp不像HTML拿來就能直接返返回給客戶,因為jsp文件中是包含有java代碼的,瀏覽器又不能解析我們jsp頁面上的java代碼,所以將jsp編譯成瀏覽器能解析的html頁面的工作就交由了我們的Tomcat來做
當我們啟動Tomcat時第一次訪問我們的這個jsp頁面,往往速度都會稍微慢一些,往后在訪問時速度就會很快。這是因為,第一訪問時,Tomcat會在他的根目錄的work/Catalina/localhos目錄下生成我們對應項目名稱的一個文件夾。
并生成一個名稱為org.apache.jsp的一個package,我們去觀察一下!

我們可以看到一個java文件和一個.class文件。還記得我剛剛才說過jsp的本質其實就是Servlet么?我們點開這個java文件來一探究竟。

我們從中觀察到這這么幾個重點

首先這是一個java類,它繼承了HttpJspBase類同時實現了兩個接口
第二個重點在這里

這是一個靜態代碼塊,靜態代碼塊在類進行加載時就會執行,先于構造代碼塊和構造方法,是一個java類中最先被執行的代碼。
我們根據其代碼內容不難看出這靜態代碼塊的作用是用來import Java類的。
接下來是一個名叫_jspService的函數,是不是特別像servlet的doGET和doPost方法?

最后我們在看這里

我們發現我們之前看到的jsp文件中的html內容,在這里被替換成了通過
JspWriter對象一句一句的寫出的。
此時是不是理解了我之前說的,Jsp的本質就是servlet。表面上上我們是在一堆HTML標簽中插入了一個又一個的java代碼,本質上Tmocat在接收到客戶端對我們這個jsp的請求后,會將我們的整個jsp文件編譯成java文件在編譯成.class文件。將HTML一句一句通過JspWriter對象的write方法一行一行的輸出。
3.2 Velocity模板引擎簡介
講解了JSP的基礎知識后不知道大家有沒有發現一個問題就是,Jsp雖然說是模板引擎的一種,但是如果只做為一個為前端服務的模板引擎來說,它的功能過于強大了,導致它不光可以書寫前端頁面,因為JSP可以毫無阻礙地訪問底層的 Servlet API 和 Java 編程語言,所以同時也可以無縫書寫后端的邏輯代碼,在展示數據的同時也可以對數據進行處理。
這樣就導致前端和后端完全就糾纏在了一起。完全違背了我們MVC的設計思想,你能想象一個前端頁面是用Servlet輸出,而后端代碼使用Jsp來寫的網站該怎么去維護么?
面向對象的優雅思想在這一刻蕩然無存。
面向對象的核心思想就是,低耦合,高內聚。每一個模塊的功能盡可能單一,盡可能的降低和別的模塊和功能之間的耦合度。
所以Thymeleaf,Velocity,Freemarker等優秀模板引擎就一個接一個的出現了。
Velocity為主我們來了解,這個在MVC設計模式中,為View層服務的優秀模板引擎。
剛才通過對Jsp的介紹,我們理解了,一個模板引擎他的主要功能就是負責將后端代碼也就是servlet處理完成的數據,提取并按照之前寫好的樣式展示出來。
Velocity是一個基于java的模板引擎(template engine)。它允許任何人僅僅使用簡單的模板語言(template language)來引用由java代碼定義的對象。
當Velocity應用于web開發時,界面設計人員可以和java程序開發人員同步開發一個遵循MVC架構的web站點,也就是說,頁面設計人員可以只關注頁面的顯示效果,而由java程序開發人員關注業務邏輯編碼。Velocity將java代碼從web頁面中分離出來,這樣為web站點的長期維護提供了便利,同時也為我們在JSP和PHP之外又提供了一種可選的方案。
前面說了這么多,現在我們在這里簡單演示下Velocity這個模板引擎,給大家一個更直觀的概念。
首先導入以下的包

然后我們創建一個演示類

這里我們首先實例話了一個VelocityEngine,并設置加載加載classpath目錄下的vm文件
然后初始化VelocityEngine,接著就是加載一個模板,這里模板的名字叫“Hellovelocity.vm” 接下來的操作就是我們向模板的上下文中添加我們要傳遞的參數和值了。
最后的t.merget就會開始循環遍歷生成的Velocity AST語法書的各個節點,執行每個節點的渲染方法。
我們看一下我們加載的這個模板的具體實現

和最終的執行結果

我們看到這里可以將我們之前后端代碼中傳輸的值直接取出也可以循環取出。
這樣我們就可以提前將靜態部分用HTML和JavaScript寫好,然后需要動態交互的部分就可以使用Velocity語法來進行編寫。
4 漏洞和POC構造分析
4.1 漏洞分析環境搭建
首先我們下載Apache Slor 8.2.0源碼
https://mirrors.tuna.tsinghua.edu.cn/apache/lucene/solr/8.2.0/solr-8.2.0-src.tgz
下載完成后
我們進入Solr源碼根目錄
執行命令
ant ivy-bootstrap

然后再執行ant idea命令將源碼轉化成idea可以導入的模式!

然后我們打開idea,選擇open!

最后導入完成后的樣子

為了可以調試源碼,我們需要再做一些配置
點開左上角的Edit Configuration

然后新增Remote
并按照如下配置

配置完成后我們進入solr的服務端的bin目錄,并執行如下命令


然后我們帶idea中點擊debug按鈕,當有如下顯示時代表調試環境搭建成功

接下來我們就可以在自己想下斷點的地方下斷點了。
4.2 POC第一部分執行和構造分析
首先我們就來一步一步分析這個漏洞吧,審計一個web項目我們首先先看有沒有web.xml這個文件

我們找到了web.xml這個文件,位置在solr/webapp/WEB-INF/目錄下
我們打開看一下內容
首先這個web.xml文件一開始就是一個filter過濾器,這個過濾器類路徑是
org.apache.solr.servlet.SolrDispatchFilter,攔截的范圍是所有請求

所以我們首先就需要去這個SolrDispatchFilter這個類去觀察
此時我們有兩條分析接下來漏洞走向的方式,我們通過查閱網上的資料得知

我們去目錄下查看一下

果然有這兩個文件
然后我們看下兩個文件的部分內容,先看下solrconfig.xml

可以看到velocity.params.resource.loader.enabled參數默認是flase,也就是說是默認是不開啟的。
我們在看一看configoverlay.json文件

看到這里存儲著我們上傳上來的參數,這里我們將params.resource.loader.enabled制為true
我們可以通過觀察該文件何時被修改來判斷,是否該跟進代碼中。
然后我們觀察poc的時候不難發現請求的API為“/config”
我們通過查閱資料發現

Solr中有很多的RequestHandler,默認配置在solrconfig.xml中,同時也有很多沒有配置在solrconfig.xml,稱為隱式RequestHandler。而“/config”就是其中之一,我們可以看到SolrConfigHandler便是處理提交我們提交poc的API之一
但是為了,講的更加清晰,我們還是從SolrDispatchFilter.doFilter方法來一步一步的跟蹤。
首先SolrDispatchFilter.doFilter方法執行到第 423行的時候,
會調用HttpSolrCall.call方法

我們跟進這個方法
然后代碼執行到execute()方法時configoverlay.json文件更新了 所以我們跟進這個函數繼續跟進


按照上面的思路,執行到handler.handleRequest()繼續跟進


此時就進入到了一開始我們從資料中所看到的“/config”所對應的類SolrConfigHandler

由于此時進入這個函數是為了調用它的handleRequestBody方法,所以我們接著向下執行
這里POST用來修改數據。GET用來查詢數據,所以我們執行到
command.handlePOST()方法然后跟進

執行到handleCommands()方法 此時傳入的opsCopy就是我們從前端傳入的配置信息,而overlay時當前的配置信息

繼續跟進,當執行到SolrResourceLoader.persistConfLocally()方法時
configoverlay.json,文件更新了

此時我們看到,關鍵參數時overlay.toButeArray()

而overlay參數最近的一次賦值動作是在這行代碼里進行的,我們先跟進updateNamedPlugin()方法看一看

updateNamedPlugin方法中將op 和overlay參數都傳入了進去
當執行到這個if判斷時,判斷為真,返回overlay,所以關鍵在于
Verifyclass()這個函數。
這里op仍然為我們 post傳入的配置參數 clz的值為“solr.VelocityResponseWriter”
繼續跟進

跟進函數之后我們看到這樣一行代碼

根據執行邏輯首先執行getCore方法,返回一個SolrCore對象


然后執行op.getDataMap()方法,返回一個Map對像


然后new 一個PluginInfo對象,構造方法里的主要操作就是向一個 NameList類型的對象中存值,存入的是我們POST傳入的配置參數



createInitInstance()方法

泛型變量o是根據我們傳入的參數PulginInfo對象的className屬性“solr.VelocityResponseWriter”然后通過createInstance()方法反射獲得的VelocityResponseWriter對象
因為VelocityResponseWriter對象實現了NamedListInitializedPlugin接口

所以執行

跟進
然后我們進入了VelocityResponseWriter對象的init方法,在這里有這么幾行代碼

可以看到在這里我們將VelocityResponseWriter對像的兩個重要屬性
paramsResourceLoaderEnabled,
solrResourceLoaderEnabled
設置為了true,也就是允許我們上傳自定義模板了
緊接著init方法執行結束后,就會將VelocityResponseWriter對象按原路返回到SolrConfigHandler并賦值給overly屬性

緊接著執行到第504行代碼時configoverlay.json文件更新了,我們跟進這個方法

在調用SolrResourceLoader.persistConfLocally()方法時,可以看到我們將
overly作為參數傳遞了進去
此時觀察代碼我們就明白了,真正將我們post傳遞的配置參數寫入文件的操作是在這一步進行的。至此 poc的第一部分追蹤完畢。
4.3 POC第二部分執行和構造分析
接下來是poc執行的第二階段
老規矩先從SolrDispatchFilter類看起
執行到HttpSolrCall.call步入

緊接著執行到HttpSolrCall.writeResponse方法

觀察此刻傳入的三個參數,solrRsp參數是一個 SolrQueryResponse對象,我們GET傳入的playload存儲在該對象的value屬性中

這個responseWriter對象相當重要,這里我們看到了兩個參數
paramsResourceLoaderEnabled和solrResourceLoaderEnabled
這是我們poc第一步中修改的兩個配置屬性,只有這兩個屬性為true我們才可以上傳自定義模板成功

responseWriter參數指向的是一個VelocityResponseWriter對象,responseWriter最近一次被賦值是在,下面這行代碼中

本著刨根問底,以及鍛煉我們分析代碼執行邏輯能力的目的,我們深入了解一下
我們跟蹤進HttpSolrCall.getResponseWriter方法
可以看到,這里將我們GET穿入的key的值為wt的屬性里面的值velocity取出并作為參數傳給了core.getQueryResponseWriter方法,core參數指向的是一個SolrCore對象

跟入SolrCore.getQueryResponseWriter方法

跟入responseWriters.get方法
此時我們來到了一個PluginBag對象的get方法

在執行完T result = get(name)方法后 result的結果中是一個VelocityResponseWriter對象且
paramsResourceLoaderEnabled和solrResourceLoaderEnabled屬性都已被置為true,就是說給這兩個屬性賦值的操作就在get(name)這個方法里。繼續跟進

還是繼續跟進result的無參get方法

到這里,就出現問題了
這里會判斷一個名字叫lazyInst的屬性是否為空,如果不為空,則返回這個屬性

我們來看看此時這個lazyInst屬性是什么,

可以看到就是我們最終返回的VelocityResponseWriter對象。
那么問題就來了,我們這執行過程中并沒有看到lazyInst對象被賦值,那么lazyInst屬性指向的VelocityResponseWriter對象是哪來的呢?
我們會退一步,觀察這行代碼
PluginHolder

registry是一個hashmap類型,有final標識符

觀察此時registry里面的內容

又因為registry.get(name)傳入的name參數的值為velocity
我們打開這里的velocity

赫然看到那個lazyInst就在里面,我們知道標示的final的屬性就是常量了,在對像生成被賦值了一次以后就不會再更改了。我通過多次發送poc請求測試發現每次到這個斷點時當前的對象ID都是相同的,所以每次執行調用的都是同一個對像。
我們重新發送poc的第一部分。Poc第一部分請求完成后再在此處下斷點

此時lazyInst屬性就為空了
我們繼續執行

此時由于lazyInst為空了,所以不會直接返回,我們跟進createInst方法,
看到在createInst方法的最后lazyInst屬性被賦值,我們向上尋找這個localInst變量

在下面這行代碼中localInst第一次被賦值

此時localInst中的內容為

也就是說此時程序只是從solrconfig.xml中讀取了默認的配置,還并沒有讀取
configoverlay.json中我們更新的配置。
所以這行就不跟進了。

我們清楚的看到參數被更新了,那我們就跟入這行代碼

跟入((NamedListInitializedPlugin) inst).init(info.initArgs)

然后就又看到了我們執行poc第一部分時所碰到的代碼了,至此獲取
configoverlay.json中我們更新的配置信息的執行邏輯我們已經分析完畢

接下來繼續原路返回到我們調用HttpSolrCall.getResponseWriter的位置

繼續跟進writeResponse(solrRsp, responseWriter, reqMethod);
此時solrRsp中存放的是我們Get傳入的poc,responseWriter中存放的是我們configoverlay.json文件中存放的更新配置。
跟入QueryResponseWriterUtil.writeQueryResponse方法

跟入responseWriter.write方法

我們執行createEngine()方法時生成了一個VelocityEngine對象

我們進入createEngine()方法后可以看到方法內的第一行代碼就是new一個VelocityEngine對象

關鍵點在以下這幾行代碼,這里對
paramsResourceLoaderEnabled
solrResourceLoaderEnabled兩個參數進行了判斷,當
paramsResourceLoaderEnabled參數為true時執行
loaders.add("params");
engine.setProperty("params.resource.loader.instance", new SolrParamResourceLoader(request));

根據網上查到的資料我們可以看到params.resource.loader.instance這個屬性的含義

也就是說當開啟這個屬性的時候,我們就可以通過Solr來上傳我們自定義的模板了。
最后返回VelocityEngine對象

返回到responseWriter.write方法,繼續執行到
Template template = getTemplate(engine, request);

這里我們生成了一個template
跟進去后我們看到

從我們Get傳入的參數中獲取V.template作為模板的名字


同時將我們傳入的Poc也就時Velocity模板語句解析成AST抽象語法樹
這里就要對velocity的AST抽象語法樹做一下簡單的介紹了
在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這里特指編程語言的源代碼。樹上的每個節點都表示源代碼中的一種結構。
之所以說語法是「抽象」的,是因為這里的語法并不會表示出真實語法中出現的每個細節。
Velocity是通過JavaCC和JJTree生成抽象語法樹的,
javaCC 是一個能生成語法和詞法分析器的生成程序。語法和詞法分析器是字符串處理軟件的重要組件,javacc是類似lex/yacc的parser生成器,可以把一段文本轉換為抽象語法樹(AST)。
JJTree是javaCC的預處理器,用于在JavaCC生成的源代碼中的各個地方插入表示語義動作的分析樹
用網上的一張圖來介紹一下AST的一些節點

Velocity的語法相對簡單,所以它的語法節點并不是很多,總共有50幾個,它們可以劃分為如下幾種類型。
-
塊節點類型:主要用來表示一個代碼塊,它們本身并不表示某個具體的語法節點,也不會有什么渲染規則。這種類型的節點主要由ASTReference、ASTBlock和ASTExpression等組成。
-
擴展節點類型:這些節點可以被擴展,可以自己去實現,如我們上面提到的#foreach,它就是一個擴展類型的ASTDirective節點,我們同樣可以自己再擴展一個ASTDirective類型的節點。
-
中間節點類型:位于樹的中間,它的下面有子節點,它的渲染依賴于子節點才能完成,如ASTIfStatement和ASTSetDirective等。
-
葉子節點:它位于樹的葉子上,沒有子節點,這種類型的節點要么直接輸出值,要么寫到writer中,如ASTText和ASTTrue等。
我們再來看一下poc中的Velocity語句,和children中的節點信息
#set($x='')
#set(x.class.forName('java.lang.Runtime'))
#set(rt.getRuntime().exec('open /Applications/Calculator.app/'))

#set最終被解析為Velocity AST語法樹中的ASTSetDirective類,根據上面的Velocity AST語法樹的圖我們看到ASTSetDirective節點有兩個字節點
分別是ASTReference,和ASTExpression,

我們看到下標為0的ASTSetDirective類中有兩個屬性。right和left
分別代表了$x=''中“=”號的兩邊,左邊的ASTReference有兩種可能,
一就是用來進行賦值操作的變量名
例:#set( $iAmVariable = 'good!')將字面量“good”賦值給名字為iAmVariable的變量
第二種也是賦值操作,但是賦值操作的目標是一個對象的某個屬性
例:#set($Persion.name = 'kkk')
這種賦值方式的本質其實是調用Persion的setName方法。
區分這兩種賦值方式我們可以動過觀察此時的ASTReference這個節點是否有子節點來判斷
譬如第一種#set( $iAmVariable = 'good!') 我們觀察一下

可以看到最后的children屬性為空
再觀察第二種#set($Persion.name = 'kkk')

可以看到children屬性中,是有子節點的。
Velocity通過ASTReference類來表示一個變量和變量的方法調用,ASTReference類如果有子節點,就表示這個變量有方法調用,方法調用同樣是通過“.”來區分的,每一個點后面會對應一個方法調用。ASTReference有兩種類型的子節點,分別是ASTIdentifier和ASTMethod。它們分別代表兩種類型的方法調用,其中ASTIdentifier主要表示隱式的“get”和“set”類型的方法調用。而ASTMethod表示所有其他類型的方法調用,如所有帶括號的方法調用都會被解析成ASTMethod類型的節點。
所謂隱式方法調用在Velocity中通常有如下幾種。
1.Set類型,如#set($person.name=”junshan”),如下: - person.setName(“junshan”)
-
person.setname(“junshan”)
-
person.put(“name”,”junshan”)
2.Get類型,如#set(person.name)中的$person.name,如下:
-
person.getName()
-
person.getname()
-
person.get(“name”)
-
person.isname()
-
person.isName()
接下來我們來看ASTText節點,我們從節點圖中看到ASTText沒有任何子節點了,它是一個葉子結點,所以這種類型的節點要么直接輸出值,要么寫到writer中。

到這里我們簡單介紹了下Velocity AST語法樹的一些基礎知識。接下來我們回歸我們程序的執行邏輯。
接下來的velocity模板引擎的執行邏輯現在這里簡單說明一下,其實也很簡單,其實就是會不停的遍歷和執行各個子節點中的render方法
首先根據Velocity AST語法樹的那張圖,我們看到總的根節點是ASTprocess
所以會首先調用ASTprocess的render方法,具體在哪里調用呢,我們來看代碼

繼續跟入

當執行到((SimpleNode)data).render(ica,writer);
這行代碼是,我們可以看到此時的data就是ASTprocess節點,所以Template.merge方法中調用了AST的根節點(ASTprocess)的render方法((SimpleNode)data).render(ica,writer);。此調用將迭代處理各個子節點render方法。如果是ASTReference類型的節點則在render方法中會調用execute方法執行反射替換相關處理。

當進入到ASTprocess節點的render方法后會根據深度優先遍歷算法開始遍歷整棵樹,遍歷算法如下

即依次執行當前節點中的所有子節點的render方法,而每個節點的具體渲染規則都在其對應節點的render方法中實現。
這里我們可以打印一下我們poc所生成的語法樹的詳細結構

有了這個語法樹結構后,程序的執行順序就相當清晰了。
我們首先調用了ASTSetDirective類的render方法,看到該方法中首先調用了ASTExpression類value方法。

而ASTExpression類value方法中又調用了它的子節點ASTStringLiteral
節點的value方法

最后ASTStringLiteral類的value方法返回一個字面量

接著返回到ASTSetDirective類執行它的第二個子節點也就是等號左邊的$x
這里對應的是ASTReference類,這里是調用了ASTReference類的setValue方法

跟入方法后可以看到,由于該ASTReference節點沒有子節點了,所以
直接執行
context.put(rootString, value);這里的value就是我們剛剛獲得的“=”號右邊的字面量!

我們跟進去看一眼,能看得出后續就是賦值操作了,就不繼續深入了

Poc第一行#set($x='')執行完畢
然后開始遍歷第二個節點
第二個節點是ASTText節點,這個沒什么好說的,就只是直接輸出或著寫到write中

然后開始遍歷第三個節點
第三個節點仍然是ASTSetDirective類,它的render方法中仍然是先執行“=”號右邊的子節點ASTExpression類的value方法
當執行到該方法時我們可以看到,此時的ASTExpression節點還有一個子節點,但是不是ASTStringLiteral節點了,而是ASTReference節點

所以此次執行的將會是ASTReference類的value方法

執行execute方法
我們重點看execute中的這行代碼
Object result = getVariableValue(context, rootString);
這里返回的是我們給$x所賦的值“”然后程序會判斷該值是否為空
如果一開始我們沒有執行#set(x賦一個值的話,此時會執行下面的
EventHandlerUtil.invalidGetMethod()方法,該方法會因為$x的值為空而不會向下繼續執行。
所以我們poc的第一步就需要先為一個變量賦值,賦任何值都可以。

接下來執行到下面這些代碼時,就開始遍歷當前ASTReference的兩個子節點


執行完ASTIdentifier類的execute返回一個Class對象

接下來就是遍歷第二個節點也就是ASTMethod節點,
執行ASTMethod節點的execute方法。
Execute方法中執行了method的invoke方法跟入

最調用doInvoke方法

我們看一下doInvoke方法的內容

這一路下來的反射調用到最終獲取Runtime類的class對象我用更直觀的方式重寫了一下方便理解

這一系列的操作等同于Class.forName("java.lang.Runtime")
后面的poc的第三行
#set(rt.getRuntime().exec('open /Applications/Calculator.app/'))
執行邏輯和上面的如出一轍,就不再深入分析了,感興趣的朋友可以自己跟蹤代碼分析一下。
最后放一下最終的一個調用鏈

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1107/
暫無評論