~$ skillshelf
← OverTheWire Bandit

Bandit 16 → 17: when the server says wrong, believe it

banditnetworkingtlsopenssllinux

The last level handed me the port. This one makes me find it — somewhere in a thousand of them — and then work out which of the survivors is a real server and which are just mirrors. The scanning is the easy half. The interesting half is that an echo server will happily accept a wrong password, so almost nothing in this level can tell you when you’re carrying one.

the goal

The credentials for the next level can be retrieved by submitting the password of the current level to a port on localhost in the range 31000 to 32000. First find out which of these ports have a server listening on them. Then find out which of those speak SSL/TLS and which don’t. There is only 1 server that will give the next credentials, the others will simply send back to you whatever you send to it.

the approach

Three questions stacked, and the goal states them as three rather than one: what’s listening, which of those speak TLS, and which single one isn’t an echo server.

The tempting move is to collapse all of it into one loop — walk the range, send the password, stop as soon as something comes back that isn’t what was sent:

for ((p=31000; p<=32000; p++)); do
  nc -z -w1 localhost "$p" || continue
  resp=$(echo "$pass" | nc -w2 localhost "$p")
  if [ "$resp" = "$pass" ]; then
    echo "$p echoes"
  else
    echo "$p does NOT echo"; echo "$resp"; break
  fi
done

That break is the bug. It fires on port 31518, prints an empty response, and quits with 480 ports left unscanned. The response is empty because a TLS server handed raw plaintext can’t parse it and just closes — and empty isn’t equal to the password, so the else branch counts it as success.

“Not an echo” is a much weaker signal than it looks. It’s satisfied by the right server, by every TLS server, and by anything that hangs up on you.

Drop the break and the same loop does the job:

31046: echoes
31518: no echo (resp='')
31691: echoes
31790: no echo (resp='')
31960: echoes

Five listening ports out of a thousand, three of them plainly mirrors. One detail worth catching: 31790 also made bash print warning: command substitution: ignored null byte in input, and the other silent port didn’t. Something was coming back on 31790 — bytes that aren’t text, which is what a handshake looks like read as a string.

Two candidates, so openssl s_client to talk to them properly:

for p in 31518 31790; do
  echo "== $p =="
  echo "$pass" | openssl s_client -quiet -connect localhost:$p 2>/dev/null
done
== 31518 ==
<password>

== 31790 ==
Wrong! Please enter the correct current password.

31518 hands the string straight back — an echo server that happens to speak TLS, exactly the trap the level describes. 31790 is the real one.

And it says Wrong!, which reads like “wrong port” and isn’t. It means what it says. The password in the pass= variable had come from the previous level’s file rather than this one.

That’s the part worth sitting with: an echo server echoes whatever you send it. Every test up to that point tested the server’s behaviour, not the credential. A wrong password produces an identical result to a right one against four of the five ports. The first thing in the entire level capable of detecting a bad password is the one server that actually checks it.

Reconnecting with the correct string:

openssl s_client -quiet -connect localhost:31790
depth=0 CN=SnakeOil
verify error:num=18:self-signed certificate
<password>
Correct!
-----BEGIN OPENSSH PRIVATE KEY-----
<redacted>
-----END OPENSSH PRIVATE KEY-----

A private key rather than a password, so level 17 gets logged into with -i and a key file at 600 instead of a pasted string. (The OPENSSH PRIVATE KEY header is a container format, not an algorithm — ssh-keygen -y -f <file> is what tells you which kind of key is inside.) The self-signed CN=SnakeOil certificate throws a verify error that doesn’t matter here — nothing on this box is trying to prove it’s who it says it is.

One detour the level page explicitly warns about: without -quiet, an interactive s_client session answers the password with KEYUPDATE instead of sending it anywhere. That’s the client, not the server. s_client reads certain single letters at the start of a line as commands to itself — renegotiate, quit, key update — and this level’s password happens to begin with one of them, so the input never reaches the wire. It’s in the CONNECTED COMMANDS section of man s_client, which is what the level page means when it asks whether you’re getting DONE, RENEGOTIATING or KEYUPDATE.

the takeaway

Design the test around what a positive looks like, not what a negative doesn’t. resp != pass feels like a success condition and is really just “something unexpected happened” — empty responses, hangups and encrypted garbage all satisfy it. The moment a pass condition is defined by absence, every failure mode gets promoted to a result.

Don’t break out of an enumeration on the first hit. The whole value of a scan is the complete list; three echo servers and a null-byte warning say more about the shape of the problem than 31518 does alone. Enumerate fully, then narrow.

A credential is only verified by something that checks it. A wrong password can travel through an entire chain of steps that all accept it happily, because none of them are authenticating anything. When one end finally says Wrong!, that’s the first honest signal in the system — believe it before going looking for a different port. The authoritative copy sits on the box at /etc/bandit_pass/<user>, readable as the user you’re already logged in as.

-quiet on openssl s_client — and the general point underneath it: an interactive tool sitting between you and a socket may be reading your input before the socket ever does.