From 439078892e65c5418024c1c0785b6734063a9624 Mon Sep 17 00:00:00 2001 From: Lanjelot Date: Fri, 25 Nov 2011 17:35:09 +0100 Subject: [PATCH] public release --- patator.py | 2749 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2749 insertions(+) create mode 100755 patator.py diff --git a/patator.py b/patator.py new file mode 100755 index 0000000..5a941b4 --- /dev/null +++ b/patator.py @@ -0,0 +1,2749 @@ +#!/usr/bin/env python + +# Copyright (C) 2011 Sebastien MACKE +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License version 2, as published by the +# Free Software Foundation +# +# 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 (http://www.gnu.org/licenses/gpl.txt). + +__author__ = 'Sebastien Macke' +__email__ = 'patator@hsc.fr' +__url__ = 'http://www.hsc.fr/ressources/outils/patator/' +__git__ = 'http://code.google.com/p/patator/' +__version__ = '0.1' +__license__ = 'GPLv2' + +# README {{{ + +''' +INTRODUCTION +------------ + +* What ? + +Patator is a multi-purpose brute-forcer, with a modular design and a flexible usage. + +Currently it supports the following modules: + - ftp_login : Brute-force FTP + - ssh_login : Brute-force SSH + - telnet_login : Brute-force Telnet + - smtp_vrfy : Enumerate valid users using the SMTP 'VRFY' command + - smtp_rcpt : Enumerate valid users using the SMTP 'RCPT TO' command + - http_fuzz : Brute-force HTTP/HTTPS + - pop_passd : Brute-force poppassd (not POP3) + - ldap_login : Brute-force LDAP + - smb_login : Brute-force SMB + - mssql_login : Brute-force MSSQL + - oracle_login : Brute-force Oracle + - mysql_login : Brute-force MySQL + - pgsql_login : Brute-force PostgreSQL + - vnc_login : Brute-force VNC + + - dns_forward : Forward lookup subdomains + - dns_reverse : Reverse lookup subnets + - snmp_login : Brute-force SNMPv1/2 and SNMPv3 + + - unzip_pass : Brute-force the password of encrypted ZIP files + - keystore_pass : Brute-force the password of Java keystore files + +Future modules to be implemented: + - rdp_login + - vmware_login (902/tcp) + - pop3_login + - smtp_login + +The name "Patator" comes from http://www.youtube.com/watch?v=xoBkBvnTTjo +"Whatever the payload to fire, always use the same launch tube" + +* Why ? + +Basically, I got tired of using Medusa, Hydra, ncrack, metasploit auxiliary modules, nmap NSE scripts and the like because: + - they either do not work or are not reliable (got me false negatives several times in the past) + - they are slow (not multi-threaded or not testing multiple passwords within the same TCP connection) + - they lack very useful features that are easy to code in python (eg. interactive runtime) + + +FEATURES +-------- + * No false negatives, as it is the user that decides what results to ignore based on: + + status code of response + + size of response + + matching string or regex in response data + + ... see --help + + * Modular design + + not limited to network modules (eg. the unzip_pass module) + + not limited to brute-forcing (eg. remote exploit testing, or vulnerable version probing) + + * Interactive runtime + + show verbose progress + + pause/unpause execution + + increase/decrease verbosity + + add new actions & conditions during runtime in order to exclude more types of response from showing + + ... press h to see all available interactive commands + + * Use persistent connections (ie. will test several passwords until the server disconnects) + + * Multi-threaded + + * Flexible user input + - Any part of a payload is fuzzable: + + use FILE[0-9] keywords to iterate on a file + + use COMBO[0-9] keywords to iterate on the combo entries of a file + + use NET[0-9] keywords to iterate on every host of a network subnet + + - Iteration over the joined wordlists may be done in any order + + * Save every response (along with request) to seperate log files for later reviewing + + +INSTALL +------- + +* Prerequisites + + Dependency | Required for | URL | Version | +-------------------------------------------------------------------------------------------------- +paramiko | SSH | http://www.lag.net/paramiko/ | 1.7.7.1 | +-------------------------------------------------------------------------------------------------- +pycurl | HTTP | http://pycurl.sourceforge.net/ | 7.19.0 | +-------------------------------------------------------------------------------------------------- +openldap | LDAP | http://www.openldap.org/ | 2.4.24 | +-------------------------------------------------------------------------------------------------- +impacket | SMB | http://oss.coresecurity.com/projects/impacket.html | svn#414 | +-------------------------------------------------------------------------------------------------- +cx_Oracle | Oracle | http://cx-oracle.sourceforge.net/ | 5.0.4 | +-------------------------------------------------------------------------------------------------- +mysql-python | MySQL | http://sourceforge.net/projects/mysql-python/ | 1.2.3 | +-------------------------------------------------------------------------------------------------- +psycopg | PostgreSQL | http://initd.org/psycopg/ | 2.4.1 | +-------------------------------------------------------------------------------------------------- +pycrypto | VNC | http://www.dlitz.net/software/pycrypto/ | 2.3 | +-------------------------------------------------------------------------------------------------- +pydns | DNS | http://pydns.sourceforge.net/ | 2.3.4 | +-------------------------------------------------------------------------------------------------- +pysnmp | SNMP | http://pysnmp.sf.net/ | 4.1.16a | +-------------------------------------------------------------------------------------------------- +IPy | NETx keywords | https://github.com/haypo/python-ipy/wiki | 0.75 | +-------------------------------------------------------------------------------------------------- + +* Shortcuts (optionnal) +ln -s path/to/patator.py /usr/bin/ftp_login +ln -s path/to/patator.py /usr/bin/http_fuzz +so on ... + + +USAGE +----- + +$ python patator.py -h +or +$ -h (if you created the shortcuts) + +There are global options and module options: + - all global options start with - or -- + - all module options are of the form option=value + +All module options are fuzzable: +--------- +./module host=FILE0 port=FILE1 foobar=FILE2.google.FILE3 0=hosts.txt 1=ports.txt 2=foo.txt 3=bar.txt + +The keywords (FILE, COMBO, NET, ...) act as place-holders. They indicate the type of wordlist +and where to replace themselves with the actual words to test. + +Each keyword is numbered in order to: + - match the corresponding wordlist + - and indicate in what order to iterate over all the wordlists + +For instance, this would be the classic order: +--------- +./module host=FILE0 user=FILE1 password=FILE2 0=hosts.txt 1=logins.txt 2=passwords.txt +10.0.0.1 root password +10.0.0.1 root 123456 +10.0.0.1 root qsdfghj +.... +10.0.0.1 test password +10.0.0.1 test 123456 +10.0.0.1 test qsdfghj +... +10.0.0.2 root password +... + +When a better way may be: +--------- +./module host=FILE2 password=FILE1 user=FILE0 0=logins.txt 1=passwords.txt 2=hosts.txt +10.0.0.1 root password +10.0.0.2 root password +10.0.0.1 admin password +10.0.0.2 admin password +10.0.0.1 root 123456 +10.0.0.2 root 123456 +10.0.0.1 admin 123456 +... + + +* Keywords + +Brute-force a list of hosts with a file containing combo entries (each line := login:password). +--------- +./module host=FILE0 user=COMBO10 password=COMBO11 0=hosts.txt 1=combos.txt + + +Scan subnets to just grab version banners. +--------- +./module host=NET0 0=10.0.1.0/24,10.0.2.0/24,10.0.3.128-10.0.3.255 + + +* Actions & Conditions + +Use the -x option to do specific actions upon receiving expected results. For instance: + +To ignore responses with status code 301 *AND* a size within a range. +--------- +./module host=10.0.0.1 user=FILE0 -x ignore:code=301,size=57-74 + +To ignore responses with status code 500 *OR* containing "Internal error". +--------- +./module host=10.0.0.1 user=FILE0 -x ignore:code=500 -x ignore:fgrep='Internal error' + +Remember that conditions are ANDed within the same -x option, use multiple -x options to +specify ORed conditions. + + +* Failures (--failure-delay and --max-retries options) + +During execution, failures may happen, such as a TCP connect timeout for +instance. A failure is actually an exception that is not caught by the module, +and as a result the exception is caught upstream by the controller. By default, +such exceptions, or failures, are not reported to the user, the controller will +try 5 more times before reporting the failed payload with the code "xxx" +(--max-retries defaults to 5). + +After catching a failure, the controller will discard the module instance that +may be in a dubious state to create a brand new one, and then sleep for 0.5 +second before trying again the same payload (--failure-delay defaults to 0.5). + + +* Read carefully the following examples to get a good understanding of how patator works. +{{{ FTP + +* Brute-force authentication. + (a) Establish a new TCP connection for every login attempt (slow). +--------- (a) +ftp_login host=10.0.0.1 user=FILE0 password=FILE1 0=logins.txt 1=passwords.txt persistent=0 + +NB. If you get errors like "too many connections from your IP address", try + decreasing the number of threads, the server may be enforcing a maximum + number of concurrent connections. + + +* Same as before, but without persistent=0 in order to re-use the TCP connection (faster). + (a) Establish a new TCP connection after 3 login attempts were done using the same TCP connection. + (b) Do not report wrong passwords. + (c) Reconnect when a valid password is found (need to logoff before testing other passwords). +--------- (a) (b) (c) +ftp_login ... --rate-reset 3 -x ignore:mesg='Login incorrect.' -x reset:fgrep='Login successful' + + +* Same as before, but without --rate-reset as we automatically detect when the server has + closed the connection. + (a) Do not report everytime the server shuts down the TCP connection, reconnect and + retry last login/password. + (b) Exit execution as soon as a valid password is found. +--------- (a) (b) +ftp_login ... -x ignore,reset,retry:code=500 -x quit:fgrep='Login successful' + + +* Find anonymous FTP servers on a subnet. +--------- +ftp_login host=NET0 user=anonymous password=test@example.com 0=10.0.0.0/24 + +}}} +{{{ SSH +* Brute-force authentication. + (a) Test 3 passwords within the same SSH session before reconnecting. + (b) Reconnect when a valid password is found (need to logoff before testing other passwords). +--------- (a) (b) +ssh_login host=10.0.0.1 user=root password=FILE0 0=passwords.txt --rate-reset 3 -x reset:code=0 + +NB. If you get errors like "Error reading SSH protocol banner ... Connection reset by peer", + try decreasing the number of threads, the server may be enforcing a maximum + number of concurrent connections (eg. MaxStartups in OpenSSH). + + +* Same as before, but without --rate-reset as we automatically detect when we have reached + the maximum number of login attempts permitted per connection (eg. MaxAuthTries > 3 in OpenSSH). + (a) Do not report wrong passwords. + (b) Do not report everytime the server shuts down the TCP connection, reconnect and + retry last password. +--------- (a) (b) +ssh_login ... -x ignore:mesg='Authentication failed.' -x ignore,reset,retry:mesg='No existing session' + +}}} +{{{ Telnet + +* Brute-force authentication. + (a) Enter login after first prompt is detected, enter password after second prompt. + (b) Reconnect everytime the server returns a different error message. + (c) Do not report these error messages. +------------ (a) +telnet_login host=10.0.0.1 inputs='FILE0\nFILE1' 0=logins.txt 1=passwords.txt + -x reset:fgrep!='Login incorrect' -x ignore:egrep='Login incorrect|telnet connection closed' + (b) (c) + +NB. If you still get errors like "telnet connection closed", this is because + they occur at TCP connect time, so try decreasing the number of threads, + the server may be enforcing a maximum number of concurrent connections. + +}}} +{{{ SMTP + +* Enumerate valid users using the VRFY command. + (a) Do not report invalid recipients. + (b) Do not report when the server shuts us down with "421 too many errors", + reconnect and resume testing. +--------- (a) +smtp_vrfy host=10.0.0.1 user=FILE0 0=logins.txt -x ignore:fgrep='User unknown in local + recipient table' -x ignore,reset,retry:code=421 + (b) + +* Use the RCPT TO command in case the VRFY command was disabled. +--------- +smtp_rcpt host=10.0.0.1 user=FILE0@localhost 0=logins.txt helo='EHLO mx.fb.com' mail_from=root + + +}}} +{{{ HTTP + +* Find hidden Web resources. + (a) Use a specific header. + (b) Follow redirects. + (c) Do not report 404 errors. + (d) Retry on 500 errors. +--------- (a) +http_fuzz url=http://localhost/FILE0 0=words.txt header='Cookie: SESSID=A2FD8B2DA4' + follow=1 -x ignore:code=404 -x ignore,retry:code=500 + (b) (c) (d) + +NB. To go 10 times faster, you should use webef (http://www.hsc.fr/ressources/outils/webef/). + It is the fastest HTTP brute-forcer I know, yet at the moment it still lacks useful + features, that will prevent you from performing the following attacks. + +* Brute-force phpMyAdmin logon. + (a) Use POST requests. + (b) Follow redirects using cookies sent by server. + (c) Ignore failed authentications. +--------- (a) (b) (b) +http_fuzz url=http://10.0.0.1/phpmyadmin/index.php method=POST follow=1 accept_cookie=1 + body='pma_username=root&pma_password=FILE0&server=1&lang=en' 0=passwords.txt + -x ignore:fgrep='Cannot log in to the MySQL server' + (c) + +* Scan subnet for directory listings. + (a) Ignore not matching reponses. + (b) Save matching responses into directory. +--------- +http_fuzz url=http://NET0/FILE1 0=10.0.0.0/24 1=dirs.txt -x ignore:fgrep!='Index of' + -l /tmp/dirlist (a) + (b) + +* Brute-force Basic authentication. + (a) Single mode (login == password). + (b) Do not use persistent connections (otherwise the Tomcat will crash). +--------- +http_fuzz url=http://10.0.0.1/manager/html user_pass=FILE0:FILE0 0=logins.txt + -x ignore:code=401 persistent=0 (a) + (b) + +* Find hidden virtual hosts. + (a) Read template from file. + (b) Fuzz both the Host and User-Agent headers. +--------- +echo -e 'Host: FILE0\nUser-Agent: FILE1' > headers.txt + (a) (b) +http_fuzz url=http://10.0.0.1/ header=@headers.txt 0=vhosts.txt 1=agents.txt + + +* Brute-force logon using GET requests. + (a) Encode everything surrounded by the tags ENC__ and __ENC in hexadecimal. + (b) Ignore HTTP 200 responses with a content size (header+body) within given range + and that also contain the given string. + (c) Use a different delimiter string (the comma cannot be escaped, yet). +--------- (a) +http_fuzz url='http://localhost/login?username=admin&password=ENC__FILE0__ENC' encoding=ENC:hex + 0=words.txt follow=1 -x ignore:'code=200|size=100-500|fgrep=Welcome, unauthenticated user' -X'|' + (b) (c) +}}} +{{{ LDAP + +* Brute-force authentication. + (a) Do not report wrong passwords. + (b) Talk SSL/TLS to port 636. +--------- +ldap_login host=10.0.0.1 bindn='cn=FILE0,dc=example,dc=com' 0=logins.txt bindpw=FILE1 1=passwords.txt + -x ignore:mesg='ldap_bind: Invalid credentials (49)' ssl=1 port=636 + (a) (b) +}}} +{{{ SMB + +* Brute-force authentication. + (a) Do not report wrong passwords. + (b) Reconnect when a valid password is found (need to logoff before testing other passwords). +--------- +smb_login host=10.0.0.1 user=FILE0 password=FILE1 0=logins.txt 1=passwords.txt + -x ignore:fgrep=STATUS_LOGON_FAILURE -x reset:code=0 + (a) (b) + +NB. If you suddenly get STATUS_ACCOUNT_LOCKED_OUT errors for an account + although it is not the first password you test on this account, then you must + have locked it. + + +* Pass-the-hash. + (a) Test a list of hosts. + (b) Test every user (each line := login:rid:LM hash:NT hash). +--------- (a) (b) +smb_login host=FILE0 0=hosts.txt user=COMBO10 password_hash=COMBO12:COMBO13 1=pwdump.txt -x ... + +}}} +{{{ MSSQL + +* Brute-force authentication. +----------- +mssql_login host=10.0.0.1 user=sa password=FILE0 0=passwords.txt -x ignore:fgrep='Login failed for user' + +}}} +{{{ Oracle +Beware, by default in Oracle, accounts are permanently locked out after 10 wrong passwords, +except for the SYS account. + +* Brute-force authentication. +------------ +oracle_login host=10.0.0.1 user=SYS password=FILE0 0=passwords.txt sid=ORCL -x ignore:code=ORA-01017 + +NB0. With Oracle 10g XE (Express Edition), you do not need to pass a SID. + +NB1. If you get ORA-12516 errors, it may be because you reached the limit of + concurrent connections or db processes, try using "--rate-limit 0.5 -t 2" to be + more polite. Also you can run "alter system set processes=150 scope=spfile;" + and restart your database to get rid of this. + + +* Brute-force SID. +------------ +oracle_login host=10.0.0.1 sid=FILE0 0=sids.txt -x ignore:code=ORA-12505 + +NB. Against Oracle9, it may crash (Segmentation fault) as soon as a valid SID is + found (cx_Oracle bug). Sometimes, the SID gets printed out before the crash, + so try running the same command again if it did not. + +}}} +{{{ MySQL + +* Brute-force authentication. +----------- +mysql_login host=10.0.0.1 user=FILE0 password=FILE0 0=logins.txt -x ignore:fgrep='Access denied for user' + +}}} +{{{ PostgresSQL + +* Brute-force authentication. +----------- +pgsql_login host=10.0.0.1 user=postgres password=FILE0 0=passwords.txt + -x ignore:fgrep='password authentication failed for user' + +}}} +{{{ VNC +Some VNC servers have built-in anti-bruteforce functionnality that temporarily +blacklists the attacker IP address after too many wrong passwords. + - RealVNC-4.1.3 or TightVNC-1.3.10 for example, allow 5 failed attempts and + then enforce a 10 second delay. For each subsequent failed attempt that + delay is doubled. + - RealVNC-3.3.7 or UltraVNC allow 6 failed attempts and then enforce a 10 + second delay between each following attempt. + +* Brute-force authentication. + (a) No need to use more than one thread. + (b) Keep retrying the same password when we are blacklisted by the server. + (c) Exit execution as soon as a valid password is found. +--------- (a) +vnc_login host=10.0.0.1 password=FILE0 0=passwords.txt --threads 1 + -x retry:fgrep!='Authentication failure' --max-retries -1 -x quit:code=0 + (b) (b) (c) +}}} +{{{ Unzip + +* Brute-force the ZIP file password. +---------- +unzip_pass zipfile=path/to/file.zip password=FILE0 0=passwords.txt -x ignore:code!=0 + +}}} +{{{ DNS + +* Forward lookup subdomains. + (a) Ignore NXDOMAIN responses (rcode 3). +----------- +dns_forward domain=FILE0.google.com 0=names.txt -x ignore:code=3 + (a) +* Forward lookup domain with all possible TLDs. +----------- +dns_forward domain=google.MOD0 0=TLD -x ignore:code=3 + +* Foward lookup SRV records. +----------- +dns_forward domain=MOD0.microsoft.com 0=SRV qtype=SRV -x ignore:code=3 + +* Reverse lookup several subnets. + (a) Ignore names that do not contain 'google.com'. + (b) Ignore generic PTR records. +----------- +dns_reverse host=NET0 0=216.239.32.0-216.239.47.255,8.8.8.0/24 -x ignore:code=3 -x ignore:fgrep!=google.com -x ignore:fgrep=216-239- + (a) (b) +}}} +{{{ SNMP + +* SNMPv1/2 : Find valid community names. +---------- +snmp_login host=10.0.0.1 community=FILE0 1=names.txt -x ignore:mesg='No SNMP response received before timeout' + + +* SNMPv3 : Find valid usernames. +---------- +snmp_login host=10.0.0.1 version=3 user=FILE0 0=logins.txt -x ignore:mesg=unknownUserName + + +* SNMPv3 : Find valid passwords. +---------- +snmp_login host=10.0.0.1 version=3 user=myuser auth_key=FILE0 0=passwords.txt -x ignore:mesg=wrongDigest + +NB0. If you get "notInTimeWindow" error messages, increase the retries option. +NB1. SNMPv3 requires passphrases to be at least 8 characters long. + +}}} + +CHANGELOG +--------- + +* v0.1 2011/11/25 : Public release + + +TODO +---- + * SSL support for SMTP, MySQL, ... (use socat in the meantime) + * new option -e ns like in Medusa (not likely to be implemented due to design) + * replace PyDNS|paramiko|IPy with a better module (scapy|libssh2|... ?) +''' + +# }}} + +# imports and logging {{{ +import logging +formatter = logging.Formatter('%(asctime)s %(name)-7s %(levelname)7s - %(message)s', datefmt='%H:%M:%S') +handler = logging.StreamHandler() +handler.setFormatter(formatter) +logger = logging.getLogger('patator') +logger.setLevel(logging.INFO) +logger.addHandler(handler) + +import re +from time import sleep, time +from Queue import Queue, Empty, Full +from threading import Thread, active_count +from select import select +from sys import stdin, exc_info, exit +import os +from time import localtime, strftime, sleep +from itertools import product, chain, islice +from string import ascii_lowercase +from binascii import hexlify +from base64 import b64encode +from datetime import timedelta, datetime +from struct import unpack +import socket +import subprocess + +warnings = [] +try: + from IPy import IP +except ImportError: + warnings.append('IPy (https://github.com/haypo/python-ipy/wiki) is required for using NETx keywords.') + +# imports }}} + +# utils {{{ +def create_dir(top_path): + top_path = os.path.abspath(top_path) + if os.path.isdir(top_path): + files = os.listdir(top_path) + if files: + if raw_input("Directory '%s' is not empty, do you want to wipe it ? [Y/n]: " % top_path) == 'n': + exit(0) + for root, dirs, files in os.walk(top_path): + if dirs: + print("Directory '%s' contains sub-directories, safely aborting..." % root) + exit(0) + for f in files: + os.unlink(os.path.join(root, f)) + break + else: + os.mkdir(top_path) + return top_path + +def create_time_dir(top_path, desc): + now = localtime() + date, time = strftime('%Y-%m-%d', now), strftime('%H%M%S', now) + top_path = os.path.abspath(top_path) + date_path = os.path.join(top_path, date) + time_path = os.path.join(top_path, date, time + '_' + desc) + + if not os.path.isdir(top_path): + os.makedirs(top_path) + if not os.path.isdir(date_path): + os.mkdir(date_path) + if not os.path.isdir(time_path): + os.mkdir(time_path) + + return time_path + +def pprint_seconds(seconds, fmt): + return fmt % reduce(lambda x,y: divmod(x[0], y) + x[1:], [(seconds,),60,60]) + +# }}} + +# Controller {{{ +class Controller: + actions = {} + paused = False + total_size = 1 + log_dir = None + thread_report = [] + thread_progress = [] + + payload = {} + iter_keys = {} + enc_keys = [] + + builtin_actions = ( + ('ignore', 'do not report'), + ('retry', 'try payload again'), + ('quit', 'terminate execution now'), + ) + + available_encodings = { + 'hex': (hexlify, 'encode in hexadecimal'), + 'b64': (b64encode, 'encode in base64'), + } + + def expand_key(self, arg): + yield arg.split('=', 1) + + def find_file_keys(self, value): + return map(int, re.findall(r'FILE(\d)', value)) + + def find_net_keys(self, value): + return map(int, re.findall(r'NET(\d)', value)) + + def find_combo_keys(self, value): + return [map(int, t) for t in re.findall(r'COMBO(\d)(\d)', value)] + + def find_module_keys(self, value): + return map(int, re.findall(r'MOD(\d)', value)) + + def usage_parser(self, name): + from optparse import OptionParser + from optparse import OptionGroup + + usage_hints = self.module.usage_hints + + available_actions = self.builtin_actions + self.module.available_actions + available_conditions = self.module.Response.available_conditions + + parser = OptionParser() + usage = ''' +%s''' % '\n'.join(usage_hints) + + usage += ''' + +Module options: +%s + +* Allowed format in () +* Allowed values in [] with the default value always listed first +''' % ('\n'.join(' %-14s: %s' % (k, v) for k, v in self.module.available_options)) + + usage += ''' +Syntax: + -x arg := actions:conditions + actions := action[,action]* + action := "%s" + conditions := condition=value[,condition=value]* + condition := "%s" +''' % ('" | "'.join(k for k, v in available_actions), + '" | "'.join(k for k, v in available_conditions)) + + usage += ''' +%s + +%s +''' % ('\n'.join(' %-12s: %s' % (k, v) for k, v in available_actions), + '\n'.join(' %-12s: %s' % (k, v) for k, v in available_conditions)) + + usage += ''' + -e meta:encoding + + encoding := "%s" + +%s''' % ('" | "'.join(k for k in self.available_encodings), + '\n'.join(' %-12s: %s' % (k, v) for k, (f, v) in self.available_encodings.iteritems())) + + + parser.usage = usage.replace('%prog', name) + + exe_grp = OptionGroup(parser, 'Execution') + exe_grp.add_option('-x', dest='actions', action='append', default=[], metavar='arg', help='actions and conditions, see Syntax above') + exe_grp.add_option('--start', dest='start', type='int', default=0, metavar='N', help='start from N (resume feature)') + exe_grp.add_option('--stop', dest='stop', type='int', default=None, metavar='N', help='stop at N') + exe_grp.add_option('-e', dest='encodings', action='append', default=[], metavar='meta:encoding', help='encode everything inside meta__.+?__meta') + exe_grp.add_option('-C', dest='combo_delim', default=':', metavar='str', help="delimiter string in combo files (default is ':')") + exe_grp.add_option('-X', dest='condition_delim', default=',', metavar='str', help="delimiter string in conditions (default is ',')") + + opt_grp = OptionGroup(parser, 'Optimization') + opt_grp.add_option('--rate-limit', dest='rate_limit', type='float', default=0, metavar='N', help='wait N seconds between tests (default is 0)') + opt_grp.add_option('--rate-reset', dest='rate_reset', type='int', default=0, metavar='N', help='reset module every N tests (default is 0)') + opt_grp.add_option('--failure-delay', dest='failure_delay', type='float', default=0.5, metavar='N', help='wait N seconds after a failure (default is 0.5)') + opt_grp.add_option('--max-retries', dest='max_retries', type='int', default=5, metavar='N', help='skip payload after N failures (default is 5) (-1 for unlimited)') + opt_grp.add_option('-t', '--threads', dest='num_threads', type='int', default=10, metavar='N', help='number of threads (default is 10)') + + log_grp = OptionGroup(parser, 'Logging') + log_grp.add_option('-l', dest='log_dir', metavar='DIR', help="save output and response data into DIR ") + log_grp.add_option('-L', dest='auto_log', metavar='SFX', help="automatically save into DIR/yyyy-mm-dd/h:m:s_SFX (DIR defaults to '/tmp/patator')") + + dbg_grp = OptionGroup(parser, 'Debugging') + dbg_grp.add_option('-d', '--debug', dest='debug', action='store_true', default=False, help='enable debug messages') + + parser.option_groups.extend([exe_grp, opt_grp, log_grp, dbg_grp]) + + return parser + + def parse_usage(self, argv): + parser = self.usage_parser(argv[0]) + opts, args = parser.parse_args(argv[1:]) + + if opts.debug: + logger.setLevel(logging.DEBUG) + + if not len(args) > 0: + parser.error('wrong usage') + + return opts, args + + def __init__(self, module, argv): + self.module = module + opts, args = self.parse_usage(argv) + + self.combo_delim = opts.combo_delim + self.condition_delim = opts.condition_delim + self.rate_reset = opts.rate_reset + self.rate_limit = opts.rate_limit + self.failure_delay = opts.failure_delay + self.max_retries = opts.max_retries + self.num_threads = opts.num_threads + self.start, self.stop = opts.start, opts.stop + + wlists = {} + kargs = [] + for arg in args: # ('host=NET0', '0=10.0.0.0/24', 'user=COMBO10', 'password=COMBO11', '1=combos.txt', 'domain=MOD2', '2=TLD') + for k, v in self.expand_key(arg): + logger.debug('k: %s, v: %s' % (k, v)) + + if k.isdigit(): + wlists[k] = v + + else: + if v.startswith('@'): + p = os.path.expanduser(v[1:]) + v = open(p).read() + kargs.append((k, v)) + + iter_vals = [v for k, v in sorted(wlists.iteritems())] + logger.debug('iter_vals: %s' % iter_vals) + logger.debug('kargs: %s' % kargs) + # iter_vals == ['10.0.0.0/24', 'combos.txt', 'TLD'] + # kargs == [('host', 'NET0'), ('user', 'COMBO10'), ('password', 'COMBO11'), ('domain', 'MOD2')] + + for k, v in kargs: + + for e in opts.encodings: + meta, enc = e.split(':') + if re.search(r'{0}__.+?__{0}'.format(meta), v): + self.enc_keys.append((k, meta, self.available_encodings[enc][0])) + + for i in self.find_file_keys(v): + if i not in self.iter_keys: + self.iter_keys[i] = ('FILE', iter_vals[i], []) + self.iter_keys[i][2].append(k) + + else: + for i in self.find_net_keys(v): + if i not in self.iter_keys: + self.iter_keys[i] = ('NET', iter_vals[i], []) + self.iter_keys[i][2].append(k) + + else: + for i, j in self.find_combo_keys(v): + if i not in self.iter_keys: + self.iter_keys[i] = ('COMBO', iter_vals[i], []) + self.iter_keys[i][2].append((j, k)) + + else: + for i in self.find_module_keys(v): + if i not in self.iter_keys: + self.iter_keys[i] = ('MOD', iter_vals[i], []) + self.iter_keys[i][2].append(k) + + else: + self.payload[k] = v + + logger.debug('iter_keys: %s' % self.iter_keys) +# { 0: ('NET', '10.0.0.0/24', ['host']), 1: ('COMBO', 'combos.txt', [(0, 'user'), (1, 'password')]), 2: ('MOD', 'TLD', ['domain']) + logger.debug('enc_keys: %s' % self.enc_keys) +# [('password', 'ENC', hexlify), ('header', 'B64', b64encode), ... + logger.debug('payload: %s' % self.payload) + + for k, _ in self.builtin_actions: + self.actions[k] = [] + + self.module_actions = [k for k, _ in self.module.available_actions] + for k in self.module_actions: + self.actions[k] = [] + + for x in opts.actions: + self.update_actions(x) + + logger.debug('actions: %s' % self.actions) + + if opts.auto_log: + self.log_dir = create_time_dir(opts.log_dir or '/tmp/patator', opts.auto_log) + elif opts.log_dir: + self.log_dir = create_dir(opts.log_dir) + + if self.log_dir: + log_file = os.path.join(self.log_dir, 'RUNTIME.log') + with open(log_file, 'w') as f: + f.write('$ %s\n' % ' '.join(argv)) + + handler = logging.FileHandler(log_file) + handler.setFormatter(formatter) + logging.getLogger('patator').addHandler(handler) + + def update_actions(self, arg): + actions, conditions = arg.split(':', 1) + + for action in actions.split(','): + conds = conditions.split(self.condition_delim) + new_cond = [] + + for cond in conds: + key, val = cond.split('=', 1) + new_cond.append((key, val)) + + self.actions[action].append(new_cond) + + def lookup_actions(self, resp): + actions = [] + for action, conditions in self.actions.iteritems(): + for condition in conditions: + for key, val in condition: + if key[-1] == '!': + if resp.match(key[:-1], val): + break + else: + if not resp.match(key, val): + break + else: + actions.append(action) + return actions + + def fire(self): + + try: + self.start_threads() + self.monitor_progress() + except SystemExit: pass + except KeyboardInterrupt: print + + self.show_final() + + total_time = time() - self.start_time + speed_avg = self.total_size / total_time + + hits_count = sum(p.hits_count for p in self.thread_progress) + done_count = sum(p.done_count for p in self.thread_progress) + fail_count = sum(p.fail_count for p in self.thread_progress) + + logger.info('Hits/Done/Size/Fail: %d/%d/%d/%d, Avg: %d r/s, Time: %s' % (hits_count, + done_count, self.total_size, fail_count, speed_avg, pprint_seconds(total_time, '%dh %dm %ds'))) + + if self.total_size != done_count: + logger.info('To resume execution, pass --start %d' % (self.start + done_count)) + + def push_final(self, resp): pass + def show_final(self): pass + + def start_threads(self): + gqueues = [Queue(maxsize=10000) for i in range(self.num_threads)] + + # producer + t = Thread(target=self.produce, args=(gqueues,)) + t.daemon = True + t.start() + + class Progress: + def __init__(self): + self.current = '' + self.done_count = 0 + self.hits_count = 0 + self.fail_count = 0 + self.seconds = [1]*25 # avoid division by zero early bug condition + + # consumers + for num in range(self.num_threads): + pqueue = Queue() + t = Thread(target=self.consume, args=(gqueues[num], pqueue)) + t.daemon = True + t.start() + self.thread_report.append(pqueue) + self.thread_progress.append(Progress()) + + def produce(self, queues): + + iterables = [] + for t, v, _ in self.iter_keys.itervalues(): + + if t in ('FILE', 'COMBO'): + #iterable, size = self.builtin_keywords[t](v) + files = map(os.path.expanduser, v.split(',')) + size = sum(sum(1 for _ in open(f)) for f in files) + iterable = chain(*map(open, files)) + + elif t == 'NET': + subnets = [IP(n, make_net=True) for n in v.split(',')] + size = sum(len(s) for s in subnets) + iterable = chain(*subnets) + + elif t == 'MOD': + iterable, size = self.module.available_keys[v]() + + else: + raise NotImplementedError("Incorrect keyword '%s'" % t) + + self.total_size *= size + iterables.append(iterable) + + if self.stop: + self.total_size = self.stop - self.start + else: + self.total_size -= self.start + + logger.info('Starting Patator v%s' % __version__) + logger.info('-' * 63) + logger.info('%-15s | %-25s \t | %5s | %s ..' % ('code & size', 'candidate', 'num', 'mesg')) + logger.info('-' * 63) + + self.start_time = time() + count = 0 + for prod in islice(product(*iterables), self.start, self.stop): + queues[count % self.num_threads].put(map(lambda s: str(s).strip('\r\n'), prod)) + count += 1 + + for q in queues: + q.put(None) + + def consume(self, gqueue, pqueue): + module = self.module() + rate_count = 0 + + while True: + prod = gqueue.get() + if not prod: return + + payload = self.payload.copy() + + for i, (t, _, keys) in self.iter_keys.iteritems(): + if t == 'FILE': + for k in keys: + payload[k] = payload[k].replace('FILE%d' % i, prod[i]) + elif t == 'NET': + for k in keys: + payload[k] = payload[k].replace('NET%d' % i, prod[i]) + elif t == 'COMBO': + for j, k in keys: + payload[k] = payload[k].replace('COMBO%d%d' % (i, j), prod[i].split(self.combo_delim)[j]) + elif t == 'MOD': + for k in keys: + payload[k] = payload[k].replace('MOD%d' %i, prod[i]) + + for k, m, e in self.enc_keys: + payload[k] = re.sub(r'{0}__(.+?)__{0}'.format(m), lambda m: e(m.group(1)), payload[k]) + + pp_prod = ':'.join(prod) + logger.debug('pp_prod: %s' % pp_prod) + + num_try = 0 + start_time = time() + while num_try < self.max_retries or self.max_retries < 0: + num_try += 1 + + while self.paused: + sleep(1) + + if self.rate_reset > 0: + if rate_count == self.rate_reset: + logger.debug('Reset module') + module = self.module() + rate_count = 0 + else: + rate_count += 1 + + if self.rate_limit: + sleep(self.rate_limit) + + try: + logger.debug('Trying: %s' % payload) + resp = module.execute(**payload) + + except: + e_type, e_value, _ = exc_info() + resp = '%s, %s' % (e_type, e_value.args) + logger.debug('except: %s' % resp) + module = self.module() + sleep(self.failure_delay) + continue + + actions = self.lookup_actions(resp) + pqueue.put_nowait((actions, pp_prod, resp, time() - start_time)) + + for a in self.module_actions: + if a in actions: + getattr(module, a)(**payload) + + if 'retry' in actions: + logger.debug('Retry %d/%d: %s' % (num_try, self.max_retries, resp)) + sleep(self.failure_delay) + continue + + break + + else: + pqueue.put_nowait((['fail'], pp_prod, resp, time() - start_time)) + + def monitor_progress(self): + while active_count() > 1: + self.report_progress() + self.monitor_interaction() + + self.report_progress() + + def report_progress(self): + for i, pq in enumerate(self.thread_report): + while True: + + try: + actions, current, resp, seconds = pq.get_nowait() + logger.debug('actions: %s' % actions) + + except Empty: + break + + p = self.thread_progress[i] + offset = (self.start + p.done_count * self.num_threads) + i + 1 + p.current = current + p.seconds[p.done_count % len(p.seconds)] = seconds + + if 'fail' in actions: + p.fail_count += 1 + p.done_count += 1 + logger.warn('%-15s | %-25s \t | %5d | %s' % ('xxx', current, offset, resp)) + continue + + if 'ignore' not in actions: + p.hits_count += 1 + logger.info('%-15s | %-25s \t | %5d | %s' % (resp.compact(), current, offset, resp)) + + if self.log_dir: + filename = '%d_%s' % (offset, resp.compact().replace(' ', '_')) + with open('%s/%s.txt' % (self.log_dir, filename), 'w') as f: + f.write(resp.dump()) + + self.push_final(resp) + + if 'retry' not in actions: + p.done_count += 1 + + if 'quit' in actions: + logger.info('Quitting (user match condition)') + raise SystemExit + + + def monitor_interaction(self): + + i, _, _ = select([stdin], [], [], .1) + if not i: return + command = stdin.readline().strip() + + if command == 'h': + logger.info('''Available commands: + h show help + show progress + d/D increase/decrease debug level + p pause progress + f show verbose progress + x arg add monitor condition + a show all active conditions + q terminate execution now + ''') + + elif command == 'q': + raise KeyboardInterrupt + + elif command == 'p': + self.paused = not self.paused + logger.info(self.paused and 'Paused' or 'Unpaused') + + elif command == 'd': + logger.setLevel(logging.DEBUG) + + elif command == 'D': + logger.setLevel(logging.INFO) + + elif command == 'a': + logger.info(self.actions) + + elif command.startswith('x'): + _, arg = command.split(' ', 1) + self.update_actions(arg) + + else: # show progress + total_count = sum(p.done_count for p in self.thread_progress) + speed_avg = self.num_threads / (sum(sum(p.seconds) / len(p.seconds) for p in self.thread_progress) / self.num_threads) + remain_seconds = (self.total_size - total_count) / speed_avg + etc_time = datetime.now() + timedelta(seconds = remain_seconds) + + logger.info('Progress: {0:>3}% ({1}/{2}) | Speed: {3:.0f} r/s | ETC: {4} ({5} remaining) {6}'.format( + total_count * 100/self.total_size, + total_count, + self.total_size, + speed_avg, + etc_time.strftime('%H:%M:%S'), + pprint_seconds(remain_seconds, '%02d:%02d:%02d'), + self.paused and '| Paused' or '')) + + if command == 'f': + for i, p in enumerate(self.thread_progress): + logger.info(' #{0}: {1:>3}% ({2}/{3}) {4}'.format( + i, + p.done_count * 100/(self.total_size/self.num_threads), + p.done_count, + self.total_size/self.num_threads, + p.current)) + +# }}} + +# Response_Base {{{ +def match_size(size, val): + if '-' in val: + size_min, size_max = map(int, val.split('-')) + return size_min <= size <= size_max + else: + return size == int(val) + +class Response_Base: + + available_conditions = ( + ('code', 'match status code'), + ('size', 'match size (N or N-N)'), + ('mesg', 'match message'), + ('fgrep', 'search for string'), + ('egrep', 'search for regex'), + ) + + def __init__(self, code, mesg, trace=''): + self.code, self.mesg = code, mesg + self.size = len(self.mesg) + self.trace = trace + + def compact(self): + return '%s %s' % (self.code, self.size) + + def __str__(self): + return self.mesg + + def match(self, key, val): + return getattr(self, 'match_'+key)(val) + + def match_code(self, val): + return val == str(self.code) + + def match_size(self, val): + return match_size(self.size, val) + + def match_mesg(self, val): + return val == self.mesg + + def match_fgrep(self, val): + return val in str(self) + + def match_egrep(self, val): + return re.search(val, str(self)) + + def dump(self): + return self.trace or str(self) + +# }}} + +# TCP_Cache {{{ +class TCP_Cache: + + available_actions = ( + ('reset', 'close current connection in order to reconnect for next probe'), + ) + + available_options = ( + ('persistent', 'use persistent connections [1|0]'), + ) + + cache_keys = ('host', 'port') + + def __init__(self): + self.cache = {} # {'10.0.0.1:21': fp, ...} + + def __del__(self): + for k in self.cache.keys(): + self.del_tcp(k) + + def get_key(self, **kwargs): + keys = [] + for k in self.cache_keys: + if k in kwargs: + keys.append(kwargs[k]) + return ':'.join(k for k in keys if k is not None), keys + + def get_tcp(self, persistent, **kwargs): + k, z = self.get_key(**kwargs) + if k not in self.cache: + + logger.debug('New connection: %s' % k) + fp, banner = self.new_tcp(*z) + + if persistent == '1': + self.cache[k] = fp + + else: + fp, banner = self.cache[k], '' + + return fp, banner + + def del_tcp(self, k): + if k in self.cache: + logger.debug('Delete connection: %s' % k) + fp = self.cache[k] + try: fp.close() + except: pass + del self.cache[k] + + def reset(self, **kwargs): + k, _ = self.get_key(**kwargs) + logger.debug('Reset connection: %s' % k) + self.del_tcp(k) + +# }}} + +# FTP {{{ +from ftplib import FTP, Error as FTP_Error +class FTP_login(TCP_Cache): + '''Brute-force FTP authentication''' + + usage_hints = ( + """%prog host=10.0.0.1 user=FILE0 password=FILE1 0=logins.txt 1=passwords.txt""" + """ -x ignore:mesg='Login incorrect.' -x ignore,reset,retry:code=500 -x reset:fgrep='Login successful'""", + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [21]'), + ('user', 'usernames to test'), + ('password', 'passwords to test'), + ) + available_options += TCP_Cache.available_options + + Response = Response_Base + + def new_tcp(self, host, port): + fp = FTP() + resp = fp.connect(host, int(port or 21)) + return fp, resp + + def execute(self, host, port=None, user=None, password=None, persistent='1'): + try: + fp, resp = self.get_tcp(persistent, host=host, port=port) + + if user is not None: + resp = fp.sendcmd('USER ' + user) + if password is not None: + resp = fp.sendcmd('PASS ' + password) + + logger.debug('No error: %s' % resp) + + except FTP_Error as (resp,): + logger.debug('FTP_Error: %s' % resp) + + except EOFError: + logger.debug('EOFError') + resp = '500 Connection reset by peer' + + except socket.error: + logger.debug('socket.error') + resp = '500 Connection reset by peer' + + code, mesg = resp.split(' ', 1) + return self.Response(code, mesg) + +# }}} + +# SSH {{{ +try: + import paramiko + l = logging.getLogger('paramiko.transport') + l.setLevel(logging.CRITICAL) + l.addHandler(handler) +except ImportError: + warnings.append('paramiko (http://www.lag.net/paramiko/) is required for SSH.') + +class SSH_login(TCP_Cache): + '''Brute-force SSH authentication''' + + usage_hints = ( + """%prog host=10.0.0.1 user=root password=FILE0 0=passwords.txt""" + """ -x ignore:mesg='Authentication failed.' -x ignore,reset,retry:mesg='No existing session' -x reset:code=0""", + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [22]'), + ('user', 'usernames to test'), + ('password', 'passwords to test'), + ('auth_type', 'auth type to use [password|keyboard-interactive]'), + ) + available_options += TCP_Cache.available_options + + Response = Response_Base + + cache_keys = ('host', 'port', 'user') + def new_tcp(self, host, port, user): + fp = paramiko.Transport('%s:%s' % (host, int(port or 22))) + fp.start_client() + return fp, fp.remote_version + + def execute(self, host, port=None, user=None, password=None, persistent='1', auth_type='password'): + try: + fp, resp = self.get_tcp(persistent, host=host, port=port, user=user) + + if user is not None and password is not None: + if auth_type == 'password': + fp.auth_password(user, password, fallback=False) + + elif auth_type == 'keyboard-interactive': + fp.auth_interactive(user, lambda a,b,c: [password] if len(c) == 1 else []) + + else: + raise NotImplementedError("Incorrect auth_type '%s'" % auth_type) + + logger.debug('No error') + code, mesg = '0', resp + + except paramiko.AuthenticationException as e: + logger.debug('AuthenticationException: %s' % e) + code, mesg = '1', str(e) + + except paramiko.SSHException as e: + logger.debug('SSHException: %s' % e) + code, mesg = '1', str(e) + + return self.Response(code, mesg) + +# }}} + +# Telnet {{{ +from telnetlib import Telnet +class Telnet_login(TCP_Cache): + '''Brute-force Telnet authentication''' + + usage_hints = ( + """%prog host=10.0.0.1 inputs='FILE0\\nFILE1' 0=logins.txt 1=passwords.txt""" + """ -x reset:fgrep!='Login incorrect' -x ignore:egrep='Login incorrect|telnet connection closed'""", + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [23]'), + ('inputs', 'list of values to input'), + ('prompt_re', 'regular expression to match prompts [\w+]'), + ('timeout', 'seconds to wait for prompt_re to match received data [10]'), + ) + available_options += TCP_Cache.available_options + + Response = Response_Base + + def new_tcp(self, host, port): + fp = Telnet(host, int(port or 23)) + self.prompt_count = 0 + return fp, None + + def execute(self, host, port=None, inputs=None, prompt_re='\w+:', timeout=10, persistent='1'): + fp, _ = self.get_tcp(persistent, host=host, port=port) + trace = '' + + if self.prompt_count == 0: + _, _, raw = fp.expect([prompt_re], timeout=timeout) + raw += fp.read_very_eager() + logger.debug('raw banner: %s' % repr(raw)) + trace += raw + self.prompt_count += 1 + + try: + for val in inputs.split(r'\n'): + logger.debug('val: %s' % val) + cmd = val + '\n' + fp.write(cmd) + trace += cmd + + _, _, raw = fp.expect([prompt_re], timeout=timeout) + raw += fp.read_very_eager() + logger.debug('raw %d: %s' % (self.prompt_count, repr(raw))) + trace += raw + self.prompt_count += 1 + + mesg = repr(raw)[1:-1] # strip enclosing single quotes + + except EOFError as e: + mesg, = e + + return self.Response('0', mesg, trace) + +# }}} + +# SMTP {{{ +from smtplib import SMTP +class SMTP_vrfy(TCP_Cache): + '''Enumerate valid users using SMTP VRFY''' + + usage_hints = ( + '''%prog host=10.0.0.1 user=FILE0 0=logins.txt [helo='EHLO blah.example.com']''' + ''' -x ignore:fgrep='User unknown' -x ignore,reset,retry:code=421''', + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [25]'), + ('user', 'usernames to test'), + ('helo', 'first command to send after connect'), + ) + available_options += TCP_Cache.available_options + + Response = Response_Base + + def new_tcp(self, host, port): + fp = SMTP() + resp = fp.connect(host, int(port or 25)) + return fp, resp + + def execute(self, host, port=None, helo=None, user=None, persistent='1'): + fp, resp = self.get_tcp(persistent, host=host, port=port) + + if helo is not None and resp == '': + resp = fp.docmd(helo) + + if user is not None: + resp = fp.docmd('VRFY ' + user) + + code, mesg = resp + return self.Response(code, mesg) + +class SMTP_rcpt(TCP_Cache): + """Enumerate valid users using SMTP RCPT TO""" + + usage_hints = ( + '''%prog host=10.0.0.1 user=FILE0@localhost 0=logins.txt [helo='EHLO blah.example.com']''' + ''' [mail_from=foo@bar.org] -x ignore:fgrep='User unknown' -x ignore,reset,retry:code=421''', + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [25]'), + ('user', 'usernames to test'), + ('mail_from', 'sender email [test@example.org]'), + ('helo', 'first command to send after connect'), + ) + available_options += TCP_Cache.available_options + + Response = Response_Base + + def new_tcp(self, host, port): + fp = SMTP() + resp = fp.connect(host, int(port or 25)) + return fp, resp + + def execute(self, host, port=None, helo=None, mail_from='test@example.org', user=None, persistent='1'): + fp, resp = self.get_tcp(persistent, host=host, port=port) + + if helo is not None and resp == '': + resp = fp.docmd(helo) + + if mail_from: + resp = fp.docmd('MAIL FROM: ' + mail_from) + + if rcpt_to: + resp = fp.docmd('RCPT TO: ' + user) + + fp.docmd('RSET') + + code, mesg = resp + return self.Response(code, mesg) +# }}} + +# LDAP {{{ +class Response_LDAP(Response_Base): + def __init__(self, resp): + self.code, self.out, self.err = resp + self.size = len(self.out + self.err) + self.mesg = ', '.join(p.strip() for p in self.out.splitlines() + self.err.splitlines()) + + def dump(self): + return '\n'.join(['out:', self.out, 'err:', self.err]) + +# Because python-ldap-2.4.4 did not allow using a PasswordPolicyControl +# during bind authentication (cf. http://article.gmane.org/gmane.comp.python.ldap/1003), +# I chose to wrap around ldapsearch with "-e ppolicy". + +class LDAP_login: + '''Brute-force LDAP authentication''' + + usage_hints = ( + """%prog host=10.0.0.1 bindn='cn=Directory Manager' bindpw=FILE0 0=passwords.txt""" + """ -x ignore:mesg='ldap_bind: Invalid credentials (49)'""", + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [389]'), + ('binddn', 'usernames to test'), + ('bindpw', 'passwords to test'), + ('basedn', 'base DN for search'), + ('ssl', 'use SSL/TLS [0|1]'), + ) + available_actions = () + + Response = Response_LDAP + + def execute(self, host, port='389', binddn='', bindpw='', basedn='', ssl='0'): + uri = 'ldap%s://%s:%s' % ('s' if ssl != '0' else '', host, port) + cmd = ['ldapsearch', '-H', uri, '-e', 'ppolicy', '-D', binddn, '-w', bindpw, '-b', basedn] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env={'LDAPTLS_REQCERT': 'never'}) + out = p.stdout.read() + err = p.stderr.read() + code = p.wait() + + return self.Response((code, out, err)) + +# }}} + +# SMB {{{ +try: + from impacket import smb as impacket_smb +except ImportError: + warnings.append('impacket (http://oss.coresecurity.com/projects/impacket.html) is required for SMB.') + +class SMB_login(TCP_Cache): + '''Brute-force SMB authentication''' + + usage_hints = ( + """%prog host=10.0.0.1 user=FILE0 password=FILE1 0=logins.txt 1=passwords.txt""" + """ -x ignore:fgrep=STATUS_LOGON_FAILURE -x reset:code=0""", + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [139]'), + ('user', 'usernames to test'), + ('password', 'passwords to test'), + ('password_hash', "LM/NT hashes to test, at least one hash must be provided ('lm:nt' or ':nt' or 'lm:')"), + ('domain', 'domains to test'), + ) + available_options += TCP_Cache.available_options + + Response = Response_Base + + # ripped from medusa smbnt.c + error_map = { + 0xFF: 'UNKNOWN_ERROR_CODE', + 0x00: 'STATUS_SUCCESS', + 0x0D: 'STATUS_INVALID_PARAMETER', + 0x5E: 'STATUS_NO_LOGON_SERVERS', + 0x6D: 'STATUS_LOGON_FAILURE', + 0x6E: 'STATUS_ACCOUNT_RESTRICTION', + 0x6F: 'STATUS_INVALID_LOGON_HOURS', + 0x70: 'STATUS_INVALID_WORKSTATION', + 0x71: 'STATUS_PASSWORD_EXPIRED', + 0x72: 'STATUS_ACCOUNT_DISABLED', + 0x5B: 'STATUS_LOGON_TYPE_NOT_GRANTED', + 0x8D: 'STATUS_TRUSTED_RELATIONSHIP_FAILURE', + 0x93: 'STATUS_ACCOUNT_EXPIRED', + 0x24: 'STATUS_PASSWORD_MUST_CHANGE', + 0x34: 'STATUS_ACCOUNT_LOCKED_OUT', + 0x01: 'AS400_STATUS_LOGON_FAILURE', + } + + def new_tcp(self, host, port): + fp = impacket_smb.SMB("*SMBSERVER", host, sess_port=int(port or 139)) + return fp, fp.get_server_name() + + def execute(self, host, port=None, user=None, password=None, password_hash=None, domain='', persistent='1'): + fp, mesg = self.get_tcp(persistent, host=host, port=port) + + try: + if user is not None: + if password is not None: + fp.login(user, password, domain) + + else: + lmhash, nthash = password_hash.split(':') + fp.login(user, '', domain, lmhash, nthash) + + code = '0' + + except impacket_smb.SessionError as e: + code = '%x-%x' % (e.error_class, e.error_code) + mesg = self.error_map.get(e.error_code, '') + + error_class = e.error_classes.get(e.error_class, None) # (ERRNT, {}) + if error_class: + class_str = error_class[0] # 'ERRNT' + error_tuple = error_class[1].get(e.error_code, None) # ('ERRnoaccess', 'Access denied.') or None + + if error_tuple: + mesg += ' - %s %s' % error_tuple + else: + mesg += ' - %s' % class_str + + return self.Response(code, mesg) + +# }}} + +# POP {{{ +class Passd_Error(Exception): pass +class Passd: + def connect(self, host, port): + self.fp = socket.create_connection((host, port)) + return self.getresp() # welcome banner + + def close(self): + self.fp.close() + + def sendcmd(self, cmd): + self.fp.sendall(cmd + '\r\n') + return self.getresp() + + def getresp(self): + resp = self.fp.recv(1024) + while not resp.endswith('\r\n'): + resp += self.fp.recv(1024) + + code, _ = self.unparse(resp) + if not code.startswith('2'): + raise Passd_Error, resp + + return resp + + def unparse(self, resp): + i = resp.rstrip().rfind('\n') + 1 + code = resp[i:i+3] + mesg = resp[i+4:] + + return code, mesg + +class POP_passd: + '''Brute-force poppassd authentication (http://netwinsite.com/poppassd/ not POP3)''' + + usage_hints = ( + '''%prog host=10.0.0.1 user=FILE0 password=FILE1 0=logins.txt 1=passwords.txt -x ignore:code=500''', + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [106]'), + ('user', 'usernames to test'), + ('password', 'passwords to test'), + ) + available_actions = () + + Response = Response_Base + + def execute(self, host, port=None, user=None, password=None): + try: + fp = Passd() + resp = fp.connect(host, int(port or 106)) + trace = resp + + if user is not None: + cmd = 'user %s' % user + resp = fp.sendcmd(cmd) + trace += '\r\n'.join((cmd, resp)) + + if password is not None: + cmd = 'pass %s' % password + resp = fp.sendcmd(cmd) + trace += '\r\n'.join((cmd, resp)) + + except Passd_Error as (resp,): + logger.debug('Passd_Error: %s' % resp) + trace += '\r\n'.join((cmd, resp)) + + finally: + fp.close() + + code, mesg = fp.unparse(resp) + return self.Response(code, mesg, trace) + +# }}} + +# MySQL {{{ +try: + import _mysql +except ImportError: + warnings.append('mysql-python (http://sourceforge.net/projects/mysql-python/) is required for MySQL.') + +class MySQL_login: + '''Brute-force MySQL authentication''' + + usage_hints = ( + """%prog host=10.0.0.1 user=FILE0 password=FILE1 0=logins.txt 1=passwords.txt -x ignore:fgrep='Access denied for user'""", + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [3306]'), + ('user', 'usernames to test'), + ('password', 'passwords to test'), + ) + available_actions = () + + Response = Response_Base + + def execute(self, host, port=None, user='anony', password=''): + + try: + fp = _mysql.connect(host=host, port=int(port or 3306), user=user, passwd=password) + resp = '0', fp.get_server_info() + + except _mysql.Error, resp: pass + + code, mesg = resp + return self.Response(code, mesg) + +# }}} + +# MSSQL {{{ +# I did not use pymssql because neither version 1.x nor 2.0.0b1_dev were multithreads safe (they all segfault) +class MSSQL: + # ripped from medusa mssql.c + hdr = '\x02\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + pt2 = '\x30\x30\x30\x30\x30\x30\x61\x30\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x20\x18\x81\xb8\x2c\x08\x03\x01\x06\x0a\x09\x01\x01\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x73\x71\x75\x65\x6c\x64\x61\x20\x31\x2e\x30\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + pt3 = '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x04\x02\x00\x00\x4d\x53\x44\x42\x4c\x49\x42\x00\x00\x00\x07\x06\x00\x00' \ + '\x00\x00\x0d\x11\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00' + + langp = '\x02\x01\x00\x47\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' \ + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x30\x30\x00\x00' \ + '\x00\x03\x00\x00\x00' + + def connect(self, host, port): + self.fp = socket.create_connection((host, port)) + + def login(self, user, password): + MAX_LEN = 30 + user_len = len(user) + password_len = len(password) + data = self.hdr + user[:MAX_LEN] + '\x00' * (MAX_LEN - user_len) + chr(user_len) + \ + password[:MAX_LEN] + '\x00' * (MAX_LEN - password_len) + chr(password_len) + self.pt2 + chr(password_len) + \ + password[:MAX_LEN] + '\x00' * (MAX_LEN - password_len) + self.pt3 + + self.fp.sendall(data) + self.fp.sendall(self.langp) + + resp = self.fp.recv(1024) + code, size = self.parse(resp) + + return code, size + + def parse(self, resp): + i = 8 + while True: + resp = resp[i:] + code, size = unpack('.+)://)?(?P.+?)(?::(?P[^/]+))?/(?P[^?]*)(?:\?(?P.*))?', val).groupdict() + if g['scheme'] == 'https' and not g['port']: + g['port'] = '443' + for k, v in g.iteritems(): + if v: yield (k, v) + else: + yield (key, val) + +class Response_HTTP(Response_Base): + + def __init__(self, code, content_length, response, request): + self.code, self.content_length, \ + self.response, self.request = code, content_length, response, request + self.size = len(self.response) + self.trace = '%s\n%s\n\n%s' % (self.request, '='*80, self.response) + + def compact(self): + return '%s %s' % (self.code, '%d:%d' % (self.size, self.content_length)) + + def __str__(self): + i = self.response.rfind('HTTP/', 0, 5000) + if i == -1: + return '' + else: + j = self.response.find('\n', i) + line = self.response[i:j] + return line.strip() + + def match_clen(self, val): + return match_size(self.content_length, val) + + def match_fgrep(self, val): + return val in self.response + + def match_egrep(self, val): + return re.search(val, self.response) + + available_conditions = Response_Base.available_conditions + available_conditions += ( + ('clen', 'match Content-Length header (N or N-N)'), + ) + +class HTTP_fuzz(TCP_Cache): + '''Fuzz HTTP/HTTPS''' + + usage_hints = [ + """%prog url=http://10.0.0.1/FILE0 0=paths.txt -x ignore:code=404 -x ignore,retry:code=500""", + + """%prog url=http://NET0/manager/html user_pass=FILE1:FILE2 persistent=0 0=10.0.0.0/24 1=logins.txt 2=passwords.txt""" + """ -x ignore:code=401""", + + """%prog url=http://10.0.0.1/phpmyadmin/index.php method=POST""" + """ body='pma_username=COMBO00&pma_password=COMBO01&server=1&lang=en' 0=combos.txt follow=1 accept_cookie=1""" + """ -x ignore:fgrep='Cannot log in to the MySQL server'""", + ] + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target'), + ('scheme', 'scheme [http | https]'), + ('path', 'web path [/]'), + ('query', 'query string'), + ('body', 'body data'), + ('url', 'main url to target (scheme://host(:port)?/path?query)'), + ('header', 'use custom headers, delimited with "\\r\\n"'), + ('method', 'method to use [GET | POST | HEAD | ...]'), + ('user_pass', 'username and password for HTTP authentication (user:pass)'), + ('auth_type', 'type of HTTP authentication [basic | digest | ntlm]'), + ('follow', 'follow any Location redirect [0|1]'), + ('max_follow', 'redirection limit [5]'), + ('accept_cookie', 'save received cookies to issue them in future requests [0|1]'), + ('http_proxy', 'HTTP proxy to use (host:port)'), + ('ssl_cert', 'client SSL certificate file (cert+key in PEM format)'), + ('timeout_tcp', 'seconds to wait for a TCP handshake [10]'), + ('timeout', 'seconds to wait for a HTTP response [20]'), + ('before_urls', 'comma-separated URLs to query before main url'), + ('after_urls', 'comma-separated URLs to query after main url'), + ('max_mem', 'store no more than N bytes of request or response data in memory [-1 (unlimited)]'), + ) + available_options += TCP_Cache.available_options + + Response = Response_HTTP + + def new_tcp(self, host, port): + fp = pycurl.Curl() + fp.setopt(pycurl.SSL_VERIFYPEER, 0) + fp.setopt(pycurl.HEADER, 1) + fp.setopt(pycurl.USERAGENT, 'Mozilla/5.0') + fp.setopt(pycurl.NOSIGNAL, 1) + + return fp, None + + def execute(self, host, port=None, scheme='http', path='/', query='', body='', header='', method='GET', user_pass='', auth_type='basic', + follow='0', max_follow='5', accept_cookie='0', http_proxy='', ssl_cert='', timeout_tcp='10', timeout='20', persistent='1', + before_urls='', after_urls='', max_mem='-1'): + + fp, _ = self.get_tcp(persistent, host=host, port=port) + + fp.setopt(pycurl.FOLLOWLOCATION, int(follow)) + fp.setopt(pycurl.MAXREDIRS, int(max_follow)) + fp.setopt(pycurl.CONNECTTIMEOUT, int(timeout_tcp)) + fp.setopt(pycurl.TIMEOUT, int(timeout)) + fp.setopt(pycurl.PROXY, http_proxy) + + def noop(buf): pass + fp.setopt(pycurl.WRITEFUNCTION, noop) + + def debug_func(t, s): + if t in (pycurl.INFOTYPE_HEADER_IN, pycurl.INFOTYPE_DATA_IN): + if max_mem < 0 or response.tell() < max_mem: + response.write(s) + + elif t in (pycurl.INFOTYPE_HEADER_OUT, pycurl.INFOTYPE_DATA_OUT): + if max_mem < 0 or response.tell() < max_mem: + request.write(s) + + max_mem = int(max_mem) + response, request = StringIO(), StringIO(), + fp.setopt(pycurl.DEBUGFUNCTION, debug_func) + fp.setopt(pycurl.VERBOSE, 1) + + if user_pass: + fp.setopt(pycurl.USERPWD, user_pass) + if auth_type == 'basic': + fp.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_BASIC) + elif auth_type == 'digest': + fp.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_DIGEST) + elif auth_type == 'ntlm': + fp.setopt(pycurl.HTTPAUTH, pycurl.HTTPAUTH_NTLM) + else: + raise NotImplementedError("Incorrect auth_type '%s'" % auth_type) + + if ssl_cert: + fp.setopt(pycurl.SSLCERT, ssl_cert) + + headers = [h.strip('\r') for h in header.split('\n') if h] + fp.setopt(pycurl.HTTPHEADER, headers) # warning: this disables the use of "Expect: 100-continue" header + + if accept_cookie == '1': + fp.setopt(pycurl.COOKIEFILE, '') + # warning: do not pass a Cookie: header into HTTPHEADER if using COOKIEFILE as it will + # produce requests with more than one Cookie: header + # and the server will process only one of them (eg. Apache only reads the last one) + + #if rrange: # commented out because the user may instead pass header='Range: -1024' + # fp.setopt(pycurl.RANGE, rrange) + + def setup_fp(fp, method, url): + if method == 'GET': + fp.setopt(pycurl.HTTPGET, 1) + + elif method == 'POST': + fp.setopt(pycurl.POST, 1) + fp.setopt(pycurl.POSTFIELDS, body) + + elif method == 'HEAD': + fp.setopt(pycurl.NOBODY, 1) + + else: + fp.setopt(pycurl.CUSTOMREQUEST, method) + + fp.setopt(pycurl.URL, url) + + if before_urls: + for before_url in before_urls.split(','): + setup_fp(fp, 'GET', before_url) + fp.perform() + + path = quote(path) + query = urlencode(parse_qsl(query, True)) + body = urlencode(parse_qsl(body, True)) + + url = urlunparse((scheme, '%s:%s' % (host, port or '80'), path, None, query, None)) + setup_fp(fp, method, url) + fp.perform() + + if after_urls: + for after_url in after_urls.split(','): + setup_fp(fp, 'GET', after_url) + fp.perform() + + http_code = fp.getinfo(pycurl.HTTP_CODE) + content_length = fp.getinfo(pycurl.CONTENT_LENGTH_DOWNLOAD) + + return self.Response(http_code, content_length, response.getvalue(), request.getvalue()) + +# }}} + +# VNC {{{ +try: + from Crypto.Cipher import DES +except ImportError: + warnings.append('dev-python/pycrypto (http://www.dlitz.net/software/pycrypto/) is required for VNC.') + +class VNC_Error(Exception): pass +class VNC: + def connect(self, host, port): + self.fp = socket.create_connection((host, port)) + resp = self.fp.recv(1024) # banner + self.version = resp[:11] + + if len(resp) > 12: + raise VNC_Error, self.version + ' ' + resp[20:] + + return self.version + + def login(self, password): + logger.debug("Remote version: %s" % self.version) + major, minor = self.version[6], self.version[10] + + if major == '3' and minor == '8': + proto = 'RFB 003.008\n' + + elif major == '3' and minor == '7': + proto = 'RFB 003.007\n' + + else: + proto = 'RFB 003.003\n' + + logger.debug('Client version: %s' % proto[:-1]) + self.fp.sendall(proto) + + if minor in ('7', '8'): + # send security type + resp = self.fp.recv(1024) + logger.debug("Security types supported: %s" % repr(resp)) + self.fp.sendall('\x02') # always use classic VNC authentication + + # read server challenge + resp = self.fp.recv(1024) + logger.debug('Remote challenge: %s' % repr(resp)) + + if minor == '3': + if len(resp) < 4: + raise VNC_Error, 'Unexpected response size (%d > 4): %s' % (len(resp), repr(resp)) + + code = ord(resp[3]) + if code == 0: + raise VNC_Error, 'Session setup failed: %s' % repr(resp) + + elif code == 1: + raise VNC_Error, 'No authentication required: %s' % repr(resp) + + elif code == 2: + if len(resp) != 20: + raise VNC_Error, 'Unexpected challenge size (unsupported authentication type ?): %s' % repr(resp) + + resp = resp[4:20] + + else: + raise VNC_Error, 'Session setup unknown response' + + pw = (password + '\0' * 8)[:8] # make sure it is 8 chars long, zero padded + key = self.gen_key(pw) + logger.debug('key: %s' % repr(key)) + + des = DES.new(key, DES.MODE_ECB) + enc = des.encrypt(resp) + logger.debug('enc: %s' % repr(enc)) + + self.fp.sendall(enc) + resp = self.fp.recv(1024) + logger.debug('resp: %s' % repr(resp)) + + code = ord(resp[3]) + mesg = resp[8:] + + if code == 1: + return code, mesg or 'Authentication failure' + + elif code == 0: + return mesg or 'OK' + + else: + raise VNC_Error, 'Unknown response: %s (code: %s)' % (repr(resp), code) + + + def gen_key(self, key): + newkey = [] + for ki in range(len(key)): + bsrc = ord(key[ki]) + btgt = 0 + for i in range(8): + if bsrc & (1 << i): + btgt = btgt | (1 << 7-i) + newkey.append(chr(btgt)) + return ''.join(newkey) + + +class VNC_login: + '''Brute-force VNC authentication''' + + usage_hints = ( + """%prog host=10.0.0.1 password=FILE0 0=passwords.txt -x retry:fgrep!='Authentication failure' --max-retries -1 -x quit:code=0""", + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [5900]'), + ('password', 'passwords to test'), + ) + available_actions = () + + Response = Response_Base + + def __init__(self): + self.m = VNC() + def execute(self, host, port=None, password=None): + try: + code, mesg = '0', self.m.connect(host, int(port or 5900)) + + if password is not None: + code, mesg = self.m.login(password) + + except VNC_Error as (e,): + logger.debug('VNC_Error: %s' % e) + code, mesg = '2', e + + return self.Response(code, mesg) + +# }}} + +# DNS {{{ +class HostInfo: + def __init__(self): + self.name = set() + self.ip = set() + self.alias = set() + + def __str__(self): + line = '' + if self.name: + line = ' '.join(self.name) + if self.ip: + if line: line += ' / ' + line += ' '.join(map(str, self.ip)) + if self.alias: + if line: line += ' / ' + line += ' '.join(self.alias) + + return line + +class Controller_DNS(Controller): + hostmap = {} + + # show_final {{{ + def show_final(self): + ''' + 1.2.3.4 ftp.example.com + . www.example.com + . www2.example.com + noip cms.example.com -> www.mistake.com + ''' + ipmap = {} + noips = set() + + ''' + hostmap = { + 'ftp.example.com': {'ip': ['1.2.3.4'], 'alias': []}, + 'www.example.com': {'ip': ['1.2.3.4'], 'alias': ['www2.example.com']}, + 'www.mistake.com': {'ip': [], 'alias': ['cms.example.com']}, ...} + ipmap = {'1.2.3.4': {'name': ['www.example.com', 'ftp.example.com'], 'alias': ('www2.example.com')}} + noips = ['cms.example.com -> www.mistake.com', ...] + ''' + for name, hinfo in self.hostmap.iteritems(): + logger.debug('%s -> %s' % (name, hinfo)) + if not hinfo.ip: # orphan CNAME hostnames (with no IP address) may be still valid virtual hosts + for alias in hinfo.alias: + noips.add('%s -> %s' % (alias, name)) + else: + for ip in hinfo.ip: + if ip not in ipmap: ipmap[ip] = HostInfo() + ipmap[ip].name.add(name) + ipmap[ip].alias.update(hinfo.alias) + + # pretty print + def pprint_info(key, infos): + first = True + for info in infos: + if first: + print('%34s %s' % (info, key)) + first = False + else: + print('%34s %s' % (info, key)) + + print('Hostmap ' + '-'*42) + for ip, hinfo in sorted(ipmap.iteritems()): + pprint_info( ip, hinfo.name) + pprint_info('.', hinfo.alias) + + pprint_info('noip', noips) + + print('Domains ' + '-'*42) + domains = {} + networks = {} + for ip, hinfo in ipmap.iteritems(): + for name in hinfo.name: + i = 1 if name.count('.') > 1 else 0 + d = '.'.join(name.split('.')[i:]) + if d not in domains: domains[d] = 0 + domains[d] += 1 + + for domain, count in sorted(domains.iteritems(), key=lambda a:a[0].split('.')[-1::-1]): + print('%34s %d' % (domain, count)) + + print('Networks ' + '-'*41) + nets = {} + for ip in set(ipmap): + if not ip.version() == 4: + nets[ip] = [ip] + else: + n = ip.make_net('255.255.255.0') + if n not in nets: nets[n] = [] + nets[n].append(ip) + + for net, ips in sorted(nets.iteritems()): + if len(ips) == 1: + print(' '*10 + '%39s' % ips[0]) + else: + print(' '*10 + '%37s.x' % '.'.join(str(net).split('.')[:-1])) + + # }}} + + def push_final(self, resp): + for name, hinfo in resp.hostmap.iteritems(): + if name not in self.hostmap: + self.hostmap[name] = hinfo + else: + self.hostmap[name].ip.update(hinfo.ip) + self.hostmap[name].alias.update(hinfo.alias) + +def generate_tld(): + gtld = [ + 'aero', 'arpa', 'asia', 'biz', 'cat', 'com', 'coop', 'edu', + 'gov', 'info', 'int', 'jobs', 'mil', 'mobi', 'museum', 'name', + 'net', 'org', 'pro', 'tel', 'travel'] + + cctld = [''.join(i) for i in product(*[ascii_lowercase]*2)] + tld = gtld + cctld + return tld, len(tld) + +def generate_srv(): + common = [ + '_gc._tcp', '_kerberos._tcp', '_kerberos._udp', '_ldap._tcp', + '_test._tcp', '_sips._tcp', '_sip._udp', '_sip._tcp', '_aix._tcp', '_aix._udp', + '_finger._tcp', '_ftp._tcp', '_http._tcp', '_nntp._tcp', '_telnet._tcp', + '_whois._tcp', '_h323cs._tcp', '_h323cs._udp', '_h323be._tcp', '_h323be._udp', + '_h323ls._tcp', '_h323ls._udp', '_sipinternal._tcp', '_sipinternaltls._tcp', + '_sip._tls', '_sipfederationtls._tcp', '_jabber._tcp', '_xmpp-server._tcp', '_xmpp-client._tcp', + '_imap.tcp', '_certificates._tcp', '_crls._tcp', '_pgpkeys._tcp', '_pgprevokations._tcp', + '_cmp._tcp', '_svcp._tcp', '_crl._tcp', '_ocsp._tcp', '_PKIXREP._tcp', + '_smtp._tcp', '_hkp._tcp', '_hkps._tcp', '_jabber._udp', '_xmpp-server._udp', + '_xmpp-client._udp', '_jabber-client._tcp', '_jabber-client._udp', + '_adsp._domainkey', '_policy._domainkey', '_domainkey', '_ldap._tcp.dc._msdcs', '_ldap._udp.dc._msdcs'] + + def distro(): + import os + import re + files = ['/usr/share/nmap/nmap-protocols', '/usr/share/nmap/nmap-services', '/etc/protocols', '/etc/services'] + ret = [] + for f in files: + if not os.path.isfile(f): + logger.warn("File '%s' is missing, there will be less records to test" % f) + continue + for line in open(f): + match = re.match(r'([a-zA-Z0-9]+)\s', line) + if not match: continue + for w in re.split(r'[^a-z0-9]', match.group(1).strip().lower()): + ret.extend(['_%s.%s' % (w, i) for i in ('_tcp', '_udp')]) + return ret + + srv = set(common + distro()) + return srv, len(srv) + +try: + from DNS import DnsRequest, DNSError +except ImportError: + warnings.append('pydns (http://pydns.sourceforge.net/) is required for DNS.') + +class DNS_reverse: + '''Reverse lookup subnets''' + + usage_hints = [ + """%prog host=NET0 0=192.168.0.0/24 -x ignore:code=3""", + """%prog host=NET0 0=216.239.32.0-216.239.47.255,8.8.8.0/24 -x ignore:code=3 -x ignore:fgrep!=google.com -x ignore:fgrep=216-239-""", + ] + + available_options = ( + ('host', 'IP addresses to reverse'), + ('server', 'name server to query (directly asking a zone authoritative NS may return more results) [8.8.8.8]'), + ('timeout', 'seconds to wait for a DNS response [10]'), + ) + available_actions = () + + Response = Response_Base + + def execute(self, host, server='8.8.8.8', timeout='10'): + resolver = DnsRequest(qtype='PTR', server=server, timeout=int(timeout)) + + ip = IP(host) + ptr = ip.reverseName() + result = resolver.req(ptr.rstrip('.')) + hostnames = [ans['data'] for ans in result.answers] + + hostmap = {} + for n in hostnames: + if n not in hostmap: hostmap[n] = HostInfo() + hostmap[n].ip.add(ip) + + code = result.header['rcode'] + status = result.header['status'] + mesg = '%s %s' % (status, ', '.join(hostnames)) + + resp = self.Response(code, mesg) + resp.hostmap = hostmap + + return resp + +class DNS_forward: + '''Forward lookup subdomains''' + + usage_hints = [ + """%prog domain=FILE0.google.com 0=names.txt -x ignore:code=3""", + """%prog domain=google.MOD0 0=TLD -x ignore:code=3""", + """%prog domain=MOD0.microsoft.com 0=SRV qtype=SRV -x ignore:code=3""", + ] + + available_options = ( + ('domain', 'domains to lookup'), + ('server', 'name server to query (directly asking the zone authoritative NS may return more results) [8.8.8.8]'), + ('timeout', 'seconds to wait for a DNS response [10]'), + ('qtype', 'comma-separated list of types to query [ANY,A,AAAA]'), + ) + available_actions = () + + available_keys = { + 'TLD': generate_tld, + 'SRV': generate_srv, + } + + Response = Response_Base + + def execute(self, domain, server='8.8.8.8', timeout='10', qtype='ANY,A,AAAA'): + resolver = DnsRequest(server=server, timeout=int(timeout)) + + hostmap = {} + for qt in qtype.split(','): + result = resolver.req(domain, qtype=qt.strip()) + + for r in result.answers + result.additional + result.authority: + t = r['typename'] + n = r['name'] + d = r['data'] + + if t not in ('A', 'AAAA', 'CNAME', 'DNAME', 'SRV'): + continue + + if t == 'SRV': + _, _, _, d = d + + if t in ('CNAME', 'DNAME', 'SRV'): + n, d = d, n + + if n not in hostmap: + hostmap[n] = HostInfo() + + if t == 'A': + hostmap[n].ip.add(IP(d)) + + elif t == 'AAAA': + hostmap[n].ip.add(IP(hexlify(d))) + + elif t in ('CNAME', 'DNAME'): + hostmap[n].alias.add(d) + + elif t == 'SRV': + hostmap[n].alias.add(d) + + code = result.header['rcode'] + status = result.header['status'] + mesg = '%s %s' % (status, ' | '.join('%s / %s' % (k, v) for k, v in hostmap.iteritems())) + + resp = self.Response(code, mesg) + resp.hostmap = hostmap + + return resp + +# }}} + +# SNMP {{{ +try: + from pysnmp.entity.rfc3413.oneliner import cmdgen +except ImportError: + warnings.append('pysnmp (http://pysnmp.sf.net/) is required for SNMP') + +class SNMP_login: + '''Brute-force SNMP v1/2/3 authentication''' + + usage_hints = ( + """%prog host=10.0.0.1 version=2 community=FILE0 1=names.txt -x ignore:mesg='No SNMP response received before timeout'""", + """%prog host=10.0.0.1 version=3 user=FILE0 0=logins.txt -x ignore:mesg=unknownUserName""", + """%prog host=10.0.0.1 version=3 user=myuser auth_key=FILE0 0=passwords.txt -x ignore:mesg=wrongDigest""", + ) + + available_options = ( + ('host', 'hostnames or subnets to target'), + ('port', 'ports to target [161]'), + ('version', 'SNMP version to use [2|3|1]'), + #('security_name', 'SNMP v1/v2 username, for most purposes it can be any arbitrary string [test-agent]'), + ('community', 'SNMPv1/2c community names to test [public]'), + ('user', 'SNMPv3 usernames to test [myuser]'), + ('auth_key', 'SNMPv3 pass-phrases to test [my_password]'), + #('priv_key', 'SNMP v3 secret key for encryption'), # see http://pysnmp.sourceforge.net/docs/4.x/index.html#UsmUserData + #('auth_protocol', ''), + #('priv_protocol', ''), + ('timeout', 'seconds to wait for a response [1]'), + ('retries', 'number of successive request retries [2]'), + ) + available_actions = () + + Response = Response_Base + + def execute(self, host, port=None, version='2', community='public', user='myuser', auth_key='my_password', timeout='1', retries='2'): + if version in ('1', '2'): + security_model = cmdgen.CommunityData('test-agent', community, 0 if version == '1' else 1) + + elif version == '3': + security_model = cmdgen.UsmUserData(user, auth_key) # , priv_key) + if len(auth_key) < 8: + return self.Response('1', 'SNMPv3 requires passphrases to be at least 8 characters long') + + else: + raise NotImplementedError("Incorrect SNMP version '%s'" % version) + + errorIndication, errorStatus, errorIndex, varBinds = cmdgen.CommandGenerator().getCmd( + security_model, + cmdgen.UdpTransportTarget((host, int(port or 161)), timeout=int(timeout), retries=int(retries)), + (1,3,6,1,2,1,1,1,0) + ) + + code = '%d-%d' % (errorStatus, errorIndex) + if not errorIndication: + mesg = '%s' % varBinds + else: + mesg = '%s' % errorIndication + + return self.Response(code, mesg) + +# }}} + +# Unzip {{{ +class Response_Unzip(Response_Base): + def __init__(self, resp): + self.code, self.out, self.err = resp + self.size = len(self.out + self.err) + if '\n' in self.out: + self.mesg = self.out.splitlines()[-1] + else: + self.mesg = self.out + + def __str__(self): + return '%s [%s] %s' % (self.code, self.size, self.mesg) + + def dump(self): + return 'out: %s\n\nerr: %s' % (self.out, self.err) + +class Unzip_pass: + '''Brute-force the password of encrypted ZIP files''' + + usage_hints = [ + """%prog zipfile=path/to/file.zip password=FILE0 0=passwords.txt -x ignore:code!=0""", + ] + + available_options = ( + ('zipfile', 'ZIP files to test'), + ('password', 'passwords to test'), + ) + + available_actions = () + + Response = Response_Unzip + + def execute(self, zipfile, password): + zipfile = os.path.abspath(zipfile) + cmd = ['unzip', '-t', '-q', '-P', password, zipfile] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out = p.stdout.read() + err = p.stderr.read() + code = p.wait() + + return self.Response((code, out, err)) + +# }}} + +# Keystore {{{ +class Response_Keystore(Response_Base): + def __init__(self, resp): + self.code, self.out, self.err = resp + self.size = len(self.out + self.err) + self.mesg = self.out.replace('\n', ' ') + + def __str__(self): + return '%s [%s] %s' % (self.code, self.size, self.mesg) + + def dump(self): + return 'out: %s\nerr: %s' % (self.out, self.err) + +class Keystore_pass: + '''Brute-force the password of Java keystore files''' + + usage_hints = [ + """%prog keystore=path/to/keystore.jks password=FILE0 0=passwords.txt -x ignore:fgrep='password was incorrect'""", + ] + + available_options = ( + ('keystore', 'keystore files to test'), + ('password', 'passwords to test'), + ('storetype', 'type of keystore to test'), + ) + + available_actions = () + + Response = Response_Keystore + + def execute(self, keystore, password, storetype='jks'): + keystore = os.path.abspath(keystore) + cmd = ['keytool', '-list', '-keystore', keystore, '-storepass', password, '-storetype', storetype] + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out = p.stdout.read() + err = p.stderr.read() + code = p.wait() + + return self.Response((code, out, err)) + +# }}} + +# modules {{{ +modules = ( + 'ftp_login', (Controller, FTP_login), + 'ssh_login', (Controller, SSH_login), + 'telnet_login', (Controller, Telnet_login), + 'smtp_vrfy', (Controller, SMTP_vrfy), + 'smtp_rcpt', (Controller, SMTP_rcpt), + 'http_fuzz', (Controller_HTTP, HTTP_fuzz), + 'pop_passd', (Controller, POP_passd), + 'smb_login', (Controller, SMB_login), + 'ldap_login', (Controller, LDAP_login), + 'mssql_login', (Controller, MSSQL_login), + 'oracle_login', (Controller, Oracle_login), + 'mysql_login', (Controller, MySQL_login), + #'rdp_login', '', + 'pgsql_login', (Controller, Pgsql_login), + 'vnc_login', (Controller, VNC_login), + + 'dns_reverse', (Controller_DNS, DNS_reverse), + 'dns_forward', (Controller_DNS, DNS_forward), + 'snmp_login', (Controller, SNMP_login), + + 'unzip_pass', (Controller, Unzip_pass), + 'keystore_pass', (Controller, Keystore_pass), + ) + +# }}} + +# main {{{ +if __name__ == '__main__': + from sys import argv + from os.path import basename + + def show_usage(): + print('''Usage: + $ ./patator.py module --help +or + $ ln -s patator.py module + $ ./module --help + +Available modules: +%s''' % '\n'.join(' + %-13s : %s' % (k, v[1].__doc__) for k, v in modules)) + if warnings: + print('\nWARNING missing dependencies (see README inside)') + for w in warnings: + print('- %s' % w) + exit(2) + + modules = zip(modules[0::2], modules[1::2]) + available = dict((k, v) for k, v in modules) + + name = basename(argv[0]).lower() + if name not in available: + if len(argv) == 1: + show_usage() + name = basename(argv[1]).lower() + if name not in available: + show_usage() + argv = argv[1:] + + ctrl, module = available[name] + powder = ctrl(module, [name] + argv[1:]) + powder.fire() + +# }}} + +# vim: ts=2 sw=2 sts=2 et fdm=marker