def dataReceived(self, rawData): self.resetTimeout() # TODO: Verifizieren, dass möglichst alle 503-Fälle ("Bad sequence of commands") abgedeckt sind if (rawData.startswith(b'\xff') or rawData.startswith(b'\x04')): # ignore Ctrl+C/D/Z etc. pass else: # binary data like b"\xff\x..." causes trouble when decoding (simply ignore it) try: # decode raw data data = rawData.decode("UTF-8") except Exception as e: # noqa data = "" # get first line line = data[:data.find("\r\n")] # restrict maximum input lenght (doesn't affect mail transmission) line = line[:4094] if not self.state["data"] and not self.state[ "authLOGIN"] and not self.state["authLOGINuser"]: log.request(self.name, self.peerOfAttacker, Config.smtp.port, line, self.username) # moved this block to the top: # if self.state["data"] gets verified after commands, a mail body containing these valid commands executes them if self.state[ "data"]: # doesn't have to start with anything specific self.msg += data if ("\r\n.\r\n" in self.msg): self.msg = self.msg[:self.msg.find( "\r\n.\r\n" ) + 2] # "+2" adds linebreak ("\r\n") to the end of the mail body self.state["data"] = False self.state["msg"] = True response = "250 OK\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "250 OK") self.transport.write(response.encode("UTF-8")) elif re.match('^RSET( .*)?$', line, re.IGNORECASE): # TODO: store data received from attacker somewhere else self.state = { "connected": False, "hello": False, "auth": False, "mailfrom": False, "mailto": False, "data": False, "msg": False, "auth": False, "authLOGIN": False, "authLOGINuser": False } self.msg = "" self.mailFrom = "" self.mailTo = "" self.username = "" self.password = "" response = "250 OK\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "250 OK") self.transport.write(response.encode("UTF-8")) elif re.match('^NOOP( .*)?$', line, re.IGNORECASE): response = "250 OK\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "250 OK") self.transport.write(response.encode("UTF-8")) elif re.match('^QUIT( .*)?$', line, re.IGNORECASE): # make sure QUIT doesn't have parameters (unimportant for correct functioning but good for concealment) if (line == "QUIT"): self.state["connected"] = False response = "221 Service closing transmission channel\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "221 OK (closing)") self.transport.write(response.encode("UTF-8")) # close connection self.transport.loseConnection() else: response = "501 Syntax error in parameters or arguments\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "501 (syntax)") self.transport.write(response.encode("UTF-8")) elif self.state[ "authLOGIN"]: # doesn't start with anything recognisable # state after client chose LOGIN authentication method # TODO: missing protection against malformed inputs self.username = base64.b64decode(line).decode("utf-8") self.state["authLOGIN"] = False self.state["authLOGINuser"] = True response = "334 UGFzc3dvcmQ6\r\n" # "334 Password:"******"UTF-8")) elif self.state[ "authLOGINuser"]: # doesn't start with anything recognisable # state after client sent username for LOGIN authentication method # TODO: missing protection against malformed inputs self.password = base64.b64decode(line).decode("utf-8") self.state["authLOGINuser"] = False # TODO: implement honeytokendb check if False: log.login(self.name, self.peerOfAttacker, Config.smtp.port, True, self.username, self.password, "") self.state["auth"] = True self.setTimeout(self.timeoutPostAuth) response = "235 OK\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "235 OK") else: log.login(self.name, self.peerOfAttacker, Config.smtp.port, False, self.username, self.password, "") response = "535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (credentials)") self.transport.write(response.encode("UTF-8")) elif re.match('^HELP( .*)?$', line, re.IGNORECASE): # don't react to arguments (exactly like e.g. smtp.outlook.com) response = "214 This server supports the following commands:\r\n214 HELO EHLO RCPT DATA MAIL QUIT HELP AUTH VRFY RSET NOOP\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "214 OK") self.transport.write(response.encode("UTF-8")) elif re.match('^(HELO|EHLO)( .*)?$', line, re.IGNORECASE): self.state["hello"] = True if (self.AuthMethods != ""): response = "250 AUTH" + self.AuthMethods + "\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "250 OK") else: # authentication completed without authentication self.state["auth"] = True self.setTimeout(self.timeoutPostAuth) response = "250 OK\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "250 OK") self.transport.write(response.encode("UTF-8")) elif re.match("^AUTH( .*)?$", line, re.IGNORECASE): if (line == "AUTH"): response = "501 Syntax error in parameters or arguments\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "501 (syntax") else: if self.state["hello"]: if re.match("^AUTH PLAIN \S*$", line, re.IGNORECASE): if ("PLAIN" in self.AuthMethods): # b64decode everything from 12th char # TODO: missing protection against malformed inputs credentials = base64.b64decode( line[11:]).decode("utf-8")[1:] self.username = credentials[:credentials. find("\x00")] self.password = credentials[credentials. find("\x00") + 1:] # TODO: implement honeytokendb check if False: self.state["auth"] = True self.setTimeout(self.timeoutPostAuth) response = "235 OK\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "235 OK") else: response = "535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (credentials)") else: response = "535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (unsupported)") elif line == "AUTH LOGIN": if ("LOGIN" in self.AuthMethods): self.state["authLOGIN"] = True response = "334 VXNlcm5hbWU6\r\n" # "334 Username:"******"535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (unsupported)") elif line == "AUTH CRAM-MD5": # TODO: implement CRAM-MD5 or disable this code path if ("CRAM-MD5" in self.AuthMethods): print("CRAM-MD5 not yet implemented") response = "504 Command parameter not implemented\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "504 (not implemented)") self.transport.write(response.encode("UTF-8")) self.transport.loseConnection() else: response = "535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (unsupported)") elif line == "AUTH SCRAM-SHA-1": # TODO: implement SCRAM-SHA-1 or disable this code path if "SCRAM-SHA-1" in self.AuthMethods: print("SCRAM-SHA-1 not yet implemented") response = "504 Command parameter not implemented\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "504 (not implemented)") self.transport.write(response.encode("UTF-8")) self.transport.loseConnection() else: response = "535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (unsupported)") else: response = "501 Syntax error in parameters or arguments\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "501 (syntax)") else: response = "503 Bad sequence of commands\r\n" # "Send hello first" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "503 (sequence)") self.transport.write(response.encode("UTF-8")) elif re.match("^MAIL FROM:(.*)$", line, re.IGNORECASE): # tolerate additional whitespace if re.match( "^MAIL FROM:[ ]?<(.*)>$", line, re.IGNORECASE ): # use "<(.*@.*\..*?)>" to check basic adress syntax if self.state["auth"]: self.state["mailfrom"] = True self.mailFrom = re.match( "^MAIL FROM:[ ]?<(.*)>$", line, re.IGNORECASE ).groups( )[0] # use "<(.*@.*\..*?)>" to check basic adress syntax response = "250 OK\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "250 OK") elif self.state["hello"]: response = "535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (credentials)") else: response = "503 Bad sequence of commands\r\n" # "Send hello first" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "503 (sequence)") else: # no email adress given etc. response = "501 Syntax error in parameters or arguments\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "501 (syntax)") self.transport.write(response.encode("UTF-8")) elif re.match("^RCPT TO:(.*)$", line, re.IGNORECASE): # tolerate additional whitespace if re.match( "^RCPT TO:[ ]?<(.*)>$", line, re.IGNORECASE ): # use "<(.*@.*\..*?)>" to check basic adress syntax if self.state["mailfrom"]: self.state["mailto"] = True self.mailTo = re.match( "^RCPT TO:[ ]?<(.*)>$", line, re.IGNORECASE ).groups( )[0] # use "<(.*@.*\..*?)>" to check basic adress syntax # use "<(.*@.*\..*?)>" to check basic adress syntax response = "250 OK\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "250 OK") elif self.state["hello"]: response = "535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (credentials)") else: response = "503 Bad sequence of commands\r\n" # "Send hello first" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "503 (sequence)") else: # no email adress given etc. response = "501 Syntax error in parameters or arguments\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "501 (syntax)") self.transport.write(response.encode("UTF-8")) elif re.match("^DATA( .*)?$", line, re.IGNORECASE): if line == "DATA": if self.state["mailto"]: self.state["data"] = True response = "354 Start mail input\r\n" elif self.state["auth"]: response = "503 Bad sequence of commands\r\n" # "Send hello first" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "503 (sequence)") elif self.state["hello"]: response = "535 SMTP Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "535 (credentials)") else: response = "503 Bad sequence of commands\r\n" # "Send hello first" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "503 (sequence)") else: response = "501 Syntax error in parameters or arguments\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "501 (syntax)") self.transport.write(response.encode("UTF-8")) elif re.match("^VRFY( .*)?$", line, re.IGNORECASE): response = "252 Cannot VRFY user\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "252 OK") self.transport.write(response.encode("UTF-8")) elif re.match("^SIZE( .*)?$", line, re.IGNORECASE): response = "502 Command not implemented\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "502 (not implemented)") self.transport.write(response.encode("UTF-8")) else: response = "500 Unrecognized command \'" + line + "\'\r\n" log.response(self.name, self.peerOfAttacker, Config.smtp.port, "", self.username, "500 (command)") self.transport.write(response.encode("UTF-8"))
def dataReceived(self, rawData): self.resetTimeout() local = self.transport.getHost() remote = self.transport.getPeer() # problem: at least Thunderbird pushes sometimes two commands so quickly they are received by twisted as one line # solution: put received data in buffer and fetch single lines from there # TODO: Verifizieren, dass möglichst alle 503-Fälle ("Bad sequence of commands") abgedeckt sind if (rawData.startswith(b'\xff') or rawData.startswith(b'\x04')): # ignore Ctrl+C/D/Z etc. pass else: # binary data like b"\xff\x..." causes trouble when decoding (simply ignore it) try: # decode raw data and add it to buffer self.dataBuffer += rawData.decode("UTF-8") except Exception as e: # noqa pass # get first line from buffer if ("\r\n" in self.dataBuffer): line = self.dataBuffer[:self.dataBuffer.find("\r\n")] self.dataBuffer = self.dataBuffer[self.dataBuffer.find("\r\n" ) + 2:] else: print( "IMAPService: Warning: Client command didn't end with CRLF!" ) line = self.dataBuffer self.dataBuffer = "" if (not self.state["appendCleanUp"] and not self.state["appendData"]): log.request(self.name, remote.host, remote.port, local.host, local.ip, line, self.username) # ignore one (maybe optional) empty line after append command if (self.state["appendCleanUp"]): if (line != ""): self.dataBuffer = line + "\r\n" + self.dataBuffer self.state["appendCleanUp"] = False elif (self.state["appendData"] ): # wait for another part of the mail # reunify first line and remaining buffer self.dataBuffer = line + "\r\n" + self.dataBuffer # buffer is bigger than remaining announced transmission if (len(self.dataBuffer) >= self.appendLength): self.appendBuffer += self.dataBuffer[:self.appendLength] self.dataBuffer = self.dataBuffer[self.appendLength:] self.appendLength = 0 self.state["appendData"] = False self.state["appendCleanUp"] = True # add uploaded mail to storage mail = self.appendBuffer self.saveEmail(self.appendMailbox, self.appendFlaglist, mail) response = self.appendTag + " OK APPEND completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") self.transport.write(response.encode("UTF-8")) else: # wait for another part of the mail self.appendBuffer += self.dataBuffer self.appendLength -= len(self.dataBuffer) self.dataBuffer = "" elif (len(line) > self.maxCommandLength): response = "* BAD Command line too long\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (too long)") self.transport.write(response.encode("UTF-8")) elif (len(line) == 0): response = "* BAD Empty command line\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (too short)") self.transport.write(response.encode("UTF-8")) elif (re.match("^[A-Za-z\d]+ .+$", line, re.IGNORECASE)): # provides tag value for following answers in all cases tag = re.match("^(?P<tag>[A-Za-z\d]+) .+$", line, re.IGNORECASE).group("tag") # "command-any" (Valid in all states) if (re.match("^" + tag + " CAPABILITY$", line, re.IGNORECASE)): response = "* CAPABILITY IMAP4rev1 LOGIN LOGOUT NOOP LIST LSUB UID SELECT\r\n" + tag + " OK CAPABILITY completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") self.transport.write(response.encode("UTF-8")) elif (re.match("^" + tag + " LOGOUT$", line, re.IGNORECASE)): self.state["connected"] = False response = "* BYE IMAP4rev1 Server logging out\r\n" + tag + " OK LOGOUT completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") self.transport.write(response.encode("UTF-8")) # close connection self.transport.loseConnection() elif (re.match("^" + tag + " NOOP$", line, re.IGNORECASE)): response = tag + " OK NOOP completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") self.transport.write(response.encode("UTF-8")) elif (re.match( "^" + tag + " ((CAPABILITY)|(LOGOUT)|(NOOP)) .*$", line, re.IGNORECASE)): # unified error message for several commands response = "* BAD Command argument error\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (syntax)") self.transport.write(response.encode("UTF-8")) # "command-nonauth" (Valid only when in Not Authenticated state) elif (re.match( "^" + tag + " LOGIN (?P<userid>\S+) (?P<password>\S+)$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.NotAuth): arguments = re.match( "^" + tag + " LOGIN (?P<userid>\S+) (?P<password>\S+)$", line, re.IGNORECASE) self.username = self.stripPadding( arguments.group("userid")) self.password = self.stripPadding( arguments.group("password")) if (True): log.login(self.name, remote.host, local.port, True, self.username, self.password, "") self.setTimeout(self.timeoutPostAuth) self.stateRFC = IMAPState.Auth response = tag + " OK LOGIN completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") else: log.login(self.name, remote.host, local.port, False, self.username, self.password, "") response = tag + " NO Invalid credentials\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "NO (credentials") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match("^" + tag + " LOGIN .*$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.NotAuth): response = "* BAD Command argument error\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (syntax)") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence") self.transport.write(response.encode("UTF-8")) # TODO: implement AUTHENTICATE command (provides SASL auth (GSSAPI etc.)) # "command-auth" (Valid only in Authenticated or Selected state) elif (re.match( "^" + tag + " LIST (?P<mailbox>\S+) (?P<listmailbox>\S+)$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Auth or self.stateRFC == IMAPState.Selected): arguments = re.match( "^" + tag + " LIST (?P<mailbox>\S+) (?P<listmailbox>\S+)$", line, re.IGNORECASE) reference = self.stripPadding( arguments.group("mailbox")) mailboxName = self.stripPadding( arguments.group("listmailbox")) response = "" # doesn't really support the wildcard feature, just distinguishes between "everything" and "one specific mailbox" if (mailboxName in ["*", "%"]): for i in self.mailboxes: response += "* LIST (\\HasNoChildren) \".\" \"" + i + "\"\r\n" response += tag + " OK LIST Completed\r\n" else: if (mailboxName in self.mailboxes): response += "* LIST (\\HasNoChildren) \".\" \"" + mailboxName + "\"\r\n" response += tag + " OK LIST Completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match( "^" + tag + " LSUB (?P<mailbox>\S+) (?P<listmailbox>\S+)$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Auth or self.stateRFC == IMAPState.Selected): arguments = re.match( "^" + tag + " LSUB (?P<mailbox>\S+) (?P<listmailbox>\S+)$", line, re.IGNORECASE) # XXX: why is this unused? reference = self.stripPadding( arguments.group("mailbox")) mailboxName = self.stripPadding( arguments.group("listmailbox")) response = "" # doesn't really support the wildcard feature, just distinguishes between "everything" and "one specific mailbox" if (mailboxName in ["*", "%"]): for i in self.mailboxes: response += "* LSUB (\\HasNoChildren) \".\" \"" + i + "\"\r\n" response += tag + " OK LSUB Completed\r\n" else: if (mailboxName in self.mailboxes): response += "* LSUB (\\HasNoChildren) \".\" \"" + mailboxName + "\"\r\n" response += tag + " OK LSUB Completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match("^" + tag + " SELECT (?P<mailbox>\S+)$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Auth or self.stateRFC == IMAPState.Selected): arguments = re.match( "^" + tag + " SELECT (?P<mailbox>\S+)$", line, re.IGNORECASE) mailbox = self.stripPadding(arguments.group("mailbox")) if (mailbox in self.mailboxes): self.stateRFC = IMAPState.Selected self.selectedMailbox = mailbox response = "* " + str( self.mailboxes[mailbox] ) + " EXISTS\r\n* 0 RECENT\r\n" + tag + " OK SELECT completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") else: response = tag + " NO Mailbox not found\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "NO (mailbox not found)") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match( "^" + tag + " STATUS (?P<mailbox>\S+) \((?P<statusatt>\S+( \S+)*)\)$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Auth or self.stateRFC == IMAPState.Selected): arguments = re.match( "^" + tag + " STATUS (?P<mailbox>\S+) \((?P<statusatt>\S+( \S+)*)\)$", line, re.IGNORECASE) mailbox = self.stripPadding(arguments.group("mailbox")) statusatt = arguments.group("statusatt") if (mailbox in self.mailboxes): statusatt = statusatt.replace( "MESSAGES", "MESSAGES " + str(self.mailboxes[mailbox])) if ("RECENT" in statusatt): c = 0 for mail in self.emails: if (mail[0] == self.selectedMailbox ): # just mails in this mailbox if ("\\Recent" in mail[1]): c += 1 statusatt = statusatt.replace( "RECENT", "RECENT " + str(c)) if ("UNSEEN" in statusatt): c = 0 for mail in self.emails: if (mail[0] == self.selectedMailbox ): # just mails in this mailbox if "\\Seen" not in mail[1]: c += 1 statusatt = statusatt.replace( "UNSEEN", "UNSEEN " + str(c)) # TODO: research possible values (nonsense values at the moment) statusatt = statusatt.replace( "UIDNEXT", "UIDNEXT " + str(1)) statusatt = statusatt.replace( "UIDVALIDITY", "UIDVALIDITY " + str(0)) response = "* STATUS " + mailbox + " (" + statusatt + ")\r\n" response += tag + " OK STATUS completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") else: response = "* NO STATUS failure: no status for that name\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "NO (mailbox not found)") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match( "^" + tag + " APPEND (?P<mailbox>\S+) \((?P<flaglist>\S+( \S+)*)?\)(?P<datetime> .+)? \{(?P<literal>\d+)\}$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Auth or self.stateRFC == IMAPState.Selected): arguments = re.match( "^" + tag + " APPEND (?P<mailbox>\S+) \((?P<flaglist>\S+( \S+)*)?\)(?P<datetime> .+)? \{(?P<literal>\d+)\}$", line, re.IGNORECASE) mailbox = self.stripPadding(arguments.group("mailbox")) flaglist = arguments.group("flaglist") # XXX: Why is this unused?? datetime = arguments.group("datetime") literal = arguments.group("literal") # grant up to about 20 * 1 MiB storage (memory-DOS protection) if (int(literal) <= 1048576 and len(self.emails) < 20): self.state["appendData"] = True self.appendTag = tag self.appendMailbox = mailbox self.appendFlaglist = flaglist self.appendBuffer = "" self.appendLength = int(literal) response = "+ Ready for literal data\r\n" self.transport.write(response.encode("UTF-8")) else: response = "* NO APPEND error: can't append to that mailbox, error in flags or date/time or message text\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "NO (storage limitation)") self.transport.write(response.encode("UTF-8")) else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match( "^" + tag + " (LIST|LSUB|SELECT|STATUS|APPEND) .*$", line, re.IGNORECASE)): # unified error message for several commands if (self.stateRFC == IMAPState.Auth or self.stateRFC == IMAPState.Selected): response = "* BAD Command argument error\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (syntax)") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) # "command-select" (Valid only when in Selected state) elif (re.match("^" + tag + " CLOSE$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Selected): # TODO: delete accordingly flagged mails self.stateRFC = IMAPState.Auth response = tag + " OK CLOSE completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match("^" + tag + "CLOSE .*$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Selected): response = "* BAD Command argument error\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (syntax)") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match( "^" + tag + " UID FETCH (?P<sequenceset>\S+) (?P<entries>.+)$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Selected): arguments = re.match( "^" + tag + " UID FETCH (?P<sequenceset>\S+) (?P<entries>.+)$", line, re.IGNORECASE) sequenceset = arguments.group("sequenceset") entries = arguments.group("entries") # resolve fetch macros (according to RFC 3501) for parsing macros = { "ALL": "(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE)", "FAST": "(FLAGS INTERNALDATE RFC822.SIZE)", "FULL": "(FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY)" } if (entries in macros): entries = macros[entries] response = "" c = 1 # sequence number of mail in mailbox (use position in mail array as ascending UID) for i, mail in enumerate(self.emails): if (mail[0] == self.selectedMailbox ): # just mails in this mailbox if ( sequenceset == "1:*" or self.sequenceNumberInSet( sequenceset, i) ): # important performance improvement for default parameter if (entries in ["FLAGS", "(FLAGS)"]): response += "* " + str( c) + " FETCH (UID " + str( i) + " FLAGS (" + (" ".join( mail[1])) + "))\r\n" else: response += "* " + str(c) + " FETCH " # TODO: best solution: parse fetch-att parameters and select accordingly which attributes to send entriesProcessed = entries.upper()[:-1] entriesProcessed = entriesProcessed.replace( "UID", "UID " + str(i)) entriesProcessed = entriesProcessed.replace( "RFC822.SIZE", "RFC822.SIZE " + str(len(mail[3]) + len(mail[4]))) entriesProcessed = entriesProcessed.replace( "FLAGS", "FLAGS (" + (" ".join(mail[1])) + ")") entriesProcessed = entriesProcessed.replace( "BODY.PEEK", "BODY") buffer = "" if ("BODY" in entriesProcessed or "ENVELOPE" in entriesProcessed or "TEXT" in entriesProcessed): buffer += mail[3] + "\r\n" + mail[4] response += entriesProcessed + " {" + str( len(buffer)) + "}\r\n" + buffer response += "\r\n)\r\n" c += 1 response += tag + " OK FETCH complete\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) elif (re.match("^" + tag + " UID FETCH .+$", line, re.IGNORECASE)): response = "* BAD Command argument error\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (syntax)") self.transport.write(response.encode("UTF-8")) # temporary workaround to avoid the client to throw an error after uploading the mail elif (re.match("^" + tag + " UID SEARCH *$", line, re.IGNORECASE)): if (self.stateRFC == IMAPState.Selected): response = "* SEARCH\r\n" + tag + " OK SEARCH completed\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "OK") else: response = tag + " BAD Command received in invalid state.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (sequence)") self.transport.write(response.encode("UTF-8")) else: response = "* BAD Command unknown\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (command)") self.transport.write(response.encode("UTF-8")) else: response = "* BAD Syntax error\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "BAD (syntax)") self.transport.write(response.encode("UTF-8"))
def dataReceived(self, data): data = data.decode('utf-8') self.requestType = data.split(' ', 1)[0] if self.requestType == "GET": self.page = data[data.find("GET ") + 4:data.find(" HTTP/1.1")] elif self.requestType == "POST": self.page = data[data.find("POST ") + 5:data.find(" HTTP/1.1")] pageNotFound = True for serviceLink in HTTPService.supportedSites: if self.page == serviceLink: pageNotFound = False self.attackingSite = str( self.path / HTTPService.html_dictionary[serviceLink][0]) if len(HTTPService.html_dictionary[serviceLink]) > 1: self.loginSuccessfulSite = str( self.path / HTTPService.html_dictionary[serviceLink][1]) else: self.loginSuccessfulSite = str( self.path / HTTPService.html_dictionary['404'][0]) self.short = serviceLink break if pageNotFound: self.notFoundSite = str(self.path / HTTPService.html_dictionary['404'][0]) # Handle GETs if self.requestType == "GET" and ( '.gif' in self.page or '.png' in self.page or '/dashboard_files/' in self.page or '.jpg' in self.page or '.woff' in self.page or '.ttf' in self.page or '.svg' in self.page): message = HTTPService.notFoundStatus + "\n" for k in HTTPService.responseHeadersNotFound.keys(): message = message + k + ": " + HTTPService.responseHeadersNotFound[ k] + "\n" self.transport.write(message.encode('UTF-8')) self.transport.loseConnection() elif self.requestType == "GET" and self.page in HTTPService.supportedSites: log.request("HTTP", self.peerOfAttacker, HTTPService.port, self.page, "", "GET") message = HTTPService.okStatus + "\n" for k in HTTPService.responseHeadersOkStatus.keys(): message = message + k + ": " + HTTPService.responseHeadersOkStatus[ k] + "\n" with open(self.attackingSite, encoding='utf8') as file: message = message + "\n" + file.read() self.transport.write(message.encode('UTF-8')) if self.page in HTTPService.supportedSites: log.response("HTTP", self.peerOfAttacker, HTTPService.port, self.page, "", "200 OK") self.transport.loseConnection() # Handle POSTs elif (self.requestType == "POST" and self.page in HTTPService.supportedSites): self.page = data[data.find("POST ") + 5:data.find(" HTTP/1.1")] login_string = "" password_string = "" login_index = data.find("log=") + 4 if login_index == 3: login_string = "fritz.box" else: login_string = data[login_index:data.find("&")] password_index = data.find("pwd=") + 4 if data[password_index:data.find("&")] != -1: password_string = data[password_index:len(data)] else: password_string = data[password_index:data.find("&")] log.request("HTTP", self.peerOfAttacker, HTTPService.port, self.page, login_string, "POST") result = HTTPService.htdb.requestAvatarId( HTTPAvatar(login_string, password_string)) if isinstance(result, Deferred): if isinstance(result.result, failure.Failure): # Failure result.addErrback(self.errorBack) log.response("HTTP", self.peerOfAttacker, HTTPService.port, "", login_string, "403 FORBIDDEN") log.login( "HTTP", self.peerOfAttacker, HTTPService.port, False, login_string, password_string, str( HTTPService.htdb.getActual(login_string, password_string))) else: # Success message = HTTPService.okStatus + "\n" for k in HTTPService.responseHeadersOkStatus.keys(): message = message + k + ": " + HTTPService.responseHeadersOkStatus[ k] + "\n" with open(self.loginSuccessfulSite, encoding='utf8') as file: message = message + "\n" + file.read() self.transport.write(message.encode('UTF-8')) self.page = "wp-admin_content.html" log.response("HTTP", self.peerOfAttacker, HTTPService.port, self.page, login_string, "200 OK") log.login( "HTTP", self.peerOfAttacker, HTTPService.port, True, login_string, password_string, str( HTTPService.htdb.getActual(login_string, password_string))) self.transport.loseConnection() else: message = HTTPService.notFoundStatus + "\n" for k in HTTPService.responseHeadersNotFound.keys(): message = message + k + ": " + HTTPService.responseHeadersNotFound[ k] + "\n" with open(self.notFoundSite, encoding='utf8') as file: message = message + "\n" + file.read() self.transport.write(message.encode('UTF-8')) log.request("HTTP", self.peerOfAttacker, HTTPService.port, self.page, "", "GET") self.page = "404_login.html" log.response("HTTP", self.peerOfAttacker, HTTPService.port, self.page, "", "404 Not Found") self.transport.loseConnection()
def dataReceived(self, rawData): self.resetTimeout() local = self.transport.getHost() remote = self.transport.getPeer() # TODO: Verifizieren, dass möglichst alle 503-Fälle ("Bad sequence of commands") abgedeckt sind if(rawData.startswith(b'\xff') or rawData.startswith(b'\x04')): # ignore Ctrl+C/D/Z etc. pass else: # binary data like b"\xff\x..." causes trouble when decoding (simply ignore it) try: # decode raw data data = rawData.decode("UTF-8") except Exception as e: # noqa data = "" # get first line line = data[:data.find("\r\n")] # restrict maximum input lenght (doesn't affect mail transmission) line = line[:4094] log.request(self.name, remote.host, remote.port, local.host, local.port, line, self.username) if(re.match("^USER( \S+)?$", line, re.IGNORECASE)): if(re.match("^USER \S+$", line, re.IGNORECASE)): arguments = re.match("^USER (?P<username>.+)$", line, re.IGNORECASE) self.username = arguments.group("username") self.state["user"] = True response = "+OK Please enter password\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: response = "-ERR Syntax error in parameters or arguments\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR") self.transport.write(response.encode("UTF-8")) elif(re.match("^PASS( .*)?$", line, re.IGNORECASE)): if(re.match("^PASS \S+$", line, re.IGNORECASE)): if(self.state["user"]): arguments = re.match("^PASS (?P<password>\S+)$", line, re.IGNORECASE) self.password = arguments.group("password") # TODO: implement honeytokendb check if (True): log.login(self.name, remote.host, local.port, True, self.username, self.password, "") self.state["auth"] = True self.setTimeout(self.timeoutPostAuth) response = "+OK mailbox locked and ready\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: log.login(self.name, remote.host, local.port, False, self.username, self.password, "") response = "-ERR POP3 Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (credentials)") else: response = "-ERR Bad sequence of commands\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (sequence of commands)") else: response = "-ERR Syntax error in parameters or arguments\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (syntax)") # print("POP3Service sent: \""+response+"\"") self.transport.write(response.encode("UTF-8")) elif(re.match("^STAT( .*)?$", line, re.IGNORECASE)): if(line == "STAT"): if (self.state["auth"]): response = "+OK "+str(self.mailcount)+" "+str(self.mailsize)+"\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: response = "-ERR POP3 Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (credentials)") else: response = "-ERR Syntax error in parameters or arguments\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (syntax)") self.transport.write(response.encode("UTF-8")) elif(re.match("^LIST( .*)?$", line, re.IGNORECASE)): if(re.match("^LIST( \d+)?$", line, re.IGNORECASE)): if (self.state["auth"]): if(line == "LIST"): response = "+OK mailbox has "+str(self.mailcount)+" messages ("+str(self.mailsize)+" octets)\r\n" for i, mail in enumerate(self.emails): response += str(i+1)+" "+str(len(mail[1]))+"\r\n" response += ".\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: # n-th mail requested n = int(re.match("^LIST( (\d+))$", line, re.IGNORECASE).groups()[1]) if (0 <= (n-1) < len(self.emails)): response = "+OK "+str(n)+" "+str(len(self.emails[n-1][1]))+"\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: response = "-ERR Email "+str(n)+" not available\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (mail not found)") self.transport.write(response.encode("UTF-8")) else: response = "-ERR POP3 Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (credentials)") self.transport.write(response.encode("UTF-8")) else: response = "-ERR Syntax error in parameters or arguments\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (syntax)") self.transport.write(response.encode("UTF-8")) elif(re.match("^UIDL( .*)?$", line, re.IGNORECASE)): if(re.match("^UIDL( \d+)?$", line, re.IGNORECASE)): if (self.state["auth"]): if(line == "UIDL"): response = "+OK\r\n" for i, mail in enumerate(self.emails): response += str(i+1)+" "+mail[2]+"\r\n" response += ".\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: # n-th mail requested n = int(re.match("^UIDL( (\d+))$", line).groups()[1]) if (0 <= (n-1) < len(self.emails)): response = "+OK "+str(n)+" "+hashlib.md5(self.emails[n-1][1].encode("UTF-8")).hexdigest()+"\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: response = "-ERR Email "+str(n)+" not available\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (mail not found)") self.transport.write(response.encode("UTF-8")) else: response = "-ERR POP3 Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (credentials)") self.transport.write(response.encode("UTF-8")) else: response = "-ERR Syntax error in parameters or arguments\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (syntax)") self.transport.write(response.encode("UTF-8")) elif(re.match("^RETR( .*)?$", line, re.IGNORECASE)): if(re.match("^RETR( \d+)$", line, re.IGNORECASE)): if (self.state["auth"]): # n-th mail requested n = int(re.match("^RETR( (\d+))$", line, re.IGNORECASE).groups()[1]) if (0 <= (n-1) < len(self.emails)): response = "+OK "+str(len(self.emails[n-1][1]))+" octets\r\n"+self.emails[n-1][0]+"\r\n"+self.emails[n-1][1]+"\r\n.\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: response = "-ERR message "+str(n)+" not available\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (mail not found)") self.transport.write(response.encode("UTF-8")) else: response = "-ERR POP3 Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (credentials)") self.transport.write(response.encode("UTF-8")) else: response = "-ERR Syntax error in parameters or arguments\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (syntax)") self.transport.write(response.encode("UTF-8")) elif(re.match("^DELE( .*)?$", line, re.IGNORECASE)): if(re.match("^DELE( \d+)?$", line, re.IGNORECASE)): if (self.state["auth"]): # n-th mail requested n = int(re.match("^DELE( (\d+))$", line, re.IGNORECASE).groups()[1]) if (0 <= (n-1) < len(self.emails)): response = "+OK message "+str(n)+" deleted\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") else: response = "-ERR message "+str(n)+" not available\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (mail not found)") self.transport.write(response.encode("UTF-8")) else: response = "-ERR POP3 Authentication unsuccessful/Bad username or password\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (credentials)") self.transport.write(response.encode("UTF-8")) else: response = "-ERR Syntax error in parameters or arguments\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (syntax)") self.transport.write(response.encode("UTF-8")) elif(re.match("^QUIT( .*)?$", line, re.IGNORECASE)): # make sure QUIT doesn't have parameters (unimportant for correct functioning but good for concealment) if (line == "QUIT"): self.state["connected"] = False response = "+OK bye\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "+OK") self.transport.write(response.encode("UTF-8")) # close connection self.transport.loseConnection() else: response = "-ERR Syntax error in parameters or arguments\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (syntax") self.transport.write(response.encode("UTF-8")) else: response = "-ERR Unrecognized command \'"+line+"\'\r\n" log.response(self.name, remote.host, remote.port, local.host, local.port, "", self.username, "-ERR (command)") self.transport.write(response.encode("UTF-8"))