rwthCTF 2012 Write-Up: ezpz

December 8, 2012

TLDWTR (Too long, don’t want to read): Great service, many little bugs, great CTF! Otherwise: please continue reading 🙂

Although it was quite some time ago, I wanted to do a complete write-up on ezpz as I liked the service quite a bit 😉 There is one from Team Lobotomy and one from Fluxfingers, but they both only cover one problem in the services.

The service in the CTF came together with a client, which made things very simple because we didn’t have to copy the whole protocol implementation. Looking at the code, what jumps out quite quickly is the @expose keyword before almost all of the methods in the server itself. So, where are the flags hidden in this service? Looking at initstore tells us a bit more

def initstore(self):
  self.store = rage.BTree()
  self.store.open('./ezdata', rage.BDBOWRITER | rage.BDBOCREAT)
  self.store['priv'] = self.priv
  self.store['pub'] = self.pub

Looking up “rage.BTree()” on Google did not help and the import stated

import rwthctfstore.btree as rage

That also did not help that much. So, finally I tried took to look at the MIME type of the created file which revealed its type as “ezdata: Tokyo Cabinet (1.0:910), B+ tree”. Ok, googling for those keywords leads us to the python-tokyocabinet project on Google Code, which reveals the important functions like fwmkeys, which appearantly searches for keys in the tree. This method was called in 3 different functions of which two were exposed.

@expose
def list(self, kw):
  r = self.store.fwmkeys(kw, 50)
  return random.sample(self.store.fwmkeys(kw), min(20, len(r)))

and

@expose
def admin_check(self, kw, count, conn):
  # authorization
  if conn.pubkey == self.pub:
    return self.store.fwmkeys(kw, count)
  else:
    return 'Denied.'

As I worked on the flightprocess service in ruCTF, the first thought that came to mind was “what about search with an empty string as the key?”. So, I tried first to use the list function to get some keys. TBH, at this point I did not yet notice the admin_check function as this sufficed for now. So, there goes our first vuln and first exploit – just connect to the service, call the list function with an empty string as parameter (via RPC) and get a list of flags. Afterwards, call the function get with the IDs (they had the form of item_123456 and we only needed 123456, so cut off the item_ part) and get flags! Woohuu! 🙂

In the CTF, we fixed this very crudely as follows:

@expose
def list(self, kw):
  print kw
  r = None
  if (kw == "" or (kw.startswith("item") and not re.match("item_[0-9a-f]{8}", kw))):
    print "HACK!"
    r = self.store.fwmkeys("NONONO",50)
  else:
    r = self.store.fwmkeys(kw, 50)
  return random.sample(self.store.fwmkeys(kw), min(20, len(r)))

In hindsight, we would have been better off just checking the length of the return value from random.sample as the gameserver only ever looked for one specific entry. However, this kept people out at this point. The next vuln we discovered was a combination of the evaluate function and the put function. The evaluate function allowed for code execution using a secure computation wrapper which disallowed a couple of functions. It basically received a file from the client, wrote it to a temporary file and executed the code afterwards. The problem herein mostly lied in the fact that any file could be opened for writing. By default the put function which is shown below logged data into a predefined logfile called ./log/ezlog.

@expose
def put(self, k, v):
  self.store.put('item_{0}'.format(k), str(v))
  print >>elog, 'item_{0}'.format(k), '->', str(v)
  self.keeptrack(v)
  return True

The easy exploit here was to open the file for reading and get all the flags this way. We fixed this by just renaming the logfile. Although this was far from secure as a targeted attack might open our ezpz.py first to find the path to the logfile, this worked against all the automated exploits during the CTF 😉 A vuln that is very closely affiliated is the fact that the gameserver every now and then used the evaluate function to put a flag to /tmp/flagout and read the content into the BTree afterwards. However, this file was not deleted, so this could also be stolen. To fix this, just make sure to delete /tmp/flagout after each call to evaluate is finished. This at least limits the chances for any team go grab the flag unless they have a race condition and connect while the server is still connected…
So, with out hesitation, lets move to the next vulns. Looking at proto.py we find a function handle_ezpz which dispatched the functions in the main ezpz.py. On line 62, we find the following:

if 'last' in params: params.append(self.ezpz.latest)

What this does is basically append the value latest which actually contains the set of latest values added to the database. So, all the need to do now is to find a method that accepts more than one parameter which outputs back to the client. A quick look shows us

@expose
def hello(self, msg, msg2=''):
  return 'hello {0}{1}'.format(msg, msg2)

which usually just says hello to the client and has an optional second parameter. So let’s call hello with the parameter “last” and get flaaaaaaaaags. We fixed this by just commenting out the line in proto.py.

Now, lets get back to the admin_check function. We saw earlier that it somehow checks the public key of the the connection against the public key and returns the all flag IDs if asked nicely 😉 We never got to this part in the CTF and only came up with a solution afterwards. The first question here is: how can we get the public key of the service itself? That  answer is very easy – it is stored in the BTree in initstore and is returned when whois is called. So the exploit here is to download the public key via whois, call register with the newly received key… and we pass the admin_check. However, having a basic understanding of cryptography, something is phishy here. Why is there a public key that can be set by the client – without providing some proof that he actually has the private key? So, we find a find a function called verify in proto.py which appearantly checks the private key. However, at the end of it, the code is as follows:

if hashed == should or DEBUG:
  return True
return False

Well, guess what – the DEBUG mode is enabled. So, lets remove that and thereby effectively disable the admin_check – another possibility, which actually works better, is to remove the admin_check function as such. So, why is that if we can enforce the private key checking? Well, we can retrieve the private key!

Although it is generated each time the service starts, it is stored in the BTree (see initstore above). Let’s take a look at the get function.

@expose
def get(self, kw, kw2=None):
  return self.store.get(kw2 or 'item_{0}'.format(kw))

The function can either be called with the ID of a flag and then searches for item_ID or directly get any entry by passing a second parameter. So, we can recover the private key by calling get(“bla”,”priv”) and use this in the rest of the conversation to authenticate our packets.

So far, we have seen a lot of exploits of this service. However, we are not yet done! 🙂 I only came up with this days after the CTF when talking a second look at the source. I will omit most part of the code, you can look it up in ezprocess.py. The important part is in the handle_pipecommand function.

def handle_pipecommand(self, p, line):
  ....
  elif line.startswith('put'):
    cmd, key = line.split(' ', 1)
    key = key.strip()
    try:
      content = open(self.errput.strip(), 'r').read()
      self.fns.put(key, content)
      p.stdin.write('put ok!')
    except:
      p.stdin.write('put fail!')
      traceback.print_exc()

Looking at the code in ezprocess, we see that errput is filled up with all data that is sent to STDERR. So, what this basically does is try and open a file whose names is denoted on STDERR and read this to the BTree under a key we can control. So, just for the fun of it, why not the fun of it, read either the logfile, /tmp/flagout, the BTree itself or a patched ezpz from another team? This was tricky to patch and could only be done by expecting the gameserver to only read /tmp/flagout. In this case, we could have matched the filename against some expression to ensure that the attacker could not read abitrary files.

TLDR

As usual, we discovered most of the vulns after the CTF 😉 However, I really liked the service very much with all its little bugs and thus spend some time on the write-up. Thanks again to the guys from RWTH for the great CTF and a big f*ck you to the guys who thought forkbombs are fun 🙂

Leave a Reply




Get Adobe Flash player