NMAP scripting con NSE

Il Nmap Scripting Engine (NSE) è una potente estensione di Nmap che permette di aggiungere funzionalità come il vulnerability scanning e il bruteforcing. Vengono illustrate le modalità di utilizzo degli script NSE, inclusa la sintassi per richiamarli e passare loro argomenti. Il documento analizza anche gli internals di NSE, mostrando come vengano inizializzati e caricati gli script. Infine, vengono presentati due esempi pratici di script NSE: uno per il bruteforcing di server IMAP e uno per il rilevamento di una vulnerabilità in Apache. Attraverso questi esempi, il documento offre una comprensione approfondita delle potenzialità e delle caratteristiche del Nmap Scripting Engine.

Nmap Scripting Engine (NSE): una potente estensione di Nmap

Vi prometto che non spenderemo una riga su cos’è Nmap, su quanto sia utile e potente. Partiamo dal presupposto che sappiamo cosa sia e lo abbiamo utilizzato.

Quello che forse pochi sanno è che Nmap è programmabile. È infatti dotato di uno scripting engine (Nmap Scripting Engine, NSE) vero e proprio che permette di aggiungere funzionalità trasformando ad esempio Nmap in un vulnerability scanner o in un exploitation framework.

Nato e sviluppatosi in secondo piano, lo scripting engine si è evoluto enormemente fino a diventare il “primary focus” delle ultime release stabili dello scanner ed essere presentato alle conferenze DefCon e BlackHat 2010. L’ultima versione conta oltre 500 script e oltre 100 librerie.

Questo ha permesso a Nmap di fare un salto di qualità, aumentando il numero di protocolli gestiti e interrogabili: grazie a NSE è possibile eseguire query verso web server, database, DNS server e qualsiasi application protocol.

Utilizzo degli script NSE

Per provare gli script, il primo passo è aggiornare il database lanciando:

# nmap --script-updatedb
Starting Nmap 7.40 ( https://nmap.org ) at 2017-12-07 11:13 CET
NSE: Updating rule database.
NSE: Script Database updated successfully.
Nmap done: 0 IP addresses (0 hosts up) scanned in 0.42 seconds

Il database degli script, script.db, si dovrebbe trovare /usr/local/share/nmap/scripts/ oppure /usr/share/nmap/scripts/ e nella stessa directory si trovano gli script veri e propri.

La prima opzione per utilizzare gli script è –script=<Lua scripts> che esegue gli script indicati, che possono essere uno o più script, directory o categorie. Per indicare più script è possibile sia elencare le categorie separate da virgole sia usare gli operatori “and”, “or” e “not” per costruire espressioni booleane, ad esempio molto utile è il seguente comando:

# nmap --script="not intrusive" 192.168.3.15

che ad esempio esegue tutti gli script non appartenenti alla categoria “intrusive“.

Alcuni script necessitano di parametri, che possono essere passati con l’opzione –script-args=<n1=v1,[n2=v2,…]>, ad esempio:

# nmap -v --script=smb-enum-shares --script-args=smbuser=user,smbpass=pass 192.168.1.33

Per vedere tutto ciò che succede, è possibile specificare –script-trace: utile in fase di sviluppo e per capire come agisce uno script.

Per maggiori informazioni sugli script, è possibile usare –script-help=<Lua scripts> con la stessa sintassi di –script.

Alcuni semplici esempi della sintassi sono:

# nmap --script=ftp-brute 192.168.3.15
# nmap --script="ftp*" 192.168.3.15
# nmap --script=discovery 192.168.3.15

Nel primo caso eseguiamo solo lo script ftp-brute.nse; nel secondo tutti gli script che iniziano per ftp (ftp-anon.nse, ftp-bounce.nse, ftp-brute.nse, ftp-libopie.nse, ftp-proftpd-backdoor.nse etc.); nel terzo esempio eseguiamo tutti gli script appartenenti alla categoria “discovery“.

Esiste, inoltre, una forma breve per –script=default, -sC. L’elenco delle categorie che è possibile indicare è riportato su http://nmap.org/nsedoc/

Internals di Nmap Scripting Engine

Diamo, per iniziare, uno sguardo agli internals di NSE. L’inizializzazione del motore di scripting avviene all’avvio di Nmap prima di qualsiasi scanning. Come prima cosa viene creato un’istanza dell’interprete Lua [Riq2] unica per tutta l’esecuzione, viene quindi caricato un sottoinsieme della libreria standard di Lua (debug, io, math, os, package, string e table) e le librerie C++ (nmap, pcre, bin, bit, e openssl) di NSE ed eseguito infine il core di NSE (nse_main.lua) che prepara l’ambiente di esecuzione e carica gli script lanciati dall’utente. [Riq 3]

Bruteforcing: esempi di script

Per meglio capire come funziona NSE e quali solo le sue potenzialità andiamo ad analizzare in dettaglio alcuni script. Iniziamo da “imap-brute” appartenente alle categorie “brute” e “intrusive” che esegue un attacco di brute forcing contro un server IMAP usando vari meccanismi di autenticazione (LOGIN, PLAIN, CRAM-MD5, DIGEST-MD5 e NTLM) configurabili in fase di esecuzione dello script. I sorgenti sono disponibili nella directory degli script oppure online nel repository di Nmap all’indirizzo http://nmap.org/svn/scripts/imap-brute.nse.

Ogni script è costituito da tre parti: una parte descrittiva, una o più regole (funzioni che restituiscono un valore booleano) che determinano quando lo script deve essere eseguito e un’azione che definisce il comportamento vero e proprio dello script.

La prima parte di ogni script è occupata dall’inclusione delle librerie [Tabella1]:

local brute = require "brute"
local coroutine = require "coroutine"
local creds = require "creds"
local imap = require "imap"
local shortport = require "shortport"
local stdnse = require "stdnse"

è possibile sia usare librerie Lua (come ad esempio coroutine) sia quelle specifiche di NSE (che si trovano in /usr/local/share/nmap/nselib/ o in un percorso analogo). Seguono la descrizione dell’azione che sarà eseguita e le informazioni sull’autore, la categoria e la licenza:

description = [[
Performs brute force password auditing against IMAP servers using either LOGIN, PLAIN, CRAM-MD5, DIGEST-MD5 or NTLM authentication.
]]
author = "Patrik Karlsson"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"brute", "intrusive"}

Dopodiché inizia lo script vero e proprio: bisogna decidere quando e in quali circostanze far eseguire a Nmap il nostro script.

La scelta avviene attraverso la definizione di una o più regole che definiscono il momento dell’esecuzione dello script in relazione all’operazione di scanning. Ad ogni regola va associata una funzione che, se valutata positivamente, definisce se eseguire o meno lo script.

Le possibili regole sono:

  • prerule: eseguito una volta sola prima dello scanning degli host (fase di pre-scanning)
  • hostrule e portrule: eseguito dopo lo scanning di ogni gruppo di host
  • postrule: eseguito una volta solo dopo lo scanning di tutti gli host (fase di post-scanning)

Nello script in esame abbiamo la riga:

portrule = shortport.port_or_service({143,993}, {"imap","imaps"})

che utilizzando il metodo port_or_service della libreria NSE shortport (inclusa in precedenza) fornisce una funzione per portrule che ritorna “true” quando viene trovata una porta aperta che verifica o il numero di porta specificato o il nome del servizio. Nell’esempio vengono passate due tabelle, una di porte e una di nomi corrispondenti al protocollo IMAP e IMAPS (IMAP over SSL).

Quindi il nostro script verrà eseguito dopo lo scanning di ogni host solo se ci troviamo in presenza delle porte o dei servizi indicati.

Inserimento del driver parametro

Tornando all’azione vera e propria dello script, prima di istanziare un engine per le operazioni di brute forcing occorre implementare un driver da passare come parametro al costruttore in modo da definirne il comportamento.

Il nostro driver (classe Driver della libreria brute) dovrà implementare le seguente funzioni: login, connect e disconnect.

Nel nostro esempio nella funzione connect() viene aperta una connessione verso il server IMAP utilizzando l’Helper della libreria imap:

connect = function( self )
  self.helper = imap.Helper:new( self.host, self.port )
  self.helper:connect()
  return true
end

Nella funzione login date le credenziali (username e password) viene restituito un oggetto di tipo Account in caso di successo, un errore altrimenti:

login = function( self, username, password )
  local status, err = self.helper:login( username, password, mech )
  if ( status ) then
    self.helper:close()
    self.helper:connect()
    return true, brute.Account:new(username, password, creds.State.VALID)
  end
  if ( err:match("^ERROR: Failed to .* data$") ) then
    self.helper:close()
    self.helper:connect()
    local err = brute.Error:new( err )
    err:setRetry( true )
    return false, err               
  end
  return false, brute.Error:new( "Incorrect password" )
end

Account è una semplice classe di utilità che converte lo stato di un account in una rappresentazione comune pronta per essere stampata a schermo.

Da notare che alla funzione login dell’helper passiamo, oltre alle credenziali, anche un terzo argomento opzionale mech che definisce il metodo di autenticazione in alternativa a quello plain-text standard. Nel nostro caso, come vedremo a breve, i meccanismi di autenticazione vengono definiti nell’azione.

In caso di errore, è interessante notare che viene ritentato il login impostando come recuperabile l’errore attraverso la funzione setRetry della libreria brute.

L’azione avviene attraverso diversi passi, si inizia connettendosi al server per cercare di recuperare informazioni sul meccanismo di autenticazione utilizzato:

local helper = imap.Helper:new(host, port)
local status = helper:connect()
local status, capabilities = helper:capabilities()

Viene quindi definita una priorità dei meccanismi utilizzando l’eventuale argomento imap-brute.auth passato allo script oppure un ordine di default:

local mech_prio = stdnse.get_script_args()
mech_prio = ( mech_prio and { mech_prio } ) or
{ "LOGIN", "PLAIN", "CRAM-MD5", "DIGEST-MD5", "NTLM" }

stdnse è una libreria (anch’essa inclusa all’inizio dello script) che contiene una serie di utili metodi per la gestione degli script come ad esempio get_script_args() che fa il parsing degli argomenti passati allo script attraverso l’opzione –script-args, date_to_timestamp() che converte una data in un timestamp, in_port_range() che controlla se una porta rientra in un determinato range. Il consiglio è dunque di dare uno sguardo a stdnse prima di implementare un metodo di utilità.

A questo punto si va alla ricerca di un meccanismo valido confrontando la lista di meccanismi di autenticazione dello script con quella recuperata dal server attraverso il metodo capabilities() di imap:

for _, m in ipairs(mech_prio) do
  if ( m == "LOGIN" and not(capabilities.LOGINDISABLED)) then
    mech = "LOGIN"
    break
  elseif ( capabilities["AUTH=" .. m] ) then
    mech = m
    break
  end
end
if ( not(mech) ) then
  return "\n ERROR: No suitable authentication mechanism was found"
end

Innanzitutto viene controllato se è possibile usare il comando LOGIN che utilizza password in chiaro testando l’assenza della capability LOGINDISABLED dalla risposta del server. In caso contrario vengono scorse tutte le restanti possibilità fino a individuare il metodo da utilizzare o a restituire errore (in Lua, l’operatore .. concatena due stringhe, nel nostro caso aggiungiamo il prefisso AUTH= ai metodi passati allo script).

Modalità di iterazione e processo di autenticazione

Dopo queste operazioni preliminari troviamo l’azione vera e propria. Viene prima istanziato l’engine di brute force passando come primo parametro il driver creato prima, poi avviato con il metodo start() e infine vengono restituiti i risultati:

local engine = brute.Engine:new(Driver, host, port)
engine.options.script_name = SCRIPT_NAME
local result
status, result = engine:start()
return result

L’engine restituisce uno stato booleano (vero in caso di successo, falso in caso contrario) e una tabella con i risultati ottenuti.

Internamente l’engine utilizza un elenco di password e username ricavato attraverso la libreria NSE unpwdb che recupera i dati rispettivamente dai file nselib/data/passwords.lst e nselib/data/usernames.lst che sono dizionari di password e username comuni.

Ad esempio in usernames.lst troveremo i seguenti valori: root, admin, administrator, webadmin, sysadmin, netadmin, guest,user, web, test.

È possibile utilizzare anche un proprio DB di username o password rispettivamente attraverso gli argomenti userdb e passdb della libreria unpwdb:

# nmap –script-args=userdb=myuser.lst,passdb=mypass.lst

Tornando all’engine, dopo aver prelevato gli username e le password da utilizzare, in base alla modalità di brute forcing scelta viene definito il metodo di iterazione.

Le modalità possibili sono tre:

  • user: per ogni username vengono testate tutte le password.
  • pass: ogni password è testata per tutti gli username.
  • creds: un set di credenziali (una coppia username/password) vengono testate contro il servizio.

Se non specificato, come nel caso in esame viene utilizzata la modalità pass e il corrispondente iteratore pw_user_iterator().

L’operazione di brute forcing viene lanciata attraverso la funzione login che a sua volta chiama self:doAuthenticate() che esegue la richiesta di autenticazione utilizzando il driver e iterando sull’insieme di credenziali:

local driver = self.driver:new( self.host, self.port, self.driver_options )
status = driver:connect()
status, response = driver:login( username, password )
driver:disconnect()

I risultati positivi provenienti dall’iterazione vengono inseriti nella tabella dei risultati e restituiti allo script che ha istanziato l’engine.

Un altro esempio di script: Vulnerability Scanner

Passiamo ora a vedere uno script completamente diverso, almeno nelle funzionalità. Analizziamo http-vuln-cve2011-3192 che tenta di individuare server Apache vulnerabili al CVE-2011-3192, ovvero ad un Denial of Service remoto.

Come argomenti vanno passati il path della richiesta e l’hostname da inserire nell’HEAD da inviare al target, ad esempio:

# nmap --script=http-vuln-cve2011-3192.nse --script-args=http-vuln-cve2011-3192.hostname=www.example.com -pT:80,443 localhost

Essendo la struttura del tutto simile allo script analizzato precedentemente vedremo in questo caso solo in che modo sono caratterizzati gli script della categoria vuln.

Anche in questo caso si tratta di uno script di tipo portrule (viene eseguito in presenza di servizi HTTP). La prima parte dell’azione è dedicata alla preparazione del report della vulnerabilità che viene descritta utilizzando una tabella richiesta dalla libreria vulns:

local vuln = {
  title = 'Apache byterange filter DoS',
  state = vulns.STATE.NOT_VULN, -- default
  IDS = {CVE = 'CVE-2011-3192', OSVDB = '74721'},
  description = [[The Apache web server is vulnerable to a denial of service
  attack when numerous overlapping byte ranges are requested.]],
  references = {
    'http://seclists.org/fulldisclosure/2011/Aug/175',
    'http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2011-3192',
    'http://nessus.org/plugins/index.php?view=single&id=55976',
  },
  dates = {disclosure = {year = '2011', month = '08', day = '19'},},
}
local vuln_report = vulns.Report:new(SCRIPT_NAME, host, port)

Vengono quindi preparate le opzioni per la richiesta HTTP, in particolare viene popolato l’HTTP header indicando tra l’altro il range richiesto:

local request_opts = {
  header = {Range = "bytes=0-100",Connection = "close"},
  bypass_cache = true
}

e viene infine eseguita una richiesta HEAD contro il target:

local response = http.head(host, port, path, request_opts)

a questo punto c’è l’azione che opera sul response restituito dalla richiesta HEAD. Se non c’è risposta alla richiesta o lo status del response è diverso dal codice 206 viene stampato un errore. Se invece in risposta all’HEAD con Range impostato viene restituito un 206 viene eseguita un’altra richiesta HEAD indicando 11 range, uno in più del numero consentito:

if not response.status then
  stdnse.print_debug(1, "Functionality check HEAD request failed")
elseif response.status == 206 then
  request_opts.header.Range = "bytes=1-0,0-0,1-1,2-2,3-3,4-4,5-5,6-6,7-7,8-8,9-9,10-10"
  response = http.head(host, port, path, request_opts)
  if not response.status then
    stdnse.print_debug(1, "Invalid response from server to the vulnerability check")
  elseif response.status == 206 then
    vuln.state = vulns.STATE.VULN
  else
    stdnse.print_debug(1, "Server isn't vulnerable")
  end
else
  stdnse.print_debug(1, "Functionality check HEAD request failed")
end

quindi, se il nuovo response ha ancora codice 206 il target è vulnerabile e viene impostata la variabile per il report finale vuln.state a vulns.STATE.VULN (di default era stata impostata a vulns.STATE.NOT_VULN nella tabella della vulnerabilità), report che viene in ogni caso generato con la funzione:

return vuln_report:make_output(vuln)

Idee per nuovi script

Segnalo infine la pagina del SecWiki “Nmap/Script Ideas” (https://secwiki.org/w/Nmap/Script_Ideas) dove è possibile suggerire idee per nuovi script agli sviluppatori Nmap e commentare le proposte esistenti ma anche prendere spunto per implementare un proprio script.

Tabella 1: Principali Librerie NSE

  • http: libreria che implementa un client HTTP. Tra le principali funzioni ci sono le classiche “get“, “post“, “put“, “head” e la generica “generic_request“. I risultati sono restituiti in una tabella contenenti gli elementi comuni di una richiesta HTTP (header, body, cookie, status).
  • vulns: libreria per la gestione (reporting e storage) delle vulnerabilità trovate.
  • brute: libreria per implementare meccanismi di password guessing basati su brute force.
  • url: libreria per il parsing e la risoluzione di URI.
  • packet: interessante libreria per la manipolazione di pacchetti grezzi.
  • nmap: interfaccia verso gli internal di Nmap.

Riquadro 1: Categorie di script NSE

  • default: script che vengono eseguiti di default se non viene specificata nessuna categoria e nessun particolare script. Le caratteristiche degli script eseguiti di default sono principalmente la velocità, la non-intrusività e l’utilità del risultato ottenuto.
  • auth: tutti gli script riguardanti l’autenticazione, come ad esempio i classici ftp-anon e http-auth ma anche script più specifici come domino-enum-users e realvnc-auth-bypass che fanno rispettivamente riferimento alle vulnerabilità CVE-2006-5835 e CVE-2006-2369 di IBM Lotus Domino e RealVNC server.
  • broadcast: script che eseguono operazioni in broadcasting, come ad esempio broadcast-listener che si mette in ascolto di tutte le comunicazioni in broadcast (dei protocolli CDP, HSRP, Spotify, DropBox, DHCP, ARP) in ingresso.
  • brute: script che prevedono attacchi brute force, molti appartengono anche alla categoria
  • discovery: script che provano a scoprire attivamente più informazioni possibili su una determinata  A questa categoria appartengono, ad esempio, quelli che recuperano informazioni via HTTP, come ad esempio http-title,   http-generator, http-headers, ma anche azioni più mirate come http-wordpress-plugins, http-drupal-enum-users e http-php-version.
  • dos: script che per loro natura possono causare un Denial of Service. Attualmente fanno parte di questa categoria tre script: broadcast-avahi-dos, smb-check-vulns e smb-flood.
  • exploit: script che tentano di sfruttare particolari vulnerabilità, come ad esempio ftp-vsftpd-backdoor che individua la presenza di una backdor nella versione 2.3.4 di vsFTPd (CVE-2011-2523).
  • external: script, generalmente presenti anche in altre categorie, che inviano dati a risorse. Ad esempio http-google-malware che controlla se l’host è presente nella blacklist di siti sospetti di Google.
  • fuzzer: a questa categoria appartiene solo lo script dns-fuzz che lancia un fuzzing attack contro i server DNS.
  • malware: script che testano la presenza di malware o backdoor. Ad esempio ftp-proftpd-backdoor e ftp-vsftpd-backdoor.
  • version: script che estendono la capacità di nmap di riconoscere le versioni di determinati server. Non possono essere lanciati esplicitamente, ma solo attraverso l’opzione -sV dello scanner. Eseguendo dunque Nmap con l’opzione -sV vengono automaticamente lanciati gli script della categoria “version”, se invece se ne vogliono eseguire solo alcuni, all’opzione -sV va aggiunta anche “–script” indicando gli script da eseguire come descritto sopra.
  • vuln: script che valutano la presenza di determinate vulnerabilità, ma che in genere non le             sfruttano come accade per quelli nella categoria exploit. Esempi sono http-vuln-cve2011-3192 e smtp-vuln-cve2011-1720 che individua un DoS rispettivamente contro Apache e Postfix.
  • safe e intrusive: nella prima categoria rientrano quegli script che non possono causare un crash del target, che non fanno un uso eccessivo di banda e che non sfruttano vulnerabilità. Alla categoria intrusive appartengono  invece tutti gli script non safe.

Riquadro 2: Perché LUA?

Direttamente dalle parole degli sviluppatori (da DefCon18), Lua è stato scelto perché è facile da imparare, ottimizzato nelle dimensioni per essere incluso come linguaggio di scripting in un altro progetto, ampiamente utilizzato, estendibile, sicuro, portabile (è scritto in ANSI C) e interpretato.

Riquadro 3: le API di Nmap

Nmap fornisce a NSE numerose facility che permettono di utilizzare negli script tutte le informazioni ricavate dall’operazione di scanning.

Ad esempio, come detto, il core di uno script è rappresentato dal metodo “action” che prende come argomenti i parametri “host” e “port“:

action = function(host, port)

che sono due tabelle che contengono tutte le informazioni disponibili sul target come ad esempio:

host.ip, host.name, host.targetname, host.os, host.interface, port.number, port.protocol, port.service e tante altre. Un elenco completo e dettagliato è disponibile alla pagina di descrizione delle “Nmap API” (http://nmap.org/book/nse-api.html).

 

A cura di: Gianluigi Spagnuolo

Profilo Autore

Si interessa di reverse engineering, attualmente si occupa della sicurezza dei firmware.
Collabora con diverse riviste del settore scrivendo di programmazione e sicurezza.

Condividi sui Social Network:

Articoli simili