MY GIT picoCTF 2026 Solution

Published: March 20, 2026

Description

I have built my own Git server with my own rules!

Launch the challenge instance and note the Git server URL and port.

You'll need git installed locally.

  1. Step 1Clone the repository
    Clone the custom Git server's repository and enter the directory.
    bash
    git clone http://<HOST>:<PORT_FROM_INSTANCE>/repo.git
    bash
    cd repo
    Learn more

    git clone downloads 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 clone only fetches refs under refs/heads/ (branches) and refs/tags/ - other namespaces like refs/hidden/ or refs/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/ and refs/tags/ namespaces.

  2. Step 2List all remote refs
    Use git ls-remote to enumerate every ref the server exposes, including custom namespaces that are hidden from normal clone.
    bash
    git ls-remote origin
    Learn more

    git ls-remote queries the remote server's ref advertisement and prints every ref the server is willing to share. Unlike git clone, which only fetches standard namespaces, ls-remote shows the complete list of available refs - including non-standard namespaces like refs/hidden/, refs/secret/, refs/notes/, refs/stash, and refs/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.hideRefs git config option can hide specific ref patterns from advertisement.

  3. Step 3Fetch custom namespaces
    Glob 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/*'
    bash
    git for-each-ref refs/hidden/  # verify fetch landed
    bash
    git fetch origin 'refs/secret/*:refs/secret/*'
    bash
    git for-each-ref refs/secret/
    bash
    git fetch origin 'refs/flag/*:refs/flag/*'
    bash
    git for-each-ref refs/flag/
    bash
    git for-each-ref --format='%(refname) %(objectname)' | head -40
    Learn more

    Git's refspec syntax src:dst maps remote refs to local refs. The glob pattern refs/hidden/*:refs/hidden/* fetches all refs under the remote's refs/hidden/ namespace and stores them locally under the same name. Without an explicit refspec, git fetch only fetches what is configured in .git/config under [remote "origin"] - typically +refs/heads/*:refs/remotes/origin/*.

    git for-each-ref lists 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/*/head and refs/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.

  4. Step 4Enumerate all branches and tags
    Check every branch and tag for flag content in commit messages, file contents, and diffs.
    bash
    git branch -a
    bash
    git tag -l
    bash
    git log --all --oneline
    bash
    git log --all -p | grep -A2 picoCTF
    Learn more

    git branch -a lists both local branches and remote-tracking branches (prefixed with remotes/origin/). git log --all shows commits reachable from any ref, including all branches and tags. The -p flag adds the diff for each commit - piping this through grep -A2 picoCTF finds 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 -l lists tag names; git show <tagname> displays the tag message and the tagged object.

  5. Step 5Check stash, notes, reflog, and dangling objects
    Order 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/stash
    bash
    git notes list                       # text attached to commits via refs/notes
    bash
    git notes show <commit>              # display a specific note
    bash
    git reflog                           # every position change of HEAD/branches
    bash
    git fsck --unreachable 2>&1 | grep blob
    bash
    git fsck --lost-found                # dump unreachable objects as files
    bash
    ls .git/lost-found/other/
    bash
    cat .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 in git log or git branch output but are visible with git 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/commits ref and are fetched separately with git 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, and git rebase operations. 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 which git gc can prune them.

  6. Step 6Inspect .git/config and working directory
    Check the git config for unusual settings and scan all working directory files for the flag.
    bash
    cat .git/config
    bash
    find . -type f | xargs grep -l picoCTF 2>/dev/null
    Learn more

    .git/config stores 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/config might reveal additional remote URLs, credential helpers, or custom hooks that hint at where the flag is hidden. Look for unusual [remote] sections with non-standard fetch refspecs.

    Scanning all working directory files with find . -type f | xargs grep -l picoCTF is 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 that git log -p might 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

Git servers leak more than developers realize. Treat the ref advertisement as the security boundary.

  • Use uploadpack.hideRefs (or transfer.hideRefs) to hide internal namespaces from advertisement entirely. Combine with uploadpack.allowReachableSHA1InWant=false so clients can't request hidden objects by hash.
  • Authenticate before serving any ref. Public Git over HTTPS without auth means anyone with git ls-remote sees the full ref list. Use Gitea/Gitlab/Forgejo with per-repo ACLs, not raw git-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.

Want more picoCTF 2026 writeups?

Useful tools for General Skills

What to try next