This commit is contained in:
whoot
2020-01-04 02:44:07 +01:00
parent 41353ecc3c
commit 48f93f7a16
27 changed files with 1068 additions and 15463 deletions

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 Jan Rude
# Typo3Scan - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2020 Jan Rude
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,84 +15,221 @@
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/)
# along with this program. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/)
#-------------------------------------------------------------------------------
import re
import string
import random
import sqlite3
from colorama import Fore
import lib.request as request
from pkg_resources import parse_version
class Domain(object):
"""
This class stores following information about a domain:
name: URL of the domain
typo3: If Typo3 is installed
typo3_version: Typo3 Version
login_found: Determines of the default login page was found or not
extensions: List of extensions to check for
installed_extensions: List of all installed extensions
interesting_header: List of interesting headers
"""
def __init__(self, name, ext_state, top=False):
if not ('http' in name):
self.__name = 'http://' + name
else:
self.__name = name
self.__typo3 = False
self.__typo3_version = ''
self.__login_found = ''
self.__path = ''
self.__extension_config = [ext_state, top]
self.__extensions = None
self.__installed_extensions = {}
self.__interesing_header = {}
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
installed_extensions: List of all installed extensions
interesting_header: List of interesting headers
"""
def __init__(self, name):
if not ('http' in name):
self.__name = 'https://' + name
else:
self.__name = name
self.__typo3 = False
self.__typo3_version = ''
self.__path = ''
self.__installed_extensions = {}
self.__interesting_header = {}
def get_name(self):
return self.__name
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
def set_name(self, name):
self.__name = name
def is_typo3(self):
return self.__typo3
def get_extensions(self):
return self.__extensions
def set_typo3(self):
self.__typo3 = True
def set_extensions(self, extensions):
self.__extensions = extensions
def set_typo3_version(self, version):
self.__typo3_version = version
def get_extension_config(self):
return self.__extension_config
def get_typo3_version(self):
return self.__typo3_version
def get_installed_extensions(self):
return self.__installed_extensions
def set_path(self, path):
self.__path = path
def set_installed_extensions(self, extension):
self.__installed_extensions[extension] = False
def get_path(self):
return self.__path
def set_installed_extensions_version(self, extension, ChangeLog):
self.__installed_extensions[extension] = ChangeLog
def set_interesting_headers(self, headers, cookies):
"""
This method searches for interesing headers in the HTTP response
Server: Displays the name of the server
X-Powered-By: Information about Frameworks (e.g. ASP, PHP, JBoss) used by the web application
X-*: Version information in other technologies
Via: Informs the client of proxies through which the response was sent
X-Forwarded-For: Originating IP address of a client connecting through an HTTP proxy or load balancer
fe_typo_user: Frontend cookie for TYPO3
"""
for header in headers:
if header == 'Server':
self.__interesting_header['Server'] = headers.get('Server')
elif header == 'X-Powered-By':
self.__interesting_header['X-Powered-By'] = headers.get('X-Powered-By')
elif header == 'X-Runtime':
self.__interesting_header['X-Runtime'] = headers.get('X-Runtime')
elif header == 'X-Version':
self.__interesting_header['X-Version'] = headers.get('X-Version')
elif header == 'X-AspNet-Version':
self.__interesting_header['X-AspNet-Version'] = headers.get('X-AspNet-Version')
elif header == 'Via':
self.__interesting_header['Via'] = headers.get('Via')
elif header == 'X-Forwarded-For':
self.__interesting_header['X-Forwarded-For'] = headers.get('X-Forwarded-For')
if 'fe_typo_user' in cookies.keys():
self.__interesting_header['fe_typo_user'] = cookies['fe_typo_user']
self.set_typo3()
def get_typo3(self):
return self.__typo3
def get_interesting_headers(self):
return self.__interesting_header
def set_typo3(self):
self.__typo3 = True
def check_root(self):
"""
This method requests the root page and searches for a specific string.
Usually there are some TYPO3 notes in the HTML comments.
If found, it searches for a Typo3 path reference
in order to determine the Typo3 installation path.
"""
response = request.get_request('{}'.format(self.get_name()))
full_path = self.get_name()
self.set_interesting_headers(response['headers'], response['cookies'])
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]
self.set_path(full_path)
def set_typo3_version(self, version):
self.__typo3_version = version
def check_default_files(self):
"""
This method requests different files, which are generated on installation.
Note: They are not accessible anymore on newer Typo3 installations
"""
files = {'typo3_src/README.md':'TYPO3 CMS',
'typo3_src/README.txt':'TYPO3 CMS',
'typo3_src/INSTALL.md':'INSTALLING TYPO3',
'typo3_src/INSTALL.txt':'INSTALLING TYPO3',
'typo3_src/LICENSE.txt':'TYPO3',
'typo3_src/CONTRIBUTING.md':'TYPO3 CMS'
}
for path, regex in files.items():
try:
response = request.get_request('{}/{}'.format(self.get_path(), path))
regex = re.compile(regex)
searchInstallation = regex.search(response['html'])
installation = searchInstallation.groups()
self.set_typo3()
return True
except:
pass
return False
def get_typo3_version(self):
return self.__typo3_version
def check_404(self):
"""
This method requests a site which is not available by using a random generated string.
TYPO3 installations usually generate a default error page,
which can be used as an indicator.
"""
random_string = ''.join(random.choice(string.ascii_lowercase) for i in range(10))
response = request.get_request('{}/{}'.format(self.get_path(), random_string))
search404 = re.search('[Tt][Yy][Pp][Oo]3 CMS', response['html'])
if search404:
self.set_typo3()
def set_path(self, path):
self.__path = path
def search_login(self):
"""
This method requests the default login page
and searches for a specific string in the title or the response.
If the access is forbidden (403), extension search is still possible.
"""
print(' \\\n [+] Backend Login')
# maybe /typo3_src/typo3/index.php too?
response = request.get_request('{}/typo3/index.php'.format(self.get_path()))
searchTitle = re.search('<title>(.*)</title>', response['html'])
if searchTitle and 'Login' in searchTitle.group(0):
print(' \u2514', 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(' \u2514', Fore.YELLOW + 'But access is forbidden (IP Address Restriction)' + Fore.RESET)
else:
print(' \u251c', Fore.RED + 'Could not be found' + Fore.RESET)
def get_path(self):
return self.__path
def search_typo3_version(self):
"""
This methos will search for version information.
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]?)',
'/ChangeLog': '[Tt][Yy][Pp][Oo]3 (\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})',
'/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})'
}
def get_login_found(self):
return self.__login_found
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))):
version = response
version_path = path
def set_login_found(self, path):
self.__login_found = path
def set_interesting_headers(self, header_key, header_value):
self.__interesing_header[header_key] = header_value
def get_interesting_headers(self):
return self.__interesing_header
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 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))
if react.startswith('y'):
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:
# 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]))
else:
print(' \u251c', Fore.RED + 'No version information found' + Fore.RESET)