
CVE-2026-33701에 대한 기술 분석: OpenTelemetry Java Agent RMI 계측의 안전하지 않은 역직렬화 취약점으로, 악용 조건, 패치 세부 사항 및 완화 전략을 포함합니다.
심각도: 치명적 (CVSS v4.0: 9.3)
영향을 받는 버전: opentelemetry-javaagent < 2.26.1
패치된 버전: 2.26.1 (2026년 3월 23일 릴리스)
OpenTelemetry는 현재 Java 생태계에서 가장 널리 채택된 관측 가능성(observability) 프레임워크입니다. 특히 Java 에이전트는 수천 개의 프로덕션 서비스에서 개발자의 코드 변경 없이 애플리케이션에 자동으로 계측을 수행하여 추적, 메트릭, 로깅을 지원하는 데 사용됩니다. javaagent 플래그로 첨부하기만 하면 백그라운드에서 모든 작업을 처리합니다.
CVE-2026-33701이 흥미로운 이유는 취약점 자체뿐만 아니라, 그것이 어떻게 도입되었는지에 있습니다. 에이전트는 RMI 계측의 부수 효과로 사용자 지정 RMI 엔드포인트를 조용히 등록합니다. 개발자는 그것이 존재한다는 사실을 모릅니다. 애플리케이션 코드에는 없습니다. 개발자가 구성한 것도 아닙니다. 에이전트가 자동으로 추가한 것이며, 2.26.1 미만 버전에서는 해당 엔드포인트가 필터 없이 들어오는 데이터를 역직렬화하고 있었습니다.
애플리케이션이 이미 RMI 또는 JMX 포트를 노출하고 있었다면, 해당 포트에 네트워크 접근 권한이 있는 공격자는 에이전트의 사용자 지정 엔드포인트에 조작된 직렬화 페이로드를 보내 JVM 프로세스의 권한으로 원격 코드 실행(RCE)을 달성할 수 있었습니다. 단, RCE에는 애플리케이션 클래스패스에 호환 가능한 가젯 체인(gadget chain)이 존재해야 한다는 조건이 있습니다. 이에 대해서는 나중에 자세히 설명합니다.
OTel Java 에이전트가 JVM에 첨부되면 컨텍스트 전파를 위해 RMI 호출을 계측합니다. 아이디어는 RMI 클라이언트가 원격 메서드를 호출할 때 에이전트가 현재 추적 컨텍스트를 서버 측으로 전파하여 서비스 경계를 넘어 스팬(span)이 올바르게 연결되도록 하는 것입니다.
이를 위해 에이전트는 하드코딩된 ObjID를 사용하여 자체 사용자 지정 RMI 객체를 등록합니다. ContextPropagator.java에서:
public static final ObjID CONTEXT_CALL_ID =
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
이 ObjID는 에이전트가 RMI 런타임 내에서 자체 엔드포인트를 식별하는 방법입니다. 계측된 RMI 클라이언트가 서버에 연결할 때 먼저 서버에 이 ObjID가 등록되어 있는지 확인합니다. 등록되어 있으면 컨텍스트 전파 페이로드를 전송합니다. 그렇지 않으면 컨텍스트 전파를 건너뛰고 일반 호출만 수행합니다.
문제는 이 엔드포인트가 JVM RMI 런타임 내부에 등록되어 있어 애플리케이션이 열어둔 RMI 또는 JMX 포트와 동일한 전송 계층을 공유한다는 점입니다. 애플리케이션이 -Dcom.sun.management.jmxremote.port를 설정했거나 자체 RMI 서비스를 내보내는 경우, 에이전트의 엔드포인트는 연결할 수 있는 모든 사람이 해당 포트를 통해 접근할 수 있습니다.
역직렬화는 ContextPayload.java에서 발생합니다. 2.26.1 미만 버전에서 read() 메서드는 다음과 같았습니다:
@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;
}
@SuppressWarnings("BanSerializableRead") 어노테이션과 // fine 주석은 사실 의미심장합니다. 팀의 누군가가 어느 시점에 이 문제를 우려 사항으로 지적했고, 억제(suppression)가 추가되었으며, 주석은 이를 정당화하기 위한 것이었습니다. 분명히 문제가 있었습니다.
직렬화 필터가 없는 oi.readObject()는 전형적인 안전하지 않은 역직렬화 패턴입니다. Java 직렬화는 역직렬화 중에 클래스패스의 모든 클래스를 자유롭게 인스턴스화하며, 공격자가 직렬화 스트림을 제어할 수 있다면 어떤 클래스를 어떤 순서로 인스턴스화할지 선택할 수 있습니다. 이것이 가젯 체인 공격의 기반입니다.
코드는 역직렬화 후 instanceof Map을 확인하지만, 그 시점에는 이미 피해가 발생했습니다. 가젯 체인은 readObject() 호출 자체 중에 실행되며, 어떤 타입 검사도 이루어지기 전입니다. instanceof 검사는 보안 관점에서 완전히 무의미합니다.
2.26.1의 수정은 역직렬화 방식을 완전히 대체합니다. Map 객체를 직렬화하고 readObject()를 호출하는 대신, 새 구현은 컨텍스트 항목을 기본 타입으로 수동으로 읽습니다:
@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);
}
그리고 쓰기 측도 일치하도록 업데이트되었습니다:
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());
}
}
이 방식은 스트림에서 기본 타입(int 및 UTF 문자열)만 읽습니다. 객체 인스턴스화가 발생하지 않습니다. readInt() 또는 readUTF()를 통해 가젯 체인이 실행될 수 없습니다. 수정은 정확하고 완전합니다.
또한 주목할 만한 두 번째 변경 사항이 있습니다. 에이전트의 사용자 지정 엔드포인트를 식별하는 데 사용되는 ObjID에 버전이 지정되었습니다:
// Before (2.26.0)
new ObjID("io.opentelemetry.javaagent.context-call".hashCode());
// After (2.26.1)
new ObjID("io.opentelemetry.javaagent.context-call-v2".hashCode());
즉, 패치된 에이전트는 이전 ObjID에 전혀 응답하지 않습니다. 이전 엔드포인트를 대상으로 하는 공격자는 패치된 서버로부터 응답을 받지 못합니다. 이는 역직렬화 수정 위에 추가된 훌륭한 하드닝 조치로, 역직렬화가 어떻게든 여전히 도달 가능하더라도 v1 엔드포인트를 대상으로 구축된 모든 익스플로잇을 사실상 무력화합니다.
이 취약점이 악용 가능하려면 다음 세 가지 조건이 동시에 충족되어야 합니다:
1. JDK 16 이하에서 OpenTelemetry Java 에이전트가 첨부된 경우
이것이 가장 중요한 제약 조건입니다. JDK 17은 RMI 내부에 상당한 변경을 도입했으며 Java 플랫폼 모듈 시스템을 통해 훨씬 더 엄격한 모듈 캡슐화를 적용합니다. Java 역직렬화에 대해 작동하는 대부분의 가젯 체인은 리플렉션을 사용하여 내부 JDK 클래스에 접근하거나 일반적으로 접근할 수 없는 메서드를 호출합니다. JDK 17은 강력한 캡슐화로 인해 이러한 리플렉션 경로를 기본적으로 차단하므로, 알려진 가젯 체인의 대부분은 JDK 17 이상에서 작동하지 않습니다.
그러나 JDK 8, 11, 16에서는 리플렉션 접근이 훨씬 허용적이며 가젯 체인이 예상대로 작동합니다. 이러한 JDK 버전은 특히 오래된 엔터프라이즈 환경에서 여전히 상당 부분의 프로덕션 Java 배포를 차지합니다.
2. 공격자가 RMI 또는 JMX 포트에 네트워크로 접근할 수 있는 경우
애플리케이션에 -Dcom.sun.management.jmxremote.port=9010과 같은 설정이 있거나 자체 RMI 서비스를 내보내야 합니다. OTel 에이전트의 엔드포인트는 이미 사용 중인 RMI 전송 계층을 활용합니다. RMI 포트가 열려 있지 않으면 엔드포인트에 접근할 수 없습니다.
3. 가젯 체인과 호환되는 라이브러리가 애플리케이션 클래스패스에 존재하는 경우
에이전트 jar 자체에는 가젯 체인과 호환되는 클래스가 포함되어 있지 않습니다. 2.26.0 에이전트 jar에서 추출한 클래스 목록을 검사하여 이를 확인했습니다. 에이전트에는 OTel 계측 코드, 셰이딩된 API 클래스, semconv 정의 및 부트스트랩 인프라가 포함되어 있습니다. Commons Collections, Commons BeanUtils 또는 Spring Framework 클래스와 같은 일반적으로 악용되는 가젯 소스는 에이전트 내부에 번들되어 있지 않습니다.
즉, 가젯 체인은 애플리케이션에서 제공되어야 합니다. 개발자가 악용 가능한 직렬화 가젯을 포함하는 라이브러리를 포함해야 하므로, 악용 가능성은 애플리케이션이 의존하는 항목에 따라 달라집니다.
위의 세 가지 조건이 제한적으로 보일 수 있지만 실제 엔터프라이즈 Java 환경에서는 함께 발생하는 경우가 매우 흔합니다. 일반적인 프로덕션 시나리오를 고려해 보세요: JDK 11 또는 JDK 17(--add-opens 플래그 포함, 컨테이너화된 배포에서 흔하며 가젯 체인을 부분적으로 다시 활성화할 수 있음)에서 실행되는 백엔드 서비스, 관측 가능성을 위해 OTel 에이전트로 계측되고, 운영 모니터링을 위해 JMX가 활성화되어 있으며, 가젯 호환 클래스를 포함할 만큼 풍부한 의존성 트리를 가진 경우.
핵심 통찰력은 개발자가 관측 가능성 에이전트를 수동적인 인프라로 신뢰한다는 것입니다. 에이전트가 관찰하기를 기대하지, 새로운 네트워크 접근 가능 엔드포인트를 열기를 기대하지 않습니다. 여기서 생성된 공격 표면은 애플리케이션 개발자의 관점에서 보이지 않습니다. 그들은 RMI 코드를 작성하지 않았습니다. RMI 엔드포인트를 구성하지도 않았습니다. 에이전트가 계측의 결과로 조용히 수행한 것입니다.
가젯 체인 요구 사항에 대한 추가 연구를 위해, ysoserial 프로젝트(공개적으로 사용 가능하며 학술 및 전문 보안 연구에서 널리 참조됨)는 이 취약점 클래스와 관련된 여러 Java 역직렬화 가젯 체인을 문서화합니다. CommonsCollections, Spring1, Spring2와 같은 체인은 기존 라이브러리 클래스를 연결하여 안전하지 않은 역직렬화를 통해 코드 실행을 달성하는 방법의 잘 문서화된 예입니다. 특정 환경에서 적용 가능한 체인은 전적으로 해당 애플리케이션의 클래스패스에 있는 라이브러리에 따라 달라집니다.
opentelemetry-javaagent 버전 2.26.1 이상으로 업그레이드하세요. 이것이 유일한 완전한 수정입니다.
즉시 업그레이드가 불가능한 경우, JVM 시작 플래그에 다음 시스템 속성을 추가하여 RMI 계측을 완전히 비활성화할 수 있습니다:
-Dotel.instrumentation.rmi.enabled=false
이렇게 하면 취약한 엔드포인트를 등록하는 계측이 비활성화되어 공격 표면이 완전히 제거됩니다. 이 플래그가 설정된 동안 RMI 호출 간 컨텍스트 전파는 작동하지 않지만, 임시 완화 조치로는 허용 가능합니다.
이 CVE와 별개로, JMX가 인증 없이 네트워크 접근 가능한 포트에 노출된 경우 OTel 에이전트의 존재 여부와 관계없이 심각한 잘못된 구성으로 취급해야 합니다.