Commitment Issues picoCTF 2024 Solution

Published: April 3, 2024

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 1
    List commits
    Observation
    I noticed the challenge description said the flag was 'deleted' and the drop-in directory contained a .git folder, which suggested the deletion was a git commit and that the flag could be recovered by inspecting the commit history with git log.
    git log walks the commit chain back from HEAD. The history shows a commit labeled "create flag" with hash 3d5ec8a26ee7b092a1760fea18f384c35e435139.
    bash
    git log
    commit 9b21a04c... (HEAD -> main)
    Author: ctf-author
    Date:   ...
        delete flag
    
    commit 3d5ec8a26ee7b092a1760fea18f384c35e435139
    Author: ctf-author
    Date:   ...
        create flag
    What didn't work first

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

    git status only shows the difference between the working tree and the current HEAD commit - it reports that message.txt does not exist, but gives no information about prior commits. git log is the right tool because it walks the entire commit chain, revealing the 'create flag' commit where the file was introduced before being removed.

    Tried: Running git diff HEAD to find the flag content

    git diff HEAD compares the working tree against the current commit and produces empty output here because both are in sync after the deletion commit. To see what changed between two commits you need git diff <hash1> <hash2>, or to read the file at a specific historical commit you need 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 deleted 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 2
    Check out the flag commit
    Observation
    I noticed git log revealed a commit hash labeled 'create flag' that preceded the deletion commit, which suggested checking out that specific commit would restore message.txt to the working tree and expose the flag.
    Run git checkout <hash> to change the working tree to that exact commit, then cat message.txt to see the flag. 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 creates a new commit that undoes the changes of the target commit - in this case it would undo the 'create flag' commit, which means it would delete message.txt again rather than restore it. The correct command is git checkout <hash>, which moves the working tree to the state at that commit so message.txt is present and readable.

    Tried: Running git stash pop expecting the deleted file to be stashed

    git stash only saves uncommitted working-tree changes; it has nothing to do with committed history. The deletion was committed as a separate commit, so there is nothing in the stash. Only git checkout <hash> or git show <hash>:message.txt can retrieve content that was part of a committed state.

    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 main (or master).

    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.

Flag

Reveal flag

picoCTF{s@n1t1z3_30e86...}

Key takeaway

Git is a content-addressable store where every committed version of every file is retained permanently in .git/objects, even after a follow-up commit removes the file. Deleting a secret in a later commit provides no protection because the original blob is still reachable by its hash. The only real remediation is to rotate the secret and rewrite history with git filter-repo, coordinating with every holder of a clone because old copies retain 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

Want more picoCTF 2024 writeups?

Useful tools for General Skills

What to try next