Спойлер: soundtrack
Окей, давайте разберёмся как GitHub устроен под капотом, ибо без этого всё дальнейшее будет выглядеть как какая-то чёрная магия. Когда вы форкаете репозиторий на гитхабе, он не копирует все данные куда-то отдельно. Ну сами подумайте, 12 тысяч форков openai-cookbook, каждый весит допустим 500мб, это 7.5 терабайт одного и того же кода, какой дурак будет так делать. По этому GitHub использует fork network, все форки и оригинал делят между собой одно общее хранилище объектов (object store). Коммиты, блобы, деревья, всё в одной куче. И вот тут начинается самое весёлое. Когда кто-то делает коммит в своём форке, коммит падает в общее хранилище. А когда этот кто-то удаляет свой форк, репозиторий да, удаляется, ссылки удаляются, но сам коммит из хранилища? Хрен там. Он становится осиротевшим. И гитхаб его не чистит. Не через день, не через месяц, не через год. Никогда, блин. Ибо сборщик мусора (git gc) работает на уровне всей fork-сети, а не отдельного репозитория. А когда в сети тысячи форков, каждый из которых теоретически может ссылаться на любой объект, ну, проще вообще нихера не трогать, чем случайно кому-то репу сломать. Логика железная, да? Да. Кароче, итого: юзер форкнул репу, запушил коммит с секретом, удалил форк. Секрет всё ещё доступен через оригинальную репу. По прямой ссылке. Без авторизации. Без каких-либо прав. Просто зная хэш коммита. И кстати, работает это в обе стороны, из форка можно вытащить коммиты приватного оригинала, если форк был сделан до того как репу закрыли. Такие дела. Ладно, погнали к практике, а то я сам уже от теории засыпаю.
Для начала нужен GitHub токен с правами на repo. Идём в Settings ~> Developer settings ~> Personal access tokens ~> Tokens (classic), генерим токен, ставим галку на repo, всё, ничего сложного. И тут момент, который меня убил. Скрипт для эксплуатации CFOR лежит... на самом гитхабе. Прямо вот тут. То есть, подождите, гитхаб хостит у себя инструмент, который эксплуатирует его же собственную архитектурную "фичу"? И не банит? Не удаляет? А, ну да, формально это не эксплойт, это просто обращение к API по документации. Ну гитхаб, ну молодцы, чё сказать. Скрипт написан просто, давайте разберём чё он делает. Первым делом лезет в REST API гитхаба и собирает все известные коммиты репозитория, те что и так видны в публичной истории, их перебирать смысла нет. Дальше генерит все возможные 4-символьные hex-комбинации, это 65 536 штук, и выкидывает те что совпадают с началом уже известных коммитов, чтоб не тратить запросы впустую. Потом пакует оставшееся в батчи по 50 и шлёт через GraphQL, в одном запросе проверяя сразу пачку коротких SHA. Если гитхаб на какую-то комбинацию возвращает полный 40-символьный хэш, значит в хранилище есть коммит которого нет в публичной истории, то есть он из чьего-то форка, возможно уже удалённого. Ну и последний этап, скачивает .diff каждого найденного коммита и складывает в папку, чтоб потом прогрепать на секреты. Запускаем и... оно просто стоит и сыпет ссылками. Сыпет, сыпет, сыпет.
import os
import re
DIFF_DIR = "путь где лежат диффы"
patterns = {
"OpenAI API Key": r"sk-[a-zA-Z0-9]{20,}",
"OpenAI Project Key": r"sk-proj-[a-zA-Z0-9_-]{20,}",
"GitHub Token": r"gh[pousr]_[a-zA-Z0-9]{36,}",
"AWS Access Key": r"AKIA[0-9A-Z]{16}",
"Telegram Bot Token": r"[0-9]{8,10}:[a-zA-Z0-9_-]{35}",
"Generic Secret": r"(?i)(?:api_key|apikey|secret_key|access_token|auth_token)\s*[=:]\s*['\"]([a-zA-Z0-9_\-]{16,})['\"]",
}
found_secrets = []
files = [f for f in os.listdir(DIFF_DIR) if f.endswith((".diff", ".patch"))]
for filename in files:
filepath = os.path.join(DIFF_DIR, filename)
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
except Exception:
continue
commit_hash = filename.replace(".diff", "").replace(".patch", "")
for secret_type, pattern in patterns.items():
matches = re.findall(pattern, content)
for match in matches:
entry = {
"type": secret_type,
"value": match,
"commit": commit_hash,
"url": f"https://github.com/openai/openai-cookbook/commit/{commit_hash}",
}
found_secrets.append(entry)
seen = set()
unique = []
for s in found_secrets:
if s["value"] not in seen:
seen.add(s["value"])
unique.append(s)
with open("secrets.txt", "w", encoding="utf-8") as f:
for s in unique:
f.write(f"[{s['type']}] {s['value']}\n")
f.write(f" {s['url']}\n\n")
Select-String -Path "путь\к\папке_с_диффами\*" -Pattern "sk-","AKIA","password","secret","token"Грубо, тупо, зато быстро, и глазами видно где что лежит.
Ну и по результатам, держитесь. Из одного репозитория openai-cookbook мы достали реальные OpenAI API ключи в формате sk-, AWS Access Key в формате AKIA, ключи от Pinecone, Stripe тестовые ключи, какие-то UUID которые явно были чьими-то секретами. Причём между плейсхолдерами типа sk-xxxxxxxx и YOUR_OPENAI_API_KEY лежат вполне себе боевые ключи, которые люди воткнули в свой форк, поигрались с примерами из кукбука, потом снесли форк к чертям думая что всё, чисто, можно спать спокойно. Ага, щас. Оно вот, через оригинальную репу OpenAI доступно кому угодно. И это, напоминаю, один репозиторий. Один. Представьте масштаб если натравить на tensorflow с 75к форков, или на kubernetes с 43к, или на first-contributions где одни новички которые слово .gitignore впервые в жизни видят. Там вообще Клондайк будет, я уверен.
LIMB