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 1List every branch
ObservationThe description talks about a team working on separate features, which means the flag is split across Git branches rather than sitting in one commit.git branch -a lists local and remote-tracking branches. In this repo all four branches are local, and each feature/part-* branch holds one segment of the flag, so you can check them out directly.bashgit branch -aSample output:
feature/part-1 feature/part-2 feature/part-3 * 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 reach every ref, but it interleaves the commits without grouping them by branch. You get hashes and messages and still cannot tell which branches exist or how many segments there are. git branch -a lists the names directly, which makes the feature/part-* convention obvious.
Tried: Run git branch without -a and assume the listed branches are everything
This drop-in happens to carry all its branches locally, so the plain listing is complete here. That is a property of this archive, not a rule: clone a real repo and unmerged work lives only under remotes/origin/, invisible without -a. Reaching for -a by reflex costs nothing and stops you concluding that main is all there is.
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 2Inspect each feature branch
ObservationThe listing shows feature/part-1, feature/part-2, and feature/part-3. Each branch holds one consecutive segment of flag.py, to be read in that 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
print("Printing the flag...") print("picoCTF{t3@mw0rk_", end='')Repeat for feature/part-2 and feature/part-3. Each adds one more print() line holding the next segment, and the three segments joined in order give the full flag.What didn't work first
Tried: Run git show feature/part-1 instead of checking out the branch first
git show on a branch name prints that branch's tip commit and its diff, not the file as it stands. A raw patch is awkward to read when you only want the string. Either check the branch out and cat the file, or ask for the blob explicitly with git show feature/part-1:flag.py.
Tried: Try to read flag.py while still on main without switching branches
On main, flag.py is a single line, print("Printing the flag..."), with no segment attached; the segments live only on the feature branches. Check out the feature branch first so the working tree reflects its version.
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 3Optional: merge for a single view
ObservationChecking out each branch works but takes three passes. Merging the branches into main assembles the whole file in one place, at the cost of resolving conflicts by hand.Merge the branches into main one at a time, in part order. The first merge fast-forwards cleanly; part-2 and part-3 each conflict on flag.py because every branch appends to the same region. Resolve by deleting the conflict markers and keeping both print() lines in part order, then run the file to print the assembled flag.bashgit checkout main && git merge feature/part-1bashgit merge feature/part-2 # conflict in flag.py: keep both lines, part-1 firstbashgit merge feature/part-3 # same again, then python3 flag.pyWhat didn't work first
Tried: Merge all three branches in one command: git merge feature/part-1 feature/part-2 feature/part-3
That asks for an octopus merge, and the octopus strategy refuses to resolve content conflicts. Git fast-forwards to part-1, hits a conflict in flag.py on part-2, and bails out with 'Should not be doing an octopus', leaving main exactly where it started. Merge them one at a time so you get a normal conflict you can actually edit.
Tried: Resolving each conflict by picking one side (git checkout --ours or --theirs) to make the merge finish quickly
Taking one side throws the other segment away, so flag.py ends up printing a third of the flag. The conflict here is not a disagreement to arbitrate; both hunks belong, in part order. Edit the file by hand, drop the <<<<<<< markers, and keep every print() line.
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.
Interactive tools
- Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
- Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.
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.