4837 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / panos-scanner.py PY
#!/usr/bin/env python3
'''
Developed with <3 by the Bishop Fox Continuous Attack Surface Testing (CAST) team.
https://www.bishopfox.com/continuous-attack-surface-testing/how-cast-works/
Author:     @noperator
Purpose:    Determine the software version of a remote PAN-OS target.
Notes:      - Requires version-table.txt in the same directory.
            - Usage of this tool for attacking targets without prior mutual
              consent is illegal. It is the end user's responsibility to obey
              all applicable local, state, and federal laws. Developers assume
              no liability and are not responsible for any misuse or damage
              caused by this program.
Usage:      python3 panos-scanner.py [-h] [-v] [-s] -t TARGET
Shamelessly stolen by:
 ____  _        _    ____ _  __  _   _    _  _____ 
| __ )| |      / \  / ___| |/ / | | | |  / \|_   _|
|  _ \| |     / _ \| |   | ' /  | |_| | / _ \ | |  
| |_) | |___ / ___ \ |___| . \  |  _  |/ ___ \| |  
|____/|_____/_/   \_\____|_|\_\ |_| |_/_/   \_\_|  
 
 _____ _____ _   _ ___ ____    _    _     
| ____|_   _| | | |_ _/ ___|  / \  | |    
|  _|   | | | |_| || | |     / _ \ | |    
| |___  | | |  _  || | |___ / ___ \| |___ 
|_____| |_| |_| |_|___\____/_/   \_\_____|
                                          
 _   _    _    ____ _  _____ _   _  ____ 
| | | |  / \  / ___| |/ /_ _| \ | |/ ___|
| |_| | / _ \| |   | ' / | ||  \| | |  _ 
|  _  |/ ___ \ |___| . \ | || |\  | |_| |
|_| |_/_/   \_\____|_|\_\___|_| \_|\____|
                                         
'''

from argparse import ArgumentParser
from datetime import datetime, timedelta
from requests import get
from requests.exceptions import HTTPError, ConnectTimeout, SSLError, ConnectionError, ReadTimeout
from sys import argv, stderr, exit
from urllib3 import disable_warnings
from urllib3.exceptions import InsecureRequestWarning

disable_warnings(InsecureRequestWarning)

def etag_to_datetime(etag):
    epoch_hex = etag[-8:]
    return datetime.fromtimestamp(
               int(epoch_hex, 16)
           ).date()

def last_modified_to_datetime(last_modified):
    return datetime.strptime(
               last_modified[:-4],
               '%a, %d %b %Y %X'
           ).date()

def get_resource(target, resources, date_headers, errors, verbose):
    try:
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:54.0) Gecko/20100101 Firefox/54.0',
            'Connection': 'close',
            'Accept-Language': 'en-US,en;q=0.5',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Upgrade-Insecure-Requests': '1'
        }
        resp = get(
            '%s/%s' % (target, resource),
            headers=headers,
            timeout=5,
            verify=False
        )
        resp.raise_for_status()
        if verbose:
            print('[+]', resource, file=stderr)
        return {h: resp.headers[h].strip('"') for h in date_headers
                if h in resp.headers}
    except (HTTPError, ReadTimeout) as e:
        if verbose:
            print('[-]', resource, '({})'.format(type(e).__name__), file=stderr)
        return None
    except errors as e:
        raise e

def load_version_table(version_table):
    with open(version_table, 'r') as f:
        entries = [line.strip().split() for line in f.readlines()]
    return {e[0]: datetime.strptime(' '.join(e[1:]), '%b %d %Y').date()
            for e in entries}

def check_date(version_table, date):
    matches = {}
    for n in [0, 1, -1, 2, -2]:
        nearby_date = date + timedelta(n)
        versions = [version for version, date in version_table.items()
                    if date == nearby_date]
        if n == 0:
            key = 'exact'
        else:
            key = 'approximate'
        if key not in matches:
            matches[key] = {'date': nearby_date, 'versions': versions}
    return matches

def get_matches(date_headers, resp_headers, verbose=False):
    matches = {}
    for header in date_headers.keys():
        if header in resp_headers:
            date = globals()[date_headers[header]](resp_headers[header])
            date_matches = check_date(version_table, date)
            for precision, match in date_matches.items():
                if match['versions']:
                    if precision not in matches.keys():
                        matches[precision] = []
                    matches[precision].append(match)
                    if verbose:
                        print(
                            '[*]',
                            '%s ~ %s' % (date, match['date']) if date != match['date'] else date,
                            '=>',
                            ','.join(match['versions']),
                            file=stderr
                        )
    return matches

if __name__ == '__main__':

    parser = ArgumentParser('Determine the software version of a remote PAN-OS target. Requires version-table.txt in the same directory.')
    parser.add_argument('-v', dest='verbose', action='store_true', help='verbose output')
    parser.add_argument('-s', dest='stop', action='store_true', help='stop after one exact match')
    parser.add_argument('-t', dest='target', required=True, help='https://example.com')
    args = parser.parse_args()

    static_resources = [
        'global-protect/login.esp',
        'php/login.php',
        'global-protect/portal/css/login.css',
        'js/Pan.js',
        'global-protect/portal/images/favicon.ico',
        'login/images/favicon.ico',
        'global-protect/portal/images/logo-pan-48525a.svg',
    ]

    version_table = load_version_table('version-table.txt')

    # The keys in "date_headers" represent HTTP response headers that we're
    # looking for. Each of those headers maps to a function in this namespace
    # that knows how to decode that header value into a datetime.
    date_headers = {
        'ETag':          'etag_to_datetime',
        'Last-Modified': 'last_modified_to_datetime'
    }

    # A match is a dictionary containing a date/version pair. When populated,
    # each precision key (i.e., "exact" and "approximate") in this
    # "total_matches" data structure will map to a single list of possibly
    # several match dictionaries.
    total_matches = {
        'exact': [],
        'approximate': []
    }

    # These errors are indicative of target-level issues. Don't continue
    # requesting other resources when encountering these; instead, bail.
    target_errors = (ConnectTimeout, SSLError, ConnectionError)

    if args.verbose:
        print('[*]', args.target, file=stderr)

    # Check for the presence of each static resource.
    for resource in static_resources:
        try:
            resp_headers = get_resource(
                args.target,
                resource,
                date_headers.keys(),
                target_errors,
                args.verbose
            )
        except target_errors as e:
            print(type(e).__name__, file=stderr)
            exit(1)
        if resp_headers == None:
            continue

        # Convert date-related HTTP headers to a standardized format, and
        # store any matching version strings.
        total_matches.update(get_matches(date_headers, resp_headers, args.verbose))
        if args.stop and len(total_matches['exact']):
            break

    # Print results.
    if not len(sum(total_matches.values(), [])):
        print('no matches found')
    else:
        printed = []
        for precision, matches in total_matches.items():
            for match in matches:
                if match['versions'] and match not in printed:
                    printed.append(match)
                    print(','.join(match['versions']), match['date'], '(%s)' % precision)