Various merge resolutions from master <- staging
* --ask option ported to new location * --version option now works * MSF version updated * All specs passingbug/bundler_fix
parent
1183c5cfeb
commit
149c3ecc63
|
@ -48,6 +48,9 @@ tags
|
|||
*.opensdf
|
||||
*.user
|
||||
|
||||
# Rails log directory
|
||||
/log
|
||||
|
||||
# ignore release/debug folders for exploits
|
||||
external/source/exploits/**/Debug
|
||||
external/source/exploits/**/Release
|
||||
|
|
|
@ -53,4 +53,4 @@ Style/StringLiterals:
|
|||
|
||||
Style/WordArray:
|
||||
Enabled: false
|
||||
Description: 'Metasploit prefers consistent use of []'
|
||||
Description: 'Metasploit prefers consistent use of []'
|
|
@ -3,6 +3,8 @@
|
|||
--exclude \.ut\.rb/
|
||||
--exclude \.ts\.rb/
|
||||
--files CONTRIBUTING.md,COPYING,HACKING,LICENSE
|
||||
app/**/*.rb
|
||||
lib/msf/**/*.rb
|
||||
lib/metasploit/**/*.rb
|
||||
lib/rex/**/*.rb
|
||||
plugins/**/*.rb
|
||||
|
|
79
Gemfile
79
Gemfile
|
@ -1,66 +1,53 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# Need 3+ for ActiveSupport::Concern
|
||||
gem 'activesupport', '>= 3.0.0', '< 4.0.0'
|
||||
# Needed for some admin modules (cfme_manageiq_evm_pass_reset.rb)
|
||||
gem 'bcrypt'
|
||||
# Needed for some admin modules (scrutinizer_add_user.rb)
|
||||
gem 'json'
|
||||
# Needed for Meterpreter on Windows, soon others.
|
||||
gem 'meterpreter_bins', '0.0.6'
|
||||
# Needed by msfgui and other rpc components
|
||||
gem 'msgpack'
|
||||
# Needed by anemone crawler
|
||||
gem 'nokogiri'
|
||||
# Needed by db.rb and Msf::Exploit::Capture
|
||||
gem 'packetfu', '1.1.9'
|
||||
# Needed by JSObfu
|
||||
gem 'rkelly-remix', '0.0.6'
|
||||
# Needed by anemone crawler
|
||||
gem 'robots'
|
||||
# Needed for some post modules
|
||||
gem 'sqlite3'
|
||||
# Add default group gems to `metasploit-framework.gemspec`:
|
||||
# spec.add_runtime_dependency '<name>', [<version requirements>]
|
||||
gemspec
|
||||
|
||||
group :db do
|
||||
# Needed for Msf::DbManager
|
||||
gem 'activerecord', '>= 3.0.0', '< 4.0.0'
|
||||
# Metasploit::Credential database models
|
||||
gem 'metasploit-credential', '>= 0.8.6', '< 0.9'
|
||||
# Database models shared between framework and Pro.
|
||||
gem 'metasploit_data_models', '0.17.0'
|
||||
gem 'metasploit_data_models', '~> 0.19'
|
||||
# Needed for module caching in Mdm::ModuleDetails
|
||||
gem 'pg', '>= 0.11'
|
||||
end
|
||||
|
||||
group :development do
|
||||
# Markdown formatting for yard
|
||||
gem 'redcarpet'
|
||||
# generating documentation
|
||||
gem 'yard'
|
||||
# for development and testing purposes
|
||||
gem 'pry'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
# supplies factories for producing model instance for specs
|
||||
# Version 4.1.0 or newer is needed to support generate calls without the
|
||||
# 'FactoryGirl.' in factory definitions syntax.
|
||||
gem 'factory_girl', '>= 4.1.0'
|
||||
# automatically include factories from spec/factories
|
||||
gem 'factory_girl_rails'
|
||||
# Make rspec output shorter and more useful
|
||||
gem 'fivemat', '1.2.1'
|
||||
# running documentation generation tasks and rspec tasks
|
||||
gem 'rake', '>= 10.0.0'
|
||||
# testing framework
|
||||
gem 'rspec', '>= 2.12', '< 3.0.0'
|
||||
# Define `rake spec`. Must be in development AND test so that its available by default as a rake test when the
|
||||
# environment is development
|
||||
gem 'rspec-rails' , '>= 2.12', '< 3.0.0'
|
||||
end
|
||||
|
||||
group :pcap do
|
||||
gem 'network_interface', '~> 0.0.1'
|
||||
# For sniffer and raw socket modules
|
||||
gem 'pcaprub'
|
||||
end
|
||||
|
||||
group :development do
|
||||
# Markdown formatting for yard
|
||||
gem 'redcarpet'
|
||||
# generating documentation
|
||||
gem 'yard'
|
||||
end
|
||||
|
||||
group :development, :test do
|
||||
# supplies factories for producing model instance for specs
|
||||
# Version 4.1.0 or newer is needed to support generate calls without the
|
||||
# 'FactoryGirl.' in factory definitions syntax.
|
||||
gem 'factory_girl', '>= 4.1.0'
|
||||
# Make rspec output shorter and more useful
|
||||
gem 'fivemat', '1.2.1'
|
||||
# running documentation generation tasks and rspec tasks
|
||||
gem 'rake', '>= 10.0.0'
|
||||
end
|
||||
|
||||
group :test do
|
||||
# Removes records from database created during tests. Can't use rspec-rails'
|
||||
# transactional fixtures because multiple connections are in use so
|
||||
# transactions won't work.
|
||||
gem 'database_cleaner'
|
||||
# testing framework
|
||||
gem 'rspec', '>= 2.12'
|
||||
gem 'shoulda-matchers'
|
||||
# code coverage for tests
|
||||
# any version newer than 0.5.4 gives an Encoding error when trying to read the source files.
|
||||
|
|
|
@ -27,8 +27,6 @@ end
|
|||
|
||||
# Create a custom group
|
||||
group :local do
|
||||
# Use pry to help view and interact with objects in the framework
|
||||
gem 'pry', '~> 0.9'
|
||||
# Use pry-debugger to step through code during development
|
||||
gem 'pry-debugger', '~> 0.2'
|
||||
# Add the lab gem so that the 'lab' plugin will work again
|
||||
|
|
179
Gemfile.lock
179
Gemfile.lock
|
@ -1,90 +1,177 @@
|
|||
PATH
|
||||
remote: .
|
||||
specs:
|
||||
metasploit-framework (4.10.1.pre.dev)
|
||||
actionpack (< 4.0.0)
|
||||
activesupport (>= 3.0.0, < 4.0.0)
|
||||
bcrypt
|
||||
json
|
||||
metasploit-model (~> 0.26.1)
|
||||
meterpreter_bins (= 0.0.6)
|
||||
msgpack
|
||||
nokogiri
|
||||
packetfu (= 1.1.9)
|
||||
railties
|
||||
rkelly-remix (= 0.0.6)
|
||||
robots
|
||||
rubyzip (~> 1.1)
|
||||
sqlite3
|
||||
tzinfo
|
||||
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
activemodel (3.2.14)
|
||||
activesupport (= 3.2.14)
|
||||
actionpack (3.2.19)
|
||||
activemodel (= 3.2.19)
|
||||
activesupport (= 3.2.19)
|
||||
builder (~> 3.0.0)
|
||||
activerecord (3.2.14)
|
||||
activemodel (= 3.2.14)
|
||||
activesupport (= 3.2.14)
|
||||
erubis (~> 2.7.0)
|
||||
journey (~> 1.0.4)
|
||||
rack (~> 1.4.5)
|
||||
rack-cache (~> 1.2)
|
||||
rack-test (~> 0.6.1)
|
||||
sprockets (~> 2.2.1)
|
||||
activemodel (3.2.19)
|
||||
activesupport (= 3.2.19)
|
||||
builder (~> 3.0.0)
|
||||
activerecord (3.2.19)
|
||||
activemodel (= 3.2.19)
|
||||
activesupport (= 3.2.19)
|
||||
arel (~> 3.0.2)
|
||||
tzinfo (~> 0.3.29)
|
||||
activesupport (3.2.14)
|
||||
activesupport (3.2.19)
|
||||
i18n (~> 0.6, >= 0.6.4)
|
||||
multi_json (~> 1.0)
|
||||
arel (3.0.2)
|
||||
arel (3.0.3)
|
||||
arel-helpers (2.0.1)
|
||||
activerecord (>= 3.1.0, < 5)
|
||||
bcrypt (3.1.7)
|
||||
builder (3.0.4)
|
||||
database_cleaner (1.1.1)
|
||||
diff-lcs (1.2.4)
|
||||
factory_girl (4.2.0)
|
||||
coderay (1.1.0)
|
||||
diff-lcs (1.2.5)
|
||||
erubis (2.7.0)
|
||||
factory_girl (4.4.0)
|
||||
activesupport (>= 3.0.0)
|
||||
factory_girl_rails (4.4.1)
|
||||
factory_girl (~> 4.4.0)
|
||||
railties (>= 3.0.0)
|
||||
fivemat (1.2.1)
|
||||
i18n (0.6.5)
|
||||
json (1.8.0)
|
||||
metasploit_data_models (0.17.0)
|
||||
activerecord (>= 3.2.13)
|
||||
hike (1.2.3)
|
||||
i18n (0.6.11)
|
||||
journey (1.0.4)
|
||||
json (1.8.1)
|
||||
metasploit-concern (0.1.1)
|
||||
activesupport (~> 3.0, >= 3.0.0)
|
||||
metasploit-credential (0.8.6)
|
||||
metasploit-concern (~> 0.1.0)
|
||||
metasploit-model (~> 0.26.1)
|
||||
metasploit_data_models (~> 0.19.4)
|
||||
pg
|
||||
rubyntlm
|
||||
rubyzip (~> 1.1)
|
||||
metasploit-model (0.26.1)
|
||||
activesupport
|
||||
metasploit_data_models (0.19.4)
|
||||
activerecord (>= 3.2.13, < 4.0.0)
|
||||
activesupport
|
||||
arel-helpers
|
||||
metasploit-concern (~> 0.1.0)
|
||||
metasploit-model (~> 0.26.1)
|
||||
pg
|
||||
meterpreter_bins (0.0.6)
|
||||
mini_portile (0.5.1)
|
||||
msgpack (0.5.5)
|
||||
method_source (0.8.2)
|
||||
mini_portile (0.6.0)
|
||||
msgpack (0.5.8)
|
||||
multi_json (1.0.4)
|
||||
network_interface (0.0.1)
|
||||
nokogiri (1.6.0)
|
||||
mini_portile (~> 0.5.0)
|
||||
nokogiri (1.6.3.1)
|
||||
mini_portile (= 0.6.0)
|
||||
packetfu (1.1.9)
|
||||
pcaprub (0.11.3)
|
||||
pg (0.16.0)
|
||||
rake (10.1.0)
|
||||
redcarpet (3.0.0)
|
||||
pg (0.17.1)
|
||||
pry (0.10.0)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.8.1)
|
||||
slop (~> 3.4)
|
||||
rack (1.4.5)
|
||||
rack-cache (1.2)
|
||||
rack (>= 0.4)
|
||||
rack-ssl (1.3.4)
|
||||
rack
|
||||
rack-test (0.6.2)
|
||||
rack (>= 1.0)
|
||||
railties (3.2.19)
|
||||
actionpack (= 3.2.19)
|
||||
activesupport (= 3.2.19)
|
||||
rack-ssl (~> 1.3.2)
|
||||
rake (>= 0.8.7)
|
||||
rdoc (~> 3.4)
|
||||
thor (>= 0.14.6, < 2.0)
|
||||
rake (10.3.2)
|
||||
rdoc (3.12.2)
|
||||
json (~> 1.4)
|
||||
redcarpet (3.1.2)
|
||||
rkelly-remix (0.0.6)
|
||||
robots (0.10.1)
|
||||
rspec (2.14.1)
|
||||
rspec-core (~> 2.14.0)
|
||||
rspec-expectations (~> 2.14.0)
|
||||
rspec-mocks (~> 2.14.0)
|
||||
rspec-core (2.14.5)
|
||||
rspec-expectations (2.14.2)
|
||||
rspec (2.99.0)
|
||||
rspec-core (~> 2.99.0)
|
||||
rspec-expectations (~> 2.99.0)
|
||||
rspec-mocks (~> 2.99.0)
|
||||
rspec-collection_matchers (1.0.0)
|
||||
rspec-expectations (>= 2.99.0.beta1)
|
||||
rspec-core (2.99.1)
|
||||
rspec-expectations (2.99.2)
|
||||
diff-lcs (>= 1.1.3, < 2.0)
|
||||
rspec-mocks (2.14.3)
|
||||
shoulda-matchers (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
rspec-mocks (2.99.2)
|
||||
rspec-rails (2.99.0)
|
||||
actionpack (>= 3.0)
|
||||
activemodel (>= 3.0)
|
||||
activesupport (>= 3.0)
|
||||
railties (>= 3.0)
|
||||
rspec-collection_matchers
|
||||
rspec-core (~> 2.99.0)
|
||||
rspec-expectations (~> 2.99.0)
|
||||
rspec-mocks (~> 2.99.0)
|
||||
rubyntlm (0.4.0)
|
||||
rubyzip (1.1.6)
|
||||
shoulda-matchers (2.6.2)
|
||||
simplecov (0.5.4)
|
||||
multi_json (~> 1.0.3)
|
||||
simplecov-html (~> 0.5.3)
|
||||
simplecov-html (0.5.3)
|
||||
slop (3.6.0)
|
||||
sprockets (2.2.2)
|
||||
hike (~> 1.2)
|
||||
multi_json (~> 1.0)
|
||||
rack (~> 1.0)
|
||||
tilt (~> 1.1, != 1.3.0)
|
||||
sqlite3 (1.3.9)
|
||||
timecop (0.6.3)
|
||||
tzinfo (0.3.37)
|
||||
yard (0.8.7)
|
||||
thor (0.19.1)
|
||||
tilt (1.4.1)
|
||||
timecop (0.7.1)
|
||||
tzinfo (0.3.40)
|
||||
yard (0.8.7.4)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activerecord (>= 3.0.0, < 4.0.0)
|
||||
activesupport (>= 3.0.0, < 4.0.0)
|
||||
bcrypt
|
||||
database_cleaner
|
||||
factory_girl (>= 4.1.0)
|
||||
factory_girl_rails
|
||||
fivemat (= 1.2.1)
|
||||
json
|
||||
metasploit_data_models (= 0.17.0)
|
||||
meterpreter_bins (= 0.0.6)
|
||||
msgpack
|
||||
metasploit-credential (>= 0.8.6, < 0.9)
|
||||
metasploit-framework!
|
||||
metasploit_data_models (~> 0.19)
|
||||
network_interface (~> 0.0.1)
|
||||
nokogiri
|
||||
packetfu (= 1.1.9)
|
||||
pcaprub
|
||||
pg (>= 0.11)
|
||||
pry
|
||||
rake (>= 10.0.0)
|
||||
redcarpet
|
||||
rkelly-remix (= 0.0.6)
|
||||
robots
|
||||
rspec (>= 2.12)
|
||||
rspec (>= 2.12, < 3.0.0)
|
||||
rspec-rails (>= 2.12, < 3.0.0)
|
||||
shoulda-matchers
|
||||
simplecov (= 0.5.4)
|
||||
sqlite3
|
||||
timecop
|
||||
yard
|
||||
|
|
86
Rakefile
86
Rakefile
|
@ -1,81 +1,11 @@
|
|||
require 'bundler/setup'
|
||||
|
||||
pathname = Pathname.new(__FILE__)
|
||||
root = pathname.parent
|
||||
|
||||
# add metasploit-framework/lib to load paths so rake files can just require
|
||||
# files normally without having to use __FILE__ and recalculating root and the
|
||||
# path to lib
|
||||
lib_pathname = root.join('lib')
|
||||
$LOAD_PATH.unshift(lib_pathname.to_s)
|
||||
#!/usr/bin/env rake
|
||||
require File.expand_path('../config/application', __FILE__)
|
||||
require 'metasploit/framework/require'
|
||||
|
||||
# @note must be before `Metasploit::Framework::Application.load_tasks`
|
||||
#
|
||||
# load rake files like a rails engine
|
||||
#
|
||||
# define db rake tasks from activerecord if activerecord is in the bundle. activerecord could be not in the bundle if
|
||||
# the user installs with `bundle install --without db`
|
||||
Metasploit::Framework::Require.optionally_active_record_railtie
|
||||
|
||||
rakefile_glob = root.join('lib', 'tasks', '**', '*.rake').to_path
|
||||
|
||||
Dir.glob(rakefile_glob) do |rakefile|
|
||||
# Skip database tasks, will load them later if MDM is present
|
||||
next if rakefile =~ /database\.rake$/
|
||||
load rakefile
|
||||
end
|
||||
|
||||
print_without = false
|
||||
|
||||
begin
|
||||
require 'rspec/core/rake_task'
|
||||
rescue LoadError
|
||||
puts "rspec not in bundle, so can't set up spec tasks. " \
|
||||
"To run specs ensure to install the development and test groups."
|
||||
|
||||
print_without = true
|
||||
else
|
||||
RSpec::Core::RakeTask.new(:spec => 'db:test:prepare')
|
||||
|
||||
task :default => :spec
|
||||
end
|
||||
|
||||
# Require yard before loading metasploit_data_models rake tasks as the yard tasks won't be defined if
|
||||
# YARD is not defined when yard.rake is loaded.
|
||||
begin
|
||||
require 'yard'
|
||||
rescue LoadError
|
||||
puts "yard not in bundle, so can't set up yard tasks. " \
|
||||
"To generate documentation ensure to install the development group."
|
||||
|
||||
print_without = true
|
||||
end
|
||||
|
||||
begin
|
||||
require 'metasploit_data_models'
|
||||
rescue LoadError
|
||||
puts "metasploit_data_models not in bundle, so can't set up db tasks. " \
|
||||
"To run database tasks, ensure to install the db bundler group."
|
||||
|
||||
print_without = true
|
||||
else
|
||||
load 'lib/tasks/database.rake'
|
||||
metasploit_data_models_task_glob = MetasploitDataModels.root.join(
|
||||
'lib',
|
||||
'tasks',
|
||||
'**',
|
||||
'*.rake'
|
||||
).to_s
|
||||
# include tasks from metasplioit_data_models, such as `rake yard`.
|
||||
# metasploit-framework specific yard options are in .yardopts
|
||||
Dir.glob(metasploit_data_models_task_glob) do |path|
|
||||
load path
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
if print_without
|
||||
puts "Bundle currently installed " \
|
||||
"'--without #{Bundler.settings.without.join(' ')}'."
|
||||
puts "To clear the without option do `bundle install --without ''` " \
|
||||
"(the --without flag with an empty string) or " \
|
||||
"`rm -rf .bundle` to remove the .bundle/config manually and " \
|
||||
"then `bundle install`"
|
||||
end
|
||||
Metasploit::Framework::Application.load_tasks
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Adds associations to `Metasploit::Credential::Core` which are inverses of association on models under
|
||||
# {BruteForce::Reuse}.
|
||||
require 'metasploit/framework/credential'
|
||||
|
||||
module Metasploit::Credential::Core::ToCredential
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
|
||||
def to_credential
|
||||
Metasploit::Framework::Credential.new(
|
||||
public: public.try(:username),
|
||||
private: private.try(:data),
|
||||
private_type: private.try(:type).try(:demodulize).try(:underscore).try(:to_sym),
|
||||
realm: realm.try(:value),
|
||||
realm_key: realm.try(:key),
|
||||
parent: self
|
||||
)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,2 @@
|
|||
require 'metasploit/framework/file_path_validator'
|
||||
require 'metasploit/framework/executable_path_validator'
|
|
@ -0,0 +1,16 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
# This is a ActiveModel custom validator that assumes the attribute
|
||||
# is supposed to be the path to a regular file. It checks whether the
|
||||
# file exists and whether or not it is an executable file.
|
||||
class ExecutablePathValidator < ActiveModel::EachValidator
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
unless ::File.executable? value
|
||||
record.errors[attribute] << (options[:message] || "is not a valid path to an executable file")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
# This is a ActiveModel custom validator that assumes the attribute
|
||||
# is supposed to be the path to a regular file. It checks whether the
|
||||
# file exists and whether or not it is a regular file.
|
||||
class FilePathValidator < ActiveModel::EachValidator
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
unless ::File.file? value
|
||||
record.errors[attribute] << (options[:message] || "is not a valid path to a regular file")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
require 'rails'
|
||||
require File.expand_path('../boot', __FILE__)
|
||||
|
||||
all_environments = [
|
||||
:development,
|
||||
:production,
|
||||
:test
|
||||
]
|
||||
|
||||
Bundler.require(
|
||||
*Rails.groups(
|
||||
db: all_environments,
|
||||
pcap: all_environments
|
||||
)
|
||||
)
|
||||
|
||||
#
|
||||
# Railties
|
||||
#
|
||||
|
||||
# For compatibility with jquery-rails (and other engines that need action_view) in pro
|
||||
require 'action_view/railtie'
|
||||
|
||||
#
|
||||
# Project
|
||||
#
|
||||
|
||||
require 'metasploit/framework/common_engine'
|
||||
require 'msf/base/config'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
class Application < Rails::Application
|
||||
include Metasploit::Framework::CommonEngine
|
||||
|
||||
environment_database_yaml = ENV['MSF_DATABASE_CONFIG']
|
||||
|
||||
if environment_database_yaml
|
||||
# DO NOT check if the path exists: if the environment variable is set, then the user meant to use this path
|
||||
# and if it doesn't exist then an error should occur so the user knows the environment variable points to a
|
||||
# non-existent file.
|
||||
config.paths['config/database'] = environment_database_yaml
|
||||
else
|
||||
user_config_root = Pathname.new(Msf::Config.get_config_root)
|
||||
user_database_yaml = user_config_root.join('database.yml')
|
||||
|
||||
# DO check if the path exists as in test environments there may be no config root, in which case the normal
|
||||
# rails location, `config/database.yml`, should contain the database config.
|
||||
if user_database_yaml.exist?
|
||||
config.paths['config/database'] = [user_database_yaml.to_path]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Silence warnings about this defaulting to true
|
||||
I18n.enforce_available_locales = true
|
|
@ -0,0 +1,39 @@
|
|||
require 'pathname'
|
||||
require 'rubygems'
|
||||
|
||||
GEMFILE_EXTENSIONS = [
|
||||
'.local',
|
||||
''
|
||||
]
|
||||
|
||||
msfenv_real_pathname = Pathname.new(__FILE__).realpath
|
||||
root = msfenv_real_pathname.parent.parent
|
||||
|
||||
unless ENV['BUNDLE_GEMFILE']
|
||||
require 'pathname'
|
||||
|
||||
GEMFILE_EXTENSIONS.each do |extension|
|
||||
extension_pathname = root.join("Gemfile#{extension}")
|
||||
|
||||
if extension_pathname.readable?
|
||||
ENV['BUNDLE_GEMFILE'] = extension_pathname.to_path
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
require 'bundler'
|
||||
rescue LoadError
|
||||
$stderr.puts "[*] Metasploit requires the Bundler gem to be installed"
|
||||
$stderr.puts " $ gem install bundler"
|
||||
exit(0)
|
||||
end
|
||||
|
||||
Bundler.setup
|
||||
|
||||
lib_path = root.join('lib').to_path
|
||||
|
||||
unless $LOAD_PATH.include? lib_path
|
||||
$LOAD_PATH.unshift lib_path
|
||||
end
|
|
@ -0,0 +1,5 @@
|
|||
# Load the rails application
|
||||
require File.expand_path('../application', __FILE__)
|
||||
|
||||
# Initialize the rails application
|
||||
Metasploit::Framework::Application.initialize!
|
File diff suppressed because it is too large
Load Diff
119
db/schema.rb
119
db/schema.rb
|
@ -11,7 +11,7 @@
|
|||
#
|
||||
# It's strongly recommended to check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(:version => 20130717150737) do
|
||||
ActiveRecord::Schema.define(:version => 20140801150537) do
|
||||
|
||||
create_table "api_keys", :force => true do |t|
|
||||
t.text "token"
|
||||
|
@ -28,6 +28,16 @@ ActiveRecord::Schema.define(:version => 20130717150737) do
|
|||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "credential_cores_tasks", :id => false, :force => true do |t|
|
||||
t.integer "core_id"
|
||||
t.integer "task_id"
|
||||
end
|
||||
|
||||
create_table "credential_logins_tasks", :id => false, :force => true do |t|
|
||||
t.integer "login_id"
|
||||
t.integer "task_id"
|
||||
end
|
||||
|
||||
create_table "creds", :force => true do |t|
|
||||
t.integer "service_id", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
|
@ -167,6 +177,113 @@ ActiveRecord::Schema.define(:version => 20130717150737) do
|
|||
t.binary "prefs"
|
||||
end
|
||||
|
||||
create_table "metasploit_credential_cores", :force => true do |t|
|
||||
t.integer "origin_id", :null => false
|
||||
t.string "origin_type", :null => false
|
||||
t.integer "private_id"
|
||||
t.integer "public_id"
|
||||
t.integer "realm_id"
|
||||
t.integer "workspace_id", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.integer "logins_count", :default => 0
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_cores", ["origin_type", "origin_id"], :name => "index_metasploit_credential_cores_on_origin_type_and_origin_id"
|
||||
add_index "metasploit_credential_cores", ["private_id"], :name => "index_metasploit_credential_cores_on_private_id"
|
||||
add_index "metasploit_credential_cores", ["public_id"], :name => "index_metasploit_credential_cores_on_public_id"
|
||||
add_index "metasploit_credential_cores", ["realm_id"], :name => "index_metasploit_credential_cores_on_realm_id"
|
||||
add_index "metasploit_credential_cores", ["workspace_id", "private_id"], :name => "unique_private_metasploit_credential_cores", :unique => true
|
||||
add_index "metasploit_credential_cores", ["workspace_id", "public_id", "private_id"], :name => "unique_realmless_metasploit_credential_cores", :unique => true
|
||||
add_index "metasploit_credential_cores", ["workspace_id", "public_id"], :name => "unique_public_metasploit_credential_cores", :unique => true
|
||||
add_index "metasploit_credential_cores", ["workspace_id", "realm_id", "private_id"], :name => "unique_publicless_metasploit_credential_cores", :unique => true
|
||||
add_index "metasploit_credential_cores", ["workspace_id", "realm_id", "public_id", "private_id"], :name => "unique_complete_metasploit_credential_cores", :unique => true
|
||||
add_index "metasploit_credential_cores", ["workspace_id", "realm_id", "public_id"], :name => "unique_privateless_metasploit_credential_cores", :unique => true
|
||||
add_index "metasploit_credential_cores", ["workspace_id"], :name => "index_metasploit_credential_cores_on_workspace_id"
|
||||
|
||||
create_table "metasploit_credential_logins", :force => true do |t|
|
||||
t.integer "core_id", :null => false
|
||||
t.integer "service_id", :null => false
|
||||
t.string "access_level"
|
||||
t.string "status", :null => false
|
||||
t.datetime "last_attempted_at"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_logins", ["core_id", "service_id"], :name => "index_metasploit_credential_logins_on_core_id_and_service_id", :unique => true
|
||||
add_index "metasploit_credential_logins", ["service_id", "core_id"], :name => "index_metasploit_credential_logins_on_service_id_and_core_id", :unique => true
|
||||
|
||||
create_table "metasploit_credential_origin_cracked_passwords", :force => true do |t|
|
||||
t.integer "metasploit_credential_core_id", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_origin_cracked_passwords", ["metasploit_credential_core_id"], :name => "originating_credential_cores"
|
||||
|
||||
create_table "metasploit_credential_origin_imports", :force => true do |t|
|
||||
t.text "filename", :null => false
|
||||
t.integer "task_id"
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_origin_imports", ["task_id"], :name => "index_metasploit_credential_origin_imports_on_task_id"
|
||||
|
||||
create_table "metasploit_credential_origin_manuals", :force => true do |t|
|
||||
t.integer "user_id", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_origin_manuals", ["user_id"], :name => "index_metasploit_credential_origin_manuals_on_user_id"
|
||||
|
||||
create_table "metasploit_credential_origin_services", :force => true do |t|
|
||||
t.integer "service_id", :null => false
|
||||
t.text "module_full_name", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_origin_services", ["service_id", "module_full_name"], :name => "unique_metasploit_credential_origin_services", :unique => true
|
||||
|
||||
create_table "metasploit_credential_origin_sessions", :force => true do |t|
|
||||
t.text "post_reference_name", :null => false
|
||||
t.integer "session_id", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_origin_sessions", ["session_id", "post_reference_name"], :name => "unique_metasploit_credential_origin_sessions", :unique => true
|
||||
|
||||
create_table "metasploit_credential_privates", :force => true do |t|
|
||||
t.string "type", :null => false
|
||||
t.text "data", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
t.string "jtr_format"
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_privates", ["type", "data"], :name => "index_metasploit_credential_privates_on_type_and_data", :unique => true
|
||||
|
||||
create_table "metasploit_credential_publics", :force => true do |t|
|
||||
t.string "username", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_publics", ["username"], :name => "index_metasploit_credential_publics_on_username", :unique => true
|
||||
|
||||
create_table "metasploit_credential_realms", :force => true do |t|
|
||||
t.string "key", :null => false
|
||||
t.string "value", :null => false
|
||||
t.datetime "created_at", :null => false
|
||||
t.datetime "updated_at", :null => false
|
||||
end
|
||||
|
||||
add_index "metasploit_credential_realms", ["key", "value"], :name => "index_metasploit_credential_realms_on_key_and_value", :unique => true
|
||||
|
||||
create_table "mod_refs", :force => true do |t|
|
||||
t.string "module", :limit => 1024
|
||||
t.string "mtype", :limit => 128
|
||||
|
|
|
@ -378,6 +378,10 @@ module Kernel #:nodoc:all
|
|||
# This method handles the loading of FASTLIB archives
|
||||
#
|
||||
def fastlib_require(name)
|
||||
if name.respond_to? :to_path
|
||||
name = name.to_path
|
||||
end
|
||||
|
||||
name = name + ".rb" if not name =~ /\.rb$/
|
||||
return false if fastlib_already_loaded?(name)
|
||||
return false if fastlib_already_tried?(name)
|
||||
|
|
|
@ -1,3 +1,29 @@
|
|||
#
|
||||
# Gems
|
||||
#
|
||||
# gems must load explicitly any gem declared in gemspec
|
||||
# @see https://github.com/bundler/bundler/issues/2018#issuecomment-6819359
|
||||
#
|
||||
|
||||
require 'active_support'
|
||||
require 'bcrypt'
|
||||
require 'json'
|
||||
require 'msgpack'
|
||||
require 'metasploit/model'
|
||||
require 'nokogiri'
|
||||
require 'packetfu'
|
||||
# railties has not autorequire defined
|
||||
# rkelly-remix is a fork of rkelly, so it's autorequire is 'rkelly' and not 'rkelly-remix'
|
||||
require 'rkelly'
|
||||
require 'robots'
|
||||
require 'zip'
|
||||
|
||||
#
|
||||
# Project
|
||||
#
|
||||
|
||||
require 'msf/core'
|
||||
|
||||
# Top-level namespace that is shared between {Metasploit::Framework
|
||||
# metasploit-framework} and pro, which uses Metasploit::Pro.
|
||||
module Metasploit
|
||||
|
@ -5,30 +31,6 @@ module Metasploit
|
|||
# works in compatible manner with activerecord's rake tasks and other
|
||||
# railties.
|
||||
module Framework
|
||||
# Returns the environment for {Metasploit::Framework}. Checks
|
||||
# `METASPLOIT_FRAMEWORK_ENV` environment variable for value. Defaults to
|
||||
# `'development'` if `METASPLOIT_FRAMEWORK_ENV` is not set in the
|
||||
# environment variables.
|
||||
#
|
||||
# {env} is a ActiveSupport::StringInquirer like `Rails.env` so it can be
|
||||
# queried for its value.
|
||||
#
|
||||
# @example check if environment is development
|
||||
# if Metasploit::Framework.env.development?
|
||||
# # runs only when in development
|
||||
# end
|
||||
#
|
||||
# @return [ActiveSupport::StringInquirer] the environment name
|
||||
def self.env
|
||||
unless instance_variable_defined? :@env
|
||||
name = ENV['METASPLOIT_FRAMEWORK_ENV']
|
||||
name ||= 'development'
|
||||
@env = ActiveSupport::StringInquirer.new(name)
|
||||
end
|
||||
|
||||
@env
|
||||
end
|
||||
|
||||
# Returns the root of the metasploit-framework project. Use in place of
|
||||
# `Rails.root`.
|
||||
#
|
||||
|
@ -42,4 +44,4 @@ module Metasploit
|
|||
@root
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,323 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'msf/core'
|
||||
require 'msf/core/exploit/tcp'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module AFP
|
||||
module Client
|
||||
|
||||
def next_id
|
||||
@request_id ||= -1
|
||||
@request_id += 1
|
||||
|
||||
@request_id
|
||||
end
|
||||
|
||||
def get_info
|
||||
packet = "\00" # Flag: Request
|
||||
packet << "\x03" # Command: FPGetSrvrInfo
|
||||
packet << [next_id].pack('n') # requestID
|
||||
packet << "\x00\x00\x00\x00" # Data offset
|
||||
packet << "\x00\x00\x00\x00" # Length
|
||||
packet << "\x00\x00\x00\x00" # Reserved
|
||||
|
||||
sock.put(packet)
|
||||
|
||||
response = sock.timed_read(1024)
|
||||
return parse_info_response(response)
|
||||
end
|
||||
|
||||
def open_session
|
||||
packet = "\00" # Flag: Request
|
||||
packet << "\x04" # Command: DSIOpenSession
|
||||
packet << [next_id].pack('n') # requestID
|
||||
packet << "\x00\x00\x00\x00" # Data offset
|
||||
packet << "\x00\x00\x00\x06" # Length
|
||||
packet << "\x00\x00\x00\x00" # Reserved
|
||||
packet << "\x01" # Attention Quantum
|
||||
packet << "\x04" # Length
|
||||
packet << "\x00\x00\x04\x00" # 1024
|
||||
|
||||
sock.put(packet)
|
||||
|
||||
response = sock.timed_read(1024)
|
||||
return parse_open_session_response(response)
|
||||
end
|
||||
|
||||
def login(user, pass)
|
||||
if user == ''
|
||||
return no_user_authent_login
|
||||
end
|
||||
|
||||
p = OpenSSL::BN.new("BA2873DFB06057D43F2024744CEEE75B", 16)
|
||||
g = OpenSSL::BN.new("7", 10)
|
||||
ra = OpenSSL::BN.new('86F6D3C0B0D63E4B11F113A2F9F19E3BBBF803F28D30087A1450536BE979FD42', 16)
|
||||
ma = g.mod_exp(ra, p)
|
||||
|
||||
padded_user = (user.length + 1) % 2 != 0 ? user + "\x00" : user
|
||||
bin_user = [padded_user.length, padded_user].pack("Ca*")
|
||||
|
||||
length = 18 + bin_user.length + ma.to_s(2).length
|
||||
|
||||
packet = "\00" # Flag: Request
|
||||
packet << "\x02" # Command: DSICommand
|
||||
packet << [next_id].pack('n') # requestID
|
||||
packet << "\x00\x00\x00\x00" # Data offset
|
||||
packet << [length].pack('N') # Length (42)
|
||||
packet << "\x00\x00\x00\x00" # Reserved
|
||||
packet << "\x12" # AFPCommand: FPLogin (18)
|
||||
packet << "\x06\x41\x46\x50\x33\x2e\x31" # AFPVersion: AFP3.1
|
||||
packet << "\x09\x44\x48\x43\x41\x53\x54\x31\x32\x38" #UAM: DHCAST128
|
||||
packet << bin_user # username
|
||||
packet << ma.to_s(2) # random number
|
||||
|
||||
sock.put(packet)
|
||||
|
||||
begin
|
||||
response = sock.timed_read(1024, self.login_timeout)
|
||||
rescue Timeout::Error
|
||||
#vprint_error("AFP #{rhost}:#{rport} Login timeout (AFP server delay response for 20 - 22 seconds after 7 incorrect logins)")
|
||||
return :connection_error
|
||||
end
|
||||
|
||||
flags, command, request_id, error_code, length, reserved = parse_header(response)
|
||||
|
||||
case error_code
|
||||
when -5001 #kFPAuthContinue
|
||||
return parse_login_response_add_send_login_count(response, {:p => p, :g => g, :ra => ra, :ma => ma,
|
||||
:password => pass, :user => user})
|
||||
when -5023 #kFPUserNotAuth (User dosen't exists)
|
||||
#print_status("AFP #{rhost}:#{rport} User #{user} dosen't exists")
|
||||
return :skip_user
|
||||
else
|
||||
return :connection_error
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def close_session
|
||||
packet = "\00" # Flag: Request
|
||||
packet << "\x01" # Command: DSICloseSession
|
||||
packet << [next_id].pack('n') # requestID
|
||||
packet << "\x00\x00\x00\x00" #Data offset
|
||||
packet << "\x00\x00\x00\x00" #Length
|
||||
packet << "\x00\x00\x00\x00" #Reserved
|
||||
|
||||
sock.put(packet)
|
||||
end
|
||||
|
||||
def no_user_authent_login
|
||||
packet = "\00" # Flag: Request
|
||||
packet << "\x02" # Command: DSICommand
|
||||
packet << [next_id].pack('n') # requestID
|
||||
packet << "\x00\x00\x00\x00" # Data offset
|
||||
packet << "\x00\x00\x00\x18" # Length (24)
|
||||
packet << "\x00\x00\x00\x00" # Reserved
|
||||
packet << "\x12" # AFPCommand: FPLogin (18)
|
||||
packet << "\x06\x41\x46\x50\x33\x2e\x31" #AFP3.1
|
||||
packet << "\x0f\x4e\x6f\x20\x55\x73\x65\x72\x20\x41\x75\x74\x68\x65\x6e\x74" #UAM: No User Authent
|
||||
|
||||
sock.put(packet)
|
||||
|
||||
begin
|
||||
response = sock.timed_read(1024, self.login_timeout)
|
||||
rescue Timeout::Error
|
||||
vprint_error("AFP #{rhost}:#{rport} Login timeout (AFP server delay response for 20 - 22 seconds after 7 incorrect logins)")
|
||||
return :connection_error
|
||||
end
|
||||
|
||||
flags, command, request_id, error_code, length, reserved = parse_header(response)
|
||||
|
||||
if error_code == 0
|
||||
return :true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def parse_login_response_add_send_login_count(response, data)
|
||||
dhx_s2civ = 'CJalbert'
|
||||
dhx_c2civ = 'LWallace'
|
||||
|
||||
flags, command, request_id, error_code, length, reserved = parse_header(response)
|
||||
body = get_body(response, length)
|
||||
id, mb, enc_data = body.unpack("nH32a*")
|
||||
|
||||
mb = OpenSSL::BN.new(mb, 16)
|
||||
k = mb.mod_exp(data[:ra], data[:p] )
|
||||
|
||||
cipher = OpenSSL::Cipher.new('cast5-cbc').decrypt
|
||||
cipher.key = k.to_s(2)
|
||||
cipher.iv = dhx_s2civ
|
||||
cipher.padding = 0
|
||||
|
||||
nonce = cipher.update(enc_data)
|
||||
nonce << cipher.final
|
||||
nonce = nonce[0..15]
|
||||
nonce = OpenSSL::BN.new(nonce, 2) + 1
|
||||
|
||||
plain_text = nonce.to_s(2) + data[:password].ljust(64, "\x00")
|
||||
cipher = OpenSSL::Cipher.new('cast5-cbc').encrypt
|
||||
cipher.key = k.to_s(2)
|
||||
cipher.iv = dhx_c2civ
|
||||
auth_response = cipher.update(plain_text)
|
||||
auth_response << cipher.final
|
||||
|
||||
packet = "\00" # Flag: Request
|
||||
packet << "\x02" # Command: DSICommand
|
||||
packet << [next_id].pack('n') # requestID
|
||||
packet << "\x00\x00\x00\x00" # Data offset
|
||||
packet << [auth_response.length + 2].pack("N") # Length
|
||||
packet << "\x00\x00\x00\x00" # Reserved
|
||||
packet << "\x13" # AFPCommand: FPLoginCont (19)
|
||||
packet << "\x00"
|
||||
packet << [id].pack('n')
|
||||
packet << auth_response
|
||||
|
||||
sock.put(packet)
|
||||
|
||||
begin
|
||||
response = sock.timed_read(1024, self.login_timeout)
|
||||
rescue Timeout::Error
|
||||
vprint_error("AFP #{rhost}:#{rport} Login timeout (AFP server delay response for 20 - 22 seconds after 7 incorrect logins)")
|
||||
return :connection_error
|
||||
end
|
||||
|
||||
flags, command, request_id, error_code, length, reserved = parse_header(response)
|
||||
if error_code == 0
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
def parse_open_session_response(response)
|
||||
_, _, _, error_code, _, _ = parse_header(response)
|
||||
return error_code == 0 ? true : false
|
||||
end
|
||||
|
||||
def parse_info_response(response)
|
||||
parsed_data = {}
|
||||
|
||||
flags, command, request_id, error_code, length, reserved = parse_header(response)
|
||||
raise "AFP #{rhost}:#{rport} Server response with error" if error_code != 0
|
||||
body = get_body(response, length)
|
||||
machine_type_offset, afp_versions_offset, uam_count_offset, icon_offset, server_flags =
|
||||
body.unpack('nnnnn')
|
||||
|
||||
server_name_length = body.unpack('@10C').first
|
||||
parsed_data[:server_name] = body.unpack("@11A#{server_name_length}").first
|
||||
|
||||
pos = 11 + server_name_length
|
||||
pos += 1 if pos % 2 != 0 #padding
|
||||
|
||||
server_signature_offset, network_addresses_offset, directory_names_offset,
|
||||
utf8_servername_offset = body.unpack("@#{pos}nnnn")
|
||||
|
||||
parsed_data[:machine_type] = read_pascal_string(body, machine_type_offset)
|
||||
parsed_data[:versions] = read_array(body, afp_versions_offset)
|
||||
parsed_data[:uams] = read_array(body, uam_count_offset)
|
||||
# skiped icon
|
||||
parsed_data[:server_flags] = parse_flags(server_flags)
|
||||
parsed_data[:signature] = body.unpack("@#{server_signature_offset}H32").first
|
||||
|
||||
network_addresses = read_array(body, network_addresses_offset, true)
|
||||
parsed_data[:network_addresses] = parse_network_addresses(network_addresses)
|
||||
# skiped directory names
|
||||
#Error catching for offset issues on this field. Need better error ahndling all through here
|
||||
begin
|
||||
parsed_data[:utf8_server_name] = read_utf8_pascal_string(body, utf8_servername_offset)
|
||||
rescue
|
||||
parsed_data[:utf8_server_name] = "N/A"
|
||||
end
|
||||
|
||||
return parsed_data
|
||||
end
|
||||
|
||||
def parse_header(packet)
|
||||
header = packet.unpack('CCnNNN') #ruby 1.8.7 don't support unpacking signed integers in big-endian order
|
||||
header[3] = packet[4..7].reverse.unpack("l").first
|
||||
return header
|
||||
end
|
||||
|
||||
def get_body(packet, body_length)
|
||||
body = packet[16..body_length + 15]
|
||||
raise "AFP #{rhost}:#{rport} Invalid body length" if body.length != body_length
|
||||
return body
|
||||
end
|
||||
|
||||
def read_pascal_string(str, offset)
|
||||
length = str.unpack("@#{offset}C").first
|
||||
return str.unpack("@#{offset + 1}A#{length}").first
|
||||
end
|
||||
|
||||
def read_utf8_pascal_string(str, offset)
|
||||
length = str.unpack("@#{offset}n").first
|
||||
return str[offset + 2..offset + length + 1]
|
||||
end
|
||||
|
||||
def read_array(str, offset, afp_network_address=false)
|
||||
size = str.unpack("@#{offset}C").first
|
||||
pos = offset + 1
|
||||
|
||||
result = []
|
||||
size.times do
|
||||
result << read_pascal_string(str, pos)
|
||||
pos += str.unpack("@#{pos}C").first
|
||||
pos += 1 unless afp_network_address
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
def parse_network_addresses(network_addresses)
|
||||
parsed_addreses = []
|
||||
network_addresses.each do |address|
|
||||
case address.unpack('C').first
|
||||
when 0 #Reserved
|
||||
next
|
||||
when 1 # Four-byte IP address
|
||||
parsed_addreses << IPAddr.ntop(address[1..4]).to_s
|
||||
when 2 # Four-byte IP address followed by a two-byte port number
|
||||
parsed_addreses << "#{IPAddr.ntop(address[1..4])}:#{address[5..6].unpack("n").first}"
|
||||
when 3 # DDP address (depricated)
|
||||
next
|
||||
when 4 # DNS name (maximum of 254 bytes)
|
||||
parsed_addreses << address[1..address.length - 1]
|
||||
when 5 # This functionality is deprecated.
|
||||
next
|
||||
when 6 # IPv6 address (16 bytes)
|
||||
parsed_addreses << "[#{IPAddr.ntop(address[1..16])}]"
|
||||
when 7 # IPv6 address (16 bytes) followed by a two-byte port number
|
||||
parsed_addreses << "[#{IPAddr.ntop(address[1..16])}]:#{address[17..18].unpack("n").first}"
|
||||
else # Something wrong?
|
||||
raise "Error parsing network addresses"
|
||||
end
|
||||
end
|
||||
return parsed_addreses
|
||||
end
|
||||
|
||||
def parse_flags(flags)
|
||||
flags = flags.to_s(2)
|
||||
result = {}
|
||||
result['Super Client'] = flags[0,1] == '1' ? true : false
|
||||
result['UUIDs'] = flags[5,1] == '1' ? true : false
|
||||
result['UTF8 Server Name'] = flags[6,1] == '1' ? true : false
|
||||
result['Open Directory'] = flags[7,1] == '1' ? true : false
|
||||
result['Reconnect'] = flags[8,1] == '1' ? true : false
|
||||
result['Server Notifications'] = flags[9,1] == '1' ? true : false
|
||||
result['TCP/IP'] = flags[10,1] == '1' ? true : false
|
||||
result['Server Signature'] = flags[11,1] == '1' ? true : false
|
||||
result['Server Messages'] = flags[12,1] == '1' ? true : false
|
||||
result['Password Saving Prohibited'] = flags[13,1] == '1' ? true : false
|
||||
result['Password Changing'] = flags[14,1] == '1' ? true : false
|
||||
result['Copy File'] = flags[5,1] == '1' ? true : false
|
||||
return result
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module API
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module API
|
||||
# @note This is a like. The API version is not semantically version and it's version has actually never changed
|
||||
# even though API changes have occured. DO NOT base compatibility on this version.
|
||||
module Version
|
||||
MAJOR = 1
|
||||
MINOR = 0
|
||||
PATCH = 0
|
||||
end
|
||||
|
||||
VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::PATCH}"
|
||||
GEM_VERSION = Gem::Version.new(VERSION)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,26 @@
|
|||
#
|
||||
# Gems
|
||||
#
|
||||
|
||||
# have to be exact so minimum is loaded prior to parsing arguments which could
|
||||
# influence loading.
|
||||
require 'active_support/dependencies/autoload'
|
||||
|
||||
# @note Must use the nested declaration of the
|
||||
# {Metasploit::Framework::Command} namespace because commands need to be able
|
||||
# to be required directly without any other part of metasploit-framework
|
||||
# besides config/boot so that the commands can parse arguments, setup
|
||||
# RAILS_ENV, and load config/application.rb correctly.
|
||||
module Metasploit
|
||||
module Framework
|
||||
module Command
|
||||
# Namespace for commands for metasploit-framework. There are
|
||||
# corresponding classes in the {Metasploit::Framework::ParsedOptions}
|
||||
# namespace, which handle for parsing the options for each command.
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :Base
|
||||
autoload :Console
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,113 @@
|
|||
#
|
||||
# Gems
|
||||
#
|
||||
|
||||
require 'active_support/core_ext/module/introspection'
|
||||
|
||||
#
|
||||
# Project
|
||||
#
|
||||
|
||||
require 'metasploit/framework/command'
|
||||
require 'metasploit/framework/parsed_options'
|
||||
require 'metasploit/framework/require'
|
||||
|
||||
# Based on pattern used for lib/rails/commands in the railties gem.
|
||||
class Metasploit::Framework::Command::Base
|
||||
#
|
||||
# Attributes
|
||||
#
|
||||
|
||||
# @!attribute [r] application
|
||||
# The Rails application for metasploit-framework.
|
||||
#
|
||||
# @return [Metasploit::Framework::Application]
|
||||
attr_reader :application
|
||||
|
||||
# @!attribute [r] parsed_options
|
||||
# The parsed options from the command line.
|
||||
#
|
||||
# @return (see parsed_options)
|
||||
attr_reader :parsed_options
|
||||
|
||||
#
|
||||
# Class Methods
|
||||
#
|
||||
|
||||
# @note {require_environment!} should be called to load
|
||||
# `config/application.rb` to so that the RAILS_ENV can be set from the
|
||||
# command line options in `ARGV` prior to `Rails.env` being set.
|
||||
# @note After returning, `Rails.application` will be defined and configured.
|
||||
#
|
||||
# Parses `ARGV` for command line arguments to configure the
|
||||
# `Rails.application`.
|
||||
#
|
||||
# @return (see parsed_options)
|
||||
def self.require_environment!
|
||||
parsed_options = self.parsed_options
|
||||
# RAILS_ENV must be set before requiring 'config/application.rb'
|
||||
parsed_options.environment!
|
||||
ARGV.replace(parsed_options.positional)
|
||||
|
||||
# allow other Rails::Applications to use this command
|
||||
if !defined?(Rails) || Rails.application.nil?
|
||||
# @see https://github.com/rails/rails/blob/v3.2.17/railties/lib/rails/commands.rb#L39-L40
|
||||
require Pathname.new(__FILE__).parent.parent.parent.parent.parent.join('config', 'application')
|
||||
end
|
||||
|
||||
# have to configure before requiring environment because
|
||||
# config/environment.rb calls initialize! and the initializers will use
|
||||
# the configuration from the parsed options.
|
||||
parsed_options.configure(Rails.application)
|
||||
|
||||
# support disabling the database
|
||||
unless parsed_options.options.database.disable
|
||||
Metasploit::Framework::Require.optionally_active_record_railtie
|
||||
end
|
||||
|
||||
Rails.application.require_environment!
|
||||
|
||||
parsed_options
|
||||
end
|
||||
|
||||
def self.parsed_options
|
||||
parsed_options_class.new
|
||||
end
|
||||
|
||||
def self.parsed_options_class
|
||||
@parsed_options_class ||= parsed_options_class_name.constantize
|
||||
end
|
||||
|
||||
def self.parsed_options_class_name
|
||||
@parsed_options_class_name ||= "#{parent.parent}::ParsedOptions::#{name.demodulize}"
|
||||
end
|
||||
|
||||
def self.start
|
||||
parsed_options = require_environment!
|
||||
new(application: Rails.application, parsed_options: parsed_options).start
|
||||
end
|
||||
|
||||
#
|
||||
# Instance Methods
|
||||
#
|
||||
|
||||
# @param attributes [Hash{Symbol => ActiveSupport::OrderedOptions,Rails::Application}]
|
||||
# @option attributes [Rails::Application] :application
|
||||
# @option attributes [ActiveSupport::OrderedOptions] :parsed_options
|
||||
# @raise [KeyError] if :application is not given
|
||||
# @raise [KeyError] if :parsed_options is not given
|
||||
def initialize(attributes={})
|
||||
@application = attributes.fetch(:application)
|
||||
@parsed_options = attributes.fetch(:parsed_options)
|
||||
end
|
||||
|
||||
# @abstract Use {#application} to start this command.
|
||||
#
|
||||
# Starts this command.
|
||||
#
|
||||
# @return [void]
|
||||
# @raise [NotImplementedError]
|
||||
def start
|
||||
raise NotImplementedError
|
||||
end
|
||||
end
|
|
@ -0,0 +1,64 @@
|
|||
#
|
||||
# Project
|
||||
#
|
||||
|
||||
require 'metasploit/framework/command'
|
||||
require 'metasploit/framework/command/base'
|
||||
|
||||
# Based on pattern used for lib/rails/commands in the railties gem.
|
||||
class Metasploit::Framework::Command::Console < Metasploit::Framework::Command::Base
|
||||
def start
|
||||
case parsed_options.options.subcommand
|
||||
when :version
|
||||
$stderr.puts "Framework Version: #{Metasploit::Framework::VERSION}"
|
||||
else
|
||||
driver.run
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# The console UI driver.
|
||||
#
|
||||
# @return [Msf::Ui::Console::Driver]
|
||||
def driver
|
||||
unless @driver
|
||||
# require here so minimum loading is done before {start} is called.
|
||||
require 'msf/ui'
|
||||
|
||||
@driver = Msf::Ui::Console::Driver.new(
|
||||
Msf::Ui::Console::Driver::DefaultPrompt,
|
||||
Msf::Ui::Console::Driver::DefaultPromptChar,
|
||||
driver_options
|
||||
)
|
||||
end
|
||||
|
||||
@driver
|
||||
end
|
||||
|
||||
def driver_options
|
||||
unless @driver_options
|
||||
options = parsed_options.options
|
||||
|
||||
driver_options = {}
|
||||
driver_options['Config'] = options.framework.config
|
||||
driver_options['ConfirmExit'] = options.console.confirm_exit
|
||||
driver_options['DatabaseEnv'] = options.environment
|
||||
driver_options['DatabaseMigrationPaths'] = options.database.migrations_paths
|
||||
driver_options['DatabaseYAML'] = options.database.config
|
||||
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
|
||||
driver_options['ModulePath'] = options.modules.path
|
||||
driver_options['Plugins'] = options.console.plugins
|
||||
driver_options['RealReadline'] = options.console.real_readline
|
||||
driver_options['Resource'] = options.console.resources
|
||||
driver_options['XCommands'] = options.console.commands
|
||||
|
||||
@driver_options = driver_options
|
||||
end
|
||||
|
||||
@driver_options
|
||||
end
|
||||
end
|
|
@ -0,0 +1,74 @@
|
|||
#
|
||||
# Standard Library
|
||||
#
|
||||
|
||||
require 'fileutils'
|
||||
|
||||
# `Rails::Engine` behavior common to both {Metasploit::Framework::Application} and {Metasploit::Framework::Engine}.
|
||||
module Metasploit::Framework::CommonEngine
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
#
|
||||
# config
|
||||
#
|
||||
|
||||
# Force binary encoding to remove necessity to set external and internal encoding when construct Strings from
|
||||
# from files. Socket#read always returns a String in ASCII-8bit encoding
|
||||
#
|
||||
# @see http://rubydoc.info/stdlib/core/IO:read
|
||||
config.before_initialize do
|
||||
encoding = 'binary'
|
||||
Encoding.default_external = encoding
|
||||
Encoding.default_internal = encoding
|
||||
end
|
||||
|
||||
config.root = Msf::Config::install_root
|
||||
config.paths.add 'data/meterpreter', glob: '**/ext_*'
|
||||
config.paths.add 'modules'
|
||||
|
||||
#
|
||||
# `initializer`s
|
||||
#
|
||||
|
||||
initializer 'metasploit_framework.merge_meterpreter_extensions' do
|
||||
Rails.application.railties.engines.each do |engine|
|
||||
merge_meterpreter_extensions(engine)
|
||||
end
|
||||
|
||||
# The Rails.application itself could have paths['data/meterpreter'], but will not be part of
|
||||
# Rails.application.railties.engines because only direct subclasses of `Rails::Engine` are returned.
|
||||
merge_meterpreter_extensions(Rails.application)
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Instance Methods
|
||||
#
|
||||
|
||||
private
|
||||
|
||||
# Merges the meterpreter extensions from `engine`'s `paths['data/meterpreter]`.
|
||||
#
|
||||
# @param engine [Rails::Engine] a Rails engine or application that has meterpreter extensions
|
||||
# @return [void]
|
||||
# @todo Make metasploit-framework look for meterpreter extension in paths['data/meterpreter'] from the engine instead of copying them.
|
||||
def merge_meterpreter_extensions(engine)
|
||||
data_meterpreter_paths = engine.paths['data/meterpreter']
|
||||
|
||||
# may be `nil` since 'data/meterpreter' is not part of the core Rails::Engine paths set.
|
||||
if data_meterpreter_paths
|
||||
source_paths = data_meterpreter_paths.existent
|
||||
destination_directory = root.join('data', 'meterpreter').to_path
|
||||
|
||||
source_paths.each do |source_path|
|
||||
basename = File.basename(source_path)
|
||||
destination_path = File.join(destination_directory, basename)
|
||||
|
||||
unless destination_path == source_path
|
||||
FileUtils.copy(source_path, destination_directory)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,74 @@
|
|||
require 'metasploit/framework/credential'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
|
||||
# This class is responsible for taking datastore options from the snmp_login module
|
||||
# and yielding appropriate {Metasploit::Framework::Credential}s to the {Metasploit::Framework::LoginScanner::SNMP}.
|
||||
# This one has to be different from {credentialCollection} as it will only have a {Metasploit::Framework::Credential#public}
|
||||
# It may be slightly confusing that the attribues are called password and pass_file, because this is what the legacy
|
||||
# module used. However, community Strings are now considered more to be public credentials than private ones.
|
||||
class CommunityStringCollection
|
||||
# @!attribute pass_file
|
||||
# Path to a file containing passwords, one per line
|
||||
# @return [String]
|
||||
attr_accessor :pass_file
|
||||
|
||||
# @!attribute password
|
||||
# @return [String]
|
||||
attr_accessor :password
|
||||
|
||||
# @!attribute prepended_creds
|
||||
# List of credentials to be tried before any others
|
||||
#
|
||||
# @see #prepend_cred
|
||||
# @return [Array<Credential>]
|
||||
attr_accessor :prepended_creds
|
||||
|
||||
# @option opts [String] :pass_file See {#pass_file}
|
||||
# @option opts [String] :password See {#password}
|
||||
# @option opts [Array<Credential>] :prepended_creds ([]) See {#prepended_creds}
|
||||
def initialize(opts = {})
|
||||
opts.each do |attribute, value|
|
||||
public_send("#{attribute}=", value)
|
||||
end
|
||||
self.prepended_creds ||= []
|
||||
end
|
||||
|
||||
# Combines all the provided credential sources into a stream of {Credential}
|
||||
# objects, yielding them one at a time
|
||||
#
|
||||
# @yieldparam credential [Metasploit::Framework::Credential]
|
||||
# @return [void]
|
||||
def each
|
||||
begin
|
||||
if pass_file.present?
|
||||
pass_fd = File.open(pass_file, 'r:binary')
|
||||
pass_fd.each_line do |line|
|
||||
line.chomp!
|
||||
yield Metasploit::Framework::Credential.new(public: line, paired: false)
|
||||
end
|
||||
end
|
||||
|
||||
if password.present?
|
||||
yield Metasploit::Framework::Credential.new(public: password, paired: false)
|
||||
end
|
||||
|
||||
ensure
|
||||
pass_fd.close if pass_fd && !pass_fd.closed?
|
||||
end
|
||||
end
|
||||
|
||||
# Add {Credential credentials} that will be yielded by {#each}
|
||||
#
|
||||
# @see prepended_creds
|
||||
# @param cred [Credential]
|
||||
# @return [self]
|
||||
def prepend_cred(cred)
|
||||
prepended_creds.unshift cred
|
||||
self
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,7 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module Core
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,19 @@
|
|||
require 'metasploit/framework/version'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
# @note This is a lie. The core libraries are not semantically versioned. This is currently just linked to the
|
||||
# Metasploit::Framework::Version, which is also not semantically versioned.
|
||||
module Core
|
||||
module Version
|
||||
MAJOR = Metasploit::Framework::Version::MAJOR
|
||||
MINOR = Metasploit::Framework::Version::MINOR
|
||||
PATCH = Metasploit::Framework::Version::PATCH
|
||||
PRERELEASE = Metasploit::Framework::Version::PRERELEASE
|
||||
end
|
||||
|
||||
VERSION = Metasploit::Framework::VERSION
|
||||
GEM_VERSION = Gem::Version.new(Metasploit::Framework::GEM_VERSION)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,105 @@
|
|||
require 'active_model'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
# This class provides an in-memory representation of a conceptual Credential
|
||||
#
|
||||
# It contains the public, private, and realm if any.
|
||||
class Credential
|
||||
include ActiveModel::Validations
|
||||
|
||||
# @!attribute paired
|
||||
# @return [Boolean] Whether BOTH a public and private are required
|
||||
# (defaults to `true`)
|
||||
attr_accessor :paired
|
||||
# @!attribute parent
|
||||
# @return [Object] the parent object that had .to_credential called on it to create this object
|
||||
attr_accessor :parent
|
||||
# @!attribute private
|
||||
# The private credential component (e.g. username)
|
||||
#
|
||||
# @return [String] if {#paired} is `true` or {#private} is `nil`
|
||||
# @return [String, nil] if {#paired} is `false` or {#private} is not `nil`.
|
||||
attr_accessor :private
|
||||
# @!attribute private_type
|
||||
# The type of private credential this object represents, e.g. a
|
||||
# password or an NTLM hash.
|
||||
#
|
||||
# @return [String]
|
||||
attr_accessor :private_type
|
||||
# @!attribute public
|
||||
# The public credential component (e.g. password)
|
||||
#
|
||||
# @return [String] if {#paired} is `true` or {#public} is `nil`
|
||||
# @return [String, nil] if {#paired} is `false` or {#public} is not `nil`.
|
||||
attr_accessor :public
|
||||
# @!attribute realm
|
||||
# @return [String,nil] The realm credential component (e.g domain name)
|
||||
attr_accessor :realm
|
||||
# @!attribute realm
|
||||
# @return [String,nil] The type of {#realm}
|
||||
attr_accessor :realm_key
|
||||
|
||||
validates :paired,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
# If we have no public we MUST have a private (e.g. SNMP Community String)
|
||||
validates :private,
|
||||
exclusion: { in: [nil] },
|
||||
if: "public.nil? or paired"
|
||||
|
||||
# These values should be #demodularized from subclasses of
|
||||
# `Metasploit::Credential::Private`
|
||||
validates :private_type,
|
||||
inclusion: { in: [ :password, :ntlm_hash, :ssh_key ] },
|
||||
if: "private_type.present?"
|
||||
|
||||
# If we have no private we MUST have a public
|
||||
validates :public,
|
||||
presence: true,
|
||||
if: "private.nil? or paired"
|
||||
|
||||
# @param attributes [Hash{Symbol => String,nil}]
|
||||
def initialize(attributes={})
|
||||
attributes.each do |attribute, value|
|
||||
public_send("#{attribute}=", value)
|
||||
end
|
||||
|
||||
self.paired = true if self.paired.nil?
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} \"#{self}\" >"
|
||||
end
|
||||
|
||||
def to_s
|
||||
if realm && realm_key == Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
|
||||
"#{self.realm}\\#{self.public}:#{self.private}"
|
||||
else
|
||||
"#{self.public}:#{self.private}#{at_realm}"
|
||||
end
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
other.respond_to?(:public) && other.public == self.public &&
|
||||
other.respond_to?(:private) && other.private == self.private &&
|
||||
other.respond_to?(:realm) && other.realm == self.realm
|
||||
end
|
||||
|
||||
def to_credential
|
||||
self.parent = self
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def at_realm
|
||||
if self.realm.present?
|
||||
"@#{self.realm}"
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,148 @@
|
|||
require 'metasploit/framework/credential'
|
||||
|
||||
class Metasploit::Framework::CredentialCollection
|
||||
|
||||
# @!attribute blank_passwords
|
||||
# Whether each username should be tried with a blank password
|
||||
# @return [Boolean]
|
||||
attr_accessor :blank_passwords
|
||||
|
||||
# @!attribute pass_file
|
||||
# Path to a file containing passwords, one per line
|
||||
# @return [String]
|
||||
attr_accessor :pass_file
|
||||
|
||||
# @!attribute password
|
||||
# @return [String]
|
||||
attr_accessor :password
|
||||
|
||||
# @!attribute prepended_creds
|
||||
# List of credentials to be tried before any others
|
||||
#
|
||||
# @see #prepend_cred
|
||||
# @return [Array<Credential>]
|
||||
attr_accessor :prepended_creds
|
||||
|
||||
# @!attribute realm
|
||||
# @return [String]
|
||||
attr_accessor :realm
|
||||
|
||||
# @!attribute user_as_pass
|
||||
# Whether each username should be tried as a password for that user
|
||||
# @return [Boolean]
|
||||
attr_accessor :user_as_pass
|
||||
|
||||
# @!attribute user_file
|
||||
# Path to a file containing usernames, one per line
|
||||
# @return [String]
|
||||
attr_accessor :user_file
|
||||
|
||||
# @!attribute username
|
||||
# @return [String]
|
||||
attr_accessor :username
|
||||
|
||||
# @!attribute userpass_file
|
||||
# Path to a file containing usernames and passwords separated by a space,
|
||||
# one pair per line
|
||||
# @return [String]
|
||||
attr_accessor :userpass_file
|
||||
|
||||
# @option opts [Boolean] :blank_passwords See {#blank_passwords}
|
||||
# @option opts [String] :pass_file See {#pass_file}
|
||||
# @option opts [String] :password See {#password}
|
||||
# @option opts [Array<Credential>] :prepended_creds ([]) See {#prepended_creds}
|
||||
# @option opts [Boolean] :user_as_pass See {#user_as_pass}
|
||||
# @option opts [String] :user_file See {#user_file}
|
||||
# @option opts [String] :username See {#username}
|
||||
# @option opts [String] :userpass_file See {#userpass_file}
|
||||
def initialize(opts = {})
|
||||
opts.each do |attribute, value|
|
||||
public_send("#{attribute}=", value)
|
||||
end
|
||||
self.prepended_creds ||= []
|
||||
end
|
||||
|
||||
# Add {Credential credentials} that will be yielded by {#each}
|
||||
#
|
||||
# @see prepended_creds
|
||||
# @param cred [Credential]
|
||||
# @return [self]
|
||||
def prepend_cred(cred)
|
||||
prepended_creds.unshift cred
|
||||
self
|
||||
end
|
||||
|
||||
# Combines all the provided credential sources into a stream of {Credential}
|
||||
# objects, yielding them one at a time
|
||||
#
|
||||
# @yieldparam credential [Metasploit::Framework::Credential]
|
||||
# @return [void]
|
||||
def each
|
||||
if pass_file.present?
|
||||
pass_fd = File.open(pass_file, 'r:binary')
|
||||
end
|
||||
|
||||
prepended_creds.each { |c| yield c }
|
||||
|
||||
if username.present?
|
||||
if password.present?
|
||||
yield Metasploit::Framework::Credential.new(public: username, private: password, realm: realm)
|
||||
end
|
||||
if user_as_pass
|
||||
yield Metasploit::Framework::Credential.new(public: username, private: username, realm: realm)
|
||||
end
|
||||
if blank_passwords
|
||||
yield Metasploit::Framework::Credential.new(public: username, private: "", realm: realm)
|
||||
end
|
||||
if pass_fd
|
||||
pass_fd.each_line do |pass_from_file|
|
||||
pass_from_file.chomp!
|
||||
yield Metasploit::Framework::Credential.new(public: username, private: pass_from_file, realm: realm)
|
||||
end
|
||||
pass_fd.seek(0)
|
||||
end
|
||||
end
|
||||
|
||||
if user_file.present?
|
||||
File.open(user_file, 'r:binary') do |user_fd|
|
||||
user_fd.each_line do |user_from_file|
|
||||
user_from_file.chomp!
|
||||
if password
|
||||
yield Metasploit::Framework::Credential.new(public: user_from_file, private: password, realm: realm)
|
||||
end
|
||||
if user_as_pass
|
||||
yield Metasploit::Framework::Credential.new(public: user_from_file, private: user_from_file, realm: realm)
|
||||
end
|
||||
if blank_passwords
|
||||
yield Metasploit::Framework::Credential.new(public: user_from_file, private: "", realm: realm)
|
||||
end
|
||||
if pass_fd
|
||||
pass_fd.each_line do |pass_from_file|
|
||||
pass_from_file.chomp!
|
||||
yield Metasploit::Framework::Credential.new(public: user_from_file, private: pass_from_file, realm: realm)
|
||||
end
|
||||
pass_fd.seek(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if userpass_file.present?
|
||||
File.open(userpass_file, 'r:binary') do |userpass_fd|
|
||||
userpass_fd.each_line do |line|
|
||||
user, pass = line.split(" ", 2)
|
||||
if pass.blank?
|
||||
pass = ''
|
||||
else
|
||||
pass.chomp!
|
||||
end
|
||||
yield Metasploit::Framework::Credential.new(public: user, private: pass, realm: realm)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ensure
|
||||
pass_fd.close if pass_fd && !pass_fd.closed?
|
||||
end
|
||||
|
||||
end
|
|
@ -8,7 +8,7 @@ module Metasploit
|
|||
end
|
||||
|
||||
def self.configurations_pathname
|
||||
Metasploit::Framework.root.join('config', 'database.yml')
|
||||
Metasploit::Framework::Application.paths['config/database'].first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
#
|
||||
# Gems
|
||||
#
|
||||
|
||||
require 'rails/engine'
|
||||
|
||||
#
|
||||
# Project
|
||||
#
|
||||
|
||||
require 'metasploit/framework/common_engine'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
class Engine < Rails::Engine
|
||||
include Metasploit::Framework::CommonEngine
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,276 @@
|
|||
require 'metasploit/framework/tcp/client'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module Ftp
|
||||
module Client
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
|
||||
#
|
||||
# This method establishes an FTP connection to host and port specified by
|
||||
# the 'rhost' and 'rport' methods. After connecting, the banner
|
||||
# message is read in and stored in the 'banner' attribute.
|
||||
#
|
||||
def connect(global = true)
|
||||
fd = super(global)
|
||||
|
||||
@ftpbuff = '' unless @ftpbuff
|
||||
|
||||
# Wait for a banner to arrive...
|
||||
self.banner = recv_ftp_resp(fd)
|
||||
|
||||
# Return the file descriptor to the caller
|
||||
fd
|
||||
end
|
||||
|
||||
#
|
||||
# This method handles establishing datasocket for data channel
|
||||
#
|
||||
def data_connect(mode = nil, nsock = self.sock)
|
||||
if mode
|
||||
res = send_cmd([ 'TYPE' , mode ], true, nsock)
|
||||
return nil if not res =~ /^200/
|
||||
end
|
||||
|
||||
# force datasocket to renegotiate
|
||||
self.datasocket.shutdown if self.datasocket != nil
|
||||
|
||||
res = send_cmd(['PASV'], true, nsock)
|
||||
return nil if not res =~ /^227/
|
||||
|
||||
# 227 Entering Passive Mode (127,0,0,1,196,5)
|
||||
if res =~ /\((\d+)\,(\d+),(\d+),(\d+),(\d+),(\d+)/
|
||||
# convert port to FTP syntax
|
||||
datahost = "#{$1}.#{$2}.#{$3}.#{$4}"
|
||||
dataport = ($5.to_i * 256) + $6.to_i
|
||||
self.datasocket = Rex::Socket::Tcp.create('PeerHost' => datahost, 'PeerPort' => dataport)
|
||||
end
|
||||
self.datasocket
|
||||
end
|
||||
|
||||
#
|
||||
# This method handles disconnecting our data channel
|
||||
#
|
||||
def data_disconnect
|
||||
self.datasocket.shutdown
|
||||
self.datasocket = nil
|
||||
end
|
||||
|
||||
#
|
||||
# Connect and login to the remote FTP server using the credentials
|
||||
# that have been supplied in the exploit options.
|
||||
#
|
||||
def connect_login(user,pass,global = true)
|
||||
ftpsock = connect(global)
|
||||
|
||||
if !(user and pass)
|
||||
return false
|
||||
end
|
||||
|
||||
res = send_user(user, ftpsock)
|
||||
|
||||
if (res !~ /^(331|2)/)
|
||||
return false
|
||||
end
|
||||
|
||||
if (pass)
|
||||
res = send_pass(pass, ftpsock)
|
||||
if (res !~ /^2/)
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
#
|
||||
# This method logs in as the supplied user by transmitting the FTP
|
||||
# 'USER <user>' command.
|
||||
#
|
||||
def send_user(user, nsock = self.sock)
|
||||
raw_send("USER #{user}\r\n", nsock)
|
||||
recv_ftp_resp(nsock)
|
||||
end
|
||||
|
||||
#
|
||||
# This method completes user authentication by sending the supplied
|
||||
# password using the FTP 'PASS <pass>' command.
|
||||
#
|
||||
def send_pass(pass, nsock = self.sock)
|
||||
raw_send("PASS #{pass}\r\n", nsock)
|
||||
recv_ftp_resp(nsock)
|
||||
end
|
||||
|
||||
#
|
||||
# This method sends a QUIT command.
|
||||
#
|
||||
def send_quit(nsock = self.sock)
|
||||
raw_send("QUIT\r\n", nsock)
|
||||
recv_ftp_resp(nsock)
|
||||
end
|
||||
|
||||
#
|
||||
# This method sends one command with zero or more parameters
|
||||
#
|
||||
def send_cmd(args, recv = true, nsock = self.sock)
|
||||
cmd = args.join(" ") + "\r\n"
|
||||
ret = raw_send(cmd, nsock)
|
||||
if (recv)
|
||||
return recv_ftp_resp(nsock)
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
#
|
||||
# This method transmits the command in args and receives / uploads DATA via data channel
|
||||
# For commands not needing data, it will fall through to the original send_cmd
|
||||
#
|
||||
# For commands that send data only, the return will be the server response.
|
||||
# For commands returning both data and a server response, an array will be returned.
|
||||
#
|
||||
# NOTE: This function always waits for a response from the server.
|
||||
#
|
||||
def send_cmd_data(args, data, mode = 'a', nsock = self.sock)
|
||||
type = nil
|
||||
# implement some aliases for various commands
|
||||
if (args[0] =~ /^DIR$/i || args[0] =~ /^LS$/i)
|
||||
# TODO || args[0] =~ /^MDIR$/i || args[0] =~ /^MLS$/i
|
||||
args[0] = "LIST"
|
||||
type = "get"
|
||||
elsif (args[0] =~ /^GET$/i)
|
||||
args[0] = "RETR"
|
||||
type = "get"
|
||||
elsif (args[0] =~ /^PUT$/i)
|
||||
args[0] = "STOR"
|
||||
type = "put"
|
||||
end
|
||||
|
||||
# fall back if it's not a supported data command
|
||||
if not type
|
||||
return send_cmd(args, true, nsock)
|
||||
end
|
||||
|
||||
# Set the transfer mode and connect to the remove server
|
||||
return nil if not data_connect(mode)
|
||||
|
||||
# Our pending command should have got a connection now.
|
||||
res = send_cmd(args, true, nsock)
|
||||
# make sure could open port
|
||||
return nil unless res =~ /^(150|125) /
|
||||
|
||||
# dispatch to the proper method
|
||||
if (type == "get")
|
||||
# failed listings jsut disconnect..
|
||||
begin
|
||||
data = self.datasocket.get_once(-1, ftp_timeout)
|
||||
rescue ::EOFError
|
||||
data = nil
|
||||
end
|
||||
else
|
||||
sent = self.datasocket.put(data)
|
||||
end
|
||||
|
||||
# close data channel so command channel updates
|
||||
data_disconnect
|
||||
|
||||
# get status of transfer
|
||||
ret = nil
|
||||
if (type == "get")
|
||||
ret = recv_ftp_resp(nsock)
|
||||
ret = [ ret, data ]
|
||||
else
|
||||
ret = recv_ftp_resp(nsock)
|
||||
end
|
||||
|
||||
ret
|
||||
end
|
||||
|
||||
#
|
||||
# This method transmits a FTP command and waits for a response. If one is
|
||||
# received, it is returned to the caller.
|
||||
#
|
||||
def raw_send_recv(cmd, nsock = self.sock)
|
||||
nsock.put(cmd)
|
||||
nsock.get_once(-1, ftp_timeout)
|
||||
end
|
||||
|
||||
#
|
||||
# This method reads an FTP response based on FTP continuation stuff
|
||||
#
|
||||
def recv_ftp_resp(nsock = self.sock)
|
||||
found_end = false
|
||||
resp = ""
|
||||
left = ""
|
||||
if !@ftpbuff.empty?
|
||||
left << @ftpbuff
|
||||
@ftpbuff = ""
|
||||
end
|
||||
while true
|
||||
data = nsock.get_once(-1, ftp_timeout)
|
||||
if not data
|
||||
@ftpbuff << resp
|
||||
@ftpbuff << left
|
||||
return data
|
||||
end
|
||||
|
||||
got = left + data
|
||||
left = ""
|
||||
|
||||
# handle the end w/o newline case
|
||||
enlidx = got.rindex(0x0a.chr)
|
||||
if enlidx != (got.length-1)
|
||||
if not enlidx
|
||||
left << got
|
||||
next
|
||||
else
|
||||
left << got.slice!((enlidx+1)..got.length)
|
||||
end
|
||||
end
|
||||
|
||||
# split into lines
|
||||
rarr = got.split(/\r?\n/)
|
||||
rarr.each do |ln|
|
||||
if not found_end
|
||||
resp << ln
|
||||
resp << "\r\n"
|
||||
if ln.length > 3 and ln[3,1] == ' '
|
||||
found_end = true
|
||||
end
|
||||
else
|
||||
left << ln
|
||||
left << "\r\n"
|
||||
end
|
||||
end
|
||||
if found_end
|
||||
@ftpbuff << left
|
||||
return resp
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# This method transmits a FTP command and does not wait for a response
|
||||
#
|
||||
def raw_send(cmd, nsock = self.sock)
|
||||
nsock.put(cmd)
|
||||
end
|
||||
|
||||
def ftp_timeout
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
||||
|
||||
protected
|
||||
|
||||
#
|
||||
# This attribute holds the banner that was read in after a successful call
|
||||
# to connect or connect_login.
|
||||
#
|
||||
attr_accessor :banner, :datasocket
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,272 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module JtR
|
||||
|
||||
class JohnNotFoundError < StandardError
|
||||
end
|
||||
|
||||
class Cracker
|
||||
include ActiveModel::Validations
|
||||
|
||||
# @!attribute config
|
||||
# @return [String] The path to an optional config file for John to use
|
||||
attr_accessor :config
|
||||
|
||||
# @!attribute format
|
||||
# @return [String] The hash format to try
|
||||
attr_accessor :format
|
||||
|
||||
# @!attribute hash_path
|
||||
# @return [String] The path to the file containing the hashes
|
||||
attr_accessor :hash_path
|
||||
|
||||
# @!attribute incremental
|
||||
# @return [String] The incremental mode to use
|
||||
attr_accessor :incremental
|
||||
|
||||
# @!attribute john_path
|
||||
# This attribute allows the user to specify a john binary to use.
|
||||
# If not supplied, the Cracker will search the PATH for a suitable john binary
|
||||
# and finally fall back to the pre-compiled versions shipped with Metasploit.
|
||||
#
|
||||
# @return [String] The file path to an alternative John binary to use
|
||||
attr_accessor :john_path
|
||||
|
||||
# @!attribute max_runtime
|
||||
# @return [Fixnum] An optional maximum duration of the cracking attempt in seconds
|
||||
attr_accessor :max_runtime
|
||||
|
||||
# @!attribute pot
|
||||
# @return [String] The file path to an alternative John pot file to use
|
||||
attr_accessor :pot
|
||||
|
||||
# @!attribute rules
|
||||
# @return [String] The wordlist mangling rules to use inside John
|
||||
attr_accessor :rules
|
||||
|
||||
# @!attribute wordlist
|
||||
# @return [String] The file path to the wordlist to use
|
||||
attr_accessor :wordlist
|
||||
|
||||
validates :config, :'Metasploit::Framework::File_path' => true, if: 'config.present?'
|
||||
|
||||
validates :hash_path, :'Metasploit::Framework::File_path' => true, if: 'hash_path.present?'
|
||||
|
||||
validates :john_path, :'Metasploit::Framework::Executable_path' => true, if: 'john_path.present?'
|
||||
|
||||
validates :pot, :'Metasploit::Framework::File_path' => true, if: 'pot.present?'
|
||||
|
||||
validates :max_runtime,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 0
|
||||
}, if: 'max_runtime.present?'
|
||||
|
||||
validates :wordlist, :'Metasploit::Framework::File_path' => true, if: 'wordlist.present?'
|
||||
|
||||
# @param attributes [Hash{Symbol => String,nil}]
|
||||
def initialize(attributes={})
|
||||
attributes.each do |attribute, value|
|
||||
public_send("#{attribute}=", value)
|
||||
end
|
||||
end
|
||||
|
||||
# This method follows a decision tree to determine the path
|
||||
# to the John the Ripper binary we should use.
|
||||
#
|
||||
# @return [NilClass] if a binary path could not be found
|
||||
# @return [String] the path to the selected JtR binary
|
||||
def binary_path
|
||||
# Always prefer a manually entered path
|
||||
if john_path && ::File.file?(john_path)
|
||||
bin_path = john_path
|
||||
else
|
||||
# Look in the Environment PATH for the john binary
|
||||
path = Rex::FileUtils.find_full_path("john") ||
|
||||
Rex::FileUtils.find_full_path("john.exe")
|
||||
|
||||
if path && ::File.file?(path)
|
||||
bin_path = path
|
||||
else
|
||||
# If we can't find john anywhere else, look at our precompiled binaries
|
||||
bin_path = select_shipped_binary
|
||||
end
|
||||
end
|
||||
raise JohnNotFoundError, 'No suitable John binary was found on the system' if bin_path.blank?
|
||||
bin_path
|
||||
end
|
||||
|
||||
# This method runs the command from {#crack_command} and yields each line of output.
|
||||
#
|
||||
# @yield [String] a line of output from the john command
|
||||
# @return [void]
|
||||
def crack
|
||||
::IO.popen(crack_command, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
yield line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method builds an array for the command to actually run the cracker.
|
||||
# It builds the command from all of the attributes on the class.
|
||||
#
|
||||
# @raise [JohnNotFoundError] if a suitable John binary was never found
|
||||
# @return [Array] An array set up for {::IO.popen} to use
|
||||
def crack_command
|
||||
cmd_string = binary_path
|
||||
cmd = [ cmd_string, '--session=' + john_session_id, '--nolog' ]
|
||||
|
||||
if config.present?
|
||||
cmd << ( "--config=" + config )
|
||||
else
|
||||
cmd << ( "--config=" + john_config_file )
|
||||
end
|
||||
|
||||
if pot.present?
|
||||
cmd << ( "--pot=" + pot )
|
||||
else
|
||||
cmd << ( "--pot=" + john_pot_file)
|
||||
end
|
||||
|
||||
if format.present?
|
||||
cmd << ( "--format=" + format )
|
||||
end
|
||||
|
||||
if wordlist.present?
|
||||
cmd << ( "--wordlist=" + wordlist )
|
||||
end
|
||||
|
||||
if incremental.present?
|
||||
cmd << ( "--incremental=" + incremental )
|
||||
end
|
||||
|
||||
if rules.present?
|
||||
cmd << ( "--rules=" + rules )
|
||||
end
|
||||
|
||||
if max_runtime.present?
|
||||
cmd << ( "--max-run-time=" + max_runtime.to_s)
|
||||
end
|
||||
|
||||
cmd << hash_path
|
||||
end
|
||||
|
||||
# This runs the show command in john and yields cracked passwords.
|
||||
#
|
||||
# @yield [String] the output lines from the command
|
||||
# @return [void]
|
||||
def each_cracked_password
|
||||
::IO.popen(show_command, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
yield line
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method returns the path to a default john.conf file.
|
||||
#
|
||||
# @return [String] the path to the default john.conf file
|
||||
def john_config_file
|
||||
::File.join( ::Msf::Config.data_directory, "john", "confs", "john.conf" )
|
||||
end
|
||||
|
||||
# This method returns the path to a default john.pot file.
|
||||
#
|
||||
# @return [String] the path to the default john.pot file
|
||||
def john_pot_file
|
||||
::File.join( ::Msf::Config.config_directory, "john.pot" )
|
||||
end
|
||||
|
||||
# This method is a getter for a random Session ID for John.
|
||||
# It allows us to dinstiguish between cracking sessions.
|
||||
#
|
||||
# @ return [String] the Session ID to use
|
||||
def john_session_id
|
||||
@session_id ||= ::Rex::Text.rand_text_alphanumeric(8)
|
||||
end
|
||||
|
||||
# This method builds the command to show the cracked passwords.
|
||||
#
|
||||
# @raise [JohnNotFoundError] if a suitable John binary was never found
|
||||
# @return [Array] An array set up for {::IO.popen} to use
|
||||
def show_command
|
||||
cmd_string = binary_path
|
||||
|
||||
pot_file = pot || john_pot_file
|
||||
cmd = [cmd_string, "--show", "--pot=#{pot_file}", "--format=#{format}" ]
|
||||
|
||||
if config
|
||||
cmd << "--config=#{config}"
|
||||
else
|
||||
cmd << ( "--config=" + john_config_file )
|
||||
end
|
||||
|
||||
cmd << hash_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method tries to identify the correct version of the pre-shipped
|
||||
# JtR binaries to use based on the platform.
|
||||
#
|
||||
# @return [NilClass] if the correct binary could not be determined
|
||||
# @return [String] the path to the selected binary
|
||||
def select_shipped_binary
|
||||
cpuinfo_base = ::File.join(Msf::Config.data_directory, "cpuinfo")
|
||||
run_path = nil
|
||||
if File.directory?(cpuinfo_base)
|
||||
data = nil
|
||||
|
||||
case ::RUBY_PLATFORM
|
||||
when /mingw|cygwin|mswin/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.exe"
|
||||
if File.exists?(fname) and File.executable?(fname)
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /sse2/
|
||||
run_path ||= ::File.join(Msf::Config.data_directory, "john", "run.win32.sse2", "john.exe")
|
||||
when /mmx/
|
||||
run_path ||= ::File.join(Msf::Config.data_directory, "john", "run.win32.mmx", "john.exe")
|
||||
else
|
||||
run_path ||= ::File.join(Msf::Config.data_directory, "john", "run.win32.any", "john.exe")
|
||||
end
|
||||
when /x86_64-linux/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.ia64.bin"
|
||||
if File.exists? fname
|
||||
::FileUtils.chmod(0755, fname) rescue nil
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /mmx/
|
||||
run_path ||= ::File.join(Msf::Config.data_directory, "john", "run.linux.x64.mmx", "john")
|
||||
else
|
||||
run_path ||= ::File.join(Msf::Config.data_directory, "john", "run.linux.x86.any", "john")
|
||||
end
|
||||
when /i[\d]86-linux/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.ia32.bin"
|
||||
if File.exists? fname
|
||||
::FileUtils.chmod(0755, fname) rescue nil
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /sse2/
|
||||
run_path ||= ::File.join(Msf::Config.data_directory, "john", "run.linux.x86.sse2", "john")
|
||||
when /mmx/
|
||||
run_path ||= ::File.join(Msf::Config.data_directory, "john", "run.linux.x86.mmx", "john")
|
||||
else
|
||||
run_path ||= ::File.join(Msf::Config.data_directory, "john", "run.linux.x86.any", "john")
|
||||
end
|
||||
end
|
||||
end
|
||||
run_path
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module JtR
|
||||
|
||||
# This class is the generic Exception raised by a {Wordlist} when
|
||||
# it fails validation. It rolls up all validation errors into a
|
||||
# single exception so that all errors can be dealt with at once.
|
||||
class InvalidWordlist < StandardError
|
||||
attr_reader :model
|
||||
|
||||
def initialize(model)
|
||||
@model = model
|
||||
|
||||
errors = @model.errors.full_messages.join(', ')
|
||||
super(errors)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,429 @@
|
|||
require 'metasploit/framework/jtr/invalid_wordlist'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module JtR
|
||||
|
||||
class Wordlist
|
||||
include ActiveModel::Validations
|
||||
|
||||
# A mapping of the mutation substitution rules
|
||||
MUTATIONS = {
|
||||
'@' => 'a',
|
||||
'0' => 'o',
|
||||
'3' => 'e',
|
||||
'$' => 's',
|
||||
'7' => 't',
|
||||
'1' => 'l',
|
||||
'5' => 's'
|
||||
}
|
||||
|
||||
# @!attribute appenders
|
||||
# @return [Array] an array of strings to append to each word
|
||||
attr_accessor :appenders
|
||||
|
||||
# @!attribute custom_wordlist
|
||||
# @return [String] the path to a custom wordlist file to include
|
||||
attr_accessor :custom_wordlist
|
||||
|
||||
# @!attribute mutate
|
||||
# @return [TrueClass] if you want each word mutated as it is added
|
||||
# @return [FalseClass] if you do not want each word mutated
|
||||
attr_accessor :mutate
|
||||
|
||||
# @!attribute prependers
|
||||
# @return [Array] an array of strings to prepend to each word
|
||||
attr_accessor :prependers
|
||||
|
||||
# @!attribute use_common_root
|
||||
# @return [TrueClass] if you want to use the common root words wordlist
|
||||
# @return [FalseClass] if you do not want to use the common root words wordlist
|
||||
attr_accessor :use_common_root
|
||||
|
||||
# @!attribute use_creds
|
||||
# @return [TrueClass] if you want to seed the wordlist with existing credential data from the database
|
||||
# @return [FalseClass] if you do not want to seed the wordlist with existing credential data from the database
|
||||
attr_accessor :use_creds
|
||||
|
||||
# @!attribute use_db_info
|
||||
# @return [TrueClass] if you want to seed the wordlist with looted database names and schemas
|
||||
# @return [FalseClass] if you do not want to seed the wordlist with looted database names and schemas
|
||||
attr_accessor :use_db_info
|
||||
|
||||
# @!attribute use_default_wordlist
|
||||
# @return [TrueClass] if you want to use the default wordlist
|
||||
# @return [FalseClass] if you do not want to use the default wordlist
|
||||
attr_accessor :use_default_wordlist
|
||||
|
||||
# @!attribute use_hostnames
|
||||
# @return [TrueClass] if you want to seed the wordlist with existing hostnames from the database
|
||||
# @return [FalseClass] if you do not want to seed the wordlist with existing hostnames from the database
|
||||
attr_accessor :use_hostnames
|
||||
|
||||
# @!attribute workspace
|
||||
# @return [Mdm::Workspace] the workspace this cracker is for.
|
||||
attr_accessor :workspace
|
||||
|
||||
validates :custom_wordlist, :'Metasploit::Framework::File_path' => true, if: 'custom_wordlist.present?'
|
||||
|
||||
validates :mutate,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
|
||||
validates :use_common_root,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :use_creds,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :use_db_info,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :use_default_wordlist,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :use_hostnames,
|
||||
inclusion: { in: [true, false], message: "must be true or false" }
|
||||
|
||||
validates :workspace,
|
||||
presence: true
|
||||
|
||||
# @param attributes [Hash{Symbol => String,nil}]
|
||||
def initialize(attributes={})
|
||||
attributes.each do |attribute, value|
|
||||
public_send("#{attribute}=", value)
|
||||
end
|
||||
@appenders ||= []
|
||||
@prependers ||= []
|
||||
end
|
||||
|
||||
# This method takes a word, and appends each word from the appenders list
|
||||
# and yields the new words.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_appended_word(word='')
|
||||
yield word
|
||||
appenders.each do |suffix|
|
||||
yield "#{word}#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
# This method checks all the attributes set on the object and calls
|
||||
# the appropriate enumerators for each option and yields the results back
|
||||
# up the call-chain.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_base_word
|
||||
# Make sure are attributes are all valid first!
|
||||
valid!
|
||||
|
||||
# Yield the expanded form of each line of the custom wordlist if one was given
|
||||
if custom_wordlist.present?
|
||||
each_custom_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
# Yield each word from the common root words list if it was selected
|
||||
if use_common_root
|
||||
each_root_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
# If the user has selected use_creds we yield each password, username, and realm name
|
||||
# that currently exists in the database.
|
||||
if use_creds
|
||||
each_cred_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
if use_db_info
|
||||
each_database_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
if use_default_wordlist
|
||||
each_default_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
if use_hostnames
|
||||
each_hostname_word do |word|
|
||||
yield word unless word.blank?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# This method searches all saved Credentials in the database
|
||||
# and yields all passwords, usernames, and realm names it finds.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_cred_word
|
||||
# We don't want all Private types here. Only Passwords make sense for inclusion in the wordlist.
|
||||
Metasploit::Credential::Password.all.each do |password|
|
||||
yield password.data
|
||||
end
|
||||
|
||||
Metasploit::Credential::Public.all.each do |public|
|
||||
yield public.username
|
||||
end
|
||||
|
||||
Metasploit::Credential::Realm.all.each do |realm|
|
||||
yield realm.value
|
||||
end
|
||||
end
|
||||
|
||||
# This method reads the file provided as custom_wordlist and yields
|
||||
# the expanded form of each word in the list.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_custom_word
|
||||
::File.open(custom_wordlist, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
expanded_words(line) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method searches the notes in the current workspace
|
||||
# for DB instance names, database names, table names, and
|
||||
# column names gathered from live database servers. It yields
|
||||
# each one that it finds.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_database_word
|
||||
# Yield database, table and column names from any looted database schemas
|
||||
workspace.notes.where('ntype like ?', '%.schema%').each do |note|
|
||||
expanded_words(note.data['DBName']) do |word|
|
||||
yield word
|
||||
end
|
||||
|
||||
note.data['Tables'].each do |table|
|
||||
expanded_words(table['TableName']) do |word|
|
||||
yield word
|
||||
end
|
||||
|
||||
table['Columns'].each do |column|
|
||||
expanded_words(column['ColumnName']) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Yield any capture MSSQL Instance names
|
||||
workspace.notes.find(:all, :conditions => ['ntype=?', 'mssql.instancename']).each do |note|
|
||||
expanded_words(note.data['InstanceName']) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method yields expanded words taken from the default john
|
||||
# wordlist that we ship in the data directory.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_default_word
|
||||
::File.open(default_wordlist_path, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
expanded_words(line) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method yields the expanded words out of all the hostnames
|
||||
# found in the current workspace.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_hostname_word
|
||||
workspace.hosts.all.each do |host|
|
||||
unless host.name.nil?
|
||||
expanded_words(host.name) do |word|
|
||||
yield nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method checks to see if the user asked for mutations. If mutations
|
||||
# have been enabled, then it creates all the unique mutations and yields
|
||||
# each result.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_mutated_word(word='')
|
||||
mutants = [ ]
|
||||
|
||||
# Run the mutations only if the option is set
|
||||
if mutate
|
||||
mutants = mutants + mutate_word(word)
|
||||
end
|
||||
|
||||
mutants << word
|
||||
mutants.uniq.each do |mutant|
|
||||
yield mutant
|
||||
end
|
||||
end
|
||||
|
||||
# This method takes a word, and prepends each word from the prependers list
|
||||
# and yields the new words.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_prepended_word(word='')
|
||||
yield word
|
||||
prependers.each do |prefix|
|
||||
yield "#{prefix}#{word}"
|
||||
end
|
||||
end
|
||||
|
||||
# This method reads the common_roots.txt wordlist
|
||||
# expands any words in the list and yields them.
|
||||
#
|
||||
# @yieldparam word [String] the expanded word
|
||||
# @return [void]
|
||||
def each_root_word
|
||||
::File.open(common_root_words_path, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
expanded_words(line) do |word|
|
||||
yield word
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method wraps around all the other enumerators. It processes
|
||||
# all of the options and yields each word generated by the options
|
||||
# selected.
|
||||
#
|
||||
# @yieldparam word [String] the word to write out to the wordlist file
|
||||
# @return [void]
|
||||
def each_word
|
||||
each_base_word do |base_word|
|
||||
each_mutated_word(base_word) do |mutant|
|
||||
each_prepended_word(mutant) do |prepended|
|
||||
yield prepended
|
||||
end
|
||||
|
||||
each_appended_word(mutant) do |appended|
|
||||
yield appended
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This method takes a string and splits it on non-word characters
|
||||
# and the underscore. It does this to find likely distinct words
|
||||
# in the string. It then yields each 'word' found this way.
|
||||
#
|
||||
# @param word [String] the string to split apart
|
||||
# @yieldparam expanded [String] the expanded words
|
||||
# @return [void]
|
||||
def expanded_words(word='')
|
||||
word.split(/[\W_]+/).each do |expanded|
|
||||
yield expanded
|
||||
end
|
||||
end
|
||||
|
||||
# This method takes a word and applies various mutation rules to that word
|
||||
# and returns an array of all the mutated forms.
|
||||
#
|
||||
# @param word [String] the word to apply the mutations to
|
||||
# @return [Array<String>] An array containing all the mutated forms of the word
|
||||
def mutate_word(word)
|
||||
results = []
|
||||
# Iterate through combinations to create each possible mutation
|
||||
mutation_keys.each do |iteration|
|
||||
next if iteration.flatten.empty?
|
||||
intermediate = word.dup
|
||||
subsititutions = iteration.collect { |key| MUTATIONS[key] }
|
||||
intermediate.tr!(subsititutions.join, iteration.join)
|
||||
results << intermediate
|
||||
end
|
||||
results.flatten.uniq
|
||||
end
|
||||
|
||||
# A getter for a memoized version fo the mutation keys list
|
||||
#
|
||||
# @return [Array<Array>] a 2D array of all mutation combinations
|
||||
def mutation_keys
|
||||
@mutation_keys ||= generate_mutation_keys
|
||||
end
|
||||
|
||||
# This method takes all the options provided and streams the generated wordlist out
|
||||
# to a {Rex::Quickfile} and returns the {Rex::Quickfile}.
|
||||
#
|
||||
# @return [Rex::Quickfile] The {Rex::Quickfile} object that the wordlist has been written to
|
||||
def to_file
|
||||
valid!
|
||||
wordlist_file = Rex::Quickfile.new("jtrtmp")
|
||||
each_word do |word|
|
||||
wordlist_file.puts word
|
||||
end
|
||||
wordlist_file
|
||||
end
|
||||
|
||||
# Raise an exception if the attributes are not valid.
|
||||
#
|
||||
# @raise [Invalid] if the attributes are not valid on this scanner
|
||||
# @return [void]
|
||||
def valid!
|
||||
unless valid?
|
||||
raise Metasploit::Framework::JtR::InvalidWordlist.new(self)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
|
||||
private
|
||||
|
||||
# This method returns the path to the common_roots.txt wordlist
|
||||
#
|
||||
# @return [String] the file path to the common_roots.txt file
|
||||
def common_root_words_path
|
||||
::File.join(Msf::Config.data_directory, 'john', 'wordlists', 'common_roots.txt')
|
||||
end
|
||||
|
||||
# This method returns the path to the passwords.lst wordlist
|
||||
#
|
||||
# @return [String] the file path to the passwords.lst file
|
||||
def default_wordlist_path
|
||||
::File.join(Msf::Config.data_directory, 'john', 'wordlists', 'password.lst')
|
||||
end
|
||||
|
||||
def generate_mutation_keys
|
||||
iterations = MUTATIONS.keys.dup
|
||||
|
||||
# Find PowerSet of all possible mutation combinations
|
||||
iterations.inject([[]]) do |accumulator,mutation_key|
|
||||
power_set = []
|
||||
accumulator.each do |i|
|
||||
power_set << i
|
||||
power_set << i+[mutation_key]
|
||||
end
|
||||
power_set
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,47 @@
|
|||
require 'metasploit/framework/credential'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
# This module provides the namespace for all LoginScanner classes.
|
||||
# LoginScanners are the classes that provide functionality for testing
|
||||
# authentication against various different protocols and mechanisms.
|
||||
module LoginScanner
|
||||
require 'metasploit/framework/login_scanner/result'
|
||||
require 'metasploit/framework/login_scanner/invalid'
|
||||
|
||||
# Gather a list of LoginScanner classes that can potentially be
|
||||
# used for a given `service`, which should usually be an
|
||||
# `Mdm::Service` object, but can be anything that responds to
|
||||
# #name and #port.
|
||||
#
|
||||
# @param service [Mdm::Service,#port,#name]
|
||||
# @return [Array<LoginScanner::Base>] A collection of LoginScanner
|
||||
# classes that will probably give useful results when run
|
||||
# against `service`.
|
||||
def self.classes_for_service(service)
|
||||
|
||||
unless @required
|
||||
# Make sure we've required all the scanner classes
|
||||
dir = File.expand_path("../login_scanner/", __FILE__)
|
||||
Dir.glob(File.join(dir, "*.rb")).each do |f|
|
||||
require f if File.file?(f)
|
||||
end
|
||||
@required = true
|
||||
end
|
||||
|
||||
self.constants.map{|sym| const_get(sym)}.select do |const|
|
||||
next unless const.kind_of?(Class)
|
||||
|
||||
(
|
||||
const.const_defined?(:LIKELY_PORTS) &&
|
||||
const.const_get(:LIKELY_PORTS).include?(service.port)
|
||||
) || (
|
||||
const.const_defined?(:LIKELY_SERVICE_NAMES) &&
|
||||
const.const_get(:LIKELY_SERVICE_NAMES).include?(service.name)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
require 'metasploit/framework/tcp/client'
|
||||
require 'metasploit/framework/afp/client'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with Apple Filing
|
||||
# Protocol.
|
||||
class AFP
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
include Metasploit::Framework::AFP::Client
|
||||
|
||||
DEFAULT_PORT = 548
|
||||
LIKELY_PORTS = [ DEFAULT_PORT ]
|
||||
LIKELY_SERVICE_NAMES = [ "afp" ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = nil
|
||||
|
||||
# @!attribute login_timeout
|
||||
# @return [Integer] Number of seconds to wait before giving up
|
||||
attr_accessor :login_timeout
|
||||
|
||||
def attempt_login(credential)
|
||||
begin
|
||||
connect
|
||||
rescue Rex::ConnectionError, EOFError, Timeout::Error
|
||||
status = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
else
|
||||
success = login(credential.public, credential.private)
|
||||
status = (success == true) ? Metasploit::Model::Login::Status::SUCCESSFUL : Metasploit::Model::Login::Status::INCORRECT
|
||||
end
|
||||
|
||||
Result.new(credential: credential, status: status)
|
||||
end
|
||||
|
||||
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
|
|
@ -0,0 +1,67 @@
|
|||
|
||||
require 'metasploit/framework/login_scanner/http'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# Tomcat Manager login scanner
|
||||
class Axis2 < HTTP
|
||||
|
||||
DEFAULT_PORT = 8080
|
||||
# Inherit LIKELY_PORTS,LIKELY_SERVICE_NAMES, and REALM_KEY from HTTP
|
||||
|
||||
CAN_GET_SESSION = true
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
|
||||
# (see Base#attempt_login)
|
||||
def attempt_login(credential)
|
||||
http_client = Rex::Proto::Http::Client.new(
|
||||
host, port, {}, ssl, ssl_version
|
||||
)
|
||||
|
||||
result_opts = {
|
||||
credential: credential
|
||||
}
|
||||
begin
|
||||
http_client.connect
|
||||
body = "userName=#{Rex::Text.uri_encode(credential.public)}&password=#{Rex::Text.uri_encode(credential.private)}&submit=+Login+"
|
||||
request = http_client.request_cgi(
|
||||
'uri' => uri,
|
||||
'method' => "POST",
|
||||
'data' => body,
|
||||
)
|
||||
response = http_client.send_recv(request)
|
||||
|
||||
if response && response.code == 200 && response.body.include?("upload")
|
||||
result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL, proof: response)
|
||||
else
|
||||
result_opts.merge!(status: Metasploit::Model::Login::Status::INCORRECT, proof: response)
|
||||
end
|
||||
rescue ::EOFError, Rex::ConnectionError, ::Timeout::Error
|
||||
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||
end
|
||||
|
||||
Result.new(result_opts)
|
||||
|
||||
end
|
||||
|
||||
# (see Base#set_sane_defaults)
|
||||
def set_sane_defaults
|
||||
self.uri = "/axis2/axis2-admin/login" if self.uri.nil?
|
||||
@method = "POST".freeze
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
# The method *must* be "POST", so don't let the user change it
|
||||
# @raise [RuntimeError]
|
||||
def method=(_)
|
||||
raise RuntimeError, "Method must be POST for Axis2"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
require 'metasploit/framework/login_scanner'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This module provides the base behaviour for all of
|
||||
# the LoginScanner classes. All of the LoginScanners
|
||||
# should include this module to establish base behaviour
|
||||
module Base
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::Validations
|
||||
|
||||
included do
|
||||
# @!attribute connection_timeout
|
||||
# @return [Fixnum] The timeout in seconds for a single SSH connection
|
||||
attr_accessor :connection_timeout
|
||||
# @!attribute cred_details
|
||||
# @return [CredentialCollection] Collection of Credential objects
|
||||
attr_accessor :cred_details
|
||||
# @!attribute host
|
||||
# @return [String] The IP address or hostname to connect to
|
||||
attr_accessor :host
|
||||
# @!attribute port
|
||||
# @return [Fixnum] The port to connect to
|
||||
attr_accessor :port
|
||||
# @!attribute proxies
|
||||
# @return [String] The proxy directive to use for the socket
|
||||
attr_accessor :proxies
|
||||
# @!attribute stop_on_success
|
||||
# @return [Boolean] Whether the scanner should stop when it has found one working Credential
|
||||
attr_accessor :stop_on_success
|
||||
|
||||
validates :connection_timeout,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 1
|
||||
}
|
||||
|
||||
validates :cred_details, presence: true
|
||||
|
||||
validates :host, presence: true
|
||||
|
||||
validates :port,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 1,
|
||||
less_than_or_equal_to: 65535
|
||||
}
|
||||
|
||||
validates :stop_on_success,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validate :host_address_must_be_valid
|
||||
|
||||
validate :validate_cred_details
|
||||
|
||||
# @param attributes [Hash{Symbol => String,nil}]
|
||||
def initialize(attributes={})
|
||||
attributes.each do |attribute, value|
|
||||
public_send("#{attribute}=", value)
|
||||
end
|
||||
set_sane_defaults
|
||||
end
|
||||
|
||||
# Attempt a single login against the service with the given
|
||||
# {Credential credential}.
|
||||
#
|
||||
# @param credential [Credential] The credential object to attmpt to
|
||||
# login with
|
||||
# @return [Result] A Result object indicating success or failure
|
||||
# @abstract Protocol-specific scanners must implement this for their
|
||||
# respective protocols
|
||||
def attempt_login(credential)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
|
||||
def each_credential
|
||||
cred_details.each do |raw_cred|
|
||||
# This could be a Credential object, or a Credential Core, or an Attempt object
|
||||
# so make sure that whatever it is, we end up with a Credential.
|
||||
credential = raw_cred.to_credential
|
||||
|
||||
if credential.realm.present? && self.class::REALM_KEY.present?
|
||||
credential.realm_key = self.class::REALM_KEY
|
||||
yield credential
|
||||
elsif credential.realm.blank? && self.class::REALM_KEY.present? && self.class::DEFAULT_REALM.present?
|
||||
credential.realm_key = self.class::REALM_KEY
|
||||
credential.realm = self.class::DEFAULT_REALM
|
||||
yield credential
|
||||
elsif credential.realm.present? && self.class::REALM_KEY.blank?
|
||||
second_cred = credential.dup
|
||||
# Strip the realm off here, as we don't want it
|
||||
credential.realm = nil
|
||||
credential.realm_key = nil
|
||||
yield credential
|
||||
# Some services can take a domain in the username like this even though
|
||||
# they do not explicitly take a domain as part of the protocol.
|
||||
second_cred.public = "#{second_cred.realm}\\#{second_cred.public}"
|
||||
second_cred.realm = nil
|
||||
second_cred.realm_key = nil
|
||||
yield second_cred
|
||||
else
|
||||
yield credential
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Attempt to login with every {Credential credential} in
|
||||
# {#cred_details}, by calling {#attempt_login} once for each.
|
||||
#
|
||||
# If a successful login is found for a user, no more attempts
|
||||
# will be made for that user.
|
||||
#
|
||||
# @yieldparam result [Result] The {Result} object for each attempt
|
||||
# @yieldreturn [void]
|
||||
# @return [void]
|
||||
def scan!
|
||||
valid!
|
||||
|
||||
# Keep track of connection errors.
|
||||
# If we encounter too many, we will stop.
|
||||
consecutive_error_count = 0
|
||||
total_error_count = 0
|
||||
|
||||
successful_users = Set.new
|
||||
|
||||
each_credential do |credential|
|
||||
# For Pro bruteforce Reuse and Guess we need to note that we skipped an attempt.
|
||||
if successful_users.include?(credential.public)
|
||||
if credential.parent.respond_to?(:skipped)
|
||||
credential.parent.skipped = true
|
||||
credential.parent.save!
|
||||
end
|
||||
next
|
||||
end
|
||||
|
||||
result = attempt_login(credential)
|
||||
result.freeze
|
||||
|
||||
yield result if block_given?
|
||||
|
||||
if result.success?
|
||||
consecutive_error_count = 0
|
||||
break if stop_on_success
|
||||
successful_users << credential.public
|
||||
else
|
||||
if result.status == Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
consecutive_error_count += 1
|
||||
total_error_count += 1
|
||||
break if consecutive_error_count >= 3
|
||||
break if total_error_count >= 10
|
||||
end
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# Raise an exception if this scanner's attributes are not valid.
|
||||
#
|
||||
# @raise [Invalid] if the attributes are not valid on this scanner
|
||||
# @return [void]
|
||||
def valid!
|
||||
unless valid?
|
||||
raise Metasploit::Framework::LoginScanner::Invalid.new(self)
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
# This method validates that the host address is both
|
||||
# of a valid type and is resolveable.
|
||||
# @return [void]
|
||||
def host_address_must_be_valid
|
||||
if host.kind_of? String
|
||||
begin
|
||||
resolved_host = ::Rex::Socket.getaddress(host, true)
|
||||
if host =~ /^\d{1,3}(\.\d{1,3}){1,3}$/
|
||||
unless host =~ Rex::Socket::MATCH_IPV4
|
||||
errors.add(:host, "could not be resolved")
|
||||
end
|
||||
end
|
||||
self.host = resolved_host
|
||||
rescue
|
||||
errors.add(:host, "could not be resolved")
|
||||
end
|
||||
else
|
||||
errors.add(:host, "must be a string")
|
||||
end
|
||||
end
|
||||
|
||||
# This is a placeholder method. Each LoginScanner class
|
||||
# will override this with any sane defaults specific to
|
||||
# its own behaviour.
|
||||
# @abstract
|
||||
# @return [void]
|
||||
def set_sane_defaults
|
||||
self.connection_timeout = 30 if self.connection_timeout.nil?
|
||||
end
|
||||
|
||||
# This method validates that the credentials supplied
|
||||
# are all valid.
|
||||
# @return [void]
|
||||
def validate_cred_details
|
||||
unless cred_details.respond_to? :each
|
||||
errors.add(:cred_details, "must respond to :each")
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,134 @@
|
|||
require 'metasploit/framework/tcp/client'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
# This is the LoginScanner class for dealing with DB2 Database servers.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
class DB2
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
|
||||
DEFAULT_PORT = 50000
|
||||
DEFAULT_REALM = 'toolsdb'
|
||||
LIKELY_PORTS = [ DEFAULT_PORT ]
|
||||
# @todo XXX
|
||||
LIKELY_SERVICE_NAMES = [ ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = Metasploit::Model::Realm::Key::DB2_DATABASE
|
||||
|
||||
# @see Base#attempt_login
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
begin
|
||||
probe_data = send_probe(credential.realm)
|
||||
|
||||
if probe_data.empty?
|
||||
result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
else
|
||||
if authenticate?(credential)
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
else
|
||||
result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
|
||||
end
|
||||
end
|
||||
rescue ::Rex::ConnectionError, ::Rex::ConnectionTimeout, ::Rex::Proto::DRDA::RespError,::Timeout::Error => e
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT,
|
||||
proof: e.message
|
||||
})
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
|
||||
private
|
||||
# This method takes the credential and actually attempts the authentication
|
||||
# @param credential [Credential] The Credential object to authenticate with.
|
||||
# @return [Boolean] Whether the authentication was successful
|
||||
def authenticate?(credential)
|
||||
# Send the login packet and get a response packet back
|
||||
login_packet = Rex::Proto::DRDA::Utils.client_auth(:dbname => credential.realm,
|
||||
:dbuser => credential.public,
|
||||
:dbpass => credential.private
|
||||
)
|
||||
sock.put login_packet
|
||||
response = sock.get_once
|
||||
if valid_response?(response)
|
||||
if successful_login?(response)
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# This method opens a socket to the target DB2 server.
|
||||
# It then sends a client probe on that socket to get information
|
||||
# back on the server.
|
||||
# @param database_name [String] The name of the database to probe
|
||||
# @return [Hash] A hash containing the server information from the probe reply
|
||||
def send_probe(database_name)
|
||||
disconnect if self.sock
|
||||
connect
|
||||
|
||||
probe_packet = Rex::Proto::DRDA::Utils.client_probe(database_name)
|
||||
sock.put probe_packet
|
||||
response = sock.get_once
|
||||
|
||||
response_data = {}
|
||||
if valid_response?(response)
|
||||
packet = Rex::Proto::DRDA::SERVER_PACKET.new.read(response)
|
||||
response_data = Rex::Proto::DRDA::Utils.server_packet_info(packet)
|
||||
end
|
||||
response_data
|
||||
end
|
||||
|
||||
# This method sets the sane defaults for things
|
||||
# like timeouts and TCP evasion options
|
||||
def set_sane_defaults
|
||||
self.connection_timeout ||= 30
|
||||
self.port ||= DEFAULT_PORT
|
||||
self.max_send_size ||= 0
|
||||
self.send_delay ||= 0
|
||||
|
||||
self.ssl = false if self.ssl.nil?
|
||||
end
|
||||
|
||||
# This method takes a response packet and checks to see
|
||||
# if the authentication was actually successful.
|
||||
#
|
||||
# @param response [String] The unprocessed response packet
|
||||
# @return [Boolean] Whether the authentication was successful
|
||||
def successful_login?(response)
|
||||
packet = Rex::Proto::DRDA::SERVER_PACKET.new.read(response)
|
||||
packet_info = Rex::Proto::DRDA::Utils.server_packet_info(packet)
|
||||
if packet_info[:db_login_success]
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# This method provides a simple test on whether the response
|
||||
# packet was valid.
|
||||
#
|
||||
# @param response [String] The response to examine from the socket
|
||||
# @return [Boolean] Whether the response is valid
|
||||
def valid_response?(response)
|
||||
response && response.length > 0
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,76 @@
|
|||
require 'metasploit/framework/ftp/client'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with FTP.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
class FTP
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::Ftp::Client
|
||||
|
||||
DEFAULT_PORT = 21
|
||||
LIKELY_PORTS = [ DEFAULT_PORT, 2121 ]
|
||||
LIKELY_SERVICE_NAMES = [ 'ftp' ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = nil
|
||||
|
||||
# @!attribute ftp_timeout
|
||||
# @return [Fixnum] The timeout in seconds to wait for a response to an FTP command
|
||||
attr_accessor :ftp_timeout
|
||||
|
||||
validates :ftp_timeout,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 1
|
||||
}
|
||||
|
||||
|
||||
|
||||
# (see Base#attempt_login)
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
begin
|
||||
success = connect_login(credential.public, credential.private)
|
||||
rescue ::EOFError, Rex::AddressInUse, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error
|
||||
result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
success = false
|
||||
end
|
||||
|
||||
|
||||
if success
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
elsif !(result_options.has_key? :status)
|
||||
result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method sets the sane defaults for things
|
||||
# like timeouts and TCP evasion options
|
||||
def set_sane_defaults
|
||||
self.connection_timeout ||= 30
|
||||
self.port ||= DEFAULT_PORT
|
||||
self.max_send_size ||= 0
|
||||
self.send_delay ||= 0
|
||||
self.ftp_timeout ||= 16
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,123 @@
|
|||
require 'rex/proto/http'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
#
|
||||
# HTTP-specific login scanner.
|
||||
#
|
||||
class HTTP
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
|
||||
DEFAULT_REALM = nil
|
||||
DEFAULT_PORT = 80
|
||||
DEFAULT_SSL_PORT = 443
|
||||
LIKELY_PORTS = [ 80, 443, 8000, 8080 ]
|
||||
LIKELY_SERVICE_NAMES = [ 'http', 'https' ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
|
||||
|
||||
# @!attribute uri
|
||||
# @return [String] The path and query string on the server to
|
||||
# authenticate to.
|
||||
attr_accessor :uri
|
||||
|
||||
# @!attribute uri
|
||||
# @return [String] HTTP method, e.g. "GET", "POST"
|
||||
attr_accessor :method
|
||||
|
||||
validates :uri, presence: true, length: { minimum: 1 }
|
||||
|
||||
validates :method,
|
||||
presence: true,
|
||||
length: { minimum: 1 }
|
||||
|
||||
# Attempt a single login with a single credential against the target.
|
||||
#
|
||||
# @param credential [Credential] The credential object to attempt to
|
||||
# login with.
|
||||
# @return [Result] A Result object indicating success or failure
|
||||
def attempt_login(credential)
|
||||
ssl = false if ssl.nil?
|
||||
|
||||
result_opts = {
|
||||
credential: credential,
|
||||
status: Metasploit::Model::Login::Status::INCORRECT,
|
||||
proof: nil
|
||||
}
|
||||
|
||||
http_client = Rex::Proto::Http::Client.new(
|
||||
host, port, {}, ssl, ssl_version,
|
||||
nil, credential.public, credential.private
|
||||
)
|
||||
if credential.realm
|
||||
http_client.set_config('domain' => credential.realm)
|
||||
end
|
||||
|
||||
begin
|
||||
http_client.connect
|
||||
request = http_client.request_cgi(
|
||||
'uri' => uri,
|
||||
'method' => method
|
||||
)
|
||||
|
||||
# First try to connect without logging in to make sure this
|
||||
# resource requires authentication. We use #_send_recv for
|
||||
# that instead of #send_recv.
|
||||
response = http_client._send_recv(request)
|
||||
if response && response.code == 401 && response.headers['WWW-Authenticate']
|
||||
# Now send the creds
|
||||
response = http_client.send_auth(
|
||||
response, request.opts, connection_timeout, true
|
||||
)
|
||||
if response && response.code == 200
|
||||
result_opts.merge!(status: Metasploit::Model::Login::Status::SUCCESSFUL, proof: response.headers)
|
||||
end
|
||||
else
|
||||
result_opts.merge!(status: Metasploit::Model::Login::Status::NO_AUTH_REQUIRED)
|
||||
end
|
||||
rescue ::EOFError, Rex::ConnectionError, ::Timeout::Error
|
||||
result_opts.merge!(status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||
ensure
|
||||
http_client.close
|
||||
end
|
||||
|
||||
Result.new(result_opts)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method sets the sane defaults for things
|
||||
# like timeouts and TCP evasion options
|
||||
def set_sane_defaults
|
||||
self.connection_timeout ||= 20
|
||||
self.max_send_size = 0 if self.max_send_size.nil?
|
||||
self.send_delay = 0 if self.send_delay.nil?
|
||||
self.uri = '/' if self.uri.blank?
|
||||
self.method = 'GET' if self.method.blank?
|
||||
|
||||
# Note that this doesn't cover the case where ssl is unset and
|
||||
# port is something other than a default. In that situtation,
|
||||
# we don't know what the user has in mind so we have to trust
|
||||
# that they're going to do something sane.
|
||||
if !(self.ssl) && self.port.nil?
|
||||
self.port = self.class::DEFAULT_PORT
|
||||
self.ssl = false
|
||||
elsif self.ssl && self.port.nil?
|
||||
self.port = self.class::DEFAULT_SSL_PORT
|
||||
elsif self.ssl.nil? && self.port == self.class::DEFAULT_PORT
|
||||
self.ssl = false
|
||||
elsif self.ssl.nil? && self.port == self.class::DEFAULT_SSL_PORT
|
||||
self.ssl = true
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This class is the generic Exception raised by LoginScanners when
|
||||
# they fail validation. It rolls up all validation errors into a
|
||||
# single exception so that all errors can be dealt with at once.
|
||||
class Invalid < StandardError
|
||||
|
||||
attr_reader :model
|
||||
|
||||
def initialize(model)
|
||||
@model = model
|
||||
|
||||
errors = @model.errors.full_messages.join(', ')
|
||||
super(errors)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,74 @@
|
|||
require 'metasploit/framework/mssql/client'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
require 'metasploit/framework/login_scanner/ntlm'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with Microsoft SQL Servers.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results
|
||||
class MSSQL
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::LoginScanner::NTLM
|
||||
include Metasploit::Framework::MSSQL::Client
|
||||
|
||||
DEFAULT_PORT = 1433
|
||||
DEFAULT_REALM = 'WORKSTATION'
|
||||
# Lifted from lib/msf/core/exploit/mssql.rb
|
||||
LIKELY_PORTS = [ 1433, 1434, 1435, 14330, 2533, 9152, 2638 ]
|
||||
# Lifted from lib/msf/core/exploit/mssql.rb
|
||||
LIKELY_SERVICE_NAMES = [ 'ms-sql-s', 'ms-sql2000', 'sybase' ]
|
||||
PRIVATE_TYPES = [ :password, :ntlm_hash ]
|
||||
REALM_KEY = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
|
||||
|
||||
# @!attribute windows_authentication
|
||||
# @return [Boolean] Whether to use Windows Authentication instead of SQL Server Auth.
|
||||
attr_accessor :windows_authentication
|
||||
|
||||
validates :windows_authentication,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
begin
|
||||
if mssql_login(credential.public, credential.private, '', credential.realm)
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
else
|
||||
result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
|
||||
end
|
||||
rescue ::Rex::ConnectionError
|
||||
result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_sane_defaults
|
||||
self.connection_timeout ||= 30
|
||||
self.port ||= DEFAULT_PORT
|
||||
self.max_send_size ||= 0
|
||||
self.send_delay ||= 0
|
||||
|
||||
# Don't use ||= with booleans
|
||||
self.send_lm = true if self.send_lm.nil?
|
||||
self.send_ntlm = true if self.send_ntlm.nil?
|
||||
self.send_spn = true if self.send_spn.nil?
|
||||
self.use_lmkey = false if self.use_lmkey.nil?
|
||||
self.use_ntlm2_session = true if self.use_ntlm2_session.nil?
|
||||
self.use_ntlmv2 = true if self.use_ntlmv2.nil?
|
||||
self.windows_authentication = false if self.windows_authentication.nil?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,91 @@
|
|||
require 'metasploit/framework/tcp/client'
|
||||
require 'rbmysql'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with MySQL Database servers.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
class MySQL
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
|
||||
DEFAULT_PORT = 3306
|
||||
LIKELY_PORTS = [ 3306 ]
|
||||
LIKELY_SERVICE_NAMES = [ 'mysql' ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = nil
|
||||
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
# manage our behind the scenes socket. Close any existing one and open a new one
|
||||
disconnect if self.sock
|
||||
connect
|
||||
|
||||
begin
|
||||
::RbMysql.connect({
|
||||
:host => host,
|
||||
:port => port,
|
||||
:read_timeout => 300,
|
||||
:write_timeout => 300,
|
||||
:socket => sock,
|
||||
:user => credential.public,
|
||||
:password => credential.private,
|
||||
:db => ''
|
||||
})
|
||||
rescue Errno::ECONNREFUSED
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT,
|
||||
proof: "Connection refused"
|
||||
})
|
||||
rescue RbMysql::ClientError
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT,
|
||||
proof: "Connection timeout"
|
||||
})
|
||||
rescue Errno::ETIMEDOUT
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT,
|
||||
proof: "Operation Timed out"
|
||||
})
|
||||
rescue RbMysql::HostNotPrivileged
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT,
|
||||
proof: "Unable to login from this host due to policy"
|
||||
})
|
||||
rescue RbMysql::AccessDeniedError
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::INCORRECT,
|
||||
proof: "Access Denied"
|
||||
})
|
||||
end
|
||||
|
||||
unless result_options[:status]
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
|
||||
# This method sets the sane defaults for things
|
||||
# like timeouts and TCP evasion options
|
||||
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
|
|
@ -0,0 +1,61 @@
|
|||
require 'metasploit/framework/login_scanner'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This Concern provides the basic accessors and validations
|
||||
# for protocols that require the use of NTLM for Authentication.
|
||||
module NTLM
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::Validations
|
||||
|
||||
included do
|
||||
# @!attribute send_lm
|
||||
# @return [Boolean] Whether to always send the LANMAN response(except if using NTLM2 Session)
|
||||
attr_accessor :send_lm
|
||||
|
||||
# @!attribute send_ntlm
|
||||
# @return [Boolean] Whether to use NTLM responses
|
||||
attr_accessor :send_ntlm
|
||||
|
||||
# @!attribute send_spn
|
||||
# @return [Boolean] Whether to support SPN for newer Windows OSes
|
||||
attr_accessor :send_spn
|
||||
|
||||
# @!attribute use_lmkey
|
||||
# @return [Boolean] Whether to negotiate with a LANMAN key
|
||||
attr_accessor :use_lmkey
|
||||
|
||||
# @!attribute send_lm
|
||||
# @return [Boolean] Whether to force the use of NTLM2 session
|
||||
attr_accessor :use_ntlm2_session
|
||||
|
||||
# @!attribute send_lm
|
||||
# @return [Boolean] Whether to force the use of NTLMv2 instead of NTLM2 Session
|
||||
attr_accessor :use_ntlmv2
|
||||
|
||||
validates :send_lm,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :send_ntlm,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :send_spn,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :use_lmkey,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :use_ntlm2_session,
|
||||
inclusion: { in: [true, false] }
|
||||
|
||||
validates :use_ntlmv2,
|
||||
inclusion: { in: [true, false] }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,87 @@
|
|||
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 POP3.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
class POP3
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
|
||||
DEFAULT_PORT = 110
|
||||
LIKELY_PORTS = [ 110, 995 ]
|
||||
LIKELY_SERVICE_NAMES = [ 'pop3', 'pop3s' ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = nil
|
||||
|
||||
# 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
|
||||
}
|
||||
|
||||
disconnect if self.sock
|
||||
|
||||
begin
|
||||
connect
|
||||
select([sock],nil,nil,0.4)
|
||||
|
||||
# Check to see if we recieved an OK?
|
||||
result_options[:proof] = sock.get_once
|
||||
if result_options[:proof] && result_options[:proof][/^\+OK.*/]
|
||||
# If we received an OK we should send the USER
|
||||
sock.put("USER #{credential.public}\r\n")
|
||||
result_options[:proof] = sock.get_once
|
||||
|
||||
if result_options[:proof] && result_options[:proof][/^\+OK.*/]
|
||||
# If we got an OK after the username we can send the PASS
|
||||
sock.put("PASS #{credential.private}\r\n")
|
||||
# Dovecot has a failed-auth penalty system that maxes at
|
||||
# sleeping for 15 seconds before sending responses to the
|
||||
# PASS command, so bump the timeout to 16.
|
||||
result_options[:proof] = sock.get_once(-1, 16)
|
||||
|
||||
if result_options[:proof] && result_options[:proof][/^\+OK.*/]
|
||||
# if the pass gives an OK, were good to go
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
rescue Rex::ConnectionError, EOFError, Timeout::Error, Errno::EPIPE => e
|
||||
result_options.merge!(
|
||||
proof: e.message,
|
||||
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
)
|
||||
end
|
||||
|
||||
disconnect if self.sock
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'postgres_msf'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with PostgreSQL database servers.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
class Postgres
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
|
||||
DEFAULT_PORT = 5432
|
||||
DEFAULT_REALM = 'template1'
|
||||
LIKELY_PORTS = [ DEFAULT_PORT ]
|
||||
LIKELY_SERVICE_NAMES = [ 'postgres' ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = Metasploit::Model::Realm::Key::POSTGRESQL_DATABASE
|
||||
|
||||
# This method attempts a single login with a single credential against the target
|
||||
# @param credential [Credential] The credential object to attmpt to login with
|
||||
# @return [Metasploit::Framework::LoginScanner::Result] The LoginScanner Result object
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
db_name = credential.realm || 'template1'
|
||||
|
||||
if ::Rex::Socket.is_ipv6?(host)
|
||||
uri = "tcp://[#{host}]:#{port}"
|
||||
else
|
||||
uri = "tcp://#{host}:#{port}"
|
||||
end
|
||||
|
||||
pg_conn = nil
|
||||
|
||||
begin
|
||||
pg_conn = Msf::Db::PostgresPR::Connection.new(db_name,credential.public,credential.private,uri)
|
||||
rescue RuntimeError => e
|
||||
case e.to_s.split("\t")[1]
|
||||
when "C3D000"
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::INCORRECT,
|
||||
proof: "C3D000, Creds were good but database was bad"
|
||||
})
|
||||
when "C28000", "C28P01"
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::INCORRECT,
|
||||
proof: "Invalid username or password"
|
||||
})
|
||||
else
|
||||
result_options.merge!({
|
||||
status: Metasploit::Model::Login::Status::INCORRECT,
|
||||
proof: e.message
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if pg_conn
|
||||
pg_conn.close
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
else
|
||||
result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
end
|
||||
|
||||
def set_sane_defaults
|
||||
self.connection_timeout ||= 30
|
||||
self.port ||= DEFAULT_PORT
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,54 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# The Result class provides a standard structure in which
|
||||
# LoginScanners can return the result of a login attempt
|
||||
|
||||
class Result
|
||||
include ActiveModel::Validations
|
||||
|
||||
# @!attribute [r] access_level
|
||||
# @return [String] the access level gained
|
||||
attr_reader :access_level
|
||||
# @!attribute [r] credential
|
||||
# @return [Credential] the Credential object the result is for
|
||||
attr_reader :credential
|
||||
# @!attribute [r] proof
|
||||
# @return [String,nil] the proof that the lgoin was successful
|
||||
attr_reader :proof
|
||||
# @!attribute [r] status
|
||||
# @return [String] the status of the attempt. Should be a member of `Metasploit::Model::Login::Status::ALL`
|
||||
attr_reader :status
|
||||
|
||||
validates :status,
|
||||
inclusion: {
|
||||
in: Metasploit::Model::Login::Status::ALL
|
||||
}
|
||||
|
||||
# @param [Hash] opts The options hash for the initializer
|
||||
# @option opts [String] :private The private credential component
|
||||
# @option opts [String] :proof The proof that the login was successful
|
||||
# @option opts [String] :public The public credential component
|
||||
# @option opts [String] :realm The realm credential component
|
||||
# @option opts [String] :status The status code returned
|
||||
def initialize(opts= {})
|
||||
@access_level = opts.fetch(:access_level, nil)
|
||||
@credential = opts.fetch(:credential)
|
||||
@proof = opts.fetch(:proof, nil)
|
||||
@status = opts.fetch(:status)
|
||||
end
|
||||
|
||||
def success?
|
||||
status == Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{credential.public}:#{credential.private}@#{credential.realm} #{status} >"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,64 @@
|
|||
require 'metasploit/framework/login_scanner'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This module provides the common mixin behaviour for
|
||||
# LoginScanner objects that rely on Rex Sockets for their
|
||||
# underlying communication.
|
||||
module RexSocket
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
|
||||
# @!attribute max_send_size
|
||||
# @return [Fixnum] The max size of the data to encapsulate in a single packet
|
||||
attr_accessor :max_send_size
|
||||
# @!attribute send_delay
|
||||
# @return [Fixnum] The delay between sending packets
|
||||
attr_accessor :send_delay
|
||||
# @!attribute ssl
|
||||
# @return [Boolean] Whether the socket should use ssl
|
||||
attr_accessor :ssl
|
||||
# @!attribute ssl_version
|
||||
# @return [String] The version of SSL to implement
|
||||
attr_accessor :ssl_version
|
||||
|
||||
validates :max_send_size,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 0
|
||||
}
|
||||
|
||||
validates :send_delay,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 0
|
||||
}
|
||||
|
||||
|
||||
private
|
||||
|
||||
def chost
|
||||
'0.0.0.0'
|
||||
end
|
||||
|
||||
def cport
|
||||
0
|
||||
end
|
||||
|
||||
def rhost
|
||||
host
|
||||
end
|
||||
|
||||
def rport
|
||||
port
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,265 @@
|
|||
require 'rex/proto/smb'
|
||||
require 'metasploit/framework'
|
||||
require 'metasploit/framework/tcp/client'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
require 'metasploit/framework/login_scanner/ntlm'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with the Server Messaging
|
||||
# Block protocol.
|
||||
class SMB
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::LoginScanner::NTLM
|
||||
|
||||
# Constants to be used in {Result#access_level}
|
||||
module AccessLevels
|
||||
# Administrative access. For SMB, this is defined as being
|
||||
# able to successfully Tree Connect to the `ADMIN$` share.
|
||||
# This definition is not without its problems, but suffices to
|
||||
# conclude that such a user will most likely be able to use
|
||||
# psexec.
|
||||
ADMINISTRATOR = "Administrator"
|
||||
# Guest access means our creds were accepted but the logon
|
||||
# session is not associated with a real user account.
|
||||
GUEST = "Guest"
|
||||
end
|
||||
|
||||
CAN_GET_SESSION = true
|
||||
DEFAULT_REALM = 'WORKSTATION'
|
||||
LIKELY_PORTS = [ 139, 445 ]
|
||||
LIKELY_SERVICE_NAMES = [ "smb" ]
|
||||
PRIVATE_TYPES = [ :password, :ntlm_hash ]
|
||||
REALM_KEY = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
|
||||
|
||||
module StatusCodes
|
||||
CORRECT_CREDENTIAL_STATUS_CODES = [
|
||||
"STATUS_ACCOUNT_DISABLED",
|
||||
"STATUS_ACCOUNT_EXPIRED",
|
||||
"STATUS_ACCOUNT_RESTRICTION",
|
||||
"STATUS_INVALID_LOGON_HOURS",
|
||||
"STATUS_INVALID_WORKSTATION",
|
||||
"STATUS_LOGON_TYPE_NOT_GRANTED",
|
||||
"STATUS_PASSWORD_EXPIRED",
|
||||
"STATUS_PASSWORD_MUST_CHANGE",
|
||||
].freeze.map(&:freeze)
|
||||
end
|
||||
|
||||
|
||||
# @!attribute simple
|
||||
# @return [Rex::Proto::SMB::SimpleClient]
|
||||
attr_accessor :simple
|
||||
|
||||
attr_accessor :smb_chunk_size
|
||||
attr_accessor :smb_name
|
||||
attr_accessor :smb_native_lm
|
||||
attr_accessor :smb_native_os
|
||||
attr_accessor :smb_obscure_trans_pipe_level
|
||||
attr_accessor :smb_pad_data_level
|
||||
attr_accessor :smb_pad_file_level
|
||||
attr_accessor :smb_pipe_evasion
|
||||
|
||||
# UNUSED
|
||||
#attr_accessor :smb_pipe_read_max_size
|
||||
#attr_accessor :smb_pipe_read_min_size
|
||||
#attr_accessor :smb_pipe_write_max_size
|
||||
#attr_accessor :smb_pipe_write_min_size
|
||||
attr_accessor :smb_verify_signature
|
||||
|
||||
attr_accessor :smb_direct
|
||||
|
||||
validates :smb_chunk_size,
|
||||
numericality:
|
||||
{
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 0
|
||||
}
|
||||
|
||||
validates :smb_obscure_trans_pipe_level,
|
||||
inclusion:
|
||||
{
|
||||
in: Rex::Proto::SMB::Evasions::EVASION_NONE .. Rex::Proto::SMB::Evasions::EVASION_MAX
|
||||
}
|
||||
|
||||
validates :smb_pad_data_level,
|
||||
inclusion:
|
||||
{
|
||||
in: Rex::Proto::SMB::Evasions::EVASION_NONE .. Rex::Proto::SMB::Evasions::EVASION_MAX
|
||||
}
|
||||
|
||||
validates :smb_pad_file_level,
|
||||
inclusion:
|
||||
{
|
||||
in: Rex::Proto::SMB::Evasions::EVASION_NONE .. Rex::Proto::SMB::Evasions::EVASION_MAX
|
||||
}
|
||||
|
||||
validates :smb_pipe_evasion,
|
||||
inclusion: { in: [true, false, nil] },
|
||||
allow_nil: true
|
||||
|
||||
# UNUSED
|
||||
#validates :smb_pipe_read_max_size, numericality: { only_integer: true }
|
||||
#validates :smb_pipe_read_min_size, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
#validates :smb_pipe_write_max_size, numericality: { only_integer: true }
|
||||
#validates :smb_pipe_write_min_size, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
|
||||
validates :smb_verify_signature,
|
||||
inclusion: { in: [true, false, nil] },
|
||||
allow_nil: true
|
||||
|
||||
|
||||
# If login is successul and {Result#access_level} is not set
|
||||
# then arbitrary credentials are accepted. If it is set to
|
||||
# Guest, then arbitrary credentials are accepted, but given
|
||||
# Guest permissions.
|
||||
#
|
||||
# @param domain [String] Domain to authenticate against. Use an
|
||||
# empty string for local accounts.
|
||||
# @return [Result]
|
||||
def attempt_bogus_login(domain)
|
||||
if defined?(@result_for_bogus)
|
||||
return @result_for_bogus
|
||||
end
|
||||
cred = Credential.new(
|
||||
public: Rex::Text.rand_text_alpha(8),
|
||||
private: Rex::Text.rand_text_alpha(8),
|
||||
realm: domain
|
||||
)
|
||||
@result_for_bogus = attempt_login(cred)
|
||||
end
|
||||
|
||||
|
||||
# (see Base#attempt_login)
|
||||
def attempt_login(credential)
|
||||
|
||||
# Disable direct SMB when SMBDirect has not been set and the
|
||||
# destination port is configured as 139
|
||||
if self.smb_direct.nil?
|
||||
self.smb_direct = case self.port
|
||||
when 139 then false
|
||||
when 445 then true
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
connect
|
||||
rescue ::Rex::ConnectionError => e
|
||||
return Result.new(credential:credential, status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT, proof: e)
|
||||
end
|
||||
proof = nil
|
||||
|
||||
begin
|
||||
# TODO: OMG
|
||||
simple.login(
|
||||
smb_name,
|
||||
credential.public,
|
||||
credential.private,
|
||||
credential.realm || "",
|
||||
smb_verify_signature,
|
||||
use_ntlmv2,
|
||||
use_ntlm2_session,
|
||||
send_lm,
|
||||
use_lmkey,
|
||||
send_ntlm,
|
||||
smb_native_os,
|
||||
smb_native_lm,
|
||||
{
|
||||
use_spn: send_spn,
|
||||
name: host
|
||||
}
|
||||
)
|
||||
|
||||
# Windows SMB will return an error code during Session
|
||||
# Setup, but nix Samba requires a Tree Connect. Try admin$
|
||||
# first, since that will tell us if this user has local
|
||||
# admin access. Fall back to IPC$ which should be accessible
|
||||
# to any user with valid creds.
|
||||
begin
|
||||
simple.connect("\\\\#{host}\\admin$")
|
||||
access_level = AccessLevels::ADMINISTRATOR
|
||||
simple.disconnect("\\\\#{host}\\admin$")
|
||||
rescue ::Rex::Proto::SMB::Exceptions::ErrorCode
|
||||
simple.connect("\\\\#{host}\\IPC$")
|
||||
end
|
||||
|
||||
# If we made it this far without raising, we have a valid
|
||||
# login
|
||||
status = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
rescue ::Rex::Proto::SMB::Exceptions::LoginError => e
|
||||
status = case e.get_error(e.error_code)
|
||||
when *StatusCodes::CORRECT_CREDENTIAL_STATUS_CODES
|
||||
Metasploit::Model::Login::Status::DENIED_ACCESS
|
||||
when 'STATUS_LOGON_FAILURE', 'STATUS_ACCESS_DENIED'
|
||||
Metasploit::Model::Login::Status::INCORRECT
|
||||
else
|
||||
Metasploit::Model::Login::Status::INCORRECT
|
||||
end
|
||||
|
||||
proof = e
|
||||
rescue ::Rex::Proto::SMB::Exceptions::Error => e
|
||||
status = Metasploit::Model::Login::Status::INCORRECT
|
||||
proof = e
|
||||
rescue ::Rex::ConnectionError
|
||||
status = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
end
|
||||
|
||||
if status == Metasploit::Model::Login::Status::SUCCESSFUL && simple.client.auth_user.nil?
|
||||
access_level ||= AccessLevels::GUEST
|
||||
end
|
||||
|
||||
Result.new(credential: credential, status: status, proof: proof, access_level: access_level)
|
||||
end
|
||||
|
||||
def connect
|
||||
disconnect
|
||||
self.sock = super
|
||||
|
||||
c = Rex::Proto::SMB::SimpleClient.new(sock, smb_direct)
|
||||
|
||||
c.client.evasion_opts['pad_data'] = smb_pad_data_level
|
||||
c.client.evasion_opts['pad_file'] = smb_pad_file_level
|
||||
c.client.evasion_opts['obscure_trans_pipe'] = smb_obscure_trans_pipe_level
|
||||
|
||||
self.simple = c
|
||||
c
|
||||
end
|
||||
|
||||
def set_sane_defaults
|
||||
self.connection_timeout = 10 if self.connection_timeout.nil?
|
||||
self.max_send_size = 0 if self.max_send_size.nil?
|
||||
self.send_delay = 0 if self.send_delay.nil?
|
||||
self.send_lm = true if self.send_lm.nil?
|
||||
self.send_ntlm = true if self.send_ntlm.nil?
|
||||
self.send_spn = true if self.send_spn.nil?
|
||||
self.smb_chunk_size = 0 if self.smb_chunk_size.nil?
|
||||
self.smb_name = "*SMBSERVER" if self.smb_name.nil?
|
||||
self.smb_native_lm = "Windows 2000 5.0" if self.smb_native_os.nil?
|
||||
self.smb_native_os = "Windows 2000 2195" if self.smb_native_os.nil?
|
||||
self.smb_obscure_trans_pipe_level = 0 if self.smb_obscure_trans_pipe_level.nil?
|
||||
self.smb_pad_data_level = 0 if self.smb_pad_data_level.nil?
|
||||
self.smb_pad_file_level = 0 if self.smb_pad_file_level.nil?
|
||||
self.smb_pipe_evasion = false if self.smb_pipe_evasion.nil?
|
||||
#self.smb_pipe_read_max_size = 1024 if self.smb_pipe_read_max_size.nil?
|
||||
#self.smb_pipe_read_min_size = 0 if self.smb_pipe_read_min_size.nil?
|
||||
#self.smb_pipe_write_max_size = 1024 if self.smb_pipe_write_max_size.nil?
|
||||
#self.smb_pipe_write_min_size = 0 if self.smb_pipe_write_min_size.nil?
|
||||
self.smb_verify_signature = false if self.smb_verify_signature.nil?
|
||||
|
||||
self.use_lmkey = true if self.use_lmkey.nil?
|
||||
self.use_ntlm2_session = true if self.use_ntlm2_session.nil?
|
||||
self.use_ntlmv2 = true if self.use_ntlmv2.nil?
|
||||
|
||||
self.smb_name = self.host if self.smb_name.nil?
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
require 'snmp'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with SNMP.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
class SNMP
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
|
||||
DEFAULT_PORT = 161
|
||||
LIKELY_PORTS = [ 161, 162 ]
|
||||
LIKELY_SERVICE_NAMES = [ 'snmp' ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = nil
|
||||
|
||||
# This method attempts a single login with a single credential against the target
|
||||
# @param credential [Credential] The credential object to attmpt to login with
|
||||
# @return [Metasploit::Framework::LoginScanner::Result] The LoginScanner Result object
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
[:SNMPv1, :SNMPv2c].each do |version|
|
||||
snmp_client = ::SNMP::Manager.new(
|
||||
:Host => host,
|
||||
:Port => port,
|
||||
:Community => credential.public,
|
||||
:Version => version,
|
||||
:Timeout => connection_timeout,
|
||||
:Retries => 2,
|
||||
:Transport => ::SNMP::RexUDPTransport,
|
||||
:Socket => ::Rex::Socket::Udp.create
|
||||
)
|
||||
|
||||
result_options[:proof] = test_read_access(snmp_client)
|
||||
if result_options[:proof].nil?
|
||||
result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
|
||||
else
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
if has_write_access?(snmp_client, result_options[:proof])
|
||||
result_options[:access_level] = "read-write"
|
||||
else
|
||||
result_options[:access_level] = "read-only"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method takes an snmp client and tests whether
|
||||
# it has write access to the remote system. It sets the
|
||||
# the sysDescr oid to the same value we already read.
|
||||
# @param snmp_client [SNMP::Manager] The SNMP client to use
|
||||
# @param value [String] the value to set sysDescr back to
|
||||
# @return [Boolean] Returns true or false for if we have write access
|
||||
def has_write_access?(snmp_client, value)
|
||||
var_bind = ::SNMP::VarBind.new("1.3.6.1.2.1.1.1.0", ::SNMP::OctetString.new(value))
|
||||
begin
|
||||
resp = snmp_client.set(var_bind)
|
||||
if resp.error_status == :noError
|
||||
return true
|
||||
end
|
||||
rescue RuntimeError
|
||||
return false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Sets the connection timeout approrpiately for SNMP
|
||||
# if the user did not set it.
|
||||
def set_sane_defaults
|
||||
self.connection_timeout = 2 if self.connection_timeout.nil?
|
||||
self.port = DEFAULT_PORT if self.port.nil?
|
||||
end
|
||||
|
||||
# This method takes an snmp client and tests whether
|
||||
# it has read access to the remote system. It checks
|
||||
# the sysDescr oid to use as proof
|
||||
# @param snmp_client [SNMP::Manager] The SNMP client to use
|
||||
# @return [String, nil] Returns a string if successful, nil if failed
|
||||
def test_read_access(snmp_client)
|
||||
proof = nil
|
||||
begin
|
||||
resp = snmp_client.get("sysDescr.0")
|
||||
resp.each_varbind { |var| proof = var.value }
|
||||
rescue RuntimeError
|
||||
proof = nil
|
||||
end
|
||||
proof
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,134 @@
|
|||
require 'net/ssh'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with the Secure Shell protocol.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
#
|
||||
class SSH
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
|
||||
#
|
||||
# CONSTANTS
|
||||
#
|
||||
|
||||
CAN_GET_SESSION = true
|
||||
DEFAULT_PORT = 22
|
||||
LIKELY_PORTS = [ DEFAULT_PORT ]
|
||||
LIKELY_SERVICE_NAMES = [ 'ssh' ]
|
||||
PRIVATE_TYPES = [ :password, :ssh_key ]
|
||||
REALM_KEY = nil
|
||||
|
||||
VERBOSITIES = [
|
||||
:debug,
|
||||
:info,
|
||||
:warn,
|
||||
:error,
|
||||
:fatal
|
||||
]
|
||||
# @!attribute ssh_socket
|
||||
# @return [Net::SSH::Connection::Session] The current SSH connection
|
||||
attr_accessor :ssh_socket
|
||||
# @!attribute verbosity
|
||||
# The verbosity level for the SSH client.
|
||||
#
|
||||
# @return [Symbol] An element of {VERBOSITIES}.
|
||||
attr_accessor :verbosity
|
||||
|
||||
validates :verbosity,
|
||||
presence: true,
|
||||
inclusion: { in: VERBOSITIES }
|
||||
|
||||
# (see {Base#attempt_login})
|
||||
# @note The caller *must* close {#ssh_socket}
|
||||
def attempt_login(credential)
|
||||
self.ssh_socket = nil
|
||||
opt_hash = {
|
||||
:port => port,
|
||||
:disable_agent => true,
|
||||
:config => false,
|
||||
:verbose => verbosity,
|
||||
:proxies => proxies
|
||||
}
|
||||
case credential.private_type
|
||||
when :password, nil
|
||||
opt_hash.update(
|
||||
:auth_methods => ['password','keyboard-interactive'],
|
||||
:password => credential.private,
|
||||
)
|
||||
when :ssh_key
|
||||
opt_hash.update(
|
||||
:auth_methods => ['publickey'],
|
||||
:key_data => credential.private,
|
||||
)
|
||||
end
|
||||
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
begin
|
||||
::Timeout.timeout(connection_timeout) do
|
||||
self.ssh_socket = Net::SSH.start(
|
||||
host,
|
||||
credential.public,
|
||||
opt_hash
|
||||
)
|
||||
end
|
||||
rescue ::EOFError, Net::SSH::Disconnect, Rex::AddressInUse, Rex::ConnectionError, ::Timeout::Error
|
||||
result_options.merge!( proof: nil, status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT)
|
||||
rescue Net::SSH::Exception
|
||||
result_options.merge!( proof: nil, status: Metasploit::Model::Login::Status::INCORRECT)
|
||||
end
|
||||
|
||||
unless result_options.has_key? :status
|
||||
if ssh_socket
|
||||
proof = gather_proof
|
||||
result_options.merge!( proof: proof, status: Metasploit::Model::Login::Status::SUCCESSFUL)
|
||||
else
|
||||
result_options.merge!( proof: nil, status: Metasploit::Model::Login::Status::INCORRECT)
|
||||
end
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method attempts to gather proof that we successfuly logged in.
|
||||
# @return [String] The proof of a connection, May be empty.
|
||||
def gather_proof
|
||||
proof = ''
|
||||
begin
|
||||
Timeout.timeout(5) do
|
||||
proof = ssh_socket.exec!("id\n").to_s
|
||||
if(proof =~ /id=/)
|
||||
proof << ssh_socket.exec!("uname -a\n").to_s
|
||||
else
|
||||
# Cisco IOS
|
||||
if proof =~ /Unknown command or computer name/
|
||||
proof = ssh_socket.exec!("ver\n").to_s
|
||||
else
|
||||
proof << ssh_socket.exec!("help\n?\n\n\n").to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue ::Exception
|
||||
end
|
||||
proof
|
||||
end
|
||||
|
||||
def set_sane_defaults
|
||||
self.connection_timeout = 30 if self.connection_timeout.nil?
|
||||
self.port = DEFAULT_PORT if self.port.nil?
|
||||
self.verbosity = :fatal if self.verbosity.nil?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,113 @@
|
|||
require 'metasploit/framework/telnet/client'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# This is the LoginScanner class for dealing with Telnet remote terminals.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
class Telnet
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::Telnet::Client
|
||||
|
||||
CAN_GET_SESSION = true
|
||||
DEFAULT_PORT = 23
|
||||
LIKELY_PORTS = [ DEFAULT_PORT ]
|
||||
LIKELY_SERVICE_NAMES = [ 'telnet' ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = nil
|
||||
|
||||
# @!attribute verbosity
|
||||
# The timeout to wait for the telnet banner.
|
||||
#
|
||||
# @return [Fixnum]
|
||||
attr_accessor :banner_timeout
|
||||
# @!attribute verbosity
|
||||
# The timeout to wait for the response from a telnet command.
|
||||
#
|
||||
# @return [Fixnum]
|
||||
attr_accessor :telnet_timeout
|
||||
|
||||
validates :banner_timeout,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 1
|
||||
}
|
||||
|
||||
validates :telnet_timeout,
|
||||
presence: true,
|
||||
numericality: {
|
||||
only_integer: true,
|
||||
greater_than_or_equal_to: 1
|
||||
}
|
||||
|
||||
# (see {Base#attempt_login})
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
if connect_reset_safe == :refused
|
||||
result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
else
|
||||
if busy_message?
|
||||
self.sock.close unless self.sock.closed?
|
||||
result_options[:status] = Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
end
|
||||
end
|
||||
|
||||
unless result_options[:status]
|
||||
unless password_prompt?
|
||||
send_user(credential.public)
|
||||
end
|
||||
|
||||
recvd_sample = @recvd.dup
|
||||
# Allow for slow echos
|
||||
1.upto(10) do
|
||||
recv_telnet(self.sock, 0.10) unless @recvd.nil? or @recvd[/#{@password_prompt}/]
|
||||
end
|
||||
|
||||
if password_prompt?(credential.public)
|
||||
send_pass(credential.private)
|
||||
|
||||
# Allow for slow echos
|
||||
1.upto(10) do
|
||||
recv_telnet(self.sock, 0.10) if @recvd == recvd_sample
|
||||
end
|
||||
end
|
||||
|
||||
if login_succeeded?
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
else
|
||||
result_options[:status] = Metasploit::Model::Login::Status::INCORRECT
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# This method sets the sane defaults for things
|
||||
# like timeouts and TCP evasion options
|
||||
def set_sane_defaults
|
||||
self.connection_timeout ||= 30
|
||||
self.max_send_size ||= 0
|
||||
self.port ||= DEFAULT_PORT
|
||||
self.send_delay ||= 0
|
||||
self.banner_timeout ||= 25
|
||||
self.telnet_timeout ||= 10
|
||||
self.connection_timeout ||= 30
|
||||
# Shim to set up the ivars from the old Login mixin
|
||||
create_login_ivars
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
|
||||
require 'metasploit/framework/login_scanner/http'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# Tomcat Manager login scanner
|
||||
class Tomcat < HTTP
|
||||
|
||||
# Inherit LIKELY_PORTS,LIKELY_SERVICE_NAMES, and REALM_KEY from HTTP
|
||||
CAN_GET_SESSION = true
|
||||
DEFAULT_PORT = 8180
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
|
||||
# (see Base#set_sane_defaults)
|
||||
def set_sane_defaults
|
||||
self.uri = "/manager/html" if self.uri.nil?
|
||||
self.method = "GET" if self.method.nil?
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
require 'metasploit/framework/tcp/client'
|
||||
require 'rex/proto/rfb'
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
# This is the LoginScanner class for dealing with the VNC RFB protocol.
|
||||
# It is responsible for taking a single target, and a list of credentials
|
||||
# and attempting them. It then saves the results.
|
||||
class VNC
|
||||
include Metasploit::Framework::LoginScanner::Base
|
||||
include Metasploit::Framework::LoginScanner::RexSocket
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
|
||||
|
||||
#
|
||||
# CONSTANTS
|
||||
#
|
||||
|
||||
LIKELY_PORTS = (5900..5910).to_a
|
||||
LIKELY_SERVICE_NAMES = [ 'vnc' ]
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
REALM_KEY = nil
|
||||
|
||||
# Error indicating retry should occur for UltraVNC
|
||||
ULTRA_VNC_RETRY_ERROR = 'connection has been rejected'
|
||||
# Error indicating retry should occur for VNC 4 Server
|
||||
VNC4_SERVER_RETRY_ERROR = 'Too many security failures'
|
||||
# Known retry errors for all supported versions of VNC
|
||||
RETRY_ERRORS = [
|
||||
ULTRA_VNC_RETRY_ERROR,
|
||||
VNC4_SERVER_RETRY_ERROR
|
||||
]
|
||||
|
||||
# This method attempts a single login with a single credential against the target
|
||||
# @param credential [Credential] The credential object to attmpt to login with
|
||||
# @return [Metasploit::Framework::LoginScanner::Result] The LoginScanner Result object
|
||||
def attempt_login(credential)
|
||||
result_options = {
|
||||
credential: credential
|
||||
}
|
||||
|
||||
begin
|
||||
# Make our initial socket to the target
|
||||
disconnect if self.sock
|
||||
connect
|
||||
|
||||
# Create our VNC client overtop of the socket
|
||||
vnc = Rex::Proto::RFB::Client.new(sock, :allow_none => false)
|
||||
|
||||
|
||||
if vnc.handshake
|
||||
if vnc_auth(vnc,credential.private)
|
||||
result_options[:status] = Metasploit::Model::Login::Status::SUCCESSFUL
|
||||
else
|
||||
result_options.merge!(
|
||||
proof: vnc.error,
|
||||
status: Metasploit::Model::Login::Status::INCORRECT
|
||||
)
|
||||
end
|
||||
else
|
||||
result_options.merge!(
|
||||
proof: vnc.error,
|
||||
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
)
|
||||
end
|
||||
rescue ::EOFError, Errno::ENOTCONN, Rex::AddressInUse, Rex::ConnectionError, Rex::ConnectionTimeout, ::Timeout::Error => e
|
||||
result_options.merge!(
|
||||
proof: e.message,
|
||||
status: Metasploit::Model::Login::Status::UNABLE_TO_CONNECT
|
||||
)
|
||||
end
|
||||
|
||||
::Metasploit::Framework::LoginScanner::Result.new(result_options)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Check the VNC error to see if we should wait and retry.
|
||||
#
|
||||
# @param error [String] The VNC error message received
|
||||
# @return [false] don't retry
|
||||
# @return [true] retry
|
||||
def retry?(error)
|
||||
RETRY_ERRORS.include?(error)
|
||||
end
|
||||
|
||||
# This method sets the sane defaults for things
|
||||
# like timeouts and TCP evasion options
|
||||
def set_sane_defaults
|
||||
self.connection_timeout ||= 30
|
||||
self.port ||= 5900
|
||||
self.max_send_size ||= 0
|
||||
self.send_delay ||= 0
|
||||
end
|
||||
|
||||
# This method attempts the actual VNC authentication. It has built in retries to handle
|
||||
# delays built into the VNC RFB authentication.
|
||||
# @param client [Rex::Proto::RFB::Client] The VNC client object to authenticate through
|
||||
# @param password [String] the password to attempt the authentication with
|
||||
def vnc_auth(client,password)
|
||||
success = false
|
||||
5.times do |n|
|
||||
if client.authenticate(password)
|
||||
success = true
|
||||
break
|
||||
end
|
||||
break unless retry?(client.error)
|
||||
|
||||
# Wait for an increasing ammount of time before retrying
|
||||
delay = (2**(n+1)) + 1
|
||||
::Rex.sleep(delay)
|
||||
end
|
||||
success
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,52 @@
|
|||
|
||||
require 'metasploit/framework/login_scanner/base'
|
||||
require 'metasploit/framework/login_scanner/rex_socket'
|
||||
require 'metasploit/framework/login_scanner/http'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module LoginScanner
|
||||
|
||||
# Windows Remote Management login scanner
|
||||
class WinRM < HTTP
|
||||
|
||||
# The default port where WinRM listens. This is what you get on
|
||||
# v1.1+ with `winrm quickconfig`. Note that before v1.1, the
|
||||
# default was 80
|
||||
DEFAULT_PORT = 5985
|
||||
|
||||
# The default realm is WORKSTATION which tells Windows authentication
|
||||
# that it is a Local Account.
|
||||
DEFAULT_REALM = 'WORKSTATION'
|
||||
|
||||
# The default port where WinRM listens when SSL is enabled. Note
|
||||
# that before v1.1, the default was 443
|
||||
DEFAULT_SSL_PORT = 5986
|
||||
|
||||
PRIVATE_TYPES = [ :password ]
|
||||
LIKELY_PORTS = [ 80, 443, 5985, 5986 ]
|
||||
REALM_KEY = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
|
||||
# Inherit LIKELY_SERVICE_NAMES, since a scanner will see it as
|
||||
# just HTTP.
|
||||
|
||||
validates :method, inclusion: { in: ["POST"] }
|
||||
|
||||
# (see Base#set_sane_defaults)
|
||||
def set_sane_defaults
|
||||
self.uri = "/wsman" if self.uri.nil?
|
||||
@method = "POST".freeze
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
# The method *must* be "POST", so don't let the user change it
|
||||
# @raise [RuntimeError] Unconditionally
|
||||
def method=(_)
|
||||
raise RuntimeError, "Method must be POST for WinRM"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,728 @@
|
|||
require 'metasploit/framework/tcp/client'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module MSSQL
|
||||
|
||||
module Client
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
|
||||
NTLM_CRYPT = Rex::Proto::NTLM::Crypt
|
||||
NTLM_CONST = Rex::Proto::NTLM::Constants
|
||||
NTLM_UTILS = Rex::Proto::NTLM::Utils
|
||||
NTLM_XCEPT = Rex::Proto::NTLM::Exceptions
|
||||
|
||||
# Encryption
|
||||
ENCRYPT_OFF = 0x00 #Encryption is available but off.
|
||||
ENCRYPT_ON = 0x01 #Encryption is available and on.
|
||||
ENCRYPT_NOT_SUP = 0x02 #Encryption is not available.
|
||||
ENCRYPT_REQ = 0x03 #Encryption is required.
|
||||
|
||||
# Packet Type
|
||||
TYPE_SQL_BATCH = 1 # (Client) SQL command
|
||||
TYPE_PRE_TDS7_LOGIN = 2 # (Client) Pre-login with version < 7 (unused)
|
||||
TYPE_RPC = 3 # (Client) RPC
|
||||
TYPE_TABLE_RESPONSE = 4 # (Server) Pre-Login Response ,Login Response, Row Data, Return Status, Return Parameters,
|
||||
# Request Completion, Error and Info Messages, Attention Acknowledgement
|
||||
TYPE_ATTENTION_SIGNAL = 6 # (Client) Attention
|
||||
TYPE_BULK_LOAD = 7 # (Client) SQL Command with binary data
|
||||
TYPE_TRANSACTION_MANAGER_REQUEST = 14 # (Client) Transaction request manager
|
||||
TYPE_TDS7_LOGIN = 16 # (Client) Login
|
||||
TYPE_SSPI_MESSAGE = 17 # (Client) Login
|
||||
TYPE_PRE_LOGIN_MESSAGE = 18 # (Client) pre-login with version > 7
|
||||
|
||||
# Status
|
||||
STATUS_NORMAL = 0x00
|
||||
STATUS_END_OF_MESSAGE = 0x01
|
||||
STATUS_IGNORE_EVENT = 0x02
|
||||
STATUS_RESETCONNECTION = 0x08 # TDS 7.1+
|
||||
STATUS_RESETCONNECTIONSKIPTRAN = 0x10 # TDS 7.3+
|
||||
|
||||
#
|
||||
# This method connects to the server over TCP and attempts
|
||||
# to authenticate with the supplied username and password
|
||||
# The global socket is used and left connected after auth
|
||||
#
|
||||
def mssql_login(user='sa', pass='', db='', domain_name='')
|
||||
|
||||
disconnect if self.sock
|
||||
connect
|
||||
|
||||
# Send a prelogin packet and check that encryption is not enabled
|
||||
if mssql_prelogin() != ENCRYPT_NOT_SUP
|
||||
print_error("Encryption is not supported")
|
||||
return false
|
||||
end
|
||||
|
||||
if windows_authentication
|
||||
idx = 0
|
||||
pkt = ''
|
||||
pkt_hdr = ''
|
||||
pkt_hdr = [
|
||||
TYPE_TDS7_LOGIN, #type
|
||||
STATUS_END_OF_MESSAGE, #status
|
||||
0x0000, #length
|
||||
0x0000, # SPID
|
||||
0x01, # PacketID (unused upon specification
|
||||
# but ms network monitor stil prefer 1 to decode correctly, wireshark don't care)
|
||||
0x00 #Window
|
||||
]
|
||||
|
||||
pkt << [
|
||||
0x00000000, # Size
|
||||
0x71000001, # TDS Version
|
||||
0x00000000, # Dummy Size
|
||||
0x00000007, # Version
|
||||
rand(1024+1), # PID
|
||||
0x00000000, # ConnectionID
|
||||
0xe0, # Option Flags 1
|
||||
0x83, # Option Flags 2
|
||||
0x00, # SQL Type Flags
|
||||
0x00, # Reserved Flags
|
||||
0x00000000, # Time Zone
|
||||
0x00000000 # Collation
|
||||
].pack('VVVVVVCCCCVV')
|
||||
|
||||
cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
|
||||
aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) ) #application and library name
|
||||
sname = Rex::Text.to_unicode( rhost )
|
||||
dname = Rex::Text.to_unicode( db )
|
||||
|
||||
ntlm_options = {
|
||||
:signing => false,
|
||||
:usentlm2_session => use_ntlm2_session,
|
||||
:use_ntlmv2 => use_ntlmv2,
|
||||
:send_lm => send_lm,
|
||||
:send_ntlm => send_ntlm
|
||||
}
|
||||
|
||||
ntlmssp_flags = NTLM_UTILS.make_ntlm_flags(ntlm_options)
|
||||
workstation_name = Rex::Text.rand_text_alpha(rand(8)+1)
|
||||
|
||||
ntlmsspblob = NTLM_UTILS::make_ntlmssp_blob_init(domain_name, workstation_name, ntlmssp_flags)
|
||||
|
||||
idx = pkt.size + 50 # lengths below
|
||||
|
||||
pkt << [idx, cname.length / 2].pack('vv')
|
||||
idx += cname.length
|
||||
|
||||
pkt << [0, 0].pack('vv') # User length offset must be 0
|
||||
pkt << [0, 0].pack('vv') # Password length offset must be 0
|
||||
|
||||
pkt << [idx, aname.length / 2].pack('vv')
|
||||
idx += aname.length
|
||||
|
||||
pkt << [idx, sname.length / 2].pack('vv')
|
||||
idx += sname.length
|
||||
|
||||
pkt << [0, 0].pack('vv') # unused
|
||||
|
||||
pkt << [idx, aname.length / 2].pack('vv')
|
||||
idx += aname.length
|
||||
|
||||
pkt << [idx, 0].pack('vv') # locales
|
||||
|
||||
pkt << [idx, 0].pack('vv') #db
|
||||
|
||||
# ClientID (should be mac address)
|
||||
pkt << Rex::Text.rand_text(6)
|
||||
|
||||
# NTLMSSP
|
||||
pkt << [idx, ntlmsspblob.length].pack('vv')
|
||||
idx += ntlmsspblob.length
|
||||
|
||||
pkt << [idx, 0].pack('vv') # AtchDBFile
|
||||
|
||||
pkt << cname
|
||||
pkt << aname
|
||||
pkt << sname
|
||||
pkt << aname
|
||||
pkt << ntlmsspblob
|
||||
|
||||
# Total packet length
|
||||
pkt[0,4] = [pkt.length].pack('V')
|
||||
|
||||
pkt_hdr[2] = pkt.length + 8
|
||||
|
||||
pkt = pkt_hdr.pack("CCnnCC") + pkt
|
||||
|
||||
# Rem : One have to set check_status to false here because sql server sp0 (and maybe above)
|
||||
# has a strange behavior that differs from the specifications
|
||||
# upon receiving the ntlm_negociate request it send an ntlm_challenge but the status flag of the tds packet header
|
||||
# is set to STATUS_NORMAL and not STATUS_END_OF_MESSAGE, then internally it waits for the ntlm_authentification
|
||||
resp = mssql_send_recv(pkt,15, false)
|
||||
|
||||
# Get default data
|
||||
begin
|
||||
blob_data = NTLM_UTILS.parse_ntlm_type_2_blob(resp)
|
||||
# a domain.length < 3 will hit this
|
||||
rescue NTLM_XCEPT::NTLMMissingChallenge
|
||||
return false
|
||||
end
|
||||
|
||||
challenge_key = blob_data[:challenge_key]
|
||||
server_ntlmssp_flags = blob_data[:server_ntlmssp_flags] #else should raise an error
|
||||
#netbios name
|
||||
default_name = blob_data[:default_name] || ''
|
||||
#netbios domain
|
||||
default_domain = blob_data[:default_domain] || ''
|
||||
#dns name
|
||||
dns_host_name = blob_data[:dns_host_name] || ''
|
||||
#dns domain
|
||||
dns_domain_name = blob_data[:dns_domain_name] || ''
|
||||
#Client time
|
||||
chall_MsvAvTimestamp = blob_data[:chall_MsvAvTimestamp] || ''
|
||||
|
||||
spnopt = {:use_spn => send_spn, :name => self.rhost}
|
||||
|
||||
resp_lm, resp_ntlm, client_challenge, ntlm_cli_challenge = NTLM_UTILS.create_lm_ntlm_responses(user, pass, challenge_key,
|
||||
domain_name, default_name, default_domain,
|
||||
dns_host_name, dns_domain_name, chall_MsvAvTimestamp,
|
||||
spnopt, ntlm_options)
|
||||
|
||||
ntlmssp = NTLM_UTILS.make_ntlmssp_blob_auth(domain_name, workstation_name, user, resp_lm, resp_ntlm, '', ntlmssp_flags)
|
||||
|
||||
# Create an SSPIMessage
|
||||
idx = 0
|
||||
pkt = ''
|
||||
pkt_hdr = ''
|
||||
pkt_hdr = [
|
||||
TYPE_SSPI_MESSAGE, #type
|
||||
STATUS_END_OF_MESSAGE, #status
|
||||
0x0000, #length
|
||||
0x0000, # SPID
|
||||
0x01, # PacketID
|
||||
0x00 #Window
|
||||
]
|
||||
|
||||
pkt_hdr[2] = ntlmssp.length + 8
|
||||
|
||||
pkt = pkt_hdr.pack("CCnnCC") + ntlmssp
|
||||
|
||||
resp = mssql_send_recv(pkt)
|
||||
|
||||
|
||||
#SQL Server Authentification
|
||||
else
|
||||
idx = 0
|
||||
pkt = ''
|
||||
pkt << [
|
||||
0x00000000, # Dummy size
|
||||
|
||||
0x71000001, # TDS Version
|
||||
0x00000000, # Size
|
||||
0x00000007, # Version
|
||||
rand(1024+1), # PID
|
||||
0x00000000, # ConnectionID
|
||||
0xe0, # Option Flags 1
|
||||
0x03, # Option Flags 2
|
||||
0x00, # SQL Type Flags
|
||||
0x00, # Reserved Flags
|
||||
0x00000000, # Time Zone
|
||||
0x00000000 # Collation
|
||||
].pack('VVVVVVCCCCVV')
|
||||
|
||||
|
||||
cname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
|
||||
uname = Rex::Text.to_unicode( user )
|
||||
pname = mssql_tds_encrypt( pass )
|
||||
aname = Rex::Text.to_unicode( Rex::Text.rand_text_alpha(rand(8)+1) )
|
||||
sname = Rex::Text.to_unicode( rhost )
|
||||
dname = Rex::Text.to_unicode( db )
|
||||
|
||||
idx = pkt.size + 50 # lengths below
|
||||
|
||||
pkt << [idx, cname.length / 2].pack('vv')
|
||||
idx += cname.length
|
||||
|
||||
pkt << [idx, uname.length / 2].pack('vv')
|
||||
idx += uname.length
|
||||
|
||||
pkt << [idx, pname.length / 2].pack('vv')
|
||||
idx += pname.length
|
||||
|
||||
pkt << [idx, aname.length / 2].pack('vv')
|
||||
idx += aname.length
|
||||
|
||||
pkt << [idx, sname.length / 2].pack('vv')
|
||||
idx += sname.length
|
||||
|
||||
pkt << [0, 0].pack('vv')
|
||||
|
||||
pkt << [idx, aname.length / 2].pack('vv')
|
||||
idx += aname.length
|
||||
|
||||
pkt << [idx, 0].pack('vv')
|
||||
|
||||
pkt << [idx, dname.length / 2].pack('vv')
|
||||
idx += dname.length
|
||||
|
||||
# The total length has to be embedded twice more here
|
||||
pkt << [
|
||||
0,
|
||||
0,
|
||||
0x12345678,
|
||||
0x12345678
|
||||
].pack('vVVV')
|
||||
|
||||
pkt << cname
|
||||
pkt << uname
|
||||
pkt << pname
|
||||
pkt << aname
|
||||
pkt << sname
|
||||
pkt << aname
|
||||
pkt << dname
|
||||
|
||||
# Total packet length
|
||||
pkt[0,4] = [pkt.length].pack('V')
|
||||
|
||||
# Embedded packet lengths
|
||||
pkt[pkt.index([0x12345678].pack('V')), 8] = [pkt.length].pack('V') * 2
|
||||
|
||||
# Packet header and total length including header
|
||||
pkt = "\x10\x01" + [pkt.length + 8].pack('n') + [0].pack('n') + [1].pack('C') + "\x00" + pkt
|
||||
|
||||
resp = mssql_send_recv(pkt)
|
||||
|
||||
end
|
||||
|
||||
info = {:errors => []}
|
||||
info = mssql_parse_reply(resp,info)
|
||||
|
||||
return false if not info
|
||||
info[:login_ack] ? true : false
|
||||
end
|
||||
|
||||
#
|
||||
# Parse an "environment change" TDS token
|
||||
#
|
||||
def mssql_parse_env(data, info)
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
buff = data.slice!(0,len)
|
||||
type = buff.slice!(0,1).unpack('C')[0]
|
||||
|
||||
nval = ''
|
||||
nlen = buff.slice!(0,1).unpack('C')[0] || 0
|
||||
nval = buff.slice!(0,nlen*2).gsub("\x00", '') if nlen > 0
|
||||
|
||||
oval = ''
|
||||
olen = buff.slice!(0,1).unpack('C')[0] || 0
|
||||
oval = buff.slice!(0,olen*2).gsub("\x00", '') if olen > 0
|
||||
|
||||
info[:envs] ||= []
|
||||
info[:envs] << { :type => type, :old => oval, :new => nval }
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a "ret" TDS token
|
||||
#
|
||||
def mssql_parse_ret(data, info)
|
||||
ret = data.slice!(0,4).unpack('N')[0]
|
||||
info[:ret] = ret
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a "done" TDS token
|
||||
#
|
||||
def mssql_parse_done(data, info)
|
||||
status,cmd,rows = data.slice!(0,8).unpack('vvV')
|
||||
info[:done] = { :status => status, :cmd => cmd, :rows => rows }
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse an "error" TDS token
|
||||
#
|
||||
def mssql_parse_error(data, info)
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
buff = data.slice!(0,len)
|
||||
|
||||
errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv')
|
||||
emsg = buff.slice!(0,elen * 2)
|
||||
emsg.gsub!("\x00", '')
|
||||
|
||||
info[:errors] << "SQL Server Error ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse an "information" TDS token
|
||||
#
|
||||
def mssql_parse_info(data, info)
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
buff = data.slice!(0,len)
|
||||
|
||||
errno,state,sev,elen = buff.slice!(0,8).unpack('VCCv')
|
||||
emsg = buff.slice!(0,elen * 2)
|
||||
emsg.gsub!("\x00", '')
|
||||
|
||||
info[:infos]||= []
|
||||
info[:infos] << "SQL Server Info ##{errno} (State:#{state} Severity:#{sev}): #{emsg}"
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a "login ack" TDS token
|
||||
#
|
||||
def mssql_parse_login_ack(data, info)
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
buff = data.slice!(0,len)
|
||||
info[:login_ack] = true
|
||||
end
|
||||
|
||||
#
|
||||
# Parse individual tokens from a TDS reply
|
||||
#
|
||||
def mssql_parse_reply(data, info)
|
||||
info[:errors] = []
|
||||
return if not data
|
||||
until data.empty?
|
||||
token = data.slice!(0,1).unpack('C')[0]
|
||||
case token
|
||||
when 0x81
|
||||
mssql_parse_tds_reply(data, info)
|
||||
when 0xd1
|
||||
mssql_parse_tds_row(data, info)
|
||||
when 0xe3
|
||||
mssql_parse_env(data, info)
|
||||
when 0x79
|
||||
mssql_parse_ret(data, info)
|
||||
when 0xfd, 0xfe, 0xff
|
||||
mssql_parse_done(data, info)
|
||||
when 0xad
|
||||
mssql_parse_login_ack(data, info)
|
||||
when 0xab
|
||||
mssql_parse_info(data, info)
|
||||
when 0xaa
|
||||
mssql_parse_error(data, info)
|
||||
when nil
|
||||
break
|
||||
else
|
||||
info[:errors] << "unsupported token: #{token}"
|
||||
end
|
||||
end
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a raw TDS reply from the server
|
||||
#
|
||||
def mssql_parse_tds_reply(data, info)
|
||||
info[:errors] ||= []
|
||||
info[:colinfos] ||= []
|
||||
info[:colnames] ||= []
|
||||
|
||||
# Parse out the columns
|
||||
cols = data.slice!(0,2).unpack('v')[0]
|
||||
0.upto(cols-1) do |col_idx|
|
||||
col = {}
|
||||
info[:colinfos][col_idx] = col
|
||||
|
||||
col[:utype] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:flags] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:type] = data.slice!(0,1).unpack('C')[0]
|
||||
|
||||
case col[:type]
|
||||
when 48
|
||||
col[:id] = :tinyint
|
||||
|
||||
when 52
|
||||
col[:id] = :smallint
|
||||
|
||||
when 56
|
||||
col[:id] = :rawint
|
||||
|
||||
when 61
|
||||
col[:id] = :datetime
|
||||
|
||||
when 34
|
||||
col[:id] = :image
|
||||
col[:max_size] = data.slice!(0,4).unpack('V')[0]
|
||||
col[:value_length] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:value] = data.slice!(0, col[:value_length] * 2).gsub("\x00", '')
|
||||
|
||||
when 36
|
||||
col[:id] = :string
|
||||
|
||||
when 38
|
||||
col[:id] = :int
|
||||
col[:int_size] = data.slice!(0,1).unpack('C')[0]
|
||||
|
||||
when 127
|
||||
col[:id] = :bigint
|
||||
|
||||
when 165
|
||||
col[:id] = :hex
|
||||
col[:max_size] = data.slice!(0,2).unpack('v')[0]
|
||||
|
||||
when 173
|
||||
col[:id] = :hex # binary(2)
|
||||
col[:max_size] = data.slice!(0,2).unpack('v')[0]
|
||||
|
||||
when 231,175,167,239
|
||||
col[:id] = :string
|
||||
col[:max_size] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:codepage] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:cflags] = data.slice!(0,2).unpack('v')[0]
|
||||
col[:charset_id] = data.slice!(0,1).unpack('C')[0]
|
||||
|
||||
else
|
||||
col[:id] = :unknown
|
||||
end
|
||||
|
||||
col[:msg_len] = data.slice!(0,1).unpack('C')[0]
|
||||
|
||||
if(col[:msg_len] and col[:msg_len] > 0)
|
||||
col[:name] = data.slice!(0, col[:msg_len] * 2).gsub("\x00", '')
|
||||
end
|
||||
info[:colnames] << (col[:name] || 'NULL')
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Parse a single row of a TDS reply
|
||||
#
|
||||
def mssql_parse_tds_row(data, info)
|
||||
info[:rows] ||= []
|
||||
row = []
|
||||
|
||||
info[:colinfos].each do |col|
|
||||
|
||||
if(data.length == 0)
|
||||
row << "<EMPTY>"
|
||||
next
|
||||
end
|
||||
|
||||
case col[:id]
|
||||
when :hex
|
||||
str = ""
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
if(len > 0 and len < 65535)
|
||||
str << data.slice!(0,len)
|
||||
end
|
||||
row << str.unpack("H*")[0]
|
||||
|
||||
when :string
|
||||
str = ""
|
||||
len = data.slice!(0,2).unpack('v')[0]
|
||||
if(len > 0 and len < 65535)
|
||||
str << data.slice!(0,len)
|
||||
end
|
||||
row << str.gsub("\x00", '')
|
||||
|
||||
when :datetime
|
||||
row << data.slice!(0,8).unpack("H*")[0]
|
||||
|
||||
when :rawint
|
||||
row << data.slice!(0,4).unpack('V')[0]
|
||||
|
||||
when :bigint
|
||||
row << data.slice!(0,8).unpack("H*")[0]
|
||||
|
||||
when :smallint
|
||||
row << data.slice!(0, 2).unpack("v")[0]
|
||||
|
||||
when :smallint3
|
||||
row << [data.slice!(0, 3)].pack("Z4").unpack("V")[0]
|
||||
|
||||
when :tinyint
|
||||
row << data.slice!(0, 1).unpack("C")[0]
|
||||
|
||||
when :image
|
||||
str = ''
|
||||
len = data.slice!(0,1).unpack('C')[0]
|
||||
str = data.slice!(0,len) if (len and len > 0)
|
||||
row << str.unpack("H*")[0]
|
||||
|
||||
when :int
|
||||
len = data.slice!(0, 1).unpack("C")[0]
|
||||
raw = data.slice!(0, len) if (len and len > 0)
|
||||
|
||||
case len
|
||||
when 0,255
|
||||
row << ''
|
||||
when 1
|
||||
row << raw.unpack("C")[0]
|
||||
when 2
|
||||
row << raw.unpack('v')[0]
|
||||
when 4
|
||||
row << raw.unpack('V')[0]
|
||||
when 5
|
||||
row << raw.unpack('V')[0] # XXX: missing high byte
|
||||
when 8
|
||||
row << raw.unpack('VV')[0] # XXX: missing high dword
|
||||
else
|
||||
info[:errors] << "invalid integer size: #{len} #{data[0,16].unpack("H*")[0]}"
|
||||
end
|
||||
else
|
||||
info[:errors] << "unknown column type: #{col.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
info[:rows] << row
|
||||
info
|
||||
end
|
||||
|
||||
#
|
||||
#this method send a prelogin packet and check if encryption is off
|
||||
#
|
||||
def mssql_prelogin(enc_error=false)
|
||||
|
||||
pkt = ""
|
||||
pkt_hdr = ""
|
||||
pkt_data_token = ""
|
||||
pkt_data = ""
|
||||
|
||||
|
||||
pkt_hdr = [
|
||||
TYPE_PRE_LOGIN_MESSAGE, #type
|
||||
STATUS_END_OF_MESSAGE, #status
|
||||
0x0000, #length
|
||||
0x0000, # SPID
|
||||
0x00, # PacketID
|
||||
0x00 #Window
|
||||
]
|
||||
|
||||
version = [0x55010008,0x0000].pack("Vv")
|
||||
encryption = ENCRYPT_NOT_SUP # off
|
||||
instoptdata = "MSSQLServer\0"
|
||||
|
||||
threadid = "\0\0" + Rex::Text.rand_text(2)
|
||||
|
||||
idx = 21 # size of pkt_data_token
|
||||
pkt_data_token << [
|
||||
0x00, # Token 0 type Version
|
||||
idx , # VersionOffset
|
||||
version.length, # VersionLength
|
||||
|
||||
0x01, # Token 1 type Encryption
|
||||
idx = idx + version.length, # EncryptionOffset
|
||||
0x01, # EncryptionLength
|
||||
|
||||
0x02, # Token 2 type InstOpt
|
||||
idx = idx + 1, # InstOptOffset
|
||||
instoptdata.length, # InstOptLength
|
||||
|
||||
0x03, # Token 3 type Threadid
|
||||
idx + instoptdata.length, # ThreadIdOffset
|
||||
0x04, # ThreadIdLength
|
||||
|
||||
0xFF
|
||||
].pack("CnnCnnCnnCnnC")
|
||||
|
||||
pkt_data << pkt_data_token
|
||||
pkt_data << version
|
||||
pkt_data << encryption
|
||||
pkt_data << instoptdata
|
||||
pkt_data << threadid
|
||||
|
||||
pkt_hdr[2] = pkt_data.length + 8
|
||||
|
||||
pkt = pkt_hdr.pack("CCnnCC") + pkt_data
|
||||
|
||||
resp = mssql_send_recv(pkt)
|
||||
|
||||
idx = 0
|
||||
|
||||
while resp and resp[0,1] != "\xff" and resp.length > 5
|
||||
token = resp.slice!(0,5)
|
||||
token = token.unpack("Cnn")
|
||||
idx -= 5
|
||||
if token[0] == 0x01
|
||||
|
||||
idx += token[1]
|
||||
break
|
||||
end
|
||||
end
|
||||
if idx > 0
|
||||
encryption_mode = resp[idx,1].unpack("C")[0]
|
||||
else
|
||||
#force to ENCRYPT_NOT_SUP and hope for the best
|
||||
encryption_mode = ENCRYPT_NOT_SUP
|
||||
end
|
||||
|
||||
if encryption_mode != ENCRYPT_NOT_SUP and enc_error
|
||||
raise RuntimeError,"Encryption is not supported"
|
||||
end
|
||||
encryption_mode
|
||||
end
|
||||
|
||||
#
|
||||
# Send and receive using TDS
|
||||
#
|
||||
def mssql_send_recv(req, timeout=15, check_status = true)
|
||||
sock.put(req)
|
||||
|
||||
# Read the 8 byte header to get the length and status
|
||||
# Read the length to get the data
|
||||
# If the status is 0, read another header and more data
|
||||
|
||||
done = false
|
||||
resp = ""
|
||||
|
||||
while(not done)
|
||||
head = sock.get_once(8, timeout)
|
||||
if !(head and head.length == 8)
|
||||
return false
|
||||
end
|
||||
|
||||
# Is this the last buffer?
|
||||
if(head[1,1] == "\x01" or not check_status )
|
||||
done = true
|
||||
end
|
||||
|
||||
# Grab this block's length
|
||||
rlen = head[2,2].unpack('n')[0] - 8
|
||||
|
||||
while(rlen > 0)
|
||||
buff = sock.get_once(rlen, timeout)
|
||||
return if not buff
|
||||
resp << buff
|
||||
rlen -= buff.length
|
||||
end
|
||||
end
|
||||
|
||||
resp
|
||||
end
|
||||
|
||||
#
|
||||
# Encrypt a password according to the TDS protocol (encode)
|
||||
#
|
||||
def mssql_tds_encrypt(pass)
|
||||
# Convert to unicode, swap 4 bits both ways, xor with 0xa5
|
||||
Rex::Text.to_unicode(pass).unpack('C*').map {|c| (((c & 0x0f) << 4) + ((c & 0xf0) >> 4)) ^ 0xa5 }.pack("C*")
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def windows_authentication
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def use_ntlm2_session
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def use_ntlmv2
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def send_lm
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def send_ntlm
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def send_spn
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
#
|
||||
# Gems
|
||||
#
|
||||
|
||||
require 'active_support/dependencies/autoload'
|
||||
|
||||
# @note Must use the nested declaration of the
|
||||
# {Metasploit::Framework::ParsedOptions} namespace because commands, which
|
||||
# use parsed options, need to be able to be required directly without any
|
||||
# other part of metasploit-framework besides config/boot so that the
|
||||
# commands can parse arguments, setup RAILS_ENV, and load
|
||||
# config/application.rb correctly.
|
||||
module Metasploit
|
||||
module Framework
|
||||
# Namespace for parsed options for {Metasploit::Framework::Command
|
||||
# commands}. The names of `Class`es in this namespace correspond to the
|
||||
# name of the `Class` in the {Metasploit::Framework::Command} namespace
|
||||
# for which this namespace's `Class` parses options.
|
||||
module ParsedOptions
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :Base
|
||||
autoload :Console
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
#
|
||||
# Standard Library
|
||||
#
|
||||
|
||||
require 'optparse'
|
||||
|
||||
#
|
||||
# Gems
|
||||
#
|
||||
|
||||
require 'active_support/ordered_options'
|
||||
|
||||
#
|
||||
# Project
|
||||
#
|
||||
|
||||
require 'metasploit/framework/parsed_options'
|
||||
require 'msf/base/config'
|
||||
|
||||
# Options parsed from the command line that can be used to change the
|
||||
# `Metasploit::Framework::Application.config` and `Rails.env`
|
||||
class Metasploit::Framework::ParsedOptions::Base
|
||||
#
|
||||
# CONSTANTS
|
||||
#
|
||||
|
||||
# msfconsole boots in production mode instead of the normal rails default of
|
||||
# development.
|
||||
DEFAULT_ENVIRONMENT = 'production'
|
||||
|
||||
#
|
||||
# Attributes
|
||||
#
|
||||
|
||||
attr_reader :positional
|
||||
|
||||
#
|
||||
# Instance Methods
|
||||
#
|
||||
|
||||
def initialize(arguments=ARGV)
|
||||
@positional = option_parser.parse(arguments)
|
||||
end
|
||||
|
||||
# Translates {#options} to the `application`'s config
|
||||
#
|
||||
# @param application [Rails::Application]
|
||||
# @return [void]
|
||||
def configure(application)
|
||||
application.config['config/database'] = options.database.config
|
||||
end
|
||||
|
||||
# Sets the `RAILS_ENV` environment variable.
|
||||
#
|
||||
# 1. If the -E/--environment option is given, then its value is used.
|
||||
# 2. The default value, 'production', is used.
|
||||
#
|
||||
# @return [void]
|
||||
def environment!
|
||||
if defined?(Rails) && Rails.instance_variable_defined?(:@_env) && Rails.env != options.environment
|
||||
raise "#{self.class}##{__method__} called too late to set RAILS_ENV: Rails.env already memoized"
|
||||
end
|
||||
|
||||
ENV['RAILS_ENV'] = options.environment
|
||||
end
|
||||
|
||||
# Options parsed from
|
||||
#
|
||||
# @return [ActiveSupport::OrderedOptions]
|
||||
def options
|
||||
unless @options
|
||||
options = ActiveSupport::OrderedOptions.new
|
||||
|
||||
options.database = ActiveSupport::OrderedOptions.new
|
||||
|
||||
user_config_root = Pathname.new(Msf::Config.get_config_root)
|
||||
user_database_yaml = user_config_root.join('database.yml')
|
||||
|
||||
if user_database_yaml.exist?
|
||||
options.database.config = user_database_yaml.to_path
|
||||
else
|
||||
options.database.config = 'config/database.yml'
|
||||
end
|
||||
|
||||
options.database.disable = false
|
||||
options.database.migrations_paths = []
|
||||
|
||||
# If RAILS_ENV is set, then it will be used, but if RAILS_ENV is set and the --environment option is given, then
|
||||
# --environment value will be used to reset ENV[RAILS_ENV].
|
||||
options.environment = ENV['RAILS_ENV'] || DEFAULT_ENVIRONMENT
|
||||
|
||||
options.framework = ActiveSupport::OrderedOptions.new
|
||||
options.framework.config = nil
|
||||
|
||||
options.modules = ActiveSupport::OrderedOptions.new
|
||||
options.modules.path = nil
|
||||
|
||||
@options = options
|
||||
end
|
||||
|
||||
@options
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Parses arguments into {#options}.
|
||||
#
|
||||
# @return [OptionParser]
|
||||
def option_parser
|
||||
@option_parser ||= OptionParser.new { |option_parser|
|
||||
option_parser.separator ''
|
||||
option_parser.separator 'Common options'
|
||||
|
||||
option_parser.on(
|
||||
'-E',
|
||||
'--environment ENVIRONMENT',
|
||||
%w{development production test},
|
||||
"The Rails environment. Will use RAIL_ENV environment variable if that is set. " \
|
||||
"Defaults to production if neither option not RAILS_ENV environment variable is set."
|
||||
) do |environment|
|
||||
options.environment = environment
|
||||
end
|
||||
|
||||
option_parser.separator ''
|
||||
option_parser.separator 'Database options'
|
||||
|
||||
option_parser.on(
|
||||
'-M',
|
||||
'--migration-path DIRECTORY',
|
||||
'Specify a directory containing additional DB migrations'
|
||||
) do |directory|
|
||||
options.database.migrations_paths << directory
|
||||
end
|
||||
|
||||
option_parser.on('-n', '--no-database', 'Disable database support') do
|
||||
options.database.disable = true
|
||||
end
|
||||
|
||||
option_parser.on(
|
||||
'-y',
|
||||
'--yaml PATH',
|
||||
'Specify a YAML file containing database settings'
|
||||
) do |path|
|
||||
options.database.config = path
|
||||
end
|
||||
|
||||
option_parser.separator ''
|
||||
option_parser.separator 'Framework options'
|
||||
|
||||
|
||||
option_parser.on('-c', '-c FILE', 'Load the specified configuration file') do |file|
|
||||
options.framework.config = file
|
||||
end
|
||||
|
||||
option_parser.on(
|
||||
'-v',
|
||||
'--version',
|
||||
'Show version'
|
||||
) do
|
||||
options.subcommand = :version
|
||||
end
|
||||
|
||||
option_parser.separator ''
|
||||
option_parser.separator 'Module options'
|
||||
|
||||
option_parser.on(
|
||||
'-m',
|
||||
'--module-path DIRECTORY',
|
||||
'An additional module path'
|
||||
) do |directory|
|
||||
options.modules.path = directory
|
||||
end
|
||||
|
||||
#
|
||||
# Tail
|
||||
#
|
||||
|
||||
option_parser.separator ''
|
||||
option_parser.on_tail('-h', '--help', 'Show this message') do
|
||||
puts option_parser
|
||||
exit
|
||||
end
|
||||
}
|
||||
end
|
||||
end
|
|
@ -0,0 +1,79 @@
|
|||
# Parsed options for {Metasploit::Framework::Command::Console}
|
||||
class Metasploit::Framework::ParsedOptions::Console < Metasploit::Framework::ParsedOptions::Base
|
||||
# Options parsed from msfconsole command-line.
|
||||
#
|
||||
# @return [ActiveSupport::OrderedOptions]
|
||||
def options
|
||||
unless @options
|
||||
super.tap { |options|
|
||||
options.console = ActiveSupport::OrderedOptions.new
|
||||
|
||||
options.console.commands = []
|
||||
options.console.confirm_exit = false
|
||||
options.console.defanged = false
|
||||
options.console.local_output = nil
|
||||
options.console.plugins = []
|
||||
options.console.quiet = false
|
||||
options.console.real_readline = false
|
||||
options.console.resources = []
|
||||
options.console.subcommand = :run
|
||||
}
|
||||
end
|
||||
|
||||
@options
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Parses msfconsole arguments into {#options}.
|
||||
#
|
||||
# @return [OptionParser]
|
||||
def option_parser
|
||||
unless @option_parser
|
||||
super.tap { |option_parser|
|
||||
option_parser.banner = "Usage: #{option_parser.program_name} [options]"
|
||||
|
||||
option_parser.separator ''
|
||||
option_parser.separator 'Console options:'
|
||||
|
||||
option_parser.on('-a', '--ask', "Ask before exiting Metasploit or accept 'exit -y'") do
|
||||
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
|
||||
|
||||
option_parser.on('-o', '--output FILE', 'Output to the specified file') do |file|
|
||||
options.console.local_output = file
|
||||
end
|
||||
|
||||
option_parser.on('-p', '--plugin PLUGIN', 'Load a plugin on startup') do |plugin|
|
||||
options.console.plugins << plugin
|
||||
end
|
||||
|
||||
option_parser.on('-q', '--quiet', 'Do not print the banner on start up') do
|
||||
options.console.quiet = true
|
||||
end
|
||||
|
||||
option_parser.on('-r', '--resource FILE', 'Execute the specified resource file') do |file|
|
||||
options.console.resources << file
|
||||
end
|
||||
|
||||
option_parser.on(
|
||||
'-x',
|
||||
'--execute-command COMMAND',
|
||||
'Execute the specified string as console commands (use ; for multiples)'
|
||||
) do |commands|
|
||||
options.console.commands += commands.split(/\s*;\s*/)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
@option_parser
|
||||
end
|
||||
end
|
|
@ -0,0 +1,92 @@
|
|||
# @note needs to use explicit nesting. so this file can be loaded directly without loading 'metasploit/framework', this
|
||||
# file can be used prior to Bundler.require.
|
||||
module Metasploit
|
||||
module Framework
|
||||
# Extension to `Kernel#require` behavior.
|
||||
module Require
|
||||
#
|
||||
# Module Methods
|
||||
#
|
||||
|
||||
# Tries to require `name`. If a `LoadError` occurs, then `without_warning` is printed to standard error using
|
||||
# `Kernel#warn`, along with instructions for reinstalling the bundle. If a `LoadError` does not occur, then
|
||||
# `with_block` is called.
|
||||
#
|
||||
# @param name [String] the name of the library to `Kernel#require`.
|
||||
# @param without_warning [String] warning to print if `name` cannot be required.
|
||||
# @yield block to run when `name` requires successfully
|
||||
# @yieldreturn [void]
|
||||
# @return [void]
|
||||
def self.optionally(name, without_warning)
|
||||
begin
|
||||
require name
|
||||
rescue LoadError
|
||||
warn without_warning
|
||||
warn "Bundle installed '--without #{Bundler.settings.without.join(' ')}'"
|
||||
warn "To clear the without option do `bundle install --without ''` " \
|
||||
"(the --without flag with an empty string) or " \
|
||||
"`rm -rf .bundle` to remove the .bundle/config manually and " \
|
||||
"then `bundle install`"
|
||||
else
|
||||
if block_given?
|
||||
yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Tries to `require 'active_record/railtie'` to define the activerecord Rails initializers and rake tasks.
|
||||
#
|
||||
# @example Optionally requiring 'active_record/railtie'
|
||||
# require 'metasploit/framework/require'
|
||||
#
|
||||
# class MyClass
|
||||
# def setup
|
||||
# if database_enabled
|
||||
# Metasploit::Framework::Require.optionally_active_record_railtie
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @return [void]
|
||||
def self.optionally_active_record_railtie
|
||||
optionally(
|
||||
'active_record/railtie',
|
||||
'activerecord not in the bundle, so database support will be disabled.'
|
||||
)
|
||||
end
|
||||
|
||||
# Tries to `require 'metasploit/credential/creation'` and include it in the `including_module`.
|
||||
#
|
||||
# @param including_module [Module] `Class` or `Module` that wants to `include Metasploit::Credential::Creation`.
|
||||
# @return [void]
|
||||
def self.optionally_include_metasploit_credential_creation(including_module)
|
||||
optionally(
|
||||
'metasploit/credential/creation',
|
||||
"metasploit-credential not in the bundle, so Metasploit::Credential creation will fail for #{including_module.name}",
|
||||
) do
|
||||
including_module.send(:include, Metasploit::Credential::Creation)
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Instance Methods
|
||||
#
|
||||
|
||||
# Tries to `require 'metasploit/credential/creation'` and include it in this `Class` or `Module`.
|
||||
#
|
||||
# @example Using in a `Module`
|
||||
# require 'metasploit/framework/require'
|
||||
#
|
||||
# module MyModule
|
||||
# extend Metasploit::Framework::Require
|
||||
#
|
||||
# optionally_include_metasploit_credential_creation
|
||||
# end
|
||||
#
|
||||
# @return [void]
|
||||
def optionally_include_metasploit_credential_creation
|
||||
Metasploit::Framework::Require.optionally_include_metasploit_credential_creation(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,186 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module Tcp
|
||||
|
||||
module EvasiveTCP
|
||||
attr_accessor :_send_size, :_send_delay, :evasive
|
||||
|
||||
def denagle
|
||||
begin
|
||||
setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
||||
rescue ::Exception
|
||||
end
|
||||
end
|
||||
|
||||
def write(buf, opts={})
|
||||
|
||||
return super(buf, opts) if not @evasive
|
||||
|
||||
ret = 0
|
||||
idx = 0
|
||||
len = @_send_size || buf.length
|
||||
|
||||
while(idx < buf.length)
|
||||
|
||||
if(@_send_delay and idx > 0)
|
||||
::IO.select(nil, nil, nil, @_send_delay)
|
||||
end
|
||||
|
||||
pkt = buf[idx, len]
|
||||
|
||||
res = super(pkt, opts)
|
||||
flush()
|
||||
|
||||
idx += len
|
||||
ret += res if res
|
||||
end
|
||||
ret
|
||||
end
|
||||
end
|
||||
|
||||
module Client
|
||||
|
||||
#
|
||||
# Establishes a TCP connection to the specified RHOST/RPORT
|
||||
#
|
||||
# @see Rex::Socket::Tcp
|
||||
# @see Rex::Socket::Tcp.create
|
||||
def connect(global = true, opts={})
|
||||
|
||||
dossl = false
|
||||
if(opts.has_key?('SSL'))
|
||||
dossl = opts['SSL']
|
||||
else
|
||||
dossl = ssl
|
||||
end
|
||||
|
||||
nsock = Rex::Socket::Tcp.create(
|
||||
'PeerHost' => opts['RHOST'] || rhost,
|
||||
'PeerPort' => (opts['RPORT'] || rport).to_i,
|
||||
'LocalHost' => opts['CHOST'] || chost || "0.0.0.0",
|
||||
'LocalPort' => (opts['CPORT'] || cport || 0).to_i,
|
||||
'SSL' => dossl,
|
||||
'SSLVersion' => opts['SSLVersion'] || ssl_version,
|
||||
'Proxies' => proxies,
|
||||
'Timeout' => (opts['ConnectTimeout'] || connection_timeout || 10).to_i
|
||||
)
|
||||
|
||||
# enable evasions on this socket
|
||||
set_tcp_evasions(nsock)
|
||||
|
||||
# Set this socket to the global socket as necessary
|
||||
self.sock = nsock if (global)
|
||||
|
||||
return nsock
|
||||
end
|
||||
|
||||
# Enable evasions on a given client
|
||||
def set_tcp_evasions(socket)
|
||||
|
||||
if( max_send_size.to_i == 0 and send_delay.to_i == 0)
|
||||
return
|
||||
end
|
||||
|
||||
return if socket.respond_to?('evasive')
|
||||
|
||||
socket.extend(EvasiveTCP)
|
||||
|
||||
if ( max_send_size.to_i > 0)
|
||||
socket._send_size = max_send_size
|
||||
socket.denagle
|
||||
socket.evasive = true
|
||||
end
|
||||
|
||||
if ( send_delay.to_i > 0)
|
||||
socket._send_delay = send_delay
|
||||
socket.evasive = true
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Closes the TCP connection
|
||||
#
|
||||
def disconnect(nsock = self.sock)
|
||||
begin
|
||||
if (nsock)
|
||||
nsock.shutdown
|
||||
nsock.close
|
||||
end
|
||||
rescue IOError
|
||||
end
|
||||
|
||||
if (nsock == sock)
|
||||
self.sock = nil
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
##
|
||||
#
|
||||
# Wrappers for getters
|
||||
#
|
||||
##
|
||||
|
||||
def max_send_size
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def send_delay
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the target host
|
||||
#
|
||||
def rhost
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the remote port
|
||||
#
|
||||
def rport
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the local host for outgoing connections
|
||||
#
|
||||
def chost
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the local port for outgoing connections
|
||||
#
|
||||
def cport
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the boolean indicating SSL
|
||||
#
|
||||
def ssl
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the string indicating SSLVersion
|
||||
#
|
||||
def ssl_version
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
#
|
||||
# Returns the proxy configuration
|
||||
#
|
||||
def proxies
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
attr_accessor :sock
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,219 @@
|
|||
require 'metasploit/framework/tcp/client'
|
||||
|
||||
module Metasploit
|
||||
module Framework
|
||||
module Telnet
|
||||
module Client
|
||||
include Metasploit::Framework::Tcp::Client
|
||||
include Msf::Auxiliary::Login
|
||||
|
||||
attr_accessor :banner
|
||||
|
||||
#
|
||||
# CONSTANTS
|
||||
#
|
||||
# Borrowing constants from Ruby's Net::Telnet class (ruby license)
|
||||
IAC = 255.chr # "\377" # "\xff" # interpret as command
|
||||
DONT = 254.chr # "\376" # "\xfe" # you are not to use option
|
||||
DO = 253.chr # "\375" # "\xfd" # please, you use option
|
||||
WONT = 252.chr # "\374" # "\xfc" # I won't use option
|
||||
WILL = 251.chr # "\373" # "\xfb" # I will use option
|
||||
SB = 250.chr # "\372" # "\xfa" # interpret as subnegotiation
|
||||
GA = 249.chr # "\371" # "\xf9" # you may reverse the line
|
||||
EL = 248.chr # "\370" # "\xf8" # erase the current line
|
||||
EC = 247.chr # "\367" # "\xf7" # erase the current character
|
||||
AYT = 246.chr # "\366" # "\xf6" # are you there
|
||||
AO = 245.chr # "\365" # "\xf5" # abort output--but let prog finish
|
||||
IP = 244.chr # "\364" # "\xf4" # interrupt process--permanently
|
||||
BREAK = 243.chr # "\363" # "\xf3" # break
|
||||
DM = 242.chr # "\362" # "\xf2" # data mark--for connect. cleaning
|
||||
NOP = 241.chr # "\361" # "\xf1" # nop
|
||||
SE = 240.chr # "\360" # "\xf0" # end sub negotiation
|
||||
EOR = 239.chr # "\357" # "\xef" # end of record (transparent mode)
|
||||
ABORT = 238.chr # "\356" # "\xee" # Abort process
|
||||
SUSP = 237.chr # "\355" # "\xed" # Suspend process
|
||||
EOF = 236.chr # "\354" # "\xec" # End of file
|
||||
SYNCH = 242.chr # "\362" # "\xf2" # for telfunc calls
|
||||
|
||||
OPT_BINARY = 0.chr # "\000" # "\x00" # Binary Transmission
|
||||
OPT_ECHO = 1.chr # "\001" # "\x01" # Echo
|
||||
OPT_RCP = 2.chr # "\002" # "\x02" # Reconnection
|
||||
OPT_SGA = 3.chr # "\003" # "\x03" # Suppress Go Ahead
|
||||
OPT_NAMS = 4.chr # "\004" # "\x04" # Approx Message Size Negotiation
|
||||
OPT_STATUS = 5.chr # "\005" # "\x05" # Status
|
||||
OPT_TM = 6.chr # "\006" # "\x06" # Timing Mark
|
||||
OPT_RCTE = 7.chr # "\a" # "\x07" # Remote Controlled Trans and Echo
|
||||
OPT_NAOL = 8.chr # "\010" # "\x08" # Output Line Width
|
||||
OPT_NAOP = 9.chr # "\t" # "\x09" # Output Page Size
|
||||
OPT_NAOCRD = 10.chr # "\n" # "\x0a" # Output Carriage-Return Disposition
|
||||
OPT_NAOHTS = 11.chr # "\v" # "\x0b" # Output Horizontal Tab Stops
|
||||
OPT_NAOHTD = 12.chr # "\f" # "\x0c" # Output Horizontal Tab Disposition
|
||||
OPT_NAOFFD = 13.chr # "\r" # "\x0d" # Output Formfeed Disposition
|
||||
OPT_NAOVTS = 14.chr # "\016" # "\x0e" # Output Vertical Tabstops
|
||||
OPT_NAOVTD = 15.chr # "\017" # "\x0f" # Output Vertical Tab Disposition
|
||||
OPT_NAOLFD = 16.chr # "\020" # "\x10" # Output Linefeed Disposition
|
||||
OPT_XASCII = 17.chr # "\021" # "\x11" # Extended ASCII
|
||||
OPT_LOGOUT = 18.chr # "\022" # "\x12" # Logout
|
||||
OPT_BM = 19.chr # "\023" # "\x13" # Byte Macro
|
||||
OPT_DET = 20.chr # "\024" # "\x14" # Data Entry Terminal
|
||||
OPT_SUPDUP = 21.chr # "\025" # "\x15" # SUPDUP
|
||||
OPT_SUPDUPOUTPUT = 22.chr # "\026" # "\x16" # SUPDUP Output
|
||||
OPT_SNDLOC = 23.chr # "\027" # "\x17" # Send Location
|
||||
OPT_TTYPE = 24.chr # "\030" # "\x18" # Terminal Type
|
||||
OPT_EOR = 25.chr # "\031" # "\x19" # End of Record
|
||||
OPT_TUID = 26.chr # "\032" # "\x1a" # TACACS User Identification
|
||||
OPT_OUTMRK = 27.chr # "\e" # "\x1b" # Output Marking
|
||||
OPT_TTYLOC = 28.chr # "\034" # "\x1c" # Terminal Location Number
|
||||
OPT_3270REGIME = 29.chr # "\035" # "\x1d" # Telnet 3270 Regime
|
||||
OPT_X3PAD = 30.chr # "\036" # "\x1e" # X.3 PAD
|
||||
OPT_NAWS = 31.chr # "\037" # "\x1f" # Negotiate About Window Size
|
||||
OPT_TSPEED = 32.chr # " " # "\x20" # Terminal Speed
|
||||
OPT_LFLOW = 33.chr # "!" # "\x21" # Remote Flow Control
|
||||
OPT_LINEMODE = 34.chr # "\"" # "\x22" # Linemode
|
||||
OPT_XDISPLOC = 35.chr # "#" # "\x23" # X Display Location
|
||||
OPT_OLD_ENVIRON = 36.chr # "$" # "\x24" # Environment Option
|
||||
OPT_AUTHENTICATION = 37.chr # "%" # "\x25" # Authentication Option
|
||||
OPT_ENCRYPT = 38.chr # "&" # "\x26" # Encryption Option
|
||||
OPT_NEW_ENVIRON = 39.chr # "'" # "\x27" # New Environment Option
|
||||
OPT_EXOPL = 255.chr # "\377" # "\xff" # Extended-Options-List
|
||||
|
||||
#
|
||||
# This method establishes an Telnet connection to host and port specified by
|
||||
# the RHOST and RPORT options, respectively. After connecting, the banner
|
||||
# message is read in and stored in the 'banner' attribute. This method has the
|
||||
# benefit of handling telnet option negotiation.
|
||||
#
|
||||
def connect(global = true, verbose = true)
|
||||
@trace = ''
|
||||
@recvd = ''
|
||||
fd = super(global)
|
||||
|
||||
self.banner = ''
|
||||
# Wait for a banner to arrive...
|
||||
begin
|
||||
Timeout.timeout(banner_timeout) do
|
||||
while(true)
|
||||
buff = recv(fd)
|
||||
self.banner << buff if buff
|
||||
if(self.banner =~ @login_regex or self.banner =~ @password_regex)
|
||||
break
|
||||
elsif self.banner =~ @busy_regex
|
||||
# It's about to drop connection anyway -- seen on HP JetDirect telnet server
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue ::Timeout::Error
|
||||
end
|
||||
|
||||
self.banner.strip!
|
||||
|
||||
# Return the file descriptor to the caller
|
||||
fd
|
||||
end
|
||||
|
||||
# Sometimes telnet servers start RSTing if you get them angry.
|
||||
# This is a short term fix; the problem is that we don't know
|
||||
# if it's going to reset forever, or just this time, or randomly.
|
||||
# A better solution is to get the socket connect to try again
|
||||
# with a little backoff.
|
||||
def connect_reset_safe
|
||||
begin
|
||||
connect
|
||||
rescue Rex::ConnectionRefused
|
||||
return :refused
|
||||
end
|
||||
return :connected
|
||||
end
|
||||
|
||||
def recv(fd=self.sock, timeout=telnet_timeout)
|
||||
recv_telnet(fd, timeout.to_f)
|
||||
end
|
||||
|
||||
#
|
||||
# Handle telnet option negotiation
|
||||
#
|
||||
# Appends to the @recvd buffer which is used to tell us whether we're at a
|
||||
# login prompt, a password prompt, or a working shell.
|
||||
#
|
||||
def recv_telnet(fd, timeout)
|
||||
|
||||
data = ''
|
||||
|
||||
begin
|
||||
data = fd.get_once(-1, timeout)
|
||||
return nil if not data or data.length == 0
|
||||
|
||||
# combine CR+NULL into CR
|
||||
data.gsub!(/#{CR}#{NULL}/no, CR)
|
||||
|
||||
# combine EOL into "\n"
|
||||
data.gsub!(/#{EOL}/no, "\n")
|
||||
|
||||
data.gsub!(/#{IAC}(
|
||||
[#{IAC}#{AO}#{AYT}#{DM}#{IP}#{NOP}]|[#{DO}#{DONT}#{WILL}#{WONT}]
|
||||
[#{OPT_BINARY}-#{OPT_NEW_ENVIRON}#{OPT_EXOPL}]|#{SB}[^#{IAC}]*#{IAC}#{SE}
|
||||
)/xno) do
|
||||
m = $1
|
||||
|
||||
if m == IAC
|
||||
IAC
|
||||
elsif m == AYT
|
||||
fd.write("YES" + EOL)
|
||||
''
|
||||
elsif m[0,1] == DO
|
||||
if(m[1,1] == OPT_BINARY)
|
||||
fd.write(IAC + WILL + OPT_BINARY)
|
||||
else
|
||||
fd.write(IAC + WONT + m[1,1])
|
||||
end
|
||||
''
|
||||
elsif m[0,1] == DONT
|
||||
fd.write(IAC + WONT + m[1,1])
|
||||
''
|
||||
elsif m[0,1] == WILL
|
||||
if m[1,1] == OPT_BINARY
|
||||
fd.write(IAC + DO + OPT_BINARY)
|
||||
# Disable Echo
|
||||
elsif m[1,1] == OPT_ECHO
|
||||
fd.write(IAC + DONT + OPT_ECHO)
|
||||
elsif m[1,1] == OPT_SGA
|
||||
fd.write(IAC + DO + OPT_SGA)
|
||||
else
|
||||
fd.write(IAC + DONT + m[1,1])
|
||||
end
|
||||
''
|
||||
elsif m[0,1] == WONT
|
||||
fd.write(IAC + DONT + m[1,1])
|
||||
''
|
||||
else
|
||||
''
|
||||
end
|
||||
end
|
||||
|
||||
@trace << data
|
||||
@recvd << data
|
||||
fd.flush
|
||||
|
||||
rescue ::EOFError, ::Errno::EPIPE
|
||||
end
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
#
|
||||
# Wrappers for getters
|
||||
#
|
||||
|
||||
def banner_timeout
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def telnet_timeout
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
module Metasploit
|
||||
module Framework
|
||||
module Version
|
||||
MAJOR = 4
|
||||
MINOR = 10
|
||||
PATCH = 1
|
||||
PRERELEASE = 'dev'
|
||||
end
|
||||
|
||||
VERSION = "#{Version::MAJOR}.#{Version::MINOR}.#{Version::PATCH}-#{Version::PRERELEASE}"
|
||||
GEM_VERSION = VERSION.gsub('-', '.pre.')
|
||||
end
|
||||
end
|
|
@ -1,6 +1,18 @@
|
|||
# -*- coding: binary -*-
|
||||
|
||||
#
|
||||
# Standard Library
|
||||
#
|
||||
|
||||
require 'fileutils'
|
||||
|
||||
#
|
||||
# Project
|
||||
#
|
||||
|
||||
require 'metasploit/framework/version'
|
||||
require 'rex/compat'
|
||||
|
||||
module Msf
|
||||
|
||||
# This class wraps interaction with global configuration that can be used as a
|
||||
|
@ -25,16 +37,16 @@ class Config < Hash
|
|||
['HOME', 'LOCALAPPDATA', 'APPDATA', 'USERPROFILE'].each do |dir|
|
||||
val = Rex::Compat.getenv(dir)
|
||||
if (val and File.directory?(val))
|
||||
return File.join(val, ".msf#{Msf::Framework::Major}")
|
||||
return File.join(val, ".msf#{Metasploit::Framework::Version::MAJOR}")
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
# First we try $HOME/.msfx
|
||||
File.expand_path("~#{FileSep}.msf#{Msf::Framework::Major}")
|
||||
File.expand_path("~#{FileSep}.msf#{Metasploit::Framework::Version::MAJOR}")
|
||||
rescue ::ArgumentError
|
||||
# Give up and install root + ".msfx"
|
||||
InstallRoot + ".msf#{Msf::Framework::Major}"
|
||||
InstallRoot + ".msf#{Metasploit::Framework::Version::MAJOR}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -9,9 +9,10 @@ module Msf
|
|||
# Ensure the module cache is accurate
|
||||
self.modules.refresh_cache_from_database
|
||||
|
||||
# Initialize the default module search paths
|
||||
if (Msf::Config.module_directory)
|
||||
self.modules.add_module_path(Msf::Config.module_directory, opts)
|
||||
add_engine_module_paths(Rails.application, opts)
|
||||
|
||||
Rails.application.railties.engines.each do |engine|
|
||||
add_engine_module_paths(engine, opts)
|
||||
end
|
||||
|
||||
# Initialize the user module search path
|
||||
|
@ -27,6 +28,25 @@ module Msf
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Add directories `engine.paths['modules']` from `engine`.
|
||||
#
|
||||
# @param engine [Rails::Engine] a rails engine or application
|
||||
# @param options [Hash] options for {Msf::ModuleManager::ModulePaths#add_module_paths}
|
||||
# @return [void]
|
||||
def add_engine_module_paths(engine, options={})
|
||||
modules_paths = engine.paths['modules']
|
||||
|
||||
if modules_paths
|
||||
modules_directories = modules_paths.existent_directories
|
||||
|
||||
modules_directories.each do |modules_directory|
|
||||
modules.add_module_path(modules_directory, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
require 'open3'
|
||||
require 'fileutils'
|
||||
require 'rex/proto/ntlm/crypt'
|
||||
|
||||
require 'metasploit/framework/jtr/cracker'
|
||||
require 'metasploit/framework/jtr/wordlist'
|
||||
|
||||
|
||||
module Msf
|
||||
|
@ -24,243 +25,28 @@ module Auxiliary::JohnTheRipper
|
|||
|
||||
register_options(
|
||||
[
|
||||
OptPath.new('JOHN_BASE', [false, 'The directory containing John the Ripper (src, run, doc)']),
|
||||
OptPath.new('JOHN_PATH', [false, 'The absolute path to the John the Ripper executable']),
|
||||
OptPath.new('Wordlist', [false, 'The path to an optional Wordlist']),
|
||||
OptBool.new('Munge',[false, 'Munge the Wordlist (Slower)', false])
|
||||
OptPath.new('CONFIG', [false, 'The path to a John config file to use instead of the default']),
|
||||
OptPath.new('CUSTOM_WORDLIST', [false, 'The path to an optional custom wordlist']),
|
||||
OptInt.new('ITERATION_TIMOUT', [false, 'The max-run-time for each iteration of cracking']),
|
||||
OptPath.new('JOHN_PATH', [false, 'The absolute path to the John the Ripper executable']),
|
||||
OptBool.new('MUTATE', [false, 'Apply common mutations to the Wordlist (SLOW)', false]),
|
||||
OptPath.new('POT', [false, 'The path to a John POT file to use instead of the default']),
|
||||
OptBool.new('USE_CREDS', [false, 'Use existing credential data saved in the database', true]),
|
||||
OptBool.new('USE_DB_INFO', [false, 'Use looted database schema info to seed the wordlist', true]),
|
||||
OptBool.new('USE_DEFAULT_WORDLIST', [false, 'Use the default metasploit wordlist', true]),
|
||||
OptBool.new('USE_HOSTNAMES', [false, 'Seed the wordlist with hostnames from the workspace', true]),
|
||||
OptBool.new('USE_ROOT_WORDS', [false, 'Use the Common Root Words Wordlist', true])
|
||||
], Msf::Auxiliary::JohnTheRipper
|
||||
)
|
||||
|
||||
@run_path = nil
|
||||
@john_path = ::File.join(Msf::Config.data_directory, "john")
|
||||
|
||||
autodetect_platform
|
||||
end
|
||||
|
||||
# @return [String] the run path instance variable if the platform is detectable, nil otherwise.
|
||||
def autodetect_platform
|
||||
return @run_path if @run_path
|
||||
cpuinfo_base = ::File.join(Msf::Config.data_directory, "cpuinfo")
|
||||
if File.directory?(cpuinfo_base)
|
||||
data = nil
|
||||
|
||||
case ::RUBY_PLATFORM
|
||||
when /mingw|cygwin|mswin/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.exe"
|
||||
if File.exists?(fname) and File.executable?(fname)
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /sse2/
|
||||
@run_path ||= "run.win32.sse2/john.exe"
|
||||
when /mmx/
|
||||
@run_path ||= "run.win32.mmx/john.exe"
|
||||
else
|
||||
@run_path ||= "run.win32.any/john.exe"
|
||||
end
|
||||
when /x86_64-linux/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.ia64.bin"
|
||||
if File.exists? fname
|
||||
::FileUtils.chmod(0755, fname) rescue nil
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /mmx/
|
||||
@run_path ||= "run.linux.x64.mmx/john"
|
||||
else
|
||||
@run_path ||= "run.linux.x86.any/john"
|
||||
end
|
||||
when /i[\d]86-linux/
|
||||
fname = "#{cpuinfo_base}/cpuinfo.ia32.bin"
|
||||
if File.exists? fname
|
||||
::FileUtils.chmod(0755, fname) rescue nil
|
||||
data = %x{"#{fname}"} rescue nil
|
||||
end
|
||||
case data
|
||||
when /sse2/
|
||||
@run_path ||= "run.linux.x86.sse2/john"
|
||||
when /mmx/
|
||||
@run_path ||= "run.linux.x86.mmx/john"
|
||||
else
|
||||
@run_path ||= "run.linux.x86.any/john"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return @run_path
|
||||
end
|
||||
|
||||
def john_session_id
|
||||
@session_id ||= ::Rex::Text.rand_text_alphanumeric(8)
|
||||
end
|
||||
|
||||
def john_pot_file
|
||||
::File.join( ::Msf::Config.config_directory, "john.pot" )
|
||||
end
|
||||
|
||||
def john_cracked_passwords
|
||||
ret = {}
|
||||
return ret if not ::File.exist?(john_pot_file)
|
||||
::File.open(john_pot_file, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
hash,clear = line.sub(/\r?\n$/, '').split(",", 2)
|
||||
ret[hash] = clear
|
||||
end
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
def john_show_passwords(hfile, format=nil)
|
||||
res = {:cracked => 0, :uncracked => 0, :users => {} }
|
||||
|
||||
john_command = john_binary_path
|
||||
|
||||
if john_command.nil?
|
||||
print_error("John the Ripper executable not found")
|
||||
return res
|
||||
end
|
||||
|
||||
pot = john_pot_file
|
||||
conf = ::File.join(john_base_path, "confs", "john.conf")
|
||||
|
||||
cmd = [ john_command, "--show", "--conf=#{conf}", "--pot=#{pot}", hfile]
|
||||
|
||||
if format
|
||||
cmd << "--format=" + format
|
||||
end
|
||||
|
||||
if RUBY_VERSION =~ /^1\.8\./
|
||||
cmd = cmd.join(" ")
|
||||
end
|
||||
|
||||
::IO.popen(cmd, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
line.chomp!
|
||||
print_status(line)
|
||||
if line =~ /(\d+) password hash(es)* cracked, (\d+) left/m
|
||||
res[:cracked] = $1.to_i
|
||||
res[:uncracked] = $2.to_i
|
||||
end
|
||||
|
||||
# XXX: If the password had : characters in it, we're screwed
|
||||
|
||||
bits = line.split(':', -1)
|
||||
|
||||
# Skip blank passwords
|
||||
next if not bits[2]
|
||||
|
||||
if (format== 'lm' or format == 'nt')
|
||||
res[ :users ][ bits[0] ] = bits[1]
|
||||
else
|
||||
bits.last.chomp!
|
||||
res[ :users ][ bits[0] ] = bits.drop(1)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
res
|
||||
end
|
||||
|
||||
def john_unshadow(passwd_file,shadow_file)
|
||||
|
||||
retval=""
|
||||
|
||||
john_command = john_binary_path
|
||||
|
||||
if john_command.nil?
|
||||
print_error("John the Ripper executable not found")
|
||||
return nil
|
||||
end
|
||||
|
||||
if File.exists?(passwd_file)
|
||||
unless File.readable?(passwd_file)
|
||||
print_error("We do not have permission to read #{passwd_file}")
|
||||
return nil
|
||||
end
|
||||
else
|
||||
print_error("File does not exist: #{passwd_file}")
|
||||
return nil
|
||||
end
|
||||
|
||||
if File.exists?(shadow_file)
|
||||
unless File.readable?(shadow_file)
|
||||
print_error("We do not have permission to read #{shadow_file}")
|
||||
return nil
|
||||
end
|
||||
else
|
||||
print_error("File does not exist: #{shadow_file}")
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
cmd = [ john_command.gsub(/john$/, "unshadow"), passwd_file , shadow_file ]
|
||||
|
||||
if RUBY_VERSION =~ /^1\.8\./
|
||||
cmd = cmd.join(" ")
|
||||
end
|
||||
::IO.popen(cmd, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
retval << line
|
||||
end
|
||||
end
|
||||
return retval
|
||||
end
|
||||
|
||||
def john_wordlist_path
|
||||
# We ship it under wordlists/
|
||||
path = ::File.join(john_base_path, "wordlists", "password.lst")
|
||||
# magnumripper/JohnTheRipper repo keeps it under run/
|
||||
unless ::File.file? path
|
||||
path = ::File.join(john_base_path, "run", "password.lst")
|
||||
end
|
||||
|
||||
path
|
||||
end
|
||||
|
||||
def john_binary_path
|
||||
path = nil
|
||||
if datastore['JOHN_PATH'] and ::File.file?(datastore['JOHN_PATH'])
|
||||
path = datastore['JOHN_PATH']
|
||||
::FileUtils.chmod(0755, path) rescue nil
|
||||
return path
|
||||
end
|
||||
|
||||
if not @run_path
|
||||
if ::RUBY_PLATFORM =~ /mingw|cygwin|mswin/
|
||||
::File.join(john_base_path, "john.exe")
|
||||
else
|
||||
path = ::File.join(john_base_path, "john")
|
||||
::FileUtils.chmod(0755, path) rescue nil
|
||||
end
|
||||
else
|
||||
path = ::File.join(john_base_path, @run_path)
|
||||
::FileUtils.chmod(0755, path) rescue nil
|
||||
end
|
||||
|
||||
if path and ::File.exists?(path)
|
||||
return path
|
||||
end
|
||||
|
||||
path = Rex::FileUtils.find_full_path("john") ||
|
||||
Rex::FileUtils.find_full_path("john.exe")
|
||||
end
|
||||
|
||||
def john_base_path
|
||||
if datastore['JOHN_BASE'] and ::File.directory?(datastore['JOHN_BASE'])
|
||||
return datastore['JOHN_BASE']
|
||||
end
|
||||
if datastore['JOHN_PATH'] and ::File.file?(datastore['JOHN_PATH'])
|
||||
return ::File.dirname( datastore['JOHN_PATH'] )
|
||||
end
|
||||
@john_path
|
||||
end
|
||||
|
||||
def john_expand_word(str)
|
||||
res = [str]
|
||||
str.split(/\W+/) {|w| res << w }
|
||||
res.uniq
|
||||
end
|
||||
|
||||
# @param pwd [String] Password recovered from cracking an LM hash
|
||||
# @param hash [String] NTLM hash for this password
|
||||
# @return [String] `pwd` converted to the correct case to match the
|
||||
# given NTLM hash
|
||||
# @return [nil] if no case matches the NT hash. This can happen when
|
||||
# `pwd` came from a john run that only cracked half of the LM hash
|
||||
def john_lm_upper_to_ntlm(pwd, hash)
|
||||
pwd = pwd.upcase
|
||||
hash = hash.upcase
|
||||
|
@ -273,179 +59,41 @@ module Auxiliary::JohnTheRipper
|
|||
end
|
||||
|
||||
|
||||
def john_crack(hfile, opts={})
|
||||
|
||||
res = {:cracked => 0, :uncracked => 0, :users => {} }
|
||||
|
||||
john_command = john_binary_path
|
||||
|
||||
if john_command.nil?
|
||||
print_error("John the Ripper executable not found")
|
||||
return nil
|
||||
end
|
||||
|
||||
# Don't bother making a log file, we'd just have to rm it when we're
|
||||
# done anyway.
|
||||
cmd = [ john_command, "--session=" + john_session_id, "--nolog"]
|
||||
|
||||
if opts[:conf]
|
||||
cmd << ( "--conf=" + opts[:conf] )
|
||||
else
|
||||
cmd << ( "--conf=" + ::File.join(john_base_path, "confs", "john.conf") )
|
||||
end
|
||||
|
||||
if opts[:pot]
|
||||
cmd << ( "--pot=" + opts[:pot] )
|
||||
else
|
||||
cmd << ( "--pot=" + john_pot_file )
|
||||
end
|
||||
|
||||
if opts[:format]
|
||||
cmd << ( "--format=" + opts[:format] )
|
||||
end
|
||||
|
||||
if opts[:wordlist]
|
||||
cmd << ( "--wordlist=" + opts[:wordlist] )
|
||||
end
|
||||
|
||||
if opts[:incremental]
|
||||
cmd << ( "--incremental=" + opts[:incremental] )
|
||||
end
|
||||
|
||||
if opts[:single]
|
||||
cmd << ( "--single=" + opts[:single] )
|
||||
end
|
||||
|
||||
if opts[:rules]
|
||||
cmd << ( "--rules=" + opts[:rules] )
|
||||
end
|
||||
|
||||
cmd << hfile
|
||||
|
||||
if RUBY_VERSION =~ /^1\.8\./
|
||||
cmd = cmd.join(" ")
|
||||
end
|
||||
|
||||
::IO.popen(cmd, "rb") do |fd|
|
||||
fd.each_line do |line|
|
||||
print_status("Output: #{line.strip}")
|
||||
end
|
||||
end
|
||||
|
||||
res
|
||||
# This method creates a new {Metasploit::Framework::JtR::Cracker} and populates
|
||||
# some of the attributes based on the module datastore options.
|
||||
#
|
||||
# @return [nilClass] if there is no active framework db connection
|
||||
# @return [Metasploit::Framework::JtR::Cracker] if it successfully creates a JtR Cracker object
|
||||
def new_john_cracker
|
||||
return nil unless framework.db.active
|
||||
Metasploit::Framework::JtR::Cracker.new(
|
||||
config: datastore['CONFIG'],
|
||||
john_path: datastore['JOHN_PATH'],
|
||||
max_runtime: datastore['ITERATION_TIMEOUT'],
|
||||
pot: datastore['POT'],
|
||||
wordlist: datastore['CUSTOM_WORDLIST']
|
||||
)
|
||||
end
|
||||
|
||||
def build_seed
|
||||
|
||||
seed = []
|
||||
#Seed the wordlist with Database , Table, and Instance Names
|
||||
|
||||
count = 0
|
||||
schemas = myworkspace.notes.where('ntype like ?', '%.schema%')
|
||||
unless schemas.nil? or schemas.empty?
|
||||
schemas.each do |anote|
|
||||
seed << anote.data['DBName']
|
||||
count += 1
|
||||
anote.data['Tables'].each do |table|
|
||||
seed << table['TableName']
|
||||
count += 1
|
||||
table['Columns'].each do |column|
|
||||
seed << column['ColumnName']
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
print_status "Seeding wordlist with DB schema info... #{count} words added"
|
||||
count = 0
|
||||
|
||||
instances = myworkspace.notes.find(:all, :conditions => ['ntype=?', 'mssql.instancename'])
|
||||
unless instances.nil? or instances.empty?
|
||||
instances.each do |anote|
|
||||
seed << anote.data['InstanceName']
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
print_status "Seeding with MSSQL Instance Names....#{count} words added"
|
||||
count = 0
|
||||
|
||||
# Seed the wordlist with usernames, passwords, and hostnames
|
||||
|
||||
myworkspace.hosts.find(:all).each do |o|
|
||||
if o.name
|
||||
seed << john_expand_word( o.name )
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
print_status "Seeding with hostnames....#{count} words added"
|
||||
count = 0
|
||||
|
||||
|
||||
myworkspace.creds.each do |o|
|
||||
if o.user
|
||||
seed << john_expand_word( o.user )
|
||||
count +=1
|
||||
end
|
||||
if (o.pass and o.ptype !~ /hash/)
|
||||
seed << john_expand_word( o.pass )
|
||||
count += 1
|
||||
end
|
||||
end
|
||||
print_status "Seeding with found credentials....#{count} words added"
|
||||
count = 0
|
||||
|
||||
# Grab any known passwords out of the john.pot file
|
||||
john_cracked_passwords.values do |v|
|
||||
seed << v
|
||||
count += 1
|
||||
end
|
||||
print_status "Seeding with cracked passwords from John....#{count} words added"
|
||||
count = 0
|
||||
|
||||
#Grab the default John Wordlist
|
||||
john = File.open(john_wordlist_path, "rb")
|
||||
john.each_line do |line|
|
||||
seed << line.chomp
|
||||
count += 1
|
||||
end
|
||||
print_status "Seeding with default John wordlist...#{count} words added"
|
||||
count = 0
|
||||
|
||||
if datastore['Wordlist']
|
||||
wordlist= File.open(datastore['Wordlist'], "rb")
|
||||
wordlist.each_line do |line|
|
||||
seed << line.chomp
|
||||
count ==1
|
||||
end
|
||||
print_status "Seeding from user supplied wordlist...#{count} words added"
|
||||
end
|
||||
|
||||
|
||||
|
||||
unless seed.empty?
|
||||
seed.flatten!
|
||||
seed.uniq!
|
||||
if datastore['Munge']
|
||||
mungedseed=[]
|
||||
seed.each do |word|
|
||||
munged = word.gsub(/[sS]/, "$").gsub(/[aA]/,"@").gsub(/[oO]/,"0")
|
||||
mungedseed << munged
|
||||
munged.gsub!(/[eE]/, "3")
|
||||
munged.gsub!(/[tT]/, "7")
|
||||
mungedseed << munged
|
||||
end
|
||||
print_status "Adding #{mungedseed.count} words from munging..."
|
||||
seed << mungedseed
|
||||
seed.flatten!
|
||||
seed.uniq!
|
||||
end
|
||||
end
|
||||
print_status "De-duping the wordlist...."
|
||||
|
||||
print_status("Wordlist Seeded with #{seed.length} words")
|
||||
|
||||
return seed
|
||||
|
||||
# This method instantiates a {Metasploit::Framework::JtR::Wordlist}, writes the data
|
||||
# out to a file and returns the {rex::quickfile} object.
|
||||
#
|
||||
# @return [nilClass] if there is no active framework db connection
|
||||
# @return [Rex::Quickfile] if it successfully wrote the wordlist to a file
|
||||
def wordlist_file
|
||||
return nil unless framework.db.active
|
||||
wordlist = Metasploit::Framework::JtR::Wordlist.new(
|
||||
custom_wordlist: datastore['CUSTOM_WORDLIST'],
|
||||
mutate: datastore['MUTATE'],
|
||||
use_creds: datastore['USE_CREDS'],
|
||||
use_db_info: datastore['USE_DB_INFO'],
|
||||
use_default_wordlist: datastore['USE_DEFAULT_WORDLIST'],
|
||||
use_hostnames: datastore['USE_HOSTNAMES'],
|
||||
use_common_root: datastore['USE_ROOT_WORDS'],
|
||||
workspace: myworkspace
|
||||
)
|
||||
wordlist.to_file
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,6 +21,10 @@ module Auxiliary::Login
|
|||
def initialize(info = {})
|
||||
super
|
||||
|
||||
create_login_ivars
|
||||
end
|
||||
|
||||
def create_login_ivars
|
||||
# Appended to by each read and gets reset after each send. Doing it
|
||||
# this way lets us deal with partial reads in the middle of expect
|
||||
# strings, e.g., the first recv returns "Pa" and the second returns
|
||||
|
|
|
@ -19,6 +19,5 @@ require 'msf/core/auxiliary/login'
|
|||
require 'msf/core/auxiliary/rservices'
|
||||
require 'msf/core/auxiliary/cisco'
|
||||
require 'msf/core/auxiliary/nmap'
|
||||
require 'msf/core/auxiliary/jtr'
|
||||
require 'msf/core/auxiliary/iax2'
|
||||
require 'msf/core/auxiliary/pii'
|
||||
|
|
|
@ -8,10 +8,13 @@ module Msf
|
|||
###
|
||||
|
||||
module Auxiliary::Report
|
||||
extend Metasploit::Framework::Require
|
||||
|
||||
optionally_include_metasploit_credential_creation
|
||||
|
||||
def initialize(info = {})
|
||||
super
|
||||
# This method overrides the method from Metasploit::Credential to check for an active db
|
||||
def active_db?
|
||||
framework.db.active
|
||||
end
|
||||
|
||||
# Shortcut method for detecting when the DB is active
|
||||
|
@ -23,6 +26,18 @@ module Auxiliary::Report
|
|||
@myworkspace = framework.db.find_workspace(self.workspace)
|
||||
end
|
||||
|
||||
# This method safely get the workspace ID. It handles if the db is not active
|
||||
#
|
||||
# @return [NilClass] if there is no DB connection
|
||||
# @return [Fixnum] the ID of the current {Mdm::Workspace}
|
||||
def myworkspace_id
|
||||
if framework.db.active
|
||||
myworkspace.id
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def mytask
|
||||
if self[:task]
|
||||
return self[:task].record
|
||||
|
@ -387,6 +402,9 @@ module Auxiliary::Report
|
|||
print_status "Collecting #{cred_opts[:user]}:#{cred_opts[:pass]}"
|
||||
framework.db.report_auth_info(cred_opts)
|
||||
end
|
||||
|
||||
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
require 'csv'
|
||||
require 'tmpdir'
|
||||
require 'uri'
|
||||
require 'zip'
|
||||
|
||||
#
|
||||
#
|
||||
|
@ -56,6 +55,7 @@ require 'rex/parser/retina_xml'
|
|||
# Project
|
||||
#
|
||||
|
||||
require 'metasploit/framework/require'
|
||||
require 'msf/core/db_manager/import_msf_xml'
|
||||
|
||||
module Msf
|
||||
|
@ -156,7 +156,10 @@ end
|
|||
#
|
||||
###
|
||||
class DBManager
|
||||
extend Metasploit::Framework::Require
|
||||
|
||||
include Msf::DBManager::ImportMsfXml
|
||||
optionally_include_metasploit_credential_creation
|
||||
|
||||
def rfc3330_reserved(ip)
|
||||
case ip.class.to_s
|
||||
|
@ -1371,8 +1374,6 @@ class DBManager
|
|||
=end
|
||||
ntype = opts.delete(:type) || opts.delete(:ntype) || (raise RuntimeError, "A note :type or :ntype is required")
|
||||
data = opts[:data]
|
||||
method = nil
|
||||
args = []
|
||||
note = nil
|
||||
|
||||
conditions = { :ntype => ntype }
|
||||
|
@ -1381,15 +1382,7 @@ class DBManager
|
|||
|
||||
case mode
|
||||
when :unique
|
||||
notes = wspace.notes.where(conditions)
|
||||
|
||||
# Only one note of this type should exist, make a new one if it
|
||||
# isn't there. If it is, grab it and overwrite its data.
|
||||
if notes.empty?
|
||||
note = wspace.notes.new(conditions)
|
||||
else
|
||||
note = notes[0]
|
||||
end
|
||||
note = wspace.notes.where(conditions).first_or_initialize
|
||||
note.data = data
|
||||
when :unique_data
|
||||
notes = wspace.notes.where(conditions)
|
||||
|
@ -2184,9 +2177,15 @@ class DBManager
|
|||
# @return [Integer] ID of created report
|
||||
def report_report(opts)
|
||||
return if not active
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
created = opts.delete(:created_at)
|
||||
updated = opts.delete(:updated_at)
|
||||
state = opts.delete(:state)
|
||||
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
report = Report.new(opts)
|
||||
report.created_at = created
|
||||
report.updated_at = updated
|
||||
|
||||
unless report.valid?
|
||||
errors = report.errors.full_messages.join('; ')
|
||||
raise RuntimeError "Report to be imported is not valid: #{errors}"
|
||||
|
@ -2201,10 +2200,14 @@ class DBManager
|
|||
# Creates a ReportArtifact based on passed parameters.
|
||||
# @param opts [Hash] of ReportArtifact attributes
|
||||
def report_artifact(opts)
|
||||
return if not active
|
||||
|
||||
artifacts_dir = Report::ARTIFACT_DIR
|
||||
tmp_path = opts[:file_path]
|
||||
artifact_name = File.basename tmp_path
|
||||
new_path = File.join(artifacts_dir, artifact_name)
|
||||
created = opts.delete(:created_at)
|
||||
updated = opts.delete(:updated_at)
|
||||
|
||||
unless File.exists? tmp_path
|
||||
raise DBImportError 'Report artifact file to be imported does not exist.'
|
||||
|
@ -2222,6 +2225,9 @@ class DBManager
|
|||
FileUtils.copy(tmp_path, new_path)
|
||||
opts[:file_path] = new_path
|
||||
artifact = ReportArtifact.new(opts)
|
||||
artifact.created_at = created
|
||||
artifact.updated_at = updated
|
||||
|
||||
unless artifact.valid?
|
||||
errors = artifact.errors.full_messages.join('; ')
|
||||
raise RuntimeError "Artifact to be imported is not valid: #{errors}"
|
||||
|
@ -2906,29 +2912,36 @@ class DBManager
|
|||
|
||||
data = ""
|
||||
::File.open(filename, 'rb') do |f|
|
||||
data = f.read(4)
|
||||
# This check is the largest (byte-wise) that we need to do
|
||||
# since the other 4-byte checks will be subsets of this larger one.
|
||||
data = f.read(Metasploit::Credential::Exporter::Pwdump::FILE_ID_STRING.size)
|
||||
end
|
||||
if data.nil?
|
||||
raise DBImportError.new("Zero-length file")
|
||||
end
|
||||
|
||||
case data[0,4]
|
||||
when "PK\x03\x04"
|
||||
data = Zip::ZipFile.open(filename)
|
||||
when "\xd4\xc3\xb2\xa1", "\xa1\xb2\xc3\xd4"
|
||||
data = PacketFu::PcapFile.new(:filename => filename)
|
||||
if data.index(Metasploit::Credential::Exporter::Pwdump::FILE_ID_STRING)
|
||||
data = ::File.open(filename, 'rb')
|
||||
else
|
||||
::File.open(filename, 'rb') do |f|
|
||||
sz = f.stat.size
|
||||
data = f.read(sz)
|
||||
case data[0,4]
|
||||
when "PK\x03\x04"
|
||||
data = Zip::File.open(filename)
|
||||
when "\xd4\xc3\xb2\xa1", "\xa1\xb2\xc3\xd4"
|
||||
data = PacketFu::PcapFile.new(:filename => filename)
|
||||
else
|
||||
::File.open(filename, 'rb') do |f|
|
||||
sz = f.stat.size
|
||||
data = f.read(sz)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
if block
|
||||
import(args.merge(:data => data)) { |type,data| yield type,data }
|
||||
else
|
||||
import(args.merge(:data => data))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# A dispatcher method that figures out the data's file type,
|
||||
|
@ -2937,7 +2950,6 @@ class DBManager
|
|||
# is unknown.
|
||||
def import(args={}, &block)
|
||||
data = args[:data] || args['data']
|
||||
wspace = args[:wspace] || args['wspace'] || workspace
|
||||
ftype = import_filetype_detect(data)
|
||||
yield(:filetype, @import_filedata[:type]) if block
|
||||
self.send "import_#{ftype}".to_sym, args, &block
|
||||
|
@ -2958,6 +2970,7 @@ class DBManager
|
|||
# :ip_list
|
||||
# :libpcap
|
||||
# :mbsa_xml
|
||||
# :msf_cred_dump_zip
|
||||
# :msf_pwdump
|
||||
# :msf_xml
|
||||
# :msf_zip
|
||||
|
@ -2979,9 +2992,11 @@ class DBManager
|
|||
# :wapiti_xml
|
||||
#
|
||||
# If there is no match, an error is raised instead.
|
||||
#
|
||||
# @raise DBImportError if the type can't be detected
|
||||
def import_filetype_detect(data)
|
||||
|
||||
if data and data.kind_of? Zip::ZipFile
|
||||
if data and data.kind_of? Zip::File
|
||||
if data.entries.empty?
|
||||
raise DBImportError.new("The zip file provided is empty.")
|
||||
end
|
||||
|
@ -2991,6 +3006,11 @@ class DBManager
|
|||
@import_filedata[:zip_basename] = @import_filedata[:zip_filename].gsub(/\.zip$/,"")
|
||||
@import_filedata[:zip_entry_names] = data.entries.map {|x| x.name}
|
||||
|
||||
if @import_filedata[:zip_entry_names].include?(Metasploit::Credential::Importer::Zip::MANIFEST_FILE_NAME)
|
||||
@import_filedata[:type] = "Metasploit Credential Dump"
|
||||
return :msf_cred_dump_zip
|
||||
end
|
||||
|
||||
xml_files = @import_filedata[:zip_entry_names].grep(/^(.*)\.xml$/)
|
||||
|
||||
# TODO This check for our zip export should be more extensive
|
||||
|
@ -3014,6 +3034,12 @@ class DBManager
|
|||
return :libpcap
|
||||
end
|
||||
|
||||
# msfpwdump
|
||||
if data.present? && data.kind_of?(::File)
|
||||
@import_filedata[:type] = "Metasploit PWDump Export"
|
||||
return :msf_pwdump
|
||||
end
|
||||
|
||||
# This is a text string, lets make sure its treated as binary
|
||||
data = data.unpack("C*").pack("C*")
|
||||
if data and data.to_s.strip.length == 0
|
||||
|
@ -3376,7 +3402,7 @@ class DBManager
|
|||
end
|
||||
end # tcp or udp
|
||||
|
||||
inspect_single_packet(pkt,wspace,args[:task])
|
||||
inspect_single_packet(pkt,wspace,args)
|
||||
|
||||
end # data.body.map
|
||||
|
||||
|
@ -3389,16 +3415,17 @@ class DBManager
|
|||
# Do all the single packet analysis we can while churning through the pcap
|
||||
# the first time. Multiple packet inspection will come later, where we can
|
||||
# do stream analysis, compare requests and responses, etc.
|
||||
def inspect_single_packet(pkt,wspace,task=nil)
|
||||
def inspect_single_packet(pkt,wspace,args)
|
||||
if pkt.is_tcp? or pkt.is_udp?
|
||||
inspect_single_packet_http(pkt,wspace,task)
|
||||
inspect_single_packet_http(pkt,wspace,args)
|
||||
end
|
||||
end
|
||||
|
||||
# Checks for packets that are headed towards port 80, are tcp, contain an HTTP/1.0
|
||||
# line, contains an Authorization line, contains a b64-encoded credential, and
|
||||
# extracts it. Reports this credential and solidifies the service as HTTP.
|
||||
def inspect_single_packet_http(pkt,wspace,task=nil)
|
||||
def inspect_single_packet_http(pkt,wspace,args)
|
||||
task = args.fetch(:task, nil)
|
||||
# First, check the server side (data from port 80).
|
||||
if pkt.is_tcp? and pkt.tcp_src == 80 and !pkt.payload.nil? and !pkt.payload.empty?
|
||||
if pkt.payload =~ /^HTTP\x2f1\x2e[01]/n
|
||||
|
@ -3442,17 +3469,37 @@ class DBManager
|
|||
:name => "http",
|
||||
:task => task
|
||||
)
|
||||
report_auth_info(
|
||||
:workspace => wspace,
|
||||
:host => pkt.ip_daddr,
|
||||
:port => pkt.tcp_dst,
|
||||
:proto => "tcp",
|
||||
:type => "password",
|
||||
:active => true, # Once we can build a stream, determine if the auth was successful. For now, assume it is.
|
||||
:user => user,
|
||||
:pass => pass,
|
||||
:task => task
|
||||
)
|
||||
|
||||
service_data = {
|
||||
address: pkt.ip_daddr,
|
||||
port: pkt.tcp_dst,
|
||||
service_name: 'http',
|
||||
protocol: 'tcp',
|
||||
workspace_id: wspace.id
|
||||
}
|
||||
service_data[:task_id] = task.id if task
|
||||
|
||||
filename = args[:filename]
|
||||
|
||||
credential_data = {
|
||||
origin_type: :import,
|
||||
private_data: pass,
|
||||
private_type: :password,
|
||||
username: user,
|
||||
filename: filename
|
||||
}
|
||||
credential_data.merge!(service_data)
|
||||
credential_core = create_credential(credential_data)
|
||||
|
||||
login_data = {
|
||||
core: credential_core,
|
||||
status: Metasploit::Model::Login::Status::UNTRIED
|
||||
}
|
||||
|
||||
login_data.merge!(service_data)
|
||||
|
||||
create_credential_login(login_data)
|
||||
|
||||
# That's all we want to know from this service.
|
||||
return :something_significant
|
||||
end
|
||||
|
@ -3496,109 +3543,15 @@ class DBManager
|
|||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Metasploit PWDump Export
|
||||
#
|
||||
# This file format is generated by the db_export -f pwdump and
|
||||
# the Metasploit Express and Pro report types of "PWDump."
|
||||
#
|
||||
# This particular block scheme is temporary, since someone is
|
||||
# bound to want to import gigantic lists, so we'll want a
|
||||
# stream parser eventually (just like the other non-nmap formats).
|
||||
#
|
||||
# The file format is:
|
||||
# # 1.2.3.4:23/tcp (telnet)
|
||||
# username password
|
||||
# user2 p\x01a\x02ss2
|
||||
# <BLANK> pass3
|
||||
# user3 <BLANK>
|
||||
# smbuser:sid:lmhash:nthash:::
|
||||
#
|
||||
# Note the leading hash for the host:port line. Note also all usernames
|
||||
# and passwords must be in 7-bit ASCII (character sequences of "\x01"
|
||||
# will be interpolated -- this includes spaces, which must be notated
|
||||
# as "\x20". Blank usernames or passwords should be <BLANK>.
|
||||
#
|
||||
|
||||
# Perform in an import of an msfpwdump file
|
||||
def import_msf_pwdump(args={}, &block)
|
||||
data = args[:data]
|
||||
wspace = args[:wspace] || workspace
|
||||
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
||||
last_host = nil
|
||||
|
||||
addr = nil
|
||||
port = nil
|
||||
proto = nil
|
||||
sname = nil
|
||||
ptype = nil
|
||||
active = false # Are there cases where imported creds are good? I just hate trusting the import right away.
|
||||
|
||||
data.each_line do |line|
|
||||
case line
|
||||
when /^[\s]*#/ # Comment lines
|
||||
if line[/^#[\s]*([0-9.]+):([0-9]+)(\x2f(tcp|udp))?[\s]*(\x28([^\x29]*)\x29)?/n]
|
||||
addr = $1
|
||||
port = $2
|
||||
proto = $4
|
||||
sname = $6
|
||||
end
|
||||
when /^[\s]*Warning:/
|
||||
# Discard warning messages.
|
||||
next
|
||||
|
||||
# SMB Hash
|
||||
when /^[\s]*([^\s:]+):[0-9]+:([A-Fa-f0-9]+:[A-Fa-f0-9]+):[^\s]*$/
|
||||
user = ([nil, "<BLANK>"].include?($1)) ? "" : $1
|
||||
pass = ([nil, "<BLANK>"].include?($2)) ? "" : $2
|
||||
ptype = "smb_hash"
|
||||
|
||||
# SMB Hash
|
||||
when /^[\s]*([^\s:]+):([0-9]+):NO PASSWORD\*+:NO PASSWORD\*+[^\s]*$/
|
||||
user = ([nil, "<BLANK>"].include?($1)) ? "" : $1
|
||||
pass = ""
|
||||
ptype = "smb_hash"
|
||||
|
||||
# SMB Hash with cracked plaintext, or just plain old plaintext
|
||||
when /^[\s]*([^\s:]+):(.+):[A-Fa-f0-9]*:[A-Fa-f0-9]*:::$/
|
||||
user = ([nil, "<BLANK>"].include?($1)) ? "" : $1
|
||||
pass = ([nil, "<BLANK>"].include?($2)) ? "" : $2
|
||||
ptype = "password"
|
||||
|
||||
# Must be a user pass
|
||||
when /^[\s]*([\x21-\x7f]+)[\s]+([\x21-\x7f]+)?/n
|
||||
user = ([nil, "<BLANK>"].include?($1)) ? "" : dehex($1)
|
||||
pass = ([nil, "<BLANK>"].include?($2)) ? "" : dehex($2)
|
||||
ptype = "password"
|
||||
else # Some unknown line not broken by a space.
|
||||
next
|
||||
end
|
||||
|
||||
next unless [addr,port,user,pass].compact.size == 4
|
||||
next unless ipv46_validator(addr) # Skip Malformed addrs
|
||||
next unless port[/^[0-9]+$/] # Skip malformed ports
|
||||
if bl.include? addr
|
||||
next
|
||||
else
|
||||
yield(:address,addr) if block and addr != last_host
|
||||
last_host = addr
|
||||
end
|
||||
|
||||
cred_info = {
|
||||
:host => addr,
|
||||
:port => port,
|
||||
:user => user,
|
||||
:pass => pass,
|
||||
:type => ptype,
|
||||
:workspace => wspace,
|
||||
:task => args[:task]
|
||||
}
|
||||
cred_info[:proto] = proto if proto
|
||||
cred_info[:sname] = sname if sname
|
||||
cred_info[:active] = active
|
||||
|
||||
report_auth_info(cred_info)
|
||||
user = pass = ptype = nil
|
||||
end
|
||||
|
||||
filename = File.basename(args[:data].path)
|
||||
wspace = args[:wspace] || workspace
|
||||
origin = Metasploit::Credential::Origin::Import.create!(filename: filename)
|
||||
importer = Metasploit::Credential::Importer::Pwdump.new(input: args[:data], workspace: wspace, filename: filename, origin:origin)
|
||||
importer.import!
|
||||
importer.input.close unless importer.input.closed?
|
||||
end
|
||||
|
||||
# If hex notation is present, turn them into a character.
|
||||
|
@ -3649,7 +3602,7 @@ class DBManager
|
|||
# XXX: Refactor so it's not quite as sanity-blasting.
|
||||
def import_msf_zip(args={}, &block)
|
||||
data = args[:data]
|
||||
wpsace = args[:wspace] || workspace
|
||||
wspace = args[:wspace] || workspace
|
||||
bl = validate_ips(args[:blacklist]) ? args[:blacklist].split : []
|
||||
|
||||
new_tmp = ::File.join(Dir::tmpdir,"msf","imp_#{Rex::Text::rand_text_alphanumeric(4)}",@import_filedata[:zip_basename])
|
||||
|
@ -3680,9 +3633,10 @@ class DBManager
|
|||
}
|
||||
|
||||
data.entries.each do |e|
|
||||
target = ::File.join(@import_filedata[:zip_tmp],e.name)
|
||||
target = ::File.join(@import_filedata[:zip_tmp], e.name)
|
||||
data.extract(e,target)
|
||||
if target =~ /^.*.xml$/
|
||||
|
||||
if target =~ /\.xml\z/
|
||||
target_data = ::File.open(target, "rb") {|f| f.read 1024}
|
||||
if import_filetype_detect(target_data) == :msf_xml
|
||||
@import_filedata[:zip_extracted_xml] = target
|
||||
|
@ -3690,6 +3644,16 @@ class DBManager
|
|||
end
|
||||
end
|
||||
|
||||
# Import any creds if there are some in the import file
|
||||
Dir.entries(@import_filedata[:zip_tmp]).each do |entry|
|
||||
if entry =~ /^.*#{Regexp.quote(Metasploit::Credential::Exporter::Core::CREDS_DUMP_FILE_IDENTIFIER)}.*/
|
||||
manifest_file_path = File.join(@import_filedata[:zip_tmp], entry, Metasploit::Credential::Importer::Zip::MANIFEST_FILE_NAME)
|
||||
if File.exists? manifest_file_path
|
||||
import_msf_cred_dump(manifest_file_path, wspace)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This will kick the newly-extracted XML file through
|
||||
# the import_file process all over again.
|
||||
if @import_filedata[:zip_extracted_xml]
|
||||
|
@ -3856,6 +3820,31 @@ class DBManager
|
|||
end
|
||||
end
|
||||
|
||||
# Import credentials given a path to a valid manifest file
|
||||
#
|
||||
# @param creds_dump_manifest_path [String]
|
||||
# @param workspace [Mdm::Workspace] Default: {#workspace}
|
||||
# @return [void]
|
||||
def import_msf_cred_dump(creds_dump_manifest_path, workspace)
|
||||
manifest_file = File.open(creds_dump_manifest_path)
|
||||
origin = Metasploit::Credential::Origin::Import.create!(filename: File.basename(creds_dump_manifest_path))
|
||||
importer = Metasploit::Credential::Importer::Core.new(workspace: workspace, input: manifest_file, origin: origin)
|
||||
importer.import!
|
||||
end
|
||||
|
||||
# Import credentials given a path to a valid manifest file
|
||||
#
|
||||
# @option args [String] :filename
|
||||
# @option args [Mdm::Workspace] :wspace Default: {#workspace}
|
||||
# @return [void]
|
||||
def import_msf_cred_dump_zip(args = {})
|
||||
wspace = args[:wspace] || workspace
|
||||
origin = Metasploit::Credential::Origin::Import.create!(filename: File.basename(args[:filename]))
|
||||
importer = Metasploit::Credential::Importer::Zip.new(workspace: wspace, input: File.open(args[:filename]), origin: origin)
|
||||
importer.import!
|
||||
nil
|
||||
end
|
||||
|
||||
# @param report [REXML::Element] to be imported
|
||||
# @param args [Hash]
|
||||
# @param base_dir [String]
|
||||
|
|
|
@ -28,175 +28,19 @@ class Export
|
|||
true
|
||||
end
|
||||
|
||||
# Creates the PWDUMP text file. smb_hash and ssh_key credentials are
|
||||
# treated specially -- all other ptypes are treated as plain text.
|
||||
#
|
||||
# Some day in the very near future, this file format will be importable --
|
||||
# the comment preceding the credential is always in the format of IPAddr:Port/Proto (name),
|
||||
# so it should be a simple matter to read them back in and store credentials
|
||||
# in the right place. Finally, this format is already parsable by John the Ripper,
|
||||
# so hashes can be bruteforced offline.
|
||||
|
||||
# Performs an export of the workspace's `Metasploit::Credential::Login` objects in pwdump format
|
||||
# @param path [String] the path on the local filesystem where the exported data will be written
|
||||
# @return [void]
|
||||
def to_pwdump_file(path, &block)
|
||||
yield(:status, "start", "password dump") if block_given?
|
||||
creds = extract_credentials
|
||||
report_file = ::File.open(path, "wb")
|
||||
report_file.write "# Metasploit PWDump Export v1\n"
|
||||
report_file.write "# Generated: #{Time.now.utc}\n"
|
||||
report_file.write "# Project: #{myworkspace.name}\n"
|
||||
report_file.write "#\n"
|
||||
report_file.write "#" * 40; report_file.write "\n"
|
||||
exporter = Metasploit::Credential::Exporter::Pwdump.new(workspace: workspace)
|
||||
|
||||
count = count_credentials("smb_hash",creds)
|
||||
scount = creds.has_key?("smb_hash") ? creds["smb_hash"].size : 0
|
||||
yield(:status, "start", "LM/NTLM Hash dump") if block_given?
|
||||
report_file.write "# LM/NTLM Hashes (%d services, %d hashes)\n" % [scount, count]
|
||||
write_credentials("smb_hash",creds,report_file)
|
||||
|
||||
count = count_credentials("smb_netv1_hash",creds)
|
||||
scount = creds.has_key?("smb_netv1_hash") ? creds["smb_netv1_hash"].size : 0
|
||||
yield(:status, "start", "NETLMv1/NETNTLMv1 Hash dump") if block_given?
|
||||
report_file.write "# NETLMv1/NETNTLMv1 Hashes (%d services, %d hashes)\n" % [scount, count]
|
||||
write_credentials("smb_netv1_hash",creds,report_file)
|
||||
|
||||
count = count_credentials("smb_netv2_hash",creds)
|
||||
scount = creds.has_key?("smb_netv2_hash") ? creds["smb_netv2_hash"].size : 0
|
||||
yield(:status, "start", "NETLMv2/NETNTLMv2 Hash dump") if block_given?
|
||||
report_file.write "# NETLMv2/NETNTLMv2 Hashes (%d services, %d hashes)\n" % [scount, count]
|
||||
write_credentials("smb_netv2_hash",creds,report_file)
|
||||
|
||||
count = count_credentials("ssh_key",creds)
|
||||
scount = creds.has_key?("ssh_key") ? creds["ssh_key"].size : 0
|
||||
yield(:status, "start", "SSH Key dump") if block_given?
|
||||
report_file.write "# SSH Private Keys (%d services, %d keys)\n" % [scount, count]
|
||||
write_credentials("ssh_key",creds,report_file)
|
||||
|
||||
count = count_credentials("text",creds)
|
||||
scount = creds.has_key?("text") ? creds["text"].size : 0
|
||||
yield(:status, "start", "Plaintext Credential dump") if block_given?
|
||||
report_file.write "# Plaintext Credentials (%d services, %d credentials)\n" % [scount, count]
|
||||
write_credentials("text",creds,report_file)
|
||||
|
||||
report_file.flush
|
||||
report_file.close
|
||||
yield(:status, "complete", "password dump") if block_given?
|
||||
File.open(path, 'w') do |file|
|
||||
file << exporter.rendered_output
|
||||
end
|
||||
true
|
||||
end
|
||||
|
||||
# Counts the total number of credentials for its type.
|
||||
def count_credentials(ptype,creds)
|
||||
sz = 0
|
||||
if creds[ptype]
|
||||
creds[ptype].each_pair { |svc, data| data.each { |c| sz +=1 } }
|
||||
end
|
||||
return sz
|
||||
end
|
||||
|
||||
# Formats credentials according to their type, and writes it out to the
|
||||
# supplied report file. Note for reimporting: Blank values are <BLANK>
|
||||
def write_credentials(ptype,creds,report_file)
|
||||
if creds[ptype]
|
||||
creds[ptype].each_pair do |svc, data|
|
||||
report_file.write "# #{svc}\n"
|
||||
case ptype
|
||||
when "smb_hash"
|
||||
data.each do |c|
|
||||
user = (c.user.nil? || c.user.empty?) ? "<BLANK>" : c.user
|
||||
pass = (c.pass.nil? || c.pass.empty?) ? "<BLANK>" : c.pass
|
||||
report_file.write "%s:%d:%s:::\n" % [user,c.id,pass]
|
||||
end
|
||||
when "smb_netv1_hash"
|
||||
data.each do |c|
|
||||
user = (c.user.nil? || c.user.empty?) ? "<BLANK>" : c.user
|
||||
pass = (c.pass.nil? || c.pass.empty?) ? "<BLANK>" : c.pass
|
||||
report_file.write "%s::%s\n" % [user,pass]
|
||||
end
|
||||
when "smb_netv2_hash"
|
||||
data.each do |c|
|
||||
user = (c.user.nil? || c.user.empty?) ? "<BLANK>" : c.user
|
||||
pass = (c.pass.nil? || c.pass.empty?) ? "<BLANK>" : c.pass
|
||||
if pass != "<BLANK>"
|
||||
pass = (c.pass.upcase =~ /^[\x20-\x7e]*:[A-F0-9]{48}:[A-F0-9]{50,}/nm) ? c.pass : "<BLANK>"
|
||||
end
|
||||
if pass == "<BLANK>"
|
||||
# Basically this is an error (maybe around [\x20-\x7e] in regex) above
|
||||
report_file.write(user + "::" + pass + ":")
|
||||
report_file.write(pass + ":" + pass + ":" + pass + "\n")
|
||||
else
|
||||
datas = pass.split(":")
|
||||
if datas[1] != "00" * 24
|
||||
report_file.write "# netlmv2\n"
|
||||
report_file.write(user + "::" + datas[0] + ":")
|
||||
report_file.write(datas[3] + ":" + datas[1][0,32] + ":" + datas[1][32,16] + "\n")
|
||||
end
|
||||
report_file.write "# netntlmv2\n"
|
||||
report_file.write(user + "::" + datas[0] + ":")
|
||||
report_file.write(datas[3] + ":" + datas[2][0,32] + ":" + datas[2][32..-1] + "\n")
|
||||
end
|
||||
end
|
||||
when "ssh_key"
|
||||
data.each do |c|
|
||||
if ::File.exists?(c.pass) && ::File.readable?(c.pass)
|
||||
user = (c.user.nil? || c.user.empty?) ? "<BLANK>" : c.user
|
||||
key = ::File.open(c.pass) {|f| f.read f.stat.size}
|
||||
key_id = (c.proof && c.proof[/^KEY=/]) ? c.proof[4,47] : "<NO-ID>"
|
||||
report_file.write "#{user} '#{key_id}'\n"
|
||||
report_file.write key
|
||||
report_file.write "\n" unless key[-1,1] == "\n"
|
||||
# Report file missing / permissions issues in the report itself.
|
||||
elsif !::File.exists?(c.pass)
|
||||
report_file.puts "Warning: missing private key file '#{c.pass}'."
|
||||
else
|
||||
report_file.puts "Warning: could not read the private key '#{c.pass}'."
|
||||
end
|
||||
end
|
||||
when "text"
|
||||
data.each do |c|
|
||||
user = (c.user.nil? || c.user.empty?) ? "<BLANK>" : Rex::Text.ascii_safe_hex(c.user, true)
|
||||
pass = (c.pass.nil? || c.pass.empty?) ? "<BLANK>" : Rex::Text.ascii_safe_hex(c.pass, true)
|
||||
report_file.write "%s:%s:::\n" % [user,pass]
|
||||
end
|
||||
end
|
||||
report_file.flush
|
||||
end
|
||||
else
|
||||
report_file.write "# No credentials for this type were discovered.\n"
|
||||
end
|
||||
report_file.write "#" * 40; report_file.write "\n"
|
||||
end
|
||||
|
||||
# Extracts credentials and organizes by type, then by host, and finally by individual
|
||||
# credential data. Will look something like:
|
||||
#
|
||||
# {"smb_hash" => {"host1:445" => [user1,user2,user3], "host2:445" => [user4,user5]}},
|
||||
# {"ssh_key" => {"host3:22" => [user10,user20]}},
|
||||
# {"text" => {"host4:23" => [user100,user101]}}
|
||||
#
|
||||
# This hash of hashes of arrays is, in turn, consumed by gen_export_pwdump.
|
||||
def extract_credentials
|
||||
creds = Hash.new
|
||||
creds["ssh_key"] = {}
|
||||
creds["smb_hash"] = {}
|
||||
creds["text"] = {}
|
||||
myworkspace.each_cred do |cred|
|
||||
next unless host_allowed?(cred.service.host.address)
|
||||
# Skip anything that's not associated with a specific host and port
|
||||
next unless (cred.service && cred.service.host && cred.service.host.address && cred.service.port)
|
||||
# TODO: Toggle active/all
|
||||
next unless cred.active
|
||||
svc = "%s:%d/%s (%s)" % [cred.service.host.address,cred.service.port,cred.service.proto,cred.service.name]
|
||||
case cred.ptype
|
||||
when /^password/
|
||||
ptype = "text"
|
||||
else
|
||||
ptype = cred.ptype
|
||||
end
|
||||
creds[ptype] ||= {}
|
||||
creds[ptype][svc] ||= []
|
||||
creds[ptype][svc] << cred
|
||||
end
|
||||
return creds
|
||||
end
|
||||
|
||||
|
||||
def to_xml_file(path, &block)
|
||||
|
||||
|
@ -205,7 +49,7 @@ class Export
|
|||
report_file = ::File.open(path, "wb")
|
||||
|
||||
report_file.write %Q|<?xml version="1.0" encoding="UTF-8"?>\n|
|
||||
report_file.write %Q|<MetasploitV4>\n|
|
||||
report_file.write %Q|<MetasploitV5>\n|
|
||||
report_file.write %Q|<generated time="#{Time.now.utc}" user="#{myusername}" project="#{myworkspace.name.gsub(/[^A-Za-z0-9\x20]/n,"_")}" product="framework"/>\n|
|
||||
|
||||
yield(:status, "start", "hosts") if block_given?
|
||||
|
@ -226,12 +70,6 @@ class Export
|
|||
extract_service_info(report_file)
|
||||
report_file.write %Q|</services>\n|
|
||||
|
||||
yield(:status, "start", "credentials") if block_given?
|
||||
report_file.write %Q|<credentials>\n|
|
||||
report_file.flush
|
||||
extract_credential_info(report_file)
|
||||
report_file.write %Q|</credentials>\n|
|
||||
|
||||
yield(:status, "start", "web sites") if block_given?
|
||||
report_file.write %Q|<web_sites>\n|
|
||||
report_file.flush
|
||||
|
@ -263,7 +101,7 @@ class Export
|
|||
report_file.write %Q|</module_details>\n|
|
||||
|
||||
|
||||
report_file.write %Q|</MetasploitV4>\n|
|
||||
report_file.write %Q|</MetasploitV5>\n|
|
||||
report_file.flush
|
||||
report_file.close
|
||||
|
||||
|
@ -277,7 +115,6 @@ class Export
|
|||
extract_host_entries
|
||||
extract_event_entries
|
||||
extract_service_entries
|
||||
extract_credential_entries
|
||||
extract_note_entries
|
||||
extract_vuln_entries
|
||||
extract_web_entries
|
||||
|
@ -306,8 +143,7 @@ class Export
|
|||
|
||||
# Extracts all credentials from a project, storing them in @creds
|
||||
def extract_credential_entries
|
||||
@creds = []
|
||||
myworkspace.each_cred {|cred| @creds << cred}
|
||||
@creds = Metasploit::Credential::Core.with_logins.with_public.with_private.workspace_id(myworkspace.id)
|
||||
end
|
||||
|
||||
# Extracts all notes from a project, storing them in @notes
|
||||
|
@ -346,14 +182,18 @@ class Export
|
|||
end
|
||||
end
|
||||
|
||||
def create_xml_element(key,value)
|
||||
def create_xml_element(key,value,skip_encoding=false)
|
||||
tag = key.gsub("_","-")
|
||||
el = REXML::Element.new(tag)
|
||||
if value
|
||||
data = marshalize(value)
|
||||
data.force_encoding(Encoding::BINARY) if data.respond_to?('force_encoding')
|
||||
data.gsub!(/([\x00-\x08\x0b\x0c\x0e-\x1f\x80-\xFF])/n){ |x| "\\x%.2x" % x.unpack("C*")[0] }
|
||||
el << REXML::Text.new(data)
|
||||
unless skip_encoding
|
||||
data = marshalize(value)
|
||||
data.force_encoding(Encoding::BINARY) if data.respond_to?('force_encoding')
|
||||
data.gsub!(/([\x00-\x08\x0b\x0c\x0e-\x1f\x80-\xFF])/n){ |x| "\\x%.2x" % x.unpack("C*")[0] }
|
||||
el << REXML::Text.new(data)
|
||||
else
|
||||
el << value
|
||||
end
|
||||
end
|
||||
return el
|
||||
end
|
||||
|
@ -572,22 +412,6 @@ class Export
|
|||
end
|
||||
report_file.write(" </vulns>\n")
|
||||
|
||||
# Credential sub-elements
|
||||
report_file.write(" <creds>\n")
|
||||
@creds.each do |cred|
|
||||
next unless cred.service.host.id == host_id
|
||||
report_file.write(" <cred>\n")
|
||||
report_file.write(" #{create_xml_element("port",cred.service.port)}\n")
|
||||
report_file.write(" #{create_xml_element("sname",cred.service.name)}\n")
|
||||
cred.attributes.each_pair do |k,v|
|
||||
next if k.strip =~ /id$/
|
||||
el = create_xml_element(k,v)
|
||||
report_file.write(" #{el}\n")
|
||||
end
|
||||
report_file.write(" </cred>\n")
|
||||
end
|
||||
report_file.write(" </creds>\n")
|
||||
|
||||
report_file.write(" </host>\n")
|
||||
end
|
||||
report_file.flush
|
||||
|
@ -621,19 +445,6 @@ class Export
|
|||
report_file.flush
|
||||
end
|
||||
|
||||
# Extract credential data from @creds
|
||||
def extract_credential_info(report_file)
|
||||
@creds.each do |c|
|
||||
report_file.write(" <credential>\n")
|
||||
c.attributes.each_pair do |k,v|
|
||||
cr = create_xml_element(k,v)
|
||||
report_file.write(" #{cr}\n")
|
||||
end
|
||||
report_file.write(" </credential>\n")
|
||||
report_file.write("\n")
|
||||
end
|
||||
report_file.flush
|
||||
end
|
||||
|
||||
# Extract service data from @services
|
||||
def extract_service_info(report_file)
|
||||
|
|
|
@ -22,6 +22,13 @@ class DBManager
|
|||
include Msf::DBManager::Migration
|
||||
include Msf::Framework::Offspring
|
||||
|
||||
#
|
||||
# CONSTANTS
|
||||
#
|
||||
|
||||
# The adapter to use to establish database connection.
|
||||
ADAPTER = 'postgresql'
|
||||
|
||||
# Mainly, it's Ruby 1.9.1 that cause a lot of problems now, along with Ruby 1.8.6.
|
||||
# Ruby 1.8.7 actually seems okay, but why tempt fate? Let's say 1.9.3 and beyond.
|
||||
def warn_about_rubies
|
||||
|
@ -34,16 +41,19 @@ class DBManager
|
|||
|
||||
# Returns true if we are ready to load/store data
|
||||
def active
|
||||
return false if not @usable
|
||||
# We have established a connection, some connection is active, and we have run migrations
|
||||
(ActiveRecord::Base.connected? && ActiveRecord::Base.connection_pool.connected? && migrated)# rescue false
|
||||
# usable and migrated a just Boolean attributes, so check those first because they don't actually contact the
|
||||
# database.
|
||||
usable && migrated && connection_established?
|
||||
end
|
||||
|
||||
# Returns true if the prerequisites have been installed
|
||||
attr_accessor :usable
|
||||
|
||||
# Returns the list of usable database drivers
|
||||
attr_accessor :drivers
|
||||
def drivers
|
||||
@drivers ||= []
|
||||
end
|
||||
attr_writer :drivers
|
||||
|
||||
# Returns the active driver
|
||||
attr_accessor :driver
|
||||
|
@ -86,9 +96,7 @@ class DBManager
|
|||
# Database drivers can reset our KCODE, do not let them
|
||||
$KCODE = 'NONE' if RUBY_VERSION =~ /^1\.8\./
|
||||
|
||||
require "active_record"
|
||||
|
||||
initialize_metasploit_data_models
|
||||
add_rails_engine_migration_paths
|
||||
|
||||
@usable = true
|
||||
|
||||
|
@ -98,22 +106,10 @@ class DBManager
|
|||
return false
|
||||
end
|
||||
|
||||
# Only include Mdm if we're not using Metasploit commercial versions
|
||||
# If Mdm::Host is defined, the dynamically created classes
|
||||
# are already in the object space
|
||||
begin
|
||||
unless defined? Mdm::Host
|
||||
MetasploitDataModels.require_models
|
||||
end
|
||||
rescue NameError => e
|
||||
warn_about_rubies
|
||||
raise e
|
||||
end
|
||||
|
||||
#
|
||||
# Determine what drivers are available
|
||||
#
|
||||
initialize_drivers
|
||||
initialize_adapter
|
||||
|
||||
#
|
||||
# Instantiate the database sink
|
||||
|
@ -123,53 +119,69 @@ class DBManager
|
|||
true
|
||||
end
|
||||
|
||||
# Checks if the spec passed to `ActiveRecord::Base.establish_connection` can connect to the database.
|
||||
#
|
||||
# @return [true] if an active connection can be made to the database using the current config.
|
||||
# @return [false] if an active connection cannot be made to the database.
|
||||
def connection_established?
|
||||
begin
|
||||
# use with_connection so the connection doesn't stay pinned to the thread.
|
||||
ActiveRecord::Base.connection_pool.with_connection {
|
||||
ActiveRecord::Base.connection.active?
|
||||
}
|
||||
rescue ActiveRecord::ConnectionNotEstablished, PG::ConnectionBad => error
|
||||
elog("Connection not established: #{error.class} #{error}:\n#{error.backtrace.join("\n")}")
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Scan through available drivers
|
||||
#
|
||||
def initialize_drivers
|
||||
self.drivers = []
|
||||
tdrivers = %W{ postgresql }
|
||||
tdrivers.each do |driver|
|
||||
def initialize_adapter
|
||||
ActiveRecord::Base.default_timezone = :utc
|
||||
|
||||
if connection_established? && ActiveRecord::Base.connection_config[:adapter] == ADAPTER
|
||||
dlog("Already established connection to #{ADAPTER}, so reusing active connection.")
|
||||
self.drivers << ADAPTER
|
||||
self.driver = ADAPTER
|
||||
else
|
||||
begin
|
||||
ActiveRecord::Base.default_timezone = :utc
|
||||
ActiveRecord::Base.establish_connection(:adapter => driver)
|
||||
if(self.respond_to?("driver_check_#{driver}"))
|
||||
self.send("driver_check_#{driver}")
|
||||
end
|
||||
ActiveRecord::Base.establish_connection(adapter: ADAPTER)
|
||||
ActiveRecord::Base.remove_connection
|
||||
self.drivers << driver
|
||||
rescue ::Exception
|
||||
rescue Exception => error
|
||||
@adapter_error = error
|
||||
else
|
||||
self.drivers << ADAPTER
|
||||
self.driver = ADAPTER
|
||||
end
|
||||
end
|
||||
|
||||
if(not self.drivers.empty?)
|
||||
self.driver = self.drivers[0]
|
||||
end
|
||||
|
||||
# Database drivers can reset our KCODE, do not let them
|
||||
$KCODE = 'NONE' if RUBY_VERSION =~ /^1\.8\./
|
||||
end
|
||||
|
||||
# Loads Metasploit Data Models and adds its migrations to migrations paths.
|
||||
#
|
||||
# @return [void]
|
||||
def initialize_metasploit_data_models
|
||||
# Provide access to ActiveRecord models shared w/ commercial versions
|
||||
require "metasploit_data_models"
|
||||
def add_rails_engine_migration_paths
|
||||
unless defined? ActiveRecord
|
||||
fail "Bundle installed '--without #{Bundler.settings.without.join(' ')}'. To clear the without option do " \
|
||||
"`bundle install --without ''` (the --without flag with an empty string) or `rm -rf .bundle` to remove " \
|
||||
"the .bundle/config manually and then `bundle install`"
|
||||
end
|
||||
|
||||
metasploit_data_model_migrations_pathname = MetasploitDataModels.root.join(
|
||||
'db',
|
||||
'migrate'
|
||||
)
|
||||
metasploit_data_model_migrations_path = metasploit_data_model_migrations_pathname.to_s
|
||||
Rails.application.railties.engines.each do |engine|
|
||||
migrations_paths = engine.paths['db/migrate'].existent_directories
|
||||
|
||||
# Since ActiveRecord::Migrator.migrations_paths can persist between
|
||||
# instances of Msf::DBManager, such as in specs,
|
||||
# metasploit_data_models_migrations_path may already be part of
|
||||
# migrations_paths, in which case it should not be added or multiple
|
||||
# migrations with the same version number errors will occur.
|
||||
unless ActiveRecord::Migrator.migrations_paths.include? metasploit_data_model_migrations_path
|
||||
ActiveRecord::Migrator.migrations_paths << metasploit_data_model_migrations_path
|
||||
migrations_paths.each do |migrations_path|
|
||||
# Since ActiveRecord::Migrator.migrations_paths can persist between
|
||||
# instances of Msf::DBManager, such as in specs,
|
||||
# migrations_path may already be part of
|
||||
# migrations_paths, in which case it should not be added or multiple
|
||||
# migrations with the same version number errors will occur.
|
||||
unless ActiveRecord::Migrator.migrations_paths.include? migrations_path
|
||||
ActiveRecord::Migrator.migrations_paths << migrations_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -208,25 +220,22 @@ class DBManager
|
|||
|
||||
begin
|
||||
self.migrated = false
|
||||
create_db(nopts)
|
||||
|
||||
# Configure the database adapter
|
||||
ActiveRecord::Base.establish_connection(nopts)
|
||||
# Check ActiveRecord::Base was already connected by Rails::Application.initialize! or some other API.
|
||||
unless connection_established?
|
||||
create_db(nopts)
|
||||
|
||||
# Migrate the database, if needed
|
||||
migrate
|
||||
|
||||
# Set the default workspace
|
||||
framework.db.workspace = framework.db.default_workspace
|
||||
|
||||
# Flag that migration has completed
|
||||
self.migrated = true
|
||||
# Configure the database adapter
|
||||
ActiveRecord::Base.establish_connection(nopts)
|
||||
end
|
||||
rescue ::Exception => e
|
||||
self.error = e
|
||||
elog("DB.connect threw an exception: #{e}")
|
||||
dlog("Call stack: #{$@.join"\n"}", LEV_1)
|
||||
return false
|
||||
ensure
|
||||
after_establish_connection
|
||||
|
||||
# Database drivers can reset our KCODE, do not let them
|
||||
$KCODE = 'NONE' if RUBY_VERSION =~ /^1\.8\./
|
||||
end
|
||||
|
@ -234,6 +243,29 @@ class DBManager
|
|||
true
|
||||
end
|
||||
|
||||
# Finishes {#connect} after `ActiveRecord::Base.establish_connection` has succeeded by {#migrate migrating database}
|
||||
# and setting {#workspace}.
|
||||
#
|
||||
# @return [void]
|
||||
def after_establish_connection
|
||||
self.migrated = false
|
||||
|
||||
begin
|
||||
# Migrate the database, if needed
|
||||
migrate
|
||||
|
||||
# Set the default workspace
|
||||
framework.db.workspace = framework.db.default_workspace
|
||||
rescue ::Exception => exception
|
||||
self.error = exception
|
||||
elog("DB.connect threw an exception: #{exception}")
|
||||
dlog("Call stack: #{exception.backtrace.join("\n")}", LEV_1)
|
||||
else
|
||||
# Flag that migration has completed
|
||||
self.migrated = true
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Attempt to create the database
|
||||
#
|
||||
|
@ -259,7 +291,13 @@ class DBManager
|
|||
errstr = e.to_s
|
||||
if errstr =~ /does not exist/i or errstr =~ /Unknown database/
|
||||
ilog("Database doesn't exist \"#{opts['database']}\", attempting to create it.")
|
||||
ActiveRecord::Base.establish_connection(opts.merge('database' => nil))
|
||||
ActiveRecord::Base.establish_connection(
|
||||
opts.merge(
|
||||
'database' => 'postgres',
|
||||
'schema_search_path' => 'public'
|
||||
)
|
||||
)
|
||||
|
||||
ActiveRecord::Base.connection.create_database(opts['database'])
|
||||
else
|
||||
ilog("Trying to continue despite failed database creation: #{e}")
|
||||
|
|
|
@ -13,40 +13,40 @@ module Msf
|
|||
# Elements that can be treated as text (i.e. do not need to be
|
||||
# deserialized) in {#import_msf_web_page_element}
|
||||
MSF_WEB_PAGE_TEXT_ELEMENT_NAMES = [
|
||||
'auth',
|
||||
'body',
|
||||
'code',
|
||||
'cookie',
|
||||
'ctype',
|
||||
'location',
|
||||
'mtime'
|
||||
'auth',
|
||||
'body',
|
||||
'code',
|
||||
'cookie',
|
||||
'ctype',
|
||||
'location',
|
||||
'mtime'
|
||||
]
|
||||
|
||||
# Elements that can be treated as text (i.e. do not need to be
|
||||
# deserialized) in {#import_msf_web_element}.
|
||||
MSF_WEB_TEXT_ELEMENT_NAMES = [
|
||||
'created-at',
|
||||
'host',
|
||||
'path',
|
||||
'port',
|
||||
'query',
|
||||
'ssl',
|
||||
'updated-at',
|
||||
'vhost'
|
||||
'created-at',
|
||||
'host',
|
||||
'path',
|
||||
'port',
|
||||
'query',
|
||||
'ssl',
|
||||
'updated-at',
|
||||
'vhost'
|
||||
]
|
||||
|
||||
# Elements that can be treated as text (i.e. do not need to be
|
||||
# deserialized) in {#import_msf_web_vuln_element}.
|
||||
MSF_WEB_VULN_TEXT_ELEMENT_NAMES = [
|
||||
'blame',
|
||||
'category',
|
||||
'confidence',
|
||||
'description',
|
||||
'method',
|
||||
'name',
|
||||
'pname',
|
||||
'proof',
|
||||
'risk'
|
||||
'blame',
|
||||
'category',
|
||||
'confidence',
|
||||
'description',
|
||||
'method',
|
||||
'name',
|
||||
'pname',
|
||||
'proof',
|
||||
'risk'
|
||||
]
|
||||
|
||||
#
|
||||
|
@ -80,8 +80,8 @@ module Msf
|
|||
# FIXME https://www.pivotaltracker.com/story/show/46578647
|
||||
# FIXME https://www.pivotaltracker.com/story/show/47128407
|
||||
unserialized_params = unserialize_object(
|
||||
element.elements['params'],
|
||||
options[:allow_yaml]
|
||||
element.elements['params'],
|
||||
options[:allow_yaml]
|
||||
)
|
||||
info[:params] = nils_for_nulls(unserialized_params)
|
||||
|
||||
|
@ -127,8 +127,8 @@ module Msf
|
|||
# FIXME https://www.pivotaltracker.com/story/show/46578647
|
||||
# FIXME https://www.pivotaltracker.com/story/show/47128407
|
||||
unserialized_headers = unserialize_object(
|
||||
element.elements['headers'],
|
||||
options[:allow_yaml]
|
||||
element.elements['headers'],
|
||||
options[:allow_yaml]
|
||||
)
|
||||
info[:headers] = nils_for_nulls(unserialized_headers)
|
||||
|
||||
|
@ -174,8 +174,8 @@ module Msf
|
|||
# FIXME https://www.pivotaltracker.com/story/show/46578647
|
||||
# FIXME https://www.pivotaltracker.com/story/show/47128407
|
||||
unserialized_params = unserialize_object(
|
||||
element.elements['params'],
|
||||
options[:allow_yaml]
|
||||
element.elements['params'],
|
||||
options[:allow_yaml]
|
||||
)
|
||||
info[:params] = nils_for_nulls(unserialized_params)
|
||||
|
||||
|
@ -347,34 +347,40 @@ module Msf
|
|||
end
|
||||
end
|
||||
|
||||
host.elements.each('creds/cred') do |cred|
|
||||
cred_data = {}
|
||||
cred_data[:workspace] = wspace
|
||||
cred_data[:host] = hobj
|
||||
%W{port ptype sname proto proof active user pass}.each {|datum|
|
||||
if cred.elements[datum].respond_to? :text
|
||||
cred_data[datum.intern] = nils_for_nulls(cred.elements[datum].text.to_s.strip)
|
||||
## Handle old-style (pre 4.10) XML files
|
||||
if btag == "MetasploitV4"
|
||||
if host.elements['creds'].present?
|
||||
unless host.elements['creds'].elements.empty?
|
||||
origin = Metasploit::Credential::Origin::Import.create(filename: "console-import-#{Time.now.to_i}")
|
||||
|
||||
host.elements.each('creds/cred') do |cred|
|
||||
username = cred.elements['user'].try(:text)
|
||||
proto = cred.elements['proto'].try(:text)
|
||||
sname = cred.elements['sname'].try(:text)
|
||||
port = cred.elements['port'].try(:text)
|
||||
|
||||
# Handle blanks by resetting to sane default values
|
||||
proto = "tcp" if proto.blank?
|
||||
pass = cred.elements['pass'].try(:text)
|
||||
pass = "" if pass == "*MASKED*"
|
||||
|
||||
private = create_credential_private(private_data: pass, private_type: :password)
|
||||
public = create_credential_public(username: username)
|
||||
core = create_credential_core(private: private, public: public, origin: origin, workspace_id: wspace.id)
|
||||
|
||||
create_credential_login(core: core,
|
||||
workspace_id: wspace.id,
|
||||
address: hobj.address,
|
||||
port: port,
|
||||
protocol: proto,
|
||||
service_name: sname,
|
||||
status: Metasploit::Model::Login::Status::UNTRIED)
|
||||
end
|
||||
end
|
||||
}
|
||||
%W{created-at updated-at}.each { |datum|
|
||||
if cred.elements[datum].respond_to? :text
|
||||
cred_data[datum.gsub("-","_")] = nils_for_nulls(cred.elements[datum].text.to_s.strip)
|
||||
end
|
||||
}
|
||||
%W{source-type source-id}.each { |datum|
|
||||
if cred.elements[datum].respond_to? :text
|
||||
cred_data[datum.gsub("-","_").intern] = nils_for_nulls(cred.elements[datum].text.to_s.strip)
|
||||
end
|
||||
}
|
||||
if cred_data[:pass] == "*MASKED*"
|
||||
cred_data[:pass] = ""
|
||||
cred_data[:active] = false
|
||||
elsif cred_data[:pass] == "*BLANK PASSWORD*"
|
||||
cred_data[:pass] = ""
|
||||
end
|
||||
report_cred(cred_data)
|
||||
end
|
||||
|
||||
|
||||
host.elements.each('sessions/session') do |sess|
|
||||
sess_id = nils_for_nulls(sess.elements["id"].text.to_s.strip.to_i)
|
||||
sess_data = {}
|
||||
|
@ -399,9 +405,9 @@ module Msf
|
|||
end
|
||||
|
||||
existing_session = get_session(
|
||||
:workspace => sess_data[:host].workspace,
|
||||
:addr => sess_data[:host].address,
|
||||
:time => sess_data[:opened_at]
|
||||
:workspace => sess_data[:host].workspace,
|
||||
:addr => sess_data[:host].address,
|
||||
:time => sess_data[:opened_at]
|
||||
)
|
||||
this_session = existing_session || report_session(sess_data)
|
||||
next if existing_session
|
||||
|
@ -423,6 +429,7 @@ module Msf
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
# Import web sites
|
||||
doc.elements.each("/#{btag}/web_sites/web_site") do |web|
|
||||
info = {}
|
||||
|
@ -450,11 +457,11 @@ module Msf
|
|||
%W{page form vuln}.each do |wtype|
|
||||
doc.elements.each("/#{btag}/web_#{wtype}s/web_#{wtype}") do |element|
|
||||
send(
|
||||
"import_msf_web_#{wtype}_element",
|
||||
element,
|
||||
:allow_yaml => allow_yaml,
|
||||
:workspace => wspace,
|
||||
&block
|
||||
"import_msf_web_#{wtype}_element",
|
||||
element,
|
||||
:allow_yaml => allow_yaml,
|
||||
:workspace => wspace,
|
||||
&block
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -474,9 +481,9 @@ module Msf
|
|||
# @raise [Msf::DBImportError] if unsupported format
|
||||
def check_msf_xml_version!(document)
|
||||
metadata = {
|
||||
# FIXME https://www.pivotaltracker.com/story/show/47128407
|
||||
:allow_yaml => false,
|
||||
:root_tag => nil
|
||||
# FIXME https://www.pivotaltracker.com/story/show/47128407
|
||||
:allow_yaml => false,
|
||||
:root_tag => nil
|
||||
}
|
||||
|
||||
if document.elements['MetasploitExpressV1']
|
||||
|
@ -493,6 +500,8 @@ module Msf
|
|||
metadata[:root_tag] = 'MetasploitExpressV4'
|
||||
elsif document.elements['MetasploitV4']
|
||||
metadata[:root_tag] = 'MetasploitV4'
|
||||
elsif document.elements['MetasploitV5']
|
||||
metadata[:root_tag] = 'MetasploitV5'
|
||||
end
|
||||
|
||||
unless metadata[:root_tag]
|
||||
|
@ -580,3 +589,4 @@ module Msf
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
require 'rex/exploitation/cmdstager'
|
||||
require 'msf/core/exploit/exe'
|
||||
require 'msf/base/config'
|
||||
|
||||
module Msf
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# -*- coding: binary -*-
|
||||
require 'metasploit/framework/version'
|
||||
require 'msf/core'
|
||||
require 'msf/util'
|
||||
|
||||
|
@ -16,10 +17,10 @@ class Framework
|
|||
# Versioning information
|
||||
#
|
||||
|
||||
Major = 4
|
||||
Minor = 9
|
||||
Point = 3
|
||||
Release = "-dev"
|
||||
Major = Metasploit::Framework::Version::MAJOR
|
||||
Minor = Metasploit::Framework::Version::MINOR
|
||||
Point = Metasploit::Framework::Version::PATCH
|
||||
Release = "-#{Metasploit::Framework::Version::PRERELEASE}"
|
||||
|
||||
if(Point)
|
||||
Version = "#{Major}.#{Minor}.#{Point}#{Release}"
|
||||
|
@ -41,14 +42,6 @@ class Framework
|
|||
# EICAR canary
|
||||
EICARCorrupted = ::Msf::Util::EXE.is_eicar_corrupted?
|
||||
|
||||
# API Version
|
||||
APIMajor = 1
|
||||
APIMinor = 0
|
||||
|
||||
# Base/API Version
|
||||
VersionCore = Major + (Minor / 10.0)
|
||||
VersionAPI = APIMajor + (APIMinor / 10.0)
|
||||
|
||||
#
|
||||
# Mixin meant to be included into all classes that can have instances that
|
||||
# should be tied to the framework, such as modules.
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
require 'metasploit/framework/api/version'
|
||||
require 'metasploit/framework/core/version'
|
||||
|
||||
# Concern for behavior that all namespace modules that wrap Msf::Modules must support like version checking and
|
||||
# grabbing the version specific-Metasploit* class.
|
||||
module Msf::Modules::Namespace
|
||||
|
@ -54,11 +57,11 @@ module Msf::Modules::Namespace
|
|||
def version_compatible!(module_path, module_reference_name)
|
||||
if const_defined?(:RequiredVersions)
|
||||
required_versions = const_get(:RequiredVersions)
|
||||
minimum_core_version = required_versions[0]
|
||||
minimum_api_version = required_versions[1]
|
||||
minimum_core_version = Gem::Version.new(required_versions[0].to_s)
|
||||
minimum_api_version = Gem::Version.new(required_versions[1].to_s)
|
||||
|
||||
if (minimum_core_version > ::Msf::Framework::VersionCore or
|
||||
minimum_api_version > ::Msf::Framework::VersionAPI)
|
||||
if (minimum_core_version > Metasploit::Framework::Core::GEM_VERSION ||
|
||||
minimum_api_version > Metasploit::Framework::API::GEM_VERSION)
|
||||
raise Msf::Modules::VersionCompatibilityError.new(
|
||||
:module_path => module_path,
|
||||
:module_reference_name => module_reference_name,
|
||||
|
|
|
@ -46,4 +46,17 @@ class Msf::Post < Msf::Module
|
|||
|
||||
mod
|
||||
end
|
||||
|
||||
# This method returns the ID of the {Mdm::Session} that the post module
|
||||
# is currently running agaist.
|
||||
#
|
||||
# @return [NilClass] if there is no database record for the session
|
||||
# @return [Fixnum] if there is a database record to get the id for
|
||||
def session_db_id
|
||||
if session.db_record
|
||||
session.db_record.id
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,9 @@ module RPC
|
|||
class RPC_Db < RPC_Base
|
||||
|
||||
private
|
||||
|
||||
include Metasploit::Credential::Creation
|
||||
|
||||
def db
|
||||
self.framework.db.active
|
||||
end
|
||||
|
@ -15,6 +18,21 @@ private
|
|||
self.framework.db.workspace
|
||||
end
|
||||
|
||||
def fix_cred_options(opts)
|
||||
new_opts = fix_options(opts)
|
||||
|
||||
# Convert some of are data back to symbols
|
||||
if new_opts[:origin_type]
|
||||
new_opts[:origin_type] = new_opts[:origin_type].to_sym
|
||||
end
|
||||
|
||||
if new_opts[:private_type]
|
||||
new_opts[:private_type] = new_opts[:private_type].to_sym
|
||||
end
|
||||
|
||||
new_opts
|
||||
end
|
||||
|
||||
def fix_options(opts)
|
||||
newopts = {}
|
||||
opts.each do |k,v|
|
||||
|
@ -88,6 +106,40 @@ private
|
|||
|
||||
public
|
||||
|
||||
def rpc_create_cracked_credential(xopts)
|
||||
opts = fix_cred_options(xopts)
|
||||
create_credential(opts)
|
||||
end
|
||||
|
||||
def rpc_create_credential(xopts)
|
||||
opts = fix_cred_options(xopts)
|
||||
core = create_credential(opts)
|
||||
|
||||
ret = {
|
||||
username: core.public.try(:username),
|
||||
private: core.private.try(:data),
|
||||
private_type: core.private.try(:type),
|
||||
realm_value: core.realm.try(:value),
|
||||
realm_key: core.realm.try(:key)
|
||||
}
|
||||
|
||||
if opts[:last_attempted_at] && opts[:status]
|
||||
opts[:core] = core
|
||||
opts[:last_attempted_at] = opts[:last_attempted_at].to_datetime
|
||||
login = create_credential_login(opts)
|
||||
|
||||
ret[:host] = login.service.host.address,
|
||||
ret[:sname] = login.service.name
|
||||
ret[:status] = login.status
|
||||
end
|
||||
ret
|
||||
end
|
||||
|
||||
def rpc_invalidate_login(xopts)
|
||||
opts = fix_cred_options(xopts)
|
||||
invalidate_login(opts)
|
||||
end
|
||||
|
||||
def rpc_hosts(xopts)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
opts, wspace = init_db_opts_workspace(xopts)
|
||||
|
@ -490,33 +542,6 @@ public
|
|||
}
|
||||
end
|
||||
|
||||
def rpc_report_auth_info(xopts)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
opts, wspace = init_db_opts_workspace(xopts)
|
||||
res = self.framework.db.report_auth_info(opts)
|
||||
return { :result => 'success' } if(res)
|
||||
{ :result => 'failed' }
|
||||
}
|
||||
end
|
||||
|
||||
def rpc_get_auth_info(xopts)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
opts, wspace = init_db_opts_workspace(xopts)
|
||||
ret = {}
|
||||
ret[:auth_info] = []
|
||||
# XXX: This method doesn't exist...
|
||||
ai = self.framework.db.get_auth_info(opts)
|
||||
ai.each do |i|
|
||||
info = {}
|
||||
i.each do |k,v|
|
||||
info[k.to_sym] = v
|
||||
end
|
||||
ret[:auth_info] << info
|
||||
end
|
||||
ret
|
||||
}
|
||||
end
|
||||
|
||||
def rpc_get_ref(name)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
db_check
|
||||
|
@ -828,42 +853,6 @@ public
|
|||
}
|
||||
end
|
||||
|
||||
# requires host, port, user, pass, ptype, and active
|
||||
def rpc_report_cred(xopts)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
opts, wspace = init_db_opts_workspace(xopts)
|
||||
res = framework.db.find_or_create_cred(opts)
|
||||
return { :result => 'success' } if res
|
||||
{ :result => 'failed' }
|
||||
}
|
||||
end
|
||||
|
||||
#right now workspace is the only option supported
|
||||
def rpc_creds(xopts)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
opts, wspace = init_db_opts_workspace(xopts)
|
||||
limit = opts.delete(:limit) || 100
|
||||
offset = opts.delete(:offset) || 0
|
||||
|
||||
ret = {}
|
||||
ret[:creds] = []
|
||||
::Mdm::Cred.find(:all, :include => {:service => :host}, :conditions => ["hosts.workspace_id = ?",
|
||||
framework.db.workspace.id ], :limit => limit, :offset => offset).each do |c|
|
||||
cred = {}
|
||||
cred[:host] = c.service.host.address if(c.service.host)
|
||||
cred[:updated_at] = c.updated_at.to_i
|
||||
cred[:port] = c.service.port
|
||||
cred[:proto] = c.service.proto
|
||||
cred[:sname] = c.service.name
|
||||
cred[:type] = c.ptype
|
||||
cred[:user] = c.user
|
||||
cred[:pass] = c.pass
|
||||
cred[:active] = c.active
|
||||
ret[:creds] << cred
|
||||
end
|
||||
ret
|
||||
}
|
||||
end
|
||||
|
||||
def rpc_import_data(xopts)
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
|
@ -1046,7 +1035,7 @@ public
|
|||
end
|
||||
|
||||
cdb = ""
|
||||
if ::ActiveRecord::Base.connected?
|
||||
if framework.db.connection_established?
|
||||
::ActiveRecord::Base.connection_pool.with_connection { |conn|
|
||||
if conn.respond_to? :current_database
|
||||
cdb = conn.current_database
|
||||
|
|
|
@ -99,7 +99,12 @@ class ThreadManager < Array
|
|||
begin
|
||||
argv.shift.call(*argv)
|
||||
rescue ::Exception => e
|
||||
elog("thread exception: #{::Thread.current[:tm_name]} critical=#{::Thread.current[:tm_crit]} error:#{e.class} #{e} source:#{::Thread.current[:tm_call].inspect}")
|
||||
elog(
|
||||
"thread exception: #{::Thread.current[:tm_name]} critical=#{::Thread.current[:tm_crit]} " \
|
||||
"error: #{e.class} #{e}\n" \
|
||||
" source:\n" \
|
||||
" #{::Thread.current[:tm_call].join "\n "}"
|
||||
)
|
||||
elog("Call Stack\n#{e.backtrace.join("\n")}")
|
||||
raise e
|
||||
ensure
|
||||
|
|
|
@ -33,18 +33,6 @@ if (RUBY_VERSION =~ /^1\.9\.1/)
|
|||
$stderr.puts "*** Ruby 1.9.1 is not supported, please upgrade to Ruby 1.9.3 or newer."
|
||||
end
|
||||
|
||||
if(RUBY_VERSION =~ /^(1\.9|2\.0)\./)
|
||||
# Load rubygems before changing default_internal, otherwise we may get
|
||||
# Encoding::UndefinedConversionError as the gemspec files are loaded
|
||||
require 'rubygems'
|
||||
Gem::Version # trigger Rubygems to fully load
|
||||
|
||||
# Force binary encoding for Ruby versions that support it
|
||||
if(Object.const_defined?('Encoding') and Encoding.respond_to?('default_external='))
|
||||
Encoding.default_external = Encoding.default_internal = "binary"
|
||||
end
|
||||
end
|
||||
|
||||
if(RUBY_PLATFORM == 'java')
|
||||
require 'socket'
|
||||
s = Socket.new(::Socket::AF_INET, ::Socket::SOCK_STREAM, ::Socket::IPPROTO_TCP)
|
||||
|
|
|
@ -408,7 +408,7 @@ class Core
|
|||
avdwarn = nil
|
||||
|
||||
banner_trailers = {
|
||||
:version => "%yelmetasploit v#{Msf::Framework::Version} [core:#{Msf::Framework::VersionCore} api:#{Msf::Framework::VersionAPI}]%clr",
|
||||
:version => "%yelmetasploit v#{Msf::Framework::Version} [core:#{Metasploit::Framework::Core::GEM_VERSION} api:#{Metasploit::Framework::API::GEM_VERSION}]%clr",
|
||||
:exp_aux_pos => "#{framework.stats.num_exploits} exploits - #{framework.stats.num_auxiliary} auxiliary - #{framework.stats.num_post} post",
|
||||
:pay_enc_nop => "#{framework.stats.num_payloads} payloads - #{framework.stats.num_encoders} encoders - #{framework.stats.num_nops} nops",
|
||||
:free_trial => "Free Metasploit Pro trial: http://r-7.co/trymsp",
|
||||
|
|
|
@ -18,6 +18,8 @@ class Db
|
|||
# TODO: Not thrilled about including this entire module for just store_local.
|
||||
include Msf::Auxiliary::Report
|
||||
|
||||
include Metasploit::Credential::Creation
|
||||
|
||||
#
|
||||
# The dispatcher's name.
|
||||
#
|
||||
|
@ -217,7 +219,6 @@ class Db
|
|||
return unless active?
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
onlyup = false
|
||||
host_search = nil
|
||||
set_rhosts = false
|
||||
mode = :search
|
||||
delete_count = 0
|
||||
|
@ -580,7 +581,6 @@ class Db
|
|||
return
|
||||
end
|
||||
|
||||
mode = :search
|
||||
while (arg = args.shift)
|
||||
case arg
|
||||
#when "-a","--add"
|
||||
|
@ -648,73 +648,125 @@ class Db
|
|||
end
|
||||
|
||||
def cmd_creds_help
|
||||
print_line "Usage: creds [addr range]"
|
||||
print_line "Usage: creds -a <addr range> -p <port> -t <type> -u <user> -P <pass>"
|
||||
print_line
|
||||
print_line " -a,--add Add creds to the given addresses instead of listing"
|
||||
print_line " -d,--delete Delete the creds instead of searching"
|
||||
print_line " -h,--help Show this help information"
|
||||
print_line " -o <file> Send output to a file in csv format"
|
||||
print_line " -p,--port <portspec> List creds matching this port spec"
|
||||
print_line " -s <svc names> List creds matching these service names"
|
||||
print_line " -t,--type <type> Add a cred of this type (only with -a). Default: password"
|
||||
print_line " -u,--user Add a cred for this user (only with -a). Default: blank"
|
||||
print_line " -P,--password Add a cred with this password (only with -a). Default: blank"
|
||||
print_line " -R,--rhosts Set RHOSTS from the results of the search"
|
||||
print_line " -S,--search Search string to filter by"
|
||||
print_line " -c,--columns Columns of interest"
|
||||
print_line "With no sub-command, list credentials. If an address range is"
|
||||
print_line "given, show only credentials with logins on hosts within that"
|
||||
print_line "range."
|
||||
|
||||
print_line
|
||||
print_line "Examples:"
|
||||
print_line " creds # Default, returns all active credentials"
|
||||
print_line " creds all # Returns all credentials active or not"
|
||||
print_line "Usage - Listing credentials:"
|
||||
print_line " creds [filter options] [address range]"
|
||||
print_line
|
||||
print_line "Usage - Adding credentials:"
|
||||
print_line " creds add-ntlm <user> <ntlm hash> [domain]"
|
||||
print_line " creds add-password <user> <password> [realm] [realm-type]"
|
||||
print_line " creds add-ssh-key <user> </path/to/id_rsa> [realm-type]"
|
||||
print_line "Where [realm type] can be one of:"
|
||||
Metasploit::Model::Realm::Key::SHORT_NAMES.each do |short, description|
|
||||
print_line " #{short} - #{description}"
|
||||
end
|
||||
|
||||
print_line
|
||||
print_line "General options"
|
||||
print_line " -h,--help Show this help information"
|
||||
print_line
|
||||
print_line "Filter options for listing"
|
||||
print_line " -P,--password <regex> List passwords that match this regex"
|
||||
print_line " -p,--port <portspec> List creds with logins on services matching this port spec"
|
||||
print_line " -s <svc names> List creds matching comma-separated service names"
|
||||
print_line " -u,--user <regex> List users that match this regex"
|
||||
|
||||
print_line
|
||||
print_line "Examples, listing:"
|
||||
print_line " creds # Default, returns all credentials"
|
||||
print_line " creds 1.2.3.4/24 # nmap host specification"
|
||||
print_line " creds -p 22-25,445 # nmap port specification"
|
||||
print_line " creds 10.1.*.* -s ssh,smb all"
|
||||
print_line " creds -s ssh,smb # All creds associated with a login on SSH or SMB services"
|
||||
print_line
|
||||
|
||||
print_line
|
||||
print_line "Examples, adding:"
|
||||
print_line " # Add a user with an NTLMHash"
|
||||
print_line " creds add-ntlm alice 5cfe4c82d9ab8c66590f5b47cd6690f1:978a2e2e1dec9804c6b936f254727f9a"
|
||||
print_line " # Add a user with a blank password and a domain"
|
||||
print_line " creds add-password bob '' contosso"
|
||||
print_line " # Add a user with an SSH key"
|
||||
print_line " creds add-ssh-key root /root/.ssh/id_rsa"
|
||||
print_line
|
||||
end
|
||||
|
||||
#
|
||||
# Can return return active or all, on a certain host or range, on a
|
||||
# certain port or range, and/or on a service name.
|
||||
#
|
||||
def cmd_creds(*args)
|
||||
return unless active?
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
|
||||
search_param = nil
|
||||
inactive_ok = false
|
||||
type = "password"
|
||||
|
||||
set_rhosts = false
|
||||
output_file = nil
|
||||
|
||||
host_ranges = []
|
||||
port_ranges = []
|
||||
rhosts = []
|
||||
svcs = []
|
||||
delete_count = 0
|
||||
search_term = nil
|
||||
|
||||
cred_table_columns = [ 'host', 'port', 'user', 'pass', 'type', 'proof', 'active?' ]
|
||||
user = nil
|
||||
|
||||
# Short-circuit help
|
||||
if args.delete "-h"
|
||||
cmd_creds_help
|
||||
return
|
||||
# @param private_type [Symbol] See `Metasploit::Credential::Creation#create_credential`
|
||||
# @param username [String]
|
||||
# @param password [String]
|
||||
# @param realm [String]
|
||||
# @param realm_type [String] A key in `Metasploit::Model::Realm::Key::SHORT_NAMES`
|
||||
def creds_add(private_type, username, password=nil, realm=nil, realm_type=nil)
|
||||
cred_data = {
|
||||
username: username,
|
||||
private_data: password,
|
||||
private_type: private_type,
|
||||
workspace_id: framework.db.workspace,
|
||||
origin_type: :import,
|
||||
filename: "msfconsole"
|
||||
}
|
||||
if realm.present?
|
||||
if realm_type.present?
|
||||
realm_key = Metasploit::Model::Realm::Key::SHORT_NAMES[realm_type]
|
||||
if realm_key.nil?
|
||||
valid = Metasploit::Model::Realm::Key::SHORT_NAMES.keys.map{|n|"'#{n}'"}.join(", ")
|
||||
print_error("Invalid realm type: #{realm_type}. Valid values: #{valid}")
|
||||
return
|
||||
end
|
||||
end
|
||||
realm_key ||= Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
|
||||
cred_data.merge!(
|
||||
realm_value: realm,
|
||||
realm_key: realm_key
|
||||
)
|
||||
end
|
||||
|
||||
mode = :search
|
||||
begin
|
||||
create_credential(cred_data)
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
print_error("Failed to add #{private_type}: #{e}")
|
||||
end
|
||||
end
|
||||
|
||||
def creds_add_non_replayable_hash(*args)
|
||||
creds_add(:non_replayable_hash, *args)
|
||||
end
|
||||
|
||||
def creds_add_ntlm_hash(*args)
|
||||
creds_add(:ntlm_hash, *args)
|
||||
end
|
||||
|
||||
def creds_add_password(*args)
|
||||
creds_add(:password, *args)
|
||||
end
|
||||
|
||||
def creds_add_ssh_key(username, *args)
|
||||
key_file, realm = args
|
||||
begin
|
||||
key_data = File.read(key_file)
|
||||
rescue ::Errno::EACCES, ::Errno::ENOENT => e
|
||||
print_error("Failed to add ssh key: #{e}")
|
||||
else
|
||||
creds_add(:ssh_key, username, key_data, realm)
|
||||
end
|
||||
end
|
||||
|
||||
def creds_search(*args)
|
||||
host_ranges = []
|
||||
port_ranges = []
|
||||
svcs = []
|
||||
|
||||
#cred_table_columns = [ 'host', 'port', 'user', 'pass', 'type', 'proof', 'active?' ]
|
||||
cred_table_columns = [ 'host', 'service', 'public', 'private', 'realm', 'private_type' ]
|
||||
user = nil
|
||||
delete_count = 0
|
||||
|
||||
while (arg = args.shift)
|
||||
case arg
|
||||
when "-a","--add"
|
||||
mode = :add
|
||||
when "-d"
|
||||
mode = :delete
|
||||
when "-h"
|
||||
cmd_creds_help
|
||||
return
|
||||
when '-o'
|
||||
output_file = args.shift
|
||||
if (!output_file)
|
||||
|
@ -745,32 +797,14 @@ class Db
|
|||
print_error("Argument required for -P")
|
||||
return
|
||||
end
|
||||
when "-R"
|
||||
set_rhosts = true
|
||||
when '-S', '--search'
|
||||
search_term = /#{args.shift}/nmi
|
||||
when "-u","--user"
|
||||
user = args.shift
|
||||
if (!user)
|
||||
print_error("Argument required for -u")
|
||||
return
|
||||
end
|
||||
when '-c','--columns'
|
||||
columns = args.shift
|
||||
unless columns
|
||||
print_error("Argument required for -c, you may use any of #{cred_table_columns.join(',')}")
|
||||
return
|
||||
end
|
||||
cred_table_columns = columns.split(/[\s]*,[\s]*/).select do |col|
|
||||
cred_table_columns.include?(col)
|
||||
end
|
||||
if cred_table_columns.empty?
|
||||
print_error("Argument -c requires valid columns")
|
||||
return
|
||||
end
|
||||
when "all"
|
||||
# The user wants inactive passwords, too
|
||||
inactive_ok = true
|
||||
when "-d"
|
||||
mode = :delete
|
||||
else
|
||||
# Anything that wasn't an option is a host to search for
|
||||
unless (arg_host_range(arg, host_ranges))
|
||||
|
@ -779,33 +813,13 @@ class Db
|
|||
end
|
||||
end
|
||||
|
||||
if mode == :add
|
||||
if port_ranges.length != 1 or port_ranges.first.length != 1
|
||||
print_error("Exactly one port required")
|
||||
return
|
||||
end
|
||||
port = port_ranges.first.first
|
||||
host_ranges.each do |range|
|
||||
range.each do |host|
|
||||
cred = framework.db.find_or_create_cred(
|
||||
:host => host,
|
||||
:port => port,
|
||||
:user => (user == "NULL" ? nil : user),
|
||||
:pass => (pass == "NULL" ? nil : pass),
|
||||
:type => ptype,
|
||||
:sname => service,
|
||||
:active => true
|
||||
)
|
||||
print_status("Time: #{cred.updated_at} Credential: host=#{cred.service.host.address} port=#{cred.service.port} proto=#{cred.service.proto} sname=#{cred.service.name} type=#{cred.ptype} user=#{cred.user} pass=#{cred.pass} active=#{cred.active}")
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
# If we get here, we're searching. Delete implies search
|
||||
if user
|
||||
user_regex = Regexp.compile(user)
|
||||
end
|
||||
if pass
|
||||
pass_regex = Regexp.compile(pass)
|
||||
end
|
||||
|
||||
# normalize
|
||||
ports = port_ranges.flatten.uniq
|
||||
|
@ -815,91 +829,130 @@ class Db
|
|||
'Columns' => cred_table_columns
|
||||
}
|
||||
|
||||
tbl_opts.merge!(
|
||||
'ColProps' => {
|
||||
'pass' => { 'MaxChar' => 64 },
|
||||
'proof' => { 'MaxChar' => 56 }
|
||||
}
|
||||
) if search_term.nil?
|
||||
tbl = Rex::Ui::Text::Table.new(tbl_opts)
|
||||
|
||||
creds_returned = 0
|
||||
inactive_count = 0
|
||||
# Now do the actual search
|
||||
framework.db.each_cred(framework.db.workspace) do |cred|
|
||||
# skip if it's inactive and user didn't ask for all
|
||||
if !cred.active && !inactive_ok
|
||||
inactive_count += 1
|
||||
next
|
||||
end
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
query = Metasploit::Credential::Core.where(
|
||||
workspace_id: framework.db.workspace,
|
||||
)
|
||||
|
||||
if search_term
|
||||
next unless cred.attribute_names.any? { |a| cred[a.intern].to_s.match(search_term) }
|
||||
end
|
||||
# Also skip if the user is searching for something and this
|
||||
# one doesn't match
|
||||
includes = false
|
||||
host_ranges.map do |rw|
|
||||
includes = rw.include? cred.service.host.address
|
||||
break if includes
|
||||
end
|
||||
next unless host_ranges.empty? or includes
|
||||
query.each do |core|
|
||||
|
||||
# Same for ports
|
||||
next unless ports.empty? or ports.include? cred.service.port
|
||||
# Exclude creds that don't match the given user
|
||||
if user_regex.present? && !core.public.username.match(user_regex)
|
||||
next
|
||||
end
|
||||
|
||||
# Same for service names
|
||||
next unless svcs.empty? or svcs.include?(cred.service.name)
|
||||
# Exclude creds that don't match the given pass
|
||||
if pass_regex.present? && !core.private.data.match(pass_regex)
|
||||
next
|
||||
end
|
||||
|
||||
if user_regex
|
||||
next unless user_regex.match(cred.user)
|
||||
end
|
||||
if core.logins.empty?
|
||||
# Skip cores that don't have any logins if the user specified a
|
||||
# filter based on host, port, or service name
|
||||
next if host_ranges.any? || ports.any? || svcs.any?
|
||||
|
||||
row = cred_table_columns.map do |col|
|
||||
case col
|
||||
when 'host'
|
||||
cred.service.host.address
|
||||
when 'port'
|
||||
cred.service.port
|
||||
when 'type'
|
||||
cred.ptype
|
||||
tbl << [
|
||||
"", # host
|
||||
"", # port
|
||||
core.public,
|
||||
core.private,
|
||||
core.realm,
|
||||
core.private ? core.private.class.model_name.human : "",
|
||||
]
|
||||
else
|
||||
cred.send(col.intern)
|
||||
core.logins.each do |login|
|
||||
if svcs.present? && !svcs.include?(login.service.name)
|
||||
next
|
||||
end
|
||||
|
||||
if ports.present? && !ports.include?(login.service.port)
|
||||
next
|
||||
end
|
||||
|
||||
# If none of this Core's associated Logins is for a host within
|
||||
# the user-supplied RangeWalker, then we don't have any reason to
|
||||
# print it out. However, we treat the absence of ranges as meaning
|
||||
# all hosts.
|
||||
if host_ranges.present? && !host_ranges.any? { |range| range.include?(login.service.host.address) }
|
||||
next
|
||||
end
|
||||
row = [ login.service.host.address ]
|
||||
if login.service.name.present?
|
||||
row << "#{login.service.port}/#{login.service.proto} (#{login.service.name})"
|
||||
else
|
||||
row << "#{login.service.port}/#{login.service.proto}"
|
||||
end
|
||||
|
||||
row += [
|
||||
core.public,
|
||||
core.private,
|
||||
core.realm,
|
||||
core.private ? core.private.class.model_name.human : "",
|
||||
]
|
||||
tbl << row
|
||||
end
|
||||
end
|
||||
if mode == :delete
|
||||
core.destroy
|
||||
delete_count += 1
|
||||
end
|
||||
end
|
||||
|
||||
tbl << row
|
||||
if mode == :delete
|
||||
cred.destroy
|
||||
delete_count += 1
|
||||
end
|
||||
if set_rhosts
|
||||
addr = (cred.service.host.scope ? cred.service.host.address + '%' + cred.service.host.scope : cred.service.host.address )
|
||||
rhosts << addr
|
||||
end
|
||||
creds_returned += 1
|
||||
end
|
||||
|
||||
print_line
|
||||
if output_file.nil?
|
||||
print_line(tbl.to_s)
|
||||
if !inactive_ok && inactive_count > 0
|
||||
# Then we're not printing the inactive ones. Let the user know
|
||||
# that there are some they are not seeing and how to get at
|
||||
# them.
|
||||
print_line "Also found #{inactive_count} inactive creds (`creds all` to list them)"
|
||||
print_line
|
||||
end
|
||||
else
|
||||
# create the output file
|
||||
::File.open(output_file, "wb") { |f| f.write(tbl.to_csv) }
|
||||
print_status("Wrote services to #{output_file}")
|
||||
print_status("Deleted #{delete_count} creds") if delete_count > 0
|
||||
}
|
||||
end
|
||||
|
||||
#
|
||||
# Can return return active or all, on a certain host or range, on a
|
||||
# certain port or range, and/or on a service name.
|
||||
#
|
||||
def cmd_creds(*args)
|
||||
return unless active?
|
||||
|
||||
# Short-circuit help
|
||||
if args.delete "-h"
|
||||
cmd_creds_help
|
||||
return
|
||||
end
|
||||
|
||||
set_rhosts_from_addrs(rhosts.uniq) if set_rhosts
|
||||
subcommand = args.shift
|
||||
case subcommand
|
||||
when "add-ntlm"
|
||||
creds_add_ntlm_hash(*args)
|
||||
when "add-password"
|
||||
creds_add_password(*args)
|
||||
when "add-hash"
|
||||
creds_add_non_replayable_hash(*args)
|
||||
when "add-ssh-key"
|
||||
creds_add_ssh_key(*args)
|
||||
else
|
||||
# then it's not actually a subcommand
|
||||
args.unshift(subcommand) if subcommand
|
||||
creds_search(*args)
|
||||
end
|
||||
|
||||
print_status("Deleted #{delete_count} credentials") if delete_count > 0
|
||||
}
|
||||
end
|
||||
|
||||
def cmd_creds_tabs(str, words)
|
||||
case words.length
|
||||
when 1
|
||||
# subcommands
|
||||
tabs = [ 'add-ntlm', 'add-password', 'add-hash', 'add-ssh-key', ]
|
||||
when 2
|
||||
tabs = if words[1] == 'add-ssh-key'
|
||||
tab_complete_filenames(str, words)
|
||||
else
|
||||
[]
|
||||
end
|
||||
#when 5
|
||||
# tabs = Metasploit::Model::Realm::Key::SHORT_NAMES.keys
|
||||
else
|
||||
tabs = []
|
||||
end
|
||||
return tabs
|
||||
end
|
||||
|
||||
def cmd_notes_help
|
||||
|
@ -1139,8 +1192,8 @@ class Db
|
|||
info = args.shift
|
||||
if(!info)
|
||||
print_error("Can't make loot with no info")
|
||||
return
|
||||
end
|
||||
return
|
||||
end
|
||||
when '-t'
|
||||
typelist = args.shift
|
||||
if(!typelist)
|
||||
|
@ -1188,8 +1241,8 @@ class Db
|
|||
range.each do |host|
|
||||
file = File.open(filename, "rb")
|
||||
contents = file.read
|
||||
lootfile = framework.db.find_or_create_loot(:type => type, :host => host,:info => info, :data => contents,:path => filename,:name => name)
|
||||
print_status("Added loot #{host}")
|
||||
lootfile = framework.db.find_or_create_loot(:type => type, :host => host, :info => info, :data => contents, :path => filename, :name => name)
|
||||
print_status("Added loot for #{host} (#{lootfile})")
|
||||
end
|
||||
end
|
||||
return
|
||||
|
@ -1345,7 +1398,7 @@ class Db
|
|||
def cmd_db_import(*args)
|
||||
return unless active?
|
||||
::ActiveRecord::Base.connection_pool.with_connection {
|
||||
if (args.include?("-h") or not (args and args.length > 0))
|
||||
if args.include?("-h") || ! (args && args.length > 0)
|
||||
cmd_db_import_help
|
||||
return
|
||||
end
|
||||
|
@ -1408,8 +1461,8 @@ class Db
|
|||
next
|
||||
rescue REXML::ParseException => e
|
||||
print_error("Failed to import #{filename} due to malformed XML:")
|
||||
print_error("#{$!.class}: #{$!}")
|
||||
elog("Failed to import #{filename}: #{$!.class}: #{$!}")
|
||||
print_error("#{e.class}: #{e}")
|
||||
elog("Failed to import #{filename}: #{e.class}: #{e}")
|
||||
dlog("Call stack: #{$@.join("\n")}", LEV_3)
|
||||
next
|
||||
end
|
||||
|
@ -1577,7 +1630,8 @@ class Db
|
|||
#
|
||||
def cmd_db_status(*args)
|
||||
return if not db_check_driver
|
||||
if ::ActiveRecord::Base.connected?
|
||||
|
||||
if framework.db.connection_established?
|
||||
cdb = ""
|
||||
::ActiveRecord::Base.connection_pool.with_connection { |conn|
|
||||
if conn.respond_to? :current_database
|
||||
|
@ -1710,7 +1764,6 @@ class Db
|
|||
end
|
||||
|
||||
def db_find_tools(tools)
|
||||
found = true
|
||||
missed = []
|
||||
tools.each do |name|
|
||||
if(! Rex::FileUtils.find_full_path(name))
|
||||
|
|
|
@ -179,50 +179,60 @@ class Driver < Msf::Ui::Driver
|
|||
end
|
||||
end
|
||||
|
||||
# Look for our database configuration in the following places, in order:
|
||||
# command line arguments
|
||||
# environment variable
|
||||
# configuration directory (usually ~/.msf3)
|
||||
dbfile = opts['DatabaseYAML']
|
||||
dbfile ||= ENV["MSF_DATABASE_CONFIG"]
|
||||
dbfile ||= File.join(Msf::Config.get_config_root, "database.yml")
|
||||
if (dbfile and File.exists? dbfile)
|
||||
if File.readable?(dbfile)
|
||||
dbinfo = YAML.load(File.read(dbfile))
|
||||
dbenv = opts['DatabaseEnv'] || "production"
|
||||
db = dbinfo[dbenv]
|
||||
else
|
||||
print_error("Warning, #{dbfile} is not readable. Try running as root or chmod.")
|
||||
end
|
||||
if not db
|
||||
print_error("No database definition for environment #{dbenv}")
|
||||
else
|
||||
if not framework.db.connect(db)
|
||||
if framework.db.error.to_s =~ /RubyGem version.*pg.*0\.11/i
|
||||
print_error("***")
|
||||
print_error("*")
|
||||
print_error("* Metasploit now requires version 0.11 or higher of the 'pg' gem for database support")
|
||||
print_error("* There a three ways to accomplish this upgrade:")
|
||||
print_error("* 1. If you run Metasploit with your system ruby, simply upgrade the gem:")
|
||||
print_error("* $ rvmsudo gem install pg ")
|
||||
print_error("* 2. Use the Community Edition web interface to apply a Software Update")
|
||||
print_error("* 3. Uninstall, download the latest version, and reinstall Metasploit")
|
||||
print_error("*")
|
||||
print_error("***")
|
||||
print_error("")
|
||||
print_error("")
|
||||
end
|
||||
if framework.db.connection_established?
|
||||
framework.db.after_establish_connection
|
||||
else
|
||||
# Look for our database configuration in the following places, in order:
|
||||
# command line arguments
|
||||
# environment variable
|
||||
# configuration directory (usually ~/.msf3)
|
||||
dbfile = opts['DatabaseYAML']
|
||||
dbfile ||= ENV["MSF_DATABASE_CONFIG"]
|
||||
dbfile ||= File.join(Msf::Config.get_config_root, "database.yml")
|
||||
|
||||
print_error("Failed to connect to the database: #{framework.db.error}")
|
||||
if (dbfile and File.exists? dbfile)
|
||||
if File.readable?(dbfile)
|
||||
dbinfo = YAML.load(File.read(dbfile))
|
||||
dbenv = opts['DatabaseEnv'] || Rails.env
|
||||
db = dbinfo[dbenv]
|
||||
else
|
||||
self.framework.modules.refresh_cache_from_database
|
||||
print_error("Warning, #{dbfile} is not readable. Try running as root or chmod.")
|
||||
end
|
||||
|
||||
if self.framework.modules.cache_empty?
|
||||
print_status("The initial module cache will be built in the background, this can take 2-5 minutes...")
|
||||
end
|
||||
if not db
|
||||
print_error("No database definition for environment #{dbenv}")
|
||||
else
|
||||
framework.db.connect(db)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# framework.db.active will be true if after_establish_connection ran directly when connection_established? was
|
||||
# already true or if framework.db.connect called after_establish_connection.
|
||||
if framework.db.active
|
||||
self.framework.modules.refresh_cache_from_database
|
||||
|
||||
if self.framework.modules.cache_empty?
|
||||
print_status("The initial module cache will be built in the background, this can take 2-5 minutes...")
|
||||
end
|
||||
elsif !framework.db.error.nil?
|
||||
if framework.db.error.to_s =~ /RubyGem version.*pg.*0\.11/i
|
||||
print_error("***")
|
||||
print_error("*")
|
||||
print_error("* Metasploit now requires version 0.11 or higher of the 'pg' gem for database support")
|
||||
print_error("* There a three ways to accomplish this upgrade:")
|
||||
print_error("* 1. If you run Metasploit with your system ruby, simply upgrade the gem:")
|
||||
print_error("* $ rvmsudo gem install pg ")
|
||||
print_error("* 2. Use the Community Edition web interface to apply a Software Update")
|
||||
print_error("* 3. Uninstall, download the latest version, and reinstall Metasploit")
|
||||
print_error("*")
|
||||
print_error("***")
|
||||
print_error("")
|
||||
print_error("")
|
||||
end
|
||||
|
||||
print_error("Failed to connect to the database: #{framework.db.error}")
|
||||
end
|
||||
end
|
||||
|
||||
# Initialize the module paths only if we didn't get passed a Framework instance
|
||||
|
|
|
@ -2,31 +2,18 @@
|
|||
# Use bundler to load dependencies
|
||||
#
|
||||
|
||||
GEMFILE_EXTENSIONS = [
|
||||
'.local',
|
||||
''
|
||||
]
|
||||
# Override the normal rails default, so that msfconsole will come up in production mode instead of development mode
|
||||
# unless the `--environment` flag is passed.
|
||||
ENV['RAILS_ENV'] ||= 'production'
|
||||
|
||||
unless ENV['BUNDLE_GEMFILE']
|
||||
require 'pathname'
|
||||
require 'pathname'
|
||||
root = Pathname.new(__FILE__).expand_path.parent.parent
|
||||
config = root.join('config')
|
||||
require config.join('boot')
|
||||
|
||||
msfenv_real_pathname = Pathname.new(__FILE__).realpath
|
||||
root = msfenv_real_pathname.parent.parent
|
||||
|
||||
GEMFILE_EXTENSIONS.each do |extension|
|
||||
extension_pathname = root.join("Gemfile#{extension}")
|
||||
|
||||
if extension_pathname.readable?
|
||||
ENV['BUNDLE_GEMFILE'] ||= extension_pathname.to_path
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
require 'bundler/setup'
|
||||
rescue ::LoadError
|
||||
$stderr.puts "[*] Metasploit requires the Bundler gem to be installed"
|
||||
$stderr.puts " $ gem install bundler"
|
||||
exit(0)
|
||||
# Requiring environment will define the Metasploit::Framework::Application as the one and only Rails::Application in
|
||||
# this process and cause an error if a Rails.application is already defined, such as when loading msfenv through
|
||||
# msfconsole in Metasploit Pro.
|
||||
unless defined?(Rails) && !Rails.application.nil?
|
||||
require config.join('environment')
|
||||
end
|
||||
|
|
|
@ -35,9 +35,9 @@ module Net; module SSH
|
|||
# appropriately. The new key is returned. If the key itself is
|
||||
# encrypted (requiring a passphrase to use), the user will be
|
||||
# prompted to enter their password unless passphrase works.
|
||||
def load_private_key(filename, passphrase=nil)
|
||||
def load_private_key(filename, passphrase=nil, ask_passphrase=true)
|
||||
data = File.open(File.expand_path(filename), "rb") {|f| f.read(f.stat.size)}
|
||||
load_data_private_key(data, passphrase, filename)
|
||||
load_data_private_key(data, passphrase, ask_passphrase, filename)
|
||||
end
|
||||
|
||||
# Loads a private key. It will correctly determine
|
||||
|
@ -45,7 +45,7 @@ module Net; module SSH
|
|||
# appropriately. The new key is returned. If the key itself is
|
||||
# encrypted (requiring a passphrase to use), the user will be
|
||||
# prompted to enter their password unless passphrase works.
|
||||
def load_data_private_key(data, passphrase=nil, filename="")
|
||||
def load_data_private_key(data, passphrase=nil, ask_passphrase= true, filename="")
|
||||
if data.match(/-----BEGIN DSA PRIVATE KEY-----/)
|
||||
key_type = OpenSSL::PKey::DSA
|
||||
elsif data.match(/-----BEGIN RSA PRIVATE KEY-----/)
|
||||
|
@ -62,7 +62,7 @@ module Net; module SSH
|
|||
begin
|
||||
return key_type.new(data, passphrase || 'invalid')
|
||||
rescue OpenSSL::PKey::RSAError, OpenSSL::PKey::DSAError => e
|
||||
if encrypted_key
|
||||
if encrypted_key && ask_passphrase
|
||||
tries += 1
|
||||
if tries <= 3
|
||||
passphrase = prompt("Enter passphrase for #{filename}:", false)
|
||||
|
|
|
@ -739,11 +739,15 @@ class Error < ::RuntimeError
|
|||
# returns an error string if it exists, otherwise just the error code
|
||||
def get_error(error)
|
||||
string = ''
|
||||
if @@errors[error]
|
||||
if error && @@errors[error]
|
||||
string = @@errors[error]
|
||||
else
|
||||
elsif error
|
||||
string = sprintf('0x%.8x',error)
|
||||
else
|
||||
string = "Unknown error"
|
||||
end
|
||||
|
||||
string
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -781,6 +785,10 @@ class InvalidPacket < Error
|
|||
attr_accessor :word_count
|
||||
attr_accessor :command
|
||||
attr_accessor :error_code
|
||||
|
||||
def error_name
|
||||
get_error(error_code)
|
||||
end
|
||||
end
|
||||
|
||||
class InvalidWordCount < InvalidPacket
|
||||
|
@ -807,7 +815,7 @@ end
|
|||
class ErrorCode < InvalidPacket
|
||||
def to_s
|
||||
'The server responded with error: ' +
|
||||
self.get_error(self.error_code) +
|
||||
self.error_name +
|
||||
" (Command=#{self.command} WordCount=#{self.word_count})"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -275,9 +275,9 @@ protected
|
|||
nameline << pad(' ', last_col, last_idx)
|
||||
|
||||
remainder = colprops[last_idx]['MaxWidth'] - last_col.length
|
||||
if (remainder < 0)
|
||||
remainder = 0
|
||||
end
|
||||
if (remainder < 0)
|
||||
remainder = 0
|
||||
end
|
||||
barline << (' ' * (cellpad + remainder))
|
||||
end
|
||||
nameline << col
|
||||
|
@ -305,7 +305,7 @@ protected
|
|||
last_cell = nil
|
||||
last_idx = nil
|
||||
row.each_with_index { |cell, idx|
|
||||
if (last_cell)
|
||||
if (idx != 0)
|
||||
line << pad(' ', last_cell.to_s, last_idx)
|
||||
end
|
||||
# line << pad(' ', cell.to_s, idx)
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
load 'active_record/railties/databases.rake'
|
||||
|
||||
require 'metasploit/framework'
|
||||
require 'metasploit/framework/database'
|
||||
|
||||
# A modification to remove dependency on Rails.env
|
||||
#
|
||||
# @see https://github.com/rails/rails/blob/ddce29bfa12462fde2342a0c2bd0eefd420c0eab/activerecord/lib/active_record/railties/databases.rake#L550
|
||||
def configs_for_environment
|
||||
environments = [Metasploit::Framework.env]
|
||||
|
||||
if Metasploit::Framework.env.development?
|
||||
environments << 'test'
|
||||
end
|
||||
|
||||
environment_configurations = ActiveRecord::Base.configurations.values_at(*environments)
|
||||
present_environment_configurations = environment_configurations.compact
|
||||
valid_environment_configurations = present_environment_configurations.reject { |config|
|
||||
config['database'].blank?
|
||||
}
|
||||
|
||||
valid_environment_configurations
|
||||
end
|
||||
|
||||
# emulate initializer "active_record.initialize_database" from active_record/railtie
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
self.configurations = Metasploit::Framework::Database.configurations
|
||||
puts "Connecting to database specified by #{Metasploit::Framework::Database.configurations_pathname}"
|
||||
|
||||
spec = configurations[Metasploit::Framework.env]
|
||||
establish_connection(spec)
|
||||
end
|
||||
|
||||
#
|
||||
# Remove tasks that aren't supported
|
||||
#
|
||||
|
||||
Rake::TaskManager.class_eval do
|
||||
def remove_task(task_name)
|
||||
@tasks.delete(task_name.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
Rake.application.remove_task('db:fixtures:load')
|
||||
|
||||
# completely replace db:load_config and db:seed as they will attempt to use
|
||||
# Rails.application, which does not exist
|
||||
Rake::Task['db:load_config'].clear
|
||||
Rake::Task['db:seed'].clear
|
||||
|
||||
db_namespace = namespace :db do
|
||||
task :load_config do
|
||||
ActiveRecord::Base.configurations = Metasploit::Framework::Database.configurations
|
||||
|
||||
ActiveRecord::Migrator.migrations_paths = [
|
||||
# rails isn't in Gemfile, so can't use the more appropriate
|
||||
# Metasploit::Engine.instance.paths['db/migrate'].to_a since using
|
||||
# Metasploit::Engine requires rails.
|
||||
MetasploitDataModels.root.join('db', 'migrate').to_s
|
||||
]
|
||||
end
|
||||
|
||||
desc 'Load the seed data from db/seeds.rb'
|
||||
task :seed do
|
||||
db_namespace['abort_if_pending_migrations'].invoke
|
||||
seeds_pathname = Metasploit::Framework.root.join('db', 'seeds.rb')
|
||||
|
||||
if seeds_pathname.exist?
|
||||
load(seeds_pathname)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace :db do
|
||||
# Add onto the task so that after adding Rails.application.paths['db/migrate']
|
||||
task :load_config do
|
||||
# It's important to call to_a or the paths will just be relative and not realpaths
|
||||
ActiveRecord::Migrator.migrations_paths += Metasploit::Credential::Engine.instance.paths['db/migrate'].to_a
|
||||
end
|
||||
end
|
|
@ -1,21 +0,0 @@
|
|||
# Rake tasks added for compatibility with rake tasks that depend on a Rails
|
||||
# environment, such as those in activerecord
|
||||
|
||||
# Would normally load config/environment.rb of the rails application.
|
||||
#
|
||||
# @see https://github.com/rails/rails/blob/e2908356672d4459ada0064f773efd820efda822/railties/lib/rails/application.rb#L190
|
||||
task :environment do
|
||||
# ensures that Mdm models are available for migrations which use the models
|
||||
MetasploitDataModels.require_models
|
||||
|
||||
# avoids the need for Rails.root in db:schema:dump
|
||||
schema_pathname = Metasploit::Framework.root.join('db', 'schema.rb')
|
||||
ENV['SCHEMA'] = schema_pathname.to_s
|
||||
end
|
||||
|
||||
# This would normally default RAILS_ENV to development if ENV['RAILS_ENV'] is
|
||||
# not set
|
||||
#
|
||||
# @see https://github.com/rails/rails/blob/1a275730b290c1f06d4e8df680d22ae1b41ab585/railties/lib/rails/tasks/misc.rake#L3
|
||||
task :rails_env do
|
||||
end
|
|
@ -1 +0,0 @@
|
|||
require 'zip/zip'
|
1146
lib/zip/ChangeLog
1146
lib/zip/ChangeLog
File diff suppressed because it is too large
Load Diff
162
lib/zip/NEWS
162
lib/zip/NEWS
|
@ -1,162 +0,0 @@
|
|||
= Version 0.9.4
|
||||
|
||||
Changed ZipOutputStream.put_next_entry signature (API CHANGE!). Now
|
||||
allows comment, extra field and compression method to be specified.
|
||||
|
||||
= Version 0.9.3
|
||||
|
||||
Fixed: Added ZipEntry::name_encoding which retrieves the character
|
||||
encoding of the name and comment of the entry. Also added convenience
|
||||
methods ZipEntry::name_in(enc) and ZipEntry::comment_in(enc) for
|
||||
getting zip entry names and comments in a specified character
|
||||
encoding.
|
||||
|
||||
= Version 0.9.2
|
||||
|
||||
Fixed: Renaming an entry failed if the entry's new name was a
|
||||
different length than its old name. (Diego Barros)
|
||||
|
||||
= Version 0.9.1
|
||||
|
||||
Added symlink support and support for unix file permissions. Reduced
|
||||
memory usage during decompression.
|
||||
|
||||
New methods ZipFile::[follow_symlinks, restore_times, restore_permissions, restore_ownership].
|
||||
New methods ZipEntry::unix_perms, ZipInputStream::eof?.
|
||||
Added documentation and test for new ZipFile::extract.
|
||||
Added some of the API suggestions from sf.net #1281314.
|
||||
Applied patch for sf.net bug #1446926.
|
||||
Applied patch for sf.net bug #1459902.
|
||||
Rework ZipEntry and delegate classes.
|
||||
|
||||
= Version 0.5.12
|
||||
|
||||
Fixed problem with writing binary content to a ZipFile in MS Windows.
|
||||
|
||||
= Version 0.5.11
|
||||
|
||||
Fixed name clash file method copy_stream from fileutils.rb. Fixed
|
||||
problem with references to constant CHUNK_SIZE.
|
||||
ZipInputStream/AbstractInputStream read is now buffered like ruby IO's
|
||||
read method, which means that read and gets etc can be mixed. The
|
||||
unbuffered read method has been renamed to sysread.
|
||||
|
||||
= Version 0.5.10
|
||||
|
||||
Fixed method name resolution problem with FileUtils::copy_stream and
|
||||
IOExtras::copy_stream.
|
||||
|
||||
= Version 0.5.9
|
||||
|
||||
Fixed serious memory consumption issue
|
||||
|
||||
= Version 0.5.8
|
||||
|
||||
Fixed install script.
|
||||
|
||||
= Version 0.5.7
|
||||
|
||||
install.rb no longer assumes it is being run from the toplevel source
|
||||
dir. Directory structure changed to reflect common ruby library
|
||||
project structure. Migrated from RubyUnit to Test::Unit format. Now
|
||||
uses Rake to build source packages and gems and run unit tests.
|
||||
|
||||
= Version 0.5.6
|
||||
|
||||
Fix for FreeBSD 4.9 which returns Errno::EFBIG instead of
|
||||
Errno::EINVAL for some invalid seeks. Fixed 'version needed to
|
||||
extract'-field incorrect in local headers.
|
||||
|
||||
= Version 0.5.5
|
||||
|
||||
Fix for a problem with writing zip files that concerns only ruby 1.8.1.
|
||||
|
||||
= Version 0.5.4
|
||||
|
||||
Significantly reduced memory footprint when modifying zip files.
|
||||
|
||||
= Version 0.5.3
|
||||
|
||||
Added optimization to avoid decompressing and recompressing individual
|
||||
entries when modifying a zip archive.
|
||||
|
||||
= Version 0.5.2
|
||||
|
||||
Fixed ZipFile corruption bug in ZipFile class. Added basic unix
|
||||
extra-field support.
|
||||
|
||||
= Version 0.5.1
|
||||
|
||||
Fixed ZipFile.get_output_stream bug.
|
||||
|
||||
= Version 0.5.0
|
||||
|
||||
List of changes:
|
||||
* Ruby 1.8.0 and ruby-zlib 0.6.0 compatibility
|
||||
* Changed method names from camelCase to rubys underscore style.
|
||||
* Installs to zip/ subdir instead of directly to site_ruby
|
||||
* Added ZipFile.directory and ZipFile.file - each method return an
|
||||
object that can be used like Dir and File only for the contents of the
|
||||
zip file.
|
||||
* Added sample application zipfind which works like Find.find, only
|
||||
Zip::ZipFind.find traverses into zip archives too.
|
||||
|
||||
Bug fixes:
|
||||
* AbstractInputStream.each_line with non-default separator
|
||||
|
||||
|
||||
= Version 0.5.0a
|
||||
|
||||
Source reorganized. Added ziprequire, which can be used to load ruby
|
||||
modules from a zip file, in a fashion similar to jar files in
|
||||
Java. Added gtkRubyzip, another sample application. Implemented
|
||||
ZipInputStream.lineno and ZipInputStream.rewind
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Read and write date and time information correctly for zip entries.
|
||||
* Fixed read() using separate buffer, causing mix of gets/readline/read to
|
||||
cause problems.
|
||||
|
||||
= Version 0.4.2
|
||||
|
||||
Performance optimizations. Test suite runs in half the time.
|
||||
|
||||
= Version 0.4.1
|
||||
|
||||
Windows compatibility fixes.
|
||||
|
||||
= Version 0.4.0
|
||||
|
||||
Zip::ZipFile is now mutable and provides a more convenient way of
|
||||
modifying zip archives than Zip::ZipOutputStream. Operations for
|
||||
adding, extracting, renaming, replacing and removing entries to zip
|
||||
archives are now available.
|
||||
|
||||
Runs without warnings with -w switch.
|
||||
|
||||
Install script install.rb added.
|
||||
|
||||
|
||||
= Version 0.3.1
|
||||
|
||||
Rudimentary support for writing zip archives.
|
||||
|
||||
|
||||
= Version 0.2.2
|
||||
|
||||
Fixed and extended unit test suite. Updated to work with ruby/zlib
|
||||
0.5. It doesn't work with earlier versions of ruby/zlib.
|
||||
|
||||
|
||||
= Version 0.2.0
|
||||
|
||||
Class ZipFile added. Where ZipInputStream is used to read the
|
||||
individual entries in a zip file, ZipFile reads the central directory
|
||||
in the zip archive, so you can get to any entry in the zip archive
|
||||
without having to skipping through all the preceeding entries.
|
||||
|
||||
|
||||
= Version 0.1.0
|
||||
|
||||
First working version of ZipInputStream.
|
|
@ -1,72 +0,0 @@
|
|||
= rubyzip
|
||||
|
||||
rubyzip is a ruby library for reading and writing zip files.
|
||||
|
||||
= Install
|
||||
|
||||
If you have rubygems you can install rubyzip directly from the gem
|
||||
repository
|
||||
|
||||
gem install rubyzip
|
||||
|
||||
Otherwise obtain the source (see below) and run
|
||||
|
||||
ruby install.rb
|
||||
|
||||
To run the unit tests you need to have test::unit installed
|
||||
|
||||
rake test
|
||||
|
||||
|
||||
= Documentation
|
||||
|
||||
There is more than one way to access or create a zip archive with
|
||||
rubyzip. The basic API is modeled after the classes in
|
||||
java.util.zip from the Java SDK. This means there are classes such
|
||||
as Zip::ZipInputStream, Zip::ZipOutputStream and
|
||||
Zip::ZipFile. Zip::ZipInputStream provides a basic interface for
|
||||
iterating through the entries in a zip archive and reading from the
|
||||
entries in the same way as from a regular File or IO
|
||||
object. ZipOutputStream is the corresponding basic output
|
||||
facility. Zip::ZipFile provides a mean for accessing the archives
|
||||
central directory and provides means for accessing any entry without
|
||||
having to iterate through the archive. Unlike Java's
|
||||
java.util.zip.ZipFile rubyzip's Zip::ZipFile is mutable, which means
|
||||
it can be used to change zip files as well.
|
||||
|
||||
Another way to access a zip archive with rubyzip is to use rubyzip's
|
||||
Zip::ZipFileSystem API. Using this API files can be read from and
|
||||
written to the archive in much the same manner as ruby's builtin
|
||||
classes allows files to be read from and written to the file system.
|
||||
|
||||
rubyzip also features the
|
||||
zip/ziprequire.rb[link:files/lib/zip/ziprequire_rb.html] module which
|
||||
allows ruby to load ruby modules from zip archives.
|
||||
|
||||
For details about the specific behaviour of classes and methods refer
|
||||
to the test suite. Finally you can generate the rdoc documentation or
|
||||
visit http://rubyzip.sourceforge.net.
|
||||
|
||||
= License
|
||||
|
||||
rubyzip is distributed under the same license as ruby. See
|
||||
http://www.ruby-lang.org/en/LICENSE.txt
|
||||
|
||||
|
||||
= Website and Project Home
|
||||
|
||||
http://rubyzip.sourceforge.net
|
||||
|
||||
http://sourceforge.net/projects/rubyzip
|
||||
|
||||
== Download (tarballs and gems)
|
||||
|
||||
http://sourceforge.net/project/showfiles.php?group_id=43107&package_id=35377
|
||||
|
||||
= Authors
|
||||
|
||||
Thomas Sondergaard (thomas at sondergaard.cc)
|
||||
|
||||
Technorama Ltd. (oss-ruby-zip at technorama.net)
|
||||
|
||||
extra-field support contributed by Tatsuki Sugiura (sugi at nemui.org)
|
16
lib/zip/TODO
16
lib/zip/TODO
|
@ -1,16 +0,0 @@
|
|||
|
||||
* ZipInputStream: Support zip-files with trailing data descriptors
|
||||
* Adjust rdoc stylesheet to advertise inherited methods if possible
|
||||
* Suggestion: Add ZipFile/ZipInputStream example that demonstrates extracting all entries.
|
||||
* Suggestion: ZipFile#extract destination should default to "."
|
||||
* Suggestion: ZipEntry should have extract(), get_input_stream() methods etc
|
||||
* SUggestion: ZipInputStream/ZipOutputStream should accept an IO object in addition to a filename.
|
||||
* (is buffering used anywhere with write?)
|
||||
* Inflater.sysread should pass the buffer to produce_input.
|
||||
* Implement ZipFsDir.glob
|
||||
* ZipFile.checkIntegrity method
|
||||
* non-MSDOS permission attributes
|
||||
** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2"
|
||||
* Packager version, required unpacker version in zip headers
|
||||
** See mail from Ned Konz to ruby-talk subj. "Re: SV: [ANN] Archive 0.2"
|
||||
* implement storing attributes and ownership information
|
|
@ -1,165 +0,0 @@
|
|||
module IOExtras #:nodoc:
|
||||
|
||||
CHUNK_SIZE = 131072
|
||||
|
||||
RANGE_ALL = 0..-1
|
||||
|
||||
def self.copy_stream(ostream, istream)
|
||||
s = ''
|
||||
ostream.write(istream.read(CHUNK_SIZE, s)) until istream.eof?
|
||||
end
|
||||
|
||||
def self.copy_stream_n(ostream, istream, nbytes)
|
||||
s = ''
|
||||
toread = nbytes
|
||||
while (toread > 0 && ! istream.eof?)
|
||||
tr = toread > CHUNK_SIZE ? CHUNK_SIZE : toread
|
||||
ostream.write(istream.read(tr, s))
|
||||
toread -= tr
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Implements kind_of? in order to pretend to be an IO object
|
||||
module FakeIO
|
||||
def kind_of?(object)
|
||||
object == IO || super
|
||||
end
|
||||
end
|
||||
|
||||
# Implements many of the convenience methods of IO
|
||||
# such as gets, getc, readline and readlines
|
||||
# depends on: input_finished?, produce_input and read
|
||||
module AbstractInputStream
|
||||
include Enumerable
|
||||
include FakeIO
|
||||
|
||||
def initialize
|
||||
super
|
||||
@lineno = 0
|
||||
@outputBuffer = ""
|
||||
end
|
||||
|
||||
attr_accessor :lineno
|
||||
|
||||
def read(numberOfBytes = nil, buf = nil)
|
||||
tbuf = nil
|
||||
|
||||
if @outputBuffer.length > 0
|
||||
if numberOfBytes <= @outputBuffer.length
|
||||
tbuf = @outputBuffer.slice!(0, numberOfBytes)
|
||||
else
|
||||
numberOfBytes -= @outputBuffer.length if (numberOfBytes)
|
||||
rbuf = sysread(numberOfBytes, buf)
|
||||
tbuf = @outputBuffer
|
||||
tbuf << rbuf if (rbuf)
|
||||
@outputBuffer = ""
|
||||
end
|
||||
else
|
||||
tbuf = sysread(numberOfBytes, buf)
|
||||
end
|
||||
|
||||
return nil unless (tbuf)
|
||||
|
||||
if buf
|
||||
buf.replace(tbuf)
|
||||
else
|
||||
buf = tbuf
|
||||
end
|
||||
|
||||
buf
|
||||
end
|
||||
|
||||
def readlines(aSepString = $/)
|
||||
retVal = []
|
||||
each_line(aSepString) { |line| retVal << line }
|
||||
return retVal
|
||||
end
|
||||
|
||||
def gets(aSepString=$/)
|
||||
@lineno = @lineno.next
|
||||
return read if aSepString == nil
|
||||
aSepString="#{$/}#{$/}" if aSepString == ""
|
||||
|
||||
bufferIndex=0
|
||||
while ((matchIndex = @outputBuffer.index(aSepString, bufferIndex)) == nil)
|
||||
bufferIndex=@outputBuffer.length
|
||||
if input_finished?
|
||||
return @outputBuffer.empty? ? nil : flush
|
||||
end
|
||||
@outputBuffer << produce_input
|
||||
end
|
||||
sepIndex=matchIndex + aSepString.length
|
||||
return @outputBuffer.slice!(0...sepIndex)
|
||||
end
|
||||
|
||||
def flush
|
||||
retVal=@outputBuffer
|
||||
@outputBuffer=""
|
||||
return retVal
|
||||
end
|
||||
|
||||
def readline(aSepString = $/)
|
||||
retVal = gets(aSepString)
|
||||
raise EOFError if retVal == nil
|
||||
return retVal
|
||||
end
|
||||
|
||||
def each_line(aSepString = $/)
|
||||
while true
|
||||
yield readline(aSepString)
|
||||
end
|
||||
rescue EOFError
|
||||
end
|
||||
|
||||
alias_method :each, :each_line
|
||||
end
|
||||
|
||||
|
||||
# Implements many of the output convenience methods of IO.
|
||||
# relies on <<
|
||||
module AbstractOutputStream
|
||||
include FakeIO
|
||||
|
||||
def write(data)
|
||||
self << data
|
||||
data.to_s.length
|
||||
end
|
||||
|
||||
|
||||
def print(*params)
|
||||
self << params.join << $\.to_s
|
||||
end
|
||||
|
||||
def printf(aFormatString, *params)
|
||||
self << sprintf(aFormatString, *params)
|
||||
end
|
||||
|
||||
def putc(anObject)
|
||||
self << case anObject
|
||||
when Fixnum then anObject.chr
|
||||
when String then anObject
|
||||
else raise TypeError, "putc: Only Fixnum and String supported"
|
||||
end
|
||||
anObject
|
||||
end
|
||||
|
||||
def puts(*params)
|
||||
params << "\n" if params.empty?
|
||||
params.flatten.each {
|
||||
|element|
|
||||
val = element.to_s
|
||||
self << val
|
||||
self << "\n" unless val[-1,1] == "\n"
|
||||
}
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # IOExtras namespace module
|
||||
|
||||
|
||||
|
||||
# Copyright (C) 2002-2004 Thomas Sondergaard
|
||||
# rubyzip is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the ruby license.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue