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 1
Decompress and quick strings checkObservationI noticed the challenge provides a compressed disk image (.img.gz), which meant the raw flag bytes would only be recoverable after decompression, suggesting a quick strings scan of the decompressed image as the fastest possible triage before doing any filesystem work.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.
gzip compression replaces the original byte patterns with entropy-dense compressed data, so strings produces only gzip header noise and no readable ASCII. The flag bytes are not present in the compressed stream in their original form. You must decompress with gunzip first so strings can scan the raw disk bytes.
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 2
Browse and extract the disk image with libguestfsObservationI noticed that mounting a raw disk image requires knowing the partition offset and root access, which suggested using libguestfs tools (virt-ls and virt-copy-out) to safely browse and extract the filesystem contents without offset arithmetic or kernel-level mounting.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 disk image typically starts with a partition table (MBR or GPT), and the actual filesystem begins at a sector offset inside the image, not at byte 0. Mounting without the correct offset attaches the wrong bytes and the kernel either rejects the mount or shows an empty or garbled filesystem. You need mmls or fdisk -l to find the start sector and compute the byte offset, making virt-ls the far simpler path.
Tried: Running virt-copy-out targeting /home instead of the specific /home/ctf-player subdirectory.
virt-copy-out copies the named path as a single unit; targeting /home extracts the entire home directory tree, which may include large files and takes much longer. More importantly the output is placed differently than expected depending on the tool version. Targeting the specific subdirectory /home/ctf-player keeps the extraction focused and places just the needed files under /tmp/.
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 3
Find and copy the git repositoryObservationI noticed the challenge is named 'forensics-git-0' and the disk image contained a home directory called flag_repo, which indicated a git repository was the target and that copying it to a locally owned path would be necessary to run git commands without ownership errors.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 may be owned by the original UID from the disk image (e.g. UID 1000 for ctf-player) rather than your current user, and git 2.35.2+ refuses to run in a directory owned by a different user with 'detected dubious ownership'. The error message points to safe.directory but the simpler fix is to cp -r the repo to /tmp where you own the copy, then run git there.
Tried: Using find to search for files named 'flag' or '*.txt' instead of looking for the .git directory.
The challenge stores the flag in a git commit message, not in a plaintext file in the working tree. A find-based search locates only files that exist in the current checked-out state. If the flag was added and then deleted in a later commit, or exists only as a commit message, find will return nothing useful. The .git directory is the correct target.
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 4
Comprehensive git history searchObservationI noticed the flag was not present in any plaintext file in the working tree, which suggested it had been committed and possibly removed from later history, pointing to a thorough git log --all search across every branch, stash, tag, reflog, 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 only walks commits reachable from the current HEAD branch. If the flag commit is on a different branch, in a detached-HEAD state, or on a remote-tracking ref, it is completely invisible to the command. Adding --all tells git to walk every ref including remote-tracking branches, tags, and stash refs, which is essential when you do not know where the challenge author hid the data.
Tried: Skipping git fsck and assuming the flag must be in a reachable commit or file.
Challenge authors often store the flag in a dangling blob or a dropped stash entry specifically to trip up solvers who only check the visible history. Dangling objects have no ref pointing to them so they never appear in git log output at all. git fsck --unreachable lists them by hash and git cat-file -p reads their content, making the two-command combo essential for a complete search.
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 5
Read flag contentObservationI noticed the git log or fsck output identified a specific commit hash or blob hash containing 'picoCTF', which indicated using git show or git cat-file -p to display the full object content and extract the flag.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
- 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_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.