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

Forging diamond and sapphire tickets #18560

Merged
merged 12 commits into from
Nov 28, 2023

Conversation

smashery
Copy link
Contributor

@smashery smashery commented Nov 21, 2023

This implements Diamond and Sapphire tickets in Metasploit. These are both like golden tickets, but in some circumstances may be preferable for reasons of stealth. Both require the same preconditions as Golden tickets (knowledge of krbtgt), but base the ticket off real tickets from the environment.

Diamond ticket: A TGT is requested (low-priv is fine), and the krbtgt hash is used to update it to be golden ticket-like.
Sapphire ticket: S4U2 and U2U logic is used to request a PAC for another user; we then copy those parameters across to our fake ticket. In this case, the PAC should look very realistic; although in practice, it needs to be different because of the new PAC elements added in KB5008380, which need to change.

Verification

  • Verify that the Diamond ticket's PAC is similar to a real one. You can use the inspect_ticket module for this. We would expect it to have a lot of fields filled in which might otherwise be empty in a golden ticket.
  • Compare sapphire PAC to a real TGT ticket for that user (should be nearly identical, other than things that need to be per-ticket, such as signatures, auth times, etc.)
  • Test with NTHASH on an older Windows system (we wouldn't expect it to work on Server 2022, as it seems to be rejecting them. I've added a warning for this)
  • Test AES_KEY on a fully patched Server 2022 system.

Bugfixes

As part of this work, this PR also fixes two bugs.

There was an intermittent issue with Golden/Silver tickets. The PAC's logon_time value needs to be identical to the ticket's auth_time value; otherwise, the ticket is rejected with KRB_AP_ERR_MODIFIED. Both logon_time value and the auth_time values are set with independent calls to Time.now. This could be several milliseconds difference. Because the KerberosTime type has a resolution of 1 second, usually both calls to Time.now would fall within the same second; but it was possible for the two calls to fall on either side of a second boundary, thus getting them out of sync, and causing the Kerberos error. You could replicate this issue in the existing code by adding a 0.5 second sleep prior to generating the ticket. Sometimes using the ticket would succeed, and other times it would fail. By accessing the current time only once, and using that same value in all locations, the bug is fixed.

There was also a bug in Krb5UpnDnsInfo structure. The cause, as best as I understand it, is that a Krb5PacInfoBuffer, which is a delayed_io object, relies on asking another object with delayed_io (Krb5UpnDnsInfo) how long it is (num_bytes). This meant that the code calculated the size of Krb5UpnDnsInfo without its strings. I created to unit test to show the effect of this:

    it 'writes then reads back to its original state' do
      pac.assign(pac_elements: pac_elements)
      pac.sign!
      data = pac.to_binary_s
      result = Rex::Proto::Kerberos::Pac::Krb5Pac.read(data)
      expect(result).to eq(pac)
    end

Prior to this change, the test case fails with junk ending up in the fields:

Diff:
       @@ -87,8 +87,10 @@
                :sam_name_offset=>74,
                :sid_length=>28,
                :sid_offset=>82,
       -        :upn=>"[email protected]",
       -        :dns_domain_name=>"DEMO.LOCAL",
       +        :upn=>
       +         "\a\u0000\u9C94\u0254\u16A4\uA2A4\u0414\uE2FA\u40BF\uB5F2\u0000\u0000\a\u0000\uE687",
       +        :dns_domain_name=>
       +         "\u0B4D\u7080\u4738\u4587\u48C7\u087D\u58BA\u0000\u0000L",
                :sam_name=>"juan",
                :sid=>"S-1-5-21-1755879683-3641577184-3486455962-1038"},
              :padding=>"\x00\x00\x00\x00"}},

This bug was probably never experienced in MSF, because the structure was never written. With sapphire tickets, I needed to add them (to make the PAC as identical as we could).

@smashery smashery changed the title New forge tickets Forging diamond and sapphire tickets Nov 21, 2023
@smcintyre-r7 smcintyre-r7 self-assigned this Nov 21, 2023
Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the most part things look good. I just left a few comments but the themes were updating docs, catching some additional exceptions in the module and fixing an error when VERBOSE was printing the ticket.

I was able to test both new actions are working as intended.

modules/auxiliary/admin/kerberos/forge_ticket.rb Outdated Show resolved Hide resolved
modules/auxiliary/admin/kerberos/forge_ticket.rb Outdated Show resolved Hide resolved
modules/auxiliary/admin/kerberos/forge_ticket.rb Outdated Show resolved Hide resolved
modules/auxiliary/admin/kerberos/forge_ticket.rb Outdated Show resolved Hide resolved
Copy link

Thanks for your pull request! Before this can be merged, we need the following documentation for your module:

@@ -288,7 +289,7 @@ def send_request_tgt(options = {})
initial_as_res = send_request_as(req: initial_as_req)

# If we receive an AS_REP response immediately, no-preauthentication was required and we can return immediately
if initial_as_res.msg_type == Rex::Proto::Kerberos::Model::AS_REP
if initial_as_res.msg_type == Rex::Proto::Kerberos::Model::AS_REP && stop_if_preauth_not_required
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in what situations do we want to stop_if_preauth_not_required?
I'm also not sure I expect to continue as we did previously if stop_if_preauth_not_required is true to me that says if pre auth isn't required something has gone wrong and we should stop rather than return the TgtResponse but that might just be me

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One module that wants to stop in this circumstance is kerberos_login: that one is designed to return immediately, to avoid unnecessary requests: no point in brute forcing if you can just crack offline and avoid the risk of detection/locking out accounts.

But I think there is a better behaviour available to us in this situation: rather than re-sending the request, we could attempt to decrypt it using the password or key provided. Essentially, the behaviour should mirror what a real kerberos client would do:

  • If we have been provided a password, just attempt to decrypt the ticket.
  • If success, return as normal (the caller is none the wiser, unless they check preauth_required)
  • If fail, return with a nil decrypted_part as per before, with preauth_required set to false
  • If we have been provided a key, attempt to decrypt with that key
  • If success, return as normal (the caller is none the wiser, unless they check preauth_required)
  • If fail, return with a nil decrypted_part as per before, with preauth_required set to false

I've implemented this in place of the stop_if_preauth_not_required - now, instead of directing the function to try the password regardless (and dealing with the possibility of an error), the caller is now responsible for dealing with the possibility of a nil decrypted_part (and with preauth_required set to false).

With pre-auth not required, wrong password:

msf6 auxiliary(admin/kerberos/get_ticket) > run rhosts=192.168.20.210 domain=pod8.lan password=Password123 username=no.preauth
[*] Running module against 192.168.20.210

[*] 192.168.20.210:88 - Getting TGT for [email protected]
[-] Auxiliary aborted due to failure: unknown: Kerberos ticket does not require preauthentication. It is not possible to decrypt the encrypted message to request further TGS tickets. Try cracking the password via AS-REP Roasting techniques.
[*] Auxiliary module execution completed

With pre-auth not required, correct password:

msf6 auxiliary(admin/kerberos/get_ticket) > run rhosts=192.168.20.210 domain=pod8.lan password=Password123! username=no.preauth
[*] Running module against 192.168.20.210

[*] 192.168.20.210:88 - Getting TGT for [email protected]
[+] 192.168.20.210:88 - Received a valid TGT-Response
[*] 192.168.20.210:88 - TGT MIT Credential Cache ticket saved to /home/smash/.msf4/loot/20231127125836_default_192.168.20.210_mit.kerberos.cca_103727.bin
[*] Auxiliary module execution completed

Other happy paths:

msf6 auxiliary(admin/kerberos/get_ticket) > run rhosts=192.168.20.210 domain=pod8.lan password=Password123 username=nonexistent
[*] Running module against 192.168.20.210

[*] 192.168.20.210:88 - Getting TGT for [email protected]
[-] Auxiliary aborted due to failure: unknown: Kerberos Error - KDC_ERR_C_PRINCIPAL_UNKNOWN (6) - Client not found in Kerberos database
[*] Auxiliary module execution completed
msf6 auxiliary(admin/kerberos/get_ticket) > run rhosts=192.168.20.210 domain=pod8.lan password=Password123 username=Administrator
[*] Running module against 192.168.20.210

[*] 192.168.20.210:88 - Getting TGT for [email protected]
[-] Auxiliary aborted due to failure: unknown: Kerberos Error - KDC_ERR_PREAUTH_FAILED (24) - Pre-authentication information was invalid
[*] Auxiliary module execution completed
msf6 auxiliary(admin/kerberos/get_ticket) > run rhosts=192.168.20.210 domain=pod8.lan password=Password123! username=Administrator
[*] Running module against 192.168.20.210

[*] 192.168.20.210:88 - Getting TGT for [email protected]
[+] 192.168.20.210:88 - Received a valid TGT-Response
[*] 192.168.20.210:88 - TGT MIT Credential Cache ticket saved to /home/smash/.msf4/loot/20231127130111_default_192.168.20.210_mit.kerberos.cca_787617.bin
[*] Auxiliary module execution completed

Because the stop_if_preauth_not_required flag was added just for this module, I don't believe it should affect any other modules (except now providing better usability; possibly fixing some bugs where the module failed to take the possibility of pre-auth into account).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added tests for these cases in client_spec.rb

modules/auxiliary/admin/kerberos/forge_ticket.rb Outdated Show resolved Hide resolved
)

tgs_ticket, tgs_auth = kerberos_authenticator.u2uself(credential, impersonate: datastore['USER'])
ticket = modify_ticket(tgs_ticket, tgs_auth, datastore['USER'], datastore['USER_RID'], datastore['DOMAIN'], extra_sids, session_key.value, enc_type, enc_key, true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditions above suggest that USER_RID isn't used for the sapphire ticket.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - I've passed in nil for sapphire, since it should retrieve this from the PAC.

Copy link
Contributor

@smcintyre-r7 smcintyre-r7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything looks good to me now. I tested the module in a bunch of scenarios and all of the issues I found have been fixed. I tested the module on Server 2012, Server 2019 and Server 2022. In all cases it worked as intended. I also retested the kerberos_login module because of the new changes in the kerberos client.

I used the changes #18565 to automate testing the TGT after it was forged.

test_krbtgt.rc.txt

test_krbtgt_output.txt

@smcintyre-r7 smcintyre-r7 merged commit 708c795 into rapid7:master Nov 28, 2023
57 checks passed
@smcintyre-r7
Copy link
Contributor

Release Notes

This updates the existing Kerberos ticket-forging module with new actions for forging tickets with fields copied from ones issued by the legitimate KDC using the Diamond and Sapphire techniques.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement rn-enhancement release notes enhancement
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

6 participants