diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 78975cb..d0c7e58 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,3 +1,8 @@ +## Version 0.6.1 + +* Bugfix of URL determination +* Support for JSON output + ## Version 0.6 * Added version regex for composer installations diff --git a/lib/config.json b/lib/config.json index 7fcf83f..f44949d 100644 --- a/lib/config.json +++ b/lib/config.json @@ -1 +1 @@ -{"threads": 5, "timeout": 10, "cookie": "", "auth": "", "User-Agent": "Mozilla/5.0 (X11; Linux i686; rv:64.0) Gecko/20100101 Firefox/64.0"} \ No newline at end of file +{"threads": 5, "timeout": 10, "cookie": "", "auth": "", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2"} \ No newline at end of file diff --git a/lib/domain.py b/lib/domain.py index ac5ea26..468fc7f 100644 --- a/lib/domain.py +++ b/lib/domain.py @@ -29,9 +29,11 @@ class Domain: """ This class stores following information about a domain: name: URL of the domain - typo3: If Typo3 is installed - typo3_version: Typo3 Version path: Full path to Typo3 installation + typo3: If Typo3 is installed + backend: URL to Typo3 backend login + typo3_version: Typo3 Version + typo3_vulnerabilities: List of known CORE vulnerabilities installed_extensions: List of all installed extensions """ def __init__(self, name): @@ -39,10 +41,12 @@ class Domain: self.__name = 'https://' + name else: self.__name = name - self.__typo3 = False - self.__typo3_version = '' self.__path = '' - self.__installed_extensions = {} + self.__typo3 = False + self.__backend = 'Could not be found' + self.__typo3_version = 'Unknown' + self.__typo3_vulnerabilities = '' + self.__installed_extensions = {'installed': 0} def get_name(self): return self.__name @@ -50,23 +54,35 @@ class Domain: def set_name(self, name): self.__name = name + def set_path(self, path): + self.__path = path + + def get_path(self): + return self.__path + def is_typo3(self): return self.__typo3 def set_typo3(self): self.__typo3 = True + def set_backend(self, url): + self.__backend = url + + def get_backend(self): + return self.__backend + def set_typo3_version(self, version): self.__typo3_version = version def get_typo3_version(self): return self.__typo3_version - def set_path(self, path): - self.__path = path + def set_typo3_vulns(self, vuln): + self.__typo3_vulnerabilities = vuln - def get_path(self): - return self.__path + def get_typo3_vulns(self): + return self.__typo3_vulnerabilities def check_root(self): """ @@ -76,16 +92,13 @@ class Domain: in order to determine the Typo3 installation path. """ response = request.get_request('{}'.format(self.get_name())) + self.set_name(response['url'][:-1]) full_path = self.get_name() if re.search('powered by TYPO3', response['html']): self.set_typo3() - path = re.search('="/?(\S*?)/?(?:typo3temp|typo3conf)/'.format(self.get_name()), response['html']) - if path and path.groups()[0] != '': - path = path.groups()[0].replace(self.get_name(), '') - if path != '': - full_path = '{}/{}'.format(self.get_name(), path) - if full_path.endswith('/'): - full_path = full_path[:-1] + path = re.search('="(?:{})/?(\S*?)/?(?:typo3temp|typo3conf)/'.format(self.get_name()), response['html']) + if path and path.group(1) != '': + full_path = '{}/{}'.format(self.get_name(), path) self.set_path(full_path) def check_default_files(self): @@ -137,9 +150,11 @@ class Domain: searchTitle = re.search('(.*)', response['html']) if searchTitle and 'Login' in searchTitle.group(0): print(' \u251c {}'.format(Fore.GREEN + '{}/typo3/index.php'.format(self.get_path()) + Fore.RESET)) + self.set_backend('{}/typo3/index.php'.format(self.get_path())) elif ('Backend access denied: The IP address of your client' in response['html']) or (response['status_code'] == 403): print(' \u251c {}'.format(Fore.GREEN + '{}/typo3/index.php'.format(self.get_path()) + Fore.RESET)) print(' \u251c {}'.format(Fore.YELLOW + 'But access is forbidden (IP Address Restriction)' + Fore.RESET)) + self.set_backend('{}/typo3/index.php'.format(self.get_path())) else: print(' \u251c {}'.format(Fore.RED + 'Could not be found' + Fore.RESET)) @@ -149,29 +164,29 @@ class Domain: The exact version can be found in the ChangeLog, therefore it will be requested first. Less specific version information can be found in the NEWS or INSTALL file. """ - files = {'/typo3_src/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"([0-9]+\.[0-9]+\.?[0-9x]?[0-9x]?)"', - '/typo3_src/public/typo3/sysext/install/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"([0-9]+\.[0-9]+\.?[0-9x]?[0-9x]?)"', - '/typo3_src/typo3/sysext/adminpanel/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"([0-9]+\.[0-9]+\.?[0-9x]?[0-9x]?)"', - '/typo3_src/typo3/sysext/backend/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)"', - '/typo3_src/typo3/sysext/info/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)"', - '/typo3_src/ChangeLog': '[Tt][Yy][Pp][Oo]3 (\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', - '/ChangeLog': '[Tt][Yy][Pp][Oo]3 (\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', - '/typo3/sysext/backend/ext_emconf.php': '(?:CMS |typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', - '/typo3_src/typo3/sysext/install/Start/Install.php': '(?:CMS |typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', - '/typo3/sysext/install/Start/Install.php': '(?:CMS |typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', - '/typo3_src/NEWS.txt': 'http://wiki.typo3.org/TYPO3_(\d{1,2}\.\d{1,2})', - '/typo3_src/NEWS.md': '[Tt][Yy][Pp][Oo]3 [Cc][Mm][Ss] (\d{1,2}\.\d{1,2}) - WHAT\'S NEW', - '/NEWS.txt': 'http://wiki.typo3.org/TYPO3_(\d{1,2}\.\d{1,2})', - '/NEWS.md': '[Tt][Yy][Pp][Oo]3 [Cc][Mm][Ss] (\d{1,2}\.\d{1,2}) - WHAT\'S NEW', - '/typo3_src/INSTALL.md': '(?:typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9x]?[0-9]?)', - '/typo3_src/INSTALL.txt': '(?:typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9x]?[0-9]?)', - '/INSTALL.md': '(?:typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9x]?[0-9]?)', - '/INSTALL.txt': '(?:typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9x]?[0-9]?)' + files = {'typo3_src/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"([0-9]+\.[0-9]+\.?[0-9x]?[0-9x]?)"', + 'typo3_src/public/typo3/sysext/install/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"([0-9]+\.[0-9]+\.?[0-9x]?[0-9x]?)"', + 'typo3_src/typo3/sysext/adminpanel/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"([0-9]+\.[0-9]+\.?[0-9x]?[0-9x]?)"', + 'typo3_src/typo3/sysext/backend/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)"', + 'typo3_src/typo3/sysext/info/composer.json': '(?:"typo3/cms-core":|"typo3/cms-backend":)\s?"(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)"', + 'typo3_src/ChangeLog': '[Tt][Yy][Pp][Oo]3 (\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', + 'ChangeLog': '[Tt][Yy][Pp][Oo]3 (\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', + 'typo3/sysext/backend/ext_emconf.php': '(?:CMS |typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', + 'typo3_src/typo3/sysext/install/Start/Install.php': '(?:CMS |typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', + 'typo3/sysext/install/Start/Install.php': '(?:CMS |typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', + 'typo3_src/NEWS.txt': 'http://wiki.typo3.org/TYPO3_(\d{1,2}\.\d{1,2})', + 'typo3_src/NEWS.md': '[Tt][Yy][Pp][Oo]3 [Cc][Mm][Ss] (\d{1,2}\.\d{1,2}) - WHAT\'S NEW', + 'NEWS.txt': 'http://wiki.typo3.org/TYPO3_(\d{1,2}\.\d{1,2})', + 'NEWS.md': '[Tt][Yy][Pp][Oo]3 [Cc][Mm][Ss] (\d{1,2}\.\d{1,2}) - WHAT\'S NEW', + 'typo3_src/INSTALL.md': '(?:typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9x]?[0-9]?)', + 'typo3_src/INSTALL.txt': '(?:typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9x]?[0-9]?)', + 'INSTALL.md': '(?:typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9x]?[0-9]?)', + 'INSTALL.txt': '(?:typo3_src-)(\d{1,2}\.\d{1,2}\.?[0-9x]?[0-9]?)' } version = None for path, regex in files.items(): - response = request.version_information(self.get_path()+path, regex) + response = request.version_information('{}/{}'.format(self.get_path(), path), regex) if response and (version is None or (len(response) > len(version))): version = response version_path = path @@ -186,27 +201,29 @@ class Domain: if react.startswith('y'): version = version + '.0' else: - return False + return False + self.set_typo3_version(version) # sqlite stuff conn = sqlite3.connect('lib/typo3scan.db') c = conn.cursor() c.execute('SELECT advisory, vulnerability, subcomponent, affected_version_max, affected_version_min FROM core_vulns WHERE (?<=affected_version_max AND ?>=affected_version_min)', (version, version,)) data = c.fetchall() - vuln_list = [] if data: + json_list = {} for vulnerability in data: # maybe instead use this: https://oraerr.com/database/sql/how-to-compare-version-string-x-y-z-in-mysql-2/ if parse_version(version) <= parse_version(vulnerability[3]): - vuln_list.append(Style.BRIGHT + ' [!] {}'.format(Fore.RED + vulnerability[0] + Style.RESET_ALL)) - vuln_list.append(' \u251c Vulnerability Type:'.ljust(28) + vulnerability[1]) - vuln_list.append(' \u251c Subcomponent:'.ljust(28) + vulnerability[2]) - vuln_list.append(' \u251c Affected Versions:'.ljust(28) + '{} - {}'.format(vulnerability[3], vulnerability[4])) - vuln_list.append(' \u2514 Advisory URL:'.ljust(28) + 'https://typo3.org/security/advisory/{}\n'.format(vulnerability[0].lower())) - if vuln_list: + json_list[vulnerability[0]] = {'Type': vulnerability[1], 'Subcomponent': vulnerability[2], 'Affected': '{} - {}'.format(vulnerability[3], vulnerability[4]), 'Advisory': 'https://typo3.org/security/advisory/{}'.format(vulnerability[0].lower())} + if json_list: + self.set_typo3_vulns(json_list) print(' \u2514 Known Vulnerabilities:\n') - for vulnerability in vuln_list: - print(vulnerability) - else: - print(' \u2514 No Known Vulnerabilities') + for vulnerability in json_list.keys(): + print(Style.BRIGHT + ' [!] {}'.format(Fore.RED + vulnerability + Style.RESET_ALL)) + print(' \u251c Vulnerability Type:'.ljust(28) + json_list[vulnerability]['Type']) + print(' \u251c Subcomponent:'.ljust(28) + json_list[vulnerability]['Subcomponent']) + print(' \u251c Affected Versions:'.ljust(28) + json_list[vulnerability]['Affected']) + print(' \u2514 Advisory URL:'.ljust(28) + json_list[vulnerability]['Advisory'] + '\n') + else: + print(' \u2514 No Known Vulnerabilities') else: print(' \u2514', Fore.RED + 'No Version Information Found.' + Fore.RESET) \ No newline at end of file diff --git a/lib/extensions.py b/lib/extensions.py index ca73e51..79dcc71 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -94,6 +94,7 @@ class Extensions: c = conn.cursor() print('\n\n [+] Extension Information') print(' -------------------------') + json_list = {} for extension,info in extension_dict.items(): c.execute('SELECT title,version,state FROM extensions where extensionkey=?', (extension,)) data = c.fetchone() @@ -101,18 +102,23 @@ class Extensions: print(' \u251c Extension Title: '.ljust(28) + '{}'.format(data[0])) print(' \u251c Extension Repo: '.ljust(28) + 'https://extensions.typo3.org/extension/{}'.format(extension)) print(' \u251c Current Version: '.ljust(28) + '{} ({})'.format(data[1], data[2])) + json_list[extension] = {'Title': data[0], 'Repo': 'https://extensions.typo3.org/extension/{}'.format(extension), 'Current': '{} ({})'.format(data[1], data[2]), 'Version': '', 'Vulnerabilities':''} if info['version']: + json_list[extension].update(Version = info['version']) c.execute('SELECT advisory, vulnerability, affected_version_max, affected_version_min FROM extension_vulns WHERE (extensionkey=? AND ?<=affected_version_max AND ?>=affected_version_min)', (extension, info['version'], info['version'],)) data = c.fetchall() print(' \u251c Identified Version: '.ljust(28) + '{}'.format(Style.BRIGHT + Fore.GREEN + info['version'] + Style.RESET_ALL)) vuln_list = [] if data: + vuln = {} for vulnerability in data: if parse_version(info['version']) <= parse_version(vulnerability[2]): vuln_list.append(Style.BRIGHT + ' [!] {}'.format(Fore.RED + vulnerability[0] + Style.RESET_ALL)) vuln_list.append(' \u251c Vulnerability Type: '.ljust(28) + vulnerability[1]) vuln_list.append(' \u251c Affected Versions: '.ljust(28) + '{} - {}'.format(vulnerability[2], vulnerability[3])) vuln_list.append(' \u2514 Advisory URL:'.ljust(28) + 'https://typo3.org/security/advisory/{}\n'.format(vulnerability[0].lower())) + vuln[vulnerability[0]] = {'Type': vulnerability[1], 'Affected': '{} - {}'.format(vulnerability[2], vulnerability[3]), 'Advisory': 'https://typo3.org/security/advisory/{}'.format(vulnerability[0].lower())} + json_list[extension].update(Vulnerabilities = vuln) if vuln_list: print(' \u251c Version File: '.ljust(28) + '{}'.format(info['file'])) print(' \u2514 Known Vulnerabilities:\n') @@ -121,6 +127,7 @@ class Extensions: else: print(' \u2514 Version File: '.ljust(28) + '{}'.format(info['file'])) else: - print(' \u2514 Identified Version: '.ljust(28) + '-unknown-') + print(' \u2514 Identified Version: '.ljust(28) + '-unknown-') print() - conn.close() \ No newline at end of file + conn.close() + return json_list \ No newline at end of file diff --git a/lib/request.py b/lib/request.py index e766ff3..cd62c3c 100644 --- a/lib/request.py +++ b/lib/request.py @@ -54,6 +54,7 @@ def get_request(url): response['html'] = r.text response['headers'] = r.headers response['cookies'] = r.cookies + response['url'] = r.url return response except requests.exceptions.Timeout as e: print(e) diff --git a/lib/typo3scan.db b/lib/typo3scan.db index 085655a..bc7ef43 100644 Binary files a/lib/typo3scan.db and b/lib/typo3scan.db differ diff --git a/typo3scan.py b/typo3scan.py old mode 100644 new mode 100755 index 4192354..13eefe4 --- a/typo3scan.py +++ b/typo3scan.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/) #------------------------------------------------------------------------------- -__version__ = '0.6' +__version__ = '0.6.1' __program__ = 'Typo3Scan' __description__ = 'Automatic Typo3 enumeration tool' __author__ = 'https://github.com/whoot' @@ -100,9 +100,13 @@ class Typo3: print ('\n \u251c Found {} extensions'.format(len(ext_list))) print (' \u251c Brute-Forcing Version Information'.format(len(self.__extensions))) ext_list = extensions.search_ext_version(ext_list, args.threads) - extensions.output(ext_list, database) + json_ext = extensions.output(ext_list, database) else: print ('\n [!] No extensions found.\n') + if args.json: + json_log = {} + json_log[check.get_name()] = {'Backend': check.get_backend(), 'Version': check.get_typo3_version(), 'Vulnerabilities':check.get_typo3_vulns(), 'Extensions': json_ext} + json.dump(json_log, open('typo3scan.json', 'w')) except KeyboardInterrupt: print('\nReceived keyboard interrupt.\nQuitting...') exit(-1) @@ -155,6 +159,8 @@ Options: --threads THREADS The number of threads to use for enumerating extensions. Default: 5 + --json Output results to json file + General: -u | --update Update the database. -r | --reset Reset the database. @@ -173,6 +179,7 @@ Options: parser.add_argument('--cookie', dest='cookie', type=str, default='') parser.add_argument('--agent', dest='user_agent', type=str, default='') parser.add_argument('--timeout', dest='timeout', type=int, default=10) + parser.add_argument('--json', dest='json', action='store_true') help.add_argument( '-h', '--help', action='store_true') args = parser.parse_args()