Collaborative Development picoCTF 2024 Solution

Published: April 3, 2024

Description

My team has been working very hard on new features for our flag printing program! I wonder how they'll work together?

Download the provided challenge.zip archive.

Extract it locally and move into the drop-in repository where the Git history lives.

bash
wget https://artifacts.picoctf.net/c_titan/71/challenge.zip && \
unzip challenge.zip && \
cd drop-in/
This challenge builds on the Git fundamentals from Time Machine and Commitment Issues. For file-specific investigation, see Blame Game. Each feature branch here contains a slice of the flag inside flag.py. You can either read them one at a time or merge them back into main to watch Git handle the conflicts.
  1. Step 1List every branch
    git branch -a lists local and remote-tracking branches. Each feature/part-* branch holds one segment of the flag, and you can check them out directly.
    bash
    git branch -a

    Sample output:

    * main
      feature/part-1
      feature/part-2
      feature/part-3
      remotes/origin/main
    Learn more

    Git branches are lightweight pointers to commits, enabling parallel lines of development. The -a flag shows both local branches and remote-tracking branches (remotes/origin/...).

    The naming convention feature/part-1 follows GitFlow, where feature branches are prefixed with feature/. Other common conventions: feat/, issue numbers (issue-123), or author initials. Consistent naming makes intent obvious at a glance.

    In security research, inspecting every branch can reveal information that was accidentally committed to a feature branch and never merged. Credentials, API keys, and partial implementations sometimes hide in abandoned branches. Tools like trufflehog and git-secrets automate branch-wide secret scanning.

  2. Step 2Inspect each feature branch
    Checkout each branch and read flag.py. Each branch's flag.py defines or prints one segment of the flag; concatenate the three segments in order (part-1, part-2, part-3) to assemble the full flag.
    bash
    git checkout feature/part-1 && cat flag.py
    Repeat for feature/part-2 and feature/part-3 to gather the middle and final segments.
    Learn more

    git checkout <branch> switches your working tree to a different branch, updating all tracked files to match that branch's latest commit. In modern Git (2.23+), the more explicit git switch <branch> is preferred for branch switching while git checkout is reserved for file restoration.

    Splitting a secret across branches demonstrates a real security concern: data spread across feature branches is easy to miss compared to a single committed secret. Automated secret scanners need to check every branch, not just the default branch.

    Reading the branches one at a time avoids merge conflicts entirely. If you do try to merge them, expect conflicts where parts overlap or modify the same line of flag.py. Resolving by reading is faster than resolving by merging.

  3. Step 3Optional: merge for a single view
    If you prefer one unified output, merge the feature branches into main (in any order) and resolve the minor conflicts. The combined flag prints once the merges complete.
    bash
    git checkout main && \
    git merge feature/part-1 feature/part-2 feature/part-3
    Learn more

    git merge integrates changes from one or more branches into the current branch. When multiple branches modify the same file in the same location, Git creates a merge conflict - it marks the conflicting sections and requires a human to choose how to resolve them. This is a fundamental part of collaborative development.

    Git uses a three-way merge algorithm: it finds the common ancestor commit of both branches and compares each branch's changes against that ancestor. Changes that don't overlap are merged automatically; overlapping changes become conflicts. Understanding this algorithm helps predict when conflicts will occur and how to structure commits to minimize them.

    In production workflows, merges are often done via pull requests on platforms like GitHub or GitLab, which add code review, CI/CD checks, and discussion threads before the merge happens. The --no-ff flag forces a merge commit even when a fast-forward is possible, preserving branch history in the commit graph for auditing purposes.

Related guides

Linux Command Line Basics for CTF

git, cat, and the rest of the shell tools used here are covered in the Linux CLI guide alongside the other commands that appear across General Skills challenges.

Flag

picoCTF{t3@mw0rk_m@k3s_th3_dr3@m_w0rk_4c2...}

Concatenate the outputs from each feature/part-* branch (or resolve the merged file) to reveal the full flag above.

How to prevent this

Feature branches are public the moment they are pushed. Treat them as production from a secrecy standpoint.

  • Branch protection on every shared repo: require PR review, require CI to pass, block force-push to main. Pre-receive hooks (gitleaks, GitHub Push Protection) reject pushes containing secrets.
  • Split secret material across collaborators only when there is an operational reason (Shamir secret sharing, MPC). Splitting flags across branches as in this challenge is theatrical; secrets in any branch should be assumed compromised.
  • Educate the team: deleted branches and abandoned forks still hold the data. The only durable fix once a secret is committed is rotation. Make rotation easy enough that people will actually do it.

Want more picoCTF 2024 writeups?

Useful tools for General Skills

Related reading

What to try next