OneDrive: an easter egg into MS library - XSS on Microsoft and not only
- 6 minsWebsite, web services or web apps continuously evolves to add new features for customers/users. That’s why a good hunter comes back and restart searching. That’s what happened when I noticed a new features on Vimeo (maybe it’s and old one for you but I’m not Vimeo addicted so it was the first time I saw it).
The feature
It’s possible to upload video from Google Drive, Dropbox, OneDrive and Box. I knew how Google Drive and Dropbox work so I decided to focus on OneDrive first and then Box. Vimeo’s dev self-hosted a so called OneDrive.min.js library subjected to a XSS attack. Written a poc, submitted the bug to Vimeo and then quitted.
Something I missed
Have you ever had that feeling? That’s why I reopened my laptop once back and googled these words: onedrive, microsoft, js. As first result I got this page:
https://dev.onedrive.com/sdk/javascript-picker-saver.htm
It was late, as I said, and I was a bit tired so I didn’t noticed that Microsoft hosts different version of OneDrive library so I picked up the v7, and after few changes to vimeo poc the famous alert(document.domain) was executed on dev.onedrive.com.
How does it works?
And now, the interesting stuffs:
e.handleRedirect = function() {
var t = p.readCurrentUrlParameters()
, r = f.getWindowState()
, n = t[i.PARAM_STATE];
if (!n)
return null ;
a.logMessage("current state: " + n);
n !== i.STATE_OPEN_POPUP || r.options || (r = JSON.parse(t[i.PARAM_SDK_STATE]));
var s = r.options
, d = r.optionsMode
, h = c[d];
s || o.throwError("missing options from serialized state");
if (t[i.PARAM_ERROR] === i.ERROR_LOGIN_REQUIRED || t[i.PARAM_ERROR] === i.ERROR_AUTH_REQUIRED) {
l.deleteLoghinHint(s.clientId);
l.loginHint = null ;
e.redirectToAADLogin(s, r, !0);
return null
}
l.getLoginHint(s.clientId);
var g = u.validateType(s.openInNewWindow, i.TYPE_BOOLEAN);
g && e._displayOverlay();
s.advanced && s.advanced.sharePointTenantPersonalUrl || o.throwError("advanced options is missing");
switch (n) {
case i.STATE_OPEN_POPUP:
e.redirectToAADLogin(s, r);
break;
case i.STATE_AAD_LOGIN:
e._handleAADLogin(t, s, h);
break;
case i.STATE_MSA_PICKER:
case i.STATE_AAD_PICKER:
var _ = {
windowState: r,
queryParameters: t
};
a.logMessage("sending invoker response");
if (!g)
return _;
e._sendResponse(_);
break;
default:
o.throwError("invalid value for redirect state: " + n)
}
return null
}
Once initialized, the code get params from GET query calling the readCurrentUrlParameters method and parse the content of window.name calling getWindowState:
e.getWindowState = function() {
return n.deserializeJSON(window.name || "{}")
}
We can control both GET params and window.name so we can pass to next step and see what the code does:
n = t[i.PARAM_STATE];
...
switch (n) {
...
}
The n param control the switch jump and there’s an interesting case (STATE_AAD_LOGIN).
case i.STATE_AAD_LOGIN:
e._handleAADLogin(t, s, h);
e._handleAADLogin = function(t, r, n) {
r.openInNewWindow || e._displayOverlay();
r.advanced.accessToken = t[i.PARAM_ACCESS_TOKEN];
if (r.sharePointTenantPersonalUrl)
e._redirectToTenant(r, n, r.openInNewWindow);
...
}
e._redirectToTenant = function(t, r, n) {
var a = function(i) {
...
e.redirect(i, {
picker: {
accessLevel: n,
selectionMode: a,
viewType: o,
filter: t.advanced.filter
},
options: t,
ODBParams: {
p: "2"
}
})
};
if (t.sharePointTenantPersonalUrl) {
var l = t.advanced.sharePointTenantPersonalUrl;
a(l)
} else {
...
}
}
e.redirect = function(e, t, r) {
void 0 === t && (t = null );
void 0 === r && (r = null );
t && f.setWindowState(t, r);
window.location.replace(e)
}
handleAADLogin call _redirectToTenant that call a(l) which falls into redirect method. Wow! There’s no check before _window.location.replace(e). We can trig a XSS with a simple payload.
All in one(drive)
And here you’re a simple poc:
Easter egg
While writing this post I noticed that Microsoft hosts other two OneDrive libraries (v5 and v6). After some checks I reported another XSS in v6 and discovered that v5 (the oldest one) is not affected.