В закладки

Что же на этот раз?

На этот раз уязвимость была найдена уже в другом методе небезызвестного API «ВКонтакте», под названием . Суть уязвимости заключается в следующем: если сообщество сделало репост чужой записи, то получая список репостов исходной записи, можно засветить того, кто сделал этот репост. Вроде бы всё верно, метод для того и создан. Но если репост был сделан в сообщество или группу, то засветится id того, кто этот репост сделал.

Как использовать?

Для теста было создано 2 группы:
№1 -
№2 -

На стене группы №2 была создана запись, которая репостнулась в группу №1, естественно репост произведён от имени группы и без подписи администратора. Далее следует посетить страницу знакомого вам человека, дабы не спутать результаты, если вы проводите эксперимент на своих сообществах, если же на других, то можно остаться в своём профиле и перейти на страницу с описанием метода . Внизу этой странице есть форма для тестирования работы метода. Она нам и понадобится.

В поле owner_id указываем -id группы в которой был изначальный пост, в случае с примером выше -120708468 .
В поле post_id указываем id самого поста, в данном случае 1 .
И смотрим на результат работы.

Результат работы метода

Здесь нас интересует объект profiles . В нём мы видим сообщество из которого сделали репост, сообщество в которое сделали репост, профиль с которого отправлялся запрос (Мирослава Покровская) и очень странно, но мы видим профиль, который сделал этот самый репост (Иван Зимин).

И что же делать?

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

Обращение в техподдержку

P. S.

Эта статья появилась тут не просто так. В истории с Алексеем Злодеевым было много вопросов по типу «А почему не обратиться в техподдержку», «Почему не обратиться на HackerOne». Дело в том, что если объективно смотреть на ситуацию, то пока разместишь репорт на HackerOne, сойдёшь с ума. Поэтому я выбрал вариант написать в техподдержку.

Прошло 14 дней (две недели), а исправлений уязвимости так и не последовало. ИМХО, 14 дней игнорировать действующий баг, хоть и незначительный - не уровень социальной сети такого масштаба. Пост висит на «Хабре», и им пользуются люди, доказательством чего является сегодняшнее сообщение, которое я получил во «ВКонтакте». В нём шла речь о найденном администраторе анонимного сообщества с помощью этого маленького бага.

Недавно я решил подготовить очередную презентацию, посвященную web-уязвимостям, в частности, XSS-атакам. Для этого мне нужно было изучить некоторые особенности современных систем фильтрации.

В качестве целевого сайта для тестирования я выбрал самый посещаемый сайт Рунета – vkontakte.ru. Мое внимание привлекла обновленная система статусов.

Код HTML-странички в месте, где происходит редактирование статуса, выглядит следующим образом:

Как видно, фильтр расположен непосредственно в функции infoCheck(). Сам же статус располагается в этой строке:

В данном случае имеем двухступенчатую фильтрацию. Первая ступень – непосредственно фильтрация у пользователя при вводе статуса. Вторая – преобразование введенного статуса в текст и возвращение его на страницу в том виде, в котором статус увидят другие пользователи.

В то время как вторая ступень работает безусловно хорошо, и превратить введенное пользователем в активную XSS явно не удалось бы, с первой не все так гладко. Именно о первой ступени и пойдет речь.

Как и предполагалось, простой

alert()

не сработал, статус остался пустым. Вариации на «околоscript-ные» темы тоже не прошли – судя по всему, конкретно эта последовательность фильтруется явным образом.

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

Первая уязвимость на пользовательской машине достигается использованием тега

: введя в статус строку

Мы добьемся выполнения этой самой функции. Для наглядности можно вызвать функцию profile.infoSave(), вызываемую с пустым аргументом при очистке статуса, с нашим аргументом. Так, введя

Получаем в статусе строчку “XSS”:

Вторая забавная уязвимость фильтра – в отсутствии фильтрования тега

Вводим в статус

XSS

Как мы помним, XSS = cross site scripting, поэтому в следующей уязвимости я решил использовать сторонний сайт с загруженным туда скриптом. Помимо отсутствия фильтрации вышеуказанных тегов, проходит фильтр и тег

Таким образом, введя в статусную строку

Получаем iframe с запуском того самого загруженного скрипта. Пример такого “айфрейма”:

Эта уязвимость является более серьезной, чем две предыдущие. Один из способов эксплуатации – составление URL для изменения статуса пользователя и последующий клик пользователя по этому URL жертвой. Еще до того, как статус будет опубликован, скрипт успеет выполниться на странице пользователя. Таким образом, получаем классическую пассивную XSS.

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 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 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] 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}&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)