Description
I have built my own Git server with my own rules!
Setup
Launch the challenge instance and note the Git server URL and port.
You'll need git installed locally.
Solution
Walk me through it- Step 1Clone the repositoryClone the custom Git server's repository and enter the directory.bash
git clone http://<HOST>:<PORT_FROM_INSTANCE>/repo.gitbashcd repoLearn more
git clonedownloads a repository's entire object store and ref namespace from a remote server. The Git protocol (over HTTP, HTTPS, or SSH) negotiates which objects the client needs and streams pack files containing them. By default,git cloneonly fetches refs underrefs/heads/(branches) andrefs/tags/- other namespaces likerefs/hidden/orrefs/secret/are not fetched unless explicitly requested.A custom Git server can enforce non-standard rules: restricting which refs are advertised, requiring authentication for certain namespaces, or exposing additional ref namespaces that standard Git clients don't fetch by default. The "own rules" in this challenge's description hints that the server exposes refs outside the standard
refs/heads/andrefs/tags/namespaces. - Step 2List all remote refsUse git ls-remote to enumerate every ref the server exposes, including custom namespaces that are hidden from normal clone.bash
git ls-remote originLearn more
git ls-remotequeries the remote server's ref advertisement and prints every ref the server is willing to share. Unlikegit clone, which only fetches standard namespaces,ls-remoteshows the complete list of available refs - including non-standard namespaces likerefs/hidden/,refs/secret/,refs/notes/,refs/stash, andrefs/pull/(GitHub pull request refs).In real Git server administration, understanding which refs are advertised is important for security. A poorly configured git server might expose internal branches (feature branches with sensitive code, configuration with credentials, or work-in-progress that was never meant to be public) through non-standard ref namespaces that developers don't know about. The
uploadpack.hideRefsgit config option can hide specific ref patterns from advertisement. - Step 3Fetch custom namespacesGlob fetches succeed silently when the namespace is empty (no error, no output). After every fetch, run git for-each-ref to confirm something actually landed locally. Otherwise you can't tell a successful 'no refs in this namespace' from a typo.bash
git fetch origin 'refs/hidden/*:refs/hidden/*'bashgit for-each-ref refs/hidden/ # verify fetch landedbashgit fetch origin 'refs/secret/*:refs/secret/*'bashgit for-each-ref refs/secret/bashgit fetch origin 'refs/flag/*:refs/flag/*'bashgit for-each-ref refs/flag/bashgit for-each-ref --format='%(refname) %(objectname)' | head -40Learn more
Git's refspec syntax
src:dstmaps remote refs to local refs. The glob patternrefs/hidden/*:refs/hidden/*fetches all refs under the remote'srefs/hidden/namespace and stores them locally under the same name. Without an explicit refspec,git fetchonly fetches what is configured in.git/configunder[remote "origin"]- typically+refs/heads/*:refs/remotes/origin/*.git for-each-reflists all refs in the local repository with customisable output format. After fetching non-standard namespaces, it shows every ref including the newly fetched ones. This is a quick way to verify that the fetch succeeded and to see which objects the refs point to.This technique is used in real scenarios to fetch GitHub pull request refs (
refs/pull/*/headandrefs/pull/*/merge), which are not fetched by default but are useful for CI/CD systems that need to test code from unmerged pull requests. The same mechanism lets you fetch any non-standard ref namespace that the server exposes. - Step 4Enumerate all branches and tagsCheck every branch and tag for flag content in commit messages, file contents, and diffs.bash
git branch -abashgit tag -lbashgit log --all --onelinebashgit log --all -p | grep -A2 picoCTFLearn more
git branch -alists both local branches and remote-tracking branches (prefixed withremotes/origin/).git log --allshows commits reachable from any ref, including all branches and tags. The-pflag adds the diff for each commit - piping this throughgrep -A2 picoCTFfinds any commit that added or removed the flag string along with 2 lines of context.Annotated tags (created with
git tag -a) are full objects with their own messages, authors, and timestamps - separate from the commit they point to. A flag can be hidden in an annotated tag's message without appearing in any commit.git tag -llists tag names;git show <tagname>displays the tag message and the tagged object. - Step 5Check stash, notes, reflog, and dangling objectsOrder by likelihood: git stash list (committer hid uncommitted work as a stash), git notes show (text attached to commits via refs/notes), git reflog (every HEAD movement, including reset --hard), git fsck --lost-found (unreachable blobs, last resort).bash
git stash list # uncommitted work hidden in refs/stashbashgit notes list # text attached to commits via refs/notesbashgit notes show <commit> # display a specific notebashgit reflog # every position change of HEAD/branchesbashgit fsck --unreachable 2>&1 | grep blobbashgit fsck --lost-found # dump unreachable objects as filesbashls .git/lost-found/other/bashcat .git/lost-found/other/<hash>Learn more
Git stash saves uncommitted changes as a special commit referenced by
refs/stash. Stash entries are not shown ingit logorgit branchoutput but are visible withgit stash list. Each stash entry stores both the index state and the working tree state as separate commits, linked to the stash ref.Git notes attach arbitrary text to any git object (usually commits) without modifying the object itself. Notes are stored in a special
refs/notes/commitsref and are fetched separately withgit fetch origin refs/notes/*:refs/notes/*. GitHub uses notes internally; some CI systems attach build metadata as notes.The reflog records every position change of HEAD and branch tips, including
git reset --hard,git checkout, andgit rebaseoperations. It is a local safety net that lets you recover commits even after they are no longer reachable from any branch. Reflog entries expire after 90 days by default (30 days for unreachable commits), after whichgit gccan prune them. - Step 6Inspect .git/config and working directoryCheck the git config for unusual settings and scan all working directory files for the flag.bash
cat .git/configbashfind . -type f | xargs grep -l picoCTF 2>/dev/nullLearn more
.git/configstores the repository's local configuration, including remote URLs, fetch refspecs, branch tracking relationships, and any custom git settings. On a custom git server, the.git/configmight reveal additional remote URLs, credential helpers, or custom hooks that hint at where the flag is hidden. Look for unusual[remote]sections with non-standardfetchrefspecs.Scanning all working directory files with
find . -type f | xargs grep -l picoCTFis a brute-force last resort - but it catches flags hidden in plain text files that aren't in the git history (e.g., files that were written by a git hook, created by a CI script, or placed in the repository root outside of any commit). It also catches flags in committed files thatgit log -pmight miss if the search term appears in a file that was not modified between commits.The combination of all these techniques (fetching non-standard refs, searching commit history, checking stash/notes/reflog, and scanning the filesystem) covers all possible hiding places in a git-based challenge. This systematic approach ensures nothing is missed regardless of which creative location the challenge designer chose. See the Linux CLI guide for related shell-driven enumeration patterns.
Flag
picoCTF{g1t_...}
The flag is hidden in a non-standard git ref namespace (refs/hidden/*, refs/secret/*, or refs/flag/*) on the custom Git server. Use git fetch with explicit refspecs and git for-each-ref to enumerate all refs.
How to prevent this
How to prevent this
Git servers leak more than developers realize. Treat the ref advertisement as the security boundary.
- Use
uploadpack.hideRefs(ortransfer.hideRefs) to hide internal namespaces from advertisement entirely. Combine withuploadpack.allowReachableSHA1InWant=falseso clients can't request hidden objects by hash. - Authenticate before serving any ref. Public Git over HTTPS without auth means anyone with
git ls-remotesees the full ref list. Use Gitea/Gitlab/Forgejo with per-repo ACLs, not rawgit-http-backend. - Don't use Git as a secret store. Anything pushed (even to a non-default branch, even to a deleted branch) lives in the object database forever and can be recovered with
git fsck --lost-found. Rotate exposed credentials; assume Git history is permanent.