panos-check.py
// #!/usr/bin/env python3
import argparse
import datetime
import json
import logging
import requests
import requests.exceptions
import sys
import time
import urllib3
import urllib3.exceptions
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
verbose = False
# Set up logging.
logging.basicConfig(
format="%(asctime)s %(levelname)-8s [%(funcName)s] %(message)s",
datefmt="%Y-%m-%dT%H:%M:%SZ",
)
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logging.Formatter.converter = time.gmtime
def etag_to_datetime(etag: str) -> datetime.date:
epoch_hex = etag[-8:]
return datetime.datetime.fromtimestamp(int(epoch_hex, 16)).date()
def last_modified_to_datetime(last_modified: str) -> datetime.date:
return datetime.datetime.strptime(last_modified[:-4], "%a, %d %b %Y %X").date()
def get_resource(target: str, resource: str, date_headers: dict, errors: tuple) -> dict:
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",
}
logger.debug(resource)
try:
resp = requests.get(
"%s/%s" % (target, resource), headers=headers, timeout=5, verify=False
)
resp.raise_for_status()
return {
h: resp.headers[h].strip('"') for h in date_headers if h in resp.headers
}
except (requests.exceptions.HTTPError, requests.exceptions.ReadTimeout) as e:
logger.warning(type(e).__name__)
return None
except errors as e:
raise e
def load_version_table(version_table: str) -> dict:
with open(version_table, "r") as f:
entries = [line.strip().split() for line in f.readlines()]
return {
e[0]: datetime.datetime.strptime(" ".join(e[1:]), "%b %d %Y").date()
for e in entries
}
def check_date(version_table: dict, date: datetime.date) -> list:
matches = []
for n in [0, 1, -1, 2, -2]:
nearby_date = date + datetime.timedelta(n)
versions = [
version for version, date in version_table.items() if date == nearby_date
]
if not len(versions):
continue
precision = "exact" if n == 0 else "approximate"
append = True
for match in matches:
if match["precision"] == precision:
append = False
if append:
matches.append(
{
"date": nearby_date,
"versions": versions,
"precision": precision
}
)
return matches
def get_matches(date_headers: dict, resp_headers: dict, version_table: dict) -> list:
matches = []
for header in date_headers.keys():
if header in resp_headers:
date = globals()[date_headers[header]](resp_headers[header])
matches.extend(check_date(version_table, date))
return matches
def main():
# Parse arguments.
parser = argparse.ArgumentParser(
description="""
Determine the software version of a remote PAN-OS target. Requires
version-table.txt in the same directory. See
https://security.paloaltonetworks.com/?product=PAN-OS for security
advisories for specific PAN-OS versions.
"""
)
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.
total_matches = []
# These errors are indicative of target-level issues. Don't continue
# requesting other resources when encountering these; instead, bail.
target_errors = (
requests.exceptions.ConnectTimeout,
requests.exceptions.SSLError,
requests.exceptions.ConnectionError,
)
if args.verbose:
logger.setLevel(logging.DEBUG)
logger.debug(f"scanning target: {args.target}")
# 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,
)
except target_errors as e:
logger.error(f"could not connect to target: {type(e).__name__}")
sys.exit(1)
if resp_headers == None:
continue
# Convert date-related HTTP headers to a standardized format, and
# store any matching version strings.
resource_matches = get_matches(date_headers, resp_headers, version_table)
for match in resource_matches:
match["resource"] = resource
total_matches.extend(resource_matches)
# Stop if we've got an exact match.
stop = False
if args.stop:
for match in resource_matches:
if match["precision"] == "exact":
stop = True
if stop:
break
# Print results.
results = {"match": {}, "all": total_matches}
if not len(total_matches):
logger.error("no matching versions found")
sys.exit(1)
else:
closest = sorted(total_matches, key=lambda x: x["precision"], reverse=True)[0]
results["match"] = closest
print(json.dumps(results, default=str))
if __name__ == "__main__":
main()
Last updated