Source

PeepingTom / peepingtom.py

#!/usr/bin/env python

import sys
import urllib2
import subprocess
import re
import time
import os
import hashlib

#=================================================
# MAIN FUNCTION
#=================================================

def main():
    # depenency check
    if not all([os.path.exists('phantomjs'), os.path.exists('/usr/bin/curl')]):
        print '[!] PhantomJS and cURL required.'
        return
    # parse options
    import optparse
    usage = """

PeepingTom - Tim Tomes (@LaNMaSteR53) (www.lanmaster53.com)

Dependencies:
 - PhantomJS
 - cURL

$ python ./%prog <mode> <path>"""
    parser = optparse.OptionParser(usage=usage)
    parser.add_option('-l', help='list input mode. path to list file.', dest='list_file', type='string', action='store')
    parser.add_option('-x', help='xml input mode. path to Nessus/Nmap XML file.', dest='xml_file', type='string', action='store')
    parser.add_option('-s', help='single input mode. path to target, remote URL or local path.', dest='target', type='string', action='store')
    parser.add_option('-t', help='socket timeout in seconds. default is 8 seconds.', dest='timeout', type='int', action='store')
    (opts, args) = parser.parse_args()

    # process options
    if opts.list_file:
        try:
            targets = open(opts.list_file).read().split()
        except IOError:
            print '[!] Invalid path to list file: \'%s\'' % opts.list_file
            return
    elif opts.xml_file:
        # optimized portion of Peeper (https://github.com/invisiblethreat/peeper) by Scott Walsh (@blacktip)
        import xml.etree.ElementTree as ET
        try: tree = ET.parse(opts.xml_file)
        except IOError:
            print '[!] Invalid path to XML file: \'%s\'' % opts.xml_file
            return
        except ET.ParseError:
            print '[!] Not a valid XML file: \'%s\'' % opts.xml_file
            return
        root = tree.getroot()
        if root.tag.lower() == 'nmaprun':
            # parse nmap file
            targets = parseNmap(root)
        elif root.tag.lower() == 'nessusclientdata_v2':
            # parse nessus file
            targets = parseNessus(root)
        print '[*] Parsed targets:'
        for x in targets: print x
    elif opts.target:
        targets = [opts.target]
    else:
        print '[!] Input mode required.'
        return
    timeout = opts.timeout if opts.timeout else 8

    print '[*] Analyzing %d targets.' % (len(targets))

    # setup data storage location
    directory = time.strftime('%y%m%d_%H%M%S', time.localtime())
    print '[*] Storing data in \'%s/\'' % (directory)
    os.mkdir(directory)
    report = 'peepingtom.html'
    outfile = '%s/%s' % (directory, report)

    # logic to gather screenshots and headers for the given targets
    db = {'targets': []}
    cnt = 0
    tot = len(targets) * 2
    try:
        for target in targets:
            #print '[*] %s' % (target)
            printProgress(cnt, tot)
            filename = '%s.png' % re.sub('\W','',target)
            filepath = '%s/%s' % (directory, filename)
            getCapture(target, filepath, timeout)
            cnt += 1
            printProgress(cnt, tot)
            target_data = {}
            target_data['url'] = target
            target_data['path'] = filename
            target_data['hash'] = hashlib.md5(open(filepath).read()).hexdigest() if os.path.exists(filepath) else 'z'*32
            target_data['headers'] = getHeaders(target, timeout)
            db['targets'].append(target_data)
            cnt += 1
        print printProgress(1,1)
    except Exception as e:
        print '[!] %s' % (e.__str__())
    
    # build the report and exit
    buildReport(db, outfile)
    import webbrowser
    path = os.getcwd()
    w = webbrowser.get()
    w.open('file://%s/%s/%s' % (path, directory, report))
    print '[*] Done.'

#=================================================
# SUPPORT FUNCTIONS
#=================================================

def parseNmap(root):
    targets = []
    # iterate through all host nodes
    for host in root.iter('host'):
        hostname = host.find('address').get('addr')
        # hostname node doesn't always exist. when it does, overwrite address previosuly assigned to hostanme
        hostname_node = host.find('hostnames').find('hostname')
        if hostname_node is not None: hostname = hostname_node.get('name')
        # iterate through all port nodes reported for the current host
        for item in host.iter('port'):
            state = item.find('state').get('state')
            service_node = item.find('service')
            # service node doesn't always exist when a port is open. assume not http if no service is found
            if service_node is None: continue
            service = service_node.get('name')
            # if the service is unknown, then use the fingerprint
            if service.lower() == 'unknown': service = item.find('service').get('servicefp')
            if (state.lower() == 'open' and 'http' in service.lower()):
                port = item.get('portid')
                proto = 'http'
                #if item.find('service').get('tunnel'):
                if 'https' in service.lower():
                    proto = 'https'
                url = '%s://%s:%s' % (proto, hostname, port)
                if not url in targets:
                    targets.append(url)
    return targets

def parseNessus(root):
    targets = []
    for host in root.iter('ReportHost'):
        name = host.get('name')
        for item in host.iter('ReportItem'):
            svc = item.get('svc_name')
            plugname = item.get('pluginName')
            if (svc in ['www','http?','https?'] and plugname.lower().startswith('service detection')):
                port = item.get('port')
                output = item.find('plugin_output').text.strip()
                proto = guessProto(output)
                url = '%s://%s:%s' % (proto, name, port)
                if not url in targets:
                    targets.append(url)
    return targets

def guessProto(output):
    # optimized portion of Peeper (https://github.com/invisiblethreat/peeper) by Scott Walsh (@blacktip)
    secure = re.search('TLS|SSL', output)
    if secure:
        return "https"
    return "http"

def getCapture(url, filename, timeout):
    cmd = './phantomjs --ignore-ssl-errors=yes ./capture.js %s %s %d' % (url, filename, timeout*1000)
    returncode, response = runCommand(cmd)
    return returncode

def getHeaders(url, timeout):
    cmd = 'curl -sILk %s --connect-timeout %d' % (url, timeout)
    returncode, response = runCommand(cmd)
    return response

def runCommand(cmd):
    proc = subprocess.Popen([cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=True)
    stdout, stderr = proc.communicate()
    response = ''
    if stdout: response += str(stdout)
    if stderr: response += str(stderr)
    return proc.returncode, response.strip()

def printProgress(cnt, tot):
    percent = 100 * float(cnt) / float(tot)
    sys.stdout.write('[%-40s] %d%%\r' % ('='*int(float(percent)/100*40), percent))
    sys.stdout.flush()
    return ''

def buildReport(db, outfile):
    live_markup = ''
    error_markup = ''
    dead_markup = ''
    # process markup for live targets
    for live in sorted(db['targets'], key=lambda k: k['hash']):
        live_markup += "<tr><td class='img'><a href='{0}'><img src='{0}' onerror=\"this.parentNode.parentNode.innerHTML='No image available.';\" /></a></td><td class='head'><a href='{1}' target='_blank'>{1}</a><br /><pre>{2}</pre></td></tr>\n".format(live['path'],live['url'],live['headers'])
    # add markup to the report
    file = open(outfile, 'w')
    file.write("""
<!doctype html>
<head>
<style>
table, td, th {border: 1px solid black;border-collapse: collapse;padding: 5px;font-size: .9em;font-family: tahoma;}
table {width: 100%%;table-layout: fixed;min-width: 1000px;}
td.img {width: 40%%;}
img {width: 100%%;}
td.head {vertical-align: top;word-wrap: break-word;}
</style>
</head>
<body>
<table>
%s
</table>
</body>
</html>""" % (live_markup))
    file.close()

#=================================================
# START
#=================================================

if __name__ == "__main__": main()