jenkins 無限制 rce 分析
宣告:僅用於技術研究,不恰當使用造成的危害後果自負
簡言
首先,文章裡沒有直接 rce 的exp,想要exp的大哥抱歉了23333
但是由於執行了 groovy 程式碼,所以瞎j2搞的話我也不知道會出啥問題
挖jenkins好幾個月了,一直莫得比較好用的洞,12.05 公佈越權動態呼叫就搞得心裡癢癢,給重新挖了下,此文章寫的比較隨意,大佬輕噴,歡迎交流
分析過程
先講講偷窺思路
從 orange 大佬12.20公佈發現了一個 jenkins 未授權 rce 開始,我就一直在試圖將其挖掘出來,一直到 1.16 大佬公佈第一部分 Jenkins 動態路由利用這篇文章,才真正拿到觸發鏈,不過到最後 groovy sandbox 繞過實在是不會了......
在 orange 的文章中,其實幫助最大的還是貼出了官方的一個漏洞通告連結(沒有收集漏洞通報的良好習慣2333)和對 Descriptor 的理解還有利用
官方在 1.8 號公佈了一個通告: https://jenkins.io/security/advisory/2019-01-08/#jenkins-security-advisory-2019-01-08
大致講的是 pipeline 這個外掛裡對groovy指令碼進行解析的時候會出現 sandbox 被繞過的情況以及 REST API 也會直接訪問到,但是他是需要 Overall/Read 許可權的
這裡我思考了一會兒,雖然這通告涉及到 bypass groovy sandbox,一定程度上和 RCE 有關聯,但是它需要許可權的。Overall/read 在jenkins 中屬於比 ANONYMOUS 許可權高一丁點的許可權,但是它預設是 FALSE 的,就是預設配置安裝的 Jenkins 是沒有 Overall/read 許可權的,不登入的情況下只有 ANONYMOUS 許可權,然後呢我們需要的是一個未授權 RCE ,所以這裡很有可能是一個最後的一個任意程式碼執行的地方,那麼還得需要去尋找繞過許可權檢查的觸發路由(這裡我沒有對 REST API 直接訪問導致的 RCE 做研究)
在更早的時候,官方有通告如下:
https://jenkins.io/security/advisory/2018-12-05/
這也是 CVE-2018-1000861 ,造成的影響呢是能夠一定程度上呼叫 Jenkins 中的任意 getter 函式,造成了越權的情況
將兩者結合起來的話就很有可能是未授權 RCE
首先因為我之前一直在搞 Jenkins 的反序列化黑名單,所以對其路由解析過程算是比較熟悉(因為不熟悉ACL機制,甚至當時根本沒聽說過,導致沒有察覺到這個任意 getter 呼叫的漏洞點,只是覺得jenkins的路由對映做的很奇怪233333),所以能在拿到官方通報後就能猜到整體觸發流程,接下來我會先對 CVE-2018-1000861 做一點簡單的分析,然後再從尋找 RCE 的角度去分析挖掘方式
然後還有想說的就是,從通告中可以直接拿到外掛程式碼diff,可以很方便的找到漏洞點
動態路由形成過程
CVE-2018-1000861 其實還是和之前 orange 大佬發現的 Jenkins 任意檔案讀取相關核心: Stapler 有關係
它對於 Jenkins 來說就是一個小型路由生成器
完完全全可以直接從 web.xml 開始跟入 Stapler 類中 service 函式的,因為官方補丁diff的話,反而找不到 Jenkins 路由生成過程,對後面的漏洞挖掘會造成一定理解上的困難
我不貼 service 函式的程式碼,簡單說說就好,service 函式中最後呼叫到了 invoke 函式,一直跟著 invoke 關鍵字的話,會進入到 Stapler 中的 tryInvoke 函式中,關鍵程式碼塊如下:
大致意思是,會得到 MetaClass 的一個物件,然後對請求包進行輪詢呼叫其中的 dispatcher.disspatch,這個 dispatcher 就是一個個“節點”下的小路由,比如說一個請求的url:
那麼它先對 123
這個節點匹配對應的 dispatcher ,然後在 dispatch 中進行反射呼叫具體函式,然後再對 abc
這個節點做匹配以此類推
那麼關鍵就是 dispatcher 的生成了,跟進 getMetaClass 函式
主要是根據傳入的 node 變數生成了一個 MetaClass 物件,node 變數經過了一定包裝,大致是獲取了類相關資訊,繼續跟進 MetaClass 看看
記錄 node 的各種資訊,然後呼叫 buildDispatchers 函式,函式體太長總共301行就不貼出來了,這個函式主要功能就是對 node 對應的 class 、此 class的父類、此 class繼承類 (簡單來說就是繼承家族樹中的所有類) 進行一個函式資訊提取,然後獲取指定的函式相關資訊,做一個函式反射呼叫和 url 節點名稱的儲存,儲存在 MetaClass.dispatchers 中,這就是製作路由的過程了,也是 orange 文章中提到的,如下:
只要在繼承家族樹中,任意類滿足在以上 11 種規則的函式,統統可以直接在 URL 中訪問到(get的意思是以 get
開始的名字,do 和 js 同理)
然後呢,我隨便截一個 dispatcher 的生成過程,如下圖:
那麼這裡整個就是一次對當前 node 進行動態路由製作的過程了,如果思路延展一下,可以發現整個是一個迭代的過程,我稍微描述下:
先解析 root 節點(也就是第一次傳入的 node ),然後對 123 做適配,匹配到的是滿足上圖中 11 中規則的並且存在於當前 root 節點家族樹的函式,並對他進行反射呼叫,目標函式流程走完了後,根據返回結果型別進行下一步處理,因為在上圖中的 req.getStapler().invoke 呼叫中,目標函式的返回結果傳遞給了第三個形參,如下:
其實就是一個新的 node ,那麼對 abc 做適配的,就是新 node 的家族樹中的函數了,以此遞迴下去,直到匹配出錯或者所有節點匹配完成
那現在我們看一看,誰是 root node :
- 如果 url 是以 /$stapler/bound/ 開頭的話,就 org.kohsuke.stapler.bind.BoundObjectTable 為 root
- 如果其他的話就是以 Jenkins.model.Jenkins 為 root
上者和 Object 繫結功能相關,預設情況下是不會有什麼利用點的
稍微看看 Jenkins 類的一部分家族樹,如下圖:
所以我們訪問 Jenkins 的時候,第一個解析的路由節點,就是上圖中滿足那11個規則的函式
現在問題來了,這些類中所有滿足規則的函式都能訪問嗎,那豈不是幾乎沒有任何限制了。
繞過路由訪問限制
那麼限制在哪兒呢?
還是在 Stapler 類中 tryInvoke 函式中,函式一開頭就做了一個操作如下:
如果是 StaplerProxy 的實現類,那麼就會呼叫當前 node 的 getTarget() 函式,從家族樹中看見 Jenkins 這個類確實實現了 StaplerProxy,那麼它的 getTraget 函式如下:
上圖中會檢查當前使用者是否擁有 READ 許可權,如果沒有的話會丟擲異常,然後進入 isSubjectToMandatoryReadPermissionCheck 函式中,並且帶入了當前訪問的路由節點名,如下:
滿足上面三個條件的路由節點名,都會放行當前請求通過,如果都不滿足則記錄請求並轉到 login 視窗
檢視一下 ALWAYS_READABLE_PATHS :
上圖中的節點路由都可以通過,不過這數量少得可憐。但是其中 securityRealm 就是繞過 ACL 限制的跳板入口。
這裡不禁想起來自己挖洞的時候記錄2333333,如下:
瘋狂打臉233333,因為當時草草看了下,一心對反序列化黑名單著迷,沒有對相關函式的返回型別做家族樹調查
跳-跳-跳
對 /securityRealm 訪問時進行動態除錯發現,返回的是 Hudson.security.HudsonPrivateSecurityRealm 類,我們跟過去,檢視其中的 getUser 函式,如下:
這裡根據傳入的下一節點名當做 id,然後生成一個 User 出來,稍微測試了下,不存在的使用者也能正常生成 User,未對這個原因進行深究,此時目標函式返回的是 User 類。我們看看 User 類的家族樹,找到一個關鍵點如下:
檢視 getDescriptorByName 如下:
其實就是呼叫了 Jenkins.getDescriptorByName,這個函式主要根據傳入的 id(String),然後獲取到程式中所有繼承了 Descriptor 的子類
總結下這裡的利用類連續跳動過程:
Jenkins -> HudsonPrivateSecurityRealm -> User -> DescriptorByNameOwner -> Jenkins -> Descriptor
Descriptor 繞過 ACL 的主角
Descriptor ,從這個類名都能感覺到,是描述功能相關,並且其中擁有大量的 getter ,從設計上思考的話,這個 Descriptor 很有可能是會對很多功能點的相關描述
動態調了下,預設配置的 Jenkins 擁有約579個 Descriptor
如何尋找 RCE
其實在研究動態路由的過程中,就發現了,想要 RCE 還是要依靠外掛中的一些指令碼解析功能才行,但是突然懶癌發作,看著一堆外掛就不想動手去分析了23333
這裡我們還是簡單一點,根據官方漏洞通告尋找補丁diff:
https://github.com/jenkinsci/workflow-cps-plugin/commit/d09583eda7898eafdd15297697abdd939c6ba5b6從中看見幾個修改的類檔案:
src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowDefinition.java
src/main/java/org/jenkinsci/plugins/workflow/cps/CpsGroovyShellFactory.java
src/main/java/org/jenkinsci/plugins/workflow/cps/replay/ReplayAction.java
稍微篩選下,就能找出 CpsFlowDefinition 才是主角(雖然通過 ReplayAction 也能夠觸發,但是根據我的跟蹤中發現需要一定許可權才可以)
檢視關鍵點 CpsFlowDefinition$DescriptorImpl 如下:
繼承的 FlowDefinitionDescriptor ,這個類繼承自 Descriptor,上圖中有兩個滿足那11個規則的函式,其中帶上 @QueryParameter 註解的引數都可以通過引數請求傳遞進來
OK,到此為止已經拿到了 Jenkins 的無限制 RCE 觸發鏈,但是最終它是解析 Groovy 指令碼的,並且似乎上了沙盒,雖然官方補丁diff中含有一點 bypass sandbox 的技術點,但是我對 groovy 是一竅不通,搞了好幾天都沒辦法,各位師傅如果有經驗的話,試試呢?
目前為止的效果
都是官方補丁diff的bypass
使用 Grab 註解如下:
使用 ASTTest 註解如下:
總結(我菜如狗)
Stapler 的動態路由製作過程
Jenkins 本身的白名單路由
Descriptor 的利用,這裡的利用過程相當曲折,從 Jenkins 入口跳出去最終再跳到 Jenkins 自己這裡獲取 Descriptor ,然後再從各種繼承類中尋找到 groovy 解析的利用點,膜 orange
Groovy sandbox 的繞過,實在是不會弄了,Orz,求大哥們教教
Links:
hacking-Jenkins-part1-play-with-dynamic-routing:
https://devco.re/blog/2019/01/16/hacking-Jenkins-part1-play-with-dynamic-routing/
jenkins官方通告:
https://jenkins.io/security/advisory/2019-01-08/#jenkins-security-advisory-2019-01-08
https://jenkins.io/security/advisory/2018-12-05/
pipeline-groovy外掛相關:
https://plugins.jenkins.io/workflow-cps
https://github.com/jenkinsci/workflow-cps-plugin/commit/d09583eda7898eafdd15297697abdd939c6ba5b6