Intigriti’s Flask Challenge Breakdown

Let’s see what the dev’s have cooked up at Intigriti today! A damn vulnerable & broken Flask application. Let’s hack it for Fun & Learning!

SecurityGOAT
11 min readAug 13, 2021

Introduction

As usual, Intigriti’s devs had been working hard after the last defeat:

And this time, not only did they fixed all their previous issues like using POST requests for getting the credentials instead of GET (so that the web server logs, if leaked might not expose those!), and they added the credentials to the database, and wait for it… they got rid of PHP!! Hooray!

So now you must be thinking that they are safe! Right?

Or are they?

Let’s talk about it!

The Issues

Here’s the commented version of the code:

Now you can see what all is happening and thus be in a much better position to follow up on the stuff that I would be writing out!

Prep Work — a long, intimidating journey

In case you wish to follow along, here’s the code:

from flask import Flask, request
import sqlite3
app = Flask(__name__)@app.route("/authenticate", methods = ["POST"])
def authenticate():
# Get the name & password from the request (submitted via a form)
name = requests.form.get("name", "")
password = requests.form.get("password", "")
# Execute SQL query.
# Password isn't hashed... Quite bad!
sqlite3.execute("SELECT name FROM login WHERE password = ?" +
"AND name = ?", (password, name))
# Fetch all the maching records!
username = sqlite3.fetchall()
# No check on if more than one records are returned (which could be an easy indicator of an SQLi,
# not always, but a good one to have)
# Attacker can pull only 1 record but again, only if they know such checks are there ;)
# But the gain won't be that much, just the usernames!# If the record exists, let's get the welcome message...
if username:
# Use of f-strings - Second order SQLi!
# If a stored SQLi payload in the 'name' field finds its way in the database, the following
# query would result in an SQLi!
sqlite3.execute(f"SELECT message FROM welcome_messages WHERE name = {username}")
# Returning all records... not good again!
return sqlite3.fetchall()
# Password not matched! So we will not give back anything. Well done devs!
else: return "You're not logged in"
if __name__ == "__main__":
# Let's run this application on localhost:8080 with debugging enabled for our ease!
app.run(host = "127.0.0.1", port = 8080, debug = True)

So this should be the code and you can install Flask and then run it!

Ofcourse it won’t work because you would need to have the sqlite3 database as well!

To be honest, I didn’t had any plans to add this but I felt it might be good to add so that if some of you are curious then you all can try it out!

Had to take some pains, but I hope you all make it worthwhile :)

So, here’s the code, just for you all, my curious friends:

import sqlite3def create(dbConnection):# Create login table
query = "CREATE TABLE login (name TEXT, password TEXT)"
dbConnection.execute(query)
# Create welcome messages table
query = "CREATE TABLE welcome_messages (name TEXT, message TEXT)"
dbConnection.execute(query)
def insert(dbConnection):# Let's add a bunch of name-password pairs
loginInfo = [
('admin', 'admin'),
('zucks', 'dadada'),
('root', 'toor'),
('SecurityGOAT', 'Is_The_Best')
]
# We'll execute this query to insert all the pairs at once, using the executemany function!
query = "INSERT INTO login VALUES (?, ?)"
dbConnection.executemany(query, loginInfo)
welcomeMessages = [
('admin', 'Hola! Admin'),
('zucks', 'Welcome to Facebook'),
('root', 'The louder you become, the more you get heard ;)'),
('SecurityGOAT', 'On my way of becoming a G.O.A.T!')
]
# Let's insert all the welcome messages too...
query = "INSERT INTO welcome_messages VALUES (?, ?)"
dbConnection.executemany(query, welcomeMessages)
def select(dbConnection):# Getting the cursor, so we can read back the response
# returned by the query!
cursor = dbConnection.cursor()
query = "SELECT * FROM login"
cursor.execute(query)
print ("Login Details:", cursor.fetchall())query = "SELECT * FROM welcome_messages"
cursor.execute(query)
print ("Welcome Messages:", cursor.fetchall())# Let's connect to the DB - in the file: /tmp/users.db
dbConnection = sqlite3.connect("/tmp/users.db")
# Create the tables: login & welcome_messages
create(dbConnection)
# Add data to db
insert(dbConnection)
# Save the changes
dbConnection.commit()
# Let's see if things worked!
select(dbConnection)
# Let's close the db connection
dbConnection.close()

Run it and get the database generated. The SQLite database would be the file: “/tmp/users.db”.

If you read this code, then you must have known by now that sqlite3 doesn’t has any execute function!

So the code won’t even execute (laughing my ass off right now!)

And this is how Intigriti fooled you before April Fool’s day!

Another fool’s surprise is that Flask doesn’t has requests object! It is request.

So indeed the code won’t run y’all. You’ve been fooled. Not once but twice!!

Anyways, if you still assumed that the code would run, why not take a look at it!

But first things first… Let’s see the proper Python code (Flask app) with the fixed data retrieval code! (Intigiriti devs, please check the server logs :)

from flask import Flask, request
import sqlite3
app = Flask(__name__)@app.route("/authenticate", methods = ["POST"])
def authenticate():
# Get the name & password from the request (submitted via a form)
name = request.form.get("name", "")
password = request.form.get("password", "")
# Execute SQL query.
# Password isn't hashed... Quite bad!
dbConnection = sqlite3.connect("/tmp/users.db")
cursor = dbConnection.cursor()
cursor.execute("SELECT name FROM login WHERE password = ?" +
"AND name = ?", (password, name))
# Fetch all the maching records!
username = cursor.fetchall()
# No check on if more than one records are returned (which could be an easy indicator of an SQLi,
# not always, but a good one to have)
# Attacker can pull only 1 record but again, only if they know such checks are there ;)
# But the gain won't be that much, just the usernames!# If the record exists, let's get the welcome message...
if username:
# Use of f-strings - Second order SQLi!
# If a stored SQLi payload in the 'name' field finds its way in the database, the following
# query would result in an SQLi!
cursor.execute(f"SELECT message FROM welcome_messages WHERE name = {username}")
# Returning all records... not good again!
return cursor.fetchall()
# Password not matched! So we will not give back anything. Well done devs!
else: return "You're not logged in"
if __name__ == "__main__":
# Let's run this application on localhost:8080 with debugging enabled for our ease!
app.run(host = "127.0.0.1", port = 8080, debug = True)

So I had generated the database by running the previous script and also ran the Flask application:

Since there is no login page, we will use our old friend: curl.

Yaay! It works!!!

Holy $h!t

It still doesn’t works :/

Let me fix it! Final time okay! Don’t want any errors now!!!

from flask import Flask, request
import json
import sqlite3
app = Flask(__name__)@app.route("/authenticate", methods = ["POST"])
def authenticate():
# Get the name & password from the request (submitted via a form)
name = request.form.get("name", "")
password = request.form.get("password", "")
print ("name:", request.form.get("name", ""))
print ("password:", request.form.get("password", ""))
# Execute SQL query.
# Password isn't hashed... Quite bad!
dbConnection = sqlite3.connect("/tmp/users.db")
cursor = dbConnection.cursor()
cursor.execute("SELECT name FROM login WHERE password = ?" +
"AND name = ?", (password, name))
# Fetch all the maching records!
username = cursor.fetchall()
# No check on if more than one records are returned (which could be an easy indicator of an SQLi,
# not always, but a good one to have)
# Attacker can pull only 1 record but again, only if they know such checks are there ;)
# But the gain won't be that much, just the usernames!# If the record exists, let's get the welcome message...
if username:
# Get the first entry and the first tuple value, containing the name!
username = username[0][0]

# Use of f-strings - Second order SQLi!
# If a stored SQLi payload in the 'name' field finds its way in the database, the following
# query would result in an SQLi!
cursor.execute(f"SELECT message FROM welcome_messages WHERE name = '{username}'")
# Returning all records... not good again!
return json.dumps({
"msg": cursor.fetchall()
})
# Password not matched! So we will not give back anything. Well done devs!
else: return "You're not logged in"
if __name__ == "__main__":
# Let's run this application on localhost:8080 with debugging enabled for our ease!
app.run(host = "127.0.0.1", port = 8080, debug = True)

Ah! Here’s the masterpiece!

Let’s run it and login with my creds ;)

Yaaaaay!! Login part does works now :)

Hunting down the bugs

Now we have the test application running, let’s have some fun and add a new user to the database with SQLi payload and see the second-order SQLi in action, shall we!

Hacking Time!!!

And we got back the welcome messages for all the users!

Second Order SQLi in action :)

Extra Bits

Someone says this statement!

What do you think? Is he right? Will that happen?

Let’s find out!

And guess what! He was indeed right!!

Why though?

Documentation is your best friend here :)

So except the LIKE operator, if you use any normal comparisons, then the extra spaces at the end are ignored…

So beware of that folks!!

More Extra Bits: Who’s Fault was this?

Maybe it’s not Intigriti’s fault, must definitely be the *intern* right!

We all should have know that! And if you don’t then make a note — “All the silly mistakes that happen are to be blamed onto the interns!” — that’s what the infosec history has taught us, right ;)

Prevention

1. Don’t send all the matching records back! Place a limit on them.

Now some of you might be thinking, that you can exfiltrate data in a single entry using SQLi fu, and sure you can, but this still is a sane advice. Not returning more that needed so if there are any errors that the devs (I meant, the interns) do, the excessive data doesn’t gets returned!

2. Don’t pass user input directly to the query! Use the parameterized queries like you did in the first query!

3. Add a safelist of characters that the username must have. Reject all bad input! Imposing length restrictions also helps as it constraints the attackers in crafting shorter payloads — making things harder for the attackers!

A name is not supposed to have a ‘--’, ‘(’ or ‘)’ right! And neither does it has to be of 50–60 characters — I am talking about usernames, not your full names :)

4. Please hash your passwords, with salts (and peppers?) so that even with SQLi, it’s not a straight enough task for ̶a̶ ̶s̶c̶r̶i̶p̶t̶ ̶k̶i̶d̶d̶i̶e̶ an attacker to get the credentials.

Reason is simple — if you check the code, the first SQL statement gets you the records by taking the username and password. Now let’s say that 2 or more people were using same passwords. In that case SQLi would happen even with the first statement, even though it is a prepared statement!

5. Please don’t use debug mode (in case you do) in production!

And on top of that if you have debug mode enabled, atleast don’t disable the PIN and wait for someone to hack you! (Not commonly done but we never know of those interns)!

6. Please check the docs for any code you write! Your code didn’t worked in the first place folks!! Let’s get that fixed now :)

7. Update any components you use in your applications!

Dependency Hell is a real beast folks!!

8. Please don’t give any major projects to the newly hired *interns*? We can do better folks!

All-in-all, let’s follow defense in depth and add all the roadblocks we can, as security-oriented developers, just to make the lives of attackers harder, if not worse ;)

Securing things is easy as long as you don’t try to be smart and copy things from StackOverflow or use GitHub’s Copilot to do the heavy lifting for you!

So maybe I am giving out a sane advice or maybe I am giving you out a new excuse to save yourself and blame those sources instead of the intern?

Maybe… But at the end of the day, it’s important to understand how these issues happen in the code and make sure that these don’t slip by the code review! Especially if the code doesn’t runs, let’s not even push it on GitHub or GitLab or whatever you prefer!

Closing Thoughts

This was indeed a quite heavy post. Took a lot of effort to pull it off! But that’s what we have to do right :)

Since my goal is to get you the best infosec material so that all this pain becomes worth it in the end!

If you enjoyed this post, then please share it with all your friends and colleagues (developers, security folks and even interns) in the infosec & developer community!

Shoutout to Intigriti to creating these lovely testbeds so I can teach some important lessons :)

Let me know your feedback in the comments below and feel free to connect on twitter: @_SecurityGOAT

You can also send any topic you wish to learn more about. Send me DM on Twitter or let me know in the comments :)

Lastly, the line which should mostly be a plain ol’ boring one which I have to copy from my previous post every single time!!!! — if you have been enjoying my work and would love to support me, consider checking my Patreon page or you can even Buy Me a Coffee :)

And also, get ready for more fun posts! I have more fun ideas lately and planning to expand more, with more technical content for your brain muscles!

So help me spread these posts around to help make infosec more approachable for the beginners and more fun for the GOATS ;)

See ya!
Until next time my friend, keep learning and happy hacking.

--

--