This commit is contained in:
whoot
2020-01-31 23:38:50 +01:00
parent 002d1560ce
commit 527769110e
8 changed files with 85 additions and 70 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/lib/config.json
*.pyc

View File

@@ -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).

View File

@@ -1,3 +1,8 @@
## Version 0.6
* Added version regex for composer installations
* Output bugfix
## Version 0.5.2
* Removed 'interesting header' output

View File

@@ -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"}
{"threads": 5, "timeout": 10, "cookie": "", "auth": "", "User-Agent": "Mozilla/5.0 (X11; Linux i686; rv:64.0) Gecko/20100101 Firefox/64.0"}

View File

@@ -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('<title>(.*)</title>', 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})',
'/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)
print(' \u2514', Fore.RED + 'No Version Information Found.' + Fore.RESET)

View File

@@ -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()

View File

@@ -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:

View File

@@ -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: