Update to 0.4.3

This commit is contained in:
Jan Rude
2015-08-27 21:16:49 +02:00
parent 58d616106c
commit ca74f659ef
11 changed files with 261 additions and 58 deletions

View File

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

View File

@@ -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 ## Version 0.4.2
* Added new algorithms for Typo3 installation and used path * Added new algorithms for Typo3 installation and used path
@@ -8,7 +16,7 @@
* Fixed link to socksipy for python 3 * Fixed link to socksipy for python 3
* Fixed bug in versionsearch * Fixed bug in versionsearch
* Fixed TOR issues * Fixed TOR issues
* Fixed some little bugs * Fixed some bugs
## Version 0.4 ## Version 0.4
@@ -23,7 +31,6 @@
## Version 0.3.3 ## 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. * 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 * Installed extensions are shown immediately
## Version 0.3.2 ## Version 0.3.2

View File

@@ -1,8 +1,3 @@
# TODO # TODO
* Help message
* Documentation
* Stop extension enumeration with ctrl-c * Stop extension enumeration with ctrl-c
* Privoxy check
* --threads
* --user-agent (default is Mozilla/5.0)

1
lib/config.json Normal file
View File

@@ -0,0 +1 @@
{"threads": 5, "timeout": 10, "agent": "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.0"}

View File

@@ -19,16 +19,24 @@
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
import os.path import os.path
import json
from lib.request import Request from lib.request import Request
from lib.output import Output from lib.output import Output
from lib.thread_pool import ThreadPool from lib.thread_pool import ThreadPool
class Extensions: class Extensions:
"""
Extension class
"""
def __init__(self, ext_state, top): def __init__(self, ext_state, top):
self.__ext_state = ext_state self.__ext_state = ext_state
self.__top = top self.__top = top
def load_extensions(self): 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 = [] extensions = []
for state in self.__ext_state: for state in self.__ext_state:
ext_file = state + '_extensions' ext_file = state + '_extensions'
@@ -48,20 +56,29 @@ class Extensions:
return extensions return extensions
def search_extension(self, domain, 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() thread_pool = ThreadPool()
for ext in extensions: for ext in extensions:
# search local installation path
thread_pool.add_job((Request.head_request, (domain.get_name(), '/typo3conf/ext/' + ext))) 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))) #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.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(): for installed_extension in thread_pool.get_result():
domain.set_installed_extensions(installed_extension[1][1]) domain.set_installed_extensions(installed_extension[1][1])
def search_ext_version(self, domain, extension_dict): 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() thread_pool = ThreadPool()
for extension_path in extension_dict: 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')))
@@ -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.md')))
thread_pool.add_job((Request.head_request, (domain.get_name(), extension_path + '/README.rst'))) 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(): for changelog_path in thread_pool.get_result():
ext, path = self.parse_extension(changelog_path) ext, path = self.parse_extension(changelog_path)

View File

@@ -28,11 +28,12 @@ class Output:
pass pass
def typo3_installation(domain): 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('')
if domain.get_login_found(): 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: else:
print('[+] Typo3 backend login:'.ljust(30) + Fore.RED + 'not found' + Fore.RESET) print('[+] Typo3 backend login:'.ljust(30) + Fore.RED + 'not found' + Fore.RESET)
@@ -42,10 +43,17 @@ class Output:
print('') print('')
def interesting_headers(name, value): def interesting_headers(name, value):
"""
This method prints interesting headers
"""
string = '[!] ' + name + ':' string = '[!] ' + name + ':'
print(string.ljust(30) + value) print(string.ljust(30) + value)
def extension_output(path, extens): 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: if not extens:
print(Fore.RED + ' | No extension found' + Fore.RESET) print(Fore.RED + ' | No extension found' + Fore.RESET)
else: else:

View File

@@ -18,23 +18,30 @@
# 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 requests
import re import re
import json
import requests
from colorama import Fore from colorama import Fore
requests.packages.urllib3.disable_warnings() requests.packages.urllib3.disable_warnings()
from lib.output import Output 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: class Request:
""" """
This class is used to make all server requests This class is used to make all server requests
""" """
@staticmethod @staticmethod
def get_request(domain_name, path): 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: 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')) httpResponse = str((r.text).encode('utf-8'))
headers = r.headers headers = r.headers
cookies = r.cookies cookies = r.cookies
@@ -50,8 +57,17 @@ class Request:
@staticmethod @staticmethod
def head_request(domain_name, path): 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: 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) status_code = str(r.status_code)
if status_code == '405': if status_code == '405':
print("WARNING, (HEAD) method not allowed!!") print("WARNING, (HEAD) method not allowed!!")
@@ -66,12 +82,27 @@ class Request:
@staticmethod @staticmethod
def interesting_headers(headers, cookies): 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 = {} found_headers = {}
for header in headers: for header in headers:
if header == 'server': if header == 'server':
found_headers['Server'] = headers.get('server') found_headers['Server'] = headers.get('server')
elif header == 'x-powered-by': elif header == 'x-powered-by':
found_headers['X-Powered-By'] = headers.get('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': elif header == 'via':
found_headers['Via'] = headers.get('via') found_headers['Via'] = headers.get('via')
try: try:
@@ -88,7 +119,13 @@ class Request:
@staticmethod @staticmethod
def version_information(domain_name, path, regex): 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: if r.status_code == 200:
try: 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):

97
lib/tor.py Normal file
View File

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

View File

@@ -36,12 +36,18 @@ class Update:
# Progressbar # Progressbar
def dlProgress(self, count, blockSize, totalSize): def dlProgress(self, count, blockSize, totalSize):
"""
Progressbar for extension download
"""
percent = int(count*blockSize*100/totalSize) percent = int(count*blockSize*100/totalSize)
sys.stdout.write('\r[+] Downloading extentions: ' + '%d%%' % percent) sys.stdout.write('\r[+] Downloading extentions: ' + '%d%%' % percent)
sys.stdout.flush() sys.stdout.flush()
# Download extensions from typo3 repository # Download extensions from typo3 repository
def download_ext(self): def download_ext(self):
"""
Download extensions from server and unpack the ZIP
"""
try: try:
urllib.request.urlretrieve('http://ter.sitedesign.dk/ter/extensions.xml.gz', 'extensions.gz', reporthook=self.dlProgress) urllib.request.urlretrieve('http://ter.sitedesign.dk/ter/extensions.xml.gz', 'extensions.gz', reporthook=self.dlProgress)
with gzip.open('extensions.gz', 'rb') as f: with gzip.open('extensions.gz', 'rb') as f:
@@ -56,6 +62,10 @@ class Update:
# Parse extensions.xml and save extensions in files # Parse extensions.xml and save extensions in files
def generate_list(self): def generate_list(self):
"""
Parse the extension file and
sort them according to state and download count
"""
experimental = {} # 'experimental' and 'test' experimental = {} # 'experimental' and 'test'
alpha = {} alpha = {}
beta = {} beta = {}

View File

@@ -25,8 +25,8 @@ from lib.request import Request
class VersionInformation: class VersionInformation:
""" """
This class will search for version information. This class will search for version information.
The exact version can be found in the ChangeLog, 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. Less specific version information can be found in the NEWS or INSTALL file.
""" """
def search_typo3_version(self, domain): 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]?)', changelog = {'/typo3_src/ChangeLog':'[Tt][Yy][Pp][Oo]3 (\d{1,2}\.\d{1,2}\.?[0-9]?[0-9]?)',

View File

@@ -18,7 +18,7 @@
# 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/)
#------------------------------------------------------------------------------- #-------------------------------------------------------------------------------
__version__ = '0.4.2' __version__ = '0.4.3'
__program__ = 'Typo-Enumerator' __program__ = 'Typo-Enumerator'
__description__ = 'Automatic Typo3 enumeration tool' __description__ = 'Automatic Typo3 enumeration tool'
__author__ = 'https://github.com/whoot' __author__ = 'https://github.com/whoot'
@@ -27,6 +27,7 @@ import sys
import os.path import os.path
import datetime import datetime
import argparse import argparse
import json
from colorama import Fore, init, deinit, Style from colorama import Fore, init, deinit, Style
from lib.check_installation import Typo3_Installation from lib.check_installation import Typo3_Installation
from lib.version_information import VersionInformation from lib.version_information import VersionInformation
@@ -41,30 +42,75 @@ class Typo3:
self.__domain_list = [] self.__domain_list = []
self.__extensions = None 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): 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() group = parser.add_mutually_exclusive_group()
anonGroup = 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('-f', '--file', dest='file')
group.add_argument('-d', '--domain', dest='domain', type=str, nargs='+') group.add_argument('-d', '--domain', dest='domain', type=str, nargs='+')
group.add_argument('-u', '--update', dest='update', action='store_true') group.add_argument('-u', '--update', dest='update', action='store_true')
parser.add_argument('--top', type=int, dest='top', metavar='VALUE') 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']) 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('--tor', action='store_true')
anonGroup.add_argument('--privoxy', help='using only Privoxy for connections', action='store_true') parser.add_argument('-p', '--port', dest='port', type=int)
anonGroup.add_argument('--tp', help='using TOR and Privoxy for connections', action='store_true') parser.add_argument('--threads', dest='threads', type=int, default = 5)
parser.add_argument('-p', '--port', dest='port', help='Port for TOR/Privoxy (default: 9050/8118)', type=int) 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)
parser.add_argument( '-h', '--help', action='help') help.add_argument( '-h', '--help', action='store_true')
args = parser.parse_args() args = parser.parse_args()
force = 'n'
if args.help:
Typo3.print_help()
return True
try: try:
if args.update: if args.update:
Update() Update()
return True return True
if args.tor: if args.tor:
from lib.tor_only import Tor from lib.tor import Tor
if args.port: if args.port:
tor = Tor(args.port) tor = Tor(args.port)
else: else:
@@ -72,24 +118,6 @@ class Typo3:
tor.start_daemon() tor.start_daemon()
tor.connect() 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: if args.domain:
for dom in args.domain: for dom in args.domain:
self.__domain_list.append(Domain(dom, args.ext_state, args.top)) self.__domain_list.append(Domain(dom, args.ext_state, args.top))
@@ -102,6 +130,9 @@ class Typo3:
for line in f: for line in f:
self.__domain_list.append(Domain(line.strip('\n'), args.ext_state, args.top)) 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: for domain in self.__domain_list:
print('\n\n' + Fore.CYAN + Style.BRIGHT + '[ Checking ' + domain.get_name() + ' ]' + '\n' + '-'* 73 + Fore.RESET + Style.RESET_ALL) print('\n\n' + Fore.CYAN + Style.BRIGHT + '[ Checking ' + domain.get_name() + ' ]' + '\n' + '-'* 73 + Fore.RESET + Style.RESET_ALL)
Typo3_Installation.run(domain) Typo3_Installation.run(domain)