From ca74f659efff37b4c0644b8934283bbaef04734d Mon Sep 17 00:00:00 2001 From: Jan Rude Date: Thu, 27 Aug 2015 21:16:49 +0200 Subject: [PATCH] Update to 0.4.3 --- README.md | 4 +- doc/CHANGELOG.md | 11 ++++- doc/TODO.md | 7 +-- lib/config.json | 1 + lib/extensions.py | 27 +++++++++-- lib/output.py | 18 +++++-- lib/request.py | 53 +++++++++++++++++---- lib/tor.py | 97 ++++++++++++++++++++++++++++++++++++++ lib/update.py | 10 ++++ lib/version_information.py | 4 +- typo3_enumerator.py | 87 +++++++++++++++++++++++----------- 11 files changed, 261 insertions(+), 58 deletions(-) create mode 100644 lib/config.json create mode 100644 lib/tor.py diff --git a/README.md b/README.md index 504ce16..f830d3b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Typo3-Enumerator Typo3-Enumerator is an open source penetration testing tool that automates the process of detecting the [Typo3](https://typo3.org) CMS and it's installed [extensions](https://typo3.org/extensions/repository/?id=23&L=0&q=&tx_solr[filter][outdated]=outdated%3AshowOutdated) (also the outdated ones). If the --top parameter is set to a value, only the specified most downloaded extensions are tested. -It is possible to do all requests through the [TOR Hidden Service](https://www.torproject.org/) network or [Privoxy](http://sourceforge.net/projects/ijbswa/files/). Also you can combine TOR with Privoxy in order to prevent DNS leakage. +It is possible to do all requests through the [TOR Hidden Service](https://www.torproject.org/) network. Installation ---- @@ -81,4 +81,4 @@ 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/) +along with this program. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/) \ No newline at end of file diff --git a/doc/CHANGELOG.md b/doc/CHANGELOG.md index 493a33d..5e9aa7c 100644 --- a/doc/CHANGELOG.md +++ b/doc/CHANGELOG.md @@ -1,3 +1,11 @@ +## Version 0.4.3 + +* Added --threads +* Added --user-agent +* Added --timeout +* Added help message (-h, --help) +* No support for privoxy anymore + ## Version 0.4.2 * Added new algorithms for Typo3 installation and used path @@ -8,7 +16,7 @@ * Fixed link to socksipy for python 3 * Fixed bug in versionsearch * Fixed TOR issues -* Fixed some little bugs +* Fixed some bugs ## Version 0.4 @@ -23,7 +31,6 @@ ## Version 0.3.3 * Extensions are now saved into different files, separated by state (experimental | alpha | beta | stable | outdated | all). This makes it possible to check more specific ones. -* Colorama is only used if installed. It doesn't need to be installed anymore. * Installed extensions are shown immediately ## Version 0.3.2 diff --git a/doc/TODO.md b/doc/TODO.md index baf8152..1de8a48 100644 --- a/doc/TODO.md +++ b/doc/TODO.md @@ -1,8 +1,3 @@ # TODO -* Help message -* Documentation -* Stop extension enumeration with ctrl-c -* Privoxy check -* --threads -* --user-agent (default is Mozilla/5.0) \ No newline at end of file +* Stop extension enumeration with ctrl-c \ No newline at end of file diff --git a/lib/config.json b/lib/config.json new file mode 100644 index 0000000..1dae22b --- /dev/null +++ b/lib/config.json @@ -0,0 +1 @@ +{"threads": 5, "timeout": 10, "agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0"} \ No newline at end of file diff --git a/lib/extensions.py b/lib/extensions.py index 9806dfe..6045aef 100644 --- a/lib/extensions.py +++ b/lib/extensions.py @@ -19,16 +19,24 @@ #------------------------------------------------------------------------------- import os.path +import json from lib.request import Request from lib.output import Output from lib.thread_pool import ThreadPool class Extensions: + """ + Extension class + """ def __init__(self, ext_state, top): self.__ext_state = ext_state self.__top = top 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' @@ -48,20 +56,29 @@ class Extensions: return 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('lib/config.json')) thread_pool = ThreadPool() for ext in extensions: - # search local installation path thread_pool.add_job((Request.head_request, (domain.get_name(), '/typo3conf/ext/' + ext))) - # search global installation path #thread_pool.add_job((Request.head_request, (domain.get_name(), '/typo3/ext/' + ext))) - # search extensions shipped with core #thread_pool.add_job((Request.head_request, (domain.get_name(), '/typo3/sysext/' + ext))) - thread_pool.start(6) + thread_pool.start(config['threads']) for installed_extension in thread_pool.get_result(): domain.set_installed_extensions(installed_extension[1][1]) 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'))) @@ -70,7 +87,7 @@ class Extensions: 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(6, True) + thread_pool.start(config['threads'], True) for changelog_path in thread_pool.get_result(): ext, path = self.parse_extension(changelog_path) diff --git a/lib/output.py b/lib/output.py index 895e17d..f692fdc 100644 --- a/lib/output.py +++ b/lib/output.py @@ -22,18 +22,19 @@ from colorama import Fore class Output: """ - This class handles the 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('') if domain.get_login_found(): - if domain.get_name().endswith('/'): - print('[+] Typo3 backend login:'.ljust(30) + Fore.GREEN + domain.get_name() + 'typo3/index.php' + Fore.RESET) - else: - print('[+] Typo3 backend login:'.ljust(30) + Fore.GREEN + domain.get_name() + '/typo3/index.php' + Fore.RESET) + print('[+] Typo3 backend login:'.ljust(30) + Fore.GREEN + domain.get_name() + '/typo3/index.php' + Fore.RESET) else: print('[+] Typo3 backend login:'.ljust(30) + Fore.RED + 'not found' + Fore.RESET) print('[+] Typo3 version:'.ljust(30) + Fore.GREEN + domain.get_typo3_version() + Fore.RESET) @@ -42,10 +43,17 @@ class Output: 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: diff --git a/lib/request.py b/lib/request.py index e9520ba..0704fbb 100644 --- a/lib/request.py +++ b/lib/request.py @@ -18,23 +18,30 @@ # along with this program. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/) #------------------------------------------------------------------------------- -import requests import re +import json +import requests from colorama import Fore requests.packages.urllib3.disable_warnings() from lib.output import Output -header = {'User-Agent' : "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)"} -timeout = 10 - class Request: """ - This class is used to make all server requests + 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: - r = requests.get(domain_name + path, timeout=timeout, headers=header, verify=False) + config = json.load(open('lib/config.json')) + r = requests.get(domain_name + path, timeout=config['timeout'], headers={'User-Agent' : config['agent']}, verify=False) httpResponse = str((r.text).encode('utf-8')) headers = r.headers cookies = r.cookies @@ -50,8 +57,17 @@ class Request: @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: - r = requests.head(domain_name + path, timeout=timeout, headers=header, allow_redirects=False, verify=False) + config = json.load(open('lib/config.json')) + r = requests.head(domain_name + path, timeout=config['timeout'], headers={'User-Agent' : config['agent']}, allow_redirects=False, verify=False) status_code = str(r.status_code) if status_code == '405': print("WARNING, (HEAD) method not allowed!!") @@ -66,12 +82,27 @@ class Request: @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: @@ -88,7 +119,13 @@ class Request: @staticmethod def version_information(domain_name, path, regex): - r = requests.get(domain_name + path, stream=True, timeout=timeout, headers=header, verify=False) + """ + 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']}, verify=False) if r.status_code == 200: try: for content in r.iter_content(chunk_size=400, decode_unicode=False): diff --git a/lib/tor.py b/lib/tor.py new file mode 100644 index 0000000..5fb1b25 --- /dev/null +++ b/lib/tor.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +#------------------------------------------------------------------------------- +# Typo3 Enumerator - Automatic Typo3 Enumeration Tool +# Copyright (c) 2015 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...') \ No newline at end of file diff --git a/lib/update.py b/lib/update.py index 2b096fe..652e187 100644 --- a/lib/update.py +++ b/lib/update.py @@ -36,12 +36,18 @@ class Update: # 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() # Download extensions from typo3 repository def download_ext(self): + """ + Download extensions from server and unpack the ZIP + """ try: urllib.request.urlretrieve('http://ter.sitedesign.dk/ter/extensions.xml.gz', 'extensions.gz', reporthook=self.dlProgress) with gzip.open('extensions.gz', 'rb') as f: @@ -56,6 +62,10 @@ class Update: # Parse extensions.xml 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 = {} diff --git a/lib/version_information.py b/lib/version_information.py index c61880e..88d1214 100644 --- a/lib/version_information.py +++ b/lib/version_information.py @@ -25,8 +25,8 @@ from lib.request import Request class VersionInformation: """ This class will search for version information. - The exact version can be found in the ChangeLog, - less specific version information can be found in the NEWS or INSTALL file. + 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): changelog = {'/typo3_src/ChangeLog':'[Tt][Yy][Pp][Oo]3 (\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)', diff --git a/typo3_enumerator.py b/typo3_enumerator.py index 430baa3..f6d9994 100644 --- a/typo3_enumerator.py +++ b/typo3_enumerator.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/](http://www.gnu.org/licenses/) #------------------------------------------------------------------------------- -__version__ = '0.4.2' +__version__ = '0.4.3' __program__ = 'Typo-Enumerator' __description__ = 'Automatic Typo3 enumeration tool' __author__ = 'https://github.com/whoot' @@ -27,6 +27,7 @@ import sys import os.path import datetime import argparse +import json from colorama import Fore, init, deinit, Style from lib.check_installation import Typo3_Installation from lib.version_information import VersionInformation @@ -41,30 +42,75 @@ class Typo3: self.__domain_list = [] self.__extensions = None + def print_help(): + print( +"""\nUsage: python3 typoenum.py [options] + +Options: + -h, --help Show this help message and exit + + Target: + At least one of these options has to be provided to define the target(s) + + -d [DOMAIN, ...], --domain [DOMAIN, ...] Target domain(s) + -f FILE, --file FILE Parse targets from file (one domain per line) + + + Optional: + You dont need to specify this arguments, but you may want to + + --top TOP Test if top [TOP] downloaded extensions are installed + Default: every in list + --state STATE Extension state [all, experimental, alpha, beta, stable, outdated] + Default: all + --timeout TIMEOUT The timeout for all requests + Default: 10 seconds + --agent USER_AGENT The user-agent used for all requests + Default: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0 + --threads THREADS The number of threads used for enumerating the extensions + Default: 5 + + + Anonymity: + This options can be used to proxy all requests through TOR/Privoxy + + --tor Using only TOR for connections + --port PORT Port for TOR + Default: 9050 + + General: + -u, --update Update TYPO3 extensions +""") + def run(self): - parser = argparse.ArgumentParser(usage='typoenum.py [options]', add_help=False) + parser = argparse.ArgumentParser(add_help=False) group = parser.add_mutually_exclusive_group() anonGroup = parser.add_mutually_exclusive_group() + help = parser.add_mutually_exclusive_group() group.add_argument('-f', '--file', dest='file') group.add_argument('-d', '--domain', dest='domain', type=str, nargs='+') group.add_argument('-u', '--update', dest='update', action='store_true') parser.add_argument('--top', type=int, dest='top', metavar='VALUE') parser.add_argument('--state', dest='ext_state', choices = ['all', 'experimental', 'alpha', 'beta', 'stable', 'outdated'], nargs='+', default = ['all']) - anonGroup.add_argument('--tor', help='using only TOR for connections', action='store_true') - anonGroup.add_argument('--privoxy', help='using only Privoxy for connections', action='store_true') - anonGroup.add_argument('--tp', help='using TOR and Privoxy for connections', action='store_true') - parser.add_argument('-p', '--port', dest='port', help='Port for TOR/Privoxy (default: 9050/8118)', type=int) - - parser.add_argument( '-h', '--help', action='help') + anonGroup.add_argument('--tor', action='store_true') + parser.add_argument('-p', '--port', dest='port', type=int) + parser.add_argument('--threads', dest='threads', type=int, default = 5) + parser.add_argument('--agent', dest='agent', type=str, default = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0') + parser.add_argument('--timeout', dest='timeout', type=int, default = 10) + help.add_argument( '-h', '--help', action='store_true') args = parser.parse_args() - force = 'n' + + if args.help: + Typo3.print_help() + return True + try: if args.update: Update() return True if args.tor: - from lib.tor_only import Tor + from lib.tor import Tor if args.port: tor = Tor(args.port) else: @@ -72,24 +118,6 @@ class Typo3: tor.start_daemon() tor.connect() - elif args.privoxy: - from lib.privoxy_only import Privoxy - if args.port: - privoxy = Privoxy(args.port) - else: - privoxy = Privoxy() - privoxy.start_daemon() - privoxy.connect() - - elif args.tp: - from lib.tor_with_privoxy import Tor_with_Privoxy - if args.port: - tp = Tor_with_Privoxy(args.port) - else: - tp = Tor_with_Privoxy() - tp.start_daemon() - tp.connect() - if args.domain: for dom in args.domain: self.__domain_list.append(Domain(dom, args.ext_state, args.top)) @@ -102,6 +130,9 @@ class Typo3: for line in f: self.__domain_list.append(Domain(line.strip('\n'), args.ext_state, args.top)) + config = {'threads':args.threads, 'agent':args.agent, 'timeout':args.timeout} + json.dump(config, open('lib/config.json', 'w')) + for domain in self.__domain_list: print('\n\n' + Fore.CYAN + Style.BRIGHT + '[ Checking ' + domain.get_name() + ' ]' + '\n' + '-'* 73 + Fore.RESET + Style.RESET_ALL) Typo3_Installation.run(domain)