jborean93 / smbprotocol

Python SMBv2 and v3 Client
MIT License
318 stars 73 forks source link

Compatibility with "Server SPN target name validation" GPO #169

Closed grawity closed 2 years ago

grawity commented 2 years ago

This is somewhat more of a pyspnego issue, but it involves pysmbprotocol passing it the correct information, so I ended up filing it here.

Windows servers have a Server SPN target name validation security policy available. When enabled, it causes SMB servers to reject authentication if the client claims it's connecting to a different hostname than what the server expects. With Kerberos this is already business as usual (either the client wouldn't be able to get tickets for the wrong SPN, or the server would be unable to decrypt the tickets), but the GPO extends this protection also to NTLM, in order to prevent NTLM relay attacks.

Now none of my python-smbprotocol scripts use NTLM, so this is not an issue for me, but I still decided to test it anyway and found that even when connecting to an allowed server name (i.e. where Samba's smbclient would work), python-smbprotocol nevertheless gets an "Access denied": smbprotocol.exceptions.AccessDenied: Received unexpected status from the server: A process has requested access to an object but has not been granted those access rights. (3221225506) STATUS_ACCESS_DENIED: 0xc0000022

Specifically, the difference seems to be that with Samba (which works), the MSV_AV_TARGET_NAME field in AvPairs includes the cifs/ service name like a Kerberos principal would, whereas with python-smbprotocol it does not. [MS-NLMP] seems to confirm that "ClientSuppliedTargetName" is a SPN, so the caller needs to supply the correct service name and not just the hostname.

$ cat security-blob-samba.hex | unhex | python -m spnego
[...]
                                {
                                    "AvId": "MSV_AV_TARGET_NAME (9)",
                                    "Value": "cifs/fs1.example.com"
                                },

$ cat security-blob-pysmbprotocol.hex | unhex | python -m spnego
[...]
                                {
                                    "AvId": "MSV_AV_TARGET_NAME (9)",
                                    "Value": "fs1.example.com"
                                },

Samba also sends MSV_AV_SINGLE_HOST but I don't know if it's related.

To enable strict SPN validation on a SMB server:

The place to configure "allowed" aliases seems to be SrvAllowedServerNames REG_MULTI_SZ at the same location.

jborean93 commented 2 years ago

Interesting, it looks like pyspnego's NTLM implementation is setting MSV_AV_TARGET_NAME to the SPN passed in from smbprotocol which will be cifs/hostname https://github.com/jborean93/pyspnego/blob/main/src/spnego/_ntlm.py#L706.

When testing this purely I see the NTLM auth message contains the SPN in the NT challenge response

NtChallengeResponse:
  ResponseType: NTLMv2
  NTProofStr: 321303F0587F894BF528994391DAB3BE
  ClientChallenge:
    RespType: 1
    HiRespType: 1
    Reserved1: 0
    Reserved2: 0
    TimeStamp: '2022-03-15T10:13:11.2261824Z'
    ChallengeFromClient: 471AE177EF44C84D
    Reserved3: 0
    AvPairs:
    - AvId: MSV_AV_NB_DOMAIN_NAME (2)
      Value: DOMAIN
    - AvId: MSV_AV_NB_COMPUTER_NAME (1)
      Value: SERVER2019
    - AvId: MSV_AV_DNS_DOMAIN_NAME (4)
      Value: domain.test
    - AvId: MSV_AV_DNS_COMPUTER_NAME (3)
      Value: SERVER2019.domain.test
    - AvId: MSV_AV_DNS_TREE_NAME (5)
      Value: domain.test
    - AvId: MSV_AV_TIMESTAMP (7)
      Value: '2022-03-15T10:13:11.2261824Z'
    - AvId: MSV_AV_TARGET_NAME (9)
      Value: cifs/server2019.domain.test
    - AvId: MSV_AV_FLAGS (6)
      Value:
        raw: 2
        flags:
        - MIC_PROVIDED (2)
    - AvId: MSV_AV_EOL (0)
      Value:
    Reserved4: 0

When testing further it looks like the problem is stemming from gss-ntlmssp which is used instead of the Python implementation as a GSSAPI mechanism if available. When running that I see the target name is just the hostname without the SPN. Looks like this has recently been reported and also fixed with https://github.com/gssapi/gss-ntlmssp/issues/63. Will require a new release to make it's way through the various package repositories.

Unfortunately there is no flag to state use the builtin Python NTLM implementation over the GSSAPI one if the latter is available as it is considered to be the source implementation for Linux. In this case if you need to use NTLM with this mode you have 3 options:

If you are able to do at least step 2 for your tests to ensure that the pyspnego NTLM implementation is all good. From what I can see it should work.

jborean93 commented 2 years ago

Samba also sends MSV_AV_SINGLE_HOST but I don't know if it's related.

I'm somewhat surprised it sends that, the value here is somewhat undocumented but it's mostly used for loopback authentication. I don't think it really applies here.

simo5 commented 2 years ago

Would you be able to test with gssntlssmp from the main branch where this should be fixed?

jborean93 commented 2 years ago

Yep, was planning on building the main branch and testing that my code correctly passes the SPN to gss-ntlmssp sometime tomorrow. Will also enable that policy myself to verify the settings work with it as well.

grawity commented 2 years ago

Thanks for the information – I just tried latest gss-ntlmssp from Git, and it indeed sends the full cifs/foo.example.com SPN, but unfortunately that still doesn't work and I get the same "Access denied".

I'm guessing this is because it now also sets the UNTRUSTED_SPN_SOURCE flag in MSV_AV_FLAGS, and if I'm reading the NTLM spec right, this makes the server pretend no SPN was sent at all.

If I uninstall gss-ntlmssp and let pyspnego do things on its own, NTLM indeed works.

simo5 commented 2 years ago

This is interesting, @grawity could you open an issue in gssntlmssp about this problem?

I tried to be conservative and not claim anything trusted because it was unclear to me what that flag meant. @jborean93 are you aware of a MS doc that clearly explain what it means for a name to be trusted vs untrusted?

grawity commented 2 years ago

I would guess that it means the SPN was obtained from the server itself – there's a field in "negHints" where modern systems send not_defined_in_RFC4178@please_ignore but I faintly remember that old servers, maybe Win2000 or XP, used to advertise their actual SPN there.

On the other hand, if the SPN was directly derived from the user-specified URL or UNC path, then it would make sense for it to be "trusted", following SSL/TLS logic.

simo5 commented 2 years ago

That would seem odd, my thinking was that untrusted may be a name derived by a reverse DNS query for example... but that is one reason why I set untrusted I did not want to claim anything that I was not sure I should claim. Documentation is something I really need to be sure I can set this appropriately. And I would like to do it before making a release.

jborean93 commented 2 years ago

@jborean93 are you aware of a MS doc that clearly explain what it means for a name to be trusted vs untrusted?

I've briefly seen it before but cannot remember how it is done through SSPI but it makes sense that having it set also still fails. Let me do some digging and I'll get back to you.

jborean93 commented 2 years ago

Closing as per the resolution in https://github.com/gssapi/gss-ntlmssp/issues/67. Have tested the proposed fix and things work just fine.