Поиск секретов путем декомпиляции байт-кода Python в общедоступных репозиториях

tl; dr: Кэш управляет всем, что меня окружает. pyc файлы могут содержать секреты и не должны быть возвращены в систему контроля версий. Используйте стандартный Python .gitignore.

Когда вы импортируете файл Python в первый раз, интерпретатор Python скомпилирует его и кэширует полученный байткод в файл .pyc, чтобы последующему импорту не пришлось справляться с накладными расходами, связанными с повторным разбором или компиляцией кода.

Также обычной практикой для проектов Python является хранение конфигурации, ключей и паролей (совместно называемых "секретами") в файле Python с именем типа secrets.py, config.py или settings.py, который импортируется другими частями проекта. Это обеспечивает приятное разделение секретов и исходного кода, который проверяется, и по большей части такая настройка работает хорошо. А так как он использует механизм импорта языка, то этим проектам не нужно суетиться с файлами ввода/вывода или форматами вроде JSON.

Но по той же причине, по которой этот шаблон быстрый и удобный, он также потенциально небезопасен. Поскольку он использует механизм импорта языка, который имеет привычку создавать и кэшировать .pyc файлы, эти секреты также хранятся в скомпилированном байт-коде! Некоторые первоначальные исследования с использованием API GitHub показывают, что тысячи репозиториев GitHub содержат секреты, скрытые внутри их байт-кода.

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

Рассмотрите возможность пожертвования в залоговый фонд местного сообщества.

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

В техническом сообществе мы много говорим об инклюзивности и разнообразии. Сейчас настало время предпринять конкретные действия.

https://www.communityjusticeexchange.org/nbfn-directory

Ускоренный курс по кешированному источнику

В более ранних версиях Python эти файлы хранились рядом с исходными файлами, но начиная с Python 3.2 все эти файлы находятся в папке, которая называется __pycache__корневым каталогом импортированного модуля.

Предположим, у нас есть файл Python, содержащий этот секретный пароль:

SECRET_KEY = "Green eggs and ham"

Байт-код, соответствующий этой строке кода, выглядит следующим образом:

0 LOAD_CONST               1 ('Green eggs and ham')
2 STORE_FAST               0 (SECRET_KEY)

Обратите внимание, что имя переменной и строка воспроизводятся полностью! Кроме того, оказывается, что байт-код Python часто содержит достаточно информации для восстановления исходной структуры кода. Такие инструменты, как uncompyle6, могут переводить .pyc файлы обратно в их исходные формы. *

$ uncompyle6 secrets.cpython-38.pyc

# uncompyle6 version 3.6.7
# Python bytecode 3.8 (3413)
# Decompiled from: Python 3.8.2 (default, Apr  8 2020, 14:31:25) 
# [GCC 9.3.0]
# Embedded file name: secrets.py
# Compiled at: 2020-05-12 17:16:29
# Size of source mod 2**32: 34 bytes
SECRET_KEY = 'Green eggs and ham'
# okay decompiling secrets.cpython-38.pyc

Кэширование

Чтобы выяснить, насколько широко распространена эта проблема, я написал короткий скрипт для поиска .pycфайлов в GitHub и декомпиляции их для поиска секретов. В итоге я нашел тысячи ключей Twitter, токенов Stripe, учетных данных AWS и паролей социальных сетей . Я предупредил все организации, ключи которых я нашел таким образом.

import base64
import io
import os
import tempfile
import uncompyle6
from github import Github

GITHUB_KEY = os.environ.get("GITHUB_KEY")

g = Github(GITHUB_KEY)
items = g.search_code("filename:secrets.pyc")
for item in items:
    print(f"DECOMPILING REPO https://github.com/{item.repository.full_name}")
    print(f"OWNER TYPE: {item.repository.owner.type}")
    try:
        contents = base64.b64decode(item.content)
        with tempfile.NamedTemporaryFile(suffix=".pyc") as f:
            f.write(contents)
            f.seek(0)

            out = io.StringIO()
            uncompyle6.decompile_file(f.name, out)
            out.seek(0)
            print(out.read())
    except Exception as e:
        print(e)
        print(f"COULD NOT DECOMPILE REPO https://github.com/{item.repository.full_name}")
        continue
    print("\n\n\n")

Попробуйте сами!

В этом посте есть небольшая лаборатория в стиле «поймай флаг», чтобы ты сам попробовал этот стиль атаки.

Вы можете найти его на https://github.com/veggiedefender/pyc-secret-lab/

Еда на вынос

Кэшированный байт-код - это низкоуровневая внутренняя оптимизация производительности, и именно это Python должен был освободить нас от необходимости думать! Содержимое .pycфайлов является непонятным без специальных инструментов, таких как дизассемблер или декомпилятор. И когда эти файлы скрыты внутри __pycache__( сигнал двойного подчеркивания «не использовать; только для внутреннего использования»), их легко не заметить. Многие текстовые редакторы и IDE скрывают эти папки и файлы от дерева исходных текстов, чтобы не загромождать экран, что позволяет легко забыть о том, что они вообще существуют.

То есть опытному программисту очень легко случайно передать свои секреты, и он почти гарантировал, что новичок совершит эту ошибку. Чтобы избежать этого, нужно либо стать удачливым с хорошим gitignore, либо получить промежуточные знания о внутренностях git и Python.

Актуальные предметы, которые вы можете сделать:

  • Просмотрите ваши репозитории на наличие потерянных .pycфайлов и удалите их
  • Если у вас есть .pycфайлы и они содержат секреты, то отзовите и поверните свои секреты
  • Используйте стандартный gitignore для предотвращения проверки .pycфайлов
  • Используйте файлы JSON или переменные среды для конфигурации