❌

Normal view

There are new articles available, click to refresh the page.
Before yesterdaycode white | Blog

Java Exploitation Restrictions in Modern JDK Times

11 April 2023 at 13:12
Java deserialization gadgets have a long history in context of vulnerability research and at least go back to the year 2015. One of the most popular tools providing a large set of different gadgets isΒ ysoserial by Chris Frohoff. Recently, we observed increasing concerns from the community why several gadgets do not seem to work anymore with more recent versions of JDKs. In this blog post we try to summarize certain facts to reenable some capabilities which seemed to be broken. But our journey did not begin with deserialization in the first place but rather looking for alternative ways of executing Java code in recent JDK versions. In this blost post, we'll focus on OpenJDK and Oracle implementations. Defenders should therefore adjust their search patterns to these alternative code execution patterns accordingly.

ScriptEngineManager - It's Gone

Initially, our problems began on another exploitation track not related to deserialization. Often code execution payloads in Java end with a final call to 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).

But since Nashorn was marked as deprecated in Java 11 (JEP 335), and removed entirely in Java 15 (JEP 372), this means that a target using a JDK version >= 15Β won't process JavaScript payloads anymore by default. Instead of hoping for other manually added JavaScript engines by developers for a specific target, we could make use of a "new" Java code evaluation API: JShell, a read-eval-print loop (REPL) tool that was introduced with Java 9Β (JEP 222). Mainly used in combination with a command line interface (CLI)Β for testing Java code snippets, it allows programmatic access as well (see JShell API). This new evaluation call reads like 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."

So, we started to play with some Java code snippets using the JShell API. First, we realized that it is indeed possible to use 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.

We also learned that it makes a huge difference between using the CLI vs. the API programmatically. The jshell CLI tool supports the listing of pre-imported packages:

I.e. a code snippet in the CLI executing 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)));.

Thus, it seems that we don't have access to a lot of "useful" classes with the programmatic approach. But as you already might have guessed, using fully qualified class names can be used as well. We don't have to "fix" the 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

Besides the fact, that we could now benefit from this approach to inject these kinds of payloads in various attacking scenarios, this blog post should also be about insecure deserialization exploitation. Starting with a well-known gadget 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.


After a small adjustment to the pom.xmlΒ 

we're ready to rebuild the ysoserial package with maven. But creating a payload with a recent version of JDK (version 17 in our case) revealed the following error.

In JDK9, the Java Platform Module System (JPMS)Β was introduced based on the "historical" project Jigsaw. We highly recommend the reader to look through the historical timeline with the corresponding JEPs in this IBM Java tutorial. E.g. JEP 260 describes the fact that most internal JDK APIs should be encapsulated properly such that Getters and Setters have to be used for access/change of otherwise privately declared internal member variables. Also the new Java module structure should explicitely restrict access between different modules, i.e. declaring lists of exported packages will become a "must" to allow inter-module access via the new module descriptor module-info.java. Additionally, since JDK16 the default strategy with respect to Java Reflection API is set to "deny by default" (JEP 396).
The CommonsCollections library is not implemented as Java module so that by definition it falls in the category unnamedΒ (compare with exception message above).

Browsing through the ysoserial GitHub issue tracker, it appears people seem to have similar problems recently. One of the best articles explaining this kind of issue comes from Oracle itself. The chapter "Illegal Reflective Access" nicely summarizes the adjustments to JDK versions with respect to access of otherwise inaccessible members between packages via Java Reflection API.

"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

Another popular gadget is 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).

But let's get back to trying to create a payload in JDK17 which unfortunately results in a different exception compared to CommonsCollections6.

This kind of error is expected, cross-checking with the Oracle article mentioned above, and can therefore be solved with the same approach: 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).

You might be aware of the deserializer test class in ysoserial. This can be called by piping the payload creation result directly into java -cp ./target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.Deserializer. You should first test this with our CommonsCollections6 case above.
But what if we do this with our successfully created CommonsBeanutils1 gadget?

Sounds familiar? Unfortunately, this scenario is equivalent to server side deserialization processing, i.e. no code execution! If you add the --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!

Since 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 of ScriptEngineManager 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 on Runtime.getRuntime().exec or ScriptEngineManager().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.

CVE-2019-19470: Rumble in the Pipe

17 January 2020 at 09:18
This blog post describes an interesting privilege escalation from a local user to SYSTEM for a well-known local firewall solution called TinyWall in versions prior to 2.1.13. Besides a .NET deserialization flaw through Named Pipe communication, an authentication bypass is explained as well.

Β Introduction

TinyWall is a local firewall written in .NET. It consists of a single executable that runs once as SYSTEM and once in the user context to configure it. The server listens on a Named Pipe for messages that are transmitted in the form of serialized object streams using the well-known and beloved BinaryFormatter. However, there is an additional authentication check that we found interesting to examine and that we want to elaborate here a little more closely as it may also be used by other products to protect themselves from unauthorized access.

For the sake of simplicity the remaining article will use the terms Server for the receiving SYSTEM process and Client for the sending process within an authenticated user context, respectively. Keep in mind that the authenticated user does not need any special privileges (e.g. SeDebugPrivilege) to exploit this vulnerability described.

Named Pipe Communication

Many (security) products use Named Pipes for inter-process communication (e.g. see Anti Virus products). One of the advantages of Named Pipes is that a Server process has access to additional information on the sender like the origin Process ID, Security Context etc. through Windows' Authentication model. Access to Named Pipes from a programmatic perspective is provided through Windows API calls but can also be achieved e.g. via direct filesystem access. The Named Pipe filessystem (NPFS) is accessible via the Named Pipe's name with a prefix \\.\pipe\.

The screenshot below confirms that a Named Pipe "TinyWallController" exists and could be accessed and written into by any authenticated user.


Talking to SYSTEM

First of all, let's look how the Named Pipe is created and used. When TinyWall starts, a PipeServerWorker method takes care of a proper Named Pipe setup. For this the Windows API provides System.IO.Pipes.NamedPipeServerStream with one of it's constructors taking a parameter of System.IO.Pipes.PipeSecurity. This allows for fine-grained access control via System.IO.PipeAccessRule objects using SecurityIdentifiers and alike. Well, as one can observe from the first screenshot above, the only restriction seems to be that the Client process has to be executed in an authenticated user context which doesn't seem to be a hard restriction after all.



But as it turned out (again take a look at the screenshot above) an AuthAsServer() method is implemented to do some further checking. What we want is to reach the ReadMsg() call, responsible for deserializing the content from the message received.



If the check fails, an InvalidOperationException with "Client authentication failed" is thrown. Following the code brought us to a "authentication check" based on Process IDs, namely checking if the MainModule.FileName of the Server and Client process match. The idea behind this implementation seems to be that the same trusted TinyWall binary should be used to send and receive well-defined messages over the Named Pipe.


Since the test for equality using the MainModule.FileName property could automatically be passed when the original binary is used in a debugging context, let's verify the untrusted deserialization with a debugger first.

Testing the deserialization

Thus, to test if the deserialization with a malicious object would be possible at all, the following approach was taken. Starting (not attaching) the TinyWall binary out of a debugger (dnSpy in this case) would fulfill the requirement mentioned above such that setting a breakpoint right before the Client writes the message into the pipe would allow us to change the serialized object accordingly. The System.IO.PipeStream.writeCore() method in the Windows System.Core.dll is one candidate in the process flow where a breakpoint could be set for this kind of modification. Therefore, starting the TinyWall binary in a debugging session out of dnSpy and setting a breakpoint at this method immediately resulted in the breakpoint being hit.



Now, we created a malicious object withΒ ysoserial.NET and James Forshaw's TypeConfuseDelegate gadget to pop a calc process. In the debugger, we use System.Convert.FromBase64String("...") as expression to replace the current value and also adjust the count accordingly.



Releasing the breakpoint resulted in a calc process running as SYSTEM. Since the deserialization took place before the explicit cast was triggered, it was already to late. If one doesn't like InvalidCastExceptions, the malicious object could also be put into a TinyWall PKSoft.Message object's Arguments member, an exercise left to the reader.

Faking the MainModule.FileName

After we have verified the deserialization flaw by debugging the client, let's see if we can get rid of the debugging requirement. So somehow the following restriction had to be bypassed:



The GetNamedPipeClientProcessId() method from Windows API retrieves the client process identifier for the specified Named Pipe. For a final proof-of-concept Exploit.exe our Client process somehow had to fake its MainModule.FileName property matching the TinyWall binary path. This property is retrieved from System.Diagnostics.ProcessModule's member System.Diagnostics.ModuleInfo.FileName which is set by a native call GetModuleFileNameEx() from psapi.dll. These calls are made in System.Diagnostics.NtProcessManager expressing the transition from .NET into the Windows Native API world. So we had to ask ourselves if it'd be possible to control this property.



As it turned out this property was retrieved from the Process Environment Block (PEB) which is under full control of the process owner. The PEB by design is writeable from userland. Using NtQueryInformationProcess to get a handle on the process' PEB in the first place is therefore possible. The _PEB struct is built of several entries as e.g. PRTL_USER_PROCESS_PARAMETERS ProcessParameters and a double linked list PPEB_LDR_DATA Ldr. Both could be used to overwrite the relevant Unicode Strings in memory. The first structure could be used to fake the ImagePathName and CommandLine entries but more interesting for us was the double linked list containing the FullDllName and BaseDllName. These are exactly the PEB entries which are read by the Windows API call of TinyWall's MainModule.FileName code. There is also a nice Phrack article from 2007 explaining the underlying data structures in great detail.

Fortunately, Ruben Boonen (@FuzzySec) already did some research on these kind of topics and released several PowerShell scripts. One of these scripts is called Masquerade-PEB which operates on the Process Environment Block (PEB) of a running process to fake the attributes mentioned above in memory. With a slight modification of this script (also left to the reader) this enabled us to fake the MainModule.FileName.


Even though the PowerShell implementation could have been ported to C#, we chose the lazy path and imported the System.Management.Automation.dll into our C# Exploit.exe. Creating a PowerShell instance, reading in the modified Masquerade-PEB.ps1 and invoking the code hopefully would result in our faked PEB entries of our Exploit.exe.



Checking the result with a tool like Sysinternals Process Explorer confirmed our assumption such that the full exploit could be implemented now to pop some calc without any debugger.

Popping the calc

Implementing the full exploit now was straight-forward. Using our existing code of James Forshaw's TypeConfuseDelegate code combined with Ruben Boonen's PowerShell script being invoked at the very beginning of our Exploit.exe now was extended by connecting to the Named Pipe TinyWallController. The System.IO.Pipes.NamedPipeClientStream variable pipeClient was finally fed into a BinaryFormatter.Serialize() together with the gadget popping the calc.



Thanks to Ruben Boonen's work and support of my colleague Markus Wulftange the final exploit was implemented quickly.

Responsible disclosure

The vulnerability details were sent to the TinyWall developers on 2019-11-27 and fixed in version 2.1.13 (available since 2019-12-31).


❌
❌