diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43583a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/lib/config.json +*.pyc \ No newline at end of file diff --git a/README.md b/README.md index c99769a..3a2b6f1 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,6 @@ It also has a database with known vulnerabilities for core and extensions. Typo3Scan does not exploit any vulnerabilities! It´s soley purpose was to enumerate version info and installed extensions in penetration tests ever since. -**Note:** -When I started this project many years ago, the version information could be easily read from text files (Readmes, Changelogs, etc.). Since then a lot has changed. -Typo3 now restricts access to directories and files by default and version information of extensions may not available in files anymore. -In addition, various basic functions have changed over time. -For these reasons this tool will probably *not receive further major releases*. - - ## Installation You can download the latest tarball by clicking [here](https://github.com/whoot/Typo3Scan/tarball/master) or latest zipball by clicking [here](https://github.com/whoot/Typo3Scan/zipball/master). diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 4b41eb4..78975cb 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,3 +1,8 @@ +## Version 0.6 + +* Added version regex for composer installations +* Output bugfix + ## Version 0.5.2 * Removed 'interesting header' output diff --git a/lib/config.json b/lib/config.json index 86777f4..7fcf83f 100644 --- a/lib/config.json +++ b/lib/config.json @@ -1 +1 @@ -{"threads": 5, "timeout": 10, "cookie": "", "auth": "", "User-Agent": "Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/46.0.2486.0 Safari/537.36 Edge/13.9200"} \ No newline at end of file +{"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 diff --git a/lib/domain.py b/lib/domain.py index cfc2801..ac5ea26 100644 --- a/lib/domain.py +++ b/lib/domain.py @@ -21,7 +21,7 @@ import re import string import random import sqlite3 -from colorama import Fore +from colorama import Fore, Style import lib.request as request from pkg_resources import parse_version @@ -98,7 +98,8 @@ class Domain: 'typo3_src/INSTALL.md':'INSTALLING TYPO3', 'typo3_src/INSTALL.txt':'INSTALLING TYPO3', 'typo3_src/LICENSE.txt':'TYPO3', - 'typo3_src/CONTRIBUTING.md':'TYPO3 CMS' + 'typo3_src/CONTRIBUTING.md':'TYPO3 CMS', + 'typo3_src/composer.json':'TYPO3' } for path, regex in files.items(): try: @@ -135,12 +136,12 @@ class Domain: response = request.get_request('{}/typo3/index.php'.format(self.get_path())) searchTitle = re.search('(.*)', response['html']) if searchTitle and 'Login' in searchTitle.group(0): - print(' \u251c', Fore.GREEN + '{}/typo3/index.php'.format(self.get_path()) + Fore.RESET) + print(' \u251c {}'.format(Fore.GREEN + '{}/typo3/index.php'.format(self.get_path()) + Fore.RESET)) elif ('Backend access denied: The IP address of your client' in response['html']) or (response['status_code'] == 403): - print(' \u251c', Fore.GREEN + '{}/typo3/index.php'.format(self.get_path()) + Fore.RESET) - print(' \u251c', Fore.YELLOW + 'But access is forbidden (IP Address Restriction)' + Fore.RESET) + 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)) else: - print(' \u251c', Fore.RED + 'Could not be found' + Fore.RESET) + print(' \u251c {}'.format(Fore.RED + 'Could not be found' + Fore.RESET)) def search_typo3_version(self): """ @@ -148,30 +149,37 @@ 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/ChangeLog': '[Tt][Yy][Pp][Oo]3 (\d{1,2}\.\d{1,2}\.?[0-9]?[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/typo3/sysext/backend/composer.json': '"typo3/cms-core": "(\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})', + '/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', - '/INSTALL.md': '[Tt][Yy][Pp][Oo]3 [Cc][Mm][Ss] (\d{1,2}(.\d{1,2})?)', - '/INSTALL.txt': '[Tt][Yy][Pp][Oo]3 v(\d{1})' + '/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) - if not (response is None) and (version is None or (len(response) > len(version))): + if response and (version is None or (len(response) > len(version))): version = response version_path = path print(' |\n[+] Version Information') - if not (version is None): - print(' \u251c {}'.format(Fore.GREEN + version + Fore.RESET)) - print(' \u251c see: {}{}'.format(self.get_path(), version_path)) + if version: + print(' \u251c Identified Version: '.ljust(28) + '{}'.format(Style.BRIGHT + Fore.GREEN + version + Style.RESET_ALL)) + print(' \u251c Version File: '.ljust(28) + '{}{}'.format(self.get_path(), version_path)) if len(version) == 3: print(' \u251c Could not identify exact version.') react = input(' \u251c Do you want to print all vulnerabilities for branch {}? (y/n): '.format(version)) @@ -179,21 +187,26 @@ class Domain: version = version + '.0' else: return False - print(' \u2514 Known vulnerabilities:\n') # 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() - if not data: - print(' \u251c None.') - else: - for vuln in data: + vuln_list = [] + if data: + 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(vuln[3]): - print(' [!] {}'.format(Fore.RED + vuln[0] + Fore.RESET)) - print(' \u251c Vulnerability Type:'.ljust(29), vuln[1]) - print(' \u251c Subcomponent:'.ljust(29), vuln[2]) - print(' \u2514 Affected Versions:'.ljust(29), '{} - {}\n'.format(vuln[3], vuln[4])) + 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: + print(' \u2514 Known Vulnerabilities:\n') + for vulnerability in vuln_list: + print(vulnerability) + else: + print(' \u2514 No Known Vulnerabilities') else: - print(' \u2514', Fore.RED + 'No version information found.' + Fore.RESET) \ No newline at end of file + 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 f88c79b..ca73e51 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -19,7 +19,7 @@ #------------------------------------------------------------------------------- import sqlite3 -from colorama import Fore +from colorama import Fore, Style import lib.request as request from lib.thread_pool import ThreadPool from pkg_resources import parse_version @@ -72,9 +72,6 @@ class Extensions: thread_pool.add_job((request.version_information, (values['url'] + 'ChangeLog', None))) thread_pool.add_job((request.version_information, (values['url'] + 'CHANGELOG.md', None))) thread_pool.add_job((request.version_information, (values['url'] + 'ChangeLog.txt', None))) - #thread_pool.add_job((request.version_information, (values['url'] + 'Readme.txt', None))) - #thread_pool.add_job((request.version_information, (values['url'] + 'README.md', None))) - #thread_pool.add_job((request.version_information, (values['url'] + 'README.rst', None))) thread_pool.start(threads, version_search=True) @@ -95,30 +92,35 @@ class Extensions: def output(self, extension_dict, database): conn = sqlite3.connect(database) c = conn.cursor() - print('\n\n [+] Extension information') + print('\n\n [+] Extension Information') print(' -------------------------') for extension,info in extension_dict.items(): - c.execute('SELECT title,state FROM extensions where extensionkey=?', (extension,)) + c.execute('SELECT title,version,state FROM extensions where extensionkey=?', (extension,)) data = c.fetchone() - print(' [+] Name: {}'.format(Fore.GREEN + extension + Fore.RESET)) - print(' \u251c Title: {}'.format(data[0])) - print(' \u251c State (of current version): {}'.format(data[1])) + print(Style.BRIGHT + ' [+] {}'.format(Fore.GREEN + extension + Style.RESET_ALL)) + 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])) if 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 Version: {}'.format(Fore.GREEN + info['version'] + Fore.RESET)) + print(' \u251c Identified Version: '.ljust(28) + '{}'.format(Style.BRIGHT + Fore.GREEN + info['version'] + Style.RESET_ALL)) + vuln_list = [] if data: - print(' \u251c see: {}'.format(info['file'])) - print(' \u2514 Known vulnerabilities:\n') - for vuln in data: - if parse_version(info['version']) <= parse_version(vuln[2]): - print(' [!] {}'.format(Fore.RED + vuln[0] + Fore.RESET)) - print(' \u251c Vulnerability Type:'.ljust(29), vuln[1]) - print(' \u2514 Affected Versions:'.ljust(29), '{} - {}'.format(vuln[2], vuln[3])) - print() + 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())) + if vuln_list: + print(' \u251c Version File: '.ljust(28) + '{}'.format(info['file'])) + print(' \u2514 Known Vulnerabilities:\n') + for vulnerability in vuln_list: + print(vulnerability) else: - print(' \u2514 see: {}'.format(info['file'])) + print(' \u2514 Version File: '.ljust(28) + '{}'.format(info['file'])) else: - print(' \u2514 Version: -unknown-') + print(' \u2514 Identified Version: '.ljust(28) + '-unknown-') print() conn.close() \ No newline at end of file diff --git a/lib/request.py b/lib/request.py index 6152ccb..e766ff3 100644 --- a/lib/request.py +++ b/lib/request.py @@ -119,23 +119,23 @@ def version_information(url, regex): else: r = requests.get(url, stream=True, timeout=config['timeout'], headers=custom_headers, verify=False) if r.status_code == 200: + version = None if ('manual.sxw' in url) and not ('Page Not Found' in r.text): return 'check manually' - try: - for content in r.iter_content(chunk_size=400, decode_unicode=False): + for content in r.iter_content(chunk_size=400, decode_unicode=False): + try: search = re.search(regex, str(content)) version = search.group(1) - r.close() - return version - except: - try: - search = re.search('([0-9]+-[0-9]+-[0-9]+)', str(content)) - version = search.group(1) - r.close() - return version except: + try: + search = re.search('([0-9]+-[0-9]+-[0-9]+)', str(content)) + version = search.group(1) + except: + continue + if version: r.close() - return None + break + return version except requests.exceptions.Timeout: print(Fore.RED + ' [x] Connection timed out on "{}"'.format(url) + Fore.RESET) except requests.exceptions.RequestException as e: diff --git a/typo3scan.py b/typo3scan.py index b5061c1..4d0a245 100644 --- 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.5.2' +__version__ = '0.' __program__ = 'Typo3Scan' __description__ = 'Automatic Typo3 enumeration tool' __author__ = 'https://github.com/whoot' @@ -31,7 +31,7 @@ import argparse from lib.domain import Domain from lib.extensions import Extensions from colorama import Fore, init, deinit, Style -init() +init(strip=False) class Typo3: def __init__(self): @@ -90,12 +90,12 @@ class Typo3: for row in c.execute('SELECT extensionkey FROM extensions'): self.__extensions.append(row[0]) conn.close() - print (' \u251c Brute-Forcing {} extensions'.format(len(self.__extensions))) + print (' \u251c Brute-Forcing {} Extensions'.format(len(self.__extensions))) extensions = Extensions() ext_list = extensions.search_extension(check.get_path(), self.__extensions, args.threads) if ext_list: print ('\n \u251c Found {} extensions'.format(len(ext_list))) - print (' |\n \u251c Brute-Forcing version information'.format(len(self.__extensions))) + 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) else: