Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom SMTP EHLO hostname #682

Merged
merged 7 commits into from
Dec 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions sslyze/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import sys
from datetime import UTC, datetime
from datetime import datetime, timezone
from typing import Optional, TextIO

from sslyze.cli.console_output import ObserverToGenerateConsoleOutput
Expand All @@ -23,7 +23,7 @@

def main() -> None:
# Parse the supplied command line
date_scans_started = datetime.now(UTC)
date_scans_started = datetime.now(timezone.utc)
sslyze_parser = CommandLineParser(__version__)
try:
parsed_command_line = sslyze_parser.parse_command_line()
Expand Down Expand Up @@ -82,7 +82,7 @@ def main() -> None:
for bad_server in parsed_command_line.invalid_servers
],
date_scans_started=date_scans_started,
date_scans_completed=datetime.now(UTC),
date_scans_completed=datetime.now(timezone.utc),
)
json_output_as_str = json_output.model_dump_json(indent=2)
json_file_out.write(json_output_as_str)
Expand Down
59 changes: 44 additions & 15 deletions sslyze/connection_helpers/opportunistic_tls_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import struct
from abc import abstractmethod, ABC
from enum import Enum
from smtplib import SMTP, SMTPException
from typing import ClassVar, Optional


Expand Down Expand Up @@ -61,20 +62,46 @@ def prepare_socket_for_tls_handshake(self, sock: socket.socket) -> None:
class _SmtpHelper(_OpportunisticTlsHelper):
"""Perform an SMTP StartTLS negotiation."""

def __init__(self, smtp_ehlo_hostname: str):
self._smtp_ehlo_hostname = smtp_ehlo_hostname

def prepare_socket_for_tls_handshake(self, sock: socket.socket) -> None:
# Get the SMTP banner
sock.recv(2048)
# SMTP parsing has some complicated areas and some unusual but legal
# server behavior - this code uses Python's smtplib to handle the protocol.
smtp = SMTP(local_hostname=self._smtp_ehlo_hostname)
smtp.sock = sock

# Send a EHLO and wait for the 250 status
sock.send(b"EHLO sslyze.scan\r\n")
data = sock.recv(2048)
if b"250 " not in data:
raise OpportunisticTlsError(f"SMTP EHLO was rejected: {repr(data)}")
try:
code, server_reply_as_bytes = smtp.getreply()
except SMTPException as exc:
raise OpportunisticTlsError(f"Unexpected error while performing the SMTP EHLO handshake: {str(exc)}")

if code != 220:
server_reply_as_str = server_reply_as_bytes.decode()
raise OpportunisticTlsError(
f"Server did not send a '220 service ready' SMTP message: {server_reply_as_str}"
)

try:
code, server_reply_as_bytes = smtp.ehlo()
except SMTPException as exc:
raise OpportunisticTlsError(f"Unexpected error while performing the SMTP EHLO handshake: {str(exc)}")

if code != 250:
server_reply_as_str = server_reply_as_bytes.decode()
raise OpportunisticTlsError(f"SMTP EHLO was rejected: {server_reply_as_str}")

# Send a STARTTLS
sock.send(b"STARTTLS\r\n")
if b"220" not in sock.recv(2048):
raise OpportunisticTlsError("SMTP STARTTLS not supported")
if not smtp.has_extn("starttls"):
raise OpportunisticTlsError("Server does not support STARTTLS with SMTP")

try:
code, server_reply_as_bytes = smtp.docmd("STARTTLS")
except SMTPException as exc:
raise OpportunisticTlsError(f"Unexpected error while performing the SMTP EHLO handshake: {str(exc)}")

if code != 220:
server_reply_as_str = server_reply_as_bytes.decode()
raise OpportunisticTlsError(f"SMTP STARTTLS rejected: {server_reply_as_str}")


class _XmppHelper(_OpportunisticTlsHelper):
Expand Down Expand Up @@ -220,14 +247,16 @@ class _PostgresHelper(_GenericOpportunisticTlsHelper):


def get_opportunistic_tls_helper(
protocol: ProtocolWithOpportunisticTlsEnum, xmpp_to_hostname: Optional[str]
protocol: ProtocolWithOpportunisticTlsEnum, xmpp_to_hostname: Optional[str], smtp_ehlo_hostname: Optional[str]
) -> _OpportunisticTlsHelper:
helper_cls = _START_TLS_HELPER_CLASSES[protocol]
if protocol not in [ProtocolWithOpportunisticTlsEnum.XMPP, ProtocolWithOpportunisticTlsEnum.XMPP_SERVER]:
opportunistic_tls_helper = helper_cls()
else:
if protocol in [ProtocolWithOpportunisticTlsEnum.XMPP, ProtocolWithOpportunisticTlsEnum.XMPP_SERVER]:
if xmpp_to_hostname is None:
raise ValueError("Received None for xmpp_to_hostname")
opportunistic_tls_helper = helper_cls(xmpp_to=xmpp_to_hostname)
elif protocol == ProtocolWithOpportunisticTlsEnum.SMTP:
opportunistic_tls_helper = helper_cls(smtp_ehlo_hostname=smtp_ehlo_hostname)
else:
opportunistic_tls_helper = helper_cls()

return opportunistic_tls_helper
4 changes: 3 additions & 1 deletion sslyze/connection_helpers/tls_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,9 @@ def _do_pre_handshake(self) -> None:
# Do the Opportunistic/StartTLS negotiation if needed
if self._network_configuration.tls_opportunistic_encryption:
opportunistic_tls_helper = get_opportunistic_tls_helper(
self._network_configuration.tls_opportunistic_encryption, self._network_configuration.xmpp_to_hostname
self._network_configuration.tls_opportunistic_encryption,
self._network_configuration.xmpp_to_hostname,
self._network_configuration.smtp_ehlo_hostname,
)
try:
opportunistic_tls_helper.prepare_socket_for_tls_handshake(sock)
Expand Down
9 changes: 7 additions & 2 deletions sslyze/scanner/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,14 @@ class AllScanCommandsAttempts:
http_headers: HttpHeadersScanAttempt


_SCAN_CMD_FIELD_NAME_TO_CLS: dict[str, Type[ScanCommandAttempt]] = {
cls_field.name: cls_field.type # type: ignore
for cls_field in fields(AllScanCommandsAttempts)
}


def get_scan_command_attempt_cls(scan_command: ScanCommand) -> Type[ScanCommandAttempt]:
field_name_to_cls = {cls_field.name: cls_field.type for cls_field in fields(AllScanCommandsAttempts)}
return field_name_to_cls[scan_command.value]
return _SCAN_CMD_FIELD_NAME_TO_CLS[scan_command.value]


class ServerConnectivityStatusEnum(str, Enum):
Expand Down
18 changes: 16 additions & 2 deletions sslyze/server_setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,18 +163,20 @@ class ServerNetworkConfiguration:

Attributes:
tls_server_name_indication: The hostname to set within the Server Name Indication TLS extension.
tls_wrapped_protocol: The protocol wrapped in TLS that the server expects. It allows SSLyze to figure out
tls_opportunistic_encryption: The protocol wrapped in TLS that the server expects. It allows SSLyze to figure out
how to establish a (Start)TLS connection to the server and what kind of "hello" message
(SMTP, XMPP, etc.) to send to the server after the handshake was completed. If not supplied, standard
TLS will be used.
tls_client_auth_credentials: The client certificate and private key needed to perform mutual authentication
with the server. If not supplied, SSLyze will attempt to connect to the server without performing
client authentication.
xmpp_to_hostname: The hostname to set within the `to` attribute of the XMPP stream. If not supplied, the
server's hostname will be used. Should only be set if the supplied `tls_wrapped_protocol` is an
server's hostname will be used. Should only be set if the supplied `tls_opportunistic_encryption` is an
XMPP protocol.
http_user_agent: The User-Agent to send in HTTP requests. If not supplied, a default Chrome-like
is used that includes SSLyze's version.
smtp_ehlo_hostname: The hostname to set in the SMTP EHLO. If not supplied, the default of "sslyze.scan"
will be used. Should only be set if the supplied `tls_opportunistic_encryption` is SMTP.
network_timeout: The timeout (in seconds) to be used when attempting to establish a connection to the
server.
network_max_retries: The number of retries SSLyze will perform when attempting to establish a connection
Expand All @@ -186,6 +188,7 @@ class ServerNetworkConfiguration:
tls_client_auth_credentials: Optional[ClientAuthenticationCredentials] = None

xmpp_to_hostname: Optional[str] = None
smtp_ehlo_hostname: Optional[str] = None
http_user_agent: Optional[str] = None

network_timeout: int = 5
Expand All @@ -204,6 +207,17 @@ def __post_init__(self) -> None:
if self.xmpp_to_hostname:
raise InvalidServerNetworkConfigurationError("Can only specify xmpp_to for the XMPP StartTLS protocol.")

if self.tls_opportunistic_encryption in [
ProtocolWithOpportunisticTlsEnum.SMTP,
]:
if not self.smtp_ehlo_hostname:
object.__setattr__(self, "smtp_ehlo_hostname", "sslyze.scan")
else:
if self.smtp_ehlo_hostname:
raise InvalidServerNetworkConfigurationError(
"Can only specify smtp_ehlo_hostname for the SMTP StartTLS protocol."
)

if self.tls_opportunistic_encryption and self.http_user_agent:
raise InvalidServerNetworkConfigurationError(
"Cannot specify both tls_opportunistic_encryption and http_user_agent"
Expand Down
Loading