September 28, 2026weekly roundup

Three Java XML wrappers, two different transfer intents

Inspect ProcessBuilder command arrays across object wrappers to distinguish outbound retrieval/callback requests from local-file upload requests.

#java#xml#processbuilder#file-transfer#hunting

Correction ·

Revised 5 November 2026: replaced the archived roundup with reviewed historical analysis for the original reporting period.

The week's finding

Within the 2026-09-21 through 2026-09-27 UTC reporting week, BlackDome retained 10 matching Java XML request bodies received from 2026-09-25 through 2026-09-26 at 1 sensor. Three wrapper families carried ProcessBuilder command arrays: 8 requested curl with a destination argument, and 2 requested wget with --post-file, /etc/passwd and a destination argument. This analysis examines captured request instructions.

Follow the requested operation

The XML divides into 5 ImageIO ContainsFilter-related bodies, 2 Groovy Expando/MethodClosure bodies and 3 EventHandler proxy bodies. The common link is the ProcessBuilder object and its child command array. Eight arrays request curl with a destination argument. Two request wget with --post-file and /etc/passwd, specifying a local file as the outbound request body.

Those arguments change the investigation. Curl with a destination argument requests an outbound retrieval/callback. The --post-file form requests an account-file read and outward data transfer. A rule that says only “curl or wget appeared” loses that distinction; a rule tied to one wrapper class misses the other object forms.

MITRE ATT&CK's Exploit Public-Facing Application technique describes attempts to exploit weaknesses in exposed hosts or systems for initial access. In this case, use that only as background context: the directly supported pivot is from the HTTP/HTTPS request body and object construction to the ProcessBuilder command arguments when selecting host and network evidence. Primary-source document

Counts and variations

The current HTTP/HTTPS keyword-selected review cohort contains 901 retained event records in 251 daily source/input groups. This Java case accounts for 10 matching records, 10 daily source/input groups and 10 distinct body texts across 1 sensor.

The 2026-09-07 through 2026-09-13 retained Java cohort already contained 8 XML bodies spanning the same 3 wrapper families: 6 curl transfer requests and 2 Wget account-file upload requests. The current cohort contains 10 structured Java transfer-command records, split into 8 curl requests and 2 wget upload requests. The recurring detection target is the ProcessBuilder command relation and its arguments, not a single wrapper family.

Across the adjacent retained review windows, structured Java transfer-command records were 0 for 2026-08-31 to 2026-09-07, 8 for 2026-09-07 to 2026-09-14, 0 for 2026-09-14 to 2026-09-21 and 10 for 2026-09-21 to 2026-09-28, each end exclusive. Use that sequence to time-bound retained-body review and compare wrapper and command-argument shapes across weeks.

A practical local hunt

Run this local structural filter against retained XML bodies. It recognizes either representation of a ProcessBuilder object and checks its own command array, rather than matching disconnected class and tool names elsewhere in the document. The filter accepts a UTF-8 string up to 65536 bytes, reads it locally and performs no network request or submitted-code execution.

def matches_java_transfer(body):
    from defusedxml import ElementTree

    if not isinstance(body, str) or len(body.encode("utf-8")) > 65536:
        return False
    try:
        root = ElementTree.fromstring(body, forbid_dtd=True,
                                      forbid_entities=True, forbid_external=True)
    except (ElementTree.ParseError, ValueError):
        return False
    except Exception:
        return False
    for node in root.iter():
        if node.tag != "java.lang.ProcessBuilder" and node.get("class") != "java.lang.ProcessBuilder":
            continue
        command = node.find("command")
        if command is not None:
            args = [(item.text or "").strip() for item in command.findall("string")]
            if args and args[0] in {"curl", "wget"}:
                return True
    return False

For a hit, preserve the original XML, identify the receiving route and owning Java service, then classify the command arguments before prioritizing follow-up. Where host telemetry is available, correlate the request time with Java child processes, relevant file reads and outbound traffic. For false-positive triage, confirm the caller, the workload owner, the requested operation and any approved change or test record.