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.
wget https://artifacts.picoctf.net/c_titan/71/challenge.zip && \
unzip challenge.zip && \
cd drop-in/Solution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
List every branchObservationI noticed the challenge description mentioned a team working on separate features, which suggested the flag was split across multiple Git branches rather than hidden in a single commit or file.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.bashgit branch -aSample output:
* main feature/part-1 feature/part-2 feature/part-3 remotes/origin/main
What didn't work first
Tried: Run git log --all --oneline hoping to see every branch's commits in one list
git log --all does show commits reachable from every ref, but the output mixes commits from all branches without grouping them by branch name. You see SHA hashes and messages but still have no idea which branches exist or how many flag segments there are. git branch -a is the correct command because it lists branch names directly, making the feature/part-* naming convention immediately visible.
Tried: Run git branch without -a and assume the listed branches are everything
Without -a, git branch only shows local branches. If the feature branches were never checked out locally, only remotes/origin/feature/part-* entries exist and they are invisible in the output. You would see only main and conclude there is nothing to investigate, missing all three flag segments.
Learn more
Git branches are lightweight pointers to commits, enabling parallel lines of development. The
-aflag shows both local branches and remote-tracking branches (remotes/origin/...).The naming convention
feature/part-1follows GitFlow, where feature branches are prefixed withfeature/. 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
trufflehogandgit-secretsautomate branch-wide secret scanning.Step 2
Inspect each feature branchObservationI noticed the branch listing revealed three branches named feature/part-1, feature/part-2, and feature/part-3, which indicated that each branch held a consecutive segment of flag.py that needed to be read in order.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.bashgit checkout feature/part-1 && cat flag.pyExpected output
picoCTF{t3@mw0rk_m@k3s_th3_dr3@m_w0rk_4c2...}Repeat for feature/part-2 and feature/part-3 to gather the middle and final segments.What didn't work first
Tried: Run git show remotes/origin/feature/part-1 instead of checking out the branch first
git show on a remote-tracking ref displays only the most recent commit object and its diff, not the current state of flag.py as a readable file. You get a raw diff patch which is hard to parse when all you want is the string content. Checking out the branch and running cat flag.py gives the clean file contents directly.
Tried: Try to read flag.py while still on main without switching branches
The main branch's flag.py does not contain any flag segments - those changes exist only on the feature branches. Catting flag.py on main returns an empty file or a placeholder. You must git checkout to the specific feature branch first so the working tree reflects that branch's committed version of the file.
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 explicitgit switch <branch>is preferred for branch switching whilegit checkoutis 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.Step 3
Optional: merge for a single viewObservationI noticed that checking out each branch individually was effective but required three separate steps, which suggested that merging all feature branches into main could produce the complete flag.py in one unified file.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.bashgit checkout main && \ git merge feature/part-1 feature/part-2 feature/part-3What didn't work first
Tried: Merge all three branches at once and expect flag.py to appear cleanly with no conflicts
All three feature branches modify the same file (flag.py) in the same region, so merging them simultaneously creates three-way conflicts that Git cannot auto-resolve. The file will be left in conflict state with conflict markers (<<<<<<, =======, >>>>>>>) inserted. Reading each branch individually with git checkout and cat is faster than resolving these conflicts just to view the flag.
Tried: Merge feature/part-2 or feature/part-3 first without merging part-1 first and expect the segments to appear in correct order
Git merges do not reorder content based on branch names. Merging out of sequence means the flag segments may appear in whichever order the commits were recorded, not in the part-1, part-2, part-3 reading order. The concatenated string will be wrong or unreadable. Always read or merge in the numbered order the branch names imply.
Learn more
git mergeintegrates 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-ffflag forces a merge commit even when a fast-forward is possible, preserving branch history in the commit graph for auditing purposes.
Flag
Reveal 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.
Key takeaway
How to prevent this
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.