ScriptEngineManager - It's Gone
java.lang.Runtime.getRuntime().exec(args)
, at least in a proof-of-concept exploitation phase. But as a Red Team, we always try to maintain a low profile and avoid actions that may raise suspicion like spawing new (child) processes. This is a well-known and still hot topic discussed in the context of C2 frameworks today, especially when it comes to AV/EDR evasion techniques. But this can also be applied to Java exploitation. It is a well-known fact that an attacker has the choice between different approaches to stay within the JVM to execute arbitrary Java code, with new javax.script.ScriptEngineManager().getEngineByName(engineName).eval(scriptCode)
probably being the most popular one over the last years. The input code used is usually based on JavaScript being executed by the referenced ScriptEngine available, e.g. Nashorn (or Rhino).jdk.jshell.JShell.create().eval(javaCode)
, executing Java code snippets (not JavaScript!). Further call variants exist, too. We found this being mentioned already in 2019 used in context of a SpEL Injection payload. This all sounded to good to be true but nevertheless some restrictions seemed to apply."The input should be exactly one complete snippet of source code, that is, one expression, statement, variable declaration, method declaration, class declaration, or import."
import
statements within such snippets but interestingly the subsequent statements were not executed anymore. This should have been expected by reading the quote above, i.e. one would have actually been restricted to a single statement per snippet.jshell
CLI tool supports the listing of pre-imported packages:Files.createFile(java.nio.file.Paths.get("/tmp/RCE"));
works just fine. Calling the eval
method programmatically on a JShell
instance instead gives a different result, namely Files
not known in this context. As a side note, eval
calls do not return any exception messages printed to stdout/stderr
. For "debugging" purposes, the diagnostics
methods helps a lot: jshell.diagnostics(events.get(0).snippet()).forEach(x -> System.out.println(x.getMessage(Locale.ENGLISH)));
.import
issue mentioned above but can still use all built-in JDK classes by referencing them accordingly: java.nio.file.Files.createFile(java.nio.file.Paths.get(\"/tmp/RCE\"));
. This gives us again all the power needed to build (almost) arbitrary Java code payloads for exfiltrating data, putting them in a server response etc. pp.Ysoserial - The Possible
CommonsCollections6
, the original Runtime.getRuntime().exec(args)
will be replaced with a JShell
variant. Using the handy TransformerChain
pattern, one simply has to replace the chain accordingly.pom.xml
maven
. But creating a payload with a recent version of JDK (version 17 in our case) revealed the following error.module-info.java
. Additionally, since JDK16 the default strategy with respect to Java Reflection API is set to "deny by default" (JEP 396)."Some tools and libraries use reflection to access parts of the JDK that are meant for internal use only. This is called illegal reflective access and by default is not permitted in JDK 16 and later.
...
Code that uses reflection to access private fields of exported java.* APIs will no longer work by default. The code will throw an InaccessibileObjectException."
Furthermore, Oracle states that
"If you need to use an internal API that has been made inaccessible, then use the --add-exports runtime option. You can also use --add-exports at compile time to access internal APIs.
If you have to allow code on the class path to do deep reflection to access nonpublic members, then use the --add-opens option."
Since CommonsCollections6
(and most of other gadgets) make heavy use of the Java Reflection API via java.lang.reflect.Field.setAccessible(boolean flag)
, this restriction has to be taken into account accordingly. Oracle already gave the solution above. Note that the --add-exports
parameter does not allow "deep reflection", i.e. access to otherwise private members. So, creating the payload using java --add-opens java.base/java.util=ALL-UNNAMED -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 "java.nio.file.Files.createFile(java.nio.file.Paths.get(\"/tmp/RCE\"));"
works just fine and gives code execution in insecure deserialization sinks again.
Ysoserial - The Impossible
CommonsBeanutils1
, still frequently used in these days to gain code execution through insecure deserialization. A short side note: this gadget chain uses Gadgets.createTemplatesImpl(cmd)
to put your command into a Java statement, compiled then into bytecode which is executed later. Chris Frohoff already gave a nice hint in his code that instead of the java.lang.Runtime.getRuntime().exec(cmd)
call, one "[...] could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections". That's already a powerful primitive which might not have been used by too many people over the last years (at least not been made public as popular choice).CommonsCollections6
.java --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.trax=ALL-UNNAMED --add-opens=java.xml/com.sun.org.apache.xalan.internal.xsltc.runtime=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar CommonsBeanutils1 "[JAVA_CODE]"
(see also Chris Frohoff's comment on an issue).java -cp ./target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.Deserializer
. You should first test this with our CommonsCollections6
case above.CommonsBeanutils1
gadget?--add-opens
parameters to the ysoserial.Deserializer
as well, deserialization works as expected of course but in a remote attack scenario we obviously don't have control over this!org.apache.commons.beanutils.PropertyUtilsBean
tries to access com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
, traditional paths in gadget chains like TemplatesImpl
turned out to be useless in most cases. This, again, is because third-party libraries known from ysoserial are not Java modules and the module system strongly protects internal JDK classes. If we check the module-info.java
in JDKs java.xml/share/classes/
directory, no exports can be found matching these package names needed. Game over.Conclusions
- Use
JShell
instead ofScriptEngineManager
for JDK versions >= 15 (side note: this is not available in JREs!). This is also relevant for Defenders searching for code execution patterns only based onRuntime.getRuntime().exec
orScriptEngineManager().getEngineByName(engineName).eval
calls. Keep in mind, this already affects JDK versions >= 9. - For JDK versions < 16, use the
--add-opens
property Setters during payload creation. - For JDK versions >= 16, rely on known (or find new) Java deserialization gadgets which do not depend on access to internal JDK class members etc. However, check for the exported namespaces before giving up a certain gadget chain.