┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
MY GIT — picoCTF 2026
~ Imattas aka Zemi
 Category: General Skills
 Difficulty: Easy
 Points: 50
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

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

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

This challenge involves interacting with a custom Git server that has
non-standard behavior. The "my own rules" phrasing suggests that the server may:

1. Have custom Git hooks that enforce specific rules (pre-receive, post-receive,
update hooks)
2. Use non-standard branch naming or require specific operations
3. Hide the flag in a non-obvious location within the repo (branches, tags,
commit messages, notes, stash, or git objects)
4. Require specific Git commands to extract the flag (e.g., git log, git branch
-a, git tag, git notes, git reflog)

:: Strategy:
1. Clone the repository from the provided URL
2. Enumerate all refs: branches, tags, notes
3. Search the entire commit history
4. Check for hidden or unusual objects

────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

The flag is hidden somewhere non-obvious in the Git repository's history or
metadata. Common patterns in picoCTF Git challenges:

- Flag split across multiple branches
- Flag hidden in old commits (use git log --all)
- Flag in a deleted branch (use git reflog or git fsck)
- Flag embedded in commit messages or author fields
- Flag stored as a git note or tag annotation
- Custom server that requires specific HTTP/SSH interactions

The key insight is that a standard git clone may not fetch everything — custom
ref namespaces, notes, dangling objects, and reflog entries must be enumerated
explicitly.

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

:: Step 1: Clone the repository
-- bash --
# The challenge typically provides a URL like:
git clone <challenge_url>
cd <repo_name>
:: Step 2: Enumerate all branches
-- bash --
git branch -a
# Check all branches, including remote-tracking branches
git branch -r
:: Step 3: Check all commit history
-- bash --
# View all commits across all branches
git log --all --oneline --graph

# Search commit messages for the flag
git log --all --format="%H %s" | grep -i "flag\|pico\|secret"

# Search commit diffs for the flag
git log --all -p | grep "picoCTF{"
:: Step 4: Check tags
-- bash --
git tag -l
# Show all tag details (annotated tags may contain the flag)
git tag -l | xargs -I{} git show {}
:: Step 5: Check git notes
-- bash --
git notes list
git log --show-notes='*'
:: Step 6: Check stash
-- bash --
git stash list
git stash show -p
:: Step 7: Check for dangling objects
-- bash --
git fsck --unreachable --no-reflogs
# Examine each dangling object
git fsck --lost-found
cat .git/lost-found/other/*
:: Step 8: Check reflog
-- bash --
git reflog --all
:: Step 9: Explore custom server behavior
-- bash --
# Try listing remote refs directly
git ls-remote origin

# Try fetching all refs including non-standard ones
git fetch origin '+refs/*:refs/remotes/origin/*'

# Check for custom refs
git for-each-ref
:: Step 10: Check hidden refs or custom namespaces
-- bash --
# Some servers use custom ref namespaces
git ls-remote origin | grep -v "HEAD\|main\|master"

# Fetch everything
git fetch --all --tags --prune

# Check .git/config for unusual remote configurations
cat .git/config
────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
MY GIT - picoCTF 2026
Category: General Skills | Points: 50

Interacts with a custom Git server to find a hidden flag.
This script clones the repo and performs exhaustive enumeration of
branches, tags, commits, notes, stash, and dangling objects.

Usage:
    python3 solve.py <git_clone_url>
    python3 solve.py https://challenge.picoctf.org/repo.git
"""

import subprocess
import sys
import os
import re
import tempfile

FLAG_PATTERN = re.compile(r"picoCTF\{[^}]+\}")


def run(cmd, cwd=None, timeout=30):
    """Run a shell command and return stdout + stderr combined."""
    try:
        result = subprocess.run(
            cmd, shell=True, capture_output=True, text=True,
            cwd=cwd, timeout=timeout
        )
        return (result.stdout + "\n" + result.stderr).strip()
    except subprocess.TimeoutExpired:
        return ""


def search_for_flag(text):
    """Search text for picoCTF flag pattern."""
    return FLAG_PATTERN.findall(text)


def clone_repo(url, dest):
    """Clone the git repository."""
    print(f"[*] Cloning {url}...")
    output = run(f"git clone {url} {dest}")
    print(f"    {output}")

    if not os.path.exists(os.path.join(dest, ".git")):
        # Try with --mirror for bare repos
        print("[*] Trying mirror clone...")
        output = run(f"git clone --mirror {url} {dest}/.git")
        if os.path.exists(os.path.join(dest, ".git")):
            run("git config --bool core.bare false", cwd=dest)
            run("git checkout", cwd=dest)

    return os.path.exists(os.path.join(dest, ".git"))


def enumerate_repo(repo_dir):
    """Exhaustively enumerate a git repo for flags."""
    all_flags = []

    # ── 1. List remote refs ──
    print("\n[*] Step 1: Listing remote refs...")
    output = run("git ls-remote origin 2>/dev/null", cwd=repo_dir)
    print(f"    Remote refs:\n{output}")
    all_flags.extend(search_for_flag(output))

    # Fetch ALL refs (including non-standard namespaces)
    print("[*] Fetching all refs...")
    run("git fetch --all --tags --prune 2>/dev/null", cwd=repo_dir)
    run("git fetch origin '+refs/*:refs/remotes/origin/*' 2>/dev/null", cwd=repo_dir)
    # Some challenges hide refs under custom namespaces
    run("git fetch origin '+refs/hidden/*:refs/hidden/*' 2>/dev/null", cwd=repo_dir)
    run("git fetch origin '+refs/secret/*:refs/secret/*' 2>/dev/null", cwd=repo_dir)
    run("git fetch origin '+refs/flag/*:refs/flag/*' 2>/dev/null", cwd=repo_dir)

    # ── 2. List all branches ──
    print("\n[*] Step 2: Enumerating branches...")
    output = run("git branch -a", cwd=repo_dir)
    print(f"    Branches:\n{output}")
    branches = [b.strip().lstrip("* ").strip() for b in output.split("\n") if b.strip()]

    # ── 3. Check each branch ──
    print("\n[*] Step 3: Checking each branch...")
    for branch in branches:
        branch_name = branch.replace("remotes/origin/", "").replace("remotes/", "")
        if "HEAD" in branch:
            continue

        # Log with patches
        log_output = run(f"git log {branch} --format='%H|%an|%ae|%s' 2>/dev/null", cwd=repo_dir)
        flags = search_for_flag(log_output)
        if flags:
            print(f"    [+] Flag in commit metadata on {branch}: {flags}")
            all_flags.extend(flags)

        # Check actual file contents at branch tip
        show_output = run(f"git show {branch}: 2>/dev/null", cwd=repo_dir)
        # List and show all files on this branch
        ls_output = run(f"git ls-tree -r --name-only {branch} 2>/dev/null", cwd=repo_dir)
        if ls_output:
            for filepath in ls_output.split("\n"):
                filepath = filepath.strip()
                if filepath:
                    content = run(f"git show {branch}:{filepath} 2>/dev/null", cwd=repo_dir)
                    flags = search_for_flag(content)
                    if flags:
                        print(f"    [+] Flag in {branch}:{filepath}: {flags}")
                        all_flags.extend(flags)

        # Check diffs
        diff_output = run(f"git log {branch} -p 2>/dev/null", cwd=repo_dir)
        flags = search_for_flag(diff_output)
        if flags:
            print(f"    [+] Flag in diff on {branch}: {flags}")
            all_flags.extend(flags)

    # ── 4. Check tags ──
    print("\n[*] Step 4: Checking tags...")
    tags_output = run("git tag -l", cwd=repo_dir)
    if tags_output:
        print(f"    Tags: {tags_output}")
        for tag in tags_output.split("\n"):
            tag = tag.strip()
            if tag:
                show = run(f"git show {tag} 2>/dev/null", cwd=repo_dir)
                flags = search_for_flag(show)
                if flags:
                    print(f"    [+] Flag in tag {tag}: {flags}")
                    all_flags.extend(flags)
    else:
        print("    No tags found")

    # ── 5. Check notes ──
    print("\n[*] Step 5: Checking git notes...")
    # Fetch notes
    run("git fetch origin 'refs/notes/*:refs/notes/*' 2>/dev/null", cwd=repo_dir)
    notes_output = run("git notes list 2>/dev/null", cwd=repo_dir)
    if notes_output:
        print(f"    Notes: {notes_output}")
        for line in notes_output.split("\n"):
            parts = line.strip().split()
            if len(parts) >= 2:
                note_content = run(f"git notes show {parts[1]} 2>/dev/null", cwd=repo_dir)
                flags = search_for_flag(note_content)
                if flags:
                    print(f"    [+] Flag in note: {flags}")
                    all_flags.extend(flags)

    # Also check all notes namespaces
    notes_log = run("git log --show-notes='*' --all 2>/dev/null", cwd=repo_dir)
    flags = search_for_flag(notes_log)
    if flags:
        print(f"    [+] Flag in notes log: {flags}")
        all_flags.extend(flags)

    # ── 6. Check stash ──
    print("\n[*] Step 6: Checking stash...")
    stash_output = run("git stash list 2>/dev/null", cwd=repo_dir)
    if stash_output:
        print(f"    Stash: {stash_output}")
        stash_diff = run("git stash show -p 2>/dev/null", cwd=repo_dir)
        flags = search_for_flag(stash_diff)
        if flags:
            print(f"    [+] Flag in stash: {flags}")
            all_flags.extend(flags)
    else:
        print("    No stash entries")

    # ── 7. Check reflog ──
    print("\n[*] Step 7: Checking reflog...")
    reflog_output = run("git reflog --all 2>/dev/null", cwd=repo_dir)
    if reflog_output:
        flags = search_for_flag(reflog_output)
        if flags:
            print(f"    [+] Flag in reflog: {flags}")
            all_flags.extend(flags)

    # ── 8. Check dangling/unreachable objects ──
    print("\n[*] Step 8: Checking dangling objects...")
    fsck_output = run("git fsck --unreachable --no-reflogs 2>/dev/null", cwd=repo_dir)
    if fsck_output:
        for line in fsck_output.split("\n"):
            hash_match = re.search(r"([0-9a-f]{40})", line)
            if hash_match:
                obj_hash = hash_match.group(1)
                content = run(f"git cat-file -p {obj_hash} 2>/dev/null", cwd=repo_dir)
                flags = search_for_flag(content)
                if flags:
                    print(f"    [+] Flag in dangling object {obj_hash}: {flags}")
                    all_flags.extend(flags)

    # Also use lost-found
    run("git fsck --lost-found 2>/dev/null", cwd=repo_dir)
    lost_dir = os.path.join(repo_dir, ".git", "lost-found")
    if os.path.exists(lost_dir):
        for root, dirs, files in os.walk(lost_dir):
            for f in files:
                fpath = os.path.join(root, f)
                try:
                    content = open(fpath).read()
                    flags = search_for_flag(content)
                    if flags:
                        print(f"    [+] Flag in lost-found {f}: {flags}")
                        all_flags.extend(flags)
                except:
                    pass

    # ── 9. Check for-each-ref (all refs including custom) ──
    print("\n[*] Step 9: Checking all refs...")
    refs_output = run("git for-each-ref --format='%(refname) %(objectname)' 2>/dev/null", cwd=repo_dir)
    if refs_output:
        for line in refs_output.split("\n"):
            parts = line.strip().split()
            if len(parts) >= 2:
                ref_name, obj_hash = parts[0], parts[1]
                content = run(f"git cat-file -p {obj_hash} 2>/dev/null", cwd=repo_dir)
                flags = search_for_flag(content)
                if flags:
                    print(f"    [+] Flag in ref {ref_name}: {flags}")
                    all_flags.extend(flags)

    # ── 10. Check .git/config and description ──
    print("\n[*] Step 10: Checking .git metadata files...")
    for meta_file in ["config", "description", "info/refs", "packed-refs", "COMMIT_EDITMSG"]:
        fpath = os.path.join(repo_dir, ".git", meta_file)
        if os.path.exists(fpath):
            try:
                content = open(fpath).read()
                flags = search_for_flag(content)
                if flags:
                    print(f"    [+] Flag in .git/{meta_file}: {flags}")
                    all_flags.extend(flags)
            except:
                pass

    # ── 11. Brute-force: check ALL files in working directory ──
    print("\n[*] Step 11: Searching working directory files...")
    for root, dirs, files in os.walk(repo_dir):
        # Skip .git directory internals (objects, etc.)
        if "/.git/objects" in root or "/.git/hooks" in root:
            continue
        for f in files:
            fpath = os.path.join(root, f)
            try:
                content = open(fpath, errors="replace").read()
                flags = search_for_flag(content)
                if flags:
                    rel_path = os.path.relpath(fpath, repo_dir)
                    print(f"    [+] Flag in {rel_path}: {flags}")
                    all_flags.extend(flags)
            except:
                pass

    return list(set(all_flags))


def main():
    if len(sys.argv) < 2:
        print("Usage: python3 solve.py <git_clone_url_or_repo_path>")
        print("")
        print("Examples:")
        print("  python3 solve.py https://challenge.picoctf.org/repo.git")
        print("  python3 solve.py /path/to/cloned/repo")
        sys.exit(1)

    target = sys.argv[1]

    # Check if target is a URL or existing directory
    if os.path.isdir(target):
        repo_dir = os.path.abspath(target)
        print(f"[*] Using existing repo: {repo_dir}")
    else:
        # Clone from URL
        repo_dir = tempfile.mkdtemp(prefix="my_git_")
        success = clone_repo(target, repo_dir)
        if not success:
            print("[-] Failed to clone repository")
            print("[*] Trying alternative clone methods...")

            # Try HTTP(S) if SSH failed, or vice versa
            alt_url = target.replace("git@", "https://").replace(":", "/", 1)
            if "https://" not in target:
                alt_url = f"https://{target}"
            success = clone_repo(alt_url, repo_dir)

            if not success:
                print("[-] All clone attempts failed")
                sys.exit(1)

    # Enumerate the repo
    print(f"\n{'=' * 60}")
    print(f"Enumerating repository: {repo_dir}")
    print(f"{'=' * 60}")

    flags = enumerate_repo(repo_dir)

    # Results
    print(f"\n{'=' * 60}")
    print("RESULTS")
    print(f"{'=' * 60}")

    if flags:
        for flag in flags:
            print(f"[+] FLAG: {flag}")
    else:
        print("[-] No flags found automatically.")
        print("    Additional manual steps:")
        print("    1. Try interacting with the server directly:")
        print("       git push origin main  (check server response)")
        print("    2. Check for server-side hooks:")
        print("       git push origin HEAD:refs/heads/test")
        print("    3. Try pushing specific content:")
        print("       echo 'flag' > test.txt && git add . && git commit -m 'test' && git push")
        print("    4. Check the challenge description for specific server rules")
        print("    5. Try accessing the server via web browser (may have a web interface)")
        print(f"\n    Repo preserved at: {repo_dir}")


if __name__ == "__main__":
    main()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- A "custom Git server with my own rules" hides the flag somewhere a plain git
clone won't surface — enumerate every ref, object, and namespace.
- Search beyond the working tree: all branches (git branch -a, git log --all),
tags and tag annotations, git notes, stash, reflog, and dangling/unreachable
objects (git fsck, git cat-file -p).
- Custom servers may expose refs under non-standard namespaces — fetch with
explicit refspecs (+refs/*:..., refs/hidden/*, refs/secret/*) and inspect with
git ls-remote / git for-each-ref.
- Flags are commonly embedded in commit messages, author/email fields, or commit
diffs — grep both metadata and patches.
- Tools used: git (clone, branch, log, tag, notes, stash, reflog, fsck,
cat-file, ls-remote, for-each-ref, fetch).