Восстановление удалённых коммитов git: как reflog и fsck вытаскивают секреты из «стёртой» истории

По данным IBM X-Force Threat Intelligence Index 2025, число атак с использованием действительных учётных данных выросло на 71% за год. Заметная часть этих credential’ов — API-ключи, пароли баз данных, сервисные токены — попала к атакующим не из текущих файлов, а из «удалённых» коммитов git-репозиториев. На одном из аудитов я восстановил из dangling-коммита AWS-ключ, который команда «почистила» три месяца назад через git reset --hard и force-push. Ключ работал. Доступ к боевой инфраструктуре — тоже.
Ниже на игрушечном репозитории пройдём полный цикл: закоммитим секрет, «удалим» его, восстановим через git reflog и [git fsck](https://git-scm.com/docs/git-fsck). А потом разберём, какие процессы в команде не дают таким утечкам случаться.
Место в цепочке атаки: от репозитория до инфраструктуры
Секреты в git-истории — один из самых коротких путей от разведки до полного доступа к инфраструктуре. Не теоретическая угроза, а рабочий вектор. MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1552.001 — её идентификаторы) описывает эту цепочку тремя шагами:
-
Разведка (Reconnaissance). Атакующий находит репозитории компании на GitHub, GitLab, Bitbucket — техника T1593.003 Code Repositories. Хватает Google-дорков
site:github.com "company"или OSINT-инструментов по репозиторию. -
Поиск учётных данных (Credential Access). В клонированном репозитории атакующий ищет пароли, ключи и токены не в текущих файлах (их обычно уже убрали), а в истории коммитов, включая «удалённые» — техника T1552.001 Credentials In Files.
-
Первоначальный доступ (Initial Access). Найденные credential’ы дают вход в инфраструктуру — T1078 Valid Accounts. AWS-ключ из git-истории открывает S3-бакеты и RDS; токен GitHub — приватные репозитории и CI/CD-пайплайны.
Контекст применения: техника работает при внешнем пентесте (публичные репозитории, хватит git clone) и при внутреннем аудите (приватные репозитории, к которым есть доступ). Не зависит от ОС — git кроссплатформенный. Предшествующий шаг — разведка и получение доступа к репозиторию; следующий — эксплуатация найденных credential’ов в целевой инфраструктуре.
Что происходит внутри .git при коммите и reset
Чтобы понять, почему git reset --hard ничего не удаляет, хватит одного принципа. Он проще, чем кажется.
Каждый файл, директория и коммит в git — объект с уникальным SHA-1 хешем (строка вроде f1e2d3c...). Все объекты лежат в папке .git/objects внутри репозитория. Типов три:
- blob — содержимое одного файла. Как файл на жёстком диске.
- tree — список файлов в директории. Как папка, внутри которой ссылки на blob’ы.
- commit — запись «кто, когда, что изменил» + ссылка на tree (полный снимок проекта в этот момент) + ссылка на предыдущий коммит.
Ключевой принцип: git не изменяет и не удаляет объекты при обычных операциях. Команда git reset --hard не стирает коммиты — она двигает указатель ветки на другой коммит. Старые объекты остаются в .git/objects, просто на них больше ничего не ссылается.
Такие объекты называются dangling (висячие) или unreachable (недостижимые). Они занимают место на диске, пока сборщик мусора git gc (garbage collector — автоматическая очистка) их не уберёт. По умолчанию gc срабатывает не раньше чем через 30 дней для dangling-объектов, а записи в reflog живут 90 дней. Проверить настройки: git config --get gc.pruneExpire и git config --get gc.reflogExpire.
Аналогия проще некуда: вы удалили ярлык с рабочего стола, но сам файл лежит на диске. Пока не очистите корзину — файл доступен. В git роль корзины — .git/objects, роль очистки — git gc --prune=now.
Практика: коммитим секрет, «удаляем», восстанавливаем
Требования к окружению
- ОС: любая (Linux, macOS, Windows) с git 2.20+. Проверка:
git --version - RAM / CPU: минимальные, все операции локальные
- Права root: не нужны
- Сеть: не нужна — всё офлайн
- Дополнительный софт: для базового цикла не нужен; для автоматического сканирования понадобятся trufflehog или gitleaks (описание ниже)
Откройте терминал и создайте тестовый репозиторий с тремя коммитами. Во втором «случайно» окажется пароль к базе данных:
mkdir secret-demo && cd secret-demo && git init
echo "app_version=1.0" > config.txt
git add . && git commit -m "Initial config"
echo "DB_PASSWORD=SuperSecret123!" >> config.txt
git add . && git commit -m "Add database config"
echo "logging=true" >> config.txt
git add . && git commit -m "Enable logging"
git reset --hard HEAD~2
Что произошло: первые шесть строк создали репозиторий с тремя коммитами. Последняя — git reset --hard HEAD~2 — откатила ветку на два коммита назад, к «Initial config». Теперь git log --oneline покажет только один коммит. Коммиты с паролем и логированием «исчезли» из видимой истории. Разработчик уверен — секрет удалён.
Восстанавливаем «удалённое» двумя способами — через reflog и fsck:
git reflog
# b0a1b2c HEAD@{0}: reset: moving to HEAD~2
# a3b4c5d HEAD@{1}: commit: Enable logging
# f1e2d3c HEAD@{2}: commit: Add database config
git show f1e2d3c | grep PASSWORD
# +DB_PASSWORD=SuperSecret123!
git reflog expire --expire=now --all
git fsck --unreachable --no-reflog | grep commit
# unreachable commit f1e2d3c...
Разберём по строкам. git reflog показал журнал перемещений HEAD — каждый коммит, reset и checkout записываются сюда автоматически. Строка f1e2d3c HEAD@{2}: commit: Add database config — тот самый «удалённый» коммит с паролем. Хеши у вас будут другие — SHA-1 уникален для каждого репозитория, это нормально. Команда git show f1e2d3c вывела содержимое коммита, включая DB_PASSWORD=SuperSecret123!. Пароль на месте.
Для восстановления достаточно git branch recovered f1e2d3c — создаст ветку, указывающую на «удалённый» коммит. Или git cherry-pick f1e2d3c — перенесёт изменения в текущую ветку.
Дальше смоделировали ситуацию без reflog: git reflog expire --expire=now --all очистил журнал. Так бывает на клонированных репозиториях (reflog не передаётся при git clone) или после истечения 90-дневного срока. Команда git fsck --unreachable --no-reflog проверила объектную базу и нашла коммиты, на которые не ссылается ни одна ветка. Каждый unreachable commit можно просмотреть через git show <hash>. Нашли нужный — создаём ветку: git branch recovered-fsck f1e2d3c.
Итог: ни git reset --hard, ни удаление ветки, ни очистка reflog не уничтожают объекты из .git/objects. Пока не отработал git gc --prune=now, данные физически на диске и доступны для восстановления.
git reflog vs git fsck: что и когда применять
Два инструмента решают одну задачу — поиск удалённых данных в git — но в разных условиях:
| Критерий | git reflog | git fsck —unreachable |
|---|---|---|
| Что показывает | Журнал перемещений HEAD с временными метками | Все объекты без ссылок (dangling commits, blob’ы) |
| Доступен после clone | Нет — reflog строго локальный | Да, если объекты остались в .git/objects |
| Срок хранения данных | 90 дней по умолчанию (настраивается через gc.reflogExpire) | До срабатывания gc (~2-4 недели для unreachable) |
| Когда использовать | Свой репозиторий, недавний инцидент | Клонированный репозиторий, истёкший reflog |
| Когда не работает | Клонированный чужой репозиторий | После git gc --prune=now (объекты физически удалены) |
| Удобство поиска | Высокое — видны сообщения коммитов и хронология | Низкое — приходится вручную просматривать каждый объект |
На практике: начинайте с git reflog. Reflog пуст — переходите к git fsck --unreachable --no-reflog. Если и fsck ничего не нашёл — объекты уже удалены сборщиком мусора, восстановление средствами git невозможно.
Автоматический поиск секретов в истории git-репозитория
Ручной просмотр коммитов через git show работает на игрушечном репозитории, но не масштабируется. На реальном проекте с тысячами коммитов нужны сканеры, которые проходят всю историю и ищут паттерны секретов — API-ключи, пароли, токены.
trufflehog — сканер секретов с двумя режимами детекции: regex-паттерны (ищет строки формата AWS-ключей, GitHub-токенов) и entropy-анализ (чем «случайнее» строка, тем выше вероятность, что это ключ). Запуск по локальному репозиторию: trufflehog git file://./secret-demo. Инструмент выведет найденные секреты с указанием коммита, файла и строки.
gitleaks — аналогичный инструмент, часто быстрее на больших репозиториях, правила настраиваются через TOML-файл. Запуск: gitleaks detect --source ./secret-demo --verbose.
Оба сканера находят секреты не только в текущих файлах, но и в полной git-истории. Для пентестера — способ быстро просканировать клонированный репозиторий на этапе T1552.001 (поиск credential’ов). Для защитника — обязательный шаг в CI/CD-пайплайне.
| Возможности | Ограничения |
|---|---|
| API-ключи AWS, GCP, Azure по формату | Произвольные пароли без характерного формата |
| Токены GitHub, Slack, Stripe по regex | Объекты, удалённые git gc --prune=now |
| Пароли в конфигах по контексту (password=, secret=) | Секреты в зашифрованных файлах |
| Entropy-анализ нестандартных форматов | Ложные срабатывания на тестовых данных |
| Вся git-история, включая удалённые ветки | Репозитории с shallow clone (неполная история) |
Force-push и GitHub: почему «удалённые» коммиты остаются доступными
Локальный git reset + git push --force — стандартный способ «стереть» коммит из публичного репозитория. Но GitHub и другие хостинги не удаляют force-pushed коммиты из своей инфраструктуры.
Исследование компании Neodyme (блог «Hidden GitHub Commits and How to Reveal Them») показало: force-pushed коммиты остаются доступными по прямому URL, если известен SHA-1 хеш коммита. Хеш можно извлечь через GitHub Events API — эндпоинт PushEvents хранит информацию обо всех push-операциях, включая перезаписанные force-push’ом. Работает даже для репозиториев, которые были приватными в момент push’а и стали публичными позже — Events API сохраняет историю.
Neodyme создали инструмент github-secrets для поиска таких скрытых коммитов в публичных репозиториях. Удалить force-pushed коммиты с GitHub может только GitHub Support — обычные git-команды здесь бессильны.
Вывод жёсткий: любой секрет, попавший в push на GitHub, нужно считать скомпрометированным и ротировать немедленно. Не «когда руки дойдут», а сразу. Очистка истории — гигиена, не замена ротации.
Очистка чувствительных данных из git и предотвращение утечек
Восстановление секретов — половина задачи. Вторая — очистка истории после инцидента и предотвращение повторных утечек.
Очистка истории: BFG Repo-Cleaner. Стандартный git filter-branch переписывает историю, но работает медленно и неудобно — я ни разу не видел, чтобы его использовали без боли. BFG Repo-Cleaner — специализированный инструмент для массового удаления файлов и текстовых паттернов из всей git-истории. Типичный сценарий: bfg --replace-text passwords.txt repo.git, где passwords.txt содержит строки для замены на ***REMOVED***. После BFG нужно выполнить git reflog expire --expire=now --all && git gc --prune=now --aggressive для физического удаления старых объектов. Затем git push --force, и все участники команды должны заново клонировать репозиторий. Если кто-то выполнит git pull из старого клона — старые объекты вернутся в remote. Это ловушка, в которую попадают почти все.
Предотвращение: pre-commit хуки. Pre-commit hook — скрипт, который git запускает перед каждым коммитом. Если скрипт возвращает ненулевой код — коммит блокируется. Для предотвращения утечек хук запускает gitleaks на staged-файлах и отклоняет коммит при обнаружении паттерна секрета. Установка через фреймворк pre-commit: создать в корне репозитория файл .pre-commit-config.yaml:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
После создания файла выполнить pre-commit install — хук будет запускаться при каждом git commit. Для проверки попробуйте закоммитить файл со строкой AWS_SECRET_ACCESS_KEY=AKIA... — коммит должен быть отклонён с сообщением gitleaks о найденном секрете.
Ограничение, о котором забывают: pre-commit хуки работают только локально. Разработчик может обойти их через git commit --no-verify. Для надёжной защиты нужен серверный контроль — сканирование в CI/CD (GitHub Actions, GitLab CI), которое блокирует merge PR с секретами. Хук на машине разработчика ловит ошибки рано, CI/CD — ловит то, что прошло мимо хука.
Чеклист аудита безопасности git-репозитория
Готовый список действий — можно передать сисадмину или включить в корпоративную политику:
- Установить pre-commit хук с gitleaks на рабочие станции всех разработчиков
- Добавить gitleaks или trufflehog в CI/CD — сканирование при каждом push и pull request
- Настроить
.gitignoreдля файлов с секретами:.env,*.pem,credentials.json,*.key - Вынести секреты в менеджер (HashiCorp Vault, AWS Secrets Manager, переменные окружения CI/CD) — не хранить в коде
- Регулярно сканировать существующие репозитории полным прогоном trufflehog по всей истории:
trufflehog git file://./<repo> - Ротировать скомпрометированные ключи немедленно после обнаружения, не дожидаясь очистки истории — force-push не стирает данные с GitHub
- Очищать историю через BFG после инцидента, затем
git gc --prune=now --aggressive, force-push и переклонирование всеми участниками - Ограничить force-push на shared-ветки через protected branches в GitHub/GitLab — снижает хаос при инцидентах
- Проверить настройки gc:
git config --get gc.pruneExpire— по умолчанию 2 недели; полагаться на gc как на механизм защиты нельзя - Провести разовый аудит всех репозиториев организации с документированием найденных секретов и статуса их ротации
Большинство инцидентов с утечкой секретов через git заканчиваются не ротацией ключей, а закрытием тикета со статусом «почищено». Разработчик делает force-push, DevOps бегло проверяет git log, все расходятся. Через полгода пентестер или атакующий находит dangling-коммит через fsck или GitHub Events API — и ключ, который «почистили», открывает боевую инфраструктуру.
Проблема не в git. Git честно делает то, для чего создан — хранит историю изменений, включая ошибки. Проблема в ожидании, что git reset работает как shred, физически уничтожая данные. Это ожидание живёт даже у опытных разработчиков с годами стажа.
Единственный надёжный подход: любой секрет, попавший в коммит, считать скомпрометированным с момента push’а. Не «после того как кто-то увидит» — с момента push’а. Ротировать немедленно, очищать историю через BFG, прогонять gitleaks по репозиторию, ставить pre-commit хуки. И объяснять это команде не один раз на вводном тренинге, а при каждом инциденте — показывая на живом примере, как за минуту восстанавливается «удалённый» коммит. IB Basics — для тех, кто перешёл из IT в ИБ и нужна структура за пару месяцев.
Эту тему и смежные навыки разбирают на практике в курсе «Git. Система контроля версий» Codeby Academy.