Takeaways from PlaidCTF 2019

Very few developers are intentionally coding exploitable bugs into production software. But working code doesn’t always mean code that does what the developer intended. A few minor misunderstandings of a language’s std lib and internals can be disastrous. As a former developer I’m fascinated by the impact of language choice on security.

Exploits exist at the nexus of multiple vulnerabilities. Defensively, we use various techniques to reduce the likelihood of vulnerabilities becoming an exploit. But what happens when they are implemented incorrectly?

“Can You Guess Me” from PlaidCTF 2019 is a very concise example of the perfect storm. The key takeaway is this: sanitising inputs is great…… if you do it right

I will be ignoring the fact this was written to be intentionally vulnerable to highlight the real-world problems it showcases.

Acknowledgement

The PlaidCTF was awesome and the team who put it together did a fantastic job. I learnt a lot, tried some new things, and had loads of fun.

Working with everyone in CultofthePartyParrot made the experience all the better.

The Solution(s)

“Can You Guess Me” is a “Code Golf” challenge with a twist. By casting the input to set() before counting characters, only unique characters are counted.

Because of this, print(vars()), which appears to be too many characters, gets past the filter.

Python is a very helpful language though, so help(flag) (under the limit!) will tell you exactly what you want.

Now on to the nitty-gritty:

The Code

Since this is about coding errors, looking at the code is important. The code running the challenge was very simple:

#! /usr/bin/env python3

from sys import exit
from secret import secret_value_for_password, flag, exec

print(r"")
print(r"")
print(r"  ____         __   __           ____                     __  __       ")
print(r" / ___|__ _ _ _\ \ / /__  _   _ / ___|_   _  ___  ___ ___|  \/  | ___  ")
print(r"| |   / _` | '_ \ V / _ \| | | | |  _| | | |/ _ \/ __/ __| |\/| |/ _ \ ")
print(r"| |__| (_| | | | | | (_) | |_| | |_| | |_| |  __/\__ \__ \ |  | |  __/ ")
print(r" \____\__,_|_| |_|_|\___/ \__,_|\____|\__,_|\___||___/___/_|  |_|\___| ")
print(r"                                                                       ")
print(r"")
print(r"")

try:
    val = 0
    inp = input("Input value: ")
    count_digits = len(set(inp))
    if count_digits <= 10:          # Make sure it is a number
        val = eval(inp)
    else:
        raise

    if val == secret_value_for_password:
        print(flag)
    else:
        print("Nope. Better luck next time.")
except:
    print("Nope. No hacking.")
    exit(1)

The Problems

There are a few vulnerable pieces of code here.

Starting with the “very minor” and moving up:

unused imports

Fixing this won’t drop much risk, but for completeness sake here we go.

There is an unused import called exec.

from secret import secret_value_for_password, flag, exec

Any code on a production server is a potential liability. If it’s not being used, don’t put it in production. Some modern languages won’t even run if you include unused imports and variables.

poor sanitisation

I very strongly DO NOT recommend doing this as the code will still be vulnerable, but fixing the sanitisation could prevent exploitation. In this case, you are still only one commit away from a breach.

The poorly implemented sanitisation occurs here:

# ...snip...
count_digits = len(set(inp))
if count_digits <= 10:          # Make sure it is a number
# ...snip...

count_digits, as the comment suggests, is supposed to sanitise the input. What it actually does is cast inp, a string, to a python set and count the elements in the list. A set can only contain unique elements, so len(set("aaaaaaaaaaaaa")) is 1. This is what provides the “code-golfing” solution. It provides no benefit to the application.

Python provides a method for casting a string to an int, and will raise an exception if the string is not numeric. Had this been used, the application would not be exploitable.

using eval

Exploitable Vulnerability: using `eval()`
Unless you really want to (hint: you don't) run code you didn't write, this is bad.

# ...snip...
val = 0
# ...snip...
inp = input("Input value: ")
count_digits = len(set(inp))
if count_digits <= 10:          # Make sure it is a number
    val = eval(inp)
# ...snip...

So why write this?

eval() is used in this code to cast a string to an integer. It does actually do this. It also executes whatever string it was passed. The code passes it a string.

It is a creative way of casting a string to an int. But applying a knowledge of Python would suggest to even the most creative developer that it could have unintentional results.

Putting the fixes together

The todo-list to fix this code is:

  • remove unused imports
  • properly sanitise user input
  • remove use of eval()

That might look something like this:

from sys import exit
from secret import secret_value_for_password, flag
# ...snip...
try:
    # cast input to integer
    # ValueError raised if it's not numeric
    # this effectively implements the sanitisation from the original comment
    val = int(input("Input value: "))

    # make sure it's less than 10 digits like in the original
    # cast the int to a str for this
    if not len(str(val)) <= 10:
        raise ValueError()

    if val == secret_value_for_password:
        print(flag)
    else:
        print("Nope. Better luck next time.")
except:
    print("Nope. No hacking.")
    exit(1)