diff --git a/typoenum.py b/typoenum.py new file mode 100644 index 0000000..9b29041 --- /dev/null +++ b/typoenum.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +################## ChangeLog ################## +## v0.1 Prototype ## +## v0.2 Added version search for Typo3 ## +## v0.3 Added version guessing ## +## v0.4 Optimized requests ## +## v0.5 Added support for Typo v6.X ## +## v0.6 Added extension search ## +## v0.7 Added version search for extensions ## +############################################### + +############ Version information ############## +__version__ = "0.7" +__program__ = "Typo-Enumerator v" + __version__ +__description__ = 'Find out the Typo3 Version, Login-URL and Extensions' +__author__ = "Jan Rude" +__licence__ = "BSD Licence" +__status__ = "Development" # ("Prototype", "Development", "Final") +############################################### + +################## Imports #################### +import os +import re +import gzip +import time +import socket +import urllib +import urllib2 +import argparse +import datetime +import requests +from Queue import Queue +from colorama import Fore +from os.path import isfile +from operator import itemgetter +from threading import Thread, Lock +from collections import OrderedDict +import xml.etree.ElementTree as ElementTree +############################################### + +############### Global variables ############## +user_agent = {'User-Agent' : None} +extension_list = [] +verbosity = False +############################################### + +# Checks the Typo version +def check_typo_version_ChangeLog(domain): + global verbosity + try: + url = 'http://' + domain + '/typo3_src/ChangeLog' + f = urllib2.urlopen(url, timeout = 3.0) + changelog = f.read(200) + f.close() + regex = re.compile("RELEASE] Release of (.*)") + searchVersion = regex.search(changelog) + version = searchVersion.groups() + print "Typo3 Version:".ljust(32) + Fore.GREEN + version[0] + Fore.RESET + print "Link to vulnerabilities:".ljust(32) + "http://www.cvedetails.com/version-search.php?vendor=&product=Typo3&version=" + version[0].split()[1] + except Exception, e: + if verbosity: + print "Typo3 Version:".ljust(32) + "first check failed, trying second one..." + check_typo_version_NEWS_TXT(domain) + +def check_typo_version_NEWS_TXT(domain): + global verbosity + try: + url = 'http://' + domain + '/typo3_src/NEWS.txt' + f = urllib2.urlopen(url, timeout = 3.0) + changelog = f.read(500) + f.close() + regex = re.compile("This document contains information about (.*) which") + searchVersion = regex.search(changelog) + version = searchVersion.groups() + print "Typo3 Version:".ljust(32), Fore.GREEN + version[0] + '.XX' + Fore.RED + ' (only guessable)'+ Fore.RESET + print "Link to vulnerabilities:".ljust(32) + "http://www.cvedetails.com/version-search.php?vendor=&product=Typo3&version=" + version[0].split()[2] + except: + if verbosity: + print "Typo3 Version:".ljust(32) + "second check failed, trying third one..." + check_typo_version_NEWS_MD(domain) + +def check_typo_version_NEWS_MD(domain): + try: + url = 'http://' + domain + '/typo3_src/NEWS.md' + f = urllib2.urlopen(url, timeout = 3.0) + changelog = f.read(80) + f.close() + regex = re.compile("(.*) - WHAT'S NEW") + searchVersion = regex.search(changelog) + version = searchVersion.groups() + print "Typo3 Version:\t\t", Fore.GREEN + version[0] + '.XX' + Fore.RED + ' (only guessable)'+ Fore.RESET + print "Link to vulnerabilities:".ljust(32) + "http://www.cvedetails.com/version-search.php?vendor=&product=Typo3&version=" + version[0].split()[2] + except: + print "Typo3 Version:".ljust(32) + Fore.RED + "Not found" + Fore.RESET + +# Checks the Typo login +def check_typo_login(domain): + global user_agent + try: + r = requests.get('http://' + domain + '/typo3/index.php', allow_redirects=False, timeout=3.0, headers=user_agent) + statusCode = r.status_code + httpResponse = r.text + r.close() + if statusCode == 200: + return check_title(httpResponse, r.url) + + elif (statusCode == 301) or (statusCode == 302): + location = r.headers['location'] + if verbosity: + print "Redirect to:".ljust(32) + location + if ("http://") in location: + new = location.split("//") + new2 = new[1].split("/") + check_typo_login(new2[0]) + elif ("https://") in location: + r = requests.get(location, timeout=3.0, headers=user_agent, verify=False) + statusCode = r.status_code + httpResponse = r.text + return check_title(httpResponse, r.url) + elif statusCode == 404: + print "Typo3 Login:".ljust(32) + Fore.RED + "Typo3 is not used on this domain" + Fore.RESET + + else: + print "Oops! Got:".ljust(32) + str(statusCode) + ": " + str(r.raise_for_status()) + + except requests.exceptions.Timeout: + print Fore.RED + "Connection timed out" + Fore.RESET + except requests.exceptions.TooManyRedirects: + print Fore.RED + "Too many redirects" + Fore.RESET + except requests.exceptions.RequestException as e: + print Fore.RED + str(e) + Fore.RESET + +# Checks, if URL is a Typo-Login +def check_title(httpResponse, url): + regex = re.compile("(.*)", re.IGNORECASE) + searchTitle = regex.search(httpResponse) + title = searchTitle.groups()[0] + if 'TYPO3' in title or 'TYPO3 SVN ID:' in httpResponse: + print "Typo3 Login:".ljust(32) + Fore.GREEN + url + Fore.RESET + return True + else: + print "Typo3 Login:".ljust(32) + Fore.RED + "Typo3 is not used on this domain" + Fore.RESET + return False + +# Searches for installed extensions +def check_extensions(domain, input_queue, output_queue): + global user_agent + global verbosity + while True: + extension = input_queue.get() + try: + r = requests.get('http://' + domain + '/typo3conf/ext/' + extension + "/", allow_redirects=False, timeout=8.0, headers=user_agent) + statusCode = r.status_code + r.close() + if (statusCode == 200) or (statusCode == 403): + check_extension_version(domain, extension, output_queue) + elif statusCode == 404: + if verbosity: + output_queue.put(extension.ljust(32) + Fore.RED + "not installed" + Fore.RESET) + pass + else: + print "Oops! Got:".ljust(32) + str(statusCode) + ": " + str(r.raise_for_status()) + except Exception, e: + print "Oops! Got:".ljust(32), e + input_queue.task_done() + +# Searches for version of installed extension +def check_extension_version(domain, extension, output_queue): + global verbosity + try: + url = 'http://' + domain + '/typo3conf/ext/' + extension + '/ChangeLog' + f = urllib2.urlopen(url, timeout = 3.0) + changelog = f.read(500) + f.close() + regex = re.compile("\* (.*)") + searchVersion = regex.search(changelog) + version = searchVersion.groups() + output_queue.put(extension.ljust(32) + Fore.GREEN + "installed (" + version[0] + ")" + Fore.RESET) + except: + if verbosity: + output_queue.put(extension.ljust(32) + Fore.GREEN + "installed" + Fore.RESET + " (could not find version)") + else: + output_queue.put(extension.ljust(32) + Fore.GREEN + "installed" + Fore.RESET) + +# Output +def output_thread(q): + if q.empty(): + print Fore.RED + "No extensions are installed" + Fore.RESET + else: + while q is not q.empty(): + try: + extension = q.get() + print(extension) + q.task_done() + except Exception, e: + print "Oops! Got:", e + +# Loading extensions +def generate_extensions_list(top): + global extension_list + extension = 'extensions.xml' + print "Loading extensions..." + if not isfile(extension): + print(Fore.RED + "File not found: " + extension + "\nAborting..." + Fore.RESET) + sys.exit(-2) + + tree = ElementTree.parse(extension) + tag_dict = tree.getroot() + exten_Dict = {} + + for extensions in tag_dict.getchildren(): + ext = {extensions.get('extensionkey'):extensions[0].text} + exten_Dict.update(ext) + print 'Loaded ' , len(exten_Dict), ' extensions' + + if top is not None: + sorted_dict = sorted(exten_Dict.iteritems(), key=lambda x: int(x[1]), reverse=True) + + for i in xrange(0,top): + extension_list.append(sorted_dict[i][0]) + + else: + for extension_name in tag_dict: + extension_list.append(extension_name.get('extensionkey')) + +# Copy used extensions in queue +def copy_extensions(input_queue): + global extension_list + for ext in extension_list: + input_queue.put(ext) + +# Processbar +def dlProgress(count, blockSize, totalSize): + percent = int(count*blockSize*100/totalSize) + sys.stdout.write("\rDownloading extentions: " + "%d%%" % percent) + sys.stdout.flush() + +# Update function +def update(): + global user_agent + try: + urllib.urlretrieve('http://ter.sitedesign.dk/ter/extensions.xml.gz', 'extensions.gz', reporthook=dlProgress) + inf = gzip.open('extensions.gz', 'rb') + file_content = inf.read() + inf.close() + outF = file("extensions.xml", 'wb') + outF.write(file_content) + outF.close() + print "\n" + os.remove('extensions.gz') + except Exception, e: + print "Oops! Got:".ljust(32), e + +#Starting checks +def start(domain): + in_queue = Queue() + out_queue = Queue() + regex = re.compile("(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})") + searchIP = regex.search(domain) + if not (searchIP is None): + IP = searchIP.groups()[0] + hostname = socket.gethostbyaddr(IP) + print("\n\n[*] Check for " + domain + " (" + hostname[0] + ")") + else: + print("\n\n[*] Check for " + domain) + + if check_typo_login(domain) is True: + check_typo_version_ChangeLog(domain) + copy_extensions(in_queue) + print '\nChecking', in_queue.qsize(), 'Extensions:' + for i in xrange(0, 10): + t = Thread(target=check_extensions, args=(domain, in_queue, out_queue)) + t.daemon = True + t.start() + in_queue.join() + + t = Thread(target=output_thread, args=(out_queue,)) + t.daemon = True + t.start() + out_queue.join() + +# Main +def main(argv): + global user_agent + global verbosity + parser = argparse.ArgumentParser(add_help=False, usage='typofinder.py -d DOMAIN [DOMAIN ...] | -f FILE [--user_agent USER-AGENT] [--top VALUE] [-v]') + group = parser.add_mutually_exclusive_group() + group.add_argument('-d', '--domain', dest='domain', type=str, nargs='+') + group.add_argument('-f', '--file', dest='file', help='File with a list of domains') + group.add_argument('-u', '--update', dest='update', action='store_true',help='Get/Update the extension file') + parser.add_argument('--user_agent', dest='user_agent', default='Mozilla/5.0', metavar='USER-AGENT (default: Mozilla/5.0)') + parser.add_argument('--top', type=int, dest='top', metavar='VALUE', help='Check only most X downloaded extensions (default: all)', default=None) + parser.add_argument("-v", "--verbose", help="increase output verbosity", action="store_true") + args = parser.parse_args() + + if not args.domain and not args.file and not args.update: + parser.print_help() + return True + + if args.update: + update() + return True + + user_agent = {'User-Agent' : args.user_agent} + generate_extensions_list(args.top) + verbosity = args.verbose + + if args.domain and not args.file: + for dom in args.domain: + start(dom) + + elif not args.domain and args.file: + if not isfile(args.file): + print(Fore.RED + "\nFile not found: " + args.file + "\nAborting..." + Fore.RESET) + sys.exit(-2) + else: + with open(args.file, 'r') as f: + for line in f: + start(line.strip()) + print '\n' + now = datetime.datetime.now() + print __program__ + ' finished at ' + now.strftime("%Y-%m-%d %H:%M:%S") + '\n' + return True + +if __name__ == "__main__": + import sys + print('\n' + 70*'*') + print('\t' + __program__ ) + print('\t' + __description__) + print('\t' + '(c)2014 by ' + __author__) + print('\t' + 'Status:\t' + __status__) + print('\t' + 'For legal purposes only!') + print(70*'*' + '\n') + sys.exit( not main( sys.argv ) )