├── .gitignore ├── LICENSE.md ├── README.md ├── version-table.txt └── panos-scanner.py /.gitignore: -------------------------------------------------------------------------------- 1 | notes.md 2 | *.swp 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright © `2020` `@noperator` 5 | 6 | Permission is hereby granted, free of charge, to any person 7 | obtaining a copy of this software and associated documentation 8 | files (the “Software”), to deal in the Software without 9 | restriction, including without limitation the rights to use, 10 | copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 25 | OTHER DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PAN-OS GlobalProtect Portal Scanner 2 | 3 | Determine the Palo Alto PAN-OS software version of a remote GlobalProtect portal or management interface. 4 | 5 | Developed with ❤️ by the [Bishop Fox Cosmos](https://bishopfox.com/platform) (formerly CAST) team. 6 | 7 | - [Description](#description) 8 | - [Getting started](#getting-started) 9 | - [Back matter](#back-matter) 10 | 11 | ## Description 12 | 13 | Palo Alto's GlobalProtect portal, a feature of PAN-OS, has been the subject of 14 | [several critical-severity vulnerabilities](https://security.paloaltonetworks.com/?severity=CRITICAL&product=PAN-OS&sort=-date) that can allow authorization bypass, unauthenticated remote code execution, etc. From an external perspective, it can be difficult to tell if you're running a patched version of PAN-OS since the GlobalProtect portal and management interface don't explicitly reveal their underlying software version. 15 | 16 | To assist PAN-OS users in patching their firewalls, this scanner examines the `Last-Modified` and `ETag` HTTP response headers for several static web resources, and associates those values with specific PAN-OS releases. For example, note the `ETag` in the following HTTP response from the GlobalProtect portal login page: 17 | 18 | ``` 19 | $ curl -skI https://example.com/global-protect/login.esp 20 | HTTP/1.1 200 OK 21 | Content-Type: text/html; charset=UTF-8 22 | Connection: keep-alive 23 | ETag: "6e185d5daf9a" 24 | ``` 25 | 26 | Examining the last 8 characters of the `ETag` gives us the hexadecimal epoch time `5d5daf9a`, represented as `1566420890` in decimal format. We can convert this epoch time to a human-readable format using the UNIX `date` utility: 27 | 28 | ``` 29 | $ date -d @1566420890 30 | Wed 21 Aug 2019 08:54:50 PM UTC 31 | ``` 32 | 33 | Using the attached `version-table.txt`, we can determine that this instance of GlobalProtect portal is running on PAN-OS version `8.1.10`, and is therefore vulnerable to 34 | [CVE-2020-2034](https://security.paloaltonetworks.com/CVE-2020-2034), an OS command injection vulnerability in GlobalProtect portal, and should consequently be patched. 35 | 36 | ``` 37 | $ awk '/Aug.*21.*2019/ {print $1}' version-table.txt 38 | 8.1.10 39 | ``` 40 | 41 | This scanner automates the process described above, suggesting an exact (or approximate) underlying PAN-OS version for a remote GlobalProtect portal or management interface. When multiple versions are associated with a given date, this tool will display all version matches as a comma-separated list; e.g, `7.1.24-h1,8.0.19-h1,8.1.9-h4` for `2019-08-15`. 42 | 43 | ## Getting started 44 | 45 | ### Install 46 | 47 | ``` 48 | $ git clone https://github.com/noperator/panos-scanner.git 49 | ``` 50 | 51 | ### Usage 52 | 53 | Note that this script requires `version-table.txt` in the same directory. 54 | 55 | ``` 56 | $ python3 panos-scanner.py -h 57 | usage: Determine the software version of a remote PAN-OS target. Requires version-table.txt in the same directory. 58 | [-h] [-v] [-s] [-c] -t TARGET 59 | 60 | optional arguments: 61 | -h, --help show this help message and exit 62 | -v verbose output 63 | -s stop after one exact match 64 | -t TARGET https://example.com 65 | ``` 66 | 67 | In the following example, `https://example.com/global-protect/portal/images/favicon.ico` has an HTTP response header that indicates that it's running PAN-OS version `8.0.10`. 68 | 69 | ``` 70 | $ python3 panos-scanner.py -s -t https://example.com | jq '.match' 71 | { 72 | "date": "2018-05-04", 73 | "versions": [ 74 | "8.0.10" 75 | ], 76 | "precision": "exact", 77 | "resource": "global-protect/portal/images/favicon.ico" 78 | } 79 | ``` 80 | 81 | 124 | 125 | ## Back matter 126 | 127 | ### Legal disclaimer 128 | 129 | Usage of this tool for testing 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. 130 | 131 | ### Acknowledgements 132 | 133 | Thanks [@k4nfr3](https://github.com/k4nfr3) for providing updates to the version table, and for building in the option to print a URL for Palo Alto's security advisories page. 134 | 135 | ### See also 136 | 137 | - [Shodan Facet Analysis — PAN-OS Version](https://beta.shodan.io/search/facet?query=http.html%3A%22Global+Protect%22&facet=os) 138 | - [A Look at PAN-OS Versions with a Bit of R](https://rud.is/b/2020/07/10/a-look-at-pan-os-versions-with-a-bit-of-r/) 139 | - [Palo Alto Networks Security Advisories](https://security.paloaltonetworks.com/) 140 | 141 | ### To-do 142 | 143 | - [x] Stop after one exact match 144 | - [x] Simplify output 145 | - [x] Support verbose CLI option 146 | - [x] Perhaps output JSON instead, to be processed with `jq` 147 | 148 | ### License 149 | 150 | This project is licensed under the [MIT License](LICENSE.md). 151 | -------------------------------------------------------------------------------- /version-table.txt: -------------------------------------------------------------------------------- 1 | 6.0.0 Dec 23 2013 2 | 6.0.1 Feb 26 2014 3 | 6.0.2 Apr 18 2014 4 | 6.0.3 May 29 2014 5 | 6.0.4 Jul 30 2014 6 | 6.0.5 Sep 4 2014 7 | 6.0.5-h3 Oct 7 2014 8 | 6.0.6 Oct 7 2014 9 | 6.0.7 Nov 18 2014 10 | 6.0.8 Jan 13 2015 11 | 6.0.9 Feb 27 2015 12 | 6.0.10 Apr 22 2015 13 | 6.0.11 Aug 12 2015 14 | 6.0.12 Nov 19 2015 15 | 6.0.13 Feb 13 2016 16 | 6.0.14 Jun 28 2016 17 | 6.0.15 Oct 5 2016 18 | 6.1.0 Oct 17 2014 19 | 6.1.1 Nov 13 2014 20 | 6.1.2 Jan 23 2015 21 | 6.1.3 Mar 10 2015 22 | 6.1.4 Apr 22 2015 23 | 6.1.5 Jun 17 2015 24 | 6.1.6 Jul 23 2015 25 | 6.1.7 Sep 10 2015 26 | 6.1.8 Nov 4 2015 27 | 6.1.9 Jan 8 2016 28 | 6.1.10 Feb 12 2016 29 | 6.1.11 Apr 2 2016 30 | 6.1.12 May 21 2016 31 | 6.1.13 Jul 15 2016 32 | 6.1.14 Aug 10 2016 33 | 6.1.15 Oct 5 2016 34 | 6.1.16 Jan 10 2017 35 | 6.1.17 Apr 14 2017 36 | 6.1.18 Jul 14 2017 37 | 6.1.19 Nov 5 2017 38 | 6.1.20 Feb 13 2018 39 | 6.1.21 May 25 2018 40 | 6.1.22 Oct 15 2018 41 | 7.0.1 Jul 3 2015 42 | 7.0.2 Aug 21 2015 43 | 7.0.3 Oct 8 2015 44 | 7.0.4 Dec 12 2015 45 | 7.0.5 Jan 30 2016 46 | 7.0.5-h2 Feb 17 2016 47 | 7.0.6 Mar 12 2016 48 | 7.0.7 Apr 19 2016 49 | 7.0.8 Jun 11 2016 50 | 7.0.9 Jul 27 2016 51 | 7.0.10 Aug 29 2016 52 | 7.0.11 Oct 20 2016 53 | 7.0.12 Dec 6 2016 54 | 7.0.13 Dec 29 2016 55 | 7.0.14 Feb 8 2017 56 | 7.0.15 Apr 12 2017 57 | 7.0.16 May 30 2017 58 | 7.0.17 Jul 10 2017 59 | 7.0.18 Aug 16 2017 60 | 7.0.19 Nov 10 2017 61 | 7.1.0 Mar 16 2016 62 | 7.1.1 Apr 6 2016 63 | 7.1.2 May 3 2016 64 | 7.1.3 Jun 21 2016 65 | 7.1.4 Aug 2 2016 66 | 7.1.4-h2 Aug 12 2016 67 | 7.1.5 Sep 24 2016 68 | 7.1.6 Nov 9 2016 69 | 7.1.7 Dec 17 2016 70 | 7.1.8 Feb 14 2017 71 | 7.1.9 Mar 27 2017 72 | 7.1.9-h4 Jun 16 2017 73 | 7.1.10 May 5 2017 74 | 7.1.11 Jun 29 2017 75 | 7.1.12 Aug 18 2017 76 | 7.1.13 Sep 28 2017 77 | 7.1.14 Nov 13 2017 78 | 7.1.15 Jan 5 2018 79 | 7.1.16 Feb 20 2018 80 | 7.1.17 Apr 11 2018 81 | 7.1.18 Jun 6 2018 82 | 7.1.19 Jul 16 2018 83 | 7.1.20 Sep 7 2018 84 | 7.1.21 Oct 31 2018 85 | 7.1.22 Dec 17 2018 86 | 7.1.23 Mar 9 2019 87 | 7.1.24 Jun 14 2019 88 | 7.1.24-h1 Aug 15 2019 89 | 7.1.25 Aug 30 2019 90 | 7.1.26 Apr 21 2020 91 | 8.0.0 Jan 25 2017 92 | 8.0.1 Mar 9 2017 93 | 8.0.2 Apr 25 2017 94 | 8.0.3 Jun 8 2017 95 | 8.0.3-h4 Jun 22 2017 96 | 8.0.4 Jul 21 2017 97 | 8.0.5 Sep 10 2017 98 | 8.0.6 Nov 4 2017 99 | 8.0.6-h3 Nov 16 2017 100 | 8.0.7 Dec 24 2017 101 | 8.0.8 Jan 31 2018 102 | 8.0.9 Mar 23 2018 103 | 8.0.10 May 4 2018 104 | 8.0.11-h1 Jun 29 2018 105 | 8.0.12 Aug 4 2018 106 | 8.0.13 Sep 18 2018 107 | 8.0.14 Nov 17 2018 108 | 8.0.15 Dec 8 2018 109 | 8.0.16 Feb 12 2019 110 | 8.0.17 Mar 22 2019 111 | 8.0.18 May 13 2019 112 | 8.0.19 Jun 20 2019 113 | 8.0.19-h1 Aug 15 2019 114 | 8.0.20 Oct 18 2019 115 | 8.1.0 Mar 1 2018 116 | 8.1.1 Apr 23 2018 117 | 8.1.2 Jun 6 2018 118 | 8.1.3 Aug 8 2018 119 | 8.1.4 Oct 5 2018 120 | 8.1.5 Nov 21 2018 121 | 8.1.6 Jan 17 2019 122 | 8.1.6-h2 Jan 23 2019 123 | 8.1.7 Mar 13 2019 124 | 8.1.8 Apr 30 2019 125 | 8.1.8-h5 Jun 17 2019 126 | 8.1.9 Jul 3 2019 127 | 8.1.9-h4 Aug 15 2019 128 | 8.1.10 Aug 21 2019 129 | 8.1.11 Oct 12 2019 130 | 8.1.12 Dec 10 2019 131 | 8.1.13 Jan 25 2020 132 | 8.1.14 Apr 1 2020 133 | 8.1.14-h2 Apr 18 2020 134 | 8.1.15 Jun 13 2020 135 | 8.1.15-h3 Jun 23 2020 136 | 8.1.16 Aug 12 2020 137 | 8.1.17 Sep 23 2020 138 | 8.1.18 Nov 17 2020 139 | 9.0.0 Jan 29 2019 140 | 9.0.1 Mar 26 2019 141 | 9.0.2 May 7 2019 142 | 9.0.2-h4 Jun 21 2019 143 | 9.0.3 Jul 10 2019 144 | 9.0.3-h2 Jul 18 2019 145 | 9.0.3-h3 Aug 14 2019 146 | 9.0.4 Sep 10 2019 147 | 9.0.5 Nov 7 2019 148 | 9.0.6 Jan 24 2020 149 | 9.0.7 Mar 13 2020 150 | 9.0.8 Apr 7 2020 151 | 9.0.9 Jun 20 2020 152 | 9.0.10 Aug 20 2020 153 | 9.0.11 Oct 7 2020 154 | 9.0.12 Nov 24 2020 155 | 9.1.0 Dec 11 2019 156 | 9.1.0-h3 Dec 21 2019 157 | 9.1.1 Jan 24 2020 158 | 9.1.2 Mar 30 2020 159 | 9.1.2-h1 Apr 9 2020 160 | 9.1.3 Jun 20 2020 161 | 9.1.3-h1 Jun 26 2020 162 | 9.1.4 Jul 27 2020 163 | 9.1.5 Sep 16 2020 164 | 9.1.6 Oct 23 2020 165 | 9.1.7 Dec 15 2020 166 | 9.1.8 Feb 05 2021 167 | 10.0.0 Jul 16 2020 168 | 10.0.1 Aug 28 2020 169 | 10.0.2 Oct 27 2020 170 | 10.0.3 Dec 07 2020 171 | 10.1.7 Sep 06 2022 172 | 10.2.0 Feb 26 2022 173 | 10.2.0-h3 Apr 18 2024 174 | 10.2.1 Apr 13 2022 175 | 10.2.1-h2 Apr 18 2024 176 | 10.2.2 Jun 02 2022 177 | 10.2.2-h2 Aug 08 2022 178 | 10.2.2-h5 Apr 18 2024 179 | 10.2.3 Sep 27 2022 180 | 10.2.3-h2 Dec 09 2022 181 | 10.2.3-h4 Feb 08 2023 182 | 10.2.3-h9 Nov 03 2023 183 | 10.2.3-h11 Dec 19 2023 184 | 10.2.3-h12 Dec 19 2023 185 | 10.2.3-h13 Apr 18 2024 186 | 10.2.4 Mar 26 2023 187 | 10.2.4-h2 May 12 2023 188 | 10.2.4-h3 Jun 29 2023 189 | 10.2.4-h4 Jul 25 2023 190 | 10.2.4-h16 Apr 18 2024 191 | 10.2.5 Aug 15 2023 192 | 10.2.5-h6 Apr 16 2024 193 | 10.2.6 Sep 20 2023 194 | 10.2.6-h1 Jan 03 2024 195 | 10.2.6-h3 Apr 16 2024 196 | 10.2.7 Nov 02 2023 197 | 10.2.7-h3 Dec 16 2023 198 | 10.2.7-h6 Feb 28 2024 199 | 10.2.7-h8 Apr 15 2024 200 | 10.2.8 Feb 08 2024 201 | 10.2.8-h3 Apr 15 2024 202 | 10.2.9 Mar 30 2024 203 | 10.2.9-h1 Apr 14 2024 204 | 11.0.0 Nov 17 2022 205 | 11.0.0-h1 Nov 03 2023 206 | 11.0.0-h3 Apr 18 2024 207 | 11.0.1 Mar 26 2023 208 | 11.0.1-h4 Apr 18 2024 209 | 11.0.2 Jun 23 2023 210 | 11.0.2-h4 Apr 16 2024 211 | 11.0.3 Oct 26 2023 212 | 11.0.3-h3 Jan 13 2024 213 | 11.0.3-h5 Feb 20 2024 214 | 11.0.3-h5 Feb 20 2024 215 | 11.0.3-h10 Apr 16 2024 216 | 11.0.4 Apr 07 2024 217 | 11.0.4-h1 Apr 14 2024 218 | 11.0.4-h2 Apr 17 2024 219 | 11.1.0 Oct 31 2023 220 | 11.1.0-h2 Dec 22 2023 221 | 11.1.0-h3 Apr 16 2024 222 | 11.1.1 Dec 22 2023 223 | 11.1.1-h1 Apr 16 2024 224 | 11.1.2 Feb 23 2024 225 | 11.1.2-h1 Mar 09 2024 226 | 11.1.2-h3 Apr 14 2024 227 | -------------------------------------------------------------------------------- /panos-scanner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """ 4 | Developed with <3 by the Bishop Fox Continuous Attack Surface Testing (CAST) team. 5 | https://www.bishopfox.com/continuous-attack-surface-testing/how-cast-works/ 6 | 7 | Author: @noperator 8 | Purpose: Determine the software version of a remote PAN-OS target. 9 | Notes: - Requires version-table.txt in the same directory. 10 | - Usage of this tool for attacking targets without prior mutual 11 | consent is illegal. It is the end user's responsibility to obey 12 | all applicable local, state, and federal laws. Developers assume 13 | no liability and are not responsible for any misuse or damage 14 | caused by this program. 15 | Usage: python3 panos-scanner.py [-h] [-v] [-s] -t TARGET 16 | """ 17 | 18 | import argparse 19 | import datetime 20 | import json 21 | import logging 22 | import requests 23 | import requests.exceptions 24 | import time 25 | import urllib3 26 | import urllib3.exceptions 27 | import re 28 | from urllib.parse import urlparse 29 | 30 | urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) 31 | 32 | verbose = False 33 | 34 | # timeout value in seconds 35 | default_timeout = 2 36 | 37 | # proxies = { 38 | # 'https': 'http://127.0.0.1:8080', 39 | # } 40 | 41 | # Set up logging. 42 | logging.basicConfig( 43 | format="%(asctime)s %(levelname)-8s [%(funcName)s] %(message)s", 44 | datefmt="%Y-%m-%dT%H:%M:%SZ", 45 | ) 46 | logger = logging.getLogger(__name__) 47 | if verbose: 48 | logger.setLevel(logging.INFO) 49 | else: 50 | logger.setLevel(logging.ERROR) 51 | 52 | logging.Formatter.converter = time.gmtime 53 | 54 | 55 | def etag_to_datetime(etag: str) -> datetime.date: 56 | if etag.find('-'): 57 | epoch_hex = etag.split('-', 1)[0] 58 | else: 59 | epoch_hex = etag[-8:] 60 | try: 61 | answer = datetime.datetime.fromtimestamp(int(epoch_hex, 16)).date() 62 | toto = int(epoch_hex, 16) 63 | except : 64 | answer="" 65 | 66 | return answer 67 | 68 | 69 | def last_modified_to_datetime(last_modified: str) -> datetime.date: 70 | return datetime.datetime.strptime(last_modified[:-4], "%a, %d %b %Y %X").date() 71 | 72 | 73 | def get_resource(target: str, resource: str, date_headers: dict, errors: tuple) -> dict: 74 | headers = { 75 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:54.0) Gecko/20100101 Firefox/54.0", 76 | "Connection": "close", 77 | "Accept-Language": "en-US,en;q=0.5", 78 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", 79 | "Upgrade-Insecure-Requests": "1", 80 | } 81 | logger.debug(resource) 82 | try: 83 | resp = requests.get( 84 | "%s/%s" % (target, resource), headers=headers, timeout=default_timeout, verify=False 85 | ) 86 | resp.raise_for_status() 87 | return { 88 | h: resp.headers[h].strip('"') for h in date_headers if h in resp.headers 89 | } 90 | 91 | except (requests.exceptions.HTTPError, requests.exceptions.ReadTimeout) as e: 92 | logger.warning(type(e).__name__) 93 | return None 94 | except errors as e: 95 | raise e 96 | 97 | 98 | def load_version_table(version_table: str) -> dict: 99 | with open(version_table, "r") as f: 100 | entries = [line.strip().split() for line in f.readlines()] 101 | return { 102 | e[0]: datetime.datetime.strptime(" ".join(e[1:]), "%b %d %Y").date() 103 | for e in entries 104 | } 105 | 106 | 107 | def check_date(version_table: dict, date: datetime.date) -> list: 108 | matches = [] 109 | for n in [0, 1, -1, 2, -2]: 110 | nearby_date = date + datetime.timedelta(n) 111 | versions = [ 112 | version for version, date in version_table.items() if date == nearby_date 113 | ] 114 | if not len(versions): 115 | continue 116 | precision = "exact" if n == 0 else "approximate" 117 | append = True 118 | for match in matches: 119 | if match["precision"] == precision: 120 | append = False 121 | if append: 122 | matches.append( 123 | { 124 | "date": nearby_date, 125 | "versions": versions, 126 | "precision": precision 127 | } 128 | ) 129 | if precision == 'approximate': 130 | logger.debug(f"Appromixate version found for : {date.strftime('%d %b %Y')}") 131 | 132 | return matches 133 | 134 | 135 | def get_matches(date_headers: dict, resp_headers: dict, version_table: dict) -> list: 136 | matches = [] 137 | for header in date_headers.keys(): 138 | if header in resp_headers: 139 | date = globals()[date_headers[header]](resp_headers[header]) 140 | if date != "": 141 | matches.extend(check_date(version_table, date)) 142 | if len(matches) == 0 and 'date' in locals(): # if no matching but data return add as debug log 143 | logger.debug(f"no matching for : {date.strftime('%b %d %Y')}") 144 | 145 | return matches 146 | 147 | 148 | def strip_url(fullurl: str) -> str: 149 | """ 150 | Extracts the host and port from a full URL and returns it. 151 | 152 | Args: 153 | fullurl (str): The full URL string. 154 | 155 | Returns: 156 | str: The host and port extracted from the URL. 157 | """ 158 | parsed_url = urlparse(fullurl) 159 | # Combining the hostname and port if port is specified 160 | if parsed_url.port: 161 | return f"{parsed_url.hostname}:{parsed_url.port}" 162 | else: 163 | return parsed_url.hostname 164 | 165 | 166 | def get_targets_from_file(inputfile: str): 167 | """ 168 | Read lines from the input file and return valid targets in the format 'https://1.2.3.4/' or 'https://1.2.3.4:8889/'. 169 | 170 | Args: 171 | inputfile (str): Path to the input file. 172 | 173 | Returns: 174 | list: List of valid targets in the format 'https://1.2.3.4/' or 'https://1.2.3.4:8889/'. 175 | 176 | Raises: 177 | ValueError: If any line in the file does not match the specified format. 178 | IOError: If there's an error reading the input file. 179 | """ 180 | targets = [] 181 | try: 182 | with open(inputfile, 'r') as file: 183 | for line in file: 184 | line = line.strip() 185 | if re.match(r'^https://(?:[0-9]{1,3}\.){3}[0-9]{1,3}(?::\d+)?/$', line): 186 | targets.append(line) 187 | else: 188 | raise ValueError(f"Invalid format in line: {line}") 189 | except IOError as e: 190 | raise IOError(f"Error reading file: {e}") 191 | 192 | return targets 193 | 194 | def get_cve_link(results): 195 | outputlink = "https://security.paloaltonetworks.com/?product=PAN-OS&sort=-cvss" 196 | for match in results: 197 | if match["precision"] == "exact": 198 | outputlink += "&version=PAN-OS+" + match["versions"][0] 199 | break 200 | return outputlink 201 | def main(): 202 | 203 | # Parse arguments. 204 | parser = argparse.ArgumentParser( 205 | description=""" 206 | Determine the software version of a remote PAN-OS target. Requires 207 | version-table.txt in the same directory. See 208 | https://security.paloaltonetworks.com/?product=PAN-OS for security 209 | advisories for specific PAN-OS versions. 210 | """ 211 | ) 212 | parser.add_argument("-v", dest="verbose", action="store_true", help="verbose output") 213 | parser.add_argument("-s", dest="stop", action="store_true", help="stop after one exact match") 214 | parser.add_argument("-cve", dest="cve", action="store_true", help="Add link to official PAN security advisory page") 215 | 216 | group = parser.add_mutually_exclusive_group(required=True) 217 | group.add_argument("-t", dest="target", help="https://example.com") 218 | group.add_argument("-f", dest="file", help="inputfile. One target per line. See target format") 219 | 220 | args = parser.parse_args() 221 | 222 | static_resources = [ 223 | "login/images/favicon.ico", 224 | "global-protect/portal/images/bg.png", 225 | "global-protect/portal/css/login.css", 226 | "js/Pan.js", 227 | "global-protect/portal/images/favicon.ico", 228 | ] 229 | 230 | version_table = load_version_table("version-table.txt") 231 | 232 | # The keys in "date_headers" represent HTTP response headers that we're 233 | # looking for. Each of those headers maps to a function in this namespace 234 | # that knows how to decode that header value into a datetime. 235 | date_headers = { 236 | "ETag": "etag_to_datetime", 237 | "Last-Modified": "last_modified_to_datetime", 238 | } 239 | 240 | # These errors are indicative of target-level issues. Don't continue 241 | # requesting other resources when encountering these; instead, bail. 242 | target_errors = ( 243 | requests.exceptions.ConnectTimeout, 244 | requests.exceptions.SSLError, 245 | requests.exceptions.ConnectionError, 246 | ) 247 | if args.file is not None: 248 | targets_to_scan = get_targets_from_file(args.file) 249 | else: 250 | targets_to_scan = [args.target] 251 | 252 | if args.verbose: 253 | logger.setLevel(logging.DEBUG) 254 | logger.debug(f"scanning : {len(targets_to_scan)} target(s)") 255 | logger.debug(f"scanning target: {targets_to_scan}") 256 | 257 | # Let's scan each target 258 | for target_to_scan in targets_to_scan: 259 | 260 | # A match is a dictionary containing a date/version pair per target. 261 | total_matches = [] 262 | 263 | # Total of responses per target 264 | total_responses = 0 265 | 266 | # Check for the presence of each static resource. 267 | for resource in static_resources: 268 | try: 269 | resp_headers = get_resource( 270 | target_to_scan, 271 | resource, 272 | date_headers.keys(), 273 | target_errors 274 | ) 275 | 276 | except target_errors as e: 277 | logger.error(f"could not connect to target: {type(e).__name__}") 278 | continue 279 | if resp_headers == None: 280 | continue 281 | if len(resp_headers) > 0 : 282 | total_responses += len(resp_headers) 283 | # Convert date-related HTTP headers to a standardized format, and 284 | # store any matching version strings. 285 | resource_matches = get_matches(date_headers, resp_headers, version_table) 286 | for match in resource_matches: 287 | match["resource"] = resource 288 | total_matches.extend(resource_matches) 289 | 290 | # Stop if we've got an exact match. 291 | stop = False 292 | if args.stop: 293 | for match in resource_matches: 294 | if match["precision"] == "exact": 295 | stop = True 296 | if stop: 297 | continue 298 | 299 | # Print results. 300 | target_to_print = strip_url(target_to_scan) 301 | try: 302 | cve_link = get_cve_link(resource_matches) 303 | except: 304 | cve_link = "" 305 | if args.cve and cve_link != "": 306 | results = {"target": target_to_print, "match": {}, "all": total_matches, "cvelink": cve_link} 307 | else: 308 | results = {"target": target_to_print, "match": {}, "all": total_matches} 309 | if total_responses == 0: # not a single answer 310 | logger.error("Web service is up but no URL returned an answer. Are you sure it has GlobalProtect active ? ") 311 | if not args.verbose: 312 | logger.error("Try adding -v option for more verbosity") 313 | continue 314 | 315 | if not len(total_matches): 316 | logger.error("no matching versions found for : " + target_to_scan) 317 | continue 318 | else: 319 | closest = sorted(total_matches, key=lambda x: x["precision"], reverse=True)[0] 320 | results["match"] = closest 321 | 322 | print(json.dumps(results, default=str)) 323 | 324 | 325 | if __name__ == "__main__": 326 | main() 327 | --------------------------------------------------------------------------------