CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/lib/rex/proto/kerberos/crypto/rc4_hmac.rb
Views: 11766
1
# -*- coding: binary -*-
2
3
require 'rex/text'
4
5
module Rex
6
module Proto
7
module Kerberos
8
module Crypto
9
class Rc4Hmac
10
include Rex::Proto::Kerberos::Crypto::Utils
11
include Rex::Proto::Gss::Asn1
12
MAC_SIZE = 16
13
CONFOUNDER_SIZE = 8
14
PADDING_SIZE = 1
15
16
# Derive an encryption key based on a password and salt for the given cipher type
17
#
18
# @param password [String] The password to use as the basis for key generation
19
# @param salt [String] Ignored for this encryption algorithm
20
# @param params [String] Unused for this encryption type
21
# @return [String] The derived key
22
def string_to_key(password, salt=nil, params: nil)
23
raise Rex::Proto::Kerberos::Model::Error::KerberosError, 'Params not supported for RC4_HMAC' unless params == nil
24
25
unicode_password = password.encode('utf-16le')
26
password_digest = OpenSSL::Digest.digest('MD4', unicode_password)
27
end
28
29
# Use this class's encryption routines to create a checksum of the data based on the key and message type
30
#
31
# @param key [String] the key to use to generate the checksum
32
# @param msg_type [Integer] type of kerberos message
33
# @param data [String] the data to checksum
34
# @return [String] the generated checksum
35
def checksum(key, msg_type, data)
36
ksign = OpenSSL::HMAC.digest('MD5', key, "signaturekey\x00")
37
md5_hash = Rex::Text.md5_raw(usage_str(msg_type) + data)
38
39
ksign = OpenSSL::HMAC.digest('MD5', ksign, md5_hash)
40
end
41
42
# Decrypts the cipher using RC4-HMAC schema
43
# https://datatracker.ietf.org/doc/rfc4757/
44
#
45
# @param ciphertext [String] the data to decrypt
46
# @param key [String] the key to decrypt
47
# @param msg_type [Integer] type of kerberos message
48
# @return [String] the decrypted cipher
49
# @raise [Rex::Proto::Kerberos::Model::Error::KerberosError] if decryption doesn't succeed
50
def decrypt(ciphertext, key, msg_type)
51
unless ciphertext && ciphertext.length > MAC_SIZE
52
raise Rex::Proto::Kerberos::Model::Error::KerberosError, 'RC4-HMAC decryption failed'
53
end
54
55
checksum = ciphertext[0, MAC_SIZE]
56
data = ciphertext[MAC_SIZE, ciphertext.length - 1]
57
58
k1 = OpenSSL::HMAC.digest('MD5', key, usage_str(msg_type))
59
k3 = OpenSSL::HMAC.digest('MD5', k1, checksum)
60
61
cipher = OpenSSL::Cipher.new('rc4')
62
cipher.decrypt
63
cipher.key = k3
64
decrypted = cipher.update(data) + cipher.final
65
66
if OpenSSL::HMAC.digest('MD5', k1, decrypted) != checksum
67
raise ::Rex::Proto::Kerberos::Model::Error::KerberosError, 'RC4-HMAC decryption failed, incorrect checksum verification'
68
end
69
70
# Expect the first CONFOUNDER_SIZE bytes to be the confounder
71
raise ::Rex::Proto::Kerberos::Model::Error::KerberosDecodingError, 'EncryptedData failed to decrypt' if decrypted.length < CONFOUNDER_SIZE
72
73
# Skip the confounder when returning
74
decrypted[CONFOUNDER_SIZE,decrypted.length]
75
end
76
77
alias decrypt_asn1 decrypt
78
79
# Encrypts the cipher using RC4-HMAC schema
80
# https://datatracker.ietf.org/doc/rfc4757/
81
#
82
# @param plaintext [String] the data to encrypt
83
# @param key [String] the key to encrypt
84
# @param msg_type [Integer] type of kerberos message
85
# @param confounder [String] Optionally force the confounder to a specific value
86
# @return [String] the encrypted data
87
def encrypt(plaintext, key, msg_type, confounder: nil)
88
k1 = OpenSSL::HMAC.digest('MD5', key, usage_str(msg_type))
89
90
confounder = Random.urandom(CONFOUNDER_SIZE) if confounder == nil
91
data_encrypt = confounder + plaintext
92
93
checksum = OpenSSL::HMAC.digest('MD5', k1, data_encrypt)
94
95
k3 = OpenSSL::HMAC.digest('MD5', k1, checksum)
96
97
cipher = OpenSSL::Cipher.new('rc4')
98
cipher.encrypt
99
cipher.key = k3
100
encrypted = cipher.update(data_encrypt) + cipher.final
101
102
res = checksum + encrypted
103
res
104
end
105
106
def gss_unwrap(ciphertext, key, expected_sequence_number, is_initiator, opts={})
107
# Always 32-bit sequence number
108
expected_sequence_number &= 0xFFFFFFFF unless expected_sequence_number.nil?
109
110
mech_id, ciphertext = unwrap_pseudo_asn1(ciphertext)
111
112
raise Rex::Proto::Kerberos::Model::Error::KerberosError unless ciphertext.length > 0x20
113
header = ciphertext[0,8]
114
tok_id, alg, seal_alg, filler = header.unpack('nnnn')
115
raise Rex::Proto::Kerberos::Model::Error::KerberosError, "Invalid token id: #{tok_id}" unless tok_id == 0x0201
116
raise Rex::Proto::Kerberos::Model::Error::KerberosError, "Invalid alg: #{alg}" unless alg == 0x1100
117
raise Rex::Proto::Kerberos::Model::Error::KerberosError, "Invalid seal_alg: #{seal_alg}" unless seal_alg == 0x1000
118
raise Rex::Proto::Kerberos::Model::Error::KerberosError, "Invalid filler: #{filler}" unless filler == 0xFFFF
119
120
encrypted_sequence_num = ciphertext[8,8]
121
eight_checksum_bytes = ciphertext[16,8]
122
encrypted_confounder = ciphertext[24,8]
123
emessage = ciphertext[32, ciphertext.length - 32]
124
125
kseq = OpenSSL::HMAC.digest('MD5', key.value, [0].pack('V'))
126
127
kseq = OpenSSL::HMAC.digest('MD5', kseq, eight_checksum_bytes)
128
129
cipher_seq = OpenSSL::Cipher.new('rc4')
130
cipher_seq.decrypt
131
cipher_seq.key = kseq
132
133
decrypted_sequence_num = cipher_seq.update(encrypted_sequence_num)
134
decrypted_sequence_num = decrypted_sequence_num.unpack('N')[0]
135
136
#raise Rex::Proto::Kerberos::Model::Error::KerberosError, 'Invalid sequence number' unless (decrypted_sequence_num == expected_sequence_number || expected_sequence_number.nil?)
137
138
klocal = xor_strings(key.value, "\xF0"*16)
139
kcrypt = OpenSSL::HMAC.digest('MD5', klocal, [0].pack('V'))
140
141
# Salt it with the sequence number
142
kcrypt = OpenSSL::HMAC.digest('MD5', kcrypt, [decrypted_sequence_num].pack('N'))
143
144
cipher = OpenSSL::Cipher.new('rc4')
145
cipher.encrypt
146
cipher.key = kcrypt
147
decrypted_confounder = cipher.update(encrypted_confounder)
148
149
plaintext = cipher.update(emessage)
150
151
chksum_input = usage_str(Rex::Proto::Kerberos::Crypto::KeyUsage::KRB_PRIV_ENCPART) + header + decrypted_confounder
152
ksign = OpenSSL::HMAC.digest('MD5', key.value, "signaturekey\x00")
153
sgn_cksum = Rex::Text.md5_raw(chksum_input+plaintext)
154
sgn_cksum = OpenSSL::HMAC.digest('MD5', ksign, sgn_cksum)
155
156
verification_eight_checksum_bytes = sgn_cksum[0,8]
157
raise Rex::Proto::Kerberos::Model::Error::KerberosError, 'Checksum error' unless verification_eight_checksum_bytes == eight_checksum_bytes
158
159
# Remove padding, if present (seems MS may not send it back?)
160
pad_char = plaintext[-1].ord
161
if 1 <= pad_char && pad_char <= 8
162
plaintext = plaintext[0, plaintext.length-pad_char]
163
end
164
165
plaintext
166
end
167
168
# @option options [Boolean] :dce_style Whether the interaction is a 3-leg DCERPC interaction
169
# @option options [Symbol] :rc4_pad_style How to do padding - either :single_byte or :eight_byte_aligned
170
def gss_wrap(plaintext, key, sequence_number, is_initiator, opts={})
171
dce_style = opts.fetch(:dce_style) { false }
172
pad_style = opts.fetch(:rc4_pad_style) { :single_byte }
173
# Always 32-bit sequence number
174
sequence_number &= 0xFFFFFFFF
175
176
# Header
177
tok_id = 0x0201
178
alg = 0x1100
179
seal_alg = 0x1000
180
filler = 0xFFFF
181
header = [tok_id, alg, seal_alg, filler].pack('nnnn')
182
183
# Add padding (see RFC1964 section 1.2.2.3)
184
# Some protocols (LDAP) only support a single byte and seem to fail otherwise
185
# Others (DRSR) only support 8-byte and seem to fail otherwise
186
# Some (WinRM) are lenient and are fine with either
187
#
188
# It's not entirely clear why
189
if pad_style == :single_byte
190
pad_num = 1
191
elsif pad_style == :eight_byte_aligned
192
pad_num = (8 - (plaintext.length % 8))
193
else
194
raise ArgumentError.new('Unknown pad_style setting')
195
end
196
197
plaintext += (pad_num.chr * pad_num)
198
199
send_seq = [sequence_number].pack('N')
200
# See errata on RFC4757
201
initiator_bytes = "\xFF" * 4
202
initiator_bytes = "\x00" * 4 if is_initiator
203
send_seq += initiator_bytes
204
205
confounder = Random.urandom(CONFOUNDER_SIZE)
206
chksum_input = usage_str(Rex::Proto::Kerberos::Crypto::KeyUsage::KRB_PRIV_ENCPART) + header + confounder
207
ksign = OpenSSL::HMAC.digest('MD5', key.value, "signaturekey\x00")
208
sgn_cksum = Rex::Text.md5_raw(chksum_input+plaintext)
209
210
klocal = xor_strings(key.value, "\xF0"*16)
211
kcrypt = OpenSSL::HMAC.digest('MD5', klocal, [0].pack('V'))
212
213
# Salt it with the sequence number
214
kcrypt = OpenSSL::HMAC.digest('MD5', kcrypt, [sequence_number].pack('N'))
215
216
cipher = OpenSSL::Cipher.new('rc4')
217
cipher.encrypt
218
cipher.key = kcrypt
219
encrypted_confounder = cipher.update(confounder)
220
221
encrypted = cipher.update(plaintext)
222
223
sgn_cksum = OpenSSL::HMAC.digest('MD5', ksign, sgn_cksum)
224
eight_checksum_bytes = sgn_cksum[0,8]
225
226
kseq = OpenSSL::HMAC.digest('MD5', key.value, [0].pack('V'))
227
228
kseq = OpenSSL::HMAC.digest('MD5', kseq, eight_checksum_bytes)
229
230
cipher_seq = OpenSSL::Cipher.new('rc4')
231
cipher_seq.encrypt
232
cipher_seq.key = kseq
233
234
encrypted_sequence_num = cipher_seq.update(send_seq)
235
236
token = header + encrypted_sequence_num + eight_checksum_bytes + encrypted_confounder
237
size_prior = (token+encrypted).length
238
239
if dce_style
240
wrapped_token = wrap_pseudo_asn1(
241
::Rex::Proto::Gss::OID_KERBEROS_5,
242
token
243
) + encrypted
244
else
245
wrapped_token = wrap_pseudo_asn1(
246
::Rex::Proto::Gss::OID_KERBEROS_5,
247
token + encrypted
248
)
249
end
250
asn1_length = wrapped_token.length - size_prior
251
token_length = asn1_length + token.length
252
253
[wrapped_token, token_length, pad_num]
254
end
255
256
#
257
# The number of bytes in the encrypted plaintext that precede the actual plaintext
258
#
259
def header_byte_count
260
MAC_SIZE + CONFOUNDER_SIZE
261
end
262
263
#
264
# The number of bytes in the encrypted plaintext that follow the actual plaintext
265
#
266
def trailing_byte_count
267
0
268
end
269
270
def calculate_encrypted_length(plaintext_len)
271
# We add 1-8 bytes of padding, per RFC1964 section 1.2.2.3
272
plaintext_len + (8 - (plaintext_len % 8))
273
end
274
275
private
276
277
def usage_str(msg_type)
278
usage_table = {
279
Rex::Proto::Kerberos::Crypto::KeyUsage::AS_REP_ENCPART => Rex::Proto::Kerberos::Crypto::KeyUsage::TGS_REP_ENCPART_SESSION_KEY,
280
Rex::Proto::Kerberos::Crypto::KeyUsage::GSS_ACCEPTOR_SIGN => Rex::Proto::Kerberos::Crypto::KeyUsage::KRB_PRIV_ENCPART
281
}
282
usage_mapped = usage_table.fetch(msg_type) { msg_type }
283
[usage_mapped].pack('V')
284
end
285
end
286
end
287
end
288
end
289
end
290
291