Skip to main content

Commitment Issues picoCTF 2024 Solution

Recover a flag that was accidentally added and then deleted from a Git repository's history.

Published: April 3, 2024Updated: August 25, 2026

Description

I accidentally wrote the flag down. Good thing I deleted it!

Download the challenge zip, unzip it locally, then change into the drop-in directory.

bash
wget https://artifacts.picoctf.net/c_titan/77/challenge.zip && \
unzip challenge.zip && \
cd drop-in/

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
This is a successor to the Time Machine challenge. If you use ls -a in drop-in directory you can see the .git file which allows you to see the commits. Before looking at prior commits in the log there is a file called "message.txt" with the file contents of "TOP SECRET".
  1. Step 1List commits
    Observation
    The description says the flag was deleted, and the directory has a .git folder. Whatever scrubbed it is a commit, so the history still holds the version that came before it.
    git log walks the commit chain back from HEAD. There are only two commits: "remove sensitive info" at the tip, and "create flag" beneath it with hash 3d5ec8a26ee7b092a1760fea18f384c35e435139.
    bash
    git log
    commit e1237df82d2e69f62dd53279abc1c8aeb66f6d64 (HEAD -> master)
    Author: picoCTF <ops@picoctf.com>
    Date:   Sat Mar 9 21:10:14 2024 +0000
    
        remove sensitive info
    
    commit 3d5ec8a26ee7b092a1760fea18f384c35e435139
    Author: picoCTF <ops@picoctf.com>
    Date:   Sat Mar 9 21:10:14 2024 +0000
    
        create flag
    What didn't work first

    Tried: Running git status to look for a deleted file instead of git log

    Nothing was deleted from disk: message.txt is still there, and the tip commit only overwrote its contents with 'TOP SECRET'. git status compares the working tree against HEAD, so with a clean tree it reports nothing at all. git log walks the whole chain and surfaces the commit where the real contents were written.

    Tried: Running git diff HEAD to find the flag content

    git diff HEAD compares the working tree with the current commit, and after the sanitizing commit those agree, so the output is empty. Comparing two commits needs both hashes; reading a file as it was at one commit needs git show or git checkout.

    Learn more

    This challenge hinges on a fundamental misunderstanding many people have: deleting a file in git does not erase it from history. Git is a content-addressable store. Every version of every file ever committed is permanently recorded under .git/objects/, even after a follow-up commit deletes the file.

    You don't even have to switch the working tree to read the original contents. git show <hash>:message.txt prints the file contents from any historical commit without changing what's on disk. git checkout is the heavier hammer, useful when you need multiple files restored together.

    This is why you should never commit secrets to a repository, even "temporarily." Removing a secret in a follow-up commit does not protect it. The only safe remediation is to rotate the secret and rewrite history with git filter-repo (then force-push and coordinate with everyone who has a clone).

  2. Step 2Check out the flag commit
    Observation
    git log shows a 'create flag' commit just before the sanitizing one. Check that commit out and message.txt reverts to the version that still held the flag.
    Run git checkout <hash> to change the working tree to that exact commit, then cat message.txt to see the flag instead of 'TOP SECRET'. You'll be in detached HEAD; that's fine for reading.
    bash
    git checkout 3d5ec8a26ee7b092a1760fea18f384c35e435139 && \
    cat message.txt

    Expected output

    picoCTF{s@n1t1z3_30e86...}
    What didn't work first

    Tried: Using git revert 3d5ec8a26ee7b092a1760fea18f384c35e435139 to recover the file

    git revert makes a new commit undoing the target commit, so reverting 'create flag' would strip message.txt back to empty rather than bring the flag back. Reverting the tip commit would work, but it writes history you did not need to write. git checkout on the create-flag hash moves the working tree to that state read-only.

    Tried: Running git stash pop expecting the old contents to be stashed

    git stash saves uncommitted working-tree changes and knows nothing about history. The overwrite was its own commit and the tree is clean, so the stash is empty. Only git checkout or git show against the hash reaches committed content.

    Learn more

    git checkout <hash> moves your working tree to the exact state the repository was in at that commit. Files present at that point are restored, and files added later disappear. You end up in "detached HEAD" state, meaning HEAD points directly to a commit rather than a branch name.

    You can explore freely in this state. To get back to the latest commit on your branch, run git checkout master, which is the branch this drop-in repository uses.

    Beyond CTFs, this technique drives real debugging. If you know a bug was introduced somewhere in the last 50 commits, git bisect automates a binary search through history by checking out midpoints until it isolates the culprit.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{s@n1t1z3_30e86...}

The trailing hex differs per artifact. Recover yours from the earlier blob still present in .git/objects, as shown above.

Key takeaway

Git is a content-addressable store: every committed version of every file stays in .git/objects even after a later commit removes it. Deleting a secret protects nothing, because the original blob is still reachable by hash. The real fix is to rotate the secret and rewrite history with git filter-repo, and to coordinate with everyone holding a clone, since their copies keep the full object store.

How to prevent this

Deleting a file in a follow-up commit does not remove it from history. The blob stays in .git/objects until pruned.

  • Pre-commit secret scanning is the only intervention that prevents the leak. Install gitleaks, trufflehog, or detect-secrets as a Git hook plus a CI check.
  • If a secret slips through, rotate the secret first, then rewrite history with git filter-repo and force-push. Old clones, mirrors, and forks still have the data; rotation is the only real fix.
  • Do not store credentials in repos at all. Use a secrets manager (Vercel env vars, AWS Secrets Manager, Doppler, 1Password CLI) and pull at runtime. Then a leak yields nothing useful.

Related reading

Useful tools for General Skills

Where to go next