TryHackMe: XSS
Sean Lee

Sean Lee @seanleeys

About: An ambitious cybersecurity student. Interested in all things cybersecurity. 💡 Motto: “Know the attack to build the defence.”

Joined:
Mar 10, 2025

TryHackMe: XSS

Publish Date: Mar 17
0 0

JavaScript for XSS

In web browser, go Inspect Element, then go to Console.

Let’s review and try some essential JavaScript functions:

  • Alert: You can use the alert() function to display a JavaScript alert in a web browser. Try alert(1) or alert('XSS') (or alert("XSS")) to display an alert box with the number 1 or the text XSS. alert(document.cookie) to display cookie.
  • Console log: Similarly, you can display a value in the browser’s JavaScript console using console.log(). Try the following two examples in the console log: console.log(1) and console.log("test text") to display a number or a text string.
  • Encoding: The JavaScript function btoa("string") encodes a string of binary data to create a base64-encoded ASCII string. This is useful to remove white space and special characters or encode other alphabets. The reverse function is atob("base64_string").

Types of XSS

  • Reflected XSS: This attack relies on the user-controlled input reflected to the user. For instance, if you search for a particular term and the resulting page displays the term you searched for (reflected), the attacker would try to embed a malicious script within the search term.
  • Stored XSS: This attack relies on the user input stored in the website’s database. For example, if users can write product reviews that are saved in a database (stored) and being displayed to other users, the attacker would try to insert a malicious script in their review so that it gets executed in the browsers of other users.
  • DOM-based XSS: This attack exploits vulnerabilities within the Document Object Model (DOM) to manipulate existing page elements without needing to be reflected or stored on the server. This vulnerability is the least common among the three.

XSS Causes

1. Insufficient Input Validation & Sanitization

  • Web applications accept user input (e.g., forms) and dynamically generate HTML pages.
  • Without proper sanitization, malicious scripts can be embedded and executed.

2. Lack of Output Encoding

  • Special characters like <, >, ", ', and & must be encoded in HTML.
  • JavaScript-specific escaping is required for ', ", and \.
  • Failure to encode output leads to XSS vulnerabilities.

3. Improper Use of Security Headers

  • Content Security Policy (CSP) mitigates XSS by restricting script sources.
  • Misconfigured CSP (e.g., unsafe-inline, unsafe-eval) allows attackers to execute scripts.

4. Framework & Language Vulnerabilities

  • Older frameworks lacked built-in XSS protections.
  • Modern frameworks auto-escape user input but must be kept updated.

5. Third-Party Libraries

  • External libraries can introduce XSS vulnerabilities, even in secure applications.

XSS Implications

1. Session Hijacking

  • Attackers can steal session cookies and impersonate users.

2. Phishing & Credential Theft

  • XSS enables fake login prompts to steal user credentials.

3. Social Engineering

  • Attackers can create fake pop-ups or alerts on trusted websites.

4. Content Manipulation & Defacement

  • Attackers can modify web pages, damaging a company’s reputation.

5. Data Exfiltration

  • XSS can extract sensitive user data (e.g., financial or personal information).

6. Malware Installation

  • Drive-by downloads can install malware through XSS exploits.

By implementing proper input validation, output encoding, security headers, and secure coding practices, developers can reduce the risk of XSS attacks. 🚨


Reflected XSS

  • PHP
  • JavaScript (Node.js)
  • Python (Flask)
  • C# (ASP.NET)

PHP

Vulnerable code:

<?php
$search_query = $_GET['q'];
echo "<p>You searched for: $search_query</p>";
?>
Enter fullscreen mode Exit fullscreen mode

Example exploitation:

http://shop.thm/search.php?q=<script>alert(document.cookie)</script>

Fixed code:

<?php
$search_query = $_GET['q'];
$escaped_search_query = htmlspecialchars($search_query);
echo "<p>You searched for: $escaped_search_query</p>";
?>
Enter fullscreen mode Exit fullscreen mode

The PHP function htmlspecialchars() converts special characters to HTML entities. The characters <, >, &, ", ' are replaced by default to prevent scripts in the input from executing. You can read its documentation here.

JavaScript (Node.js)

Vulnerable code:

const express = require('express');
const app = express();

app.get('/search', function(req, res) {
    var searchTerm = req.query.q;
    res.send('You searched for: ' + searchTerm);
});

app.listen(80);
Enter fullscreen mode Exit fullscreen mode

Example exploitation:

http://shop.thm/search.php?q=<script>alert(document.cookie)</script>

Fixed code:

const express = require('express');
const sanitizeHtml = require('sanitize-html');

const app = express();

app.get('/search', function(req, res) {
    const searchTerm = req.query.q;
    const sanitizedSearchTerm = sanitizeHtml(searchTerm);
    res.send('You searched for: ' + sanitizedSearchTerm);
});

app.listen(80);
Enter fullscreen mode Exit fullscreen mode

Using functions like:

  • sanitizeHtml(): removes unsafe elements and attributes. This includes removing script tags, among other elements that could be used for malicious purposes. You can read its documentation here.
  • escapeHtml(): escape characters such as <, >, &, ", and '. You can check its homepage here.

Python (Flask)

Vulnerable code:

from flask import Flask, request

app = Flask(__name__)

@app.route("/search")
def home():
    query = request.args.get("q")
    return f"You searched for: {query}!"

if __name__ == "__main__":
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

Example exploitation:

http://shop.thm/search.php?q=<script>alert(document.cookie)</script>

Fixed code:

from flask import Flask, request
from html import escape

app = Flask(__name__)

@app.route("/search")
def home():
    query = request.args.get("q")
    escaped_query = escape(query)
    return f"You searched for: {escaped_query}!"

if __name__ == "__main__":
    app.run(debug=True)
Enter fullscreen mode Exit fullscreen mode

html.escape() (alias for markupsafe.escape()) escapes unsafe characters in strings. This function converts characters like <, >, ", ' to HTML escaped entities, disarming any malicious code the user has inserted.

C# (ASP.NET)

Vulnerable code:

public void Page_Load(object sender, EventArgs e)
{
    var userInput = Request.QueryString["q"];
    Response.Write("User Input: " + userInput);
}
Enter fullscreen mode Exit fullscreen mode

Example exploitation:

http://shop.thm/search.php?q=<script>alert(document.cookie)</script>

Fixed code;

using System.Web;

public void Page_Load(object sender, EventArgs e)
{
    var userInput = Request.QueryString["q"];
    var encodedInput = HttpUtility.HtmlEncode(userInput);
    Response.Write("User Input: " + encodedInput);
}
Enter fullscreen mode Exit fullscreen mode

HttpUtility.HtmlEncode() method, which converts various characters, such as <, >, and &, into their respective HTML entity encoding.


Stored XSS

Ways to Prevent Stored XSS

1. Validate and Sanitize Input

  • Define clear rules and enforce strict validation on all user-supplied data.
  • Example: Only alphanumeric characters should be used in usernames, and only integers should be allowed in age fields.
  • Prevents attackers from injecting harmful code.

2. Use Output Escaping

  • Encode all HTML-specific characters, such as <, >, and &, before displaying user input.
  • Ensures scripts are not executed in the browser.

3. Apply Context-Specific Encoding

  • Use appropriate encoding for different scenarios:
    • JavaScript Encoding: When inserting data within JavaScript code.
    • URL Encoding: Use percent-encoding (e.g., %20 for spaces, %3C for <) to keep URLs valid and prevent script injection.

4. Practice Defense in Depth

  • Don't rely on a single layer of defense.
  • Server-side validation is crucial as client-side validation can be bypassed by attackers.
  • Combining multiple security measures reduces risk.

PHP

Vulnerable code:

// Storing user comment
$comment = $_POST['comment'];
mysqli_query($conn, "INSERT INTO comments (comment) VALUES ('$comment')");

// Displaying user comment
$result = mysqli_query($conn, "SELECT comment FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
    echo $row['comment'];
}
Enter fullscreen mode Exit fullscreen mode

Fixed code:

// Storing user comment
$comment = mysqli_real_escape_string($conn, $_POST['comment']);
mysqli_query($conn, "INSERT INTO comments (comment) VALUES ('$comment')");

// Displaying user comment
$result = mysqli_query($conn, "SELECT comment FROM comments");
while ($row = mysqli_fetch_assoc($result)) {
    $sanitizedComment = htmlspecialchars($row['comment']);
    echo $sanitizedComment;
}
Enter fullscreen mode Exit fullscreen mode

htmlspecialchars() function to ensure all special characters are converted to HTML entities.

JavaScript (Node.js)

Vulnerable code:

app.get('/comments', (req, res) => {
  let html = '<ul>';
  for (const comment of comments) {
    html += `<li>${comment}</li>`;
  }
  html += '</ul>';
  res.send(html);
});
Enter fullscreen mode Exit fullscreen mode

Fixed code:

const sanitizeHtml = require('sanitize-html');

app.get('/comments', (req, res) => {
  let html = '<ul>';
  for (const comment of comments) {
    const sanitizedComment = sanitizeHtml(comment);
    html += `<li>${sanitizedComment}</li>`;
  }
  html += '</ul>';
  res.send(html);
});
Enter fullscreen mode Exit fullscreen mode

sanitizeHTML() to remove potentially dangerous or unsafe elements such as <script> and <onload>.

Python (Flask)

Vulnerable code:

from flask import Flask, request, render_template_string
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.String, nullable=False)

@app.route('/comment', methods=['POST'])
def add_comment():
    comment_content = request.form['comment']
    comment = Comment(content=comment_content)
    db.session.add(comment)
    db.session.commit()
    return 'Comment added!'

@app.route('/comments')
def show_comments():
    comments = Comment.query.all()
    return render_template_string(''.join(['<div>' + c.content + '</div>' for c in comments]))
Enter fullscreen mode Exit fullscreen mode

Fixed code:

from flask import Flask, request, render_template_string, escape
from flask_sqlalchemy import SQLAlchemy
from markupsafe import escape

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db'
db = SQLAlchemy(app)

class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    content = db.Column(db.String, nullable=False)

@app.route('/comment', methods=['POST'])
def add_comment():
    comment_content = request.form['comment']
    comment = Comment(content=comment_content)
    db.session.add(comment)
    db.session.commit()
    return 'Comment added!'

@app.route('/comments')
def show_comments():
    comments = Comment.query.all()
    sanitized_comments = [escape(c.content) for c in comments]
    return render_template_string(''.join(['<div>' + comment + '</div>' for comment in sanitized_comments]))
Enter fullscreen mode Exit fullscreen mode

We used the escape() function to ensure that any special characters in the user-submitted comment are replaced with HTML entities. As you would expect, the characters &, <, >, ', and " are converted to HTML entities (&amp;, &lt;, &gt;, &#39;, and &quot;). We made two changes:

Although the user-submitted input request.form['comment'] is saved verbatim, the content of each saved comment c goes through the escape() function before it is sent to the user’s browser to be displayed as HTML.

C# (ASP.NET)

Vulnerable code:

public void SaveComment(string userComment)
{
    var command = new SqlCommand("INSERT INTO Comments (Comment) VALUES ('" + userComment + "')", connection);
    // Execute the command
}

public void DisplayComments()
{
    var reader = new SqlCommand("SELECT Comment FROM Comments", connection).ExecuteReader();
    while (reader.Read())
    {
        Response.Write(reader["Comment"].ToString());
    }
    // Execute the command
}
Enter fullscreen mode Exit fullscreen mode

Fixed code:

using System.Web;

public void SaveComment(string userComment)
{
    var command = new SqlCommand("INSERT INTO Comments (Comment) VALUES (@comment)", connection);
    command.Parameters.AddWithValue("@comment", userComment);
}

public void DisplayComments()
{
    var reader = new SqlCommand("SELECT Comment FROM Comments", connection).ExecuteReader();
    while (reader.Read())
    {
        var comment = reader["Comment"].ToString();
        var sanitizedComment = HttpUtility.HtmlEncode(comment);
        Response.Write(sanitizedComment);
    }
    reader.Close();
}
Enter fullscreen mode Exit fullscreen mode

Stored-XSS is fixed by using the HttpUtility.HtmlEncode().

SQL injection vulnerability is fixed by using parametrized SQL queries using the Parameters.AddWithValue() method in the SqlCommand objects.


DOM-Based XSS

Quite rare compared to Reflected and Stored XSS due to improved security.

DOM-based XSS is completely browser-based and does not need to go to the server and back to the client.

Vulnerable Web Application:

<!DOCTYPE html>
<html>
<head>
    <title>Vulnerable Page</title>
</head>
<body>
    <div id="greeting"></div>
    <script>
        const name = new URLSearchParams(window.location.search).get('name');
        document.write("Hello, " + name);
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The page above expects the user to provide their name after ?name=. In the screenshot below:

  1. The user has entered Web Tester after ?name in the URL.
  2. The greeting worked as expected and displayed “Hello, Web Tester”.
  3. Finally, the DOM structure on the right is left intact; the <body> has three direct children.

A web browser with the Inspector tab displaying the original DOM structure of an example static site.

The user might try to inject a malicious script. In the screenshot below, we see the following:

  1. The user added <script>alert("XSS")</script> instead of only Web Tester as their name.
  2. The script was executed, and an alert dialogue box was displayed.
  3. Most importantly, we can see how the DOM tree got a new element. <body> has four children now.

A web browser with the Inspector tab displaying the manipulated DOM structure of an example static site.

This basic example illustrates a couple of things:

  • The server has no direct role in DOM-based vulnerabilities. In this demonstration, everything took place on the client’s browser without using a back end.
  • The DOM was insecurely modified using document.write().

Fixed site:

<!DOCTYPE html>
<html>
<head>
    <title>Secure Page</title>
</head>
<body>
    <div id="greeting"></div>
    <script>
        const name = new URLSearchParams(window.location.search).get('name');
        // Escape the user input to prevent XSS attacks
        const escapedName = encodeURIComponent(name);
        document.getElementById("greeting").textContent = "Hello, " + escapedName;
    </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

One way to fix this page is by avoiding adding user input directly with document.write(). Instead, we first escaped the user input using encodeURIComponent() and then added it to textContent.

A web browser with the Inspector tab displaying the DOM structure of a patched static site.


Context

XSS payloads can be injected into different parts of a web page:

  • Between HTML tags: Example: <script>alert(document.cookie)</script>.
  • Within HTML attributes: Requires breaking out of the attribute, e.g., "><script>alert(document.cookie)</script>.
  • Inside JavaScript: Requires breaking out of the script, e.g., ';alert(document.cookie)//.

Understanding the context in which the payload is executed is critical for successful exploitation.


Evasion Techniques

Attackers use various techniques to bypass XSS filters:

  • Using alternate payloads: Resources like the XSS Payload List provide different payloads.
  • Bypassing length restrictions: Tiny XSS Payloads help evade payload length filters.
  • Breaking filter detection: Special characters like tabs, new lines, and carriage returns can obfuscate payloads.

Example: Breaking Up Payloads

  • Horizontal tab (TAB) is 9 in hexadecimal representation
  • New line (LF) is A in hexadecimal representation
  • Carriage return (CR) is D in hexadecimal representation

Standard payload:

<IMG SRC="javascript:alert('XSS');">
Enter fullscreen mode Exit fullscreen mode

Broke up payload:

<IMG SRC="jav&#x09;ascript:alert('XSS');">
<IMG SRC="jav&#x0A;ascript:alert('XSS');">
<IMG SRC="jav&#x0D;ascript:alert('XSS');">
Enter fullscreen mode Exit fullscreen mode

Comments 0 total

    Add comment