Bandit 16 → 17: when the server says wrong, believe it
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.