dimanche 23 mars 2014

Nmap, scapy, python

S'il y a bien un outil essentiel lors d'un pentest, il s'agit de nmap. Maîtriser cet outil peut permettre de gagner du temps et obtenir plus d'information lors de la phase de reconnaissance d'un audit. Mais j'ai l'impression qu'il a perdu en fiabilité ces derniers temps.

En particulier, je déteste le fait qu'un plantage en fin de scan sur des milliers d'adresses perdent le résultat des machines correctement scannées. Et des plantages, ce n'est pas rare. C'est d'ailleurs extrêmement courant en UDP et avec des scripts NSE comme les enumérations netbios.

Sans quand on recherche l'exhaustivité totale, il est devenu plus logique de faire un premier scan de détection des live hosts. Là encore nmap est la référence, mais il risque de se faire détrôner par zmap ou masscan (que je n'ai pas testé de peur de faire tomber les réseaux). Pour nmap, je n'ai jamais réussi à maîtriser totalement les retry et timeout, ce qui fait que la durée des scans dérive parfois énormément. J'ai finalement opté pour mon programme en python/scapy.

Je commence par définir les paquets que je souhaite envoyer avec quelques informations :
ICMP 8
ICMP 13
ICMP 17
TCP 80 S
TCP 80 A
TCP 443 S
TCP 443 A
UDP 53
UDP 161

Puis une liste d'IP ou de hostname :
example.com
example.com
google.com
scanme.insecure.org

Et enfin mon programme qui enverra uniquement les paquets que j'ai défini et rien d'autre :

#!/usr/bin/python
# -*- coding: utf-8 -*-

import argparse
from threading import *
from scapy.all import *
import Queue
import time

conf.verb = 0
conf.noenum.add(TCP.sport, TCP.dport, UDP.sport, UDP.dport, ICMP.type)
maxConnections = 100
timeoutPing = 3
connection_lock = BoundedSemaphore(value=maxConnections)
logQueue = Queue.Queue()
threads = []
endScript = False

def printThread(logQueue,logFile):
     while True:
        if not logQueue.empty():
            val = logQueue.get()
            logFile.write(val+"\n")
            print val
        else:
            if endScript:
                break 
            time.sleep(2)

def sendPacketICMP(packet,logQueue):
    ans=sr1(packet,timeout=timeoutPing)
    text=''
    if ans:
        text =  '[+] ' + packet.sprintf("%IP.dst% ICMP/%ICMP.type%") + \
                ' > ' + ans.sprintf("%IP.src% ICMP/%ICMP.type%")
        logQueue.put(text)
    else:
        text = '[-] ' + packet.sprintf("%IP.dst% ICMP/%ICMP.type%") + ' > No response'
        logQueue.put(text)

def sendPacketTCP(packet,logQueue):
    ans=sr1(packet,timeout=timeoutPing)
    text=''
    if ans:
        text =  '[+] ' + packet.sprintf("%IP.dst% TCP/%TCP.dport%/%TCP.flags%") + \
                 ' > ' + ans.sprintf("{IP:%IP.src% {ICMP:ICMP/%ICMP.type%}{TCP:TCP/%TCP.sport%/%TCP.flags%}}")
        logQueue.put(text)
    else:
        text = '[-] ' + packet.sprintf("%IP.dst% TCP/%TCP.dport%/%TCP.flags%") + ' > No response'
        logQueue.put(text)

def sendPacketUDP(packet,logQueue):
    ans=sr1(packet,timeout=timeoutPing)
    text=''
    if ans:
        text =  '[+] ' + packet.sprintf("%IP.dst% UDP/%UDP.dport%") + \
                 ' > ' + ans.sprintf("{IP:%IP.src% {ICMP:ICMP/%ICMP.type%}{UDP:UDP/%UDP.sport%}}")
        logQueue.put(text)
    else:
        text = '[-] ' + packet.sprintf("%IP.dst% UDP/%UDP.dport%") + ' > No response'
        logQueue.put(text)

def sendPacket(ip,logQueue,proto,port,flag):
    if proto=='ICMP':
        packet=IP(dst=ip)/ICMP(type=port)
        sendPacketICMP(packet,logQueue)
    elif proto=='TCP':
        packet=IP(dst=ip)/TCP(dport=port,flags=flag)
        sendPacketTCP(packet,logQueue)
    elif proto=='UDP':
        packet=IP(dst=ip)/UDP(dport=port)
        sendPacketUDP(packet,logQueue)

def ping_ip(ip,logQueue,config):
    try:
        for p in config:
            if len(p)==2:
                sendPacket(ip,logQueue,p[0],int(p[1]),'')
            else:
                sendPacket(ip,logQueue,p[0],int(p[1]),p[2])

    finally:
        connection_lock.release()

def main():
    parser = argparse.ArgumentParser(description='Superping')
    parser.add_argument('-i', metavar='ip list', type=argparse.FileType('rt'), required=True)
    parser.add_argument('-c', metavar='config', type=argparse.FileType('rt'), required=True)
    parser.add_argument('-o', metavar='out-file', type=argparse.FileType('wt'), required=True)
    parser.add_argument('--version', action='version', version='%(prog)s 1.0')

    try:
        results = parser.parse_args()
    except IOError, msg:
        parser.error(str(msg))

    lc = sum(1 for l in results.c)
    results.c.seek(0, 0)
    print '[*] Chargement de '+ str(lc) +' types de ping'    
    config = [line.strip().split() for line in results.c.readlines()]
    
    lc = sum(1 for l in results.i)
    results.i.seek(0, 0)
    print '[*] Lancement du scan de '+ str(lc) +' machines'
    
    t = Thread(target=printThread,args=(logQueue,results.o))
    t.daemon = True
    child = t.start()   
    
    lines = results.i.readlines()
    for ip_line in lines:
        connection_lock.acquire()
        t = Thread(target=ping_ip,\
          args=(ip_line.rstrip('\n'),logQueue,config))
        t.daemon = True
        child = t.start()
        threads.append(t)

    [x.join() for x in threads]
    endScript = True
    print '[*] Fin des threads'

if __name__ == '__main__':
    main()

La commande suivante me sort le résultat attendu :

./superping.py -i ip_list -c config -o log 2> /dev/null
[*] Chargement de 9 types de ping
[*] Lancement du scan de 3 machines
[+] Net('google.com') ICMP/8 > 74.125.132.138 ICMP/0
[+] Net('example.com') ICMP/8 > 93.184.216.119 ICMP/0
[+] Net('scanme.insecure.org') ICMP/8 > 74.207.244.221 ICMP/0
[+] Net('scanme.insecure.org') ICMP/13 > 74.207.244.221 ICMP/14
[-] Net('google.com') ICMP/13 > No response
[-] Net('example.com') ICMP/13 > No response
[-] Net('scanme.insecure.org') ICMP/17 > No response
[+] Net('scanme.insecure.org') TCP/80/S > 74.207.244.221 TCP/80/SA
[-] Net('google.com') ICMP/17 > No response
[-] Net('example.com') ICMP/17 > No response
[+] Net('google.com') TCP/80/S > 74.125.132.138 TCP/80/SA
[+] Net('example.com') TCP/80/S > 93.184.216.119 TCP/80/RA
[+] Net('google.com') TCP/80/A > 74.125.132.139 TCP/80/R
[-] Net('scanme.insecure.org') TCP/80/A > No response
[+] Net('example.com') TCP/80/A > 93.184.216.119 TCP/80/R
[+] Net('google.com') TCP/443/S > 74.125.132.113 TCP/443/SA
[+] Net('scanme.insecure.org') TCP/443/S > 74.207.244.221 TCP/443/RA
[+] Net('example.com') TCP/443/S > 93.184.216.119 TCP/443/RA
[+] Net('example.com') TCP/443/A > 93.184.216.119 TCP/443/R
[+] Net('google.com') TCP/443/A > 74.125.132.138 TCP/443/R
[+] Net('scanme.insecure.org') TCP/443/A > 74.207.244.221 TCP/443/R
[+] Net('scanme.insecure.org') UDP/53 > 74.207.244.221 ICMP/3
[+] Net('scanme.insecure.org') UDP/161 > 74.207.244.221 ICMP/3
[-] Net('google.com') UDP/53 > No response
[-] Net('example.com') UDP/53 > No response
[*] Fin des threads

Et voilà ! On obtient une jolie liste des IP up qu'on va pouvoir scanner intégralement avec Nmap.