~$ skillshelf
← OverTheWire Bandit

Bandit 29 → 30: the branches you weren't shown

banditlinuxgit

Third git level in a row off an identical level page. The clone works, the README is there, and where the password should be there’s a sentence instead:

- password: <no passwords in production!>

Last level taught me the working tree isn’t everything and sent me into the history. This level punishes exactly that reflex — the history is clean, and what I needed was never hidden, just never listed.

the goal

There is a git repository at ssh://bandit29-git@localhost/home/bandit29-git/repo via the port 2220. The password for the user bandit29-git is the same as for the user bandit29. From your local machine (not the OverTheWire machine!), clone the repository and find the password for the next level.

Word for word the last two apart from the username. localhost still means whichever machine runs the command, so the host swap from 27 → 28 still applies.

the approach

reading the file as a sentence

<no passwords in production!> looks like a malfunction, the same way xxxxxxxxxx did the level before. It isn’t. No command failed and nothing was suppressed — it’s a sentence a person typed into a README and committed on purpose.

Read as a sentence, it says something specific. It does not say there is no password. It says the password is not in production — a claim about location, not existence. Someone naming one environment implies there are others.

dead end: the history

Worth checking, since it solved the level before:

git log
commit b607fba0c867d0bbdf4b4a5e62cd04b79c8fea83 (HEAD -> master, origin/master, origin/HEAD)
    fix username

commit 84c16f8255d7a07c45b96a1780234ed20f7457b7
    initial commit of README.md

Two commits. git show on the tip:

    fix username

-- username: bandit29
+- username: bandit30
 - password: <no passwords in production!>

The username line, and nothing else. The password line is identical on both sides of the diff, so it was the placeholder in the first commit and the placeholder in the last. The secret was never in this branch’s history at all — a real elimination, and it stops you re-running last level’s technique.

“there’s only one branch”

git branch
* master

One branch, called master — which looks like it kills the “other environments” idea. It doesn’t. git branch shows local branches, and a fresh clone creates exactly one of those no matter how many the remote had. Everything else arrives as a remote-tracking ref, which the default output never mentions.

The evidence is on the first line of that git log:

(HEAD -> master, origin/master, origin/HEAD)

master and origin/master. Two names, listed separately, because they’re two different refs — mine, and the record of the remote’s. Git wouldn’t print both if they were the same thing.

the branches

git branch -r
  origin/HEAD -> origin/master
  origin/dev
  origin/master
  origin/sploits-dev

dev and sploits-dev, downloaded during the clone, on disk the entire time, never listed because nobody asked. Nothing was fetched here — this was a display problem from the start, not a download problem.

dead end: refs aren’t paths

remotes/origin/dev looks like a directory, and isn’t one:

cat remotes/origin/dev
cat: remotes/origin/dev: No such file or directory
git checkout remotes/origin/de
error: pathspec 'remotes/origin/de' did not match any file(s) known to git

It’s a ref name with slashes in it, not a path on disk. And git checkout reads an argument it can’t resolve as a pathspec, which is why the error talks about files when what it got was a mistyped branch.

Spelled correctly it resolves:

git checkout remotes/origin/dev
Note: switching to 'remotes/origin/dev'.
You are in 'detached HEAD' state. ...
HEAD is now at 0bf8160 add data needed for development

Detached HEAD sounds like a problem and isn’t one here. Checking out a remote-tracking ref points you at a commit rather than a local branch, so there’s nothing to move if you commit. Reading is all this needs.

the file

ls
code  README.md

A code/ directory that wasn’t on master, holding gif2ascii.py — one byte, empty, a decoy. The README is the one that matters:

cat README.md
## credentials

- username: bandit30
- password: <redacted>

Plain text, no history digging, no diff, on a branch that was in the clone the whole time.

the takeaway

git branch lists local branches, and cloning creates exactly one. Everything else the remote had comes down with the clone and stays invisible until you ask with -r or -a. Concluding “this repo only has one branch” from bare git branch is concluding something the command never told you.

Same shape three times across two levels, and this is the pair’s real lesson:

  • the working tree isn’t everything the repo has → the history
  • the history of one branch isn’t everything the repo has → other branches
  • and what a command prints by default isn’t everything it knows

None of those were missing data. All three were on disk already, and the fix each time was a flag or a different subcommand, never another fetch.

Two smaller bricks:

Read the file, don’t diagnose it. <no passwords in production!> and xxxxxxxxxx both read like malfunctions and were both messages. A committed string is something a human chose to write, and “not in production” is a person telling you which environment they didn’t put it in. That’s a lead.

A ref is not a path. remotes/origin/dev has slashes and looks like a directory, but it names a reference inside git’s ref namespace — no such directory exists in the working tree. When git checkout says pathspec did not match any files, it usually means it couldn’t resolve what you typed as a ref and fell back to reading it as a filename. Nine times out of ten that’s a typo in a branch name, not a missing file.