Exploitable Vulnerability
Exploitable Vulnerability: using
Unless you really want to (hint: you don’t) run code you didn’t write, this is bad.
eval()
Unless you really want to (hint: you don’t) run code you didn’t write, this is bad.
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.
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 writeup is available in a different post.
“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:
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)
There are a few vulnerable pieces of code here.
Starting with the “very minor” and moving up:
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.
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.
# ...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.
The todo-list to fix this code is:
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)