Skip to content

Commit 393c286

Browse files
committed
Add the MsGkdi mixin
1 parent 13c6e28 commit 393c286

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
###
2+
#
3+
# This mixin provides methods for interacting with Microsoft Active Directory
4+
# Group Key Distribution Service
5+
#
6+
# -*- coding: binary -*-
7+
8+
require 'ruby_smb'
9+
require 'ruby_smb/error'
10+
require 'ruby_smb/dcerpc/client'
11+
12+
module Msf
13+
14+
module Exploit::Remote::MsGkdi
15+
16+
KDS_SERVICE_LABEL = "KDS service\0".encode('UTF-16LE').force_encoding('ASCII-8BIT')
17+
KDS_PUBLIC_KEY_LABEL = "KDS public key\0".encode('UTF-16LE').force_encoding('ASCII-8BIT')
18+
19+
class GkdiGroupKeyIdentifier < BinData::Record
20+
endian :little
21+
22+
uint32 :version
23+
uint8_array :magic, initial_length: 4, initial_value: [ 0x4b, 0x44, 0x53, 0x5b ]
24+
uint32 :dw_flags
25+
uint32 :l0_index
26+
uint32 :l1_index
27+
uint32 :l2_index
28+
uuid :root_key_identifier
29+
30+
uint32 :cb_context
31+
uint32 :cb_domain_name
32+
uint32 :cb_forest_name
33+
34+
uint8_array :context, initial_length: :cb_context
35+
stringz16 :domain_name
36+
stringz16 :forest_name
37+
end
38+
39+
def gkdi_get_kek(opts = {})
40+
gkdi = connect_gkdi(opts)
41+
42+
key_identifier = opts.fetch(:key_identifier)
43+
gke = gkdi.gkdi_get_key(
44+
opts.fetch(:security_descriptor),
45+
key_identifier[:root_key_identifier].to_s,
46+
key_identifier[:l0_index],
47+
key_identifier[:l1_index],
48+
key_identifier[:l2_index]
49+
)
50+
51+
gkdi_compute_kek(gke, key_identifier)
52+
end
53+
54+
def connect_gkdi(opts = {})
55+
vprint_status('Connecting to Group Key Distribution (GKDI) Protocol')
56+
dcerpc_client = RubySMB::Dcerpc::Client.new(
57+
opts.fetch(:rhost) { rhost },
58+
RubySMB::Dcerpc::Gkdi,
59+
username: opts.fetch(:username) { datastore['USERNAME'] },
60+
password: opts.fetch(:password) { datastore['PASSWORD'] }
61+
)
62+
bind_gkdi(dcerpc_client)
63+
64+
dcerpc_client
65+
end
66+
67+
def bind_gkdi(dcerpc_client)
68+
dcerpc_client.connect
69+
vprint_status('Binding to GKDI...')
70+
dcerpc_client.bind(
71+
endpoint: RubySMB::Dcerpc::Gkdi,
72+
auth_level: RubySMB::Dcerpc::RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
73+
auth_type: RubySMB::Dcerpc::RPC_C_AUTHN_WINNT
74+
)
75+
vprint_status('Bound to GKDI')
76+
end
77+
78+
def gkdi_compute_kek(gke, key_identifier)
79+
l2_key = gkdi_compute_l2_key(gke, key_identifier)
80+
81+
if (key_identifier.dw_flags & 1) == 0
82+
raise NotImplementedError.new("only public-private key pairs are supported")
83+
end
84+
85+
secret = gkdi_compute_kek_pkey(gke, key_identifier, l2_key)
86+
Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
87+
secret,
88+
32,
89+
gke.kdf_parameters.hash_algorithm_name.encode,
90+
label: KDS_SERVICE_LABEL,
91+
context: KDS_PUBLIC_KEY_LABEL
92+
).first
93+
end
94+
95+
def gkdi_compute_kek_pkey(gke, key_identifier, l2_key)
96+
private_key = Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
97+
l2_key,
98+
(gke.private_key_length / 8.0).ceil,
99+
gke.kdf_parameters.hash_algorithm_name.encode,
100+
context: gke.secret_agreement_algorithm.to_binary_s,
101+
label: KDS_SERVICE_LABEL
102+
).first
103+
104+
unless (algorithm = gke.secret_agreement_algorithm.encode) == 'DH'
105+
raise NotImplementedError.new("unsupported secret agreement algorithm: #{algorithm}")
106+
end
107+
108+
ffc_dh_key = RubySMB::Dcerpc::Gkdi::GkdiFfcDhKey.read(key_identifier.context.pack('C*'))
109+
base = Rex::Crypto.bytes_to_int(ffc_dh_key.public_key.pack('C*'))
110+
exp = Rex::Crypto.bytes_to_int(private_key)
111+
mod = Rex::Crypto.bytes_to_int(ffc_dh_key.field_order.pack('C*'))
112+
113+
key_material = Rex::Crypto.int_to_bytes(base.pow(exp, mod))
114+
gkdi_kdf_counter(32, key_material, "SHA512\0".encode('UTF-16LE').force_encoding('ASCII-8BIT') + KDS_PUBLIC_KEY_LABEL + KDS_SERVICE_LABEL)
115+
end
116+
117+
def gkdi_compute_l2_key(gke, key_identifier)
118+
unless (algorithm = gke.kdf_algorithm.encode) == 'SP800_108_CTR_HMAC'
119+
# see: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-gkdi/5d373568-dd68-499b-bd06-a3ce16ca7117
120+
raise NotImplementedError.new("unsupported key derivation function algorithm: #{algorithm}")
121+
end
122+
123+
l1 = gke.l1_index.to_i
124+
l1_key = gke.l1_key.pack('C*')
125+
l2 = gke.l2_index.to_i
126+
l2_key = gke.l2_key.pack('C*')
127+
128+
reseed_l2 = (l2 == 31 || l1 != key_identifier.l1_index)
129+
130+
l1 -= 1 if l2 != 31 && l1 != key_identifier.l1_index
131+
132+
while l1 != key_identifier.l1_index
133+
reseed_l2 = true
134+
l1 -= 1
135+
136+
l1_key = Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
137+
l1_key,
138+
64,
139+
gke.kdf_parameters.hash_algorithm_name.encode,
140+
context: gke.root_key_identifier.to_binary_s + [ gke.l0_index, l1, -1 ].pack('l<l<l<'),
141+
label: KDS_SERVICE_LABEL
142+
).first
143+
end
144+
145+
if reseed_l2
146+
l2 = 31
147+
148+
l2_key = Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
149+
l1_key,
150+
64,
151+
gke.kdf_parameters.hash_algorithm_name.encode,
152+
context: gke.root_key_identifier.to_binary_s + [ gke.l0_index, l1, l2 ].pack('l<l<l<'),
153+
label: KDS_SERVICE_LABEL
154+
).first
155+
end
156+
157+
while l2 != key_identifier.l2_index
158+
l2 -= 1
159+
160+
l2_key = Rex::Crypto::KeyDerivation::NIST_SP_800_108.counter_hmac(
161+
l2_key,
162+
64,
163+
gke.kdf_parameters.hash_algorithm_name.encode,
164+
context: gke.root_key_identifier.to_binary_s + [ gke.l0_index, l1, l2 ].pack('l<l<l<'),
165+
label: KDS_SERVICE_LABEL
166+
).first
167+
end
168+
169+
l2_key
170+
end
171+
172+
# this is mostly a variation on NIST SP 800-108
173+
def gkdi_kdf_counter(length, key_material, other_info)
174+
prf = -> (data) { OpenSSL::Digest.new('SHA256', data).digest }
175+
key_block = ''
176+
177+
counter = 0
178+
while key_block.length < length
179+
counter += 1
180+
raise RangeError.new('counter overflow') if counter > 0xffffffff
181+
182+
info = [ counter ].pack('L>') + key_material + other_info
183+
key_block << prf.call(info)
184+
end
185+
186+
key_block[...length]
187+
end
188+
end
189+
190+
end

0 commit comments

Comments
 (0)