Description
Can you find the flag in this disk image?
Setup
Download and decompress the disk image.
Mount the image and explore the git repository inside.
gunzip disk.img.gzmmls disk.imgsudo mount -o loop,offset=$((512*<start_sector>)) disk.img /mnt/diskSolution
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, and the flag may sit as plaintext in the raw data. Decompress and run strings before parsing any filesystem.Extract the disk image, then run strings as a fast pre-check for an obvious flag.bashgunzip disk.img.gzbashstrings disk.img | grep picoCTFWhat didn't work first
Tried: Running strings on disk.img.gz before decompressing
Compressed data is not printable ASCII, so strings returns almost nothing beyond the original filename in the gzip header. The flag text lives inside the compressed payload, which strings cannot decode. Decompress first.
Tried: Skipping strings and going straight to mounting because strings is too simple
strings finds plaintext anywhere in the image: loose git objects, commit messages, even unallocated space, with no filesystem parsing at all. Skipping it throws away the fastest path. If it hits, you are done in a minute.
Learn more
Running
strings disk.img | grep picoCTFis 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.stringslooks for sequences of at least 4 consecutive printable ASCII characters by default. You can lower the threshold with-n 3or raise it with-n 8. For UTF-16 strings (common in Windows artifacts) usestrings -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.Step 2Detect partition layout and mount
ObservationA raw .img opens with an MBR partition table, not a filesystem, so a loop mount at offset 0 fails. Read the table with mmls for the ext4 partition's start sector.Use mmls to find the correct partition offset, then mount it.bashmmls disk.imgbashsudo mount -o loop,offset=$((512*<start_sector>)) disk.img /mnt/diskbashls /mnt/diskWhat didn't work first
Tried: Mounting with sudo mount -o loop disk.img /mnt/disk without any offset
A raw image starts with the MBR partition table, not a filesystem, so mounting at offset 0 reports a bad superblock: the kernel found partition metadata where it expected ext4. Read the table with mmls, take the data partition's start sector, and multiply by the sector size for the mount offset.
Tried: Using fdisk -l disk.img instead of mmls to find the partition offset
fdisk does report start sectors, but it is aimed at live block devices and its output format drifts between versions. mmls is the forensic standard for raw images: it parses both MBR and GPT, labels each partition, and reads the same everywhere.
Learn more
mmlsis 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 thanfdisk -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.mmlsoutput includes aDescriptioncolumn 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.Step 3Copy the git repository
ObservationThe title points at git, so expect a .git directory inside. The mount is read-only, and git commands write internal state even when reading, so copy the repo somewhere writable first.Locate and copy the .git directory to a writable path.bashfind /mnt/disk -name '.git' -type dbashcp -r /mnt/disk/<repo_path> /tmp/repobashcd /tmp/repoWhat didn't work first
Tried: Running git commands directly inside /mnt/disk without copying first
The mount is read-only or root-owned, and git updates internal state such as the reflog even during read operations, which produces write errors mid-command. Copy the repository somewhere writable and everything runs cleanly.
Tried: Copying only the working tree files instead of the full repo directory including .git
The working tree is only a checked-out snapshot; history, dangling objects, stashes, tags, and reflogs all live in .git. Copy only the visible files and you discard everything that matters. Copy the parent directory.
Learn more
The
.gitdirectory 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
/tmpavoids 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.Step 4Comprehensive git history search
ObservationThe hint says the flag was removed from the visible history, so the reachable commits will not have it. Sweep every storage location: stash, reflog, tags, notes, and the dangling blobs fsck turns up.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.bashgit log --all -p | grep -A2 picoCTFbashgit branch -abashgit stash list && git stash show -pbashfor tag in $(git tag -l); do echo "=== $tag ==="; git show "$tag"; donebashfor c in $(git log --all --format=%H); do git notes show "$c" 2>/dev/null; donebashgit reflogbashgit fsck --unreachable 2>&1 | grep blobbashgit fsck --lost-foundbashgrep -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 walks commits reachable from named refs. Dangling blobs, dropped stashes, and commits that were amended or force-pushed over attach to no ref and never appear. Run fsck and read the reflog to catch what has been dereferenced.
Tried: Catting all files in .git/lost-found/other/ with a wildcard instead of per-file
Concatenating every lost-found blob merges their content with no separators, so you cannot tell which object held the flag or read its context. Printing a header per file preserves provenance and lets you go back to the object with git cat-file.
Learn more
Git's content-addressed storage means nothing is truly deleted until
git gcprunes 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 --unreachablelists every object in the object store that cannot be reached by following any ref. These are the objects thatgit gcwould delete. They include old versions of files, commits on deleted branches, and stash entries that were dropped.git fsck --lost-foundgoes 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.
Step 5Extract the flag
ObservationThe sweep named a specific blob or commit hash. Read that object directly with git cat-file or git show.Read content from whichever location held the flag.bashgit show <commit>bashgit 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-pflag means "pretty-print": it automatically detects the object type and formats accordingly.Once you identify a suspicious hash from
git fsck,git reflog, orgit 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
- 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_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
How to prevent this
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 --aggressiveafter history rewrites to actually drop unreachable objects. On GitHub/GitLab, the "remove sensitive data" flow is more reliable than gc.