
Technische Analyse von CVE-2026-33701, einer unsicheren Deserialisierungs-Schwachstelle in der RMI-Instrumentierung des OpenTelemetry Java Agent, einschließlich Exploit-Bedingungen, Patch-Details und Minderungsstrategien.
Schweregrad: Kritisch (CVSS v4.0: 9.3)
Betroffene Versionen: opentelemetry-javaagent < 2.26.1
Behoben in: 2.26.1 (veröffentlicht am 23. März 2026)
OpenTelemetry ist derzeit wahrscheinlich das am weitesten verbreitete Observability-Framework im Java-Ökosystem. Insbesondere der Java-Agent wird von Tausenden von Produktionsdiensten verwendet, um Anwendungen automatisch für Tracing, Metriken und Logging zu instrumentieren, ohne dass der Entwickler Codeänderungen vornehmen muss. Man hängt ihn einfach als javaagent-Flag an und er erledigt alles im Hintergrund.
Was CVE-2026-33701 interessant macht, ist nicht nur die Schwachstelle selbst, sondern die Art und Weise, wie sie eingeführt wurde. Der Agent registriert stillschweigend einen benutzerdefinierten RMI-Endpunkt als Nebeneffekt seiner RMI-Instrumentierung. Entwickler wissen nicht, dass er da ist. Er steht in keinem Anwendungscode. Er wurde von niemandem konfiguriert. Der Agent hat ihn automatisch platziert, und in Versionen unter 2.26.1 deserialisierte dieser Endpunkt eingehende Daten ohne jegliche Filter.
Wenn die Anwendung bereits einen RMI- oder JMX-Port offen hatte, konnte ein Angreifer mit Netzwerkzugriff auf diesen Port ein manipuliertes serialisiertes Payload an den benutzerdefinierten Endpunkt des Agents senden und möglicherweise Remote-Code-Ausführung mit den Rechten des JVM-Prozesses erreichen. Der Haken ist, dass RCE eine kompatible Gadget-Chain auf dem Anwendungs-Classpath erfordert. Mehr dazu später.
Wenn sich der OTel Java Agent an eine JVM anhängt, instrumentiert er RMI-Aufrufe für die Kontextweitergabe. Die Idee ist, dass der Agent beim Aufruf einer entfernten Methode durch einen RMI-Client den aktuellen Trace-Kontext an die Serverseite weitergeben muss, damit Spans korrekt über Dienstgrenzen hinweg verbunden werden.
Dazu registriert der Agent sein eigenes benutzerdefiniertes RMI-Objekt mit einer fest codierten ObjID. In ContextPropagator.java:
public static final ObjID CONTEXT_CALL_ID =
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
Diese ObjID ist die Art und Weise, wie der Agent seinen eigenen Endpunkt innerhalb der RMI-Laufzeitumgebung identifiziert. Wenn ein instrumentierter RMI-Client eine Verbindung zu einem Server herstellt, prüft er zunächst, ob der Server diese ObjID registriert hat. Wenn ja, sendet er ein Kontextweitergabe-Payload an ihn. Wenn nicht, überspringt er die Kontextweitergabe und führt einfach den regulären Aufruf aus.
Das Problem ist, dass dieser Endpunkt innerhalb der JVM-RMI-Laufzeitumgebung registriert ist, was bedeutet, dass er denselben Transport nutzt wie der RMI- oder JMX-Port, den die Anwendung offen hat. Wenn die Anwendung -Dcom.sun.management.jmxremote.port gesetzt hat oder eigene RMI-Dienste exportiert, ist der Endpunkt des Agents auf demselben Port für jeden erreichbar, der sich verbinden kann.
Die Deserialisierung findet in ContextPayload.java statt. In Versionen unter 2.26.1 sah die read()-Methode wie folgt aus:
@SuppressWarnings("BanSerializableRead") // fine
public static ContextPayload read(ObjectInput oi) throws IOException {
try {
Object object = oi.readObject();
if (object instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, String> map = (Map<String, String>) object;
return new ContextPayload(map);
}
} catch (ClassCastException | ClassNotFoundException ex) {
logger.log(FINE, "Error reading object", ex);
}
return null;
}
Die Annotation @SuppressWarnings("BanSerializableRead") mit dem Kommentar // fine ist tatsächlich ziemlich aufschlussreich. Jemand im Team hat dies irgendwann als Bedenken markiert, die Unterdrückung wurde hinzugefügt, und der Kommentar sollte dies rechtfertigen. Offensichtlich war es nicht in Ordnung.
oi.readObject() ohne Serialisierungsfilter ist das klassische Muster unsicherer Deserialisierung. Die Java-Serialisierung instanziiert während der Deserialisierung bereitwillig jede Klasse auf dem Classpath, und wenn der Angreifer den serialisierten Stream kontrollieren kann, kann er wählen, welche Klassen in welcher Reihenfolge instanziiert werden. Das ist die Grundlage von Gadget-Chain-Angriffen.
Der Code prüft nach der Deserialisierung zwar instanceof Map, aber zu diesem Zeitpunkt ist der Schaden bereits angerichtet. Die Gadget-Chain wird während des readObject()-Aufrufs selbst ausgeführt, bevor eine Typprüfung stattfindet. Die instanceof-Prüfung ist aus Sicherheitssicht völlig irrelevant.
Der Fix in 2.26.1 ersetzt den Deserialisierungsansatz vollständig. Anstatt ein Map-Objekt zu serialisieren und readObject() aufzurufen, liest die neue Implementierung die Kontexteinträge manuell als primitive Typen:
@Nullable
public static ContextPayload read(ObjectInput oi) throws IOException {
int size = oi.readInt();
if (size > MAX_CONTEXT_ENTRIES) {
logger.log(
FINE,
"RMI context propagation payload size {0} exceeds maximum allowed of {1}, skipping context propagation.",
new Object[] {size, MAX_CONTEXT_ENTRIES});
return null;
}
Map<String, String> map = new HashMap<>();
for (int i = 0; i < size; i++) {
String key = oi.readUTF();
String value = oi.readUTF();
map.put(key, value);
}
return new ContextPayload(map);
}
Und die Schreibseite wurde entsprechend aktualisiert:
public void write(ObjectOutput out) throws IOException {
int size = context.size();
if (size > MAX_CONTEXT_ENTRIES) {
out.writeInt(0);
return;
}
out.writeInt(size);
for (Map.Entry<String, String> entry : context.entrySet()) {
out.writeUTF(entry.getKey());
out.writeUTF(entry.getValue());
}
}
Dieser Ansatz liest nur primitive Typen (Ints und UTF-Strings) aus dem Stream. Es findet keine Objektinstanziierung statt. Keine Gadget-Chain kann über readInt() oder readUTF() ausgeführt werden. Der Fix ist korrekt und vollständig.
Es gibt noch eine zweite Änderung, die erwähnenswert ist. Die ObjID, die zur Identifizierung des benutzerdefinierten Endpunkts des Agents verwendet wird, wurde versioniert:
// Vorher (2.26.0)
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
// Nachher (2.26.1)
new ObjID("io.opentelemetry.javaagent.context-call-v2".hashCode());
Das bedeutet, dass ein gepatchter Agent auf die alte ObjID überhaupt nicht mehr reagiert. Ein Angreifer, der den alten Endpunkt angreift, würde von einem gepatchten Server keine Antwort erhalten. Dies ist eine zusätzliche Härtungsmaßnahme zusätzlich zum Deserialisierungs-Fix – sie bricht effektiv jeden Exploit, der gegen den v1-Endpunkt gebaut wurde, selbst wenn die Deserialisierung irgendwie noch erreichbar wäre.
Alle drei der folgenden Bedingungen müssen gleichzeitig erfüllt sein, damit dies ausnutzbar ist:
1. OpenTelemetry Java Agent auf JDK 16 oder niedriger angehängt
Dies ist die wichtigste Einschränkung. JDK 17 führte erhebliche Änderungen an den RMI-Interna ein und erzwingt eine viel strengere Modul-Kapselung über das Java Platform Module System. Die meisten Gadget-Chains, die gegen Java-Deserialisierung funktionieren, verlassen sich auf Reflektion, um auf interne JDK-Klassen zuzugreifen oder Methoden aufzurufen, die normalerweise nicht zugänglich wären. JDK 17 bricht diese Reflektionspfade standardmäßig aufgrund der starken Kapselung, was bedeutet, dass die Mehrheit der bekannten Gadget-Chains auf JDK 17 und höher einfach nicht funktioniert.
Auf JDK 8, 11 und 16 ist der Reflektionszugriff jedoch viel permissiver und Gadget-Chains funktionieren wie erwartet. Diese JDK-Versionen stellen immer noch einen erheblichen Teil der Java-Produktionsbereitstellungen dar, insbesondere in älteren Unternehmensumgebungen.
2. Ein RMI- oder JMX-Port ist für den Angreifer netzwerkerreichbar
Die Anwendung muss etwas wie -Dcom.sun.management.jmxremote.port=9010 konfiguriert haben oder eigene RMI-Dienste exportieren. Der Endpunkt des OTel-Agents nutzt den bereits verwendeten RMI-Transport mit. Wenn kein RMI-Port offen ist, ist der Endpunkt nicht erreichbar.
3. Eine mit Gadget-Chains kompatible Bibliothek ist auf dem Anwendungs-Classpath vorhanden
Das Agent-JAR selbst enthält keine mit Gadget-Chains kompatiblen Klassen. Wir haben dies bestätigt, indem wir die Klassenliste aus dem 2.26.0-Agent-JAR untersucht haben. Der Agent enthält OTel-Instrumentierungscode, schattierte API-Klassen, Semconv-Definitionen und Bootstrap-Infrastruktur. Keine der häufig ausgenutzten Gadget-Quellen wie Commons Collections, Commons BeanUtils oder Spring Framework-Klassen sind im Agent gebündelt.
Das bedeutet, dass die Gadget-Chain aus der Anwendung kommen muss. Der Entwickler muss eine Bibliothek eingebunden haben, die ausnutzbare Serialisierungs-Gadgets enthält, was bedeutet, dass die Ausnutzbarkeit davon abhängt, wovon die Anwendung abhängig ist.
Die drei oben genannten Bedingungen mögen einschränkend klingen, aber sie treten in realen Enterprise-Java-Umgebungen tatsächlich häufig gemeinsam auf. Betrachten wir ein typisches Produktionsszenario: ein Backend-Dienst, der auf JDK 11 oder JDK 17 läuft (mit --add-opens-Flags, die in Container-Bereitstellungen üblich sind und Gadget-Chains teilweise wieder aktivieren können), mit dem OTel-Agent für Observability instrumentiert, mit JMX für Betriebsüberwachung aktiviert und einem Abhängigkeitsbaum, der reich genug ist, um Gadget-kompatible Klassen zu enthalten.
Die wichtigste Erkenntnis ist, dass Entwickler Observability-Agents als passive Infrastruktur betrachten. Sie erwarten, dass der Agent beobachtet, nicht dass er neue netzwerkerreichbare Endpunkte öffnet. Die hier geschaffene Angriffsfläche ist aus Sicht des Anwendungsentwicklers unsichtbar. Sie haben keinen RMI-Code geschrieben. Sie haben keinen RMI-Endpunkt konfiguriert. Der Agent hat es stillschweigend als Folge der Instrumentierung getan.
Für weitere Forschung zur Gadget-Chain-Anforderung dokumentiert das ysoserial-Projekt (öffentlich verfügbar, in akademischer und professioneller Sicherheitsforschung weit verbreitet) mehrere Java-Deserialisierungs-Gadget-Chains, die für diese Klasse von Schwachstellen relevant sind. Chains wie CommonsCollections, Spring1 und Spring2 sind gut dokumentierte Beispiele dafür, wie vorhandene Bibliotheksklassen verkettet werden können, um Code-Ausführung durch unsichere Deserialisierung zu erreichen. Welche spezifischen Chains in einer bestimmten Umgebung anwendbar sind, hängt vollständig davon ab, welche Bibliotheken die Anwendung auf ihrem Classpath hat.
Upgrade auf opentelemetry-javaagent Version 2.26.1 oder höher. Dies ist der einzige vollständige Fix.
Wenn ein sofortiges Upgrade nicht möglich ist, kann die RMI-Instrumentierung vollständig deaktiviert werden, indem die folgende Systemeigenschaft zu den JVM-Startflags hinzugefügt wird:
-Dotel.instrumentation.rmi.enabled=false
Dies deaktiviert die Instrumentierung, die den verwundbaren Endpunkt registriert, und entfernt die Angriffsfläche vollständig. Die Kontextweitergabe über RMI-Aufrufe funktioniert bei gesetztem Flag nicht, aber das ist als temporäre Abschwächung akzeptabel.
Unabhängig von dieser CVE gilt: Wenn JMX auf einem netzwerkerreichbaren Port ohne Authentifizierung exponiert ist, sollte dies unabhängig vom Vorhandensein des OTel-Agents als kritische Fehlkonfiguration behandelt werden.