Proof of concept for CVE-2025-21535

Contents:

Recently a professor in my college released a challenge,

Create a poc for CVE-2025-21535 and win 5 grands

The offer sounds too good so naturally I am going to give it a try. What I am going to show here is execution of a command in a server that an unauthenticated user shoudn’t have been able to. Read along to find out.

# What is the CVE about

In oracle’s WebLogic servers version 12.2.1.4.0 and 14.1.1.0.0 lies a RCE (Remote Code Execution) vulnerability. This can be exploited by a specially crafted message sent via their proprietary T3/IIOP protocol to their admin servers.

Two major things to unpack here

  1. What are WebLogic servers?
  2. What is the T3/IIOP protocol?

WebLogic servers are simple servers that enable an admin to monitor a bunch of client machines. When setting it up, it asks for all the different clients the organisation has, whether to put them in a cluster or not and more. For our purposes, it is simply a server that enables special protocols like T3 to talk to it.

The thing here being that WebLogic servers are usually made to host JAVA applications and hence most of the code written will be JAVA and I have absolutely no idea about JAVA (say hello to AI).

The T3 protocol was something developed under BEA Systems (which was later acquired by Oracle) to establish communications with a server.

Lets understand T3 communications in details

# Client Initialization

The client (e.g., our Java application) initializes a connection to the WebLogic server using a T3 URL

# Connection Establishment

The client uses socket communication to connect to the WebLogic server over the specified port. T3 uses:

  • TCP/IP for reliable data transmission.
  • Custom binary encoding (tag-length-value format) for efficient data transfer.

During this phase:

The client sends a handshake to establish compatibility and version negotiation. The server responds, confirming the connection.

# Session Creation

Once the connection is established, the server creates a persistent session for the client. This session is used to track stateful interactions between the client and the server.

# Request-Response Exchange

The client sends requests to the server, such as:

  • JNDI lookups: Retrieving resources like DataSources, JMS connections, EJBs, etc
  • RMI invocations: Invoking remote methods on server-side objects.
  • Deployment commands: Deploying or managing applications.

The server processes the request and sends a response back to the client. The communication uses a tag-length-value format to efficiently encode data:

  • Tag: Identifies the type of data or command.
  • Length: Specifies the size of the data.
  • Value: Contains the actual data or payload.

What about the IIOP protocol? I didn’t really care for this one cause I figured T3 is probably the more important one. (I might be severely wrong so keeping this path open…)

# Setup procedure

  1. Installing pre-reqs

The first step is establishing a vulnerable oracle WebLogic server to test our attacks out. The prerequesites for doing this can be installed using this

sudo apt update

Use this link: oracle java archive to download JAVA version 8, linux x64 tarred and gunzipped (unless if you’re using windows, in which case good luck).

sudo mkdir -p /usr/lib/jvm 
sudo tar -xvzf ~/Downloads/jdk-8u202-linux-x64.tar.gz -C /usr/lib/jvm/

I have uses 202 cause that was the version I got, make sure you use your own.

Check the java version, if it runs and shows the correct one, then you’re good to go. Set it as default,

export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_202
export PATH=$JAVA_HOME/bin:$PATH 
echo "export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_202" >> ~/.zshrc 
echo "export PATH=\$JAVA_HOME/bin:\$PATH" >> ~/.zshrc 
source ~/.zshrc

Use bashrc if you use bash and not zsh.

  1. Install WebLogic 12.2.1.4.0

The second step is installing the WebLogic server. For this

  • Install an oracle account
  • Download the version 12.2.1.4.0 generic version
  • Unzip the file and install using JAVA

So, once you unzip you will see a .jar file. This is the installer for the server, and we will run this using

java -jar fmw_12.2.1.4.0_wls_lite_generic.jar 

This will run a GUI, and it makes it much easier to setup. The few important things to keep in mind are:

  • Template: simple webserver
  • Advance feature: All of them
  • Don’t enable SSL (not really needed)
  • No need for managed servers or clusters. Do not add server templates too. (this is different from the initial template we discussed)
  • Unix machine: yes, add a unix machine. Not enabling GID or UID, listen address is localhost and node manager is 5556.
  • Assigning the server to the node manager UNIX machine. Not setting up virtual targets.

After all this, you will be given a URL. That’s where your server will go. However, we’ll use localhost itself.

  1. Running the server

Run the following commands to run the server locally.

cd /home/purge/Oracle/Middleware/Oracle_Home/user_projects/domains/vuln_domain
./startWebLogic.sh

I have used the name vuln_domain, yours may be different, so ensure you use the correct name. After running the server, head to

	http://127.0.0.1:7001/console

This will deploy your application.

Note that T3 protocol is enabled by default

# Testing the server

The idea is to use T3 protocol to communicate with the WebLogic server and see if that works. Running the below code, it ensures that T3 is properly setup and it runs

import weblogic.jndi.WLInitialContextFactory;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;

public class WebLogicT3Client {
    public static void main(String[] args) {
        try {
            Hashtable<String, String> env = new Hashtable<>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
            env.put(Context.PROVIDER_URL, "t3://localhost:7001");

            InitialContext ctx = new InitialContext(env);
            System.out.println("Connected to WebLogic via T3!");
            ctx.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Save the above as WebLogicT3Client.java. Then run the below command to create the necessary class.

javac -cp /home/purge/Oracle/Middleware/Oracle_Home/wlserver/server/lib/weblogic.jar WebLogicT3Client.java

This should result in the creation of WebLogicT3Client.class. Then we execute the code using

java -cp .:/home/purge/Oracle/Middleware/Oracle_Home/wlserver/server/lib/weblogic.jar WebLogicT3Client

Also, change purge to whatever your username is. (Mine is purge cause purge means cool)

# The exploit

Alright now that the server is up and running, we need to find the exploit. According to the official documentation for the CVE in nvd.nist, we find that the weakness enumeration is CWE-306.

CWE-306: The product does not perform any authentication for functionality that requires a provable user identity or consumes a significant amount of resources.

or it might mean this as well.

Exposing critical functionality essentially provides an attacker with the privilege level of that functionality. The consequences will depend on the associated functionality, but they can range from reading or modifying sensitive data, accessing administrative or other privileged functionality, or possibly even executing arbitrary code.

But what does this means for us? It can be either of the two things

  1. Some functionality that only the admin should be doing, could be performed by an unauthenticated user as well
  2. Something related to JAVA serialisation vulnerability

I don’t want to look at JAVA serialisation objects yet without first checking the first option extensively. We know there are 3 major things that can be done by an admin in the WebLogic servers,

  • JNDI lookups
  • RMI invocations
  • Deployment commands

So, I am gonna first test if I can retrieve or query these using non-admin privileges. If I can, well, thats bad.


Trying to access a random resource with and without credentials


Checking for RMI

import weblogic.jndi.WLInitialContextFactory;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NameClassPair;
import java.util.Hashtable;

public class JNDIBrowser {
    public static void main(String[] args) {
        try {
            Hashtable<String, String> env = new Hashtable<>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
            env.put(Context.PROVIDER_URL, "t3://localhost:7001"); 
            // env.put(Context.SECURITY_PRINCIPAL, "admin"); 
            // env.put(Context.SECURITY_CREDENTIALS, "password");

            InitialContext ctx = new InitialContext(env);
            System.out.println("Connected to WebLogic. Browsing JNDI tree...");

            // Browse the JNDI tree
            NamingEnumeration<NameClassPair> list = ctx.list("");
            while (list.hasMore()) {
                NameClassPair ncp = list.next();
                System.out.println("Name: " + ncp.getName() + ", Class: " + ncp.getClassName());
            }

            ctx.close();
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

Apparently this can help in figuring out if RMI is running on your server. Turns out we can run this without admin credentials as well. This is how my security page looked like when I ran this without admin credentials

Security page under domain
Security page under domain

Yeah, got the same values irrespective of credentials.

	Name: mejbmejb_jarMejb_EO, Class: weblogic.rmi.cluster.ClusterableRemoteObject
	Name: jmx, Class: weblogic.jndi.internal.ServerNamingNode
	Name: javax, Class: weblogic.jndi.internal.ServerNamingNode
	Name: weblogic, Class: weblogic.jndi.internal.ServerNamingNode
	Name: __WL_GlobalJavaApp, Class: weblogic.jndi.internal.ServerNamingNode
	Name: ejb, Class: weblogic.jndi.internal.ServerNamingNode
	Name: java:global, Class: weblogic.jndi.internal.ServerNamingNode
	Name: eis, Class: weblogic.jndi.internal.ServerNamingNode
	Name: _WL_internal_JaWproFX2ApBWuB5SHvOrubDEm75UA2k8avPUe04e2Gsr6oyE6hh5WDY6n7sT2SP, Class: weblogic.jndi.internal.ServerNamingNode

These is the JNDI tree (the Java Naming and Directory Interface tree) that lists the different resources or nodes in the server that can be access via RMI, that is, invoke some methods on the server-side. This is pretty cool honestly.

If we analyse each entry in this tree to get a better feel for things

  • mejbmejb_jarMejb_EO: This is an EJB (Enterprise Java Bean). It is a server-side software element that summarizes business logic of an application. Thus, its presence means that if we invoke this node using RMI we can access the business side logic.

  • jmx: This stands for Java Management Extensions, that is used to manage and monitor applications, system objects, hardware peripheral devices and service-oriented networks. All the examples listed come under the umbrella of mbeans.

  • javax: Javax is a prefix used in Java to denote a set of standard extension packages that provide additional functionality for Java applications. It stands for Java Extension and is commonly used for application programming interfaces (APIs) related to enterprise, messaging, and other areas (straight from lenovo’s official page).

The javax and jmx node is the most low-level/system-level node there is. From what I understand, if this node is compromised, then a lot of things can happen.

  • weblogic: This is the top-level JNDI node for WebLogic-specific resources. This acts as a container for internal WebLogic services and objects.

This may expose a lot of information as well.

The next few aren’t that important probably, except perhaps eis

  • eis: An Enterprise Information System is any kind of information system which improves the functions of enterprise business processes by integration. This means typically offering high quality of service, dealing with large volumes of data and capable of supporting some large and possibly complex organization or enterprise. An EIS must be able to be used by all parts and all levels of an enterprise. (directly from wikipedia)

Basically, it might be holding access to a database

# Interacting with the nodes

FIRSTLY FIGURE WHY THESE RMI SERVICES ARE RUNNING IF I NEVER SET THEM UP?

  1. Perform a lookup

For example,

Object resource = ctx.lookup("mejbmejb_jarMejb_EO");
  1. Explore sub-nodes

nodes like jmx, javax, weblogic, ejb, and others act as containers. We can explore their contents programmatically by listing their sub-nodes:

NamingEnumeration<NameClassPair> subNodes = ctx.list("jmx");
while (subNodes.hasMore()) {
    NameClassPair ncp = subNodes.next();
    System.out.println("Name: " + ncp.getName() + ", Class: " + ncp.getClassName());
}

I will try this tomorrow. I am looking for more information weblogic, jmx, javax and eis. These seem interesting as hell!

Its like 2am, I have classes from 8, gotta sleep. Goodnight.


# Morning 9 am

I noticed that restrictive JMX policies are enabled under the advanced security tab.

Restrictive JMX policies
Restrictive JMX policies

This means restrictive policies will be used for JMX authorisation. I am not sure what these “restrictive policies” look like, so I can’t comment on what this means for us. But since this was enabled by default I will not toggle this (to replicate that dumb system admin who doesn’t change default configurations)

Let’s perform lookups for each node.

import weblogic.jndi.WLInitialContextFactory;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.NameClassPair;
import java.util.Hashtable;

public class WebLogicJNDILookupForAll {
    public static void main(String[] args) {
        try {
            // Configure WebLogic JNDI environment
            Hashtable<String, String> env = new Hashtable<>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
            env.put(Context.PROVIDER_URL, "t3://localhost:7001");
            // env.put(Context.SECURITY_PRINCIPAL, "admin");
            // env.put(Context.SECURITY_CREDENTIALS, "password");

            InitialContext ctx = new InitialContext(env);
            System.out.println("Connected to WebLogic. Performing lookups...");

            // Define the JNDI paths to query
            String[] jndiPaths = {"javax", "jmx", "weblogic", "eis"};

            for (String path : jndiPaths) {
                System.out.println("\n### Listing entries under: " + path);
                try {
                    NamingEnumeration<NameClassPair> list = ctx.list(path);
                    while (list.hasMore()) {
                        NameClassPair ncp = list.next();
                        System.out.println("Name: " + ncp.getName() + ", Class: " + ncp.getClassName());
                        
                        // lookup on each entry
                        try {
                            Object obj = ctx.lookup(path + "/" + ncp.getName());
                            System.out.println("  -> Lookup successful: " + obj);
                        } catch (Exception lookupException) {
                            System.err.println("  -> Failed to lookup " + ncp.getName() + ": " + lookupException.getMessage());
                        }
                    }
                } catch (NamingException e) {
                    System.err.println("Failed to list JNDI names under " + path + ": " + e.getMessage());
                }
            }

            // Close the context
            ctx.close();
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

Alright, few things came up. This was the result,

Connected to WebLogic. Performing lookups…

	### Listing entries under: javax
	Failed to list JNDI names under javax: User <anonymous> does not have permission on javax to perform list operation.

	### Listing entries under: jmx
	Name: runtime, Class: weblogic.jndi.internal.NonListableRef
	  -> Failed to lookup runtime: jmx/runtime cannot be looked up using a remote JNDI context. Use JSR160 interface to use JMX remotely or use a local JNDI context to look up the local MBeanServer.
	Name: domainRuntime, Class: weblogic.jndi.internal.NonListableRef
	  -> Failed to lookup domainRuntime: jmx/domainRuntime cannot be looked up using a remote JNDI context. Use JSR160 interface to use JMX remotely or use a local JNDI context to look up the local MBeanServer.
	Name: edit, Class: weblogic.jndi.internal.NonListableRef
	  -> Failed to lookup edit: jmx/edit cannot be looked up using a remote JNDI context. Use JSR160 interface to use JMX remotely or use a local JNDI context to look up the local MBeanServer.

	### Listing entries under: weblogic
	Failed to list JNDI names under weblogic: User <anonymous> does not have permission on weblogic to perform list operation.

	### Listing entries under: eis
	Name: jms, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (eis.jms)

Apparently only eis lookup was successful as an anonymous user. If I do this with proper credentials, this is what I get,

	Connected to WebLogic. Performing lookups...

	### Listing entries under: javax
	Name: jms, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (javax.jms)
	Name: transaction, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (javax.transaction)

	### Listing entries under: jmx
	Name: runtime, Class: weblogic.jndi.internal.NonListableRef
	  -> Failed to lookup runtime: jmx/runtime cannot be looked up using a remote JNDI context. Use JSR160 interface to use JMX remotely or use a local JNDI context to look up the local MBeanServer.
	Name: domainRuntime, Class: weblogic.jndi.internal.NonListableRef
	  -> Failed to lookup domainRuntime: jmx/domainRuntime cannot be looked up using a remote JNDI context. Use JSR160 interface to use JMX remotely or use a local JNDI context to look up the local MBeanServer.
	Name: edit, Class: weblogic.jndi.internal.NonListableRef
	  -> Failed to lookup edit: jmx/edit cannot be looked up using a remote JNDI context. Use JSR160 interface to use JMX remotely or use a local JNDI context to look up the local MBeanServer.

	### Listing entries under: weblogic
	Name: cluster, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.cluster)
	Name: fileSystem, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.fileSystem)
	Name: partitionId, Class: java.lang.String
	  -> Lookup successful: 0
	Name: rmi, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.rmi)
	Name: messaging, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.messaging)
	Name: jms, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.jms)
	Name: partitionName, Class: java.lang.String
	  -> Lookup successful: DOMAIN
	Name: common, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.common)
	Name: jmx, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.jmx)
	Name: management, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.management)
	Name: logging, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.logging)
	Name: MessageInterception, Class: weblogic.messaging.interception.internal.InterceptionServiceImpl
	  -> Failed to lookup MessageInterception: weblogic.messaging.interception.internal.InterceptionServiceImpl
	Name: transaction, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (weblogic.transaction)

	### Listing entries under: eis
	Name: jms, Class: weblogic.jndi.internal.ServerNamingNode
	  -> Lookup successful: WLContext (eis.jms)

I realised I completely missed this object mejbmejb_jarMejb_EO. This EJB is likely a RMI accessible MBean server node. So, it might help us get remote access to WebLogic MBeans for JMX monitioring and management.

Notice that when we tries to lookup under JMX, we got this error

	Failed to lookup runtime: jmx/runtime cannot be looked up using a remote JNDI context. Use JSR160 interface to use JMX remotely or use a local JNDI context to look up the local MBeanServer.

This happened regardless of admin access (as the error statement properly mentions, its not a privilege issue). So, let’s try to hook up mejbmejb_jarMejb_EO and see if we get access to the local MBeans server.

okay this won’t work cause I probably don’t have an MBeans server running. Got an error. I will continue working on EIS cause that might be more useful… (if this doesn’t work I shall try jmx properly).


Alight I ran an EIS check to figure out what all methods exist so that I can look it up. It is all being done without admin creds (the resuts are symmetric either way)

import weblogic.jndi.WLInitialContextFactory;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import java.lang.reflect.Method;
import javax.naming.NameClassPair;
import java.util.Hashtable;

public class testingEIS {
    public static void main(String[] args) {
        try {
            // Configure WebLogic JNDI environment
            Hashtable<String, String> env = new Hashtable<>();
            env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
            env.put(Context.PROVIDER_URL, "t3://localhost:7001"); // Change as needed

            InitialContext ctx = new InitialContext(env);
            System.out.println("Connected to WebLogic. Performing lookups...");

            // Define the JNDI paths to query
            try {
            Object obj = ctx.lookup("eis.jms");                         // Let's first get all the methods there are because QueueConnectionFactory was not resolved but eis.jmx was resolved. 
            System.out.println(" -> Lookup successful. These is the class: " + obj.getClass().getName());

            Method[] methods = obj.getClass().getMethods();

            for (Method method : methods) {
                System.out.println(method);
            }

            } catch (Exception lookupException) {
                System.err.println(" -> Failed to lookup:" + lookupException.getMessage());
            }

            // Close the context
            ctx.close();
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

What I found is, that EIS is linked NOT to a JMS object like QueueConnectionFactory, but rather it is a JNDI naming subtext (a node in our tree). What we should then do is retreive the names and objects inside of eis.jms.

I looked inside eis.jms to find a subdirectory named internal. Looking inside that I found this

	Found: WLSConnectionFactoryJNDINoTX of type javax.naming.Reference
	Found: WLSConnectionFactoryJNDIXA of type javax.naming.Reference

There are naming references, that is not actual JMS objects but sort of like a pointer to it. We now need to get the actual JMS objects. Alright, I tried to access a JMS connection factory using code but got this error (irrespective of credentials)

	Connected to WebLogic. Performing lookups...
	Found: WLSConnectionFactoryJNDINoTX of type javax.naming.Reference
	Found: WLSConnectionFactoryJNDIXA of type javax.naming.Reference
	javax.naming.NamingException: [Connector:199318]It is not supported to access connector resource outside server vuln_domain/VulnerableAdmin which defines the resource, current server is remote client.
		at weblogic.connector.deploy.JNDIHandler.getObjectInstance(JNDIHandler.java:644)
		at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:321)
		at weblogic.jndi.internal.WLContextImpl.lookup(WLContextImpl.java:451)
		at weblogic.jndi.internal.WLContextImpl.lookup(WLContextImpl.java:435)
		at javax.naming.InitialContext.lookup(InitialContext.java:417)
		at testingEIS.main(testingEIS.java:34)

I think the issue is simply that I have no JMS resources even running!

No JMS resources
No JMS resources

I will look into the different defined actions a little more. Then finally look into JAVA object serialisation and deserialisation errors.


# Deploying a rouge RMI object

InitialContext ctx = new InitialContext(env);
ctx.bind("maliciousObject", new EvilRemoteObject());

Did you like this blogpost? Then consider catching up via LinkedIn or Github!