Deserializing untrusted data can cause Java to create an object of an arbitrary attacker-specified class, provided that the class is available on the classpath specified for the JVM. Some classes have triggers that execute additional code when they are created in this manner; see SEC58-J. Deserialization methods should not perform potentially dangerous operations for more information. If such classes are poorly designed, such code could even invoke arbitrary methods, such as Runtime.exec()
with an attacker-supplied argument. Therefore, untrusted input to be deserialized should be first validated to ensure that the serialized data contains only trusted classes, perhaps specified in a whitelist of trusted classes. This can be done by overriding the resolveClass()
method of the java.io.ObjectInputStream
class.
This rule applies only to untrusted data. Data that does not cross a program's trust boundary is, by definition, trusted and can be deserialized without violating this rule. See the exception SER12-EX0 for more information.
Deserialization of data that is trusted but must cross a trust boundary (perhaps because it originates from a different host) automatically complies with this rule, but must also comply with SER02-J. Sign then seal objects before sending them outside a trust boundary for more information.
Non-Compliant Code Example
This non-compliant code deserializes a byte array without first validating what classes will be created. If the object being deserialized belongs to a class with a lax readObject()
method, this could result in remote code execution.
import java.io.*; class DeserializeExample { public static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException { Object ret = null; try (ByteArrayInputStream bais = new ByteArrayInputStream(buffer)) { try (ObjectInputStream ois = new ObjectInputStream(bais)) { ret = ois.readObject(); } } return ret; } }
Compliant Solution (Look-Ahead Java Deserialization)
This compliant solution is based on http://www.ibm.com/developerworks/library/se-lookahead/. It inspects the class of any object being deserialized, before its readObject()
method is invoked. The code consequently throws an InvalidClassException
unless the class of the object (and of all sub-objects) is either a GoodClass1
or a GoodClass2
.
import java.io.*; import java.util.*; class WhitelistedObjectInputStream extends ObjectInputStream { public Set whitelist; public WhitelistedObjectInputStream(InputStream inputStream, Set wl) throws IOException { super(inputStream); whitelist = wl; } @Override protected Class<?> resolveClass(ObjectStreamClass cls) throws IOException, ClassNotFoundException { if (!whitelist.contains(cls.getName())) { throw new InvalidClassException("Unexpected serialized class", cls.getName()); } return super.resolveClass(cls); } } class DeserializeExample { private static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException { Object ret = null; Set whitelist = new HashSet<String>(Arrays.asList(new String[]{"GoodClass1","GoodClass2"})); try (ByteArrayInputStream bais = new ByteArrayInputStream(buffer)) { try (WhitelistedObjectInputStream ois = new WhitelistedObjectInputStream(bais, whitelist)) { ret = ois.readObject(); } } return ret; } }
It might appear that the above compliant solution violates Rule OBJ09-J. Compare classes and not class names. However, the security issue addressed by that rule is applicable only when comparing the class of an object that might have been loaded by a foreign ClassLoader
, i.e., an object x for which it cannot be guaranteed that x.getClass()==Class.forName(x.getClass().getName())
. In WhitelistedObjectInputStream.resolveClass()
(which is the method that does the comparison of class names), a check could be added to verify that the return value (let's call it "ret
") is such that ret == Class.forName(ret.getName())
, but this check would always succeed, so it is pointless to add.
On a somewhat related point, it may be noted that ObjectInputStream.resolveClass()
compares the serialVersionUID
from the serialized data to the serialVersionUID
of the Class
object that it is going to return; if there is a mismatch, it throws an exception.
Whitelist
The construction of a suitable whitelist is itself a real challenge. Remote-code-execution exploits have been constructed, not only for classes in the Apache Commons Collection, but also for such core classes as java.util.concurrent.AtomicReferenceArray
(CVE 2012-0507), and even simple classes like java.util.HashSet
[Terse 2015] and java.net.URL
[Terse 2015] can cause a JVM to hang or exhaust memory.
Exceptions
SER12-EX0: Serialized data from a trusted input source does not require sanitization, provided that the code clearly documents that it relies on the input source being trustworthy. For example, if a library is being audited, a routine of that library may have a documented precondition that its callers pre-sanitize any passed-in serialized data or confirm the input source as trustworthy.
Related Vulnerabilities
CERT Vulnerability #576313 describes a family of exploitable vulnerabilities that arise from violating this rule.
Risk Assessment
Whether a violation of this rule is exploitable depends on what classes are on the JVM's classpath. (Note that this is a property of the execution environment, not of the code being audited.) In the worst case, it could lead to remote execution of arbitrary code.
Rule | Severity | Likelihood | Remediation Cost | Priority | Level |
---|---|---|---|---|---|
SER12-J | High | Likely | High | P9 | L2 |
Automated Detection
Tool | Version | Checker | Description |
---|---|---|---|
CodeSonar | 8.1p0 | JAVA.CLASS.SER.ND | Serialization Not Disabled (Java) |
Parasoft Jtest | 2024.1 | CERT.SER12.VOBD | Validate objects before deserialization |
Useful for developing exploits that detect violation of this rule |
It should not be difficult to write a static analysis to check for deserialization that fails to override resolveClass()
to compare against a whitelist.
Related Guidelines
Bibliography
[API 2014] | |
[Terse 2015] | Terse Systems, Closing the Open Door of Java Object Serialization, Nov 8, 2015 |
18 Comments
David Svoboda
Needs a compliant solution about using the SecurityManager to prevent sensitive operations during deserialization of untrusted classes.
David Svoboda
Also needs exception when either: (1) classpath is trusted (no untrusted class) or (2) deserialization cannot load untrusted data. (Prob should be two exceptions). And we need some text in front of the code samples
And a bibliography
David Svoboda
Still need an exception for when classpath is trusted.
Also the compliant solution needs to comply with OBJ09-J. Compare classes and not class names
Wouter Coekaerts
> As an alternative to validation of the serialized data, a
java.lang.SecurityManager
can be used to perform deserialization in a less-privileged contextCurrent published exploits focus mostly on code execution during serialization, because that's the most universal reliable way to exploit it.
But code execution can also be triggered later, when the deserialized objects are being used. Wrapping just the deserialization in a less-privileged context will block some exploits, but it does not block these attacks in general.
David Svoboda
I'm curious if you have proof-of-concept code for this, that doesn't violate another CERT rule?
For such an exploit to work you have to deserialize malicious code that is executed outside of the deserialization process. And your code has to have a framework that executes code based on the (untrusted) deserialized data, and you have to pass your untrusted data to that engine unsanitized. Which will likely violate another CERT rule.
An example would be if you deserialize data that contains a string to pass to your database engine. An attacker embedes an SQL injection which is correctly deserialized but the string contents are not sanitized before going to your database. This violates IDS00-J. Prevent SQL injection.
Chris Frohoff
I'm not sure if this is addressing your question, but recent PoCs execute during deserialization just because it's more reliable and generic/portable that way, but, as Wouter pointed out, variants could defer execution until after deserialization by producing an object of the type expected by the calling code and only execute the gadget chain after the calling code invokes a method on the returned object.
As an (untested) example, the ysoserial CommonsCollections1 payload could be modified to omit the outer
AnnotationInvocationHandler
instance and just return the innerAnnotationInvocationHandler
proxy instance (and the rest of the gadget chain it contains) that implements an interface expected by the calling code (i.e.java.util.List
), and when that code finally invokes a method on the proxy instance (i.e.List.get(int)
), the gadget chain would be executed.David Svoboda
Interesting.
If I was using whitelisting to make sure only objects of trusted classes got deserialized, I would definitely leave org.apache.commons.collections.map.LazyMap off of that whitelist! :)
If I am deserializing an object using a SecurityManager as chaparone, then the SecurityManager would (theoretically) guarantee that the deserialization did nothing malicious. This wouldn't stop your hypothetical LazyMap object from doing something nefarious later. But it would give me an opportunity after deserialization to sanitize your object. So I can make sure your object is not a LazyMap, for instance.
We do have rules for handling potentially malicious objects, such as MET06-J. Do not invoke overridable methods in clone().
Alvaro
A different approach is to run the payload in the finalize method. An example of this can be found in javax.media.jai.remote.SerializableRenderedImage which contains a finalize() method that ends up calling closeClient() which opens a socket to an attacker controller server that can return a payload to be deserialized at that moment so way after the adhoc securitymanager has been uninstalled
David Svoboda
Yet another class to keep off my deserialization whitelist
Alvaro
Also bear in mind that ad-hoc security managers (installed with the purpose of sandboxing the deserialization process) wont be very effective since they need to allow the runtime permission to change the security manager (so it can be uninstalled or restored after deserialization completes). If thats the case, nothing prevents the attacker to uninstall it from his payload.
David Svoboda
I suspect any security-manager-based solution will be more complex than initially planned...security managers tend to be complex. But I do believe a solution is possible...remember the objects you deserialize are all of code in your classpath. If your code never disables a SecurityManager, except in the same object & method where it added it, then you can effectively prevent premature disabling of your security manager.
This also assumes that you actually do want to disable the security manager after deserialization...a truly secure program would continue with the security manager operational. Sort of like permanently dropping setuid privileges in Unix.
Alvaro
> As an alternative to validation of the serialized data, a
java.lang.SecurityManager
can be used to perform deserialization in a less-privileged context.That sentence reads (at least to me that Im not native speaker) as if an AdHoc security manager is used to sandbox the deserialization process.
I agree that a truly secure program should not allow changing securityManagers, although sometimes you want to prevent operations during deserialization that need to be allowed during normal execution of the application. Using a system-level SecurityManager is ok because it wont have the change SecurityManager permission enabled (hopefully). If you use an AdHoc one, meaning that you want to apply a different set of policies for the deserialization operation, you will need to uninstall the installed one and install a new one which will require the permission to change SecurityManagers. If you can do that, the payload will be able to do it as well. Even if the gadgets need to be in the classpath, attackers will use dynamic code injection (eg: TemplatesImpl, JNDI, etc) and the injected code will be able to call System.setSecurityManager(null) as part of the payload.
David Svoboda
I don't know of Java code that changes security managers mid-program, other than toy demo examples or malware. Most 'real' code has a SM applied to them externally (because they are applets, for example). The security architecture allows you to use your own SM, but I don't know any code that does this....everyone either uses the default SM or none.
The default SM decides what privileges to grant code based on its class loader. But malicious deserialized objects use the same classes loaded by the application. So the SM can't discriminate 'malicious' code from application code. Which means either malicious code can disable the SM or benign code can't.
Of course, you could program deserialization to be a privilege managed by the default SM (it isn't today). This *could* mitigate the problem if we got all the details right. But part of the problem is classes that allow embedded malicious code such as org.apache.commons.collections.map.LazyMap. So anything less than enforcing serialization privileges in the Java language itself would be insufficient. Such a proposal would also break any code that uses serialization and a SM.
You could also write your own custom SM. But as that route is unexplored (and I don't see how a SM could enforce constraints on deserialization without the cooperation of all classes), I shan't suggest it.
So I took that statement out of the intro. I now think the SecurityManager would not be helpful in preventing malicious deserialization, and fixing it would require incompatible changes to the language itself.
Wouter Coekaerts
It may also be worth mentioning that not deserializing any untrusted data at all is the only solutions that can protect you against billion-laughs-style denial-of-service attacks (https://gist.github.com/coekie/a27cc406fc9f3dc7a70d).
Well, unless you limit your whitelist to the extreme, like not including any classes that can represent any kind of tree structure with a hashCode() implementation; but I don't think that is a realistic approach for most real-world uses of serialization.
David Svoboda
This is addressed by SEC58-J. Deserialization methods should not perform potentially dangerous operations.
Apostolos Giannakidis
There is another solution to the Deserialization problem that requires no profiling, no tuning, no whitelists/blacklists and no code changes.
https://dzone.com/articles/why-runtime-compartmentalization-is-the-most-compr
It can mitigate all ysoserial attacks, DoS attacks such as the serialDOS, golden-gadgets, as well as zero-day gadget chains.
David Svoboda
The solution you cite is RASP by Waratek. It sounds like a new-and-improved SecurityManager, it seems to provide some sandboxing. However, it addresses the problems at too high a level: how to securely manage a web application that may depend on insecure deserialization. It does not address how to perform deserialization correctly; that is, correctly deserializing non-malicious objects while preventing deserialization of malicious objects. Given the myriad types of malicious objects (from Coekert's DOS attack to malicious finalizer methods), this is a particularly hard problem. Which makes the halting problem appear simple by comparison.
Apostolos Giannakidis
David, this is exactly what it does. It allows deserializing non-malicious objects while preventing deserialization of malicious objects, with no profiling or white/blacklisting. It is in fact a particularly hard problem to solve, but we believe that this approach achieves the best results in the industry right now.