Как я нашел хранимую XSS в поиске Яндекса

Как я нашел хранимую XSS в поиске Яндекса

2019, Aug 23    

Денег нет, но мы держимся, как завешал нам Дмитрий Анатольевич.

Одним весенним утром (если быть точным, то в апреле) я открыл твиттер и увидел твит одного вайт хэкера который хвастался обнаруженной XSS в поисковой выдаче Google.

На тот момент я был твердо уверен что все XSS на главной гугла уже давно обнаружены, тем более ЭТО ЖЕ GOOGLE. Они платят огромные деньги исследователям безопасности, там ежедневно тысячи пентестеров долбят свои кавычки в поисковую форму.

Скинул линк товарищу, говорю: «Гля че могут»‎.
Обсудили XSS’ки, Bug Bounty и прочие прелести жизни секурити специалистов и решили попробовать поломать Яндекс.

Буквально через 10-15 минут обнаружилось странное поведение поисковой выдачи при включенном в браузере расширении AdBlock (блокировщик рекламы) или любом другом аналогичном плагине. В случае если элементы с классами .b-adv и .b-adv-art не загружались, то Яндекс считал что клиент юзает блокировщик и вешал на браузер куку bltsr=1 (Block Teaser?).

В случае если в браузере имеется эта кука, все JS-скрипты на Яндексе оборачивались в какую-то магическую функцию.

eval(decodeURIComponent(QQ5K(atob('...'),'9a8e28')));function QQ5K(data,key){var result=[];for(var i=0;i<data.length;i++){var xored=data.charCodeAt(i)^key.charCodeAt(i%key.length);result.push(String.fromCharCode(xored));}return result.join('');};var cs=document.currentScript;cs&&cs.id!=='butterfly'&&cs.parentElement&&cs.parentElement.removeChild(cs);

т.е. бекенд парсил хтмл код, находил все теги <script> и заменял их на это. Пробуем передать в поисковую строку

<script>alert(/xss/)</script>

Ой, что это у нас? Так просто?

XSS в поиске Яндекса

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

eval(decodeURIComponent(QQ5K(atob('...'), '9a8e28')));
function QQ5K(data, key) {
    var result = [];
    for (var i = 0; i < data.length; i++) {
        var xored = data.charCodeAt(i) ^ key.charCodeAt(i % key.length);
        result.push(String.fromCharCode(xored));
    }
    return result.join('');
};
var cs = document.currentScript;
cs && cs.id !== 'butterfly' && cs.parentElement && cs.parentElement.removeChild(cs);

Функция QQ5K(), принимает два параметра - data и key. При этом key всегда равен ‘9a8e28’.
data - это base64 строка. Например, в случае с алертом это было: WA1dF0YdC1kXHUFLFkQKXA==

(Судя по всему key должен был динамически меняться, но Яндекс почему-то выкатили в прод сырую версию).

В base64 лежат бинарные данные, поэтому декодируя эту строку мы будем писать вывод в файл.

echo WA1dF0YdC1kXHUFLFkQKXA== | base64 --decode > output.txt

Преобразуем через xxd файл в hex формат. Ровно 16 байт
Ровно 16 байт.

Вернемся к нашему коду, посмотрим что происходит в цикле, количество иттераций которого, кстати, равно длине data.
В нашем случае это будет 16 иттераций.

var xored = data.charCodeAt(i) ^ key.charCodeAt(i % key.length);

Функция charCodeAt() возвращает Unicode символа по указанному индексу в строке. На этом этапе я воспользуюсь возможностями языка Python и напишу простой скрипт, который прочитает файл посимвольно и выведет Unicode значение каждого char’а.

f = open('output.txt', 'r')
for char in f.read():
    print(ord(char))

Unicode значения

Разберем первую иттерацию цикла. Что же будет записано в переменную xored?

var xored = 88 ^ key.charCodeAt(i % key.length);

key.length как нам известно всегда равен 6. В первой иттерации i = 0. Следовательно: 0 % 6 = 0 Все помнят как считать остаток от деления? Каждую иттерацию цикла значения будут следующими (я уже заранее просчитал Unicode для символов и написал их в комментариях):

key.charCodeAt(0) // 57
key.charCodeAt(1) // 97
key.charCodeAt(2) // 56
key.charCodeAt(3) // 101
key.charCodeAt(4) // 50
key.charCodeAt(5) // 56
key.charCodeAt(0) // 57
key.charCodeAt(1) // 97
key.charCodeAt(2) // 56

…и так далее

Итак, в первой иттерации у нас

var xored = 88 ^ 57;

Тут у нас происходит бинарная операция, побитовое ИЛИ. Не буду вдаваться в технические подробности, просто считаем.

var xored = 97;

Далее, это значение преобразуется в символ и записывается в массив result.

String.fromCharCode(97) = "a"

Что соответствует первому символу нашего alert(/xss/).
Резюмируя скажу - данная функция предназначена для того чтобы обфусцировать JS код и сделать его нечитаемым. Судя по всему, это была не совсем удачная попытка Яндекса в борьбе с блокировщиками рекламы.

Штош
Штош, путем никому нафиг ненужного реверс инжиниринга можно предположить какой код работает на бекенде Яндекса, накидал реализацию на JS.

var input = document.body.children[0];
input.oninput = function() {
    var s = encodeURIComponent(input.value);
    var key = '9a8e28';
    var result = [];
    var bstr = '';
    for (var i = 0; i < s.length; i++) {
        var xored = s.charCodeAt(i) ^ key.charCodeAt(i % key.length);
        bstr += String.fromCharCode(xored);
    }
	document.getElementById('result').innerHTML = btoa(bstr);
};

https://jsfiddle.net/8bLcwrea/

А теперь перейдем к самой интересной части поста. В этот же день (еще раз напомню, это был апрель 2019 года) я зарепортил об обнаруженной уязвимости. Которая, кстати, позволяет угнать куки, аккаунт и вообще натворить много плохих вещей.

Demo

Google за подобного рода уязвимости платит несколько тысяч долларов. В этот же день, я уже начал выбирать себе новую модель iPhone.
Ответа не было довольно долго, спустя неделю я получаю письмо следующего содержания:

Поздравляем, за уязвимость, о которой вы сообщили, вам присужден приз — 17 000 рублей.

Ееее, целых ~250$. Пожалуй, куплю на них…а нет, ничего я на них не смогу купить. Ну ладно, по поводу суммы на самом деле у меня нет никаких претензий к Яндексу, как выяснилось все эти суммы у них прописанны в положении о публичном конкурсе “Охота за ошибками”. Так же в этом положение есть интересный пункт:

9.3. Выплата Приза осуществляется Организатором не позднее 3 (трех) месяцев со дня сообщения о результатах оценки найденной уязвимости...

Который был успешно нарушен Яндексом и на текущий момент (конец августа 2019) денег я так и не получил. С момента сообщения о вознаграждении прошло уже более 4-х месяцев.
Информация для сотрудников Яндекс, если они меня будут читать: Номер тикета # 19041818575468289

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

Всем пис энд лав.

XSS в поиске Яндекса