PratikKaran23/aem-hunter
GitHub: PratikKaran23/aem-hunter
Stars: 0 | Forks: 0
# AEM Hunter
Single-file Adobe Experience Manager (AEM) security audit tool for authorized
penetration testing and bug bounty work. Drop the script onto a box, run it,
and get a console + HTML + JSON report of misconfigurations, exposed admin
surfaces, dispatcher bypasses, and selected CVEs.
## Install
git clone https://github.com/PratikKaran23/aem-hunter.git
cd aem-hunter
pip install -r requirements.txt
Single dependency: `requests`. Everything else is standard library.
## Usage
The whole tool is just **URL + cookies**. Start it, and after every scan it
asks you to paste the next Cookie header — so you feed it one user role after
another and it keeps scanning. Press Enter on a blank prompt for an
unauthenticated scan, or type `q` to quit. Every scan writes its own report.
Start it (it will prompt for the URL if you don't pass one):
python3 aem_hunter.py
python3 aem_hunter.py https://aem.example.com
python3 aem_hunter.py -u https://aem.example.com
Pre-load the first role's cookies:
python3 aem_hunter.py -u https://aem.example.com \
-c "login-token=...; cq-authoring-mode=TOUCH"
Route through Burp / mitmproxy:
python3 aem_hunter.py -u https://aem.example.com --proxy http://127.0.0.1:8080
### HTTP/2-only targets
Many enterprise AEM deployments sit behind a CDN/WAF/LB that **only speaks
HTTP/2**. Python `requests` is HTTP/1.1-only, so a direct scan dies with
`UnknownProtocol('HTTP/2')` and every request fails. Two options:
- **Through Burp/mitmproxy** (`--proxy ...`) — the proxy downgrades HTTP/2 to
HTTP/1.1, so the default backend just works.
- **Native HTTP/2** — no proxy needed:
pip install 'httpx[http2]'
python3 aem_hunter.py -u https://aem.example.com --http2 -c "..."
The tool detects the HTTP/2 error and tells you which fix to use. The default
`requests` backend is unchanged; `--http2` only switches transport when set.
### The workflow
$ python3 aem_hunter.py -u https://aem.example.com
[?] Paste Cookie header (Enter=unauth, q=quit): login-token=AAA...; cq-authoring-mode=TOUCH
[+] Loaded 2 cookie(s) -> cookie-set-1
[+] [cookie-set-1] authenticated as: content-editor@corp
... scan runs, report written ...
[?] Paste Cookie header (Enter=unauth, q=quit): login-token=BBB... # next role
[+] Loaded 1 cookie(s) -> cookie-set-2
[+] [cookie-set-2] authenticated as: cpb-deployer@corp
... scan runs, report written ...
[?] Paste Cookie header (Enter=unauth, q=quit): q
Grab the Cookie header for each role from your browser DevTools (Network tab →
any request → Request Headers → `Cookie`) or from Burp, and paste it in when
prompted. You can also point at a file with `@`, e.g. `@/tmp/editor-cookies.txt`.
### Per-role cookie store (`--cookies-dir`)
For multi-role testing, use a cookie store so you don't re-paste every run. Two
ways, both via `--cookies-dir`:
**A — pre-save a file per role** (one Cookie header per file, filename = role):
cookies/
content-editor.txt
cpb-deployer.txt
content-reviewer-publisher.txt
python3 aem_hunter.py -u TARGET --proxy http://127.0.0.1:8080 --cookies-dir cookies --exploit
**B — let the tool collect them** (empty/missing folder): it asks for each
role's name + cookie, saves `cookies/.txt` itself, then scans — and on the
next run those files are auto-loaded (mode A).
python3 aem_hunter.py -u TARGET --cookies-dir # defaults to ./cookies
The `cookies/` folder is git-ignored (it holds live session tokens).
### All flags
| Flag | Purpose |
| -------------------- | -------------------------------------------------- |
| `target` / `-u` | Target URL (positional or `-u`; prompted if absent)|
| `-c, --cookie` | Cookie header for the first scan (optional) |
| `--proxy` | Route through a proxy (optional, e.g. Burp) |
| `-o, --output-dir` | Where reports land (default: current dir) |
| `-v, --verbose` | Verbose request logging |
TLS verification is always off (pentest default). That's the entire surface —
no roles to configure, no module flags.
## What it tests
| Category | Coverage |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| Fingerprinting | Instance type (Author vs Publish), version hints, Sling / Day / CQ headers |
| Default credentials | admin, author, anonymous, replication-receiver, Geometrixx demo users, vgnadmin, audit |
| Exposed consoles | Felix `/system/console`, CRX DE, CRX Package Manager, CRX Explorer, Groovy Console, WebDAV |
| QueryBuilder | `/bin/querybuilder.json` exposure + extension bypasses |
| Dispatcher bypass | `.css` / `.js` / `.png` / `.html` selector tricks, `;` semicolon abuse, `..;/` Jetty normalization |
| Sling info dump | `.json`, `.1.json`, `.tidy.json`, `.infinity.json`, `.harray.4.json` on common roots |
| JCR enumeration | users.1.json, groups.1.json, currentuser.json, group memberships |
| Cloud services leak | `/etc/cloudservices.infinity.json` and friends – AWS / Salesforce / 3rd-party credentials leak |
| SSRF | linkchecker, SalesforceSecretServlet (CVE-2018-5006), ReportingServicesServlet (CVE-2018-12809) |
| 2025 CVE wave | CVE-2025-54253 (OGNL RCE in Forms JEE), CVE-2025-54254 (XXE), CVE-2025-49533 |
| Path-traversal CVE | CVE-2021-43762 |
| Sling POST abuse | Arbitrary node creation, property manipulation, `:operation` and `:member` primitives |
| Replication | `/etc/replication.json` and agent transport credentials |
| Source disclosure | clientlib `.js.source` / `.source.json` quirks |
| Servlet exposure | GQLServlet, LoginStatusServlet (+ default-cred check), AuditLogServlet, CRXDE logs, Disk Usage, BackgroundServlet, BulkEditor, UserAdmin, Offloading, miscadmin, dumplibs, nodetypes, MergeMetadata |
| XSS | ChildrenList selector, CRXDE setPreferences, WCMDebugFilter (CVE-2016-7882), WCMSuggestionsServlet, reflected XSS via SWF |
| Nuclei path set | ~15 detections ported from projectdiscovery/nuclei-templates AEM set + Cappricio aem-xss (exact matchers) |
| ACS AEM Tools | AEM Fiddle JSP-eval RCE, ACS Tools presence |
| Deserialization | ExternalJobServlet Java untrusted-deserialization probe (`--exploit`) |
| Out-of-band SSRF | Salesforce / Reporting / SiteCatalyst / AutoProvisioning / Opensocial via a callback listener |
| Auth session testing | Re-runs the full battery with each pasted Cookie header + privilege-boundary checks |
Much of the servlet/XSS/SSRF coverage is ported from
[0ang3el/aem-hacker](https://github.com/0ang3el/aem-hacker), re-implemented with
this tool's auth-wall suppression, role tagging, and reporting.
### Out-of-band SSRF — Burp Collaborator (`--collaborator`)
Blind SSRF in AEM's connector servlets is confirmed out-of-band. The
recommended way is **Burp Collaborator**: paste your Collaborator payload host
and the tool fires one sub-domain per servlet, so a hit in the Collaborator tab
names the vulnerable servlet:
python3 aem_hunter.py -u TARGET --proxy http://127.0.0.1:8080 --collaborator abc123.oastify.com
It probes Salesforce / Reporting / SiteCatalyst / AutoProvisioning / Opensocial
(+ makeRequest) / linkchecker with `http://.abc123.oastify.com/`.
Then open the Collaborator tab — e.g. a DNS/HTTP hit on
`salesforcesecret.abc123.oastify.com` confirms SSRF via SalesforceSecretServlet
(CVE-2018-5006). (The tool can't poll Collaborator for you, so it reports the
probes fired + the sub-domain→servlet map for attribution.)
Alternatively, if you have a tester-reachable IP (VPS/tunnel, not via Burp), use
a self-hosted auto-confirming listener:
python3 aem_hunter.py -u TARGET --ssrf-callback 1.2.3.4:8000
### Sling `resourceType` RCE
Beyond package install and direct `/apps` writes, the escalation (`--exploit`)
also tries Mikhail Egorov's **`sling:resourceType` chain** (from
[Static-Flow/aem-rce](https://github.com/Static-Flow/aem-rce) /
[Hacking AEM Sites](https://www.slideshare.net/0ang3el/hacking-aem-sites)):
upload a JSP to `/content` → `:operation=copy` it to `/apps` → bind
`sling:resourceType` → request the node so Sling executes the JSP. This lands
RCE when `/content` is writable and the copy reaches `/apps` even if a direct
`/apps` POST-create is blocked. It also tries the QueryBuilder
`p.hits=selective&p.properties=rep:password` trick to pull user hashes.
When you paste a Cookie header, the tool first hits
`/libs/granite/security/currentuser.json` and prints who you authenticated as,
so you immediately know whether the session is valid or expired before the scan
runs. Each authenticated scan also probes admin-only surfaces (CRXDE, OSGi
bundles, cloud-services tree, user/group trees, Groovy console) and flags any
that this session can reach as a privilege-boundary violation.
## Accuracy — no "shell loaded = critical" noise
AEM author instances serve the **HTML/JSP shell** of consoles like CRXDE,
Package Manager and the Felix console to *anyone* (HTTP 200), while the actual
functionality stays behind login. Naive scanners flag that 200 as CRITICAL —
a false positive. This tool does not:
- **Login / auth-wall responses are suppressed.** A 200 that is really a login
page (`j_security_check`, `granite.shell.login`, `QUICKSTART`, sign-in forms,
auth redirects, 401/403) is never reported as access.
- **Consoles are verified functionally, not by their shell.** A CRITICAL only
fires when a privileged operation actually succeeds — `bundles.json` returns
the live OSGi inventory, the package service returns a real package listing,
or a protected JCR node returns real `jcr:primaryType` JSON. If only the shell
renders, you get a single **INFO** note ("shell loads but no privileged
access — retest with role cookies"), not a critical.
- **Data endpoints must return real JCR/JSON**, not an empty `{}` or an HTML
page, and severity is upgraded only when the body actually contains
secret-like material.
So on a locked-down author instance you'll see mostly INFO — which is the
honest answer. The real findings come from the authenticated passes: paste a
low-privilege role's cookies and the same functional checks reveal whether that
role can drive a console or read admin data it shouldn't.
## Active escalation — confirm it's real, then prove impact
When a primitive is found (Package Manager reachable, CRX DavEx readable, JCR
readable), the escalation module automatically tries to turn it into proof.
It distinguishes **intended read-only behaviour** from **real exploitability**:
Safe-confirm tier (runs by default, fully reversible):
- **Package Manager** → creates a throwaway empty package, confirms success,
deletes it. If it works → this session can create/build/install packages,
and package install = code execution. CRITICAL.
- **CRX DavEx** → `MKCOL`s a throwaway collection under `/tmp`, then `DELETE`s
it. Proves arbitrary JCR write over WebDAV.
- **Sling POST** → creates then deletes a node under `/apps`, `/var`,
`/content`, `/etc`, `/conf`, `/tmp`. A writable `/apps` (code space) is
flagged CRITICAL — that's a direct path to RCE.
- **Secret harvesting** → pulls readable trees and extracts every
password / key / token. Encrypted `{...}` values are flagged HIGH (note: a
readable `/etc/key` master key lets you decrypt them offline); plaintext
secrets are CRITICAL.
Exploit tier (only with `--exploit`, drops & removes artifacts):
python3 aem_hunter.py -u TARGET -c "login-token=..." --exploit
The canary JSP only prints `System.getProperty("user.name")` — it proves Java
code execution without running OS commands. Everything created is cleaned up.
Use `--exploit` only on targets you're authorized to actively exploit.
## Reports
For **every** scan (each cookie set + the unauthenticated baseline) you get:
- live console output with severity tags
- `report---.json` – machine readable findings
- `report---.html` – styled report with evidence,
request/response snippets, references and CVE badges
The HTML uses inline CSS, so it renders fine on an air-gapped box with no
internet access. Reports are git-ignored so findings never get committed.
## References
Built on top of public research from:
- 0ang3el/aem-hacker
- Assetnote / hopgoblin
- HackTricks AEM section
- Mikhail Egorov, "Hacking AEM" (adaptTo 2018)
- Adobe APSB advisories, CISA KEV (CVE-2025-54253)
- Various HackerOne disclosures (#1247163, #436555, #698991, ...)
## License
MIT. Use responsibly.