Forensics Git 1 picoCTF 2026 Solution

Published: March 20, 2026

Description

Can you find the flag in this disk image?

Download and decompress the disk image.

Mount the image and explore the git repository inside.

bash
gunzip disk.img.gz
bash
mmls disk.img
bash
sudo mount -o loop,offset=$((512*<start_sector>)) disk.img /mnt/disk

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Decompress and quick strings check
    Observation
    I noticed the challenge provides a compressed disk image (.img.gz), which means the flag may be stored as plaintext inside the raw disk data, suggesting that running strings on the decompressed image is the fastest possible first triage step before attempting any filesystem parsing.
    Extract the disk image, then run strings as a fast pre-check for an obvious flag.
    bash
    gunzip disk.img.gz
    bash
    strings disk.img | grep picoCTF
    What didn't work first

    Tried: Running strings on disk.img.gz before decompressing

    gzip-compressed data is binary and not printable ASCII, so strings returns almost nothing useful - only the original filename embedded in the gzip header. The flag text is stored inside the compressed payload, which strings cannot decode. Decompress first with gunzip, then run strings on the raw disk.img.

    Tried: Skipping strings and going straight to mounting because strings is too simple

    Strings finds flags stored in plaintext anywhere in the image - in git object loose files, in commit messages, even in unallocated space - without any filesystem parsing. Skipping it wastes the fastest possible solve path. Run it first; if it returns a hit you are done in under a minute.

    Learn more

    Running strings disk.img | grep picoCTF is always the first triage step for a disk image challenge. If the flag is stored in plaintext anywhere in the image (inside a file, in git object data, in filesystem metadata, or even in unallocated slack space) this one-liner finds it instantly without requiring mounting or filesystem parsing.

    strings looks for sequences of at least 4 consecutive printable ASCII characters by default. You can lower the threshold with -n 3 or raise it with -n 8. For UTF-16 strings (common in Windows artifacts) use strings -e l. The tool ignores file structure entirely, which means it finds text even in compressed sections, inside binary headers, and in deleted-but-not-overwritten file content.

  2. Step 2
    Detect partition layout and mount
    Observation
    I noticed that a raw disk image (.img) starts with an MBR partition table rather than a filesystem, which means a naive loop mount at offset 0 will fail; reading the partition table with mmls is required to find the actual start sector of the ext4 data partition before mounting.
    Use mmls to find the correct partition offset, then mount it.
    bash
    mmls disk.img
    bash
    sudo mount -o loop,offset=$((512*<start_sector>)) disk.img /mnt/disk
    bash
    ls /mnt/disk
    What didn't work first

    Tried: Mounting with sudo mount -o loop disk.img /mnt/disk without any offset

    A raw disk image starts with the MBR partition table, not a filesystem. Mounting at offset 0 gives an error like 'wrong fs type, bad option, bad superblock' because the kernel finds partition table bytes where it expects an ext4 superblock. You must read the partition table with mmls, find the data partition's start sector, and pass offset=$((512 * start_sector)) so mount lands on the actual filesystem.

    Tried: Using fdisk -l disk.img instead of mmls to find the partition offset

    fdisk -l works and does report start sectors, but it targets live block devices and its output format differs between versions. mmls from The Sleuth Kit is the forensic-standard tool for raw images: it reliably parses both MBR and GPT layouts, clearly labels each partition, and its output is consistent across distributions. Use mmls to avoid ambiguity in the sector number column.

    Learn more

    mmls is part of The Sleuth Kit (TSK), a widely used open-source digital forensics toolkit. It reads MBR and GPT partition tables and displays each partition's slot number, type, start sector, end sector, length, and description. This is faster and more forensically reliable than fdisk -l, which is designed for live disks rather than raw image files.

    Disk images often contain multiple partitions: a small EFI system partition, a swap partition, and the main Linux ext4 data partition. You need to mount the correct one. The data partition is typically the largest and is usually type 0x83 (Linux) in MBR layouts or has a matching GUID in GPT layouts. mmls output includes a Description column that helps identify it.

    After mounting, the filesystem is accessible under the mount point just like any other directory. You can use standard file tools (ls, find, cat) to explore it. Mounting with -o ro (read-only) is best practice to avoid accidentally modifying the evidence.

  3. Step 3
    Copy the git repository
    Observation
    I noticed the challenge title references git, so after mounting I expected to find a .git directory inside the image; because the mounted filesystem is typically read-only, copying the entire repo to /tmp/repo is necessary before running any git forensics commands that write to internal state.
    Locate and copy the .git directory to a writable path.
    bash
    find /mnt/disk -name '.git' -type d
    bash
    cp -r /mnt/disk/<repo_path> /tmp/repo
    bash
    cd /tmp/repo
    What didn't work first

    Tried: Running git commands directly inside /mnt/disk without copying first

    The mounted image is typically read-only (or root-owned), and git commands update internal state such as the reflog and FETCH_HEAD even for read-only operations. This causes 'unable to write to .git/logs/HEAD' errors that interrupt commands mid-run. Copying the repository to /tmp/repo gives you a writable clone where all git forensics commands run cleanly.

    Tried: Copying only the working tree files instead of the full repo directory including .git

    The working tree files are just checked-out snapshots; all history, dangling objects, stash entries, tags, and reflog data live inside the .git subdirectory. Copying only the visible files discards the entire history. You must cp -r the parent repo directory so that the .git folder is included.

    Learn more

    The .git directory is the heart of any git repository. It contains all objects (blobs, trees, commits, tags), all refs (branches, tags, HEAD), the stash, notes, reflog, and configuration. An entire repository's history is fully recoverable from just this directory: the working tree files are simply checked-out copies of objects already in .git/objects/.

    Copying the repository to a writable location is necessary because git commands write to the repository (updating the reflog, for example) and the mounted filesystem may be read-only or owned by root. Working in /tmp avoids permission issues. The copy is an exact duplicate: all objects, refs, and configs are preserved, so all git forensics commands work identically on the copy.

  4. Step 4
    Comprehensive git history search
    Observation
    I noticed the challenge hint implies the flag was hidden or deleted from the visible git history, which suggested that reachable commits alone would not contain it; a systematic sweep of every git storage location (stash, reflog, tags, notes, and dangling blobs via git fsck --lost-found) was needed to cover all hiding places.
    Walk every hiding place. Don't concatenate lost-found blobs - cat them per-file with a header so you can identify the source. Iterate every tag rather than peeking at only the first one.
    bash
    git log --all -p | grep -A2 picoCTF
    bash
    git branch -a
    bash
    git stash list && git stash show -p
    bash
    for tag in $(git tag -l); do echo "=== $tag ==="; git show "$tag"; done
    bash
    for c in $(git log --all --format=%H); do git notes show "$c" 2>/dev/null; done
    bash
    git reflog
    bash
    git fsck --unreachable 2>&1 | grep blob
    bash
    git fsck --lost-found
    bash
    grep -rB1 picoCTF .git/lost-found/other/
    What didn't work first

    Tried: Stopping after git log --all -p | grep picoCTF returns nothing

    git log --all only walks commits reachable from named refs (branches, tags, HEAD). Dangling blobs, dropped stash entries, and commits that were amended or force-pushed over are not attached to any ref and will not appear in log output. You must also run git fsck --lost-found and inspect reflog entries to catch objects that have been dereferenced.

    Tried: Catting all files in .git/lost-found/other/ with a wildcard instead of per-file

    Concatenating all lost-found blobs together mixes their content with no separators, making it impossible to identify which object held the flag or reconstruct the surrounding context. The loop pattern with a header per file (echo '=== $hash ===') preserves provenance so you can trace the flag back to its git object and retrieve it cleanly with git cat-file -p.

    Learn more

    Git's content-addressed storage means nothing is truly deleted until git gc prunes unreachable objects. A developer who commits a secret and then removes it in a later commit has only hidden it from casual inspection: the blob containing the secret remains as a dangling object in .git/objects/. The same is true for force-pushed-over commits (visible in the reflog), dropped stash entries, and deleted branches (still in the reflog until expiry).

    git fsck --unreachable lists every object in the object store that cannot be reached by following any ref. These are the objects that git gc would delete. They include old versions of files, commits on deleted branches, and stash entries that were dropped. git fsck --lost-found goes further and writes them into .git/lost-found/other/ (blobs) and .git/lost-found/commit/ (commits) so they can be read with standard tools.

    This challenge may hide the flag in any of these locations, requiring you to check all of them systematically. In real security investigations, tools like truffleHog, gitleaks, and git-secrets automate this search across all git history locations and flag any strings matching credential patterns.

  5. Step 5
    Extract the flag
    Observation
    I noticed that the previous fsck and reflog sweep identified a specific blob hash or commit hash containing the flag, which suggested using git cat-file -p or git show on that specific hash to read out the raw object content directly.
    Read content from whichever location held the flag.
    bash
    git show <commit>
    bash
    git cat-file -p <blob-hash>
    Learn more

    git cat-file -p <hash> prints the raw content of any git object. For a blob (file snapshot) it prints the file content. For a commit it prints the commit message, author, timestamp, and tree reference. For a tree it lists the directory entries. The -p flag means "pretty-print": it automatically detects the object type and formats accordingly.

    Once you identify a suspicious hash from git fsck, git reflog, or git stash list, git cat-file -p <hash> reveals its content without needing to check out any branch or restore any working tree files. This is the most direct way to inspect raw git objects and is an essential technique for both forensics and debugging git corruption issues.

    For more on the surrounding shell workflow, see Linux CLI for CTF.

Interactive tools
  • 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.
  • File Magic IdentifierIdentify file types from magic numbers. Paste hex bytes or drop a file to detect PNG, JPEG, ZIP, PDF, ELF, PCAP, SQLite, and dozens of other formats.
  • 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.

Flag

Reveal flag

picoCTF{g17_r3m3mb3r5_...}

Flag found in git history inside the disk image. Mount the image, navigate to the embedded git repository, and recover the flag from a previous commit using git log and git checkout.

Key takeaway

Git's object store is append-only during normal operation: every blob, commit, and tree added to a repository persists in .git/objects until an explicit garbage collection pass removes unreachable objects. A secret that was committed and then deleted, force-pushed over, or dropped from a stash is still recoverable via git fsck and git cat-file because the object itself was never removed, only dereferenced. Real-world breach investigations routinely recover credentials this way, which is why pre-commit scanning and immediate rotation (not history rewriting alone) are the required responses.

How to prevent this

Deleted branches, dropped stashes, and amended commits all leave objects behind. Do not rely on deletion to remove secrets.

  • Pre-commit secret scanning (gitleaks, trufflehog, detect-secrets) is the only intervention that prevents the bug. Deletion after the fact does not help; the blob is already in .git/objects/.
  • For accidental commits: rotate the secret first, then rewrite history (git filter-repo) and force-push. Notify anyone who has cloned the repo to re-clone fresh; old clones still hold the secret.
  • On servers, run git gc --prune=now --aggressive after history rewrites to actually drop unreachable objects. On GitHub/GitLab, the "remove sensitive data" flow is more reliable than gc.

Related reading

Want more picoCTF 2026 writeups?

Useful tools for Forensics

What to try next