
Analisi tecnica di CVE-2026-33701, una vulnerabilità di deserializzazione non sicura nell'instrumentazione RMI di OpenTelemetry Java Agent, inclusi condizioni di sfruttamento, dettagli della patch e strategie di mitigazione.
Gravità: Critica (CVSS v4.0: 9.3)
Versioni interessate: opentelemetry-javaagent < 2.26.1
Corretta in: 2.26.1 (rilasciata il 23 marzo 2026)
OpenTelemetry è probabilmente il framework di osservabilità più adottato nell'ecosistema Java al momento. L'agente Java in particolare viene utilizzato da migliaia di servizi in produzione per instrumentare automaticamente le applicazioni per tracing, metriche e logging senza richiedere alcuna modifica al codice da parte dello sviluppatore. Basta allegarlo come flag javaagent e fa tutto dietro le quinte.
Ciò che rende interessante CVE-2026-33701 non è solo la vulnerabilità in sé, ma la natura di come è stata introdotta. L'agente registra silenziosamente un endpoint RMI personalizzato come effetto collaterale della sua instrumentazione RMI. Gli sviluppatori non sanno che è lì. Non è in alcun codice applicativo. Non è qualcosa che hanno configurato. L'agente lo ha messo lì automaticamente e, per le versioni precedenti alla 2.26.1, quell'endpoint deserializzava i dati in arrivo senza alcun filtro applicato.
Se l'applicazione aveva già una porta RMI o JMX esposta, un attaccante con accesso di rete a quella porta poteva inviare un payload serializzato appositamente costruito all'endpoint personalizzato dell'agente e potenzialmente ottenere l'esecuzione remota di codice con i privilegi del processo JVM. Il problema è che l'RCE richiede una catena di gadget compatibile presente nel classpath dell'applicazione. Ne parleremo più avanti.
Quando l'agente OTel Java si aggancia a una JVM, instrumenta le chiamate RMI per la propagazione del contesto. L'idea è che quando un client RMI chiama un metodo remoto, l'agente deve propagare il contesto di trace corrente al lato server affinché gli span si colleghino correttamente attraverso i confini dei servizi.
Per fare ciò, l'agente registra il proprio oggetto RMI personalizzato utilizzando un ObjID hardcoded. In ContextPropagator.java:
public static final ObjID CONTEXT_CALL_ID =
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
Questo ObjID è il modo in cui l'agente identifica il proprio endpoint all'interno del runtime RMI. Quando un client RMI instrumentato si connette a un server, prima verifica se il server ha questo ObjID registrato. Se lo ha, invia un payload di propagazione del contesto. Se non lo ha, salta la propagazione del contesto e fa solo la chiamata regolare.
Il problema è che questo endpoint è registrato all'interno del runtime RMI della JVM, il che significa che condivide lo stesso trasporto di qualsiasi porta RMI o JMX che l'applicazione ha aperta. Se l'applicazione ha impostato -Dcom.sun.management.jmxremote.port, o se esporta i propri servizi RMI, l'endpoint dell'agente è raggiungibile su quella stessa porta da chiunque possa connettersi.
La deserializzazione avviene in ContextPayload.java. Nelle versioni precedenti alla 2.26.1, il metodo read() appariva così:
@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;
}
L'annotazione @SuppressWarnings("BanSerializableRead") con il commento // fine è in realtà piuttosto rivelatrice. Qualcuno nel team ha segnalato questo come una preoccupazione a un certo punto, è stata aggiunta la soppressione e il commento doveva giustificarla. Chiaramente non era a posto.
oi.readObject() senza filtri di serializzazione è il classico pattern di deserializzazione non sicura. La serializzazione Java istanzierà felicemente qualsiasi classe nel classpath durante la deserializzazione e, se l'attaccante può controllare lo stream serializzato, può scegliere quali classi istanziare e in quale ordine. Questa è la base degli attacchi a catena di gadget.
Il codice controlla instanceof Map dopo la deserializzazione, ma a quel punto il danno è già fatto. La catena di gadget si esegue durante la chiamata readObject() stessa, prima che avvenga qualsiasi controllo di tipo. Il controllo instanceof è completamente irrilevante dal punto di vista della sicurezza.
La correzione nella 2.26.1 sostituisce completamente l'approccio di deserializzazione. Invece di serializzare un oggetto Map e chiamare readObject(), la nuova implementazione legge manualmente le voci di contesto come tipi primitivi:
@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);
}
E il lato scrittura è stato aggiornato per corrispondere:
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());
}
}
Questo approccio legge solo tipi primitivi (int e stringhe UTF) dallo stream. Non avviene alcuna istanziazione di oggetti. Nessuna catena di gadget può eseguire tramite readInt() o readUTF(). La correzione è corretta e completa.
C'è anche una seconda modifica degna di nota. L'ObjID utilizzato per identificare l'endpoint personalizzato dell'agente è stato versionato:
// Prima (2.26.0)
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
// Dopo (2.26.1)
new ObjID("io.opentelemetry.javaagent.context-call-v2".hashCode());
Questo significa che un agente corretto non risponderà affatto al vecchio ObjID. Un attaccante che prende di mira il vecchio endpoint non riceverà alcuna risposta da un server corretto. Questa è una bella misura di hardening aggiuntiva oltre alla correzione della deserializzazione: rompe efficacemente qualsiasi exploit costruito contro l'endpoint v1 anche se in qualche modo la deserializzazione fosse ancora raggiungibile.
Tutte e tre le seguenti condizioni devono essere vere simultaneamente affinché questo sia sfruttabile:
1. OpenTelemetry Java Agent agganciato su JDK 16 o inferiore
Questo è il vincolo più importante. JDK 17 ha introdotto modifiche significative agli internals RMI e impone un'incapsulazione dei moduli molto più rigorosa tramite il Java Platform Module System. La maggior parte delle catene di gadget che funzionano contro la deserializzazione Java si affidano alla riflessione per accedere a classi JDK interne o per invocare metodi che normalmente non sarebbero accessibili. JDK 17 rompe quei percorsi di riflessione per impostazione predefinita a causa della forte incapsulazione, il che significa che la maggior parte delle catene di gadget note semplicemente non funziona su JDK 17 e successivi.
Su JDK 8, 11 e 16, tuttavia, l'accesso alla riflessione è molto più permissivo e le catene di gadget funzionano come previsto. Queste versioni JDK rappresentano ancora una parte sostanziale delle distribuzioni Java in produzione, specialmente in ambienti enterprise più vecchi.
2. Una porta RMI o JMX è raggiungibile in rete dall'attaccante
L'applicazione deve avere qualcosa come -Dcom.sun.management.jmxremote.port=9010 configurato, oppure deve esportare i propri servizi RMI. L'endpoint dell'agente OTel si appoggia su qualunque trasporto RMI sia già in uso. Se non c'è una porta RMI aperta, l'endpoint non è raggiungibile.
3. Una libreria compatibile con catene di gadget è presente nel classpath dell'applicazione
Il jar dell'agente stesso non contiene alcuna classe compatibile con catene di gadget. Lo abbiamo confermato ispezionando l'elenco delle classi estratte dal jar dell'agente 2.26.0. L'agente contiene codice di instrumentazione OTel, classi API shaded, definizioni semconv e infrastruttura di bootstrap. Nessuna delle sorgenti di gadget comunemente sfruttate come Commons Collections, Commons BeanUtils o classi Spring Framework è inclusa nell'agente.
Questo significa che la catena di gadget deve provenire dall'applicazione. Lo sviluppatore deve aver incluso una libreria che contiene gadget di serializzazione sfruttabili, il che significa che la sfruttabilità varia a seconda delle dipendenze dell'applicazione.
Le tre condizioni sopra potrebbero sembrare restrittive ma in realtà sono abbastanza comuni insieme negli ambienti Java enterprise reali. Considera uno scenario di produzione tipico: un servizio backend in esecuzione su JDK 11 o JDK 17 (con flag --add-opens, che sono comuni nelle distribuzioni containerizzate e possono parzialmente riabilitare le catene di gadget), instrumentato con l'agente OTel per l'osservabilità, con JMX abilitato per il monitoraggio operativo e un albero delle dipendenze abbastanza ricco da contenere classi compatibili con gadget.
Il punto chiave è che gli sviluppatori si fidano degli agenti di osservabilità come infrastruttura passiva. Si aspettano che l'agente osservi, non che apra nuovi endpoint raggiungibili in rete. La superficie di attacco creata qui è invisibile dalla prospettiva dello sviluppatore dell'applicazione. Non hanno scritto alcun codice RMI. Non hanno configurato alcun endpoint RMI. L'agente lo ha fatto silenziosamente come conseguenza dell'instrumentazione.
Per ulteriori ricerche sul requisito della catena di gadget, il progetto ysoserial (pubblicamente disponibile, ampiamente citato nella ricerca di sicurezza accademica e professionale) documenta diverse catene di gadget di deserializzazione Java rilevanti per questa classe di vulnerabilità. Catene come CommonsCollections, Spring1 e Spring2 sono esempi ben documentati di come le classi di librerie esistenti possano essere concatenate per ottenere l'esecuzione di codice tramite deserializzazione non sicura. Quali catene specifiche siano applicabili in un dato ambiente dipende interamente da quali librerie l'applicazione ha nel suo classpath.
Aggiorna a opentelemetry-javaagent versione 2.26.1 o successiva. Questa è l'unica correzione completa.
Se un aggiornamento immediato non è possibile, l'instrumentazione RMI può essere disabilitata completamente aggiungendo la seguente proprietà di sistema ai flag di avvio della JVM:
-Dotel.instrumentation.rmi.enabled=false
Questo disabilita l'instrumentazione che registra l'endpoint vulnerabile, rimuovendo completamente la superficie di attacco. La propagazione del contesto attraverso le chiamate RMI non funzionerà mentre questo flag è impostato, ma è accettabile come mitigazione temporanea.
Indipendentemente da questa CVE, se JMX è esposto su una porta raggiungibile in rete senza autenticazione, dovrebbe essere trattato come una configurazione errata critica indipendentemente dalla presenza o meno dell'agente OTel.