MongoDB Injection - How To Hack MongoDB

I use MongoDB for pretty much all of my projects now. From work, to personal projects, it is just a great database engine. However, since it is relatively new the problems with it are not as well known and can catch you off guard, as it caught me off guard. I would also like to note that NoSQL injection is not a new concept. You can read up more on it here and here. This post is to discuss a technique that I have discovered on my own by mistake and how it can be used to change records in the database if you are not careful enough. The exploit is fairly obvious, and can be avoided very easily, but in a lot of ways SQL Injection is very obvious as well. Still, many people have fallen into the trap. I have fallen into this trap with MongoDB, and I learned a lot while doing it so I would like to share my experience.

Before we start getting into the fun stuff I want to talk about the feature in MongoDB that allows for this exploit. MongoDB gives the ability to update objects using a period to access the nested keys. Let me show you an example.

We have a record in the database that looks like this:


This record can be updated using the following query:

db.people.update({"name":"John"}, {"$set":{"info.age":66}})  

Great! Pretty cool huh? Convenient for sure.

The problem is when the sub key is not hard coded. What if the user decides which key was being sent over? What if that same request looked like this?

keyName = request.form|'keyName'|  
keyData = request.form|'value'|  
db.people.update({"name":"John"}, {"$set":{"info.{}".format(keyName):keyData}})  

Now we have a problem because we are processing unsanitized user input. If there are more sensitive values within info an attacker can alter them.

I put together a "real world" test for this type of exploit just to demonstrate how this could cause some problems if bad coding practices were used. The Python code below is the entire application:

from flask import *  
import pymongo  
import bson  
import uuid

db = pymongo.MongoClient("localhost", 27017).test

form = """  
<form method="POST">  
<input type="text" name="username" placeholder="Username">  
<input type="text" name="password" placeholder="Password">  
<input type="text" name="firstname" placeholder="Firstname">  
<input type="text" name="lastname" placeholder="Lastname"/>  
<input type="text" name="age" placeholder="Age">  
<input type="submit" value="Submit">  

app = Flask(__name__)  
app.secret_key = "secret"

def logout():  
    return redirect("/login/")

def index():  
    if "_id" not in session:
        return redirect("/login/")
    name = request.args.get("name")
    lastname = request.args.get("lastname")
    if not name:
        return "<h1>Search for someone</h1><form method='GET'><input name='name' type='text' placeholder='First Name'><input name='lastname' type='text' placeholder='Last Name'><input type='submit'></form>"
        search_results = db.members.find_one({"{}".format(name):lastname})
        if search_results:
            search_results = name + " " + lastname + " is " + search_results['account_info']['age'] + " years old."
        return "{}<form><input name='name' type='text' placeholder='First Name'><input name='lastname' type='text' placeholder='Last Name'><input type='submit'></form>".format(search_results)

@app.route("/login/", methods=['GET', 'POST'])
def login():  
    if request.method == "POST":
        username = request.form['username']
        password = request.form['password']
        check = db.members.find_one({"username":username, "password":password})
        if check:
            session['_id'] = str(check)
            return rediirect("/?name={}".format)
            return "Invalid Login"
    return "<h1>Login</h1>" + form 

@app.route("/signup/", methods=['GET', 'POST'])
def signup():  
    if request.method == "POST":
        username = request.form['username']
        firstname = request.form['firstname']
        lastname = request.form['lastname']
        password = request.form['password']
        age = request.form['age']
        session['_id'] = str(db.members.insert({"username":username, "password":password, firstname:lastname, "account_info":{"age":age, "age":age, "isAdmin":False, "secret_key":uuid.uuid4().hex}}))
        return redirect("/")
    return "<h1>Signup</h1>" + form

@app.route("/settings/", methods=['GET', "POST"])
def settings():  
    if request.method == "POST":
        username = request.form['username']
        firstname = request.form['firstname']
        lastname = request.form['lastname']
        password = request.form['password']
        age = request.form['age']
        db.members.update({"_id":bson.ObjectId(session['_id'])}, {"$set":{"{}".format(firstname):lastname, "account_info.age":age, "username":username}})
        return "Values have been updated!"
    return "<h1>Settings</h1>" + form

@app.route("/admin/", methods=['GET', 'POST'])
def admin():  
    if "_id" not in session:
        return redirect("/login/")
    theUser = db.members.find_one({"_id":bson.ObjectId(session['_id'])})
    if not theUser['account_info']['isAdmin']:
        return "You do not have access to this page."
    if request.method == "POST":
        secret = request.form['secret_key']
        return str(db.members.find_one({"account_info.secret_key":secret}))
    return """<h1>Search user by secret key</h1>
    <form method="post"><input type="text" name="secret_key" placeholder="Secret Key"/><input type="submit" value="Serach"/></form>

The site is very simple. There is a login page, a signup page, a settings page, and an index page that allows users to look up a person by his/her first and last name and have it return his/her age. It is also worth noting that this code is vulnerable to the problem that was discussed in the beginning of this post. Now we will switch to attacker mode...

Our objective is to gain access to the admin page. After some recon we have discovered that there is an admin page located at /admin/ that normal users do not have access to. We have also managed to figure out what the database schema is which will make the process of actually attacking this website much quicker... it looks like this:

    "username" : "username",
    "account_info" : {
        "secret_key" : "secret",
        "age" : 45,
        "isAdmin" : false
    "password" : "password",
    "firstname" : "lastname"

That Firstname:Lastname key value pair looks interesting.

First I am going to create an account to get a little bit more access to the site. After I do that I try to access the /admin/ endpoint and this is returned:

Great... no access. Looking back at the schema isAdmin may be what is controlling access to this page, and since firstname:lastname looks like it might be set by the user we can attempt to change account_info from the settings page. This is because the settings page allows us to update the fields "username", "password", "firstname", and "lastname".

Now we can try to inject a value that will update isAdmin with "1" which will evaluate to True in Python.

No errors, I guess it worked!

The moment of truth...

It worked! Access has been granted :D

I wonder what types of info the user can get with a secret key... well I do not know my secret key but I can change it to something I do know.

This should work...

Shit that is scary. The possibilities are now endless. I could turn this into a cross site request forgery attack to change the secret key of other accounts and view all account data. Who knows what else.

So, it is obvious that everything about this website is designed poorly. There is no hashing being used for sensitive information, and keys should never be variables. If the keys need to have variables in them for whatever reason then those variables need to be checked against trusted expected inputs to make sure that this does not happen. In hindsight these types of vulnerabilities seem trivial and stupid, but they present themselves in many different ways and sometimes can be overlooked. I will admit that I have already fallen into the trap and if I had not caught it when I did it could have become a huge problem when deploying in the wild.

Moral of the story, sanitize your inputs and do not use variables in keys. It is not a good time.

Good luck.