This is the Hacking Jenkins series part two! For those people who still have not read the part one yet, you can check 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 :)
Vulnerability Analysis
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 :(
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!
What is Meta-Programming
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 <(_ _)>
How to Exploit?
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 he 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
#definec 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
__int128x[]={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>
structfib {staticconstint value = fib<n-1>::value + fib<n-2>::value;
};
template<> structfib<0> {staticconstint value = 0; };
template<> structfib<1> {staticconstint value = 1; };
intmain(){
int a = fib<10>::value; // 55int b = fib<20>::value; // 6765int 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!
For more examples, you can refer to the article Build a Compiler Bomb on StackOverflow!
First Attempt
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 pwned
Cool, 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!
Second Attempt
We continue 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:
By 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:
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?
The Way to 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?
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:
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!
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 :)
----
2019/07/02 updated
My slides "Hacking Jenkins" for #pts19 and #HITBAMS is out! This is my first time to go French and Pass the SALT(@passthesaltcon) is such a really really good and interesting conference!https://t.co/S4VUf3k1Zq
Some tips to make the exploit more reliable!
1. Check your Java(JAR) version first!
2. There are more than 3 entry points to trigger this vulnerability!
3. Sometimes, the `Grab` failed but the `ASTTest` work perfectly! https://t.co/NcTFyNIcHR
In software engineering, the Continuous Integration and Continuous Delivery is a best practice for developers to reduce routine works. In the CI/CD, the most well-known tool is Jenkins. Due to its ease of use, awesome Pipeline system and integration of Container, Jenkins is also the most widely used CI/CD application in the world. According to the JVM Ecosystem Report by Snyk in 2018, Jenkins held about 60% market share on the survey of CI/CD server.
For Red Teamers, Jenkins is also the battlefield that every hacker would like to control. If someone takes control of the Jenkins server, he can gain amounts of source code and credential, or even control the Jenkins node! In our DEVCORE Red Team cases, there are also several cases that compromised whole the corporation just from a Jenkins server as the entry point!
This article is mainly about a brief security review on Jenkins in the last year. During this review, we found 7 vulnerabilities including:
Among them, the more discussed one is the vulnerability CVE-2018-1999002. This is an arbitrary file read vulnerability through an unusual attack vector! Tencent YunDing security lab has written a detailed advisory about that, and also demonstrated how to exploit this vulnerability from arbitrary file reading to RCE on a real Jenkins site which found from Shodan!
However, we are not going to discuss that in this blogs post. Instead, this post is about another vulnerability found while digging into Stapler framework in order to find a way to bypass the least privilege requirement ANONYMOUS_READ=True of CVE-2018-1999002! If you merely take a look at the advisory description, you may be curious – Is it reality to gain code execution with just a crafted URL?
From my own perspective, this vulnerability is just an Access Control List(ACL) bypass, but because this is a problem of the architecture rather than a single program, there are various ways to exploit this bug! In order to pay off the design debt, Jenkins team also takes lots of efforts (patches in Jenkins side and Stapler side) to fix that. The patch not only introduces a new routing blacklist and whitelist but also extends the original Service Provider Interface (SPI) to protect Jenkins’ routing. Now let’s figure out why Jenkins need to make such a huge code modification!
Review Scope
This is not a complete code review (An overall security review takes lots of time…), so this review just aims at high impact bugs. The review scope includes:
Jenkins Core
Stapler Web Framework
Suggested Plugins
During the installation, Jenkins asks whether you want to install suggested plugins such as Git, GitHub, SVN and Pipeline. Basically, most people choose yes, or they will get an inconvenient and hard-to-use Jenkins.
Privilege Levels
Because the vulnerability is an ACL bypass, we need to introduce the privilege level in Jenkins first! In Jenkins, there are different kinds of ACL roles, Jenkins even has a specialized plugin Matrix Authorization Strategy Plugin(also in the suggested plugin list) to configure the detailed permission per project. From an attacker’s view, we roughly classify the ACL into 3 types:
1. Full Access
You can fully control Jenkins. Once the attacker gets this permission, he can execute arbitrary Groovy code via Script Console!
print"uname -a".execute().text
This is the most hacker-friendly scenario, but it’s hard to see this configuration publicly now due to the increase of security awareness and lots of bots scanning all the IPv4.
Under this mode, all contents are visible and readable. Such as agent logs and job/node information. For attackers, the best benefit of this mode is the accessibility of a bunch of private source codes! However, the attacker cannot do anything further or execute Groovy scripts!
Although this is not the default setting, for DevOps, they may still open this option for automations. According to a little survey on Shodan, there are about 12% servers enabled this mode! We will call this mode ANONYMOUS_READ=True in the following sections.
3. Authenticated Mode
This is the default mode. Without a valid credential, you can’t see any information! We will use ANONYMOUS_READ=False to call this mode in following sections.
Vulnerability Analysis
To explain this vulnerability, we will start with Jenkins’ Dynamic Routing. In order to provide developers more flexibilities, Jenkins uses a naming convention to resolve the URL and invoke the method dynamically.
Jenkins first tokenizes all the URL by /, and begins from jenkins.model.Jenkins as the entry point to match the token one by one. If the token matches (1)public class member or (2)public class method correspond to following naming conventions, Jenkins invokes recursively!
get<token>()
get<token>(String)
get<token>(Int)
get<token>(Long)
get<token>(StaplerRequest)
getDynamic(String, …)
doDynamic(…)
do<token>(…)
js<token>(…)
Class method with @WebMethod annotation
Class method with @JavaScriptMethod annotation
It looks like Jenkins provides developers a lot of flexibility. However, too much freedom is not always a good thing. There are two problems based on this naming convention!
1. Everything is the Subclass of java.lang.Object
In Java, everything is a subclass of java.lang.Object. Therefore, all objects must exist the method - getClass(), and the name of getClass() just matches the naming convention rule #1! So the method getClass() can be also invoked during Jenkins dynamic routing!
2. Whitelist Bypass
As mentioned before, the biggest difference between ANONYMOUS_READ=True and ANONYMOUS_READ=False is, if the flag set to False, the entry point will do one more check in jenkins.model.Jenkins#getTarget(). The check is a white-list based URL prefix check and here is the list:
That means you are restricted to those entrances, but if you can find a cross reference from the white-list entrance jump to other objects, you can still bypass this URL prefix check! It seems a little bit hard to understand. Let’s give a simple example to demonstrate the dynamic routing:
This execution chain seems smooth, but sadly, it can not retrieve the result. Therefore, this is not a potential risk, but it’s still a good case to understand the mechanism!
Once we realize the principle, the remaining part is like solving a maze. jenkins.model.Jenkins is the entry point. Every member in this object can references to a new object, so our work is to chain the object layer by layer till the exit door, that is, the dangerous method invocation!
By the way, the saddest thing is that this vulnerability cannot invoke the SETTER, otherwise this would definitely be another interesting classLoader manipulation bug just like Struts2 RCE and Spring Framework RCE!!
How to Exploit?
How to exploit? In brief, the whole thing this bug can achieve is to use cross reference objects to bypass ACL policy. To leverage it, we need to find a proper gadget so that we can invoke the object we prefer in this object-forest more conveniently! Here we choose the gadget:
In Jenkins, all configurable objects will extend the type hudson.model.Descriptor. And, any class who extends the Descriptor type is accessible by method hudson.model.DescriptorByNameOwner#getDescriptorByName(String). In general, there are totally about 500 class types can be accessed! But due to the architecture of Jenkins. Most developers will check the permission before the dangerous action again. So even we can find a object reference to the Script Console, without the permission Jenkins.RUN_SCRIPTS, we still can’t do anything :(
Even so, this vulnerability can still be considered as a stepping stone to bypass the first ACL restriction and to chain other bugs. We will show 3 vulnerability-chains as our case study! (Although we just show 3 cases, there are more than 3! If you are intersted, it’s highly recommended to find others by yourself :P )
P.S. It should be noted that in the method getUser([username]), it will invoke getOrCreateById(...) with create flag set to True. This result to the creation of a temporary user in memory(which will be listed in the user list but can’t sign in). Although it’s harmless, it is still recognized as a security issue in SECURITY-1128.
1. Pre-auth User Information Leakage
While testing Jenkins, it’s a common scenario that you want to perform a brute-force attack but you don’t know which account you can try(a valid credential can read the source at least so it’s worth to be the first attempt).
In this situation, this vulnerability is useful!
Due to the lack of permission check on search functionality. By modifying the keyword from a to z, an attacker can list all users on Jenkins!
2. Chained with CVE-2018-1000600 to a Pre-auth Fully-responded SSRF
The next bug is CVE-2018-1000600, this bug is reported by Orange Tsai(Yes, it’s me :P). About this vulnerability, the official description is:
CSRF vulnerability and missing permission checks in GitHub Plugin allowed capturing credentials
It can extract any stored credentials with known credentials ID in Jenkins. But the credentials ID is a random UUID if there is no user-supplied value provided. So it seems impossible to exploit this?(Or if someone know how to obtain credentials ID, please tell me!)
Although it can’t extract any credentials without known credentials ID, there is still another attack primitive - a fully-response SSRF! We all know how hard it is to exploit a Blind SSRF, so that’s why a fully-responded SSRF is so valuable!
In order to maximize the impact, I also find an INTERESTING remote code execution can be chained with this vulnerability to a well-deserved pre-auth RCE! But it’s still on the responsible disclosure process. Please wait and see the Part 2! (Will be published on Mid-February :P)
TODO
Here is my todo list which can make this vulnerability more perfect. If you find any of them please tell me, really appreciate it :P
Get the Plugin object reference under ANONYMOUS_READ=False. If this can be done, it can bypass the ACL restriction of CVE-2018-1999002 and CVE-2018-6356 to a indeed pre-auth arbitrary file reading!
Find another gadget to invoke the method getDescriptorByName(String) under ANONYMOUS_READ=False. In order to fix SECURITY-672, Jenkins applies a check on hudson.model.User to ensure the least privilege Jenkins.READ. So the original gadget will fail after Jenkins version 2.138.
Acknowledgement
Thanks Jenkins Security team especially Daniel Beck for the coordination and bug fixing! Here is the brief timeline:
May 30, 2018 - Report vulnerabilities to Jenkins
Jun 15, 2018 - Jenkins patched the bug and assigned CVE-2018-1000600
Jul 18, 2018 - Jenkins patched the bug and assigned CVE-2018-1999002
Aug 15, 2018 - Jenkins patched the bug and assigned CVE-2018-1999046
Dec 05, 2018 - Jenkins patched the bug and assigned CVE-2018-1000861
Dec 20, 2018 - Report Groovy vulnerability to Jenkins
Jan 08, 2019 - Jenkins patched Groovy vulnerability and assigned CVE-2019-1003000, CVE-2019-1003001 and CVE-2019-1003002
In every year’s HITCON CTF, I will prepare at least one PHP exploit challenge which the source code is very straightforward, short and easy to review but hard to exploit! I have put all my challenges in this GitHub repo you can check, and here are some lists :P
This year, I designed another one and it's the shortest one among all my challenges - One Line PHP Challenge!(There is also another PHP code review challenges called Baby Cake may be you will be interested!) It's only 3 teams(among all 1816 teams)solve that during the competition. This challenge demonstrates how PHP can be squeezed. The initial idea is from @chtg57’s PHP bug report. Since session.upload_progress is default enabled in PHP so that you can control partial content in PHP SESSION files! Start from this feature, I designed this challenge!
The challenge is simple, just one line and tell you it is running under default installation of Ubuntu 18.04 + PHP7.2 + Apache. Here is whole the source code:
With the upload progress feature, although you can control the partial content in SESSION file, there are still several parts you need to defeat!
Inclusion Tragedy
In modern PHP configuration, the allow_url_include is always Off so the RFI(Remote file inclusion) is impossible, and due to the harden of new version’s Apache and PHP, it can not also include the common path in LFI exploiting such as /proc/self/environs or /var/log/apache2/access.log.
There is also no place can leak the PHP upload temporary filename so the LFI WITH PHPINFO() ASSISTANCE is also impossible :(
Session Tragedy
The PHP check the value session.auto_start or function session_start() to know whether it need to process session on current request or not. Unfortunately, the default value of session.auto_start is Off. However, it’s interesting that if you provide the PHP_SESSION_UPLOAD_PROGRESS in multipart POST data. The PHP will enable the session for you :P
$ curl http://127.0.0.1/ -H 'Cookie: PHPSESSID=iamorange'
$ ls -a /var/lib/php/sessions/
...
$ curl http://127.0.0.1/ -H 'Cookie: PHPSESSID=iamorange' -d 'PHP_SESSION_UPLOAD_PROGRESS=blahblahblah'
$ ls -a /var/lib/php/sessions/
...
$ curl http://127.0.0.1/ -H 'Cookie: PHPSESSID=iamorange' -F 'PHP_SESSION_UPLOAD_PROGRESS=blahblahblah' -F 'file=@/etc/passwd'
$ ls -a /var/lib/php/sessions/
...sess_iamorange
Cleanup Tragedy
Although most tutorials on the Internet recommends you to set session.upload_progress.cleanup to Off for debugging purpose. The default session.upload_progress.cleanup in PHP is still On. It means your upload progress in the session will be cleaned as soon as possible!
Here we use race condition to catch our data!
(Another idea is uploading a large file to keep the progress)
Prefix Tragedy
OK, now we can control some data in remote server, but the last tragedy is the prefix. Due to the default setting of session.upload_progress.prefix, our SESSION file will start with a annoying prefix upload_progress_! Such as:
In order to match the @<?php. Here we combine multiple PHP stream filter to bypass that annoying prefix. Such as:
In PHP, the base64 will ignore invalid characters. So we combine multiple convert.base64-decode filter to that, for the payload VVVSM0wyTkhhSGRKUjBKcVpGaEtjMGxIT1hsWlZ6VnVXbE0xTUdSNU9UTk1Na3BxVEc1Q2MyWklRbXhqYlhkblRGZEJOMUI2TkhaTWVUaDJUSGs0ZGt4NU9IWk1lVGgy. The SESSION file looks like:
P.s. We add ZZ as padding to fit the previous garbage
After the the first convert.base64-decode the payload will look like:
In past two years, I started to pay more attention on the “inconsistency” bug. What's that? It’s just like my SSRF talk in Black Hat and GitHub SSRF to RCE case last year, finding inconsistency between the URL parser and the URL fetcher that leads to whole SSRF bypass!
So this year, I started focus on the “inconsistency” which lies in the path parser and path normalization!
It’s hard to write a well-designed parser. Different entity has its own standard and implementation. In order to fix a bug without impacting business logic, it’s common to apply a work-around or a filter instead of patching the bug directly. Therefore, if there is any inconsistency between the filter and the called method, the security mechanism can be easily bypassed!
While I was reading advisories, I noticed a feature called URL Path Parameter. Some researchers have already pointed out this feature may lead to security issues, but it still depends on the programming failure! With a little bit mind-mapping, I found this feature could be perfectly applied on multi-layered architectures, and this is vulnerable by default without any coding failure. If you are using reverse proxy with Java as your back-end service, you are under threat!
Back to 2015, it was the first time I found this attack surface was during in a red teaming. After that, I realized this was really cool and I’m curious about how many people know that. So I made a challenge for WCTF 2016.
(I have checked scanners in DirBuster, wFuzz, DirB and DirSearch. Until now, only DirSearch joined the pattern on 1 May, 2017)
WCTF is a competition held by Belluminar and 360. It’s not similar to general Jeopardy or Attack & Defense in other CTF competitions. It invites top 10 teams from all over the world, and every team needs to design two challenges, so there are 20 challenges! The more challenges you solved, the more points you got. However, no one solved my challenge during the competition. Therefore, I think this trick may not be well-known!
This year, I decide to share this technique. In order to convince review boards this is awesome, I need more cases to prove it works! So I started hunting bugs! It turns out that, this attack surface can not only leak information but also bypass ACL(Such as my Uber OneLogin bypass case) and lead to RCE in several bug bounty programs. This post is one of them!
(if you are interested in other stories, please check the slide ASAP!!!)
↓ The inconsistency in multi-layered architectures!
Foreword
First, thanks Amazon for the open-minded vulnerability disclosure. It’s a really good experience working with Amazon security team(so does Nuxeo team). From the Timeline, you can see how quick Amazon’s response was and the step they have taken!
The whole story started with a domain collaborate-corp.amazon.com. It seems to be a collaboration system for internal purpose. From the copyright in the bottom, we know this system was built from an open source project Nuxeo. It’s a very huge Java project, and I was just wanting to improve my Java auditing skill. So the story begins from that…!
Bugs
For me, when I get a Java source, the first thing is to read the pom.xml and find if there are any outdated packages. In Java ecosystem, most vulnerabilities are due to the OWASP Top 10 - A9. known vulnerable components.
Is there any Struts2, FastJSON, XStream or components with deserialization bugs before? If yes. Congratz!
In Nuxeo, it seems most of packages are up to date. But I find a old friend - Seam Framework. Seam is a web application framework developed by JBoss, and a division of Red Hat. It HAD BEEN a popular web framework several years ago, but there are still lots of applications based on Seam :P
I have reviewed Seam in 2016 and found numerous hacker-friendly features! (Sorry, it’s only in Chinese) However, it looks like we can not direct access the Seam part. But still remark on this, and keep on going!
1. Path normalization bug leads to ACL bypass
While looking at the access control from WEB-INF/web.xml, we find Nuxeo uses a custom authentication filter NuxeoAuthenticationFilter and maps /* to that . From the filter we know most pages require authentication, but there is a whitelist allowed few entrance such as login.jsp. All of that is implemented in a method bypassAuth.
As you can see, bypassAuth retrieves the current requested page to compare with unAuthenticatedURLPrefix. But how bypassAuth retrieves current requested page? Nuxeo writes a method to extract requested page from HttpServletRequest.RequestURI, and the first problem appears here!
In order to handle URL path parameter, Nuxeo truncates all the trailing parts by semicolon. But the behaviors in URL path parameter are various. Each web server has it’s own implementation. The Nuxeo’s way may be safe in containers like WildFly, JBoss and WebLogic. But it runs under Tomcat! So the difference between the method getRequestedPage and the Servlet Container leads to security problems!
Due to the truncation, we can forge a request that matches the whitelist in ACL but reach the unauthorized area in Servlet!
In here, we choose login.jsp as our prefix! The ACL bypass may look like this:
$ curl -I https://collaborate-corp.amazon.com/nuxeo/[unauthorized_area]
HTTP/1.1 302 Found
Location: login.jsp
...
$ curl -I https://collaborate-corp.amazon.com/nuxeo/login.jsp;/..;/[unauthorized_area]
HTTP/1.1 500 Internal Server Error
...
As you can see, we bypass the redirection for authentication, but most pages still return a 500 error. It’s because the servlet logic is unable to obtain a valid user principle so it throws a Java NullPointerException. Even though, this still gives us a chance to knock the door!
P.s. Although there is a quicker way to open the door, it’s still worth to write down the first try!
2. Code reuse feature leads to partial EL invocation
As I mentioned before, there are numerous hacker-friendly features in Seam framework. So, for me, the next step is chaining the first bug to access unauthorized Seam servlet!
In the following sections, I will explain these “features” one by one in detail!
In order to control where browser should be redirected, Seam introduces a series of HTTP parameter, and it is also buggy in these HTTP parameters… actionOutcome is one of them. In 2013, @meder found a remote code execution on that. You can read the awesome article CVE-2010-1871: JBoss Seam Framework remote code execution for details! But today, we are going to talk about another one - actionMethod!
actionMethod is a special parameter that can invoke specific JBoss EL(Expression Language) from query string. It seems dangerous but there are some preconditions before the invocation. The detailed implementation can found in method callAction. In order to invoke the EL, it must satisfy the following preconditions:
The value of actionMethod must be a pair which looks like FILENAME:EL_CODE
The FILENAME part must be a real file under context-root
The file FILENAME must have the content "#{EL_CODE}" in it (double quotes and are required)
For example:
There is a file named login.xhtml under context-root.
The previous feature looks eligible. You can not control any file under context-root so that you can’t invoke arbitrary EL on remote server. However, here is one more crazy feature…
To make things worse, if the previous one returns a string, and the string looks like an EL. Seam framework will invoke again!
Here is the detailed call stack:
callAction(Pages.java)
handleOutcome(Pages.java)
handleNavigation(SeamNavigationHandler.java)
interpolateAndRedirect(FacesManager.java)
interpolate(Interpolator.java)
interpolateExpressions(Interpolator.java)
createValueExpression(Expressions.java)
With this crazy feature. We can execute arbitrary EL if we can control the returned value!
This is very similar to ROP(Return-Oriented Programming) in binary exploitation. So we need to find a good gadget!
Why we choose this? It’s because that request.getParameter returns a string that we can control from query string! Although the whole tag is to assign a variable, we can abuse the semantics!
So now, we put our second stage payload in the directoryNameForPopup. With the first bug, we can chain them together to execute arbitrary EL without any authentication! Here is the PoC:
Is that over yet? No really! Although we can execute arbitrary EL, we still failed to pop out a shell. Why?
Let’s go to next section!
4. EL blacklist bypass leads to RCE
Seam also knows that EL is insane. Since Seam 2.2.2.Final, there is a new EL blacklist to block dangerous invocations! Unfortunately, Nuxeo uses the latest version of Seam(2.3.1.Final) so that we must find a way to bypass the blacklist. The blacklist can be found in resources/org/jboss/seam/blacklist.properties.
With a little bit studying, we found the blacklist is just a simple string matching, and we all know that blacklist is always a bad idea. The first time I saw this, I recalled the bypass of Struts2 S2-020. The idea of that bypass and this one is the same. Using array-like operators to avoid blacklist patterns! Just change:
"".getClass().forName("java.lang.Runtime")
to
""["class"].forName("java.lang.Runtime")
Is it simple? Yes! That’s all.
So the last thing is to write the shellcode in JBoss EL. We use Java reflection API to get the java.lang.Runtime Object, and list all methods from that. The index 7 is the method getRuntime() to return a Runtime instance and the index 15 is the method exec(String) to execute our command!
OK! Let’s summarize our steps and chain all together!
Path normalization bug leads to ACL bypass
Bypass whitelist to access unauthorized Seam servlet
Use Seam feature actionMethod to invoke gadgets in file suggest_add_new_directory_entry_iframe.xhtml
Prepare second stage payload in HTTP parameter directoryNameForPopup
Use array-like operators to bypass the EL blacklist
Write the shellcode with Java reflection API
Wait for our shell back and win like a boss ._./
Here is the whole exploit:
OK, by executing the Perl script, we got the shell!
The fix
I will illustrate the fix from 3 aspects!
1. JBoss
As the most buggy thing is on Seam framework. I have reported these “features” to security@jboss.org in Sept 2016. But their reply is:
Thanks very much for reporting these issues to us.
Seam was only included in EAP 5, not 6, or 7. EAP is near the end of maintenance support, which will end in Nov 2016, [1]. The upstream version you used to test was released over 3 years ago.
During maintenance support EAP 5 only receives patches for important or critical issues. While you highlight that RCE is possible, only on the precondition that the attack can first upload a file. This seems to reduce the impact to moderate.
I think we will not bother to fix these security issues at this stage of the Seam project lifecycle.
[1] https://access.redhat.com/support/policy/updates/jboss_notes/
We do appreciate your efforts in reporting these issues to us, and hope that you will continue to inform us of security issues in the future.
So due to the EOL, there seems to be no official patch for these crazy features. However, lots of Seam applications are still running in the world. So if you use Seam. I recommend you to mitigate this with Nuxeo’s fix.
2. Amazon
With a rapid investigation, Amazon security team isolated the server, discussed with the reporter about how to mitigate, and listed every step they have taken in detail! It’s a good experience working with them :)
3. Nuxeo
After the notification from Amazon, Nuxeo quickly released a patch in version 8.10. The patch overrides the method callAction() to fix the crazy feature! If you need the patch for your Seam application. You can refer the patch here!
Timeline
10 March, 2018 01:13 GMT+8 Report to Amazon security team via aws-security@amazon.com
10 March, 2018 01:38 GMT+8 Receive that they are under investigating
10 March, 2018 03:12 GMT+8 Ask that can I join the conference call with security team
10 March, 2018 05:30 GMT+8 Conference call with Amazon, get the status and the step they have taken for the vulnerability
10 March, 2018 16:05 GMT+8 Ask if it’s possible public disclosure on my Black Hat talk
15 March, 2018 04:58 GMT+8 Nuxeo released a new version 8.10 that patched the RCE vulnerability
15 March, 2018 23:00 GMT+8 Conference call with Amazon, know the status and discuss public disclosure details
05 April, 2018 05:40 GMT+8 Reward the award from Amazon