Skip to content

Commit 3e0015c

Browse files
committed
Merge branch 'smtp-improvements' of https://github.com/mxsasha/sslyze into mxsasha-smtp-improvements
sslyze/plugins/certificate_info/trust_stores/pem_files/apple.pem sslyze/plugins/certificate_info/trust_stores/pem_files/google_aosp.pem sslyze/plugins/certificate_info/trust_stores/pem_files/google_aosp.yaml sslyze/plugins/certificate_info/trust_stores/pem_files/microsoft_windows.pem sslyze/plugins/certificate_info/trust_stores/pem_files/mozilla_nss.pem sslyze/plugins/certificate_info/trust_stores/pem_files/mozilla_nss.yaml sslyze/plugins/certificate_info/trust_stores/pem_files/openjdk.pem sslyze/plugins/certificate_info/trust_stores/pem_files/oracle_java.pem
2 parents f9c3146 + 01ca04d commit 3e0015c

File tree

3 files changed

+59
-19
lines changed

3 files changed

+59
-19
lines changed

sslyze/connection_helpers/opportunistic_tls_helpers.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import struct
33
from abc import abstractmethod, ABC
44
from enum import Enum
5+
from smtplib import SMTP, SMTPException
56
from typing import ClassVar, Optional
67

78

@@ -61,20 +62,41 @@ def prepare_socket_for_tls_handshake(self, sock: socket.socket) -> None:
6162
class _SmtpHelper(_OpportunisticTlsHelper):
6263
"""Perform an SMTP StartTLS negotiation."""
6364

65+
def __init__(self, smtp_ehlo_hostname: str):
66+
self._smtp_ehlo_hostname = smtp_ehlo_hostname
67+
6468
def prepare_socket_for_tls_handshake(self, sock: socket.socket) -> None:
65-
# Get the SMTP banner
66-
sock.recv(2048)
69+
# SMTP parsing has some complicated areas and some unusual but legal
70+
# server behavior - this code uses Python's smtplib to handle the protocol.
71+
smtp = SMTP(local_hostname=self._smtp_ehlo_hostname)
72+
smtp.sock = sock
6773

68-
# Send a EHLO and wait for the 250 status
69-
sock.send(b"EHLO sslyze.scan\r\n")
70-
data = sock.recv(2048)
71-
if b"250 " not in data:
72-
raise OpportunisticTlsError(f"SMTP EHLO was rejected: {repr(data)}")
74+
try:
75+
code, server_reply = smtp.getreply()
76+
message = server_reply.decode()
77+
except SMTPException as exc:
78+
code, message = -1, str(exc)
79+
if code != 220:
80+
raise OpportunisticTlsError(f"Unable to find 220 service ready response: {message}")
7381

74-
# Send a STARTTLS
75-
sock.send(b"STARTTLS\r\n")
76-
if b"220" not in sock.recv(2048):
77-
raise OpportunisticTlsError("SMTP STARTTLS not supported")
82+
try:
83+
code, server_reply = smtp.ehlo()
84+
message = server_reply.decode()
85+
except SMTPException as exc:
86+
code, message = -1, str(exc)
87+
if code != 250:
88+
raise OpportunisticTlsError(f"SMTP EHLO was rejected: {message}")
89+
90+
if not smtp.has_extn("starttls"):
91+
raise OpportunisticTlsError(f"Server does not support STARTTLS: {message}")
92+
93+
try:
94+
code, server_reply = smtp.docmd("STARTTLS")
95+
message = server_reply.decode()
96+
except SMTPException as exc:
97+
code, message = -1, str(exc)
98+
if code != 220:
99+
raise OpportunisticTlsError(f"SMTP STARTTLS rejected: {message}")
78100

79101

80102
class _XmppHelper(_OpportunisticTlsHelper):
@@ -220,14 +242,16 @@ class _PostgresHelper(_GenericOpportunisticTlsHelper):
220242

221243

222244
def get_opportunistic_tls_helper(
223-
protocol: ProtocolWithOpportunisticTlsEnum, xmpp_to_hostname: Optional[str]
245+
protocol: ProtocolWithOpportunisticTlsEnum, xmpp_to_hostname: Optional[str], smtp_ehlo_hostname: str
224246
) -> _OpportunisticTlsHelper:
225247
helper_cls = _START_TLS_HELPER_CLASSES[protocol]
226-
if protocol not in [ProtocolWithOpportunisticTlsEnum.XMPP, ProtocolWithOpportunisticTlsEnum.XMPP_SERVER]:
227-
opportunistic_tls_helper = helper_cls()
228-
else:
248+
if protocol in [ProtocolWithOpportunisticTlsEnum.XMPP, ProtocolWithOpportunisticTlsEnum.XMPP_SERVER]:
229249
if xmpp_to_hostname is None:
230250
raise ValueError("Received None for xmpp_to_hostname")
231251
opportunistic_tls_helper = helper_cls(xmpp_to=xmpp_to_hostname)
252+
elif protocol == ProtocolWithOpportunisticTlsEnum.SMTP:
253+
opportunistic_tls_helper = helper_cls(smtp_ehlo_hostname)
254+
else:
255+
opportunistic_tls_helper = helper_cls()
232256

233257
return opportunistic_tls_helper

sslyze/connection_helpers/tls_connection.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,9 @@ def _do_pre_handshake(self) -> None:
233233
# Do the Opportunistic/StartTLS negotiation if needed
234234
if self._network_configuration.tls_opportunistic_encryption:
235235
opportunistic_tls_helper = get_opportunistic_tls_helper(
236-
self._network_configuration.tls_opportunistic_encryption, self._network_configuration.xmpp_to_hostname
236+
self._network_configuration.tls_opportunistic_encryption,
237+
self._network_configuration.xmpp_to_hostname,
238+
self._network_configuration.smtp_ehlo_hostname,
237239
)
238240
try:
239241
opportunistic_tls_helper.prepare_socket_for_tls_handshake(sock)

sslyze/server_setting.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,20 @@ class ServerNetworkConfiguration:
163163
164164
Attributes:
165165
tls_server_name_indication: The hostname to set within the Server Name Indication TLS extension.
166-
tls_wrapped_protocol: The protocol wrapped in TLS that the server expects. It allows SSLyze to figure out
166+
tls_opportunistic_encryption: The protocol wrapped in TLS that the server expects. It allows SSLyze to figure out
167167
how to establish a (Start)TLS connection to the server and what kind of "hello" message
168168
(SMTP, XMPP, etc.) to send to the server after the handshake was completed. If not supplied, standard
169169
TLS will be used.
170170
tls_client_auth_credentials: The client certificate and private key needed to perform mutual authentication
171171
with the server. If not supplied, SSLyze will attempt to connect to the server without performing
172172
client authentication.
173173
xmpp_to_hostname: The hostname to set within the `to` attribute of the XMPP stream. If not supplied, the
174-
server's hostname will be used. Should only be set if the supplied `tls_wrapped_protocol` is an
174+
server's hostname will be used. Should only be set if the supplied `tls_opportunistic_encryption` is an
175175
XMPP protocol.
176176
http_user_agent: The User-Agent to send in HTTP requests. If not supplied, a default Chrome-like
177177
is used that includes SSLyze's version.
178+
smtp_ehlo_hostname: The hostname to set in the SMTP EHLO. If not supplied, the default of "sslyze.scan"
179+
will be used. Should only be set if the supplied `tls_opportunistic_encryption` is SMTP.
178180
network_timeout: The timeout (in seconds) to be used when attempting to establish a connection to the
179181
server.
180182
network_max_retries: The number of retries SSLyze will perform when attempting to establish a connection
@@ -186,6 +188,7 @@ class ServerNetworkConfiguration:
186188
tls_client_auth_credentials: Optional[ClientAuthenticationCredentials] = None
187189

188190
xmpp_to_hostname: Optional[str] = None
191+
smtp_ehlo_hostname: Optional[str] = None
189192
http_user_agent: Optional[str] = None
190193

191194
network_timeout: int = 5
@@ -203,7 +206,18 @@ def __post_init__(self) -> None:
203206
else:
204207
if self.xmpp_to_hostname:
205208
raise InvalidServerNetworkConfigurationError("Can only specify xmpp_to for the XMPP StartTLS protocol.")
206-
209+
210+
if self.tls_opportunistic_encryption in [
211+
ProtocolWithOpportunisticTlsEnum.SMTP,
212+
]:
213+
if not self.smtp_ehlo_hostname:
214+
object.__setattr__(self, "smtp_ehlo_hostname", "sslyze.scan")
215+
else:
216+
if self.smtp_ehlo_hostname:
217+
raise InvalidServerNetworkConfigurationError(
218+
"Can only specify smtp_ehlo_hostname for the SMTP StartTLS protocol."
219+
)
220+
207221
if self.tls_opportunistic_encryption and self.http_user_agent:
208222
raise InvalidServerNetworkConfigurationError(
209223
"Cannot specify both tls_opportunistic_encryption and http_user_agent"

0 commit comments

Comments
 (0)