Description
Can you find the flag in this disk image?
Setup
Download and decompress the disk image.
Use libguestfs tools to explore and extract the git repository inside.
gunzip disk.img.gzvirt-ls -a disk.img /Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Decompress and quick strings check
ObservationThe download is a compressed disk image, so nothing is readable until it is decompressed. Once it is, a strings scan is the fastest triage before touching the filesystem.Extract the disk image, then run strings as a fast pre-check for an obvious flag before mounting.bashgunzip disk.img.gzbashstrings disk.img | grep picoCTFWhat didn't work first
Tried: Running strings on the compressed disk.img.gz file before decompressing it.
Compression replaces the original byte patterns with dense data, so strings finds only gzip header noise. The flag bytes do not exist in their original form inside the compressed stream. Decompress first.
Tried: Grepping for the flag with a lowercase 'picoctf' pattern instead of 'picoCTF'.
grep is case-sensitive by default, so 'picoctf' misses the mixed-case prefix the flag actually uses. Adding the -i flag would work, but the simplest fix is to match the exact case 'picoCTF' that all picoCTF flags share.
Learn more
stringsscans a binary file for sequences of printable ASCII characters of a minimum length (default 4). It is the fastest possible triage tool for a disk image: if the flag is stored in plaintext anywhere in the image (in a file, in git object data, or even in slack space)strings | grep picoCTFfinds it immediately without mounting or parsing the filesystem.Disk images are raw byte-for-byte copies of storage devices. A gzip-compressed image (
.img.gz) must be decompressed first because the compression transforms the bytes, makingstringsuseless on the compressed file. After decompression, the image contains the same layout as the original disk, including partition tables, filesystem metadata, and file data.In real forensic investigations this triage step is called a keyword search and is one of the first steps in any examination. Tools like Autopsy and Bulk Extractor automate keyword searches across entire disk images and can find strings in compressed or carved files as well.
Step 2Browse and extract the disk image with libguestfs
ObservationMounting a raw image means knowing the partition offset and having root. libguestfs tools browse and extract the filesystem without either.Use virt-ls to list the filesystem contents and virt-copy-out to extract the repository directory to a local writable path.bashvirt-ls -a disk.img /bashvirt-ls -a disk.img /homebashsudo virt-copy-out -a disk.img /home/ctf-player /tmp/bashls /tmp/ctf-playerExpected output
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var ctf-player flag_repo
What didn't work first
Tried: Trying to mount the disk image with 'sudo mount -o loop disk.img /mnt' without specifying a partition offset.
A raw image usually opens with a partition table, and the filesystem starts at a sector offset rather than byte 0. Mount without that offset and the kernel reads partition metadata as a superblock, then rejects it or shows garbage. Finding the start sector takes mmls or fdisk, which is why virt-ls is simpler.
Tried: Running virt-copy-out targeting /home instead of the specific /home/ctf-player subdirectory.
virt-copy-out copies the named path whole, so targeting /home pulls the entire tree, large files included, and takes far longer. Its output placement also varies by version. Name the specific subdirectory and you get just what you need.
Learn more
libguestfs is a library and set of command-line tools for accessing and modifying virtual machine disk images without mounting them.
virt-lslists files and directories inside an image (similar tols), andvirt-copy-outextracts files or directories from an image to the host filesystem. Neither tool requires computing partition offsets or running as root in the same way thatmount -o loop,offset=Ndoes.This approach is safer for forensic work because it never mounts the image as a kernel filesystem, so the kernel never writes access-time (
atime) or journal metadata back to the image. The image file itself remains byte-for-byte identical before and after the operation.The alternative approach of using
mmls(The Sleuth Kit) to find the partition start sector and thenmount -o loop,offset=$((512 * start_sector))also works but requires root and offset arithmetic. For CTF purposes, virt-ls and virt-copy-out are faster and less error-prone.Step 3Find and copy the git repository
ObservationThe challenge name says git, and the image has a home directory called flag_repo. Copy it somewhere you own, or git will refuse on ownership grounds.Locate the .git directory in the extracted files and copy it to a writable location. Git may refuse to run due to an ownership mismatch on the copied directory; add a safe.directory exception to fix it.bashfind /tmp/ctf-player -name '.git' -type dbashcp -r /tmp/ctf-player/<repo_path> /tmp/repobashcd /tmp/repobash# If git refuses with 'detected dubious ownership':bashgit config --global --add safe.directory /tmp/repobashgit statusWhat didn't work first
Tried: Running git commands directly inside the virt-copy-out output path without copying to /tmp first.
The extracted directory keeps the original UID from the image rather than yours, and modern git refuses to run in a directory owned by someone else, calling the ownership dubious. The message points at safe.directory, but copying the repo somewhere you own is simpler.
Tried: Using find to search for files named 'flag' or '*.txt' instead of looking for the .git directory.
The flag is in a commit message, not a file in the working tree, and find only sees the current checked-out state. Anything added and later deleted, or living only in a message, is invisible to it. Search .git instead.
Learn more
A git repository stores its entire history in the hidden
.gitdirectory at the root of the working tree. This directory contains the object store (.git/objects/), refs (.git/refs/), the HEAD pointer, config, and optional extras like stash and notes. Everything needed to reconstruct any version of every file ever committed is inside.git.Mounted loop devices are usually read-only or have ownership restrictions that prevent running git commands in-place. Copying the repository to
/tmp(or another writable path) before running git commands avoids permission errors and also protects the mounted evidence from accidental modification. After copying, git commands run against the local copy as if it were a normal repository.In digital forensics, working on a copy rather than the original is called working on a forensic duplicate. The original evidence (the disk image) remains intact and can be re-examined if the working copy is corrupted. This is a core principle of the ACPO Good Practice Guide and similar forensic standards.
Step 4Comprehensive git history search
ObservationNo plaintext file in the working tree holds the flag, so it was committed and later removed. Search every branch, stash, tag, reflog entry, and dangling object.In this challenge the flag phrase is stored in a git commit message. Run git log --all to read all commit messages across every branch. If git log is not enough, fall back to dangling and lost-found objects.bash# 1. Reachable history first - cheap, structured, often holds the flagbashgit log --all -p | grep -A2 picoCTFbashgit branch -abashgit tag -lbashfor c in $(git log --all --format=%H); do git notes show "$c" 2>/dev/null; donebashgit reflogbashgit stash list && git stash show -pbashbash# 2. Dangling and lost-found objectsbashgit fsck --unreachable 2>&1 | grep blobbashgit fsck --lost-foundbashgrep -rB1 picoCTF .git/lost-found/other/What didn't work first
Tried: Running 'git log -p | grep picoCTF' without the --all flag.
Without --all, git log walks only what the current branch reaches. A commit on another branch, in a detached HEAD, or on a remote-tracking ref is invisible. --all walks every ref, remote branches and tags and stashes included, which matters when you do not know where the author hid it.
Tried: Skipping git fsck and assuming the flag must be in a reachable commit or file.
Authors often park the flag in a dangling blob or a dropped stash precisely to catch solvers who check only the visible history. Nothing points to those objects, so git log never shows them. git fsck lists them by hash and git cat-file reads them.
Learn more
Git's content-addressed object store means that deleted data is not truly gone until it is garbage-collected. Every commit, tree, blob, and tag is stored as an object identified by the SHA-1 hash of its content. Deleting a file in a commit creates a new commit that does not reference the old blob, but the blob itself remains in
.git/objects/untilgit gcprunes it. The same applies to deleted branches, dropped stash entries, and amended commits: the old objects persist as dangling (unreachable) objects.The eight locations to check are:
- Commit history (
git log --all -p): shows diffs across all branches - Branches (
git branch -a): lists local and remote-tracking branches - Stash (
git stash list): saved work-in-progress snapshots - Tags (
git tag -l): annotated tags can hold arbitrary messages - Notes (
git notes list): metadata attached to commits outside the commit object - Reflog (
git reflog): every movement of HEAD and branch tips, including force-pushes and resets - Dangling blobs (
git fsck --unreachable): objects with no path to any ref - Lost-and-found (
git fsck --lost-found): writes dangling objects to.git/lost-found/
This breadth of hiding locations makes git repositories a rich source of sensitive data in real investigations. Developers often accidentally commit API keys, passwords, or private keys, then delete them in a follow-up commit, but the data remains in history. Tools like truffleHog, git-secrets, and gitleaks scan repositories for secrets in all of these locations.
- Commit history (
Step 5Read flag content
ObservationThe log or fsck output names a specific commit or blob hash. git show or git cat-file prints its full content.Once a suspicious commit or blob is found, show its full content.bashgit show <commit>bashgit cat-file -p <blob-hash>bashcat <flag-file>Learn more
git show <hash>displays the content and metadata of any git object: for commits it shows the diff; for blobs it shows the raw file content; for trees it lists the directory entries; for tags it shows the tag message and the tagged object.git cat-file -p <hash>does the same but works with raw hashes and is slightly more low-level: useful when you have a hash fromgit fsckoutput and want to inspect it without knowing the object type.Understanding git's object model at this level is valuable for both forensics and everyday development. Knowing that every version of every file is a blob object with a deterministic hash, and that commit objects reference tree objects (directory snapshots) which reference blob objects, explains why git is so reliable for history, and why deleted data persists until
git gc --prune=now --aggressiveexplicitly removes unreachable objects.For more on the underlying disk-image and shell techniques, see Linux CLI for CTF.
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.
- 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.
Flag
Reveal flag
picoCTF{g17_1n_7h3_d15k_...}
The flag is hidden in a git commit message on the disk image. Run git log --all to find it.
Key takeaway
How to prevent this
How to prevent this
Git history is permanent. Treat any commit (even one you delete) as published.
- Run a pre-commit hook with
gitleaks,trufflehog, ordetect-secretsto block secrets before they enter history. Add the same scan to CI as a backstop. - If a secret slips in, rotate it immediately.
git filter-repo+git push --forcerewrites history but does not erase clones, mirrors, GitHub forks, or backups. Assume compromise; rotation is the only real fix. - Block
/.git/at the edge (nginxlocation ~ /\.git { deny all; }or equivalent). Exposed.gitdirectories on production servers are still a top-10 finding in pentests in 2026.