I ran into something I can't quite explain and I'd rather ask before assuming it's a bug, since the code looks deliberate and I may well be missing context.
Against a server that allows guest access, a guest session works fine up to dialect 3.0 but fails at 3.1.1: the session setup succeeds and the server marks the session as guest, then the very first TREE_CONNECT (to IPC$) comes back STATUS_ACCESS_DENIED.
Since 3.1.1 is part of the default dialect list now, callers that don't pass preferredDialect land on that path, so listShares() and friends fail for guest where they used to work.
The same server serves the same share to smbclient -N as guest without complaint, and its log says the problem is a signature mismatch rather than a permissions decision, which is what makes me think the client is signing something it shouldn't, rather than the server being unhappy about access.
Configuration
- impacket
v0.14.0.dev0+20260828.120813.032dfb1b (current master)
- Python 3.14, Linux
- Target: stock Samba 4.17.12 from
debian:bookworm-slim, no patches, config below
Reproducer
Fully self-contained, a stock Debian Samba container with one guest-readable share.
Dockerfile:
FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends samba smbclient \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /srv/public /run/samba /var/lib/samba/private \
&& echo "hello" > /srv/public/readme.txt
COPY smb.conf /etc/samba/smb.conf
CMD ["smbd", "--foreground", "--no-process-group", "--debug-stdout"]
smb.conf, the only notable settings are map to guest and guest ok:
[global]
workgroup = WORKGROUP
netbios name = REPRO
server string = samba guest repro
security = user
map to guest = Bad User
guest account = nobody
log level = 1
load printers = no
printing = bsd
printcap name = /dev/null
disable spoolss = yes
[public]
path = /srv/public
browseable = yes
read only = yes
guest ok = yes
repro.py:
import sys
from impacket import version
from impacket.smbconnection import SMBConnection
from impacket.smb3structs import (
SMB2_DIALECT_002, SMB2_DIALECT_21, SMB2_DIALECT_30, SMB2_DIALECT_311,
)
TARGET = sys.argv[1]
CASES = [
("guest", "", "2.0.2", SMB2_DIALECT_002),
("guest", "", "2.1", SMB2_DIALECT_21),
("guest", "", "3.0", SMB2_DIALECT_30),
("guest", "", "3.1.1", SMB2_DIALECT_311),
("guest", "", "default", None),
("nosuchuser", "wrongpw", "default", None), # also mapped to guest
("", "", "default", None), # null session, for contrast
]
print(version.BANNER)
print(f"{'user':12} {'asked':8} {'got':8} {'guest':5} {'signing':7} result")
for user, password, label, dialect in CASES:
conn = SMBConnection(TARGET, TARGET, preferredDialect=dialect, timeout=10)
conn.login(user, password)
got = f"0x{conn.getDialect():04x}"
guest = bool(conn.isGuestSession())
signing = bool(conn._SMBConnection._Session["SigningActivated"])
try:
tid = conn.connectTree("IPC$")
conn.disconnectTree(tid)
shares = ",".join(s["shi1_netname"].rstrip("\x00") for s in conn.listShares())
result = f"OK -> {shares}"
except Exception as exc:
result = f"FAIL -> {exc}"
print(f"{user or '<null>':12} {label:8} {got:8} {str(guest):5} {str(signing):7} {result}")
conn.close()
Output
First, the same server with a stock client, to establish that guest access itself is fine:
$ docker exec repro smbclient -N -L localhost
Sharename Type Comment
--------- ---- -------
public Disk
IPC$ IPC IPC Service (samba guest repro)
$ docker exec repro smbclient -N //localhost/public -c 'ls'
. D 0 Tue Sep 1 12:30:08 2026
.. D 0 Tue Sep 1 12:30:08 2026
readme.txt N 31 Tue Sep 1 12:30:08 2026
Then impacket against that same container:
Impacket v0.14.0.dev0+20260828.120813.032dfb1b - Copyright Fortra, LLC and its affiliated companies
user asked got guest signing result
guest 2.0.2 0x0202 True False OK -> public,IPC$
guest 2.1 0x0210 True False OK -> public,IPC$
guest 3.0 0x0300 True False OK -> public,IPC$
guest 3.1.1 0x0311 True True FAIL -> STATUS_ACCESS_DENIED
guest default 0x0311 True True FAIL -> STATUS_ACCESS_DENIED
nosuchuser default 0x0311 True True FAIL -> STATUS_ACCESS_DENIED
<null> default 0x0311 False True OK -> public,IPC$
(STATUS_ACCESS_DENIED above is the full SMB SessionError: code: 0xc0000022 ..., trimmed for width.)
Two things stand out: signing flips on exactly where it breaks, and the null session, which comes back guest=False, is unaffected even though it also has signing active. So it's specifically the guest case.
What the server says
With log level = 1, smbd logs one of these per failed request:
Bad SMB2 (sign_algo_id=1) signature for message
That's smb2_signing_check_pdu() in libcli/smb/smb2_signing.c reaching its mem_equal_const_time(res, sig, 16) comparison and failing it, i.e. the server did receive a signed request and the signature didn't match, it isn't refusing access to IPC$ for any other reason.
The reason the two ends can't agree is that a guest session's session key is all zeros server-side. Samba is explicit about it in source3/auth/auth_util.c:
/* annoying, but the Guest really does have a session key, and it is
all zeros! */
session_info->session_key = data_blob_talloc_zero(session_info, 16);
The client derives its signing key from the password it actually sent, so the two signing keys differ. The session setup itself is unsigned, which is why login appears to succeed and only the next request fails.
What I think is happening client-side (please correct me)
As far as I can follow it, Connection.RequireSigning is set unconditionally for 3.1.1 in negotiateSession() — smb3.py L687-692 on master:
if (negResp['SecurityMode'] & SMB2_NEGOTIATE_SIGNING_REQUIRED) == SMB2_NEGOTIATE_SIGNING_REQUIRED or \
self._Connection['Dialect'] == SMB2_DIALECT_311:
self._Connection['RequireSigning'] = True
if self._Connection['Dialect'] == SMB2_DIALECT_311:
# Always Sign
self._Connection['RequireSigning'] = True
That flows into Session['SigningRequired'] (L1053), and then in sessionSetup() (L1135-1146) the server's session flags arrive and signing is activated:
sessionSetupResponse = SMB2SessionSetup_Response(packet['Data'])
self._Session['SessionFlags'] = sessionSetupResponse['SessionFlags']
self._Session['SessionID'] = packet['SessionID']
# Do not encrypt anonymous connections
if user == '' or self.isGuestSession():
self._Connection['SupportsEncryption'] = False
# Calculate the key derivations for dialect 3.0
if self._Session['SigningRequired'] is True:
self._Session['SigningActivated'] = True
So SMB2_SESSION_FLAG_IS_GUEST is available right there and already used to turn encryption off, but signing stays on. That's the part that makes me unsure this is an oversight, the guest case was clearly considered three lines earlier, and # Always Sign reads like a conscious decision.
Why it seems to be surfacing now
I don't think the signing code changed. git log -S"# Always Sign" puts that block in 8afe4fe (2020-03-12, "Adding SMB 3.1.1 support for Client SMB Connections"), and 8f81720 ("fix connection issue when Smb2DialectMin is SMB311 on target SMB server") doesn't touch RequireSigning at all — it added 3.0.2 and 3.1.1 to the default dialect list. So my read is that the guest behaviour has been there since 3.1.1 support landed, and callers only started reaching it by default after the dialect change. That commit looks correct in itself; guest connections just seem to be collateral. Consistent with that, the explicit 3.1.1 row above fails on any version, not just current master.
Downstream this shows up as e.g. nxc smb <ip> --shares -u guest -p '' reporting Error enumerating shares: STATUS_ACCESS_DENIED, which is what sent me digging.
For reference
MS-SMB2 3.2.5.3.1 does seem to carve guest/anonymous out client-side:
- If the security subsystem indicates that the session was established by an anonymous user, Session.SigningRequired MUST be set to FALSE and Session.IsAnonymous MUST be set to TRUE.
- If the security subsystem indicates that the session was established by a guest user, Session.SigningRequired MUST be set to FALSE and Session.IsGuest MUST be set to TRUE.
- If the SMB2_SESSION_FLAG_IS_GUEST bit is set in the SessionFlags field of the SMB2 SESSION_SETUP Response and if RequireMessageSigning is FALSE, Session.SigningRequired MUST be set to FALSE.
and Microsoft's Open Specifications write-up on SMB signing states the 3.1.1 rule with an explicit exception:
If 3.1.1 dialect is negotiated, the protocol requires signing or encrypting all TREE_CONNECT sent in non-guest, non-anonymous sessions.
NOTE: Signing cannot be required for guest or anonymous sessions as they do not have proper security context.
I'm reading those cold, though, and it's entirely possible there's a practical reason not to follow them here.
I should also be upfront that I've only tested against Samba.
Happy to put together a PR and test it more widely if you can point me at the shape you'd want — and equally happy to be told I'm holding it wrong. Thanks for taking a look.
Disclaimer: I used an AI assistant to help draft this write-up and to track down the exact line numbers and commits referenced above. The reproduction is my own: I hit the bug in the first place, built the container myself, and every line of output pasted here is from a run I did.
I ran into something I can't quite explain and I'd rather ask before assuming it's a bug, since the code looks deliberate and I may well be missing context.
Against a server that allows guest access, a guest session works fine up to dialect 3.0 but fails at 3.1.1: the session setup succeeds and the server marks the session as guest, then the very first
TREE_CONNECT(toIPC$) comes backSTATUS_ACCESS_DENIED.Since 3.1.1 is part of the default dialect list now, callers that don't pass
preferredDialectland on that path, solistShares()and friends fail for guest where they used to work.The same server serves the same share to
smbclient -Nas guest without complaint, and its log says the problem is a signature mismatch rather than a permissions decision, which is what makes me think the client is signing something it shouldn't, rather than the server being unhappy about access.Configuration
v0.14.0.dev0+20260828.120813.032dfb1b(currentmaster)debian:bookworm-slim, no patches, config belowReproducer
Fully self-contained, a stock Debian Samba container with one guest-readable share.
Dockerfile:smb.conf, the only notable settings aremap to guestandguest ok:repro.py:Output
First, the same server with a stock client, to establish that guest access itself is fine:
Then impacket against that same container:
(
STATUS_ACCESS_DENIEDabove is the fullSMB SessionError: code: 0xc0000022 ..., trimmed for width.)Two things stand out: signing flips on exactly where it breaks, and the null session, which comes back
guest=False, is unaffected even though it also has signing active. So it's specifically the guest case.What the server says
With
log level = 1, smbd logs one of these per failed request:That's
smb2_signing_check_pdu()inlibcli/smb/smb2_signing.creaching itsmem_equal_const_time(res, sig, 16)comparison and failing it, i.e. the server did receive a signed request and the signature didn't match, it isn't refusing access toIPC$for any other reason.The reason the two ends can't agree is that a guest session's session key is all zeros server-side. Samba is explicit about it in
source3/auth/auth_util.c:The client derives its signing key from the password it actually sent, so the two signing keys differ. The session setup itself is unsigned, which is why login appears to succeed and only the next request fails.
What I think is happening client-side (please correct me)
As far as I can follow it,
Connection.RequireSigningis set unconditionally for 3.1.1 innegotiateSession()—smb3.pyL687-692 on master:That flows into
Session['SigningRequired'](L1053), and then insessionSetup()(L1135-1146) the server's session flags arrive and signing is activated:So
SMB2_SESSION_FLAG_IS_GUESTis available right there and already used to turn encryption off, but signing stays on. That's the part that makes me unsure this is an oversight, the guest case was clearly considered three lines earlier, and# Always Signreads like a conscious decision.Why it seems to be surfacing now
I don't think the signing code changed.
git log -S"# Always Sign"puts that block in 8afe4fe (2020-03-12, "Adding SMB 3.1.1 support for Client SMB Connections"), and 8f81720 ("fix connection issue when Smb2DialectMin is SMB311 on target SMB server") doesn't touchRequireSigningat all — it added 3.0.2 and 3.1.1 to the default dialect list. So my read is that the guest behaviour has been there since 3.1.1 support landed, and callers only started reaching it by default after the dialect change. That commit looks correct in itself; guest connections just seem to be collateral. Consistent with that, the explicit3.1.1row above fails on any version, not just current master.Downstream this shows up as e.g.
nxc smb <ip> --shares -u guest -p ''reportingError enumerating shares: STATUS_ACCESS_DENIED, which is what sent me digging.For reference
MS-SMB2 3.2.5.3.1 does seem to carve guest/anonymous out client-side:
and Microsoft's Open Specifications write-up on SMB signing states the 3.1.1 rule with an explicit exception:
I'm reading those cold, though, and it's entirely possible there's a practical reason not to follow them here.
I should also be upfront that I've only tested against Samba.
Happy to put together a PR and test it more widely if you can point me at the shape you'd want — and equally happy to be told I'm holding it wrong. Thanks for taking a look.
Disclaimer: I used an AI assistant to help draft this write-up and to track down the exact line numbers and commits referenced above. The reproduction is my own: I hit the bug in the first place, built the container myself, and every line of output pasted here is from a run I did.