diff --git a/DumpHash.py b/DumpHash.py new file mode 100755 index 0000000..016ca15 --- /dev/null +++ b/DumpHash.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# This file is part of Responder, a network take-over set of tools +# created and maintained by Laurent Gaffie. +# email: laurent.gaffie@gmail.com +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import sqlite3 + +def DumpHashToFile(outfile, data): + with open(outfile,"w") as dump: + dump.write(data) + +def DbConnect(): + cursor = sqlite3.connect("./Responder.db") + return cursor + +def GetResponderCompleteNTLMv2Hash(cursor): + res = cursor.execute("SELECT fullhash FROM Responder WHERE type LIKE '%v2%' AND UPPER(user) in (SELECT DISTINCT UPPER(user) FROM Responder)") + Output = "" + for row in res.fetchall(): + Output += '{0}'.format(row[0])+'\n' + return Output + +def GetResponderCompleteNTLMv1Hash(cursor): + res = cursor.execute("SELECT fullhash FROM Responder WHERE type LIKE '%v1%' AND UPPER(user) in (SELECT DISTINCT UPPER(user) FROM Responder)") + Output = "" + for row in res.fetchall(): + Output += '{0}'.format(row[0])+'\n' + return Output + +cursor = DbConnect() +print "Dumping NTLMV2 hashes:" +v2 = GetResponderCompleteNTLMv2Hash(cursor) +DumpHashToFile("DumpNTLMv2.txt", v2) +print v2 +print "\nDumping NTLMv1 hashes:" +v1 = GetResponderCompleteNTLMv1Hash(cursor) +DumpHashToFile("DumpNTLMv1.txt", v1) +print v1 diff --git a/Report.py b/Report.py new file mode 100755 index 0000000..a312a87 --- /dev/null +++ b/Report.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# This file is part of Responder, a network take-over set of tools +# created and maintained by Laurent Gaffie. +# email: laurent.gaffie@gmail.com +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import sqlite3 +import os + +def color(txt, code = 1, modifier = 0): + if txt.startswith('[*]'): + settings.Config.PoisonersLogger.warning(txt) + elif 'Analyze' in txt: + settings.Config.AnalyzeLogger.warning(txt) + + if os.name == 'nt': # No colors for windows... + return txt + return "\033[%d;3%dm%s\033[0m" % (modifier, code, txt) + +def DbConnect(): + cursor = sqlite3.connect("./Responder.db") + return cursor + +def GetResponderData(cursor): + res = cursor.execute("SELECT * FROM Responder") + for row in res.fetchall(): + print('{0} : {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}'.format(row[0], row[1], row[2], row[3], row[4], row[5], row[6], row[7], row[8])) + +def GetResponderUsernamesStatistic(cursor): + res = cursor.execute("SELECT COUNT(DISTINCT UPPER(user)) FROM Responder") + for row in res.fetchall(): + print color('[+] In total {0} unique user accounts were captured.'.format(row[0]), code = 2, modifier = 1) + +def GetResponderUsernames(cursor): + res = cursor.execute("SELECT DISTINCT user FROM Responder") + for row in res.fetchall(): + print('User account: {0}'.format(row[0])) + +def GetResponderUsernamesWithDetails(cursor): + res = cursor.execute("SELECT client, user, module, type, cleartext FROM Responder WHERE UPPER(user) in (SELECT DISTINCT UPPER(user) FROM Responder) ORDER BY client") + for row in res.fetchall(): + print('IP: {0} module: {1}:{3}\nuser account: {2}'.format(row[0], row[2], row[1], row[3])) + + +def GetResponderCompleteHash(cursor): + res = cursor.execute("SELECT fullhash FROM Responder WHERE UPPER(user) in (SELECT DISTINCT UPPER(user) FROM Responder)") + for row in res.fetchall(): + print('{0}'.format(row[0])) + +def GetUniqueLookups(cursor): + res = cursor.execute("SELECT * FROM Poisoned WHERE ForName in (SELECT DISTINCT UPPER(ForName) FROM Poisoned) ORDER BY SentToIp, Poisoner") + for row in res.fetchall(): + print('IP: {0}, Protocol: {1}, Looking for name: {2}'.format(row[2], row[1], row[3])) + + +def GetStatisticUniqueLookups(cursor): + res = cursor.execute("SELECT COUNT(*) FROM Poisoned WHERE ForName in (SELECT DISTINCT UPPER(ForName) FROM Poisoned)") + for row in res.fetchall(): + print color('[+] In total {0} unique queries were poisoned.'.format(row[0]), code = 2, modifier = 1) + + +def SavePoisonersToDb(result): + + for k in [ 'Poisoner', 'SentToIp', 'ForName', 'AnalyzeMode']: + if not k in result: + result[k] = '' + +def SaveToDb(result): + + for k in [ 'module', 'type', 'client', 'hostname', 'user', 'cleartext', 'hash', 'fullhash' ]: + if not k in result: + result[k] = '' + +cursor = DbConnect() +print color("[+] Generating report...", code = 3, modifier = 1) +print color("[+] Unique lookups ordered by IP:", code = 2, modifier = 1) +GetUniqueLookups(cursor) +GetStatisticUniqueLookups(cursor) +print color("\n[+] Extracting captured usernames:", code = 2, modifier = 1) +GetResponderUsernames(cursor) +print color("\n[+] Username details:", code = 2, modifier = 1) +GetResponderUsernamesWithDetails(cursor) +GetResponderUsernamesStatistic(cursor) +#print color("\n[+] Captured hashes:", code = 2, modifier = 1) +#GetResponderCompleteHash(cursor) diff --git a/Responder.py b/Responder.py index 5170f1d..c7c39cf 100755 --- a/Responder.py +++ b/Responder.py @@ -62,6 +62,9 @@ settings.Config.ExpandIPRanges() if settings.Config.AnalyzeMode: print color('[i] Responder is in analyze mode. No NBT-NS, LLMNR, MDNS requests will be poisoned.', 3, 1) +#Create the DB, before we start Responder. +CreateResponderDb() + class ThreadingUDPServer(ThreadingMixIn, UDPServer): def server_bind(self): if OsInterfaceIsSupported(): @@ -231,7 +234,7 @@ def main(): # Load Browser Listener from servers.Browser import Browser - threads.append(Thread(target=serve_thread_udp_broadcast, args=('', 138, Browser,))) + #threads.append(Thread(target=serve_thread_udp_broadcast, args=('', 138, Browser,))) if settings.Config.HTTP_On_Off: from servers.HTTP import HTTP diff --git a/poisoners/LLMNR.py b/poisoners/LLMNR.py index 8e19514..ef375d9 100644 --- a/poisoners/LLMNR.py +++ b/poisoners/LLMNR.py @@ -62,13 +62,24 @@ class LLMNR(BaseRequestHandler): # LLMNR Server class if settings.Config.AnalyzeMode: LineHeader = "[Analyze mode: LLMNR]" print color("%s Request by %s for %s, ignoring" % (LineHeader, self.client_address[0], Name), 2, 1) + SavePoisonersToDb({ + 'Poisoner': 'LLMNR', + 'SentToIp': self.client_address[0], + 'ForName': Name, + 'AnalyzeMode': '1', + }) else: # Poisoning Mode Buffer = LLMNR_Ans(Tid=data[0:2], QuestionName=Name, AnswerName=Name) Buffer.calculate() soc.sendto(str(Buffer), self.client_address) LineHeader = "[*] [LLMNR]" print color("%s Poisoned answer sent to %s for name %s" % (LineHeader, self.client_address[0], Name), 2, 1) - + SavePoisonersToDb({ + 'Poisoner': 'LLMNR', + 'SentToIp': self.client_address[0], + 'ForName': Name, + 'AnalyzeMode': '0', + }) if Finger is not None: print text("[FINGER] OS Version : %s" % color(Finger[0], 3)) print text("[FINGER] Client Version : %s" % color(Finger[1], 3)) diff --git a/poisoners/MDNS.py b/poisoners/MDNS.py index 757ee5b..6fdc537 100644 --- a/poisoners/MDNS.py +++ b/poisoners/MDNS.py @@ -51,6 +51,12 @@ class MDNS(BaseRequestHandler): if settings.Config.AnalyzeMode: # Analyze Mode if Parse_IPV6_Addr(data): print text('[Analyze mode: MDNS] Request by %-15s for %s, ignoring' % (color(self.client_address[0], 3), color(Request_Name, 3))) + SavePoisonersToDb({ + 'Poisoner': 'MDNS', + 'SentToIp': self.client_address[0], + 'ForName': Request_Name, + 'AnalyzeMode': '1', + }) else: # Poisoning Mode if Parse_IPV6_Addr(data): @@ -60,3 +66,9 @@ class MDNS(BaseRequestHandler): soc.sendto(str(Buffer), (MADDR, MPORT)) print color('[*] [MDNS] Poisoned answer sent to %-15s for name %s' % (self.client_address[0], Request_Name), 2, 1) + SavePoisonersToDb({ + 'Poisoner': 'MDNS', + 'SentToIp': self.client_address[0], + 'ForName': Request_Name, + 'AnalyzeMode': '0', + }) diff --git a/poisoners/NBTNS.py b/poisoners/NBTNS.py index a140606..d500a80 100644 --- a/poisoners/NBTNS.py +++ b/poisoners/NBTNS.py @@ -54,6 +54,12 @@ class NBTNS(BaseRequestHandler): if settings.Config.AnalyzeMode: # Analyze Mode LineHeader = "[Analyze mode: NBT-NS]" print color("%s Request by %s for %s, ignoring" % (LineHeader, self.client_address[0], Name), 2, 1) + SavePoisonersToDb({ + 'Poisoner': 'NBT-NS', + 'SentToIp': self.client_address[0], + 'ForName': Name, + 'AnalyzeMode': '1', + }) else: # Poisoning Mode Buffer = NBT_Ans() Buffer.calculate(data) @@ -62,6 +68,13 @@ class NBTNS(BaseRequestHandler): print color("%s Poisoned answer sent to %s for name %s (service: %s)" % (LineHeader, self.client_address[0], Name, NBT_NS_Role(data[43:46])), 2, 1) + SavePoisonersToDb({ + 'Poisoner': 'NBT-NS', + 'SentToIp': self.client_address[0], + 'ForName': Name, + 'AnalyzeMode': '0', + }) + if Finger is not None: print text("[FINGER] OS Version : %s" % color(Finger[0], 3)) print text("[FINGER] Client Version : %s" % color(Finger[1], 3)) diff --git a/servers/HTTP.py b/servers/HTTP.py index 2b75ba7..4a3d3ca 100644 --- a/servers/HTTP.py +++ b/servers/HTTP.py @@ -309,4 +309,4 @@ class HTTP(BaseRequestHandler): except socket.error: pass - \ No newline at end of file + diff --git a/settings.py b/settings.py index e02dff9..c1c82f4 100644 --- a/settings.py +++ b/settings.py @@ -20,7 +20,7 @@ import subprocess from utils import * -__version__ = 'Responder 2.3.3.2' +__version__ = 'Responder 2.3.3.4' class Settings: @@ -229,9 +229,12 @@ class Settings: try: NetworkCard = subprocess.check_output(["ifconfig", "-a"]) - except subprocess.CalledProcessError as ex: - NetworkCard = "Error fetching Network Interfaces:", ex - pass + except: + try: + NetworkCard = subprocess.check_output(["ip", "address", "show"]) + except subprocess.CalledProcessError as ex: + NetworkCard = "Error fetching Network Interfaces:", ex + pass try: DNS = subprocess.check_output(["cat", "/etc/resolv.conf"]) except subprocess.CalledProcessError as ex: @@ -239,9 +242,12 @@ class Settings: pass try: RoutingInfo = subprocess.check_output(["netstat", "-rn"]) - except subprocess.CalledProcessError as ex: - RoutingInfo = "Error fetching Routing information:", ex - pass + except: + try: + RoutingInfo = subprocess.check_output(["ip", "route", "show"]) + except subprocess.CalledProcessError as ex: + RoutingInfo = "Error fetching Routing information:", ex + pass Message = "Current environment is:\nNetwork Config:\n%s\nDNS Settings:\n%s\nRouting info:\n%s\n\n"%(NetworkCard,DNS,RoutingInfo) try: diff --git a/utils.py b/utils.py index dea8463..268c419 100644 --- a/utils.py +++ b/utils.py @@ -146,14 +146,17 @@ def DumpConfig(outfile, data): with open(outfile,"a") as dump: dump.write(data + '\n') -def SaveToDb(result): - # Creating the DB if it doesn't exist +def CreateResponderDb(): if not os.path.exists(settings.Config.DatabaseFile): cursor = sqlite3.connect(settings.Config.DatabaseFile) - cursor.execute('CREATE TABLE responder (timestamp varchar(32), module varchar(16), type varchar(16), client varchar(32), hostname varchar(32), user varchar(32), cleartext varchar(128), hash varchar(512), fullhash varchar(512))') + cursor.execute('CREATE TABLE Poisoned (timestamp TEXT, Poisoner TEXT, SentToIp TEXT, ForName TEXT, AnalyzeMode TEXT)') + cursor.commit() + cursor.execute('CREATE TABLE responder (timestamp TEXT, module TEXT, type TEXT, client TEXT, hostname TEXT, user TEXT, cleartext TEXT, hash TEXT, fullhash TEXT)') cursor.commit() cursor.close() +def SaveToDb(result): + for k in [ 'module', 'type', 'client', 'hostname', 'user', 'cleartext', 'hash', 'fullhash' ]: if not k in result: result[k] = '' @@ -222,6 +225,23 @@ def SaveToDb(result): cursor.commit() cursor.close() +def SavePoisonersToDb(result): + + for k in [ 'Poisoner', 'SentToIp', 'ForName', 'AnalyzeMode' ]: + if not k in result: + result[k] = '' + + cursor = sqlite3.connect(settings.Config.DatabaseFile) + cursor.text_factory = sqlite3.Binary # We add a text factory to support different charsets + res = cursor.execute("SELECT COUNT(*) AS count FROM Poisoned WHERE Poisoner=? AND SentToIp=? AND ForName=? AND AnalyzeMode=?", (result['Poisoner'], result['SentToIp'], result['ForName'], result['AnalyzeMode'])) + (count,) = res.fetchone() + + if not count: + cursor.execute("INSERT INTO Poisoned VALUES(datetime('now'), ?, ?, ?, ?)", (result['Poisoner'], result['SentToIp'], result['ForName'], result['AnalyzeMode'])) + cursor.commit() + + cursor.close() + def Parse_IPV6_Addr(data): if data[len(data)-4:len(data)][1] =="\x1c":