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
37 changes: 36 additions & 1 deletion documentation/modules/auxiliary/admin/kerberos/forge_ticket.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Kerberos Ticket Forging (Golden/Silver tickets)

The `auxiliary/admin/kerberos/forge_ticket` module allows the forging of a golden or silver ticket.
The `auxiliary/admin/kerberos/forge_ticket` module allows the forging of a golden, silver, diamond or sapphire ticket.

## Vulnerable Application

Expand All @@ -12,6 +12,8 @@ There are two kind of actions the module can run:

1. **FORGE_SILVER** - Forge a Silver ticket - forging a service ticket. [Default]
2. **FORGE_GOLDEN** - Forge a Golden ticket - forging a ticket granting ticket.
3. **FORGE_DIAMOND** - Forge a Diamond ticket - forging a ticket granting ticket by copying the PAC of another user.
4. **FORGE_SAPPHIRE** - Forge a Golden ticket - forging a ticket granting ticket by copying the PAC of a particular user, using the S4U2Self+U2U trick.

## Pre-Verification steps

Expand Down Expand Up @@ -199,6 +201,39 @@ export KRB5CCNAME=/Users/user/.msf4/loot/20220901132003_default_192.168.123.13_k
python3 $code/impacket/examples/smbexec.py 'adf3.local/[email protected]' -dc-ip 192.168.123.13 -k -no-pass
```

### Forging Diamond ticket

A diamond ticket is just a golden ticket (thus requiring knowledge of the krbtgt hash), with an attempt to be stealthier, by:

- Performing an AS-REQ request to retrieve a TGT for any user
- Using the krbtgt hash to decrypt the real ticket
- Setting properties of the forged PAC to mirror those in the valid TGT
- Encrypting the forged ticket with the krbtgt hash

The primary requirement of a Diamond ticket is the same: knowledge of the krbtgt hash of the domain.
The `DOMAIN_SID` property is not required, as this is retrieved from the valid TGT.

To perform the first step (retrieving the TGT), you must provide sufficient information to authenticate to the domain
(i.e. `RHOST`, `USERNAME` and `PASSWORD`).

### Forging Sapphire ticket

A sapphire ticket is similar to a Diamond ticket, in that it retrieves a real TGT, and copies data from that PAC onto the forged ticket. However,
instead of using the ticket retrieved in the initial authentication, an additional step is performed to retrieve a PAC for another (presumably
high-privilege) user:

- Authenticating to the KDC
- Using the S4U2Self and U2U extensions to request a TGS for a high-privilege user (this mirrors what the real user's PAC would look like, but the ticket is unusable in high-privilege contexts)
- Decrypt this information
- Setting properties of the forged PAC to mirror those in the valid TGT
- Encrypting the forged ticket with the krbtgt hash

The primary requirement of a Sapphire ticket is the same as for Golden and Diamond tickets: knowledge of the krbtgt hash of the domain.
The `DOMAIN_SID` and `DOMAIN_RID` properties are not required, as this is retrieved from the valid TGT.

To perform the first step (retrieving the TGT), you must provide sufficient information to authenticate to the domain
(i.e. `RHOST`, `USERNAME` and `PASSWORD`).

### Common Mistakes

**Invalid hostname**
Expand Down
2 changes: 1 addition & 1 deletion lib/msf/core/exploit/remote/kerberos/auth_brute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def attempt_kerberos_logins

# Accounts that have 'Do not require Kerberos preauthentication' enabled, will receive an ASREP response with a
# ticket present without requiring a password. This can be cracked offline.
if !proof.preauth_required
if proof.decrypted_part.nil?
print_good("#{peer} - User: #{format_user(user)} does not require preauthentication. Hash: #{hash}")
else
print_good("#{peer} - User found: #{format_user(user)} with password #{password}. Hash: #{hash}")
Expand Down
62 changes: 54 additions & 8 deletions lib/msf/core/exploit/remote/kerberos/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,43 @@ def send_request_tgt(options = {})

# 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
pa_data = initial_as_res.pa_data
etype_entries = pa_data.find {|entry| entry.type == Rex::Proto::Kerberos::Model::PreAuthType::PA_ETYPE_INFO2}
if password.nil? && key.nil?
decrypted_part = nil
krb_enc_key = nil
else
# Let's try to check the password
server_ciphers = etype_entries.decoded_value
# Should only have one etype
etype_info = server_ciphers.etype_info2_entries[0]
if password
enc_key, salt = get_enc_key_from_password(password, etype_info)
elsif key
enc_key = key
end
begin
decrypted_part = decrypt_kdc_as_rep_enc_part(
initial_as_res,
enc_key,
)
krb_enc_key = {
enctype: etype_info.etype,
key: enc_key,
salt: salt
}
rescue ::Rex::Proto::Kerberos::Model::Error::KerberosError
# It's as if it were an invalid password
decrypted_part = nil
krb_enc_key = nil
end
end

return Msf::Exploit::Remote::Kerberos::Model::TgtResponse.new(
as_rep: initial_as_res,
preauth_required: false,
decrypted_part: nil,
krb_enc_key: nil,
decrypted_part: decrypted_part,
krb_enc_key: krb_enc_key
)
end

Expand Down Expand Up @@ -334,12 +366,7 @@ def send_request_tgt(options = {})
selected_etype = selected_etypeinfo.etype

if password
salt = selected_etypeinfo.salt
salt = salt.dup.force_encoding('utf-8') if salt
params = selected_etypeinfo.s2kparams

encryptor = Rex::Proto::Kerberos::Crypto::Encryption::from_etype(selected_etype)
enc_key = encryptor.string_to_key(password, salt, params: params)
enc_key, salt = get_enc_key_from_password(password, selected_etypeinfo)
elsif key
raise ArgumentError.new('Encryption key provided without one offered encryption type') unless options[:offered_etypes]&.length == 1
enc_key = key
Expand Down Expand Up @@ -401,6 +428,25 @@ def send_request_tgt(options = {})
def framework_module
self
end

private

#
# Construct the encryption key based on the etype_info passed
# @param password [String] The password to generate an encryption key for
# @param etype_info [Rex::Proto::Kerberos::Model::PreAuthEtypeInfo2Entry]
# @return [Array] The encryption key and the salt
#
def get_enc_key_from_password(password, etype_info)
salt = etype_info.salt
salt = salt.dup.force_encoding('utf-8') if salt
params = etype_info.s2kparams

encryptor = Rex::Proto::Kerberos::Crypto::Encryption::from_etype(etype_info.etype)
enc_key = encryptor.string_to_key(password, salt, params: params)

[enc_key, salt]
end
end
end
end
Expand Down
50 changes: 40 additions & 10 deletions lib/msf/core/exploit/remote/kerberos/client/pac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def build_pa_pac_request(opts = {})
# @option opts [Array<String>] :extra_sids An array of extra sids, Ex: `['S-1-5-etc-etc-519']`
# @option opts [String] :realm
# @option opts [String] :domain_id the domain SID Ex: S-1-5-21-1755879683-3641577184-3486455962
# @option opts [Time] :logon_time
# @option opts [Time] :auth_time
# @option opts[String] :checksum_enc_key Encryption key for calculating the checksum
# @option opts[Boolean] :is_golden Include requestor and pac attributes in the PAC (needed for golden tickets; not for silver)
# @return [Rex::Proto::Kerberos::Pac::Krb5Pac]
Expand All @@ -51,27 +51,52 @@ def build_pac(opts = {})
primary_group_id = opts[:group_id] || Rex::Proto::Kerberos::Pac::DOMAIN_USERS
group_ids = opts[:group_ids] || [Rex::Proto::Kerberos::Pac::DOMAIN_USERS]
extra_sids = opts[:extra_sids] || []
domain_name = opts[:realm] || ''
logon_domain_name = opts[:logon_domain_name] || opts[:realm] || ''
logon_count = opts.fetch(:logon_count) { 0 }
password_last_set = opts.fetch(:password_last_set) { nil }
domain_id = opts[:domain_id] || Rex::Proto::Kerberos::Pac::NT_AUTHORITY_SID
logon_time = opts[:logon_time] || Time.now
auth_time = opts[:auth_time] || Time.now
checksum_type = opts[:checksum_type] || Rex::Proto::Kerberos::Crypto::Checksum::RSA_MD5
ticket_checksum = opts[:ticket_checksum] || nil
is_golden = opts.fetch(:is_golden) { true }
base_vi = opts[:base_verification_info]
upn_dns_info_pac_element = opts[:upn_dns_info_pac_element]

validation_info = Rex::Proto::Kerberos::Pac::Krb5ValidationInfo.new(
logon_time: logon_time,
obj_opts = {
logon_time: auth_time,
effective_name: user_name,
user_id: user_id,
primary_group_id: primary_group_id,
logon_domain_name: domain_name,
logon_domain_name: logon_domain_name,
logon_domain_id: domain_id,
logon_count: logon_count,
full_name: '',
logon_script: '',
profile_path: '',
home_directory: '',
home_directory_drive: '',
logon_server: ''
)
logon_server: '',
password_last_set: password_last_set
}
unless base_vi.nil?
obj_opts.merge({
full_name: base_vi.full_name,
logon_script: base_vi.logon_script,
profile_path: base_vi.profile_path,
home_directory: base_vi.home_directory,
home_directory_drive: base_vi.home_directory_drive,
logon_server: base_vi.logon_server,
logon_count: base_vi.logon_count,
bad_password_count: base_vi.bad_password_count,
user_account_control: base_vi.user_account_control,
sub_auth_status: base_vi.sub_auth_status,
last_successful_i_logon: base_vi.last_successful_i_logon,
last_failed_i_logon: base_vi.last_failed_i_logon,
failed_i_logon_count: base_vi.failed_i_logon_count
})
end

validation_info = Rex::Proto::Kerberos::Pac::Krb5ValidationInfo.new(**obj_opts)
validation_info.group_ids = group_ids
if extra_sids && extra_sids.length > 0
validation_info.extra_sids = extra_sids.map do |sid|
Expand All @@ -84,7 +109,7 @@ def build_pac(opts = {})
)

client_info = Rex::Proto::Kerberos::Pac::Krb5ClientInfo.new(
client_id: logon_time,
client_id: auth_time,
name: user_name
)

Expand All @@ -109,11 +134,16 @@ def build_pac(opts = {})
client_info
]

unless upn_dns_info_pac_element.nil?
pac_elements.append(upn_dns_info_pac_element)
end

if is_golden
# These PAC elements are required for golden tickets in post-October 2022 systems
pac_elements.append(
pac_requestor,
pac_attributes)
pac_attributes
)
end

pac_elements.append(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,8 @@ def s4u2proxy(credential, options = {})

# Request a service ticket to a user on behalf of themselves
# This is mostly useful for PKINIT to recover the NT hash
# Can combine this with S4U2Self by providing an :impersonate option
# to retrieve a PAC for any account, i.e. Sapphire Ticket attack
#
# @see https://learn.microsoft.com/en-us/archive/blogs/openspecification/how-kerberos-user-to-user-authentication-works
#
Expand Down Expand Up @@ -519,6 +521,16 @@ def u2uself(credential, options = {})
additional_tickets: [ticket]
}

if options[:impersonate]
tgs_options[:pa_data] = build_pa_for_user(
{
username: options[:impersonate],
session_key: session_key,
realm: self.realm
}
)
end

request_service_ticket(
session_key,
ticket,
Expand Down Expand Up @@ -578,7 +590,7 @@ def authenticate_via_kdc(options = {})
)
end

if !tgt_result.preauth_required
if tgt_result.decrypted_part.nil? && !tgt_result.preauth_required
raise ::Rex::Proto::Kerberos::Model::Error::KerberosError.new(
'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.',
)
Expand Down
Loading