Server Message Block Research #
The Rumble scan engine received big updates this month for the HTTP, RDP, and SMB protocols. The SMB work was focused on improving protocol support for SMB1, SMB2, and SMB3, including better desktop/server detection, and reporting of available compression methods in SMB3 (to support CVE-2020-0796 investigations).
One thing that stood out during this work was the SMB2 SessionID field. Similar to a web application session, this field is allocated by the server and used to identify an authenticated session. Unlike a web application session, this field is predictable across many implementations, and that raises some questions.
- If SMB2 SessionIDs are not random, how does that impact the security of SMB?
- What can an attacker do if they can predict another user's SessionID?
- Is this better or worse when using SMB3 dialects with SMB2?
These questions led to a deep dive into the SMB protocol. Armed with Wireshark, Microsoft's documentation, and a barely-there SMB implementation in Go, we decided to find out.
SMB2 SessionID Allocation #
SMB2 SessionIDs are 64-bit integers. On the Windows platform (Windows 7+), these IDs start at semi-static values and increment as new incoming authentication requests are received via the SMB2 SESSION_SETUP
command. A typical starting value from a Windows 10 desktop is 0x00002c0000000001
. On newer macOS platforms, the high 32-bit value is static, while the lower 32-bits start off at 1 and increment as new requests are received (ex:0x4401bedc00000001
). On Linux implementations, the high 32-bits are often zero, and the lower 32-bits appear to be randomly-allocated (ex:0x00000000ec1ebff0
).
We created a utility to repeatedly connect to a SMB2+ endpoint and send the first part of the SESSION_SETUP
request. The results show that Windows systems replied with mostly sequential IDs, with some regular jumps across the bitspace, while macOS systems were strictly sequential, incrementing the SessionID by 1 with each request. The IDs below were generated by a Windows Server 2019 system.
$ go run main.go win2019dc01 sample
20:31:02 0x0000140064000075
20:31:02 0x0000140064000079
20:31:02 0x000014006400007d
20:31:02 0x0000140068000001
20:31:02 0x0000140068000005
20:31:02 0x0000140068000009
Remote Utilization Monitoring #
Since every new authentication to a SMB2 server causes the SessionID to increment, it should be possible to monitor a remote server's usage, no credentials required. On macOS, this is trivial, since the ID always increments by 1. On Windows, this takes a bit more work, as the Session IDs can jump around, and the allocation pattern isn't the same on every system.
To solve this, we tweaked our utility to sample SessionID allocations until a repeating pattern was observed. Once the cycle is learned, the pattern is used to predict the next SessionID, alerting us if the expected value was not received. If there is a gap between the expected and received value, we can walk the previous SessionID forward using the learned pattern in order to determine the number of sessions created since our last query. The counter prediction code is general purpose and part of the open source runZero Tools repository.
Running the utility in watch
mode learns the allocation pattern and starts regular queries:
$ go run main.go server2016 watch
server2016: determining the session cycle for map[ntlmssp.DNSComputer:WIN-EM7GG1U0LV3 ntlmssp.DNSDomain:WIN-EM7GG1U0LV3 ntlmssp.NTLMRevision:15 ntlmssp.NegotiationFlags:0xe28a8215 ntlmssp.NetbiosComputer:WIN-EM7GG1U0LV3 ntlmssp.NetbiosDomain:WIN-EM7GG1U0LV3 ntlmssp.TargetName:WIN-EM7GG1U0LV3 ntlmssp.Timestamp:0x01d6056d5260b1f8 ntlmssp.Version:10.0.14393 smb.Capabilities:0x0000002f smb.CipherAlg:aes-128-gcm
smb.Dialect:0x0311 smb.GUID:6edc815a-7bea-cb41-a1dd-6079352c4fce smb.HashAlg:sha512 smb.HashSaltLen:32 smb.SessionID:0x00002c3650000055 smb.Signing:enabled smb.Status:0xc0000016]
server2016: cycle found after 129 requests: fffffffff8000008-7fffffc-ffffffffc8000014-ffffffd707ffffc0-2930000038-ffffffffebffffd0-14000034-3ffff8c-8-ffffffff5c00003c-a3ffffd4-fffffffffffffff4-8-fffffffffffffff0-fffffffff800004c-7ffffcc-ffffffffcc000030-33ffffc4-18-fffffffffffffff8-1c-fffffffffc00001c-3ffffdc-fffffffff8000028-7ffffd4-fffffffffffffffc-1c-fffffffffc000034-ffffffffc3ffffa8-34000048-bffffe8-c
server2016: watching for new sessions...
If we try to authenticate to this server with an invalid credential from another machine, we can see that a new session was detected:
server2016: SESSION 0x00002c3638000079 is EXPIRED
This allows us to remotely monitor the creation of new SMB2 sessions on Windows and macOS without credentials. This seems like a bad thing, but the impact is pretty minor, and this would barely qualify as a vulnerability, especially all of the other information that SMB exposes to the network.
Impact of Session Prediction #
Since we can predict the new SessionID value and detect new sessions, what can we do if we know the SessionID of another user's active session? We tried the obvious things first, such as creating a new SMB2 connection with another user's SessionID set in the SMB2 header, and ran into immediate roadblocks. Windows will shutdown the connection if a SessionID is specified before the SESSION_SETUP
command or if any useful commands (like a TREE_CONNECT
) are issued before the SESSION_SETUP
.
The documentation for authenticating a user states that the SESSION_SETUP
command supports a Previous SessionID
field, which could allow an existing session to be invalidated, but only if our new session successfully authenticates as the same user. Since we can predict a SessionID, but not the credentials, this isn't relevant.
A second option is a new feature in SMB3 called Session Binding
. This is covered in the documentation for SERVER_SETUP, which describes how a client can establish a new channel for an existing session:
If Connection.Dialect belongs to the SMB 3.x dialect family, IsMultiChannelCapable is TRUE, and the SMB2_SESSION_FLAG_BINDING bit is set in the Flags field of the request, the server MUST perform the following:
The server MUST look up the session in GlobalSessionTable using the SessionId from the SMB2 header. If the session is not found, the server MUST fail the session setup request with STATUS_USER_SESSION_DELETED. If a session is found, the server MUST do the following:
This seems promising. The SessionID is resolved from the GlobalSessionTable
not the connection table. We know this requires a SMB3 dialect and a couple flags to be set, but what other hoops would an attacker have to jump through to take over an existing session if they can predict the ID?
If Connection.Dialect is not the same as Session.Connection.Dialect, the server MUST fail the request with STATUS_INVALID_PARAMETER.
OK, easy enough. Most clients are going to use the SMB Dialect of 3.1.1.
If the SMB2_FLAGS_SIGNED bit is not set in the Flags field in the header, the server MUST fail the request with error STATUS_INVALID_PARAMETER.
Great, we can set that.
If Session.Connection.ClientGuid is not the same as Connection.ClientGuid, the server MAY fail the request with STATUS_USER_SESSION_DELETED.
This one might be tough unless we can get the Client GUID from the original source of the predicted session. Oh, nevermind, Windows doesn't actually validate this.
If Session.State is InProgress, the server MUST fail the request with STATUS_REQUEST_NOT_ACCEPTED.
If Session.State is Expired, the server MUST fail the request with STATUS_NETWORK_SESSION_EXPIRED.
If Session.IsAnonymous or Session.IsGuest is TRUE, the server MUST fail the request with STATUS_NOT_SUPPORTED.
All good as well. We are specifying the SessionID of an active authenticated user.
If there is a session in Connection.SessionTable identified by the SessionId in the request, the server MUST fail the request with STATUS_REQUEST_NOT_ACCEPTED.
Not a problem, our connection has no other active sessions.
The server MUST verify the signature as specified in section 3.3.5.2.4, using the Session.SigningKey.
Oh boy, actual security! How is the client-side signature validated and what pieces do we control, without knowing anything at all about the SessionID we predicted?
Let's Try Forging a Signature #
Reading through the signature verification documentation we see confirmation that Session Binding
is a real thing and that is uses the Session.SigningKey
as the source of signing and validation.
How do we generate this Session.SigningKey
? The glorious document titled SMB 2 and SMB 3 security in Windows 10: the anatomy of signing and cryptographic keys (a short 31-minute read for robots) goes into the details. Since Session Binding
is limited to SMB3 dialects, let's start there.
Before signing can happen, the client and server need to negotiate signing; and the client needs authenticate first to have a SessionKey from which a SigningKey is derived
The SigningKey is derived from a KDF documented in SP800-108 section 5.1 with the following parameters:
SigningKey = SMB3KDF (SessionKey, "SMBSigningKey\0", Session.PreauthIntegrityHashValue)
Great, so we need to calculate the SessionKey
and the PreauthIntegrityHashValue
.
Starting with the PreauthIntegrityHashValue
:
SMB 3.1.1 pre-authentication integrity enhances protection against security-downgrade attacks by verifying the integrity of all messages preceding the session establishment. This mandatory feature protects against any tampering with Negotiate and Session Setup messages by leveraging cryptographic hashing SHA-512 [FIPS180-4].
This seems like a blocker at first, but it turns out that Session Binding
uses the PreauthSession from the connection and not from the original session. We control this. Great, let's move on.
In NTLM security provider, the SessionKey is the ExportedSessionKey described in MS-NLMP. NTLM is a challenge response protocol and its session key is based on password hashing that relies on weak cryptography.
Weak crypto? Sounds great! How do we calculate that ExportedSessionKey
for sessions created with NTLMv2 authentication? Let's take a look at good ole MS-NLMP:
The keys MUST be computed with the following algorithm where all strings are encoded as RPC_UNICODE_STRING ([MS-DTYP] section 2.3.10).
And there is the dead end we have been hurtling towards.
The ExportedSessionKey
is calculated using the original session's credentials, but it also mixes in the random 8-byte Client Challenge
, random 8-byte Server Challenge
and bunch of possible-but-tricky-to-predict fields in the NTLMSSP authentication process. The timestamp and target info pairs are a bit easier, but those 16 bytes of random put the brakes on this parade. A brief look into Kerberos-based session keys doesn't paint a rosier picture.
Before we shrug our shoulders and limp back home, maybe a bit more light reading would cheer us up:
For dialect 3.x family, the server must sign the final SessionSetup Response. This excludes guest and anonymous sessions since there is no SigningKey available in those types of contexts.
Wait. What? So we can't verify the predicted session, but the server will sign its response to us, using the original session's keys? Let's try it:
$ go run main.go server2016 watch
server2016: determining the session cycle for map[ntlmssp.DNSComputer:WIN-EM7GG1U0LV3 ntlmssp.DNSDomain:WIN-EM7GG1U0LV3 ntlmssp.NTLMRevision:15 ntlmssp.NegotiationFlags:0xe28a8215 ntlmssp.NetbiosComputer:WIN-EM7GG1U0LV3 ntlmssp.NetbiosDomain:WIN-EM7GG1U0LV3 ntlmssp.TargetName:WIN-EM7GG1U0LV3 ntlmssp.Timestamp:0x01d605767c7f048e ntlmssp.Version:10.0.14393 smb.Capabilities:0x0000002f smb.CipherAlg:aes-128-gcm
smb.Dialect:0x0311 smb.GUID:6edc815a-7bea-cb41-a1dd-6079352c4fce smb.HashAlg:sha512 smb.HashSaltLen:32 smb.SessionID:0x00002c3868000059 smb.Signing:enabled smb.Status:0xc0000016]
server2016: cycle found after 129 requests: fffffffff8000028-7ffffd4-fffffffffffffffc-1c-ffffffffffffffe0-fffffffffc000054-ffffffffc3ffffa8-34000048-bffffe8-c-fffffffff8000008-7fffffc-ffffffffc8000014-ffffffd707ffffc0-2930000038-ffffffffebffffd0-14000034-3ffff8c-ffffffff5c000044-a3ffffc4-10-fffffffffffffff4-8-fffffffffffffff0-fffffffff800004c-7ffffcc-ffffffffcc000030-33ffffc4-10-1c-fffffffffc00001c-3ffffdc
server2016: watching for new sessions...
Now we create an authenticated session to the target from another machine:
server2016: SESSION 0x00002c387c000061 is ACTIVE dialect:0x0311 sig:ae126f58282955bd46e889788277b79a
Well. That's something. We can leak back a 16-byte signature, itself derived from the AES-128-CMAC, which uses the SessionKey
as the KDK, which is itself derived from the ExportedSessionKey
, which once saw the user's password hash in a fever dream.
What about that Dialect field? Well, we know our connection uses SMB 3.1.1 (it must for Session Binding
) and we know that the server will return a different error if the dialects don't match in session bind attempt. We can leak back a probably-useless 16-byte cryptographic burp AND we can tell if the dialect of the predicted session is SMB 3.1.1 or something else.
So, is this a vulnerability? We don't think so, at least not on its own, but it certainly doesn't look good. We reached out to the security folks at Microsoft and Apple as a courtesy, but don't expect to see an advisory.
Thanks for joining us on this descent into SMB madness. We hope you found it interesting would love your feedback. The code described in this post can be found in the runZero Tools repository and is available under an open source license.
If you haven't had a chance yet, check out Rumble Network Discovery and let us know what you think!
Happy Scanning!
-HD