Восстановление коммита git reflog: как я спас эксплоит перед сдачей отчёта

Половина третьего ночи, через пять часов — сдача отчёта клиенту. В tmux открыто шесть панелей: Burp Suite, два терминала с оболочками, консоль базы данных, браузер с Jira и репозиторий с PoC-кодом. Набираю git reset --hard HEAD~3, целясь в ветку notes с черновиками — и не замечаю, что активная панель tmux стоит на exploit-sqli. Три часа работы над UNION-based payload’ом исчезают из git log. На экране — чистый коммит двухдневной давности. Паника длилась десять секунд — пока не вспомнил про git reflog. Восстановление заняло 30 секунд и две команды. Ниже — как это работает, зачем пентестеру ветки и теги, и как не оказаться в той же ситуации.
Зачем пентестеру git branch и теги для версионирования эксплоитов
Git branch для пентестера — не абстрактная «хорошая практика из мира разработки», а способ не смешать код разных целей в одну кучу. На типичном engagement (проект по тестированию на проникновение) работа идёт параллельно: SQLi-цепочка для веб-приложения, кастомный скрипт для перебора API-ключей, шаблон отчёта с доказательствами. Если всё лежит в одной ветке main, одна неаккуратная команда ломает всё разом. Именно это и произошло в моём ночном инциденте.
Ветки решают задачу изоляции. Отдельная ветка под каждый вектор атаки — exploit/sqli-login, exploit/ssrf-internal, report/client-name — и git reset --hard в одной ветке не затронет остальные. Элементарная гигиена, которая работает как предохранитель. Даже если вы только переходите в ИБ и пока ведёте один проект — привычка создавать ветку на каждую подзадачу спасёт при первом же сложном engagement’е с несколькими целями.
Теги (git tag) — фиксированные метки на конкретных коммитах. В отличие от веток, тег не двигается при новых коммитах. Для версионирования эксплоитов это критично: v1-blind-sqli, v2-union-based, v3-stacked-queries — каждый тег указывает на проверенную версию payload, к которой можно вернуться мгновенно. Когда клиент через месяц спрашивает «покажите тот PoC из отчёта» — достаточно git checkout v2-union-based вместо археологических раскопок в истории коммитов. Тег — ваш «бэкап бэкапа», который переживёт любой неудачный rebase.
Как git хранит коммиты и почему потерянные коммиты можно восстановить
Чтобы понять восстановление коммита через git reflog, достаточно одного принципа: git при обычных операциях почти никогда не удаляет данные физически.
Каждый файл, директория и коммит в git — объект с уникальным SHA-1 хэшем. SHA-1 — алгоритм хеширования, который создаёт «отпечаток» данных длиной 40 символов (вроде a1b2c3d4e5f6...). Объекты бывают трёх типов. Blob — содержимое одного файла, без имени и пути. Tree — список файлов в директории со ссылками на blob’ы. Commit — запись «кто, когда, что изменил» плюс ссылка на tree (полный снимок проекта в этот момент) и ссылка на предыдущий коммит. Всё это хранится в .git/objects внутри репозитория.
Когда вы запускаете git reset --hard HEAD~3, git не стирает три коммита. Он передвигает указатель текущей ветки на три коммита назад — как закладку в книге. Старые коммиты остаются в .git/objects, просто на них больше ничего не ссылается — ни ветка, ни тег. Такие объекты называются dangling (висячие) или unreachable (недостижимые).
Физически dangling-объекты удаляются только когда отработает сборщик мусора — git gc (garbage collection). По умолчанию gc не трогает dangling-объекты 30 дней, а записи в reflog живут 90 дней. Проверить настройки: git config --get gc.pruneExpire и git config --get gc.reflogExpire. Если команда ничего не вернула — действуют дефолты.
Аналогия проще некуда: вы удалили ярлык с рабочего стола, но файл на диске остался. Пока не очистите корзину — он доступен. В git роль корзины — .git/objects, роль очистки — git gc --prune=now. Практический вывод: после неаккуратного reset --hard у вас есть минимум 30 дней на восстановление испорченного коммита git. Главное — не запускайте git gc --prune=now вручную, пока не убедитесь, что всё нужное на месте.
Git reflog: примеры использования для восстановления испорченного коммита
Что понадобится
- Любая ОС с git 2.20+ (проверка:
git --version) - Терминал: bash, zsh или PowerShell
- Права root не нужны
- Сеть не нужна — все операции локальные
- Около 5 минут на полный цикл
Делай раз — создаём pentest-репозиторий и теряем коммит
Откройте терминал и выполните команды ниже. Мы создадим репозиторий с тремя коммитами (скелет эксплоита, blind-payload, UNION-payload), а затем «потеряем» два последних через git reset --hard — повторим мой ночной инцидент.
mkdir pentest-demo && cd pentest-demo && git init
echo "# SQLi PoC v0" > exploit.py
git add . && git commit -m "init: skeleton for SQLi PoC"
echo "payload = \"' OR 1=1 --\"" >> exploit.py
git add . && git commit -m "feat: blind SQLi payload"
echo "payload_v2 = \"' UNION SELECT 1,2,3 --\"" >> exploit.py
git add . && git commit -m "feat: UNION-based payload"
git reset --hard HEAD~2
Первые семь строк создали репозиторий с тремя коммитами. Последняя — git reset --hard HEAD~2 — откатила ветку на два коммита назад, к «skeleton». Выполните git log --oneline для проверки. Ожидаемый вывод — одна строка вида e0f1a2b init: skeleton for SQLi PoC (хэш у вас будет свой — SHA-1 уникален для каждого репозитория). Видите три строки — reset не сработал, проверьте, что находитесь в директории pentest-demo.
Делай два — находим потерянные коммиты git через reflog
Git reflog (reference log) — локальный журнал всех перемещений указателя HEAD. HEAD — указатель «где я сейчас нахожусь в истории репозитория». Он обновляется при каждом коммите, checkout, reset, rebase и merge, и reflog записывает все эти события автоматически. Нюанс, который новички часто упускают: reflog хранится только на вашей машине. Он не передаётся при git push и не появляется после git clone — это персональный бортовой журнал.
Выполните git reflog. Вывод будет примерно таким:
a1b2c3d HEAD@{0}: reset: moving to HEAD~2
f4e5d6c HEAD@{1}: commit: feat: UNION-based payload
b7a8c9d HEAD@{2}: commit: feat: blind SQLi payload
e0f1a2b HEAD@{3}: commit (initial): init: skeleton for SQLi PoC
Как читать: короткий хэш, индекс HEAD@{n} (сколько «ходов» назад в истории перемещений) и описание операции. HEAD@{0} — текущее состояние (результат нашего reset). HEAD@{1} — коммит с UNION-based payload, тот самый «потерянный». HEAD@{2} — blind SQLi payload. Нам нужен HEAD@{1} с хэшем f4e5d6c, потому что это последний коммит перед reset, содержащий оба payload’а.
Перед восстановлением убедитесь, что коммит — тот. Выполните git show f4e5d6c (подставив ваш хэш). Команда покажет diff — какие строки были добавлены. Видите payload_v2 = "' UNION SELECT 1,2,3 --" — это нужный коммит. Ещё полезна git reflog show main (или имя вашей ветки) — покажет историю перемещений конкретной ветки, а не всего HEAD. Под капотом это git log -g --abbrev-commit --pretty=oneline.
Делай три — восстанавливаем коммит и фиксируем тегом
Безопасный способ — создать новую ветку, указывающую на потерянный коммит, проверить содержимое и потом решить, что с ним делать:
git checkout -b recovered f4e5d6c
git log --oneline
# Ожидаемый вывод: три коммита — все payload'ы на месте
git tag -a v1-sqli-chain -m "Working SQLi chain, blind + union"
git checkout -b recovered f4e5d6c создала ветку recovered, указывающую на «потерянный» коммит. Теперь git log --oneline покажет все три коммита — оба payload’а вернулись. Тег v1-sqli-chain — дополнительная страховка: даже если вы случайно удалите ветку recovered через git branch -D, тег останется и будет указывать на этот коммит.
Альтернативный быстрый путь — вернуть указатель текущей ветки на потерянный коммит напрямую: git reset --hard f4e5d6c. Осторожно: команда перезапишет незакоммиченные изменения в рабочей директории. Если рабочее дерево «грязное» (есть несохранённые файлы), сначала git stash. Сразу после reset работает и shortcut git reset --hard ORIG_HEAD — git автоматически сохраняет предыдущее положение HEAD в специальную ссылку ORIG_HEAD перед каждым reset и rebase. Но ORIG_HEAD перезаписывается следующей деструктивной операцией, так что работает только «по горячим следам».
Третий вариант — cherry-pick. Нужны изменения из конкретного коммита, но не хотите откатывать всю ветку? git cherry-pick b7a8c9d перенесёт изменения из указанного коммита в текущую ветку как новый коммит. Удобно, когда вы уже продвинулись вперёд и хотите «подтянуть» конкретный потерянный payload, не трогая остальную историю.
Git reset и git reflog: отличия и когда что применять
Эти команды часто путают, хотя делают принципиально разные вещи:
| Критерий | git reset |
git reflog |
|---|---|---|
| Назначение | Двигает указатель ветки на другой коммит | Показывает журнал перемещений HEAD |
| Меняет историю | Да — скрывает коммиты из git log |
Нет — только читает журнал |
| Опасность | С --hard удаляет незакоммиченные файлы |
Полностью безопасна |
| Типичный сценарий | Откат к предыдущему состоянию | Поиск «потерянных» коммитов |
| Перед использованием | Запишите git rev-parse HEAD |
Запускайте сразу после ошибки |
На практике они работают в паре: git reset --hard создаёт проблему (коммиты «исчезают» из видимой истории), git reflog её решает (показывает хэши потерянных коммитов для восстановления).
Когда reflog бессилен. Есть команда [git fsck --unreachable --no-reflog](https://git-scm.com/docs/git-fsck) — она сканирует объектную базу .git/objects напрямую и находит все объекты, на которые не ссылается ни одна ветка или тег. Два сценария, где это спасает: reflog уже истёк (прошло больше 90 дней) или вы работаете с клонированным репозиторием (reflog при git clone не передаётся — строго локальный журнал). Нашли нужный коммит в выводе fsck — восстанавливаете стандартно: git branch recovered <хэш>.
Что не восстановит ни reflog, ни fsck. Незакоммиченные изменения. Если вы писали код в рабочей директории, не делали git add и git commit, а потом выполнили git reset --hard — всё, изменения потеряны безвозвратно. Git не записывал их ни в один объект, искать нечего. Правило, которое я бы повесил над монитором: коммитьте часто, даже с сообщением WIP: dirty hack, cleanup later. Грязный коммит, который можно восстановить, бесконечно ценнее чистого кода, который потерян навсегда.
Detached HEAD — ловушка, в которую попадают почти все новички при git checkout коммита по хэшу. Если выполнить git checkout f4e5d6c без флага -b, git переключится на коммит напрямую, минуя ветку. Вы окажетесь в состоянии «detached HEAD» (отсоединённая голова): файлы просматривать можно, но новые коммиты отсюда не привяжутся ни к одной ветке. При переключении обратно они станут unreachable — и придётся снова лезть в reflog. Решение простое: всегда создавайте ветку при checkout по хэшу — git checkout -b имя-ветки хэш (или в git 2.23+: git switch -c имя-ветки хэш).
Восстановление после неудачного rebase. Rebase (перебазирование — перенос коммитов на другую основу) переписывает историю. Если в процессе вы неверно разрешили конфликт, git может «потерять» часть коммитов. Ищите в reflog строку rebase (start) — запись непосредственно перед ней содержит хэш состояния ветки до rebase. git reset --hard HEAD@{N} (где N — индекс этой записи) вернёт ветку в исходное состояние. Нужны только отдельные потерянные коммиты из rebase — используйте git cherry-pick <хэш> для каждого.
Организация веток git при пентесте: рабочий workflow для red team
Git workflow для пентестера отличается от привычного разработчику Git Flow. Нет pull request’ов, code review и CI/CD-пайплайнов. Зато есть жёсткие дедлайны отчётов, параллельная работа по нескольким целям и чувствительный код, который нельзя светить.
Схема именования веток. Простая и масштабируемая:
recon/<цель>— результаты разведки, собранные URL, Google-доркиexploit/<тип-уязвимости>— PoC-код, скрипты эксплуатации (exploit/sqli-login,exploit/ssrf-internal)report/<клиент>— шаблон отчёта, скриншоты, текст с доказательствамиnotes/misc— заметки, черновики, чек-листы
Создание ветки: git checkout -b exploit/sqli-login. Переключение: git checkout exploit/ssrf-internal. Все ветки: git branch -a.
Теги — фиксация проверенных версий. Аннотированный тег с сообщением: git tag -a v1-blind-sqli -m "Blind SQLi, tested on staging env, 2025-06-15". Флаг -a добавляет сообщение и дату — через месяц вспомните, на чём тестировалось. Тег report-submitted-2025-06-15 фиксирует состояние репозитория на момент сдачи отчёта — пригодится, когда клиент вернётся с вопросами.
Все теги: git tag -l. Переход на конкретный тег: git checkout v1-blind-sqli (осторожно: detached HEAD, создайте ветку, если планируете вносить изменения).
Привычка перед деструктивными операциями. Перед любым reset, rebase или commit --amend запишите текущий хэш HEAD: git rev-parse HEAD. Скопируйте в буфер обмена. Если что-то пошло не так — git reset --hard <записанный-хэш> вернёт всё мгновенно, без ковыряния в reflog под давлением дедлайна.
OPSEC: pentest-отчёт и git-репозиторий как чувствительный актив. Репозиторий с эксплоитами — ценная цель. Если он утечёт (случайный push на публичный GitHub, компрометация ноутбука, утечка с незащищённого NAS), противник достанет ваши PoC-коды через git log или git fsck — по тем же техникам, которые описаны выше. MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1552.001 — её идентификаторы) описывает этот вектор: T1593.003 Code Repositories (тактика Reconnaissance) — атакующий находит репозитории компании на GitHub или GitLab; T1552.001 Credentials In Files (тактика Credential Access) — поиск паролей, ключей и токенов в файлах и истории коммитов. По данным IBM X-Force Threat Intelligence Index 2025, число атак с использованием действительных учётных данных выросло на 71% за год. CrowdStrike Global Threat Report 2025 подтверждает: 75% вторжений задействуют valid credentials. Заметная часть этих credential’ов попадает к атакующим именно из git-истории — в том числе из dangling-коммитов, которые разработчики считали «удалёнными».
Правила простые. Храните пентест-репозитории только локально или на self-hosted инстансе с MFA (многофакторной аутентификацией). Никогда не пушьте на публичные платформы. Случайно запушили — git push --force с очищенной историей не поможет: GitHub и GitLab сохраняют коммиты по прямой ссылке даже после force-push, пока не обратитесь в поддержку. Перед сдачей репозитория клиенту прогоните по нему сканер секретов (trufflehog или gitleaks) — эти инструменты проходят всю git-историю, включая dangling-коммиты, и находят паттерны API-ключей и токенов автоматически.
За два года работы с пятью командами пентестеров я не встретил ни одной, которая использовала бы теги для фиксации версий эксплоитов. Все хранили «финальные» PoC в папках с именами вроде exploit_final_v2_FINAL_last.py. Каждая из этих команд хотя бы раз теряла код из-за неаккуратного reset или rebase — и тратила часы на воссоздание того, что git reflog решает за полминуты. Проблема не в инструменте. Проблема в восприятии: git кажется «штукой для разработчиков», а не операционным инструментом наравне с Burp или Metasploit. Ветки, теги и reflog — не про «красивую историю коммитов». Это про то, чтобы в два часа ночи перед дедлайном спокойно набрать две команды и вернуться к работе вместо переписывания payload с нуля. На мой взгляд, git-грамотность должна входить в базовый набор навыков ИБ-специалиста наравне с умением работать в терминале — но в учебных программах этого почти нигде нет. Если вы в начале пути в ИБ и хотите выстроить базу системно — на IB Basics подобные навыки разбирают в связке с реальными рабочими задачами, а не изолированными упражнениями.
Эту тему и смежные навыки разбирают на практике в курсе «Git. Система контроля версий» Codeby Academy.
