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,141 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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/)
#-------------------------------------------------------------------------------
import re
import sys
from colorama import Fore
from lib.request import Request
from lib.output import Output
class Typo3_Installation:
"""
This class checks, if Typo3 is used on the domain with different approaches.
If Typo3 is used, a link to the default login page is shown.
"""
@staticmethod
def run(domain):
check_on_root = Typo3_Installation.check_root(domain)
if not check_on_root:
default_files = Typo3_Installation.check_default_files(domain)
if not default_files:
typo = Typo3_Installation.check_404(domain)
"""
This method requests the root page
and searches for a specific string in the response.
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.
"""
@staticmethod
def check_root(domain):
response = Request.get_request(domain.get_name(), '/')
if re.search('[Tt][Yy][Pp][Oo]3', response[0]):
domain.set_typo3()
headers = Request.interesting_headers(response[1], response[2])
for key in headers:
domain.set_interesting_headers(key, headers[key])
try:
path = re.search('(href|src|content)=(.{0,35})(typo3temp/|typo3conf/)', response[0])
if not (path.groups()[1] == '"' or '"../' in path.groups()[1]):
real_path = (path.groups()[1].split('"')[1])
if 'http' in real_path:
domain.set_name(real_path[0:len(real_path)-1])
else:
domain.set_name(domain.get_name() + real_path[0:len(real_path)-1])
domain.set_path(real_path[0:len(real_path)-1])
except:
pass
return True
else:
return False
"""
This method requests different files, which are generated on installation.
Usually they are not deleted by admins
and can be used as an indicator of a TYPO3 installation.
"""
@staticmethod
def check_default_files(domain):
files = {'/typo3_src/README.md':'[Tt][Yy][Pp][Oo]3 [Cc][Mm][Ss]',
'/typo3_src/README.txt':'[Tt][Yy][Pp][Oo]3 [Cc][Mm][Ss]',
'/typo3_src/INSTALL.txt':'INSTALLING [Tt][Yy][Pp][Oo]3',
'/typo3_src/INSTALL.md':'INSTALLING [Tt][Yy][Pp][Oo]3',
'/typo3_src/LICENSE.txt':'[Tt][Yy][Pp][Oo]3'
}
for path, regex in files.items():
try:
response = Request.get_request(domain.get_name(), path)
regex = re.compile(regex)
searchInstallation = regex.search(response[0])
installation = searchInstallation.groups()
domain.set_typo3()
return True
except:
pass
return False
"""
This method requests a site which is not available.
TYPO3 installations usually generate a default error page,
which can be used as an indicator.
"""
@staticmethod
def check_404(domain):
domain_name = domain.get_name()
response = Request.get_request((domain_name.split('/')[0] + '//' + domain_name.split('/')[2]), '/idontexist')
try:
regex = re.compile('[Tt][Yy][Pp][Oo]3 CMS')
searchInstallation = regex.search(response[0])
installation = searchInstallation.groups()
domain.set_typo3()
return True
except:
return False
"""
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.
"""
@staticmethod
def search_login(domain):
try:
response = Request.get_request(domain.get_name(), '/typo3/index.php')
regex = re.compile('<title>(.*)</title>', re.IGNORECASE)
searchTitle = regex.search(response[0])
title = searchTitle.groups()[0]
login_text = Fore.GREEN + domain.get_name() + '/typo3/index.php' + Fore.RESET
login_text += '\n | Accessible?'.ljust(30)
if ('TYPO3 Backend access denied: The IP address of your client' in response[0]) or (response[3] == 403):
login_text += (Fore.YELLOW + ' Forbidden (IP Address Restriction)' + Fore.RESET)
elif (('TYPO3 Login' in title) or ('TYPO3 CMS Login') in title):
login_text += Fore.GREEN + ' Yes' + Fore.RESET
else:
login_text = Fore.RED + 'Could not be found' + Fore.RESET
domain.set_login_found(login_text)
return True
except:
return False

View File

@@ -1 +1 @@
{"threads": 5, "pass": "ne", "user": "No", "timeout": 10, "agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0"}
{"threads": 5, "timeout": 10, "cookie": "", "auth": "", "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A"}

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)

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 Jan Rude
# 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,86 +15,102 @@
# 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 os.path
import json
from lib.request import Request
from lib.output import Output
import sqlite3
from colorama import Fore
import lib.request as request
from lib.thread_pool import ThreadPool
from pkg_resources import parse_version
class Extensions:
"""
Extension class
"""
def __init__(self, ext_state, top, path):
self.__ext_state = ext_state
self.__top = top
self.__path = path
"""
Extension class
"""
def __init__(self):
pass
def load_extensions(self):
"""
This method loads the defined extensions from the extension file.
IF the extension file is not found, an error is raised.
"""
extensions = []
for state in self.__ext_state:
ext_file = state + '_extensions'
if not os.path.isfile(os.path.join(self.__path, 'extensions', ext_file)):
raise Exception("\n\nCould not find extension file " + ext_file + '!\nTry --update')
def search_extension(self, domain, extensions, threads):
"""
This method loads the extensions from the database and searches for installed extensions.
/typo3conf/ext/: Local installation path. This is where extensions usually get installed.
/typo3/ext/: Global installation path (not used atm)
/typo3/sysext/: Extensions shipped with core
"""
found_extensions = {}
thread_pool = ThreadPool()
for ext in extensions:
thread_pool.add_job((request.head_request, ('{}/typo3conf/ext/{}/'.format(domain, ext))))
thread_pool.add_job((request.head_request, ('{}/typo3/sysext/{}/'.format(domain, ext))))
#thread_pool.add_job((request.head_request, ('{}/typo3/ext/{}/'.format(domain, ext))))
thread_pool.start(threads)
with open(os.path.join(self.__path, 'extensions', ext_file), 'r') as f:
count = 0
for extension in f:
if not(self.__top is None):
if count < self.__top:
extensions.append(extension.split('\n')[0])
count += 1
else:
extensions.append(extension.split('\n')[0])
f.close()
return extensions
for installed_extension in thread_pool.get_result():
name = installed_extension[1][:-1]
name = name[name.rfind('/')+1:]
found_extensions[name] = {'url':installed_extension[1], 'version': None, 'file': None}
return found_extensions
def search_extension(self, domain, extensions):
"""
This method searches for installed extensions.
/typo3conf/ext/: Local installation path. This is where extensions get usually installed.
/typo3/ext/: Global installation path (not used atm)
/typo3/sysext/: Extensions shipped with core (not used atm)
"""
config = json.load(open(os.path.join(self.__path, 'lib', 'config.json')))
thread_pool = ThreadPool()
for ext in extensions:
thread_pool.add_job((Request.head_request, (domain.get_name(), '/typo3conf/ext/' + ext)))
#thread_pool.add_job((Request.head_request, (domain.get_name(), '/typo3/ext/' + ext)))
#thread_pool.add_job((Request.head_request, (domain.get_name(), '/typo3/sysext/' + ext)))
thread_pool.start(config['threads'])
def search_ext_version(self, found_extensions, threads):
"""
This method adds a job for every installed extension.
The goal is to find a file with version information.
"""
thread_pool = ThreadPool()
for extension,values in found_extensions.items():
thread_pool.add_job((request.version_information, (values['url'] + 'Documentation/ChangeLog/Index.rst', None)))
thread_pool.add_job((request.version_information, (values['url'] + 'Documentation/Settings.cfg', None)))
thread_pool.add_job((request.version_information, (values['url'] + 'Documentation/Settings.yml', None)))
thread_pool.add_job((request.version_information, (values['url'] + 'Settings.yml', None)))
thread_pool.add_job((request.version_information, (values['url'] + 'Documentation/Index.rst', None)))
thread_pool.add_job((request.version_information, (values['url'] + 'composer.json', '(?:"dev-master":|"version":)\s?"([0-9]+\.[0-9]+\.[0-9x][0-9x]?)')))
thread_pool.add_job((request.version_information, (values['url'] + 'Index.rst', None)))
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)
for installed_extension in thread_pool.get_result():
domain.set_installed_extensions(installed_extension[1][1])
for version_path in thread_pool.get_result():
path = version_path[0][0]
version = version_path[1]
name = version_path[0][0]
if 'Documentation/' in name:
name = name[:name.rfind('Documentation/')+1]
name = name[name.find('ext/')+4:name.rfind('/')]
found_extensions[name]['version'] = version
found_extensions[name]['file'] = path
return found_extensions
def search_ext_version(self, domain, extension_dict):
"""
This method adds a job for every installed extension.
The goal is to find a ChangeLog or Readme in order to determine the version.
"""
config = json.load(open('lib/config.json'))
thread_pool = ThreadPool()
for extension_path in extension_dict:
thread_pool.add_job((Request.head_request, (domain.get_name(), extension_path + '/ChangeLog')))
thread_pool.add_job((Request.head_request, (domain.get_name(), extension_path + '/ChangeLog.txt')))
thread_pool.add_job((Request.head_request, (domain.get_name(), extension_path + '/Readme.txt')))
thread_pool.add_job((Request.head_request, (domain.get_name(), extension_path + '/README.md')))
thread_pool.add_job((Request.head_request, (domain.get_name(), extension_path + '/README.rst')))
thread_pool.start(config['threads'], True)
for changelog_path in thread_pool.get_result():
ext, path = self.parse_extension(changelog_path)
domain.set_installed_extensions_version(path, ext[4])
def parse_extension(self, path):
ext = (path[1][1]).split('/')
path = '/' + ext[1] + '/' + ext[2] + '/' + ext[3]
return (ext, path)
def output(self, extension_dict, database):
conn = sqlite3.connect(database)
c = conn.cursor()
print('\n\n [+] Extension information\n \\')
for extension,info in extension_dict.items():
c.execute('SELECT title FROM extensions where extensionkey=?', (extension,))
title = c.fetchone()[0]
print(' [+] Name: {}'.format(Fore.GREEN + extension + Fore.RESET))
print(' \u251c Title: {}'.format(title))
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))
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]))
else:
print(' \u2514 see: {}'.format(info['file']))
else:
print(' \u2514 Version: -unknown-')
print()
conn.close()

92
lib/initdb.py Normal file
View File

@@ -0,0 +1,92 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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/)
#-------------------------------------------------------------------------------
import sqlite3, os.path
class DB_Init:
"""
This class will empty the database, create tables and insert User-Agents
"""
def __init__(self):
database = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'typo3scan.db')
try:
conn = sqlite3.connect(database)
c = conn.cursor()
# Delete all tables
c.execute('''DROP TABLE IF EXISTS extensions''')
c.execute('''DROP TABLE IF EXISTS extension_vulns''')
c.execute('''DROP TABLE IF EXISTS core_vulns''')
c.execute('''DROP TABLE IF EXISTS settings''')
conn.commit()
# Create table extensions
c.execute('''CREATE TABLE IF NOT EXISTS extensions
(title text, extensionkey text PRIMARY KEY, description text, version text, state text)''')
# Create table extension_vulns
c.execute('''CREATE TABLE IF NOT EXISTS extension_vulns
(advisory text, extensionkey text, vulnerability text, branch_max integer, affected_version_max text, branch_max integer, affected_version_min text)''')
# Create table core_vulns
c.execute('''CREATE TABLE IF NOT EXISTS core_vulns
(advisory text, vulnerability text, subcomponent text, branch_max integer, affected_version_max text, branch_max integer, affected_version_min text, cve text)''')
# Create table UserAgents
c.execute('''CREATE TABLE IF NOT EXISTS UserAgents
(userAgent text)''')
conn.commit()
# add some User-Agents from http://www.useragentstring.com/pages/useragentstring.php
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (X11; Linux i686; rv:64.0) Gecko/20100101 Firefox/64.0',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (X11; Linux i586; rv:63.0) Gecko/20100101 Firefox/63.0',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (Windows NT 6.2; WOW64; rv:63.0) Gecko/20100101 Firefox/63.0',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/44.0.2403.155 Safari/537.36',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14931',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Chrome (AppleWebKit/537.1; Chrome50.0; Windows NT 6.3) AppleWebKit/537.36 (KHTML like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('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',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20121202 Firefox/17.0 Iceweasel/17.0.1',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1 Iceweasel/15.0.1',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (X11; Linux i686; rv:15.0) Gecko/20100101 Firefox/15.0.1 Iceweasel/15.0.1',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20120724 Debian Iceweasel/15.0',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0 Iceweasel/15.0',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Opera/9.80 (X11; Linux i686; Ubuntu/14.10) Presto/2.12.388 Version/12.16',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Opera/9.80 (Macintosh; Intel Mac OS X 10.14.1) Presto/2.12.388 Version/12.16',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (Windows NT 6.0; rv:2.0) Gecko/20100101 Firefox/4.0 Opera 12.14',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.0) Opera 12.14',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25',))
c.execute('INSERT INTO UserAgents VALUES (?)', ('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',))
conn.commit()
except sqlite3.Error as e:
if conn:
conn.rollback()
print(e)
sys.exit(-1)
finally:
if conn:
conn.close()
print('\n[+] Database resetted')
print('[!] Please update (-u) the database before using Typo3Scan.\n')

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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/)
#-------------------------------------------------------------------------------
from colorama import Fore
class Output:
"""
This class handles the output
"""
def __init__(self):
pass
def typo3_installation(domain):
"""
If TYPO3 is installed and the backend login was found, a link to a backend is printed.
Additionally, if the version search was successful, the version and a link to cvedetails is given.
"""
print('')
print('[+] Typo3 backend login:'.ljust(30) + domain.get_login_found())
if (domain.get_typo3_version() != 'could not be determined'):
print('[+] Typo3 version:'.ljust(30) + Fore.GREEN + domain.get_typo3_version() + Fore.RESET)
print(' | known vulnerabilities:'.ljust(30) + Fore.GREEN + 'http://www.cvedetails.com/version-search.php?vendor=&product=Typo3&version=' + domain.get_typo3_version() + Fore.RESET)
else:
print('[+] Typo3 version:'.ljust(30) + Fore.RED + domain.get_typo3_version() + Fore.RESET)
print('')
def interesting_headers(name, value):
"""
This method prints interesting headers
"""
string = '[!] ' + name + ':'
print(string.ljust(30) + value)
def extension_output(path, extens):
"""
This method prints every found extension.
If a Readme or ChangeLog is found, it will print a link to the file.
"""
if not extens:
print(Fore.RED + ' | No extension found' + Fore.RESET)
else:
for extension in extens:
print(Fore.BLUE + '\n[+] Name: ' + extension.split('/')[3] + '\n' + "-"* 31 + Fore.RESET)
print(' | Location:'.ljust(16) + path + extension)
if not (extens[extension] == False):
print(' | ' + extens[extension].split('.')[0] + ':'.ljust(4) + (path + extension + '/'+ extens[extension]))

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 Jan Rude
# 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,127 +15,120 @@
# 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 os.path
import json
import requests
from colorama import Fore
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
from colorama import Fore
from lib.output import Output
class Request:
"""
This class is used to make all server requests
"""
@staticmethod
def get_request(domain_name, path):
"""
All GET requests are done in this method.
This method is not used, when searching for extensions and their Readmes/ChangeLogs
There are three error types which can occur:
Connection timeout
Connection error
anything else
"""
try:
config = json.load(open('lib/config.json'))
cookie = {config['cookie'].split('=')[0]:config['cookie'].split('=')[1]}
r = requests.get(domain_name + path, timeout=config['timeout'], headers={'User-Agent' : config['agent']}, cookies=cookie, auth=(config['user'], config['pass']), verify=False)
httpResponse = str((r.text).encode('utf-8'))
headers = r.headers
cookies = r.cookies
status_code = r.status_code
response = [httpResponse, headers, cookies, status_code]
return response
except requests.exceptions.Timeout:
print(e)
print(Fore.RED + '[x] Connection timed out' + Fore.RESET)
except requests.exceptions.ConnectionError as e:
print(e)
print(Fore.RED + '[x] Connection error\n | Please make sure you provided the right URL' + Fore.RESET)
except requests.exceptions.RequestException as e:
print(Fore.RED + str(e) + Fore.RESET)
def get_request(url):
"""
All GET requests are done in this method.
This method is not used, when searching for extensions and their Readmes/ChangeLogs
There are three error types which can occur:
Connection timeout
Connection error
anything else
"""
config = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')))
timeout = config['timeout']
auth = config['auth']
cookie = config['cookie']
custom_headers = {'User-Agent' : config['User-Agent']}
try:
if cookie != '':
name = cookie.split('=')[0]
value = cookie.split('=')[1]
custom_headers[name] = value
response = {}
if auth != '':
r = requests.get(url, timeout=config['timeout'], headers=custom_headers, auth=(auth.split(':')[0], auth.split(':')[1]), verify=False)
else:
r = requests.get(url, timeout=config['timeout'], headers=custom_headers, verify=False)
response['status_code'] = r.status_code
response['html'] = r.text
response['headers'] = r.headers
response['cookies'] = r.cookies
return response
except requests.exceptions.Timeout:
print(e)
print(Fore.RED + '[x] Connection timed out' + Fore.RESET)
except requests.exceptions.ConnectionError as e:
print(e)
print(Fore.RED + '[x] Connection error\n | Please make sure you provided the right URL' + Fore.RESET)
exit(-1)
except requests.exceptions.RequestException as e:
print(Fore.RED + str(e) + Fore.RESET)
@staticmethod
def head_request(domain_name, path):
"""
All HEAD requests are done in this method.
HEAD requests are used when searching for extensions and their Readmes/ChangeLogs
There are three error types which can occur:
Connection timeout
Connection error
anything else
"""
try:
config = json.load(open('lib/config.json'))
r = requests.head(domain_name + path, timeout=config['timeout'], headers={'User-Agent' : config['agent']}, auth=(config['user'], config['pass']), allow_redirects=False, verify=False)
status_code = str(r.status_code)
if status_code == '405':
print("WARNING, (HEAD) method not allowed!!")
exit(-1)
return status_code
except requests.exceptions.Timeout:
print(Fore.RED + '[x] Connection timed out' + Fore.RESET)
except requests.exceptions.ConnectionError as e:
print(Fore.RED + '[x] Connection aborted.\n Please make sure you provided the right URL' + Fore.RESET)
except requests.exceptions.RequestException as e:
print(Fore.RED + str(e) + Fore.RESET)
def head_request(url):
"""
All HEAD requests are done in this method.
HEAD requests are used when searching for extensions and their Readmes/ChangeLogs
There are three error types which can occur:
Connection timeout
Connection error
anything else
"""
config = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')))
timeout = config['timeout']
auth = config['auth']
cookie = config['cookie']
custom_headers = {'User-Agent' : config['User-Agent']}
try:
if cookie != '':
name = cookie.split('=')[0]
value = cookie.split('=')[1]
custom_headers[name] = value
if auth != '':
r = requests.head(url, timeout=config['timeout'], headers=custom_headers, auth=(auth.split(':')[0], auth.split(':')[1]), verify=False)
else:
r = requests.head(url, timeout=config['timeout'], headers=custom_headers, allow_redirects=False, verify=False)
status_code = str(r.status_code)
if status_code == '405':
print('[x] WARNING: \'HEAD\' method not allowed!')
exit(-1)
return status_code
except requests.exceptions.Timeout:
print(Fore.RED + '[x] Connection timed out' + Fore.RESET)
except requests.exceptions.ConnectionError as e:
print(Fore.RED + '[x] Connection aborted.\n Please make sure you provided the right URL' + Fore.RESET)
except requests.exceptions.RequestException as e:
print(Fore.RED + str(e) + Fore.RESET)
@staticmethod
def interesting_headers(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.
be_typo_user: Backend cookie for TYPO3
fe_typo_user: Frontend cookie for TYPO3
"""
found_headers = {}
for header in headers:
if header == 'server':
found_headers['Server'] = headers.get('server')
elif header == 'x-powered-by':
found_headers['X-Powered-By'] = headers.get('x-powered-by')
elif header == 'x-runtime':
found_headers['X-Runtime'] = headers.get('x-runtime')
elif header == 'x-version':
found_headers['X-Version'] = headers.get('x-version')
elif header == 'x-aspnet-version':
found_headers['X-AspNet-Version'] = headers.get('x-aspnet-version')
elif header == 'via':
found_headers['Via'] = headers.get('via')
try:
typo_cookie = cookies['be_typo_user']
found_headers['be_typo_user'] = typo_cookie
except:
pass
try:
typo_cookie = cookies['fe_typo_user']
found_headers['fe_typo_user'] = typo_cookie
except:
pass
return found_headers
@staticmethod
def version_information(domain_name, path, regex):
"""
This method is used for version search only.
It performs a GET request, if the response is 200 - Found, it reads the first 400 bytes the response only,
because usually the TYPO3 version is in the first few lines of the response.
"""
config = json.load(open('lib/config.json'))
r = requests.get(domain_name + path, stream=True, timeout=config['timeout'], headers={'User-Agent' : config['agent']}, auth=(config['user'], config['pass']), verify=False)
if r.status_code == 200:
try:
for content in r.iter_content(chunk_size=400, decode_unicode=False):
regex = re.compile(regex)
search = regex.search(str(content))
version = search.groups()[0]
return version
except:
return None
def version_information(url, regex):
"""
This method is used for version search only.
It performs a GET request, if the response is 200 - Found, it reads the first 400 bytes the response only,
because usually the TYPO3 version is in the first few lines of the response.
"""
if regex is None:
regex = '([0-9]+\.[0-9]+\.[0-9x][0-9x]?)'
config = json.load(open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.json')))
timeout = config['timeout']
auth = config['auth']
cookie = config['cookie']
custom_headers = {'User-Agent' : config['User-Agent']}
if cookie != '':
name = cookie.split('=')[0]
value = cookie.split('=')[1]
custom_headers[name] = value
if auth != '':
r = requests.get(url, stream=True, timeout=config['timeout'], headers=custom_headers, auth=(auth.split(':')[0], auth.split(':')[1]), verify=False)
else:
r = requests.get(url, stream=True, timeout=config['timeout'], headers=custom_headers, verify=False)
if r.status_code == 200:
try:
for content in r.iter_content(chunk_size=400, decode_unicode=False):
search = re.search(regex, str(content))
version = search.group(1)
r.close()
return version
except:
r.close()
return None

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 Jan Rude
# 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,12 +15,15 @@
# 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 threading
from queue import Queue
from progressbar import Bar, AdaptiveETA, Percentage, ProgressBar
bar = None
number = 1
class ThreadPoolSentinel:
pass
@@ -34,6 +37,8 @@ class ThreadPool:
thread_list: List of worker threads
"""
def __init__(self):
global number
number = 1
self.__work_queue = Queue()
self.__result_queue = Queue()
self.__active_threads = 0
@@ -51,35 +56,33 @@ class ThreadPool:
active_threads -= 1
self.__result_queue.task_done()
continue
else: # Getting an actual result
self.__result_queue.task_done()
yield result
def start(self, threads, version_search=False):
global bar
toolbar_width = (self.__work_queue).qsize()
widgets = [' \u251c Processed: ', Percentage(),' ', Bar(),' ', AdaptiveETA()]
bar = ProgressBar(widgets=widgets, maxval=toolbar_width).start()
if self.__active_threads:
raise Exception('Threads already started.')
if not version_search:
# Create thread pool
try:
# Create thread pool
for _ in range(threads):
worker = threading.Thread(
target=_work_function,
args=(self.__work_queue, self.__result_queue))
worker.start()
self.__thread_list.append(worker)
self.__active_threads += 1
else:
for _ in range(threads):
worker = threading.Thread(
target=_work_function_version,
args=(self.__work_queue, self.__result_queue))
args=(self.__work_queue, self.__result_queue, version_search))
worker.daemon = True
worker.start()
self.__thread_list.append(worker)
self.__active_threads += 1
# Put sentinels to let the threads know when there's no more jobs
[self.__work_queue.put(ThreadPoolSentinel()) for worker in self.__thread_list]
# Put sentinels to let the threads know when there's no more jobs
[self.__work_queue.put(ThreadPoolSentinel()) for worker in self.__thread_list]
except KeyboardInterrupt:
print('\nReceived keyboard interrupt.\nQuitting...')
exit(-1)
def join(self): # Clean exit
self.__work_queue.join()
@@ -87,11 +90,11 @@ class ThreadPool:
self.__active_threads = 0
self.__result_queue.join()
def _work_function(job_q, result_q):
def _work_function(job_q, result_q, version_search):
"""Work function expected to run within threads."""
global number
while True:
job = job_q.get()
if isinstance(job, ThreadPoolSentinel): # All the work is done, get out
result_q.put(ThreadPoolSentinel())
job_q.task_done()
@@ -100,33 +103,17 @@ def _work_function(job_q, result_q):
function = job[0]
args = job[1]
try:
result = function(*args)
if version_search:
result = function(*args)
else:
result = function(args)
if not version_search and (result == '403' or result == '200'):
result_q.put((job))
elif version_search and result:
result_q.put((args, result))
except Exception as e:
print(e)
else:
if result == ('301' or '200' or '403'):
result_q.put((job))
finally:
job_q.task_done()
def _work_function_version(job_q, result_q):
"""Work function expected to run within threads."""
while True:
job = job_q.get()
if isinstance(job, ThreadPoolSentinel): # All the work is done, get out
result_q.put(ThreadPoolSentinel())
job_q.task_done()
break
function = job[0]
args = job[1]
try:
result = function(*args)
except Exception as e:
print(e)
else:
if result == ('200'):
result_q.put((job))
finally:
bar.update(number)
number = number+1
job_q.task_done()

View File

@@ -1,97 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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/)
#-------------------------------------------------------------------------------
import socket
import os, sys
import re
from colorama import Fore
from lib.request import Request
try:
import socks
except:
print(Fore.RED + 'The module \'SocksiPy\' is not installed.')
if sys.platform.startswith('linux'):
print('Please install it with: sudo apt-get install python-socksipy' + Fore.RESET)
else:
print('You can download it from https://code.google.com/p/socksipy-branch/' + Fore.RESET)
sys.exit(-2)
class Tor:
"""
This class initiates the usage of TOR for all requests
port: TOR port
"""
def __init__(self, port=9150):
self.__port = port
def start_daemon(self):
"""
If the OS is linux, start TOR deamon.
If not, user needs to start it manually
"""
if sys.platform.startswith('linux'):
os.system('service tor start')
elif sys.platform.startswith('win32') or sys.platform.startswith('cygwin'):
print('Please make sure TOR is running...')
else:
print('You are using', sys.platform, ', which is not supported (yet).')
sys.exit(-2)
# Using TOR for all connections
def connect(self):
"""
This method checks the connection with TOR.
If TOR is not used, the program will exit
"""
print('\nChecking connection...')
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, '127.0.0.1', self.__port, True)
socks.socket.setdefaulttimeout(20)
socket.socket = socks.socksocket
try:
request = Request.get_request('https://check.torproject.org', '/')
response = request[0]
except:
print('Failed to connect through TOR!')
print('Please make sure your configuration is right!\n')
sys.exit(-2)
try:
regex = re.compile('Congratulations. This browser is configured to use Tor.')
searchVersion = regex.search(response)
version = searchVersion.groups()
print('Connection to TOR established')
regex = re.compile("(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})")
searchIP = regex.search(response)
IP = searchIP.groups()[0]
print('Your IP is: ', IP)
except Exception as e:
print(e)
print('It seems like TOR is not used.\nAborting...\n')
sys.exit(-2)
def stop(self):
"""
This method stops the TOR deamon if running under linux
"""
print('\n')
if sys.platform.startswith('linux'):
os.system('service tor stop')
elif sys.platform.startswith('win32') or sys.platform.startswith('cygwin'):
print('You can stop TOR now...')

BIN
lib/typo3scan.db Normal file

Binary file not shown.

View File

@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 Jan Rude
# 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,131 +15,310 @@
# 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 os, sys, gzip, urllib.request, inspect
from collections import OrderedDict
import os.path
from pkg_resources import parse_version
import xml.etree.ElementTree as ElementTree
import re, os, sys, gzip, urllib.request, sqlite3, requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
database = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'typo3scan.db')
conn = sqlite3.connect(database)
c = conn.cursor()
class Update:
"""
This class updates the Typo3 extensions
"""
This class updates the extension and vulnerability database
It will download the extension file from the official repository,
unpack it and sort the extensions in different files
"""
def __init__(self, path):
print('')
self.__path = path
self.download_ext()
self.generate_list()
It will download the extension file from the official repository,
unpack it and insert the extensions in the database.
Vulnerabilities will be parsed from the official homepage.
"""
def __init__(self):
self.load_core_vulns()
self.download_ext()
self.load_extensions()
self.load_extension_vulns()
# Progressbar
def dlProgress(self, count, blockSize, totalSize):
"""
Progressbar for extension download
"""
percent = int(count*blockSize*100/totalSize)
sys.stdout.write('\r[+] Downloading extentions: ' + '%d%%' % percent)
sys.stdout.flush()
def load_core_vulns(self):
"""
Grep the CORE vulnerabilities from the security advisory website
Search for advisories and maximum pages
Request every advisory and get:
Advisory Title
Vulnerability Type
Subcomponent(s)
Affected Versions
CVE Numbers
"""
print('\n[+] Searching for new CORE vulnerabilities...')
update_counter = 0
response = requests.get('https://typo3.org/help/security-advisories/typo3-cms/1')
pages = re.findall('<a class=\"page-link\" href=\"/help/security-advisories/typo3-cms/([0-9]+)\">', response.text)
last_page = int(pages[-1])
# Download extensions from typo3 repository
def download_ext(self):
"""
Download extensions from server and unpack the ZIP
"""
try:
# Maybe someday we need to use mirrors: https://repositories.typo3.org/mirrors.xml.gz
urllib.request.urlretrieve('https://typo3.org/fileadmin/ter/extensions.xml.gz', 'extensions.xml.gz', reporthook=self.dlProgress)
with gzip.open('extensions.xml.gz', 'rb') as infile:
with open('extensions.xml', 'wb') as outfile:
for line in infile:
outfile.write(line)
infile.close()
outfile.close()
except Exception as e:
print ('\n', e)
for current_page in range(1, last_page+1):
print(' \u251c Page {}/{}'.format(current_page, last_page))
response = requests.get('https://typo3.org/help/security-advisories/typo3-cms/{}'.format(current_page), timeout=6)
advisories = re.findall('TYPO3-CORE-SA-[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9]', response.text)
for advisory in advisories:
vulnerabilities = []
affected_version_max = '0.0.0'
affected_version_min = '0.0.0'
html = requests.get('https://typo3.org/security/advisory/{}'.format(advisory.lower()))
beauty_html = html.text
beauty_html = beauty_html[beauty_html.index('Component Type'):]
beauty_html = beauty_html[:beauty_html.index('General ')]
beauty_html = beauty_html.replace('\xa0', ' ')
beauty_html = beauty_html.replace('</strong>', '')
beauty_html = beauty_html.replace('&nbsp;', ' ')
beauty_html = beauty_html.replace('&amp;', '&')
# Parse extension file and save extensions in files
def generate_list(self):
"""
Parse the extension file and
sort them according to state and download count
"""
experimental = {} # 'experimental' and 'test'
alpha = {}
beta = {}
stable = {}
outdated = {} # 'obsolete' and 'outdated'
allExt = {}
# set as global versions
advisory_items = {}
subcomponents = re.findall('([sS]ubcomponent\s?#?[0-9]?:\s?(.*?))<', beauty_html)
# if no subcomponent / CORE vuln
if len(subcomponents) == 0:
missed = re.search('Component Type:\s?(.*?)<', beauty_html).group(1)
advisory_items[missed] = []
advisory_items[missed].append(beauty_html)
subcomponents.reverse()
try:
for subcomponent in subcomponents:
index = beauty_html.rfind(subcomponent[0])
item_text = subcomponent[1]
if item_text in advisory_items:
item_text = item_text + ' (2)'
advisory_items[item_text] = []
advisory_items[item_text].append(beauty_html[index:])
beauty_html = beauty_html[:index]
print ('\n[+] Parsing file...')
tree = ElementTree.parse('extensions.xml')
root = tree.getroot()
extension = 0
# for every extension in file
for child in root:
# insert every extension in "allExt" dictionary
allExt.update({child.get('extensionkey'):child[0].text})
# and search the last version entry
version = 0
for version_entry in root[extension].iter('version'):
version +=1
# get the state of the latest version
state = (str(root[extension][version][2].text)).lower()
if state == 'experimental' or state == 'test':
experimental.update({child.get('extensionkey'):child[0].text})
elif state == 'alpha':
alpha.update({child.get('extensionkey'):child[0].text})
elif state == 'beta':
beta.update({child.get('extensionkey'):child[0].text})
elif state == 'stable':
stable.update({child.get('extensionkey'):child[0].text})
elif state == 'obsolete' or state == 'outdated':
outdated.update({child.get('extensionkey'):child[0].text})
extension+=1
for subcomponent, entry in advisory_items.items():
vulnerability_items = {}
vulnerability_type = re.findall('(Vulnerability Type:\s?(.*?)<)', entry[0])
vulnerability_type.reverse()
for type_entry in vulnerability_type:
index = entry[0].rfind(type_entry[0])
vulnerability_items[type_entry[1]] = []
vulnerability_items[type_entry[1]].append(entry[0][index:])
entry[0] = entry[0][:index]
# sorting lists according to number of downloads
print ('[+] Sorting according to number of downloads...')
sorted_experimental = sorted(experimental.items(), key=lambda x: int(x[1]), reverse=True)
sorted_alpha = sorted(alpha.items(), key=lambda x: int(x[1]), reverse=True)
sorted_beta = sorted(beta.items(), key=lambda x: int(x[1]), reverse=True)
sorted_stable = sorted(stable.items(), key=lambda x: int(x[1]), reverse=True)
sorted_outdated = sorted(outdated.items(), key=lambda x: int(x[1]), reverse=True)
sorted_allExt = sorted(allExt.items(), key=lambda x: int(x[1]), reverse=True)
for vuln_type, vuln_description in vulnerability_items.items():
cve = re.search(':\s?(CVE-.*?)(<|\"|\()', vuln_description[0])
if cve:
cve = cve.group(1)
else:
cve = 'None assigned'
search_affected = re.search('Affected Version[s]?:\s?(.+?)<', vuln_description[0])
if search_affected:
affected_versions = search_affected.group(1)
else:
affected_versions = re.search('Affected Version[s]?:\s?(.+?)<', beauty_html).group(1)
# separate versions
affected_versions = affected_versions.replace("and below", " - 0.0.0")
affected_versions = affected_versions.replace(";", ",")
affected_versions = affected_versions.replace(' and', ',')
versions = affected_versions.split(', ')
for version in versions:
version = re.findall('([0-9]+\.[0-9x]+\.?[0-9x]?[0-9x]?)', version)
if len(version) == 0:
print("[!] Unknown version info! Skipping...")
print(" \u251c Advisory:", advisory)
print(" \u251c Subcomponent:", subcomponent)
print(" \u251c Vulnerability:", vuln_type)
print(" \u251c Versions:", affected_versions)
break
elif len(version) == 1:
version = version[0]
if len(version) == 3: # e.g. version 6.2
version = version + '.0'
affected_version_max = version
affected_version_min = version
else:
if parse_version(version[0]) >= parse_version(version[1]):
affected_version_max = version[0]
affected_version_min = version[1]
else:
affected_version_max = version[1]
affected_version_min = version[0]
# add vulnerability
vulnerabilities.append([advisory, vuln_type, subcomponent, affected_version_max, affected_version_min, cve])
except Exception as e:
print("Error on receiving data for https://typo3.org/security/security-advisory/{}".format(advisory))
print(e)
exit(-1)
print ('[+] Generating files...')
f = open(os.path.join(self.__path, 'extensions', 'experimental_extensions'), 'w')
for i in range(0,len(sorted_experimental)):
f.write(sorted_experimental[i][0]+'\n')
f.close()
# Add vulnerability details to database
for ext_vuln in vulnerabilities:
c.execute('SELECT * FROM core_vulns WHERE advisory=? AND vulnerability=? AND subcomponent=? AND affected_version_max=? AND affected_version_min=? AND cve=?', (ext_vuln[0], ext_vuln[1], ext_vuln[2], ext_vuln[3], ext_vuln[4], ext_vuln[5],))
data = c.fetchall()
if not data:
update_counter+=1
c.execute('INSERT INTO core_vulns VALUES (?,?,?,?,?,?)', (ext_vuln[0], ext_vuln[1], ext_vuln[2], ext_vuln[3], ext_vuln[4], ext_vuln[5],))
conn.commit()
else:
if update_counter == 0:
print('[!] Already up-to-date.\n')
else:
print('[+] Done.')
print('[!] Added {} new CORE vulnerabilities to database.\n'.format(update_counter))
return True
f = open(os.path.join(self.__path, 'extensions', 'alpha_extensions'), 'w')
for i in range(0,len(sorted_alpha)):
f.write(sorted_alpha[i][0]+'\n')
f.close()
def dlProgress(self, count, blockSize, totalSize):
"""
Progressbar for extension download
"""
percent = int(count*blockSize*100/totalSize)
sys.stdout.write('\r \u251c Downloading ' + '%d%%' % percent)
sys.stdout.flush()
f = open(os.path.join(self.__path, 'extensions', 'beta_extensions'),'w')
for i in range(0,len(sorted_beta)):
f.write(sorted_beta[i][0]+'\n')
f.close()
def download_ext(self):
"""
Download extensions from server and unpack the ZIP
"""
print('[+] Getting extension file...')
try:
# Maybe someday we need to use mirrors: https://repositories.typo3.org/mirrors.xml.gz
urllib.request.urlretrieve('https://typo3.org/fileadmin/ter/extensions.xml.gz', 'extensions.xml.gz', reporthook=self.dlProgress)
with gzip.open('extensions.xml.gz', 'rb') as infile:
with open('extensions.xml', 'wb') as outfile:
for line in infile:
outfile.write(line)
infile.close()
outfile.close()
except Exception as e:
print ('\n', e)
f = open(os.path.join(self.__path, 'extensions', 'stable_extensions'), 'w')
for i in range(0,len(sorted_stable)):
f.write(sorted_stable[i][0]+'\n')
f.close()
def load_extensions(self):
"""
Parse the extension file and add extensions in database
"""
print('\n \u251c Parsing extension file...')
tree = ElementTree.parse('extensions.xml')
root = tree.getroot()
f = open(os.path.join(self.__path, 'extensions', 'outdated_extensions'), 'w')
for i in range(0,len(sorted_outdated)):
f.write(sorted_outdated[i][0]+'\n')
f.close()
# for every extension get:
# title, extensionkey, description, version, state
for extensions in root:
title = extensions[1][0].text
extensionkey = extensions.get('extensionkey')
description = extensions[1][1].text
version = '0.0.0'
state = ''
f = open(os.path.join(self.__path, 'extensions', 'all_extensions'), 'w')
for i in range(0,len(sorted_allExt)):
f.write(sorted_allExt[i][0]+'\n')
f.close()
# search for current version
for extension in extensions.iter('version'):
if not(extension.attrib['version'] == ''):
try:
if parse_version((extension.attrib['version']).split('-')[0]) > parse_version(version):
version = extension.attrib['version']
state = (extension.find('state')).text
except ValueError:
pass
c.execute('INSERT OR REPLACE INTO extensions VALUES (?,?,?,?,?)', (title, extensionkey, description, version, state))
print ('[+] Loaded', len(sorted_allExt), 'extensions')
os.remove('extensions.xml.gz')
os.remove('extensions.xml')
conn.commit()
os.remove('extensions.xml.gz')
os.remove('extensions.xml')
print(' \u2514 Done. Added {} extensions to database'.format(len(root.findall('extension'))))
def load_extension_vulns(self):
"""
Grep the EXTENSION vulnerabilities from the security advisory website
Search for advisories and maximum pages
Request every advisory and get:
Advisory Title
Extension Name
Vulnerability Type
Affected Versions
"""
print('\n[+] Searching for new extension vulnerabilities...')
update_counter = 0
response = requests.get('https://typo3.org/help/security-advisories/typo3-extensions/1')
pages = re.findall('<a class=\"page-link\" href=\"/help/security-advisories/typo3-extensions/([0-9]+)\">', response.text)
last_page = int(pages[-1])
for current_page in range(1, last_page+1):
print(' \u251c Page {}/{}'.format(current_page, last_page))
response = requests.get('https://typo3.org/help/security-advisories/typo3-extensions/{}'.format(current_page), timeout=6)
advisories = re.findall('TYPO3-EXT-SA-[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9]', response.text)
for advisory in advisories:
vulnerabilities = []
affected_version_max = '0.0.0'
affected_version_min = '0.0.0'
# adding vulns with odd stuff on website
if advisory == 'TYPO3-EXT-SA-2014-018':
vulnerabilities.append(['TYPO3-EXT-SA-2014-018', 'phpmyadmin', 'Cross-Site Scripting, Denial of Service, Local File Inclusion', '4.18.4', '4.18.0'])
elif advisory == 'TYPO3-EXT-SA-2014-015':
vulnerabilities.append(['TYPO3-EXT-SA-2014-015', 'dce', 'Information Disclosure', '0.11.4', '0.0.0'])
elif advisory == 'TYPO3-EXT-SA-2014-013':
vulnerabilities.append(['TYPO3-EXT-SA-2014-013', 'cal', 'Denial of Service', '1.5.8', '0.0.0'])
vulnerabilities.append(['TYPO3-EXT-SA-2014-013', 'cal', 'Denial of Service', '1.6.0', '1.6.0'])
elif advisory == 'TYPO3-EXT-SA-2014-009':
vulnerabilities.append(['TYPO3-EXT-SA-2014-009', 'news', 'Cross-Site Scripting', '3.0.0', '3.0.0'])
vulnerabilities.append(['TYPO3-EXT-SA-2014-009', 'news', 'Cross-Site Scripting', '2.3.0', '2.0.0'])
else:
try:
html = requests.get('https://typo3.org/security/advisory/{}'.format(advisory.lower()))
beauty_html = html.text.replace('\xa0', ' ')
beauty_html = beauty_html.replace('</strong>', '')
beauty_html = beauty_html.replace('&nbsp;', ' ')
beauty_html = beauty_html.replace('&amp;', '&')
advisory_info = re.search('<title>(.*)</title>', beauty_html).group(1)
vulnerability = re.findall('Vulnerability Type[s]?:\s?(.*?)<', beauty_html)
affected_versions = re.findall('Affected Version[s]?:\s?(.+?)<', beauty_html)
extensionkey = re.findall('Extension[s]?:\s?(.*?)<', beauty_html)
# Sometimes there are multiple extensions in an advisory
if len(extensionkey) == 0: # If only one extension affected
extensionkey = [advisory_info[advisory_info.find('('):]]
for item in range (0, len(extensionkey)):
extensionkey_item = extensionkey[item]
extensionkey_item = extensionkey_item[extensionkey_item.rfind('(')+1:extensionkey_item.rfind(')')]
description = vulnerability[item]
version_item = affected_versions[item]
version_item = version_item.replace("and all versions below", "- 0.0.0")
version_item = version_item.replace("and all version below", "- 0.0.0") # typo
version_item = version_item.replace("and alll versions below", "- 0.0.0") # typo
version_item = version_item.replace("and below of", "-")
version_item = version_item.replace("and below", "- 0.0.0")
version_item = version_item.replace("&nbsp;", " ")
version_item = version_item.replace(";", ",")
version_item = version_item.replace(' and', ',')
versions = version_item.split(', ')
for version in versions:
version = re.findall('([0-9]+\.[0-9x]+\.[0-9x]+)', version)
if len(version) == 1:
affected_version_max = version[0]
affected_version_min = version[0]
else:
if parse_version(version[0]) >= parse_version(version[1]):
affected_version_max = version[0]
affected_version_min = version[1]
else:
affected_version_max = version[1]
affected_version_min = version[0]
vulnerabilities.append([advisory, extensionkey_item, description, affected_version_max, affected_version_min])
except Exception as e:
print("Error on receiving data for https://typo3.org/security/advisory/{}".format(advisory))
print(e)
exit(-1)
# Add vulnerability details to database
for ext_vuln in vulnerabilities:
c.execute('SELECT * FROM extension_vulns WHERE advisory=? AND extensionkey=? AND vulnerability=? AND affected_version_max=? AND affected_version_min=?', (ext_vuln[0], ext_vuln[1], ext_vuln[2], ext_vuln[3], ext_vuln[4],))
data = c.fetchall()
if not data:
update_counter+=1
c.execute('INSERT INTO extension_vulns VALUES (?,?,?,?,?)', (ext_vuln[0], ext_vuln[1], ext_vuln[2], ext_vuln[3], ext_vuln[4]))
conn.commit()
else:
if update_counter == 0:
print('[!] Already up-to-date.\n')
else:
print(' \u2514 Done. Added {} new EXTENSION vulnerabilities to database.\n'.format(update_counter))
return True

View File

@@ -1,55 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#-------------------------------------------------------------------------------
# Typo3 Enumerator - Automatic Typo3 Enumeration Tool
# Copyright (c) 2014-2017 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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/)
#-------------------------------------------------------------------------------
import re
import time
from lib.request import Request
class VersionInformation:
"""
This class 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.
"""
def search_typo3_version(self, domain):
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/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.md':'[Tt][Yy][Pp][Oo]3 v(\d{1})'
}
version = 'could not be determined'
for path, regex in files.items():
response = Request.version_information(domain.get_name(), path, regex)
if not (response is None):
string = '[!] ' + 'Found version file:'
print(string.ljust(30) + path)
if (version is 'could not be determined'):
version = response
elif (len(response) > len(version)):
version = response
domain.set_typo3_version(version)