diff --git a/Gemfile b/Gemfile index 225cadb5e1b3c..3981e988570d7 100755 --- a/Gemfile +++ b/Gemfile @@ -3,6 +3,12 @@ source 'https://rubygems.org' # spec.add_runtime_dependency '', [] gemspec name: 'metasploit-framework' +# Metasploit::Concern hooks +gem 'metasploit-concern', github: 'crmaxx/metasploit-concern', branch: 'staging/rails-4.2' +# Things that would normally be part of the database model, but which +# are needed when there's no database +gem 'metasploit-model', github: 'crmaxx/metasploit-model', branch: 'staging/rails-4.2' + # separate from test as simplecov is not run on travis-ci group :coverage do # code coverage for tests @@ -13,6 +19,10 @@ end group :db do gemspec name: 'metasploit-framework-db' + # Metasploit::Credential database models + gem 'metasploit-credential', github: 'crmaxx/metasploit-credential', branch: 'staging/rails-4.2' + # Database models shared between framework and Pro. + gem 'metasploit_data_models', github: 'crmaxx/metasploit_data_models', branch: 'staging/rails-4.2' end group :development do @@ -46,7 +56,7 @@ group :test do # cucumber extension for testing command line applications, like msfconsole gem 'aruba' # cucumber + automatic database cleaning with database_cleaner - gem 'cucumber-rails', :require => false + gem 'cucumber-rails', require: false gem 'shoulda-matchers' # Manipulate Time.now in specs gem 'timecop' diff --git a/Gemfile.lock b/Gemfile.lock index 2319aa9af35ed..ea9169b75e499 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,14 +1,57 @@ +GIT + remote: git://github.com/crmaxx/metasploit-concern.git + revision: ec59753e8c7034ab861551a121ae579e254a553b + branch: staging/rails-4.2 + specs: + metasploit-concern (1.0.0.pre.rails.pre.4.2) + activerecord (>= 4.2.1) + activesupport (>= 4.2.1) + railties (>= 4.2.1) + +GIT + remote: git://github.com/crmaxx/metasploit-credential.git + revision: 78d97624f33d53d99bf9531fd8eb365b75d99284 + branch: staging/rails-4.2 + specs: + metasploit-credential (1.0.0.pre.rails.pre.4.2) + pg + railties + rubyntlm + rubyzip (~> 1.1) + +GIT + remote: git://github.com/crmaxx/metasploit-model.git + revision: f40715caf40583a4dd5af1afec358595862f7c62 + branch: staging/rails-4.2 + specs: + metasploit-model (1.0.0.pre.rails.pre.4.2) + activemodel (>= 4.2.1) + activesupport (>= 4.2.1) + railties (>= 4.2.1) + +GIT + remote: git://github.com/crmaxx/metasploit_data_models.git + revision: 56879dc5f369debf260400cc235b4af14f820329 + branch: staging/rails-4.2 + specs: + metasploit_data_models (1.0.1.pre.rails.pre.4.2) + activerecord (>= 4.2.1) + activesupport (>= 4.2.1) + arel-helpers + pg + postgres_ext + railties (>= 4.2.1) + recog (~> 1.0) + PATH remote: . specs: metasploit-framework (4.11.0.pre.dev) - actionpack (>= 4.0.9, < 4.1.0) - activesupport (>= 4.0.9, < 4.1.0) + actionpack (>= 4.2.1) + activesupport (>= 4.2.1) bcrypt jsobfu (~> 0.2.0) json - metasploit-concern (~> 1.0) - metasploit-model (~> 1.0) metasploit-payloads (= 0.0.7) msgpack nokogiri @@ -21,10 +64,8 @@ PATH sqlite3 tzinfo metasploit-framework-db (4.11.0.pre.dev) - activerecord (>= 4.0.9, < 4.1.0) - metasploit-credential (~> 1.0) + activerecord (>= 4.2.1) metasploit-framework (= 4.11.0.pre.dev) - metasploit_data_models (~> 1.0) pg (>= 0.11) metasploit-framework-pcap (4.11.0.pre.dev) metasploit-framework (= 4.11.0.pre.dev) @@ -34,31 +75,42 @@ PATH GEM remote: https://rubygems.org/ specs: - actionmailer (4.0.13) - actionpack (= 4.0.13) + actionmailer (4.2.1) + actionpack (= 4.2.1) + actionview (= 4.2.1) + activejob (= 4.2.1) mail (~> 2.5, >= 2.5.4) - actionpack (4.0.13) - activesupport (= 4.0.13) - builder (~> 3.1.0) - erubis (~> 2.7.0) - rack (~> 1.5.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.1) + actionview (= 4.2.1) + activesupport (= 4.2.1) + rack (~> 1.6) rack-test (~> 0.6.2) - activemodel (4.0.13) - activesupport (= 4.0.13) - builder (~> 3.1.0) - activerecord (4.0.13) - activemodel (= 4.0.13) - activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.13) - arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.4) - activesupport (4.0.13) - i18n (~> 0.6, >= 0.6.9) - minitest (~> 4.2) - multi_json (~> 1.3) - thread_safe (~> 0.1) - tzinfo (~> 0.3.37) - arel (4.0.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.1) + actionview (4.2.1) + activesupport (= 4.2.1) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.1) + activejob (4.2.1) + activesupport (= 4.2.1) + globalid (>= 0.3.0) + activemodel (4.2.1) + activesupport (= 4.2.1) + builder (~> 3.1) + activerecord (4.2.1) + activemodel (= 4.2.1) + activesupport (= 4.2.1) + arel (~> 6.0) + activesupport (4.2.1) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + arel (6.0.0) arel-helpers (2.1.0) activerecord (>= 3.1.0, < 5) aruba (0.6.2) @@ -66,14 +118,14 @@ GEM cucumber (>= 1.1.1) rspec-expectations (>= 2.7.0) bcrypt (3.1.10) - builder (3.1.4) + builder (3.2.2) capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) - childprocess (0.5.5) + childprocess (0.5.6) ffi (~> 1.0, >= 1.0.11) coderay (1.1.0) cucumber (1.3.19) @@ -100,44 +152,21 @@ GEM fivemat (1.2.1) gherkin (2.12.2) multi_json (~> 1.3) - hike (1.2.3) + globalid (0.3.5) + activesupport (>= 4.1.0) i18n (0.7.0) jsobfu (0.2.1) rkelly-remix (= 0.0.6) json (1.8.2) + loofah (2.0.2) + nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) - metasploit-concern (1.0.0) - activerecord (>= 4.0.9, < 4.1.0) - activesupport (>= 4.0.9, < 4.1.0) - railties (>= 4.0.9, < 4.1.0) - metasploit-credential (1.0.0) - metasploit-concern (~> 1.0) - metasploit-model (~> 1.0) - metasploit_data_models (~> 1.0) - pg - railties - rubyntlm - rubyzip (~> 1.1) - metasploit-model (1.0.0) - activemodel (>= 4.0.9, < 4.1.0) - activesupport (>= 4.0.9, < 4.1.0) - railties (>= 4.0.9, < 4.1.0) metasploit-payloads (0.0.7) - metasploit_data_models (1.0.1) - activerecord (>= 4.0.9, < 4.1.0) - activesupport (>= 4.0.9, < 4.1.0) - arel-helpers - metasploit-concern (~> 1.0) - metasploit-model (~> 1.0) - pg - postgres_ext - railties (>= 4.0.9, < 4.1.0) - recog (~> 1.0) method_source (0.8.2) - mime-types (2.4.3) + mime-types (2.5) mini_portile (0.6.2) - minitest (4.7.5) + minitest (5.6.1) msgpack (0.5.11) multi_json (1.11.0) multi_test (0.1.2) @@ -146,7 +175,7 @@ GEM mini_portile (~> 0.6.0) packetfu (1.1.9) pcaprub (0.12.0) - pg (0.18.1) + pg (0.18.2) pg_array_parser (0.0.9) postgres_ext (2.4.1) activerecord (>= 4.0.0) @@ -156,20 +185,31 @@ GEM coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - rack (1.5.2) + rack (1.6.1) rack-test (0.6.3) rack (>= 1.0) - rails (4.0.13) - actionmailer (= 4.0.13) - actionpack (= 4.0.13) - activerecord (= 4.0.13) - activesupport (= 4.0.13) + rails (4.2.1) + actionmailer (= 4.2.1) + actionpack (= 4.2.1) + actionview (= 4.2.1) + activejob (= 4.2.1) + activemodel (= 4.2.1) + activerecord (= 4.2.1) + activesupport (= 4.2.1) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.13) - sprockets-rails (~> 2.0) - railties (4.0.13) - actionpack (= 4.0.13) - activesupport (= 4.0.13) + railties (= 4.2.1) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.6) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.2) + loofah (~> 2.0) + railties (4.2.1) + actionpack (= 4.2.1) + activesupport (= 4.2.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rake (10.4.2) @@ -202,27 +242,24 @@ GEM rubyzip (1.1.7) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - simplecov (0.9.2) + simplecov (0.10.0) docile (~> 1.1.0) - multi_json (~> 1.0) - simplecov-html (~> 0.9.0) - simplecov-html (0.9.0) + json (~> 1.8) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) slop (3.6.0) - sprockets (2.12.3) - hike (~> 1.2) - multi_json (~> 1.0) + sprockets (3.1.0) rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.2.4) + sprockets-rails (2.3.1) actionpack (>= 3.0) activesupport (>= 3.0) sprockets (>= 2.8, < 4.0) sqlite3 (1.3.10) thor (0.19.1) thread_safe (0.3.5) - tilt (1.4.1) timecop (0.7.3) - tzinfo (0.3.43) + tzinfo (1.2.2) + thread_safe (~> 0.1) xpath (2.0.0) nokogiri (~> 1.3) yard (0.8.7.6) @@ -235,9 +272,13 @@ DEPENDENCIES cucumber-rails factory_girl_rails (~> 4.5.0) fivemat (= 1.2.1) + metasploit-concern! + metasploit-credential! metasploit-framework! metasploit-framework-db! metasploit-framework-pcap! + metasploit-model! + metasploit_data_models! pry rake (>= 10.0.0) redcarpet diff --git a/db/schema.rb b/db/schema.rb index 7d9e8d02b7e2a..71d35badb18d5 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -16,24 +16,24 @@ # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" - create_table "api_keys", force: true do |t| + create_table "api_keys", force: :cascade do |t| t.text "token" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "automatic_exploitation_match_results", force: true do |t| + create_table "automatic_exploitation_match_results", force: :cascade do |t| t.integer "match_id" t.integer "run_id" - t.string "state", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "state", limit: 255, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "automatic_exploitation_match_results", ["match_id"], name: "index_automatic_exploitation_match_results_on_match_id", using: :btree add_index "automatic_exploitation_match_results", ["run_id"], name: "index_automatic_exploitation_match_results_on_run_id", using: :btree - create_table "automatic_exploitation_match_sets", force: true do |t| + create_table "automatic_exploitation_match_sets", force: :cascade do |t| t.integer "workspace_id" t.integer "user_id" t.datetime "created_at", null: false @@ -43,14 +43,14 @@ add_index "automatic_exploitation_match_sets", ["user_id"], name: "index_automatic_exploitation_match_sets_on_user_id", using: :btree add_index "automatic_exploitation_match_sets", ["workspace_id"], name: "index_automatic_exploitation_match_sets_on_workspace_id", using: :btree - create_table "automatic_exploitation_matches", force: true do |t| + create_table "automatic_exploitation_matches", force: :cascade do |t| t.integer "module_detail_id" - t.string "state" + t.string "state", limit: 255 t.integer "nexpose_data_vulnerability_definition_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.integer "match_set_id" - t.string "matchable_type" + t.string "matchable_type", limit: 255 t.integer "matchable_id" t.text "module_fullname" end @@ -58,7 +58,7 @@ add_index "automatic_exploitation_matches", ["module_detail_id"], name: "index_automatic_exploitation_matches_on_module_detail_id", using: :btree add_index "automatic_exploitation_matches", ["module_fullname"], name: "index_automatic_exploitation_matches_on_module_fullname", using: :btree - create_table "automatic_exploitation_runs", force: true do |t| + create_table "automatic_exploitation_runs", force: :cascade do |t| t.integer "workspace_id" t.integer "user_id" t.integer "match_set_id" @@ -70,7 +70,7 @@ add_index "automatic_exploitation_runs", ["user_id"], name: "index_automatic_exploitation_runs_on_user_id", using: :btree add_index "automatic_exploitation_runs", ["workspace_id"], name: "index_automatic_exploitation_runs_on_workspace_id", using: :btree - create_table "clients", force: true do |t| + create_table "clients", force: :cascade do |t| t.integer "host_id" t.datetime "created_at" t.string "ua_string", limit: 1024, null: false @@ -79,17 +79,17 @@ t.datetime "updated_at" end - create_table "credential_cores_tasks", id: false, force: true do |t| + create_table "credential_cores_tasks", id: false, force: :cascade do |t| t.integer "core_id" t.integer "task_id" end - create_table "credential_logins_tasks", id: false, force: true do |t| + create_table "credential_logins_tasks", id: false, force: :cascade do |t| t.integer "login_id" t.integer "task_id" end - create_table "creds", force: true do |t| + create_table "creds", force: :cascade do |t| t.integer "service_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -99,38 +99,38 @@ t.string "proof", limit: 4096 t.string "ptype", limit: 256 t.integer "source_id" - t.string "source_type" + t.string "source_type", limit: 255 end - create_table "events", force: true do |t| + create_table "events", force: :cascade do |t| t.integer "workspace_id" t.integer "host_id" t.datetime "created_at" - t.string "name" + t.string "name", limit: 255 t.datetime "updated_at" t.boolean "critical" t.boolean "seen" - t.string "username" + t.string "username", limit: 255 t.text "info" end - create_table "exploit_attempts", force: true do |t| + create_table "exploit_attempts", force: :cascade do |t| t.integer "host_id" t.integer "service_id" t.integer "vuln_id" t.datetime "attempted_at" t.boolean "exploited" - t.string "fail_reason" - t.string "username" + t.string "fail_reason", limit: 255 + t.string "username", limit: 255 t.text "module" t.integer "session_id" t.integer "loot_id" t.integer "port" - t.string "proto" + t.string "proto", limit: 255 t.text "fail_detail" end - create_table "exploited_hosts", force: true do |t| + create_table "exploited_hosts", force: :cascade do |t| t.integer "host_id", null: false t.integer "service_id" t.string "session_uuid", limit: 8 @@ -140,29 +140,29 @@ t.datetime "updated_at", null: false end - create_table "host_details", force: true do |t| + create_table "host_details", force: :cascade do |t| t.integer "host_id" t.integer "nx_console_id" t.integer "nx_device_id" - t.string "src" - t.string "nx_site_name" - t.string "nx_site_importance" - t.string "nx_scan_template" + t.string "src", limit: 255 + t.string "nx_site_name", limit: 255 + t.string "nx_site_importance", limit: 255 + t.string "nx_scan_template", limit: 255 t.float "nx_risk_score" end - create_table "hosts", force: true do |t| + create_table "hosts", force: :cascade do |t| t.datetime "created_at" t.inet "address", null: false - t.string "mac" - t.string "comm" - t.string "name" - t.string "state" - t.string "os_name" - t.string "os_flavor" - t.string "os_sp" - t.string "os_lang" - t.string "arch" + t.string "mac", limit: 255 + t.string "comm", limit: 255 + t.string "name", limit: 255 + t.string "state", limit: 255 + t.string "os_name", limit: 255 + t.string "os_flavor", limit: 255 + t.string "os_sp", limit: 255 + t.string "os_lang", limit: 255 + t.string "arch", limit: 255 t.integer "workspace_id", null: false t.datetime "updated_at" t.text "purpose" @@ -176,7 +176,7 @@ t.integer "host_detail_count", default: 0 t.integer "exploit_attempt_count", default: 0 t.integer "cred_count", default: 0 - t.string "detected_arch" + t.string "detected_arch", limit: 255 end add_index "hosts", ["name"], name: "index_hosts_on_name", using: :btree @@ -186,12 +186,12 @@ add_index "hosts", ["state"], name: "index_hosts_on_state", using: :btree add_index "hosts", ["workspace_id", "address"], name: "index_hosts_on_workspace_id_and_address", unique: true, using: :btree - create_table "hosts_tags", force: true do |t| + create_table "hosts_tags", force: :cascade do |t| t.integer "host_id" t.integer "tag_id" end - create_table "listeners", force: true do |t| + create_table "listeners", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "workspace_id", default: 1, null: false @@ -205,7 +205,7 @@ t.text "macro" end - create_table "loots", force: true do |t| + create_table "loots", force: :cascade do |t| t.integer "workspace_id", default: 1, null: false t.integer "host_id" t.integer "service_id" @@ -214,7 +214,7 @@ t.text "data" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "content_type" + t.string "content_type", limit: 255 t.text "name" t.text "info" t.integer "module_run_id" @@ -222,7 +222,7 @@ add_index "loots", ["module_run_id"], name: "index_loots_on_module_run_id", using: :btree - create_table "macros", force: true do |t| + create_table "macros", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.text "owner" @@ -232,7 +232,7 @@ t.binary "prefs" end - create_table "metasploit_credential_cores", force: true do |t| + create_table "metasploit_credential_cores", force: :cascade do |t| t.integer "origin_id", null: false t.string "origin_type", null: false t.integer "private_id" @@ -256,7 +256,7 @@ add_index "metasploit_credential_cores", ["workspace_id", "realm_id", "public_id"], name: "unique_privateless_metasploit_credential_cores", unique: true, where: "(((realm_id IS NOT NULL) AND (public_id IS NOT NULL)) AND (private_id IS NULL))", using: :btree add_index "metasploit_credential_cores", ["workspace_id"], name: "index_metasploit_credential_cores_on_workspace_id", using: :btree - create_table "metasploit_credential_logins", force: true do |t| + create_table "metasploit_credential_logins", force: :cascade do |t| t.integer "core_id", null: false t.integer "service_id", null: false t.string "access_level" @@ -269,7 +269,7 @@ add_index "metasploit_credential_logins", ["core_id", "service_id"], name: "index_metasploit_credential_logins_on_core_id_and_service_id", unique: true, using: :btree add_index "metasploit_credential_logins", ["service_id", "core_id"], name: "index_metasploit_credential_logins_on_service_id_and_core_id", unique: true, using: :btree - create_table "metasploit_credential_origin_cracked_passwords", force: true do |t| + create_table "metasploit_credential_origin_cracked_passwords", force: :cascade do |t| t.integer "metasploit_credential_core_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -277,7 +277,7 @@ add_index "metasploit_credential_origin_cracked_passwords", ["metasploit_credential_core_id"], name: "originating_credential_cores", using: :btree - create_table "metasploit_credential_origin_imports", force: true do |t| + create_table "metasploit_credential_origin_imports", force: :cascade do |t| t.text "filename", null: false t.integer "task_id" t.datetime "created_at", null: false @@ -286,7 +286,7 @@ add_index "metasploit_credential_origin_imports", ["task_id"], name: "index_metasploit_credential_origin_imports_on_task_id", using: :btree - create_table "metasploit_credential_origin_manuals", force: true do |t| + create_table "metasploit_credential_origin_manuals", force: :cascade do |t| t.integer "user_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -294,7 +294,7 @@ add_index "metasploit_credential_origin_manuals", ["user_id"], name: "index_metasploit_credential_origin_manuals_on_user_id", using: :btree - create_table "metasploit_credential_origin_services", force: true do |t| + create_table "metasploit_credential_origin_services", force: :cascade do |t| t.integer "service_id", null: false t.text "module_full_name", null: false t.datetime "created_at", null: false @@ -303,7 +303,7 @@ add_index "metasploit_credential_origin_services", ["service_id", "module_full_name"], name: "unique_metasploit_credential_origin_services", unique: true, using: :btree - create_table "metasploit_credential_origin_sessions", force: true do |t| + create_table "metasploit_credential_origin_sessions", force: :cascade do |t| t.text "post_reference_name", null: false t.integer "session_id", null: false t.datetime "created_at", null: false @@ -312,7 +312,7 @@ add_index "metasploit_credential_origin_sessions", ["session_id", "post_reference_name"], name: "unique_metasploit_credential_origin_sessions", unique: true, using: :btree - create_table "metasploit_credential_privates", force: true do |t| + create_table "metasploit_credential_privates", force: :cascade do |t| t.string "type", null: false t.text "data", null: false t.datetime "created_at", null: false @@ -322,7 +322,7 @@ add_index "metasploit_credential_privates", ["type", "data"], name: "index_metasploit_credential_privates_on_type_and_data", unique: true, using: :btree - create_table "metasploit_credential_publics", force: true do |t| + create_table "metasploit_credential_publics", force: :cascade do |t| t.string "username", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -331,7 +331,7 @@ add_index "metasploit_credential_publics", ["username"], name: "index_metasploit_credential_publics_on_username", unique: true, using: :btree - create_table "metasploit_credential_realms", force: true do |t| + create_table "metasploit_credential_realms", force: :cascade do |t| t.string "key", null: false t.string "value", null: false t.datetime "created_at", null: false @@ -340,27 +340,27 @@ add_index "metasploit_credential_realms", ["key", "value"], name: "index_metasploit_credential_realms_on_key_and_value", unique: true, using: :btree - create_table "mod_refs", force: true do |t| + create_table "mod_refs", force: :cascade do |t| t.string "module", limit: 1024 t.string "mtype", limit: 128 t.text "ref" end - create_table "module_actions", force: true do |t| + create_table "module_actions", force: :cascade do |t| t.integer "detail_id" t.text "name" end add_index "module_actions", ["detail_id"], name: "index_module_actions_on_detail_id", using: :btree - create_table "module_archs", force: true do |t| + create_table "module_archs", force: :cascade do |t| t.integer "detail_id" t.text "name" end add_index "module_archs", ["detail_id"], name: "index_module_archs_on_detail_id", using: :btree - create_table "module_authors", force: true do |t| + create_table "module_authors", force: :cascade do |t| t.integer "detail_id" t.text "name" t.text "email" @@ -368,21 +368,21 @@ add_index "module_authors", ["detail_id"], name: "index_module_authors_on_detail_id", using: :btree - create_table "module_details", force: true do |t| + create_table "module_details", force: :cascade do |t| t.datetime "mtime" t.text "file" - t.string "mtype" + t.string "mtype", limit: 255 t.text "refname" t.text "fullname" t.text "name" t.integer "rank" t.text "description" - t.string "license" + t.string "license", limit: 255 t.boolean "privileged" t.datetime "disclosure_date" t.integer "default_target" t.text "default_action" - t.string "stance" + t.string "stance", limit: 255 t.boolean "ready" end @@ -391,21 +391,21 @@ add_index "module_details", ["name"], name: "index_module_details_on_name", using: :btree add_index "module_details", ["refname"], name: "index_module_details_on_refname", using: :btree - create_table "module_mixins", force: true do |t| + create_table "module_mixins", force: :cascade do |t| t.integer "detail_id" t.text "name" end add_index "module_mixins", ["detail_id"], name: "index_module_mixins_on_detail_id", using: :btree - create_table "module_platforms", force: true do |t| + create_table "module_platforms", force: :cascade do |t| t.integer "detail_id" t.text "name" end add_index "module_platforms", ["detail_id"], name: "index_module_platforms_on_detail_id", using: :btree - create_table "module_refs", force: true do |t| + create_table "module_refs", force: :cascade do |t| t.integer "detail_id" t.text "name" end @@ -413,27 +413,27 @@ add_index "module_refs", ["detail_id"], name: "index_module_refs_on_detail_id", using: :btree add_index "module_refs", ["name"], name: "index_module_refs_on_name", using: :btree - create_table "module_runs", force: true do |t| + create_table "module_runs", force: :cascade do |t| t.datetime "attempted_at" t.text "fail_detail" - t.string "fail_reason" + t.string "fail_reason", limit: 255 t.text "module_fullname" t.integer "port" - t.string "proto" + t.string "proto", limit: 255 t.integer "session_id" - t.string "status" + t.string "status", limit: 255 t.integer "trackable_id" - t.string "trackable_type" + t.string "trackable_type", limit: 255 t.integer "user_id" - t.string "username" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.string "username", limit: 255 + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end add_index "module_runs", ["session_id"], name: "index_module_runs_on_session_id", using: :btree add_index "module_runs", ["user_id"], name: "index_module_runs_on_user_id", using: :btree - create_table "module_targets", force: true do |t| + create_table "module_targets", force: :cascade do |t| t.integer "detail_id" t.integer "index" t.text "name" @@ -441,7 +441,7 @@ add_index "module_targets", ["detail_id"], name: "index_module_targets_on_detail_id", using: :btree - create_table "nexpose_consoles", force: true do |t| + create_table "nexpose_consoles", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "enabled", default: true @@ -457,7 +457,7 @@ t.text "name" end - create_table "notes", force: true do |t| + create_table "notes", force: :cascade do |t| t.datetime "created_at" t.string "ntype", limit: 512 t.integer "workspace_id", default: 1, null: false @@ -473,7 +473,7 @@ add_index "notes", ["ntype"], name: "index_notes_on_ntype", using: :btree add_index "notes", ["vuln_id"], name: "index_notes_on_vuln_id", using: :btree - create_table "profiles", force: true do |t| + create_table "profiles", force: :cascade do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false t.boolean "active", default: true @@ -482,7 +482,7 @@ t.binary "settings" end - create_table "refs", force: true do |t| + create_table "refs", force: :cascade do |t| t.integer "ref_id" t.datetime "created_at" t.string "name", limit: 512 @@ -491,19 +491,19 @@ add_index "refs", ["name"], name: "index_refs_on_name", using: :btree - create_table "report_templates", force: true do |t| + create_table "report_templates", force: :cascade do |t| t.integer "workspace_id", default: 1, null: false - t.string "created_by" + t.string "created_by", limit: 255 t.string "path", limit: 1024 t.text "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "reports", force: true do |t| + create_table "reports", force: :cascade do |t| t.integer "workspace_id", default: 1, null: false - t.string "created_by" - t.string "rtype" + t.string "created_by", limit: 255 + t.string "rtype", limit: 255 t.string "path", limit: 1024 t.text "options" t.datetime "created_at", null: false @@ -513,19 +513,19 @@ t.string "name", limit: 63 end - create_table "routes", force: true do |t| + create_table "routes", force: :cascade do |t| t.integer "session_id" - t.string "subnet" - t.string "netmask" + t.string "subnet", limit: 255 + t.string "netmask", limit: 255 end - create_table "services", force: true do |t| + create_table "services", force: :cascade do |t| t.integer "host_id" t.datetime "created_at" - t.integer "port", null: false - t.string "proto", limit: 16, null: false - t.string "state" - t.string "name" + t.integer "port", null: false + t.string "proto", limit: 16, null: false + t.string "state", limit: 255 + t.string "name", limit: 255 t.datetime "updated_at" t.text "info" end @@ -536,28 +536,28 @@ add_index "services", ["proto"], name: "index_services_on_proto", using: :btree add_index "services", ["state"], name: "index_services_on_state", using: :btree - create_table "session_events", force: true do |t| + create_table "session_events", force: :cascade do |t| t.integer "session_id" - t.string "etype" + t.string "etype", limit: 255 t.binary "command" t.binary "output" - t.string "remote_path" - t.string "local_path" + t.string "remote_path", limit: 255 + t.string "local_path", limit: 255 t.datetime "created_at" end - create_table "sessions", force: true do |t| + create_table "sessions", force: :cascade do |t| t.integer "host_id" - t.string "stype" - t.string "via_exploit" - t.string "via_payload" - t.string "desc" + t.string "stype", limit: 255 + t.string "via_exploit", limit: 255 + t.string "via_payload", limit: 255 + t.string "desc", limit: 255 t.integer "port" - t.string "platform" + t.string "platform", limit: 255 t.text "datastore" - t.datetime "opened_at", null: false + t.datetime "opened_at", null: false t.datetime "closed_at" - t.string "close_reason" + t.string "close_reason", limit: 255 t.integer "local_id" t.datetime "last_seen" t.integer "module_run_id" @@ -565,7 +565,7 @@ add_index "sessions", ["module_run_id"], name: "index_sessions_on_module_run_id", using: :btree - create_table "tags", force: true do |t| + create_table "tags", force: :cascade do |t| t.integer "user_id" t.string "name", limit: 1024 t.text "desc" @@ -576,42 +576,42 @@ t.datetime "updated_at", null: false end - create_table "task_creds", force: true do |t| + create_table "task_creds", force: :cascade do |t| t.integer "task_id", null: false t.integer "cred_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "task_hosts", force: true do |t| + create_table "task_hosts", force: :cascade do |t| t.integer "task_id", null: false t.integer "host_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "task_services", force: true do |t| + create_table "task_services", force: :cascade do |t| t.integer "task_id", null: false t.integer "service_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "task_sessions", force: true do |t| + create_table "task_sessions", force: :cascade do |t| t.integer "task_id", null: false t.integer "session_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false end - create_table "tasks", force: true do |t| + create_table "tasks", force: :cascade do |t| t.integer "workspace_id", default: 1, null: false - t.string "created_by" - t.string "module" + t.string "created_by", limit: 255 + t.string "module", limit: 255 t.datetime "completed_at" t.string "path", limit: 1024 - t.string "info" - t.string "description" + t.string "info", limit: 255 + t.string "description", limit: 255 t.integer "progress" t.text "options" t.text "error" @@ -622,44 +622,44 @@ t.binary "settings" end - create_table "users", force: true do |t| - t.string "username" - t.string "crypted_password" - t.string "password_salt" - t.string "persistence_token" + create_table "users", force: :cascade do |t| + t.string "username", limit: 255 + t.string "crypted_password", limit: 255 + t.string "password_salt", limit: 255 + t.string "persistence_token", limit: 255 t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "fullname" - t.string "email" - t.string "phone" - t.string "company" + t.string "fullname", limit: 255 + t.string "email", limit: 255 + t.string "phone", limit: 255 + t.string "company", limit: 255 t.string "prefs", limit: 524288 t.boolean "admin", default: true, null: false end - create_table "vuln_attempts", force: true do |t| + create_table "vuln_attempts", force: :cascade do |t| t.integer "vuln_id" t.datetime "attempted_at" t.boolean "exploited" - t.string "fail_reason" - t.string "username" + t.string "fail_reason", limit: 255 + t.string "username", limit: 255 t.text "module" t.integer "session_id" t.integer "loot_id" t.text "fail_detail" end - create_table "vuln_details", force: true do |t| + create_table "vuln_details", force: :cascade do |t| t.integer "vuln_id" t.float "cvss_score" - t.string "cvss_vector" - t.string "title" + t.string "cvss_vector", limit: 255 + t.string "title", limit: 255 t.text "description" t.text "solution" t.binary "proof" t.integer "nx_console_id" t.integer "nx_device_id" - t.string "nx_vuln_id" + t.string "nx_vuln_id", limit: 255 t.float "nx_severity" t.float "nx_pci_severity" t.datetime "nx_published" @@ -668,17 +668,17 @@ t.text "nx_tags" t.text "nx_vuln_status" t.text "nx_proof_key" - t.string "src" + t.string "src", limit: 255 t.integer "nx_scan_id" t.datetime "nx_vulnerable_since" - t.string "nx_pci_compliance_status" + t.string "nx_pci_compliance_status", limit: 255 end - create_table "vulns", force: true do |t| + create_table "vulns", force: :cascade do |t| t.integer "host_id" t.integer "service_id" t.datetime "created_at" - t.string "name" + t.string "name", limit: 255 t.datetime "updated_at" t.string "info", limit: 65536 t.datetime "exploited_at" @@ -688,12 +688,12 @@ add_index "vulns", ["name"], name: "index_vulns_on_name", using: :btree - create_table "vulns_refs", force: true do |t| + create_table "vulns_refs", force: :cascade do |t| t.integer "ref_id" t.integer "vuln_id" end - create_table "web_forms", force: true do |t| + create_table "web_forms", force: :cascade do |t| t.integer "web_site_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -705,7 +705,7 @@ add_index "web_forms", ["path"], name: "index_web_forms_on_path", using: :btree - create_table "web_pages", force: true do |t| + create_table "web_pages", force: :cascade do |t| t.integer "web_site_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -725,7 +725,7 @@ add_index "web_pages", ["path"], name: "index_web_pages_on_path", using: :btree add_index "web_pages", ["query"], name: "index_web_pages_on_query", using: :btree - create_table "web_sites", force: true do |t| + create_table "web_sites", force: :cascade do |t| t.integer "service_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -738,7 +738,7 @@ add_index "web_sites", ["options"], name: "index_web_sites_on_options", using: :btree add_index "web_sites", ["vhost"], name: "index_web_sites_on_vhost", using: :btree - create_table "web_vulns", force: true do |t| + create_table "web_vulns", force: :cascade do |t| t.integer "web_site_id", null: false t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -755,7 +755,7 @@ t.text "blame" t.binary "request" t.binary "proof", null: false - t.string "owner" + t.string "owner", limit: 255 t.text "payload" end @@ -763,8 +763,8 @@ add_index "web_vulns", ["name"], name: "index_web_vulns_on_name", using: :btree add_index "web_vulns", ["path"], name: "index_web_vulns_on_path", using: :btree - create_table "wmap_requests", force: true do |t| - t.string "host" + create_table "wmap_requests", force: :cascade do |t| + t.string "host", limit: 255 t.inet "address" t.integer "port" t.integer "ssl" @@ -780,8 +780,8 @@ t.datetime "updated_at" end - create_table "wmap_targets", force: true do |t| - t.string "host" + create_table "wmap_targets", force: :cascade do |t| + t.string "host", limit: 255 t.inet "address" t.integer "port" t.integer "ssl" @@ -790,13 +790,13 @@ t.datetime "updated_at" end - create_table "workspace_members", id: false, force: true do |t| + create_table "workspace_members", id: false, force: :cascade do |t| t.integer "workspace_id", null: false t.integer "user_id", null: false end - create_table "workspaces", force: true do |t| - t.string "name" + create_table "workspaces", force: :cascade do |t| + t.string "name", limit: 255 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "boundary", limit: 4096 diff --git a/lib/bit-struct.rb b/lib/bit-struct.rb index 0ff621ce07814..f07e155d6e52d 100644 --- a/lib/bit-struct.rb +++ b/lib/bit-struct.rb @@ -2,10 +2,12 @@ # A Convenience to load all field classes and yaml handling. # XXX: Pretty certian this monkeypatch isn't required in Metasploit. -if "a"[0].kind_of? Fixnum - unless Fixnum.methods.include? :ord - class Fixnum - def ord; self; end +if "a"[0].is_a?(Integer) + unless Integer.methods.include? :ord + class Integer + def ord + self + end end end end diff --git a/lib/metasploit/framework/rails_version_constraint.rb b/lib/metasploit/framework/rails_version_constraint.rb index aa75929e45d32..1f24d53ba17ae 100644 --- a/lib/metasploit/framework/rails_version_constraint.rb +++ b/lib/metasploit/framework/rails_version_constraint.rb @@ -6,7 +6,7 @@ module RailsVersionConstraint # 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 = [ '>= 4.0.9', '< 4.1.0' ] + RAILS_VERSION = [ '>= 4.2.1' ] end end -end \ No newline at end of file +end diff --git a/lib/msf/core/db_manager/module_cache.rb b/lib/msf/core/db_manager/module_cache.rb index 5e23cb6d2c4ab..b068725429bfc 100644 --- a/lib/msf/core/db_manager/module_cache.rb +++ b/lib/msf/core/db_manager/module_cache.rb @@ -48,19 +48,19 @@ def module_to_details_hash(m) res[:description] = m.description.to_s.strip m.arch.map{ |x| - bits << [ :arch, { :name => x.to_s } ] + bits << [ :arch, { name: x.to_s } ] } m.platform.platforms.map{ |x| - bits << [ :platform, { :name => x.to_s.split('::').last.downcase } ] + bits << [ :platform, { name: x.to_s.split('::').last.downcase } ] } m.author.map{|x| - bits << [ :author, { :name => x.to_s } ] + bits << [ :author, { name: x.to_s } ] } m.references.map do |r| - bits << [ :ref, { :name => [r.ctx_id.to_s, r.ctx_val.to_s].join("-") } ] + bits << [ :ref, { name: [r.ctx_id.to_s, r.ctx_val.to_s].join("-") } ] end res[:privileged] = m.privileged? @@ -77,14 +77,14 @@ def module_to_details_hash(m) if(m.type == "exploit") m.targets.each_index do |i| - bits << [ :target, { :index => i, :name => m.targets[i].name.to_s } ] + bits << [ :target, { index: i, name: m.targets[i].name.to_s } ] if m.targets[i].platform m.targets[i].platform.platforms.each do |name| - bits << [ :platform, { :name => name.to_s.split('::').last.downcase } ] + bits << [ :platform, { name: name.to_s.split('::').last.downcase } ] end end if m.targets[i].arch - bits << [ :arch, { :name => m.targets[i].arch.to_s } ] + bits << [ :arch, { name: m.targets[i].arch.to_s } ] end end @@ -97,14 +97,14 @@ def module_to_details_hash(m) m.class.mixins.each do |x| - bits << [ :mixin, { :name => x.to_s } ] + bits << [ :mixin, { name: x.to_s } ] end end if(m.type == "auxiliary") m.actions.each_index do |i| - bits << [ :action, { :name => m.actions[i].name.to_s } ] + bits << [ :action, { name: m.actions[i].name.to_s } ] end if (m.default_action) @@ -126,7 +126,7 @@ def module_to_details_hash(m) # # @return [void] def purge_all_module_details - return if not self.migrated + return unless self.migrated return if self.modules_caching ::ActiveRecord::Base.connection_pool.with_connection do @@ -141,10 +141,10 @@ def purge_all_module_details # @param refname [String] module reference name. # @return [void] def remove_module_details(mtype, refname) - return if not self.migrated + return unless self.migrated ActiveRecord::Base.connection_pool.with_connection do - Mdm::Module::Detail.where(:mtype => mtype, :refname => refname).destroy_all + Mdm::Module::Detail.where(mtype: mtype, refname: refname).destroy_all end end @@ -208,84 +208,91 @@ def search_modules(search_string) # intersection, so creating the where clause has to be delayed until all conditions can be or'd together and # passed to one call ot where. union_conditions = [] + join_sources = [] + + left_join = Arel::Nodes::OuterJoin + module_actions = Mdm::Module::Action.arel_table + module_authors = Mdm::Module::Author.arel_table + module_archs = Mdm::Module::Arch.arel_table + module_details = Mdm::Module::Detail.arel_table + module_platforms = Mdm::Module::Platform.arel_table + module_refs = Mdm::Module::Ref.arel_table + module_targets = Mdm::Module::Target.arel_table value_set_by_keyword.each do |keyword, value_set| case keyword - when 'author' - formatted_values = match_values(value_set) - - query = query.includes(:authors) - module_authors = Mdm::Module::Author.arel_table - union_conditions << module_authors[:email].matches_any(formatted_values) - union_conditions << module_authors[:name].matches_any(formatted_values) - when 'name' - formatted_values = match_values(value_set) - - module_details = Mdm::Module::Detail.arel_table - union_conditions << module_details[:fullname].matches_any(formatted_values) - union_conditions << module_details[:name].matches_any(formatted_values) - when 'os', 'platform' - formatted_values = match_values(value_set) - - query = query.includes(:platforms) - union_conditions << Mdm::Module::Platform.arel_table[:name].matches_any(formatted_values) - - query = query.includes(:targets) - union_conditions << Mdm::Module::Target.arel_table[:name].matches_any(formatted_values) - when 'text' - formatted_values = match_values(value_set) - - module_details = Mdm::Module::Detail.arel_table - union_conditions << module_details[:description].matches_any(formatted_values) - union_conditions << module_details[:fullname].matches_any(formatted_values) - union_conditions << module_details[:name].matches_any(formatted_values) - - query = query.includes(:actions) - union_conditions << Mdm::Module::Action.arel_table[:name].matches_any(formatted_values) - - query = query.includes(:archs) - union_conditions << Mdm::Module::Arch.arel_table[:name].matches_any(formatted_values) - - query = query.includes(:authors) - union_conditions << Mdm::Module::Author.arel_table[:name].matches_any(formatted_values) - - query = query.includes(:platforms) - union_conditions << Mdm::Module::Platform.arel_table[:name].matches_any(formatted_values) - - query = query.includes(:refs) - union_conditions << Mdm::Module::Ref.arel_table[:name].matches_any(formatted_values) - - query = query.includes(:targets) - union_conditions << Mdm::Module::Target.arel_table[:name].matches_any(formatted_values) - when 'type' - formatted_values = match_values(value_set) - union_conditions << Mdm::Module::Detail.arel_table[:mtype].matches_any(formatted_values) - when 'app' - formatted_values = value_set.collect { |value| - formatted_value = 'aggressive' - - if value == 'client' - formatted_value = 'passive' - end - - formatted_value - } - - union_conditions << Mdm::Module::Detail.arel_table[:stance].eq_any(formatted_values) - when 'ref' - formatted_values = match_values(value_set) - - query = query.includes(:refs) - union_conditions << Mdm::Module::Ref.arel_table[:name].matches_any(formatted_values) - when 'cve', 'bid', 'osvdb', 'edb' - formatted_values = value_set.collect { |value| - prefix = keyword.upcase - - "#{prefix}-%#{value}%" - } - - query = query.includes(:refs) - union_conditions << Mdm::Module::Ref.arel_table[:name].matches_any(formatted_values) + when 'author' + formatted_values = match_values(value_set) + + join_sources << module_details.join(module_authors, left_join).on(module_authors[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_authors[:email].matches_any(formatted_values) + union_conditions << module_authors[:name].matches_any(formatted_values) + when 'name' + formatted_values = match_values(value_set) + + union_conditions << module_details[:fullname].matches_any(formatted_values) + union_conditions << module_details[:name].matches_any(formatted_values) + when 'os', 'platform' + formatted_values = match_values(value_set) + + join_sources << module_details.join(module_platforms, left_join).on(module_platforms[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_platforms[:name].matches_any(formatted_values) + + join_sources << module_details.join(module_targets, left_join).on(module_targets[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_targets[:name].matches_any(formatted_values) + when 'text' + formatted_values = match_values(value_set) + + union_conditions << module_details[:description].matches_any(formatted_values) + union_conditions << module_details[:fullname].matches_any(formatted_values) + union_conditions << module_details[:name].matches_any(formatted_values) + + join_sources << module_details.join(module_actions, left_join).on(module_actions[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_actions[:name].matches_any(formatted_values) + + join_sources << module_details.join(module_archs, left_join).on(module_archs[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_archs[:name].matches_any(formatted_values) + + join_sources << module_details.join(module_authors, left_join).on(module_authors[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_authors[:name].matches_any(formatted_values) + + join_sources << module_details.join(module_platforms, left_join).on(module_platforms[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_platforms[:name].matches_any(formatted_values) + + join_sources << module_details.join(module_refs, left_join).on(module_refs[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_refs[:name].matches_any(formatted_values) + + join_sources << module_details.join(module_targets, left_join).on(module_targets[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_targets[:name].matches_any(formatted_values) + when 'type' + formatted_values = match_values(value_set) + union_conditions << module_details[:mtype].matches_any(formatted_values) + when 'app' + formatted_values = value_set.collect { |value| + formatted_value = 'aggressive' + + if value == 'client' + formatted_value = 'passive' + end + + formatted_value + } + + union_conditions << module_details[:stance].eq_any(formatted_values) + when 'ref' + formatted_values = match_values(value_set) + + join_sources << module_details.join(module_refs, left_join).on(module_refs[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_refs[:name].matches_any(formatted_values) + when 'cve', 'bid', 'osvdb', 'edb' + formatted_values = value_set.collect { |value| + prefix = keyword.upcase + + "#{prefix}-%#{value}%" + } + + join_sources << module_details.join(module_refs, left_join).on(module_refs[:detail_id].eq(module_details[:id])).join_sources + union_conditions << module_refs[:name].matches_any(formatted_values) end end @@ -293,7 +300,7 @@ def search_modules(search_string) union.or(condition) } - query = query.where(unioned_conditions).to_a.uniq { |m| m.fullname } + query = query.joins(join_sources).where(unioned_conditions).to_a.uniq { |m| m.fullname } end query @@ -307,7 +314,7 @@ def search_modules(search_string) # # @return [void] def update_all_module_details - return if not self.migrated + return unless self.migrated return if self.modules_caching self.framework.cache_thread = Thread.current @@ -358,7 +365,7 @@ def update_all_module_details mt[1].keys.sort.each do |mn| next if skip_reference_name_set.include? mn obj = mt[1].create(mn) - next if not obj + next unless obj begin update_module_details(obj) rescue ::Exception @@ -383,7 +390,7 @@ def update_all_module_details # Mdm::Module::Detail. # @return [void] def update_module_details(module_instance) - return if not self.migrated + return unless self.migrated ActiveRecord::Base.connection_pool.with_connection do info = module_to_details_hash(module_instance) @@ -394,18 +401,18 @@ def update_module_details(module_instance) otype, vals = args case otype - when :action - module_detail.add_action(vals[:name]) - when :arch - module_detail.add_arch(vals[:name]) - when :author - module_detail.add_author(vals[:name], vals[:email]) - when :platform - module_detail.add_platform(vals[:name]) - when :ref - module_detail.add_ref(vals[:name]) - when :target - module_detail.add_target(vals[:index], vals[:name]) + when :action + module_detail.add_action(vals[:name]) + when :arch + module_detail.add_arch(vals[:name]) + when :author + module_detail.add_author(vals[:name], vals[:email]) + when :platform + module_detail.add_platform(vals[:name]) + when :ref + module_detail.add_ref(vals[:name]) + when :target + module_detail.add_target(vals[:index], vals[:name]) end end @@ -413,4 +420,4 @@ def update_module_details(module_instance) module_detail.save! end end -end \ No newline at end of file +end diff --git a/lib/msf/ui/console/command_dispatcher/db.rb b/lib/msf/ui/console/command_dispatcher/db.rb index 63cccccf7069a..ce302cb7eae95 100644 --- a/lib/msf/ui/console/command_dispatcher/db.rb +++ b/lib/msf/ui/console/command_dispatcher/db.rb @@ -1017,21 +1017,37 @@ def creds_search(*args) tbl = Rex::Ui::Text::Table.new(tbl_opts) - ::ActiveRecord::Base.connection_pool.with_connection { - query = Metasploit::Credential::Core.where( workspace_id: framework.db.workspace ) - query = query.includes(:private, :public, :logins) - query = query.includes(logins: [ :service, { service: :host } ]) + ::ActiveRecord::Base.connection_pool.with_connection do + # query = Metasploit::Credential::Core.where( workspace_id: framework.db.workspace ) + # query = query.includes(:private, :public, :logins) + # query = query.includes(logins: [ :service, { service: :host } ]) + join_sources = [] + left_join = Arel::Nodes::OuterJoin + credential_cores = Metasploit::Credential::Core.arel_table + credential_logins = Metasploit::Credential::Login.arel_table + credential_publics = Metasploit::Credential::Public.arel_table + credential_privates = Metasploit::Credential::Private.arel_table + hosts = Mdm::Host.arel_table + services = Mdm::Service.arel_table + + join_sources << credential_cores.join(credential_privates, left_join).on(credential_cores[:private_id].eq(credential_privates[:id])).join_sources + join_sources << credential_cores.join(credential_publics, left_join).on(credential_cores[:public_id].eq(credential_publics[:id])).join_sources + join_sources << credential_cores.join(credential_logins, left_join).on(credential_cores[:id].eq(credential_logins[:core_id])).join_sources + join_sources << credential_logins.join(services, left_join).on(credential_logins[:service_id].eq(services[:id])).join_sources + join_sources << services.join(hosts, left_join).on(services[:host_id].eq(hosts[:id])).join_sources + + query = Metasploit::Credential::Core.joins(join_sources).where( workspace_id: framework.db.workspace ) if type.present? query = query.where(metasploit_credential_privates: { type: type }) end if svcs.present? - query = query.where(Mdm::Service[:name].in(svcs)) + query = query.where(services[:name].in(svcs)) end if ports.present? - query = query.where(Mdm::Service[:port].in(ports)) + query = query.where(services[:port].in(ports)) end if user.present? @@ -1117,7 +1133,7 @@ def creds_search(*args) # of hosts to go into RHOSTS. set_rhosts_from_addrs(rhosts.uniq) if set_rhosts print_status("Deleted #{delete_count} creds") if delete_count > 0 - } + end end # diff --git a/lib/net/ssh.rb b/lib/net/ssh.rb index 9a2432322985f..f3d88c3eca7db 100644 --- a/lib/net/ssh.rb +++ b/lib/net/ssh.rb @@ -1,11 +1,9 @@ -# -*- coding: binary -*- -require 'rex/socket' - # Make sure HOME is set, regardless of OS, so that File.expand_path works # as expected with tilde characters. -ENV['HOME'] ||= ENV['HOMEPATH'] ? "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" : "." +ENV['HOME'] ||= ENV['HOMEPATH'] ? "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" : Dir.pwd require 'logger' +require 'etc' require 'net/ssh/config' require 'net/ssh/errors' @@ -13,8 +11,7 @@ require 'net/ssh/transport/session' require 'net/ssh/authentication/session' require 'net/ssh/connection/session' -require 'net/ssh/command_stream' -require 'net/ssh/utils' +require 'net/ssh/prompt' module Net @@ -44,21 +41,21 @@ module Net # # == X == "execute a command and capture the output" # - # Net::SSH.start("host", "user", :password => "password") do |ssh| + # Net::SSH.start("host", "user", password: "password") do |ssh| # result = ssh.exec!("ls -l") # puts result # end # # == X == "forward connections on a local port to a remote host" # - # Net::SSH.start("host", "user", :password => "password") do |ssh| + # Net::SSH.start("host", "user", password: "password") do |ssh| # ssh.forward.local(1234, "www.google.com", 80) # ssh.loop { true } # end # # == X == "forward connections on a remote port to the local host" # - # Net::SSH.start("host", "user", :password => "password") do |ssh| + # Net::SSH.start("host", "user", password: "password") do |ssh| # ssh.forward.remote(80, "www.google.com", 1234) # ssh.loop { true } # end @@ -66,14 +63,16 @@ module SSH # This is the set of options that Net::SSH.start recognizes. See # Net::SSH.start for a description of each option. VALID_OPTIONS = [ - :auth_methods, :compression, :compression_level, :config, :encryption, - :forward_agent, :hmac, :host_key, :kex, :keys, :key_data, :languages, - :logger, :paranoid, :password, :port, :proxy, :rekey_blocks_limit, - :rekey_limit, :rekey_packet_limit, :timeout, :verbose, - :global_known_hosts_file, :user_known_hosts_file, :host_key_alias, - :host_name, :user, :properties, :passphrase, :msframework, :msfmodule, - :record_auth_info, :skip_private_keys, :accepted_key_callback, :disable_agent, - :proxies + :auth_methods, :bind_address, :compression, :compression_level, :config, + :encryption, :forward_agent, :hmac, :host_key, :remote_user, + :keepalive, :keepalive_interval, :keepalive_maxcount, :kex, :keys, :key_data, + :languages, :logger, :paranoid, :password, :port, :proxy, + :rekey_blocks_limit,:rekey_limit, :rekey_packet_limit, :timeout, :verbose, + :known_hosts, :global_known_hosts_file, :user_known_hosts_file, :host_key_alias, + :host_name, :user, :properties, :passphrase, :keys_only, :max_pkt_size, + :max_win_size, :send_env, :use_agent, :number_of_password_prompts, + :append_all_supported_algorithms, :non_interactive, :password_prompt, + :agent_socket_factory, :minimum_dh_bits, :verify_host_key ] # The standard means of starting a new SSH connection. When used with a @@ -105,6 +104,9 @@ module SSH # This method accepts the following options (all are optional): # # * :auth_methods => an array of authentication methods to try + # * :bind_address => the IP address on the connecting machine to use in + # establishing connection. (:bind_address is discarded if :proxy + # is set.) # * :compression => the compression algorithm to use, or +true+ to use # whatever is supported. # * :compression_level => the compression level to use when sending data @@ -115,9 +117,11 @@ module SSH # * :encryption => the encryption cipher (or ciphers) to use # * :forward_agent => set to true if you want the SSH agent connection to # be forwarded + # * :known_hosts => a custom object holding known hosts records. + # It must implement #search_for and add in a similiar manner as KnownHosts. # * :global_known_hosts_file => the location of the global known hosts # file. Set to an array if you want to specify multiple global known - # hosts files. Defaults to %w(/etc/ssh/known_hosts /etc/ssh/known_hosts2). + # hosts files. Defaults to %w(/etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2). # * :hmac => the hmac algorithm (or algorithms) to use # * :host_key => the host key algorithm (or algorithms) to use # * :host_key_alias => the host name to use when looking up or adding a @@ -127,14 +131,33 @@ module SSH # specified in an SSH configuration file. It lets you specify an # "alias", similarly to adding an entry in /etc/hosts but without needing # to modify /etc/hosts. + # * :keepalive => set to +true+ to send a keepalive packet to the SSH server + # when there's no traffic between the SSH server and Net::SSH client for + # the keepalive_interval seconds. Defaults to +false+. + # * :keepalive_interval => the interval seconds for keepalive. + # Defaults to +300+ seconds. + # * :keepalive_maxcount => the maximun number of keepalive packet miss allowed. + # Defaults to 3 # * :kex => the key exchange algorithm (or algorithms) to use # * :keys => an array of file names of private keys to use for publickey # and hostbased authentication # * :key_data => an array of strings, with each element of the array being # a raw private key in PEM format. + # * :keys_only => set to +true+ to use only private keys from +keys+ and + # +key_data+ parameters, even if ssh-agent offers more identities. This + # option is intended for situations where ssh-agent offers many different + # identites. # * :logger => the logger instance to use when logging - # * :paranoid => either true, false, or :very, specifying how strict - # host-key verification should be + # * :max_pkt_size => maximum size we tell the other side that is supported per + # packet. Default is 0x8000 (32768 bytes). Increase to 0x10000 (65536 bytes) + # for better performance if your SSH server supports it (most do). + # * :max_win_size => maximum size we tell the other side that is supported for + # the window. + # * :non_interactive => set to true if your app is non interactive and prefers + # authentication failure vs password prompt. Non-interactive applications + # should set it to true to prefer failing a password/etc auth methods vs. + # asking for password. + # * :paranoid => deprecated alias for :verify_host_key # * :passphrase => the passphrase to use when loading a private key (default # is +nil+, for no passphrase) # * :password => the password to use to login @@ -145,35 +168,63 @@ module SSH # * :rekey_blocks_limit => the max number of blocks to process before rekeying # * :rekey_limit => the max number of bytes to process before rekeying # * :rekey_packet_limit => the max number of packets to process before rekeying + # * :send_env => an array of local environment variable names to export to the + # remote environment. Names may be given as String or Regexp. # * :timeout => how long to wait for the initial connection to be made # * :user => the user name to log in as; this overrides the +user+ # parameter, and is primarily only useful when provided via an SSH # configuration file. + # * :remote_user => used for substitution into the '%r' part of a ProxyCommand # * :user_known_hosts_file => the location of the user known hosts file. # Set to an array to specify multiple user known hosts files. # Defaults to %w(~/.ssh/known_hosts ~/.ssh/known_hosts2). + # * :use_agent => Set false to disable the use of ssh-agent. Defaults to + # true # * :verbose => how verbose to be (Logger verbosity constants, Logger::DEBUG # is very verbose, Logger::FATAL is all but silent). Logger::FATAL is the # default. The symbols :debug, :info, :warn, :error, and :fatal are also # supported and are translated to the corresponding Logger constant. - def self.start(host, user, options={}, &block) + # * :append_all_supported_algorithms => set to +true+ to append all supported + # algorithms by net-ssh. Was the default behaviour until 2.10 + # * :number_of_password_prompts => Number of prompts for the password + # authentication method defaults to 3 set to 0 to disable prompt for + # password auth method + # * :password_prompt => a custom prompt object with ask method. See Net::SSH::Prompt + # + # * :agent_socket_factory => enables the user to pass a lambda/block that will serve as the socket factory + # Net::SSH::start(user,host,agent_socket_factory: ->{ UNIXSocket.open('/foo/bar') }) + # example: ->{ UNIXSocket.open('/foo/bar')} + # * :verify_host_key => either false, true, :very, or :secure specifying how + # strict host-key verification should be (in increasing order here). + # You can also provide an own Object which responds to +verify+. The argument + # given to +verify+ is a hash consisting of the +:key+, the +:key_blob+, + # the +:fingerprint+ and the +:session+. Returning true accepts the host key, + # returning false declines it and closes the connection. + # + # If +user+ parameter is nil it defaults to USER from ssh_config, or + # local username + def self.start(host, user=nil, options={}, &block) invalid_options = options.keys - VALID_OPTIONS if invalid_options.any? raise ArgumentError, "invalid option(s): #{invalid_options.join(', ')}" end + assign_defaults(options) + _sanitize_options(options) + options[:user] = user if user options = configuration_for(host, options.fetch(:config, true)).merge(options) host = options.fetch(:host_name, host) - if !options.key?(:logger) - options[:logger] = Logger.new(STDERR) - options[:logger].level = Logger::FATAL + if options[:non_interactive] + options[:number_of_password_prompts] = 0 end + _support_deprecated_option_paranoid(options) + if options[:verbose] options[:logger].level = case options[:verbose] - when Fixnum then options[:verbose] + when Integer then options[:verbose] when :debug then Logger::DEBUG when :info then Logger::INFO when :warn then Logger::WARN @@ -186,26 +237,21 @@ def self.start(host, user, options={}, &block) transport = Transport::Session.new(host, options) auth = Authentication::Session.new(transport, options) - user = options.fetch(:user, user) + user = options.fetch(:user, user) || Etc.getlogin if auth.authenticate("ssh-connection", user, options[:password]) connection = Connection::Session.new(transport, options) - connection.auth_info = auth.auth_info - - # Tell MSF not to auto-close this socket anymore... - # This allows the transport socket to surive with the session. - if options[:msfmodule] - options[:msfmodule].remove_socket(transport.socket) - end - if block_given? - yield connection - connection.close + begin + yield connection + ensure + connection.close unless connection.closed? + end else return connection end else transport.close - raise AuthenticationFailed, user + raise AuthenticationFailed, "Authentication failed for user #{user}@#{host}" end end @@ -218,15 +264,54 @@ def self.start(host, user, options={}, &block) # to read. # # See Net::SSH::Config for the full description of all supported options. - def self.configuration_for(host, use_ssh_config=true) + def self.configuration_for(host, use_ssh_config) files = case use_ssh_config - when true then Net::SSH::Config.default_files + when true then Net::SSH::Config.expandable_default_files when false, nil then return {} else Array(use_ssh_config) end Net::SSH::Config.for(host, files) end + + def self.assign_defaults(options) + if !options[:logger] + options[:logger] = Logger.new(STDERR) + options[:logger].level = Logger::FATAL + end + + options[:password_prompt] ||= Prompt.default(options) + + [:password, :passphrase].each do |key| + options.delete(key) if options.key?(key) && options[key].nil? + end + end + + def self._sanitize_options(options) + invalid_option_values = [nil,[nil]] + unless (options.values & invalid_option_values).empty? + nil_options = options.select { |_k,v| invalid_option_values.include?(v) }.map(&:first) + Kernel.warn "#{caller_locations(2, 1)[0]}: Passing nil, or [nil] to Net::SSH.start is deprecated for keys: #{nil_options.join(', ')}" + end + end + private_class_method :_sanitize_options + + def self._support_deprecated_option_paranoid(options) + if options.key?(:paranoid) + Kernel.warn( + ":paranoid is deprecated, please use :verify_host_key. Supported " \ + "values are exactly the same, only the name of the option has changed." + ) + if options.key?(:verify_host_key) + Kernel.warn( + "Both :paranoid and :verify_host_key were specified. " \ + ":verify_host_key takes precedence, :paranoid will be ignored." + ) + else + options[:verify_host_key] = options.delete(:paranoid) + end + end + end + private_class_method :_support_deprecated_option_paranoid end end - diff --git a/lib/net/ssh/CHANGELOG.rdoc b/lib/net/ssh/CHANGELOG.rdoc deleted file mode 100644 index 95fb02f9b7d70..0000000000000 --- a/lib/net/ssh/CHANGELOG.rdoc +++ /dev/null @@ -1,132 +0,0 @@ -=== (unreleased) - -* Use unbuffered reads when negotiating the protocol version [Steven Hazel] - - -=== 2.0.11 / 24 Feb 2009 - -* Add :key_data option for specifying raw private keys in PEM format [Alex Holems, Andrew Babkin] - - -=== 2.0.10 / 4 Feb 2009 - -* Added Net::SSH.configuration_for to make it easier to query the SSH configuration file(s) [Jamis Buck] - - -=== 2.0.9 / 1 Feb 2009 - -* Specifying non-nil user argument overrides user in .ssh/config [Jamis Buck] - -* Ignore requests for non-existent channels (workaround ssh server bug) [Jamis Buck] - -* Add terminate! method for hard shutdown scenarios [Jamis Buck] - -* Revert to pre-2.0.7 key-loading behavior by default, but load private-key if public-key doesn't exist [Jamis Buck] - -* Make sure :passphrase option gets passed to key manager [Bob Cotton] - - -=== 2.0.8 / 29 December 2008 - -* Fix private key change from 2.0.7 so that keys are loaded just-in-time, avoiding unecessary prompts from encrypted keys. [Jamis Buck] - - -=== 2.0.7 / 29 December 2008 - -* Make key manager use private keys instead of requiring public key to exist [arilerner@mac.com] - -* Fix failing tests [arilerner@mac.com] - -* Don't include pageant when running under JRuby [Angel N. Sciortino] - - -=== 2.0.6 / 6 December 2008 - -* Update the Manifest file so that the gem includes all necessary files [Jamis Buck] - - -=== 2.0.5 / 6 December 2008 - -* Make the Pageant interface comply with more of the Socket interface to avoid related errors [Jamis Buck] - -* Don't busy-wait on session close for remaining channels to close [Will Bryant] - -* Ruby 1.9 compatibility [Jamis Buck] - -* Fix Cipher#final to correctly flag a need for a cipher reset [Jamis Buck] - - -=== 2.0.4 / 27 Aug 2008 - -* Added Connection::Session#closed? and Transport::Session#closed? [Jamis Buck] - -* Numeric host names in .ssh/config are now parsed correct [Yanko Ivanov] - -* Make sure the error raised when a public key file is malformed is more informative than a MethodMissing error [Jamis Buck] - -* Cipher#reset is now called after Cipher#final, with the last n bytes used as the next initialization vector [Jamis Buck] - - -=== 2.0.3 / 27 Jun 2008 - -* Make Net::SSH::Version comparable [Brian Candler] - -* Fix errors in port forwarding when a channel could not be opened due to a typo in the exception name [Matthew Todd] - -* Use #chomp instead of #strip when cleaning the version string reported by the remote host, so that trailing whitespace is preserved (this is to play nice with servers like Mocana SSH) [Timo Gatsonides] - -* Correctly parse ssh_config entries with eq-sign delimiters [Jamis Buck] - -* Ignore malformed ssh_config entries [Jamis Buck] - -=== 2.0.2 / 29 May 2008 - -* Make sure the agent client understands both RSA "identities answers" [Jamis Buck] - -* Fixed key truncation bug that caused hmacs other than SHA1 to fail with "corrupt hmac" errors [Jamis Buck] - -* Fix detection and loading of public keys when the keys don't actually exist [David Dollar] - - -=== 2.0.1 / 5 May 2008 - -* Teach Net::SSH about a handful of default key names [Jamis Buck] - - -=== 2.0.0 / 1 May 2008 - -* Allow the :verbose argument to accept symbols (:debug, etc.) as well as Logger level constants (Logger::DEBUG, etc.) [Jamis Buck] - - -=== 2.0 Preview Release 4 (1.99.3) / 19 Apr 2008 - -* Make sure HOME is set to something sane, even on OS's that don't set it by default [Jamis Buck] - -* Add a :passphrase option to specify the passphrase to use with private keys [Francis Sullivan] - -* Open a new auth agent connection for every auth-agent channel request [Jamis Buck] - - -=== 2.0 Preview Release 3 (1.99.2) / 10 Apr 2008 - -* Session properties [Jamis Buck] - -* Make channel open failure work with a callback so that failures can be handled similarly to successes [Jamis Buck] - - -=== 2.0 Preview Release 2 (1.99.1) / 22 Mar 2008 - -* Partial support for ~/.ssh/config (and related) SSH configuration files [Daniel J. Berger, Jamis Buck] - -* Added Net::SSH::Test to facilitate testing complex SSH state machines [Jamis Buck] - -* Reworked Net::SSH::Prompt to use conditionally-selected modules [Jamis Buck, suggested by James Rosen] - -* Added Channel#eof? and Channel#eof! [Jamis Buck] - -* Fixed bug in strict host key verifier on cache miss [Mike Timm] - - -=== 2.0 Preview Release 1 (1.99.0) / 21 Aug 2007 - -* First preview release of Net::SSH v2 diff --git a/lib/net/ssh/README.rdoc b/lib/net/ssh/README.rdoc deleted file mode 100644 index 3b7c16553b54f..0000000000000 --- a/lib/net/ssh/README.rdoc +++ /dev/null @@ -1,110 +0,0 @@ -= Net::SSH - -* http://net-ssh.rubyforge.org/ssh - -== DESCRIPTION: - -Net::SSH is a pure-Ruby implementation of the SSH2 client protocol. It allows you to write programs that invoke and interact with processes on remote servers, via SSH2. - -== FEATURES: - -* Execute processes on remote servers and capture their output -* Run multiple processes in parallel over a single SSH connection -* Support for SSH subsystems -* Forward local and remote ports via an SSH connection - -== SYNOPSIS: - -In a nutshell: - - require 'net/ssh' - - Net::SSH.start('host', 'user', :password => "password") do |ssh| - # capture all stderr and stdout output from a remote process - output = ssh.exec!("hostname") - - # capture only stdout matching a particular pattern - stdout = "" - ssh.exec!("ls -l /home/jamis") do |channel, stream, data| - stdout << data if stream == :stdout - end - puts stdout - - # run multiple processes in parallel to completion - ssh.exec "sed ..." - ssh.exec "awk ..." - ssh.exec "rm -rf ..." - ssh.loop - - # open a new channel and configure a minimal set of callbacks, then run - # the event loop until the channel finishes (closes) - channel = ssh.open_channel do |ch| - ch.exec "/usr/local/bin/ruby /path/to/file.rb" do |ch, success| - raise "could not execute command" unless success - - # "on_data" is called when the process writes something to stdout - ch.on_data do |c, data| - $STDOUT.print data - end - - # "on_extended_data" is called when the process writes something to stderr - ch.on_extended_data do |c, type, data| - $STDERR.print data - end - - ch.on_close { puts "done!" } - end - end - - channel.wait - - # forward connections on local port 1234 to port 80 of www.capify.org - ssh.forward.local(1234, "www.capify.org", 80) - ssh.loop { true } - end - -See Net::SSH for more documentation, and links to further information. - -== REQUIREMENTS: - -The only requirement you might be missing is the OpenSSL bindings for Ruby. These are built by default on most platforms, but you can verify that they're built and installed on your system by running the following command line: - - ruby -ropenssl -e 'puts OpenSSL::OPENSSL_VERSION' - -If that spits out something like "OpenSSL 0.9.8g 19 Oct 2007", then you're set. If you get an error, then you'll need to see about rebuilding ruby with OpenSSL support, or (if your platform supports it) installing the OpenSSL bindings separately. - -Additionally: if you are going to be having Net::SSH prompt you for things like passwords or certificate passphrases, you'll want to have either the Highline (recommended) or Termios (unix systems only) gem installed, so that the passwords don't echo in clear text. - -Lastly, if you want to run the tests or use any of the Rake tasks, you'll need: - -* Echoe (for the Rakefile) -* Mocha (for the tests) - -== INSTALL: - -* gem install net-ssh (might need sudo privileges) - -== LICENSE: - -(The MIT License) - -Copyright (c) 2008 Jamis Buck - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/lib/net/ssh/THANKS.rdoc b/lib/net/ssh/THANKS.rdoc deleted file mode 100644 index d060dce6dd71c..0000000000000 --- a/lib/net/ssh/THANKS.rdoc +++ /dev/null @@ -1,16 +0,0 @@ -Net::SSH was originally written by Jamis Buck . In -addition, the following individuals are gratefully acknowledged for their -contributions: - -GOTOU Yuuzou - * help and code related to OpenSSL - -Guillaume Marçais - * support for communicating with the the PuTTY "pageant" process - -Daniel Berger - * help getting unit tests in earlier Net::SSH versions to pass in Windows - * initial version of Net::SSH::Config provided inspiration and encouragement - -Chris Andrews and Lee Jensen - * support for ssh agent forwarding diff --git a/lib/net/ssh/authentication/agent.rb b/lib/net/ssh/authentication/agent.rb index 8193991437659..1085d9c3023e3 100644 --- a/lib/net/ssh/authentication/agent.rb +++ b/lib/net/ssh/authentication/agent.rb @@ -1,19 +1,16 @@ -# -*- coding: binary -*- require 'net/ssh/buffer' require 'net/ssh/errors' require 'net/ssh/loggable' + require 'net/ssh/transport/server_version' +require 'socket' +require 'rubygems' -# Disable pageant, as it uses DL in a non-1.9 compatible way -=begin -require 'net/ssh/authentication/pageant' if File::ALT_SEPARATOR && !(RUBY_PLATFORM =~ /java/) -=end +require 'net/ssh/authentication/pageant' if Gem.win_platform? && RUBY_PLATFORM != "java" module Net; module SSH; module Authentication - - # A trivial exception class for representing agent-specific errors. + # Class for representing agent-specific errors. class AgentError < Net::SSH::Exception; end - # An exception for indicating that the SSH agent is not available. class AgentNotAvailable < AgentError; end @@ -32,29 +29,40 @@ module Comment attr_accessor :comment end - SSH2_AGENT_REQUEST_VERSION = 1 - SSH2_AGENT_REQUEST_IDENTITIES = 11 - SSH2_AGENT_IDENTITIES_ANSWER = 12 - SSH2_AGENT_SIGN_REQUEST = 13 - SSH2_AGENT_SIGN_RESPONSE = 14 - SSH2_AGENT_FAILURE = 30 - SSH2_AGENT_VERSION_RESPONSE = 103 + SSH2_AGENT_REQUEST_VERSION = 1 + SSH2_AGENT_REQUEST_IDENTITIES = 11 + SSH2_AGENT_IDENTITIES_ANSWER = 12 + SSH2_AGENT_SIGN_REQUEST = 13 + SSH2_AGENT_SIGN_RESPONSE = 14 + SSH2_AGENT_ADD_IDENTITY = 17 + SSH2_AGENT_REMOVE_IDENTITY = 18 + SSH2_AGENT_REMOVE_ALL_IDENTITIES = 19 + SSH2_AGENT_ADD_ID_CONSTRAINED = 25 + SSH2_AGENT_FAILURE = 30 + SSH2_AGENT_VERSION_RESPONSE = 103 - SSH_COM_AGENT2_FAILURE = 102 + SSH_COM_AGENT2_FAILURE = 102 SSH_AGENT_REQUEST_RSA_IDENTITIES = 1 SSH_AGENT_RSA_IDENTITIES_ANSWER1 = 2 SSH_AGENT_RSA_IDENTITIES_ANSWER2 = 5 SSH_AGENT_FAILURE = 5 + SSH_AGENT_SUCCESS = 6 + + SSH_AGENT_CONSTRAIN_LIFETIME = 1 + SSH_AGENT_CONSTRAIN_CONFIRM = 2 + + SSH_AGENT_RSA_SHA2_256 = 0x02 + SSH_AGENT_RSA_SHA2_512 = 0x04 # The underlying socket being used to communicate with the SSH agent. attr_reader :socket # Instantiates a new agent object, connects to a running SSH agent, # negotiates the agent protocol version, and returns the agent object. - def self.connect(logger=nil) + def self.connect(logger=nil, agent_socket_factory = nil) agent = new(logger) - agent.connect! + agent.connect!(agent_socket_factory) agent.negotiate! agent end @@ -69,14 +77,21 @@ def initialize(logger=nil) # given by the attribute writers. If the agent on the other end of the # socket reports that it is an SSH2-compatible agent, this will fail # (it only supports the ssh-agent distributed by OpenSSH). - def connect! - begin - debug { "connecting to ssh-agent" } - @socket = agent_socket_factory.open(ENV['SSH_AUTH_SOCK']) - rescue - error { "could not connect to ssh-agent" } - raise AgentNotAvailable, $!.message - end + def connect!(agent_socket_factory = nil) + debug { "connecting to ssh-agent" } + @socket = + if agent_socket_factory + agent_socket_factory.call + elsif ENV['SSH_AUTH_SOCK'] && unix_socket_class + unix_socket_class.open(ENV['SSH_AUTH_SOCK']) + elsif Gem.win_platform? && RUBY_ENGINE != "jruby" + Pageant::Socket.open + else + raise AgentNotAvailable, "Agent not configured" + end + rescue StandardError => e + error { "could not connect to ssh-agent: #{e.message}" } + raise AgentNotAvailable, $!.message end # Attempts to negotiate the SSH agent protocol version. Raises an error @@ -85,10 +100,11 @@ def negotiate! # determine what type of agent we're communicating with type, body = send_and_wait(SSH2_AGENT_REQUEST_VERSION, :string, Transport::ServerVersion::PROTO_VERSION) - if type == SSH2_AGENT_VERSION_RESPONSE - raise NotImplementedError, "SSH2 agents are not yet supported" + raise AgentNotAvailable, "SSH2 agents are not yet supported" if type == SSH2_AGENT_VERSION_RESPONSE + if type == SSH2_AGENT_FAILURE + debug { "Unexpected response type==#{type}, this will be ignored" } elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2 - raise AgentError, "unknown response from agent: #{type}, #{body.to_s.inspect}" + raise AgentNotAvailable, "unknown response from agent: #{type}, #{body.to_s.inspect}" end end @@ -102,10 +118,16 @@ def identities identities = [] body.read_long.times do - key = Buffer.new(body.read_string).read_key - key.extend(Comment) - key.comment = body.read_string - identities.push key + key_str = body.read_string + comment_str = body.read_string + begin + key = Buffer.new(key_str).read_key + key.extend(Comment) + key.comment = comment_str + identities.push key + rescue NotImplementedError => e + error { "ignoring unimplemented key:#{e.message} #{comment_str}" } + end end return identities @@ -119,63 +141,119 @@ def close # Using the agent and the given public key, sign the given data. The # signature is returned in SSH2 format. - def sign(key, data) - type, reply = send_and_wait(SSH2_AGENT_SIGN_REQUEST, :string, Buffer.from(:key, key), :string, data, :long, 0) + def sign(key, data, flags = 0) + type, reply = send_and_wait(SSH2_AGENT_SIGN_REQUEST, :string, Buffer.from(:key, key), :string, data, :long, flags) - if agent_failed(type) - raise AgentError, "agent could not sign data with requested identity" - elsif type != SSH2_AGENT_SIGN_RESPONSE - raise AgentError, "bad authentication response #{type}" - end + raise AgentError, "agent could not sign data with requested identity" if agent_failed(type) + raise AgentError, "bad authentication response #{type}" if type != SSH2_AGENT_SIGN_RESPONSE return reply.read_string end + # Adds the private key with comment to the agent. + # If lifetime is given, the key will automatically be removed after lifetime + # seconds. + # If confirm is true, confirmation will be required for each agent signing + # operation. + def add_identity(priv_key, comment, lifetime: nil, confirm: false) + constraints = Buffer.new + if lifetime + constraints.write_byte(SSH_AGENT_CONSTRAIN_LIFETIME) + constraints.write_long(lifetime) + end + constraints.write_byte(SSH_AGENT_CONSTRAIN_CONFIRM) if confirm + + req_type = constraints.empty? ? SSH2_AGENT_ADD_IDENTITY : SSH2_AGENT_ADD_ID_CONSTRAINED + type, = send_and_wait(req_type, :string, priv_key.ssh_type, :raw, blob_for_add(priv_key), + :string, comment, :raw, constraints) + raise AgentError, "could not add identity to agent" if type != SSH_AGENT_SUCCESS + end + + # Removes key from the agent. + def remove_identity(key) + type, = send_and_wait(SSH2_AGENT_REMOVE_IDENTITY, :string, key.to_blob) + raise AgentError, "could not remove identity from agent" if type != SSH_AGENT_SUCCESS + end + + # Removes all identities from the agent. + def remove_all_identities + type, = send_and_wait(SSH2_AGENT_REMOVE_ALL_IDENTITIES) + raise AgentError, "could not remove all identity from agent" if type != SSH_AGENT_SUCCESS + end + private - # Returns the agent socket factory to use. - def agent_socket_factory - if File::ALT_SEPARATOR - Pageant::Socket - else - UNIXSocket - end - end + def unix_socket_class + defined?(UNIXSocket) && UNIXSocket + end - # Send a new packet of the given type, with the associated data. - def send_packet(type, *args) - buffer = Buffer.from(*args) - data = [buffer.length + 1, type.to_i, buffer.to_s].pack("NCA*") - debug { "sending agent request #{type} len #{buffer.length}" } - @socket.send data, 0 - end + # Send a new packet of the given type, with the associated data. + def send_packet(type, *args) + buffer = Buffer.from(*args) + data = [buffer.length + 1, type.to_i, buffer.to_s].pack("NCA*") + debug { "sending agent request #{type} len #{buffer.length}" } + @socket.send data, 0 + end - # Read the next packet from the agent. This will return a two-part - # tuple consisting of the packet type, and the packet's body (which - # is returned as a Net::SSH::Buffer). - def read_packet - buffer = Net::SSH::Buffer.new(@socket.read(4)) - buffer.append(@socket.read(buffer.read_long)) - type = buffer.read_byte - debug { "received agent packet #{type} len #{buffer.length-4}" } - return type, buffer - end + # Read the next packet from the agent. This will return a two-part + # tuple consisting of the packet type, and the packet's body (which + # is returned as a Net::SSH::Buffer). + def read_packet + buffer = Net::SSH::Buffer.new(@socket.read(4)) + buffer.append(@socket.read(buffer.read_long)) + type = buffer.read_byte + debug { "received agent packet #{type} len #{buffer.length-4}" } + return type, buffer + end - # Send the given packet and return the subsequent reply from the agent. - # (See #send_packet and #read_packet). - def send_and_wait(type, *args) - send_packet(type, *args) - read_packet - end + # Send the given packet and return the subsequent reply from the agent. + # (See #send_packet and #read_packet). + def send_and_wait(type, *args) + send_packet(type, *args) + read_packet + end - # Returns +true+ if the parameter indicates a "failure" response from - # the agent, and +false+ otherwise. - def agent_failed(type) - type == SSH_AGENT_FAILURE || + # Returns +true+ if the parameter indicates a "failure" response from + # the agent, and +false+ otherwise. + def agent_failed(type) + type == SSH_AGENT_FAILURE || type == SSH2_AGENT_FAILURE || type == SSH_COM_AGENT2_FAILURE + end + + def blob_for_add(priv_key) + # Ideally we'd have something like `to_private_blob` on the various key types, but the + # nuances with encoding (e.g. `n` and `e` are reversed for RSA keys) make this impractical. + case priv_key.ssh_type + when /^ssh-dss$/ + Net::SSH::Buffer.from(:bignum, priv_key.p, :bignum, priv_key.q, :bignum, priv_key.g, + :bignum, priv_key.pub_key, :bignum, priv_key.priv_key).to_s + when /^ssh-dss-cert-v01@openssh\.com$/ + Net::SSH::Buffer.from(:string, priv_key.to_blob, :bignum, priv_key.key.priv_key).to_s + when /^ecdsa\-sha2\-(\w*)$/ + curve_name = OpenSSL::PKey::EC::CurveNameAliasInv[priv_key.group.curve_name] + Net::SSH::Buffer.from(:string, curve_name, :mstring, priv_key.public_key.to_bn.to_s(2), + :bignum, priv_key.private_key).to_s + when /^ecdsa\-sha2\-(\w*)-cert-v01@openssh\.com$/ + Net::SSH::Buffer.from(:string, priv_key.to_blob, :bignum, priv_key.key.private_key).to_s + when /^ssh-ed25519$/ + Net::SSH::Buffer.from(:string, priv_key.public_key.verify_key.to_bytes, + :string, priv_key.sign_key.keypair_bytes).to_s + when /^ssh-ed25519-cert-v01@openssh\.com$/ + # Unlike the other certificate types, the public key is included after the certifiate. + Net::SSH::Buffer.from(:string, priv_key.to_blob, + :string, priv_key.key.public_key.verify_key.to_bytes, + :string, priv_key.key.sign_key.keypair_bytes).to_s + when /^ssh-rsa$/ + # `n` and `e` are reversed compared to the ordering in `OpenSSL::PKey::RSA#to_blob`. + Net::SSH::Buffer.from(:bignum, priv_key.n, :bignum, priv_key.e, :bignum, priv_key.d, + :bignum, priv_key.iqmp, :bignum, priv_key.p, :bignum, priv_key.q).to_s + when /^ssh-rsa-cert-v01@openssh\.com$/ + Net::SSH::Buffer.from(:string, priv_key.to_blob, :bignum, priv_key.key.d, + :bignum, priv_key.key.iqmp, :bignum, priv_key.key.p, + :bignum, priv_key.key.q).to_s end + end end end; end; end - diff --git a/lib/net/ssh/authentication/certificate.rb b/lib/net/ssh/authentication/certificate.rb new file mode 100644 index 0000000000000..33dbcf8e1e5e2 --- /dev/null +++ b/lib/net/ssh/authentication/certificate.rb @@ -0,0 +1,169 @@ +require 'securerandom' + +module Net; module SSH; module Authentication + # Class for representing an SSH certificate. + # + # http://cvsweb.openbsd.org/cgi-bin/cvsweb/~checkout~/src/usr.bin/ssh/PROTOCOL.certkeys?rev=1.10&content-type=text/plain + class Certificate + attr_accessor :nonce + attr_accessor :key + attr_accessor :serial + attr_accessor :type + attr_accessor :key_id + attr_accessor :valid_principals + attr_accessor :valid_after + attr_accessor :valid_before + attr_accessor :critical_options + attr_accessor :extensions + attr_accessor :reserved + attr_accessor :signature_key + attr_accessor :signature + + # Read a certificate blob associated with a key of the given type. + def self.read_certblob(buffer, type) + cert = Certificate.new + cert.nonce = buffer.read_string + cert.key = buffer.read_keyblob(type) + cert.serial = buffer.read_int64 + cert.type = type_symbol(buffer.read_long) + cert.key_id = buffer.read_string + cert.valid_principals = buffer.read_buffer.read_all(&:read_string) + cert.valid_after = Time.at(buffer.read_int64) + cert.valid_before = Time.at(buffer.read_int64) + cert.critical_options = read_options(buffer) + cert.extensions = read_options(buffer) + cert.reserved = buffer.read_string + cert.signature_key = buffer.read_buffer.read_key + cert.signature = buffer.read_string + cert + end + + def ssh_type + key.ssh_type + "-cert-v01@openssh.com" + end + + def ssh_signature_type + key.ssh_type + end + + # Serializes the certificate (and key). + def to_blob + Buffer.from( + :raw, to_blob_without_signature, + :string, signature + ).to_s + end + + def ssh_do_sign(data) + key.ssh_do_sign(data) + end + + def ssh_do_verify(sig, data) + key.ssh_do_verify(sig, data) + end + + def to_pem + key.to_pem + end + + def fingerprint + key.fingerprint + end + + # Signs the certificate with key. + def sign!(key, sign_nonce=nil) + # ssh-keygen uses 32 bytes of nonce. + self.nonce = sign_nonce || SecureRandom.random_bytes(32) + self.signature_key = key + self.signature = Net::SSH::Buffer.from( + :string, key.ssh_signature_type, + :mstring, key.ssh_do_sign(to_blob_without_signature) + ).to_s + self + end + + def sign(key, sign_nonce=nil) + cert = clone + cert.sign!(key, sign_nonce) + end + + # Checks whether the certificate's signature was signed by signature key. + def signature_valid? + buffer = Buffer.new(signature) + buffer.read_string # skip signature format + signature_key.ssh_do_verify(buffer.read_string, to_blob_without_signature) + end + + def self.read_options(buffer) + names = [] + options = buffer.read_buffer.read_all do |b| + name = b.read_string + names << name + data = b.read_string + data = Buffer.new(data).read_string unless data.empty? + [name, data] + end + + if names.sort != names + raise ArgumentError, "option/extension names must be in sorted order" + end + + Hash[options] + end + private_class_method :read_options + + def self.type_symbol(type) + types = {1 => :user, 2 => :host} + raise ArgumentError("unsupported type: #{type}") unless types.include?(type) + types.fetch(type) + end + private_class_method :type_symbol + + private + + def type_value(type) + types = {user: 1, host: 2} + raise ArgumentError("unsupported type: #{type}") unless types.include?(type) + types.fetch(type) + end + + def ssh_time(t) + # Times in certificates are represented as a uint64. + [[t.to_i, 0].max, 2<<64 - 1].min + end + + def to_blob_without_signature + Buffer.from( + :string, ssh_type, + :string, nonce, + :raw, key_without_type, + :int64, serial, + :long, type_value(type), + :string, key_id, + :string, valid_principals.inject(Buffer.new) { |acc, elem| acc.write_string(elem) }.to_s, + :int64, ssh_time(valid_after), + :int64, ssh_time(valid_before), + :string, options_to_blob(critical_options), + :string, options_to_blob(extensions), + :string, reserved, + :string, signature_key.to_blob + ).to_s + end + + def key_without_type + # key.to_blob gives us e.g. "ssh-rsa," but we just want "". + tmp = Buffer.new(key.to_blob) + tmp.read_string # skip the underlying key type + tmp.read + end + + def options_to_blob(options) + options.keys.sort.inject(Buffer.new) do |b, name| + b.write_string(name) + data = options.fetch(name) + data = Buffer.from(:string, data).to_s unless data.empty? + b.write_string(data) + end.to_s + end + end +end; end; end diff --git a/lib/net/ssh/authentication/constants.rb b/lib/net/ssh/authentication/constants.rb index 387c78fca730c..911b9ca44c4be 100644 --- a/lib/net/ssh/authentication/constants.rb +++ b/lib/net/ssh/authentication/constants.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH; module Authentication # Describes the constants used by the Net::SSH::Authentication components @@ -16,4 +15,4 @@ module Constants USERAUTH_METHOD_RANGE = 60..79 end -end; end; end +end; end; end \ No newline at end of file diff --git a/lib/net/ssh/authentication/ed25519.rb b/lib/net/ssh/authentication/ed25519.rb new file mode 100644 index 0000000000000..a92011a523a13 --- /dev/null +++ b/lib/net/ssh/authentication/ed25519.rb @@ -0,0 +1,160 @@ +gem 'rbnacl', '>= 3.2.0', '< 5.0' +gem 'bcrypt_pbkdf', '~> 1.0' unless RUBY_PLATFORM == "java" + +begin + require 'rbnacl/libsodium' +rescue LoadError # rubocop:disable Lint/HandleExceptions +end + +require 'rbnacl' +require 'rbnacl/signatures/ed25519/verify_key' +require 'rbnacl/signatures/ed25519/signing_key' + +require 'rbnacl/hash' + +require 'base64' + +require 'net/ssh/transport/cipher_factory' +require 'bcrypt_pbkdf' unless RUBY_PLATFORM == "java" + +module Net; module SSH; module Authentication +module ED25519 + class SigningKeyFromFile < RbNaCl::Signatures::Ed25519::SigningKey + def initialize(pk,sk) + @signing_key = sk + @verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new(pk) + end + end + + class PubKey + attr_reader :verify_key + + def initialize(data) + @verify_key = RbNaCl::Signatures::Ed25519::VerifyKey.new(data) + end + + def self.read_keyblob(buffer) + PubKey.new(buffer.read_string) + end + + def to_blob + Net::SSH::Buffer.from(:mstring,"ssh-ed25519",:string,@verify_key.to_bytes).to_s + end + + def ssh_type + "ssh-ed25519" + end + + def ssh_signature_type + ssh_type + end + + def ssh_do_verify(sig,data) + @verify_key.verify(sig,data) + end + + def to_pem + # TODO this is not pem + ssh_type + Base64.encode64(@verify_key.to_bytes) + end + + def fingerprint + @fingerprint ||= OpenSSL::Digest::MD5.hexdigest(to_blob).scan(/../).join(":") + end + end + + class PrivKey + CipherFactory = Net::SSH::Transport::CipherFactory + + MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n" + MEND = "-----END OPENSSH PRIVATE KEY-----\n" + MAGIC = "openssh-key-v1" + + attr_reader :sign_key + + def initialize(datafull,password) + raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN) + raise ArgumentError.new("Expected #{MEND} at end of private key") unless datafull.end_with?(MEND) + datab64 = datafull[MBEGIN.size ... -MEND.size] + data = Base64.decode64(datab64) + raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC) + buffer = Net::SSH::Buffer.new(data[MAGIC.size+1 .. -1]) + + ciphername = buffer.read_string + raise ArgumentError.new("#{ciphername} in private key is not supported") unless + CipherFactory.supported?(ciphername) + + kdfname = buffer.read_string + raise ArgumentError.new("Expected #{kdfname} to be or none or bcrypt") unless %w(none bcrypt).include?(kdfname) + + kdfopts = Net::SSH::Buffer.new(buffer.read_string) + num_keys = buffer.read_long + raise ArgumentError.new("Only 1 key is supported in ssh keys #{num_keys} was in private key") unless num_keys == 1 + _pubkey = buffer.read_string + + len = buffer.read_long + + keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true) + raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if + ((len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0)) + + if kdfname == 'bcrypt' + salt = kdfopts.read_string + rounds = kdfopts.read_long + + raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java" + key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds) + else + key = '\x00' * (keylen + ivlen) + end + + cipher = CipherFactory.get(ciphername, key: key[0...keylen], iv:key[keylen...keylen+ivlen], decrypt: true) + + decoded = cipher.update(buffer.remainder_as_buffer.to_s) + decoded << cipher.final + + decoded = Net::SSH::Buffer.new(decoded) + check1 = decoded.read_long + check2 = decoded.read_long + + raise ArgumentError, "Decrypt failed on private key" if (check1 != check2) + + _type_name = decoded.read_string + pk = decoded.read_string + sk = decoded.read_string + _comment = decoded.read_string + + @pk = pk + @sign_key = SigningKeyFromFile.new(pk,sk) + end + + def to_blob + public_key.to_blob + end + + def ssh_type + "ssh-ed25519" + end + + def ssh_signature_type + ssh_type + end + + def public_key + PubKey.new(@pk) + end + + def ssh_do_sign(data) + @sign_key.sign(data) + end + + def self.read(data,password) + self.new(data,password) + end + + def self.read_keyblob(buffer) + ED25519::PubKey.read_keyblob(buffer) + end + end +end +end; end; end diff --git a/lib/net/ssh/authentication/ed25519_loader.rb b/lib/net/ssh/authentication/ed25519_loader.rb new file mode 100644 index 0000000000000..5c3d43f5138e7 --- /dev/null +++ b/lib/net/ssh/authentication/ed25519_loader.rb @@ -0,0 +1,31 @@ +module Net; module SSH; module Authentication + +# Loads ED25519 support which requires optinal dependecies like +# rbnacl, bcrypt_pbkdf +module ED25519Loader + +begin + require 'net/ssh/authentication/ed25519' + LOADED = true + ERROR = nil +rescue LoadError => e + ERROR = e + LOADED = false +end + +def self.raiseUnlessLoaded(message) + description = ERROR.is_a?(LoadError) ? dependenciesRequiredForED25519 : '' + description << "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR + raise NotImplementedError, "#{message}\n#{description}" unless LOADED +end + +def self.dependenciesRequiredForED25519 + result = "net-ssh requires the following gems for ed25519 support:\n" + result << " * rbnacl (>= 3.2, < 5.0)\n" + result << " * rbnacl-libsodium, if your system doesn't have libsodium installed.\n" + result << " * bcrypt_pbkdf (>= 1.0, < 2.0)\n" unless RUBY_PLATFORM == "java" + result << "See https://github.com/net-ssh/net-ssh/issues/478 for more information\n" +end + +end +end; end; end diff --git a/lib/net/ssh/authentication/key_manager.rb b/lib/net/ssh/authentication/key_manager.rb index 20efaa8a633c3..30e1be097ef36 100644 --- a/lib/net/ssh/authentication/key_manager.rb +++ b/lib/net/ssh/authentication/key_manager.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/errors' require 'net/ssh/key_factory' require 'net/ssh/loggable' @@ -13,7 +12,7 @@ class KeyManagerError < Net::SSH::Exception; end # This class encapsulates all operations done by clients on a user's # private keys. In practice, the client should never need a reference - # to a private key; instead, they grab a list of "identities" (public + # to a private key; instead, they grab a list of "identities" (public # keys) that are available from the KeyManager, and then use # the KeyManager to do various private key operations using those # identities. @@ -38,12 +37,13 @@ class KeyManager attr_reader :options # Create a new KeyManager. By default, the manager will - # use the ssh-agent (if it is running). + # use the ssh-agent if it is running and the `:use_agent` option + # is not false. def initialize(logger, options={}) self.logger = logger @key_files = [] @key_data = [] - @use_agent = true + @use_agent = !(options[:use_agent] == false) @known_identities = {} @agent = nil @options = options @@ -91,47 +91,35 @@ def finish # The origin of the identities may be from files on disk or from an # ssh-agent. Note that identities from an ssh-agent are always listed # first in the array, with other identities coming after. + # + # If key manager was created with :keys_only option, any identity + # from ssh-agent will be ignored unless it present in key_files or + # key_data. def each_identity + prepared_identities = prepare_identities_from_files + prepare_identities_from_data + + user_identities = load_identities(prepared_identities, false, true) + if agent agent.identities.each do |key| - known_identities[key] = { :from => :agent } - yield key - end - end - - key_files.each do |file| - public_key_file = file + ".pub" - if File.readable?(public_key_file) - begin - key = KeyFactory.load_public_key(public_key_file) - known_identities[key] = { :from => :file, :file => file } - yield key - rescue Exception => e - error { "could not load public key file `#{public_key_file}': #{e.class} (#{e.message})" } - end - elsif File.readable?(file) - begin - private_key = KeyFactory.load_private_key(file, options[:passphrase]) - key = private_key.send(:public_key) - known_identities[key] = { :from => :file, :file => file, :key => private_key } + corresponding_user_identity = user_identities.detect { |identity| + identity[:public_key] && identity[:public_key].to_pem == key.to_pem + } + user_identities.delete(corresponding_user_identity) if corresponding_user_identity + + if !options[:keys_only] || corresponding_user_identity + known_identities[key] = { from: :agent } yield key - rescue Exception => e - error { "could not load private key file `#{file}': #{e.class} (#{e.message})" } end end end - key_data.each do |data| - if @options[:skip_private_keys] - key = KeyFactory.load_data_public_key(data) - known_identities[key] = { :from => :key_data, :data => data } + user_identities = load_identities(user_identities, !options[:non_interactive], false) + + user_identities.each do |identity| + key = identity.delete(:public_key) + known_identities[key] = identity yield key - else - private_key = KeyFactory.load_data_private_key(data) - key = private_key.send(:public_key) - known_identities[key] = { :from => :key_data, :data => data, :key => private_key } - yield key - end end self @@ -151,15 +139,15 @@ def sign(identity, data) if info[:key].nil? && info[:from] == :file begin - info[:key] = KeyFactory.load_private_key(info[:file], options[:passphrase]) - rescue Exception => e + info[:key] = KeyFactory.load_private_key(info[:file], options[:passphrase], !options[:non_interactive]) + rescue OpenSSL::OpenSSLError, Exception => e raise KeyManagerError, "the given identity is known, but the private key could not be loaded: #{e.class} (#{e.message})" end end if info[:key] - return Net::SSH::Buffer.from(:string, identity.ssh_type, - :string, info[:key].ssh_do_sign(data.to_s)).to_s + return Net::SSH::Buffer.from(:string, identity.ssh_signature_type, + :mstring, info[:key].ssh_do_sign(data.to_s)).to_s end if info[:from] == :agent @@ -172,7 +160,6 @@ def sign(identity, data) # Identifies whether the ssh-agent will be used or not. def use_agent? - return false if @options[:disable_agent] @use_agent end @@ -189,13 +176,92 @@ def use_agent=(use_agent) # or if the agent is otherwise not available. def agent return unless use_agent? - @agent ||= Agent.connect(logger) + @agent ||= Agent.connect(logger, options[:agent_socket_factory]) rescue AgentNotAvailable @use_agent = false nil end - end + private + + # Prepares identities from user key_files for loading, preserving their order and sources. + def prepare_identities_from_files + key_files.map do |file| + if readable_file?(file) + identity = {} + cert_file = file + "-cert.pub" + public_key_file = file + ".pub" + if readable_file?(cert_file) + identity[:load_from] = :pubkey_file + identity[:pubkey_file] = cert_file + elsif readable_file?(public_key_file) + identity[:load_from] = :pubkey_file + identity[:pubkey_file] = public_key_file + else + identity[:load_from] = :privkey_file + end + identity.merge(privkey_file: file) + end + end.compact + end + + def readable_file?(path) + File.file?(path) && File.readable?(path) + end + + # Prepared identities from user key_data, preserving their order and sources. + def prepare_identities_from_data + key_data.map do |data| + { load_from: :data, data: data } + end + end + + # Load prepared identities. Private key decryption errors ignored if ignore_decryption_errors + def load_identities(identities, ask_passphrase, ignore_decryption_errors) + identities.map do |identity| + begin + case identity[:load_from] + when :pubkey_file + key = KeyFactory.load_public_key(identity[:pubkey_file]) + { public_key: key, from: :file, file: identity[:privkey_file] } + when :privkey_file + private_key = KeyFactory.load_private_key(identity[:privkey_file], options[:passphrase], ask_passphrase, options[:password_prompt]) + key = private_key.send(:public_key) + { public_key: key, from: :file, file: identity[:privkey_file], key: private_key } + when :data + private_key = KeyFactory.load_data_private_key(identity[:data], options[:passphrase], ask_passphrase, "", options[:password_prompt]) + key = private_key.send(:public_key) + { public_key: key, from: :key_data, data: identity[:data], key: private_key } + else + identity + end + + rescue OpenSSL::PKey::RSAError, OpenSSL::PKey::DSAError, OpenSSL::PKey::ECError, OpenSSL::PKey::PKeyError, ArgumentError => e + if ignore_decryption_errors + identity + else + process_identity_loading_error(identity, e) + nil + end + rescue Exception => e + process_identity_loading_error(identity, e) + nil + end + end.compact + end + + def process_identity_loading_error(identity, e) + case identity[:load_from] + when :pubkey_file + error { "could not load public key file `#{identity[:pubkey_file]}': #{e.class} (#{e.message})" } + when :privkey_file + error { "could not load private key file `#{identity[:privkey_file]}': #{e.class} (#{e.message})" } + else + raise e + end + end + + end end end end diff --git a/lib/net/ssh/authentication/methods/abstract.rb b/lib/net/ssh/authentication/methods/abstract.rb index 794d75bf2db6d..eb830aa990131 100644 --- a/lib/net/ssh/authentication/methods/abstract.rb +++ b/lib/net/ssh/authentication/methods/abstract.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/buffer' require 'net/ssh/errors' require 'net/ssh/loggable' @@ -23,6 +22,7 @@ def initialize(session, options={}) @session = session @key_manager = options[:key_manager] @options = options + @prompt = options[:password_prompt] self.logger = session.logger end @@ -56,6 +56,9 @@ def userauth_request(username, next_service, auth_method, *others) buffer end + private + + attr_reader :prompt end -end; end; end; end +end; end; end; end \ No newline at end of file diff --git a/lib/net/ssh/authentication/methods/hostbased.rb b/lib/net/ssh/authentication/methods/hostbased.rb index 908fe179eaa12..610b64ed9ca4e 100644 --- a/lib/net/ssh/authentication/methods/hostbased.rb +++ b/lib/net/ssh/authentication/methods/hostbased.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/authentication/methods/abstract' module Net @@ -52,6 +51,10 @@ def authenticate_with(identity, next_service, username, key_manager) return true when USERAUTH_FAILURE info { "hostbased failed (#{identity.fingerprint})" } + + raise Net::SSH::Authentication::DisallowedMethod unless + message[:authentications].split(/,/).include? 'hostbased' + return false else raise Net::SSH::Exception, "unexpected server response to USERAUTH_REQUEST: #{message.type} (#{message.inspect})" diff --git a/lib/net/ssh/authentication/methods/keyboard_interactive.rb b/lib/net/ssh/authentication/methods/keyboard_interactive.rb index a4f131e12b1e3..70c3c07096e13 100644 --- a/lib/net/ssh/authentication/methods/keyboard_interactive.rb +++ b/lib/net/ssh/authentication/methods/keyboard_interactive.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/prompt' require 'net/ssh/authentication/methods/abstract' @@ -9,8 +8,6 @@ module Methods # Implements the "keyboard-interactive" SSH authentication method. class KeyboardInteractive < Abstract - include Prompt # Or not - Prompt depends on stdin/stdout ATM. -todb - USERAUTH_INFO_REQUEST = 60 USERAUTH_INFO_RESPONSE = 61 @@ -19,35 +16,42 @@ def authenticate(next_service, username, password=nil) debug { "trying keyboard-interactive" } send_message(userauth_request(username, next_service, "keyboard-interactive", "", "")) + prompter = nil loop do message = session.next_message case message.type when USERAUTH_SUCCESS debug { "keyboard-interactive succeeded" } + prompter.success if prompter return true when USERAUTH_FAILURE debug { "keyboard-interactive failed" } - return false + + raise Net::SSH::Authentication::DisallowedMethod unless + message[:authentications].split(/,/).include? 'keyboard-interactive' + + return false unless interactive? + password = nil + debug { "retrying keyboard-interactive" } + send_message(userauth_request(username, next_service, "keyboard-interactive", "", "")) when USERAUTH_INFO_REQUEST name = message.read_string instruction = message.read_string debug { "keyboard-interactive info request" } - unless password - puts(name) unless name.empty? - puts(instruction) unless instruction.empty? + if password.nil? && interactive? && prompter.nil? + prompter = prompt.start(type: 'keyboard-interactive', name: name, instruction: instruction) end - lang_tag = message.read_string + _ = message.read_string # lang_tag responses =[] - + message.read_long.times do text = message.read_string echo = message.read_bool - responses << (password || "") - # Avoid actually prompting. - # responses << (password || prompt(text, echo)) + password_to_send = password || (prompter && prompter.ask(text, echo)) + responses << password_to_send end # if the password failed the first time around, don't try @@ -61,8 +65,12 @@ def authenticate(next_service, username, password=nil) end end end - end + def interactive? + options = session.transport.options || {} + !options[:non_interactive] + end + end end end end diff --git a/lib/net/ssh/authentication/methods/none.rb b/lib/net/ssh/authentication/methods/none.rb new file mode 100644 index 0000000000000..17e1781fda778 --- /dev/null +++ b/lib/net/ssh/authentication/methods/none.rb @@ -0,0 +1,37 @@ +require 'net/ssh/errors' +require 'net/ssh/authentication/methods/abstract' + +module Net + module SSH + module Authentication + module Methods + + # Implements the "none" SSH authentication method. + class None < Abstract + # Attempt to authenticate as "none" + def authenticate(next_service, user="", password="") + send_message(userauth_request(user, next_service, "none")) + message = session.next_message + + case message.type + when USERAUTH_SUCCESS + debug { "none succeeded" } + return true + when USERAUTH_FAILURE + debug { "none failed" } + + raise Net::SSH::Authentication::DisallowedMethod unless + message[:authentications].split(/,/).include? 'none' + + return false + else + raise Net::SSH::Exception, "unexpected reply to USERAUTH_REQUEST: #{message.type} (#{message.inspect})" + end + + end + end + + end + end + end +end diff --git a/lib/net/ssh/authentication/methods/password.rb b/lib/net/ssh/authentication/methods/password.rb index 8d84aad326269..7707eb843f619 100644 --- a/lib/net/ssh/authentication/methods/password.rb +++ b/lib/net/ssh/authentication/methods/password.rb @@ -1,5 +1,5 @@ -# -*- coding: binary -*- require 'net/ssh/errors' +require 'net/ssh/prompt' require 'net/ssh/authentication/methods/abstract' module Net @@ -10,25 +10,35 @@ module Methods # Implements the "password" SSH authentication method. class Password < Abstract # Attempt to authenticate the given user for the given service. If - # the password parameter is nil, this will never do anything except - # return false. + # the password parameter is nil, this will ask for password def authenticate(next_service, username, password=nil) - return false unless password + clear_prompter! + retries = 0 + max_retries = get_max_retries + return false if !password && max_retries == 0 - send_message(userauth_request(username, next_service, "password", false, password)) - message = session.next_message + begin + password_to_send = password || ask_password(username) + + send_message(userauth_request(username, next_service, "password", false, password_to_send)) + message = session.next_message + retries += 1 + + if message.type == USERAUTH_FAILURE + debug { "password failed" } + + raise Net::SSH::Authentication::DisallowedMethod unless + message[:authentications].split(/,/).include? 'password' + password = nil + end + end until (message.type != USERAUTH_FAILURE || retries >= max_retries) case message.type when USERAUTH_SUCCESS debug { "password succeeded" } - if session.options[:record_auth_info] - session.auth_info[:method] = "password" - session.auth_info[:user] = username - session.auth_info[:password] = password - end + @prompter.success if @prompter return true when USERAUTH_FAILURE - debug { "password failed" } return false when USERAUTH_PASSWD_CHANGEREQ debug { "password change request received, failing" } @@ -37,6 +47,32 @@ def authenticate(next_service, username, password=nil) raise Net::SSH::Exception, "unexpected reply to USERAUTH_REQUEST: #{message.type} (#{message.inspect})" end end + + private + + NUMBER_OF_PASSWORD_PROMPTS = 3 + + def clear_prompter! + @prompt_info = nil + @prompter = nil + end + + def ask_password(username) + host = session.transport.host + prompt_info = {type: 'password', user: username, host: host} + if @prompt_info != prompt_info + @prompt_info = prompt_info + @prompter = prompt.start(prompt_info) + end + echo = false + @prompter.ask("#{username}@#{host}'s password:", echo) + end + + def get_max_retries + options = session.transport.options || {} + result = options[:number_of_password_prompts] || NUMBER_OF_PASSWORD_PROMPTS + options[:non_interactive] ? 0 : result + end end end diff --git a/lib/net/ssh/authentication/methods/publickey.rb b/lib/net/ssh/authentication/methods/publickey.rb index 7f65487130c48..d06af5c030b64 100644 --- a/lib/net/ssh/authentication/methods/publickey.rb +++ b/lib/net/ssh/authentication/methods/publickey.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/buffer' require 'net/ssh/errors' require 'net/ssh/authentication/methods/abstract' @@ -55,23 +54,6 @@ def authenticate_with(identity, next_service, username) case message.type when USERAUTH_PK_OK - debug { "publickey will be accepted (#{identity.fingerprint})" } - - # The key is accepted by the server, trigger a callback if set - if session.accepted_key_callback - session.accepted_key_callback.call({ :user => username, :fingerprint => identity.fingerprint, :key => identity.dup }) - end - - if session.skip_private_keys - if session.options[:record_auth_info] - session.auth_info[:method] = "publickey" - session.auth_info[:user] = username - session.auth_info[:pubkey_data] = identity.inspect - session.auth_info[:pubkey_id] = identity.fingerprint - end - return true - end - buffer = build_request(identity, username, next_service, true) sig_data = Net::SSH::Buffer.new sig_data.write_string(session_id) @@ -85,15 +67,13 @@ def authenticate_with(identity, next_service, username) case message.type when USERAUTH_SUCCESS debug { "publickey succeeded (#{identity.fingerprint})" } - if session.options[:record_auth_info] - session.auth_info[:method] = "publickey" - session.auth_info[:user] = username - session.auth_info[:pubkey_data] = identity.inspect - session.auth_info[:pubkey_id] = identity.fingerprint - end - return true + return true when USERAUTH_FAILURE debug { "publickey failed (#{identity.fingerprint})" } + + raise Net::SSH::Authentication::DisallowedMethod unless + message[:authentications].split(/,/).include? 'publickey' + return false else raise Net::SSH::Exception, diff --git a/lib/net/ssh/authentication/pageant.rb b/lib/net/ssh/authentication/pageant.rb index 4ff4d93ca7914..8c86699321a1b 100644 --- a/lib/net/ssh/authentication/pageant.rb +++ b/lib/net/ssh/authentication/pageant.rb @@ -1,6 +1,23 @@ -# -*- coding: binary -*- -require 'dl/import' -require 'dl/struct' +if RUBY_VERSION < "1.9" + require 'dl/import' + require 'dl/struct' +elsif RUBY_VERSION < "2.1" + require 'dl/import' + require 'dl/types' + require 'dl' +else + require 'fiddle' + require 'fiddle/types' + require 'fiddle/import' + + # For now map DL to Fiddler versus updating all the code below + module DL + CPtr ||= Fiddle::Pointer + if RUBY_PLATFORM != "java" + RUBY_FREE ||= Fiddle::RUBY_FREE + end + end +end require 'net/ssh/errors' @@ -11,22 +28,44 @@ module Net; module SSH; module Authentication # identities. # # This code is a slightly modified version of the original implementation - # by Guillaume Marçais (guillaume.marcais@free.fr). It is used and + # by Guillaume Marçais (guillaume.marcais@free.fr). It is used and # relicensed by permission. module Pageant # From Putty pageant.c AGENT_MAX_MSGLEN = 8192 AGENT_COPYDATA_ID = 0x804e50ba - + # The definition of the Windows methods and data structures used in # communicating with the pageant process. - module Win - extend DL::Importable - - dlload 'user32' - dlload 'kernel32' - + module Win # rubocop:disable Metrics/ModuleLength + # Compatibility on initialization + if RUBY_VERSION < "1.9" + extend DL::Importable + + dlload 'user32' + dlload 'kernel32' + dlload 'advapi32' + + SIZEOF_DWORD = DL.sizeof('L') + elsif RUBY_VERSION < "2.1" + extend DL::Importer + dlload 'user32','kernel32', 'advapi32' + include DL::Win32Types + + SIZEOF_DWORD = DL::SIZEOF_LONG + else + extend Fiddle::Importer + dlload 'user32','kernel32', 'advapi32' + include Fiddle::Win32Types + SIZEOF_DWORD = Fiddle::SIZEOF_LONG + end + + if RUBY_ENGINE=="jruby" + typealias("HANDLE", "void *") # From winnt.h + typealias("PHANDLE", "void *") # From winnt.h + typealias("ULONG_PTR", "unsigned long*") + end typealias("LPCTSTR", "char *") # From winnt.h typealias("LPVOID", "void *") # From winnt.h typealias("LPCVOID", "const void *") # From windef.h @@ -34,6 +73,7 @@ module Win typealias("WPARAM", "unsigned int *") # From windef.h typealias("LPARAM", "long *") # From windef.h typealias("PDWORD_PTR", "long *") # From basetsd.h + typealias("USHORT", "unsigned short") # From windef.h # From winbase.h, winnt.h INVALID_HANDLE_VALUE = -1 @@ -44,18 +84,24 @@ module Win SMTO_NORMAL = 0 # From winuser.h + SUFFIX = if RUBY_ENGINE == "jruby" + "A" + else + "" + end + # args: lpClassName, lpWindowName - extern 'HWND FindWindow(LPCTSTR, LPCTSTR)' + extern "HWND FindWindow#{SUFFIX}(LPCTSTR, LPCTSTR)" # args: none extern 'DWORD GetCurrentThreadId()' # args: hFile, (ignored), flProtect, dwMaximumSizeHigh, # dwMaximumSizeLow, lpName - extern 'HANDLE CreateFileMapping(HANDLE, void *, DWORD, DWORD, ' + - 'DWORD, LPCTSTR)' + extern "HANDLE CreateFileMapping#{SUFFIX}(HANDLE, void *, DWORD, " + + "DWORD, DWORD, LPCTSTR)" - # args: hFileMappingObject, dwDesiredAccess, dwFileOffsetHigh, + # args: hFileMappingObject, dwDesiredAccess, dwFileOffsetHigh, # dwfileOffsetLow, dwNumberOfBytesToMap extern 'LPVOID MapViewOfFile(HANDLE, DWORD, DWORD, DWORD, DWORD)' @@ -66,8 +112,276 @@ module Win extern 'BOOL CloseHandle(HANDLE)' # args: hWnd, Msg, wParam, lParam, fuFlags, uTimeout, lpdwResult - extern 'LRESULT SendMessageTimeout(HWND, UINT, WPARAM, LPARAM, ' + - 'UINT, UINT, PDWORD_PTR)' + extern "LRESULT SendMessageTimeout#{SUFFIX}(HWND, UINT, WPARAM, LPARAM, " + + "UINT, UINT, PDWORD_PTR)" + + # args: none + extern 'DWORD GetLastError()' + + # args: none + extern 'HANDLE GetCurrentProcess()' + + # args: hProcessHandle, dwDesiredAccess, (out) phNewTokenHandle + extern 'BOOL OpenProcessToken(HANDLE, DWORD, PHANDLE)' + + # args: hTokenHandle, uTokenInformationClass, + # (out) lpTokenInformation, dwTokenInformationLength + # (out) pdwInfoReturnLength + extern 'BOOL GetTokenInformation(HANDLE, UINT, LPVOID, DWORD, ' + + 'PDWORD)' + + # args: (out) lpSecurityDescriptor, dwRevisionLevel + extern 'BOOL InitializeSecurityDescriptor(LPVOID, DWORD)' + + # args: (out) lpSecurityDescriptor, lpOwnerSid, bOwnerDefaulted + extern 'BOOL SetSecurityDescriptorOwner(LPVOID, LPVOID, BOOL)' + + # args: pSecurityDescriptor + extern 'BOOL IsValidSecurityDescriptor(LPVOID)' + + # Constants needed for security attribute retrieval. + # Specifies the access mask corresponding to the desired access + # rights. + TOKEN_QUERY = 0x8 + + # The value of TOKEN_USER from the TOKEN_INFORMATION_CLASS enum. + TOKEN_USER_INFORMATION_CLASS = 1 + + # The initial revision level assigned to the security descriptor. + REVISION = 1 + + # Structs for security attribute functions. + # Holds the retrieved user access token. + TOKEN_USER = struct ['void * SID', 'DWORD ATTRIBUTES'] + + # Contains the security descriptor, this gets passed to the + # function that constructs the shared memory map. + SECURITY_ATTRIBUTES = struct ['DWORD nLength', + 'LPVOID lpSecurityDescriptor', + 'BOOL bInheritHandle'] + + # The security descriptor holds security information. + SECURITY_DESCRIPTOR = struct ['UCHAR Revision', 'UCHAR Sbz1', + 'USHORT Control', 'LPVOID Owner', + 'LPVOID Group', 'LPVOID Sacl', + 'LPVOID Dacl'] + + # The COPYDATASTRUCT is used to send WM_COPYDATA messages + COPYDATASTRUCT = if RUBY_ENGINE == "jruby" + struct ['ULONG_PTR dwData', 'DWORD cbData', 'LPVOID lpData'] + else + struct ['uintptr_t dwData', 'DWORD cbData', 'LPVOID lpData'] + end + + # Compatibility for security attribute retrieval. + if RUBY_VERSION < "1.9" + # Alias functions to > 1.9 capitalization + %w(findWindow + getCurrentProcess + initializeSecurityDescriptor + setSecurityDescriptorOwner + isValidSecurityDescriptor + openProcessToken + getTokenInformation + getLastError + getCurrentThreadId + createFileMapping + mapViewOfFile + sendMessageTimeout + unmapViewOfFile + closeHandle).each do |name| + new_name = name[0].chr.upcase + name[1..name.length] + alias_method new_name, name + module_function new_name + end + + def self.malloc_ptr(size) + return DL.malloc(size) + end + + def self.get_ptr(data) + return data.to_ptr + end + + def self.set_ptr_data(ptr, data) + ptr[0] = data + end + elsif RUBY_ENGINE == "jruby" + %w(FindWindow CreateFileMapping SendMessageTimeout).each do |name| + alias_method name, name+"A" + module_function name + end + # :nodoc: + module LibC + extend FFI::Library + ffi_lib FFI::Library::LIBC + attach_function :malloc, [:size_t], :pointer + attach_function :free, [:pointer], :void + end + + def self.malloc_ptr(size) + Fiddle::Pointer.new(LibC.malloc(size), size, LibC.method(:free)) + end + + def self.get_ptr(ptr) + return data.address + end + + def self.set_ptr_data(ptr, data) + ptr.write_string_length(data, data.size) + end + else + def self.malloc_ptr(size) + return DL::CPtr.malloc(size, DL::RUBY_FREE) + end + + def self.get_ptr(data) + return DL::CPtr.to_ptr data + end + + def self.set_ptr_data(ptr, data) + DL::CPtr.new(ptr)[0,data.size] = data + end + end + + def self.get_security_attributes_for_user + user = get_current_user + + psd_information = malloc_ptr(Win::SECURITY_DESCRIPTOR.size) + raise_error_if_zero( + Win.InitializeSecurityDescriptor(psd_information, + Win::REVISION) + ) + raise_error_if_zero( + Win.SetSecurityDescriptorOwner(psd_information, get_sid_ptr(user), + 0) + ) + raise_error_if_zero( + Win.IsValidSecurityDescriptor(psd_information) + ) + + sa = Win::SECURITY_ATTRIBUTES.new(to_struct_ptr(malloc_ptr(Win::SECURITY_ATTRIBUTES.size))) + sa.nLength = Win::SECURITY_ATTRIBUTES.size + sa.lpSecurityDescriptor = psd_information.to_i + sa.bInheritHandle = 1 + + return sa + end + + if RUBY_ENGINE == "jruby" + def self.ptr_to_s(ptr, size) + ret = ptr.to_s(size) + ret << "\x00" while ret.size < size + ret + end + + def self.ptr_to_handle(phandle) + phandle.ptr + end + + def self.ptr_to_dword(ptr) + first = ptr.ptr.to_i + second = ptr_to_s(ptr,Win::SIZEOF_DWORD).unpack('L')[0] + raise "Error" unless first == second + first + end + + def self.to_token_user(ptoken_information) + TOKEN_USER.new(ptoken_information.to_ptr) + end + + def self.to_struct_ptr(ptr) + ptr.to_ptr + end + + def self.get_sid(user) + ptr_to_s(user.to_ptr.ptr,Win::SIZEOF_DWORD).unpack('L')[0] + end + + def self.get_sid_ptr(user) + user.to_ptr.ptr + end + else + def self.get_sid(user) + user.SID + end + + def self.ptr_to_handle(phandle) + phandle.ptr.to_i + end + + def self.to_struct_ptr(ptr) + ptr + end + + def self.ptr_to_dword(ptr) + ptr.to_s(Win::SIZEOF_DWORD).unpack('L')[0] + end + + def self.to_token_user(ptoken_information) + TOKEN_USER.new(ptoken_information) + end + + def self.get_sid_ptr(user) + user.SID + end + end + + def self.get_current_user + token_handle = open_process_token(Win.GetCurrentProcess, + Win::TOKEN_QUERY) + token_user = get_token_information(token_handle, + Win::TOKEN_USER_INFORMATION_CLASS) + return token_user + end + + def self.open_process_token(process_handle, desired_access) + ptoken_handle = malloc_ptr(Win::SIZEOF_DWORD) + + raise_error_if_zero( + Win.OpenProcessToken(process_handle, desired_access, + ptoken_handle) + ) + token_handle = ptr_to_handle(ptoken_handle) + return token_handle + end + + def self.get_token_information(token_handle, + token_information_class) + # Hold the size of the information to be returned + preturn_length = malloc_ptr(Win::SIZEOF_DWORD) + + # Going to throw an INSUFFICIENT_BUFFER_ERROR, but that is ok + # here. This is retrieving the size of the information to be + # returned. + Win.GetTokenInformation(token_handle, + token_information_class, + Win::NULL, 0, preturn_length) + ptoken_information = malloc_ptr(ptr_to_dword(preturn_length)) + + # This call is going to write the requested information to + # the memory location referenced by token_information. + raise_error_if_zero( + Win.GetTokenInformation(token_handle, + token_information_class, + ptoken_information, + ptoken_information.size, + preturn_length) + ) + + return to_token_user(ptoken_information) + end + + def self.raise_error_if_zero(result) + if result == 0 + raise "Windows error: #{Win.GetLastError}" + end + end + + # Get a null-terminated string given a string. + def self.get_cstr(str) + return str + "\000" + end end # This is the pseudo-socket implementation that mimics the interface of @@ -78,34 +392,51 @@ class Socket private_class_method :new - # The factory method for creating a new Socket instance. The location - # parameter is ignored, and is only needed for compatibility with - # the general Socket interface. - def self.open(location=nil) + # The factory method for creating a new Socket instance. + def self.open new end - # Create a new instance that communicates with the running pageant + # Create a new instance that communicates with the running pageant # instance. If no such instance is running, this will cause an error. def initialize - @win = Win.findWindow("Pageant", "Pageant") + @win = Win.FindWindow("Pageant", "Pageant") - if @win == 0 + if @win.to_i == 0 raise Net::SSH::Exception, "pageant process not running" end - @res = nil - @pos = 0 + @input_buffer = Net::SSH::Buffer.new + @output_buffer = Net::SSH::Buffer.new end - + # Forwards the data to #send_query, ignoring any arguments after - # the first. Returns 0. + # the first. def send(data, *args) - @res = send_query(data) - @pos = 0 + @input_buffer.append(data) + + ret = data.length + + while true + return ret if @input_buffer.length < 4 + msg_length = @input_buffer.read_long + 4 + @input_buffer.reset! + + return ret if @input_buffer.length < msg_length + msg = @input_buffer.read!(msg_length) + @output_buffer.append(send_query(msg)) + end end + # Reads +n+ bytes from the cached result of the last query. If +n+ + # is +nil+, returns all remaining data from the last query. + def read(n = nil) + @output_buffer.read(n) + end + + def close; end + # Packages the given query string and sends it to the pageant # process via the Windows messaging subsystem. The result is # cached, to be returned piece-wise when #read is called. @@ -113,72 +444,51 @@ def send_query(query) res = nil filemap = 0 ptr = nil - id = DL::PtrData.malloc(DL.sizeof("L")) + id = Win.malloc_ptr(Win::SIZEOF_DWORD) - mapname = "PageantRequest%08x\000" % Win.getCurrentThreadId() - filemap = Win.createFileMapping(Win::INVALID_HANDLE_VALUE, - Win::NULL, - Win::PAGE_READWRITE, 0, + mapname = "PageantRequest%08x" % Win.GetCurrentThreadId() + security_attributes = Win.get_ptr Win.get_security_attributes_for_user + + filemap = Win.CreateFileMapping(Win::INVALID_HANDLE_VALUE, + security_attributes, + Win::PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapname) - if filemap == 0 + + if filemap == 0 || filemap == Win::INVALID_HANDLE_VALUE raise Net::SSH::Exception, - "Creation of file mapping failed" + "Creation of file mapping failed with error: #{Win.GetLastError}" end - ptr = Win.mapViewOfFile(filemap, Win::FILE_MAP_WRITE, 0, 0, - AGENT_MAX_MSGLEN) + ptr = Win.MapViewOfFile(filemap, Win::FILE_MAP_WRITE, 0, 0, + 0) if ptr.nil? || ptr.null? raise Net::SSH::Exception, "Mapping of file failed" end - ptr[0] = query - - cds = [AGENT_COPYDATA_ID, mapname.size + 1, mapname]. - pack("LLp").to_ptr - succ = Win.sendMessageTimeout(@win, Win::WM_COPYDATA, Win::NULL, - cds, Win::SMTO_NORMAL, 5000, id) + Win.set_ptr_data(ptr, query) + + # using struct to achieve proper alignment and field size on 64-bit platform + cds = Win::COPYDATASTRUCT.new(Win.malloc_ptr(Win::COPYDATASTRUCT.size)) + cds.dwData = AGENT_COPYDATA_ID + cds.cbData = mapname.size + 1 + cds.lpData = Win.get_cstr(mapname) + succ = Win.SendMessageTimeout(@win, Win::WM_COPYDATA, Win::NULL, + cds.to_ptr, Win::SMTO_NORMAL, 5000, id) if succ > 0 retlen = 4 + ptr.to_s(4).unpack("N")[0] res = ptr.to_s(retlen) - end + else + raise Net::SSH::Exception, "Message failed with error: #{Win.GetLastError}" + end return res ensure - Win.unmapViewOfFile(ptr) unless ptr.nil? || ptr.null? - Win.closeHandle(filemap) if filemap != 0 + Win.UnmapViewOfFile(ptr) unless ptr.nil? || ptr.null? + Win.CloseHandle(filemap) if filemap != 0 end - - # Conceptually close the socket. This doesn't really do anthing - # significant, but merely complies with the Socket interface. - def close - @res = nil - @pos = 0 - end - - # Conceptually asks if the socket is closed. As with #close, - # this doesn't really do anything significant, but merely - # complies with the Socket interface. - def closed? - @res.nil? && @pos.zero? - end - - # Reads +n+ bytes from the cached result of the last query. If +n+ - # is +nil+, returns all remaining data from the last query. - def read(n = nil) - return nil unless @res - if n.nil? - start, @pos = @pos, @res.size - return @res[start..-1] - else - start, @pos = @pos, @pos + n - return @res[start, n] - end - end - end - end end; end; end diff --git a/lib/net/ssh/authentication/session.rb b/lib/net/ssh/authentication/session.rb index 7e07ccf6961bc..e87f669976b06 100644 --- a/lib/net/ssh/authentication/session.rb +++ b/lib/net/ssh/authentication/session.rb @@ -1,8 +1,8 @@ -# -*- coding: binary -*- require 'net/ssh/loggable' require 'net/ssh/transport/constants' require 'net/ssh/authentication/constants' require 'net/ssh/authentication/key_manager' +require 'net/ssh/authentication/methods/none' require 'net/ssh/authentication/methods/publickey' require 'net/ssh/authentication/methods/hostbased' require 'net/ssh/authentication/methods/password' @@ -10,6 +10,10 @@ module Net; module SSH; module Authentication + # Raised if the current authentication method is not allowed + class DisallowedMethod < Net::SSH::Exception + end + # Represents an authentication session. It manages the authentication of # a user over an established connection (the "transport" object, see # Net::SSH::Transport::Session). @@ -32,28 +36,16 @@ class Session # a hash of options, given at construction time attr_reader :options - # when a successful auth is made, note the auth info if session.options[:record_auth_info] - attr_accessor :auth_info - - # when a public key is accepted (even if not used), trigger a callback - attr_accessor :accepted_key_callback - - # when we only want to test a key and not login - attr_accessor :skip_private_keys - # Instantiates a new Authentication::Session object over the given # transport layer abstraction. def initialize(transport, options={}) self.logger = transport.logger @transport = transport - @auth_methods = options[:auth_methods] || %w(publickey hostbased password keyboard-interactive) + @auth_methods = options[:auth_methods] || Net::SSH::Config.default_auth_methods @options = options - @allowed_auth_methods = @auth_methods - @skip_private_keys = options[:skip_private_keys] || false - @accepted_key_callback = options[:accepted_key_callback] - @auth_info = {} + @allowed_auth_methods = @auth_methods end # Attempts to authenticate the given user, in preparation for the next @@ -63,7 +55,7 @@ def authenticate(next_service, username, password=nil) debug { "beginning authentication of `#{username}'" } transport.send_message(transport.service_request("ssh-userauth")) - message = expect_message(SERVICE_ACCEPT) + expect_message(SERVICE_ACCEPT) key_manager = KeyManager.new(logger, options) keys.each { |key| key_manager.add(key) } unless keys.empty? @@ -72,13 +64,22 @@ def authenticate(next_service, username, password=nil) attempted = [] @auth_methods.each do |name| - next unless @allowed_auth_methods.include?(name) - attempted << name - - debug { "trying #{name}" } - method = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join).new(self, :key_manager => key_manager) - - return true if method.authenticate(next_service, username, password) + begin + next unless @allowed_auth_methods.include?(name) + attempted << name + + debug { "trying #{name}" } + begin + auth_class = Methods.const_get(name.split(/\W+/).map { |p| p.capitalize }.join) + method = auth_class.new(self, key_manager: key_manager, password_prompt: options[:password_prompt]) + rescue NameError + debug{"Mechanism #{name} was requested, but isn't a known type. Ignoring it."} + next + end + + return true if method.authenticate(next_service, username, password) + rescue Net::SSH::Authentication::DisallowedMethod + end end error { "all authorization methods failed (tried #{attempted.join(', ')})" } @@ -129,13 +130,21 @@ def expect_message(type) private + # Returns an array of paths to the key files usually defined + # by system default. + def default_keys + if defined?(OpenSSL::PKey::EC) + %w(~/.ssh/id_ed25519 ~/.ssh/id_rsa ~/.ssh/id_dsa ~/.ssh/id_ecdsa + ~/.ssh2/id_ed25519 ~/.ssh2/id_rsa ~/.ssh2/id_dsa ~/.ssh2/id_ecdsa) + else + %w(~/.ssh/id_dsa ~/.ssh/id_rsa ~/.ssh2/id_dsa ~/.ssh2/id_rsa) + end + end + # Returns an array of paths to the key files that should be used when # attempting any key-based authentication mechanism. def keys - Array( - options[:keys] # || - # %w(~/.ssh/id_dsa ~/.ssh/id_rsa ~/.ssh2/id_dsa ~/.ssh2/id_rsa) - ) + Array(options[:keys] || default_keys) end # Returns an array of the key data that should be used when @@ -145,4 +154,3 @@ def key_data end end end; end; end - diff --git a/lib/net/ssh/buffer.rb b/lib/net/ssh/buffer.rb index 9c6a199a682ad..02a476cabbf2f 100644 --- a/lib/net/ssh/buffer.rb +++ b/lib/net/ssh/buffer.rb @@ -1,7 +1,9 @@ -# -*- coding: binary -*- require 'net/ssh/ruby_compat' require 'net/ssh/transport/openssl' +require 'net/ssh/authentication/certificate' +require 'net/ssh/authentication/ed25519_loader' + module Net; module SSH # Net::SSH::Buffer is a flexible class for building and parsing binary @@ -35,6 +37,7 @@ class Buffer # * :long => write a 4-byte integer (#write_long) # * :byte => write a single byte (#write_byte) # * :string => write a 4-byte length followed by character data (#write_string) + # * :mstring => same as string, but caller cannot resuse the string, avoids potential duplication (#write_moved) # * :bool => write a single byte, interpreted as a boolean (#write_bool) # * :bignum => write an SSH-encoded bignum (#write_bignum) # * :key => write an SSH-encoded key value (#write_key) @@ -160,7 +163,7 @@ def read_to(pattern) index = @content.index(pattern, @position) or return nil length = case pattern when String then pattern.length - when Fixnum then 1 + when Integer then 1 when Regexp then $&.length end index && read(index+length) @@ -183,7 +186,12 @@ def read!(count=nil) consume! data end - + + # Calls block(self) until the buffer is empty, and returns all results. + def read_all(&block) + Enumerator.new { |e| e << yield(self) until eof? }.to_a + end + # Return the next 8 bytes as a 64-bit integer (in network byte order). # Returns nil if there are less than 8 bytes remaining to be read in the # buffer. @@ -241,21 +249,48 @@ def read_key end # Read a keyblob of the given type from the buffer, and return it as - # a key. Only RSA and DSA keys are supported. + # a key. Only RSA, DSA, and ECDSA keys are supported. def read_keyblob(type) case type - when "ssh-dss" + when /^(.*)-cert-v01@openssh\.com$/ + key = Net::SSH::Authentication::Certificate.read_certblob(self, $1) + when /^ssh-dss$/ key = OpenSSL::PKey::DSA.new - key.p = read_bignum - key.q = read_bignum - key.g = read_bignum - key.pub_key = read_bignum - - when "ssh-rsa" + if key.respond_to?(:set_pqg) + key.set_pqg(read_bignum, read_bignum, read_bignum) + else + key.p = read_bignum + key.q = read_bignum + key.g = read_bignum + end + if key.respond_to?(:set_key) + key.set_key(read_bignum, nil) + else + key.pub_key = read_bignum + end + when /^ssh-rsa$/ key = OpenSSL::PKey::RSA.new - key.e = read_bignum - key.n = read_bignum - + if key.respond_to?(:set_key) + e = read_bignum + n = read_bignum + key.set_key(n, e, nil) + else + key.e = read_bignum + key.n = read_bignum + end + when /^ssh-ed25519$/ + Net::SSH::Authentication::ED25519Loader.raiseUnlessLoaded("unsupported key type `#{type}'") + key = Net::SSH::Authentication::ED25519::PubKey.read_keyblob(self) + when /^ecdsa\-sha2\-(\w*)$/ + unless defined?(OpenSSL::PKey::EC) + raise NotImplementedError, "unsupported key type `#{type}'" + else + begin + key = OpenSSL::PKey::EC.read_keyblob($1, self) + rescue OpenSSL::PKey::ECError + raise NotImplementedError, "unsupported key type `#{type}'" + end + end else raise NotImplementedError, "unsupported key type `#{type}'" end @@ -272,7 +307,14 @@ def read_buffer # Writes the given data literally into the string. Does not alter the # read position. Returns the buffer object. def write(*data) - data.each { |datum| @content << datum } + data.each { |datum| @content << datum.dup.force_encoding('BINARY') } + self + end + + # Optimized version of write where the caller gives up ownership of string + # to the method. This way we can mutate the string. + def write_moved(string) + @content << string.force_encoding('BINARY') self end @@ -309,12 +351,25 @@ def write_byte(*n) def write_string(*text) text.each do |string| s = string.to_s - write_long(s.length) + write_long(s.bytesize) write(s) end self end + # Writes each argument to the buffer as an SSH2-encoded string. Each + # string is prefixed by its length, encoded as a 4-byte long integer. + # Does not alter the read position. Returns the buffer object. + # Might alter arguments see write_moved + def write_mstring(*text) + text.each do |string| + s = string.to_s + write_long(s.bytesize) + write_moved(s) + end + self + end + # Writes each argument to the buffer as a (C-style) boolean, with 1 # meaning true, and 0 meaning false. Does not alter the read position. # Returns the buffer object. diff --git a/lib/net/ssh/buffered_io.rb b/lib/net/ssh/buffered_io.rb index c51e775de79e3..a8f8ddb153917 100644 --- a/lib/net/ssh/buffered_io.rb +++ b/lib/net/ssh/buffered_io.rb @@ -1,6 +1,6 @@ -# -*- coding: binary -*- require 'net/ssh/buffer' require 'net/ssh/loggable' +require 'net/ssh/ruby_compat' module Net; module SSH @@ -21,7 +21,7 @@ module Net; module SSH # available for write, and then call #enqueue and #read_available during # the idle times. # - # socket = Rex::Socket::Tcp.create( ... address, ... port ... ) + # socket = TCPSocket.new(address, port) # socket.extend(Net::SSH::BufferedIo) # # ssh.listen_to(socket) @@ -66,6 +66,9 @@ def fill(n=8192) debug { "read #{data.length} bytes" } input.append(data) return data.length + rescue EOFError => e + @input_errors << e + return 0 end # Read up to +length+ bytes from the input buffer. If +length+ is nil, @@ -110,7 +113,7 @@ def send_pending def wait_for_pending_sends send_pending while output.length > 0 - result = IO.select(nil, [self]) or next + result = Net::SSH::Compat.io_select(nil, [self]) or next next unless result[1].any? send_pending end @@ -143,8 +146,58 @@ def output; @output; end # Module#include to add this module. def initialize_buffered_io @input = Net::SSH::Buffer.new + @input_errors = [] @output = Net::SSH::Buffer.new + @output_errors = [] end end + + + # Fixes for two issues by Miklós Fazekas: + # + # * if client closes a forwarded connection, but the server is + # reading, net-ssh terminates with IOError socket closed. + # * if client force closes (RST) a forwarded connection, but + # server is reading, net-ssh terminates with [an exception] + # + # See: + # + # http://net-ssh.lighthouseapp.com/projects/36253/tickets/7 + # http://github.com/net-ssh/net-ssh/tree/portfwfix + # + module ForwardedBufferedIo + def fill(n=8192) + begin + super(n) + rescue Errno::ECONNRESET => e + debug { "connection was reset => shallowing exception:#{e}" } + return 0 + rescue IOError => e + if e.message =~ /closed/ then + debug { "connection was reset => shallowing exception:#{e}" } + return 0 + else + raise + end + end + end + + def send_pending + begin + super + rescue Errno::ECONNRESET => e + debug { "connection was reset => shallowing exception:#{e}" } + return 0 + rescue IOError => e + if e.message =~ /closed/ then + debug { "connection was reset => shallowing exception:#{e}" } + return 0 + else + raise + end + end + end + end + end; end diff --git a/lib/net/ssh/command_stream.rb b/lib/net/ssh/command_stream.rb deleted file mode 100644 index 661bbf3cef13c..0000000000000 --- a/lib/net/ssh/command_stream.rb +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: binary -*- -require 'rex' - -module Net -module SSH - -class CommandStream - - attr_accessor :channel, :thread, :error, :ssh - attr_accessor :lsock, :rsock, :monitor - - module PeerInfo - include ::Rex::IO::Stream - attr_accessor :peerinfo - attr_accessor :localinfo - end - - def initialize(ssh, cmd, cleanup = false) - - self.lsock, self.rsock = Rex::Socket.tcp_socket_pair() - self.lsock.extend(Rex::IO::Stream) - self.lsock.extend(PeerInfo) - self.rsock.extend(Rex::IO::Stream) - - self.ssh = ssh - self.thread = Thread.new(ssh,cmd,cleanup) do |rssh,rcmd,rcleanup| - - begin - info = rssh.transport.socket.getpeername - self.lsock.peerinfo = "#{info[1]}:#{info[2]}" - - info = rssh.transport.socket.getsockname - self.lsock.localinfo = "#{info[1]}:#{info[2]}" - - rssh.open_channel do |rch| - rch.exec(rcmd) do |c, success| - raise "could not execute command: #{rcmd.inspect}" unless success - - c[:data] = '' - - c.on_eof do - self.rsock.close rescue nil - self.ssh.close rescue nil - self.thread.kill - end - - c.on_close do - self.rsock.close rescue nil - self.ssh.close rescue nil - self.thread.kill - end - - c.on_data do |ch,data| - self.rsock.write(data) - end - - c.on_extended_data do |ch, ctype, data| - self.rsock.write(data) - end - - self.channel = c - end - end - - self.monitor = Thread.new do - while(true) - next if not self.rsock.has_read_data?(1.0) - buff = self.rsock.read(16384) - break if not buff - verify_channel - self.channel.send_data(buff) if buff - end - end - - while true - rssh.process(0.5) { true } - end - - rescue ::Exception => e - self.error = e - #::Kernel.warn "BOO: #{e.inspect}" - #::Kernel.warn e.backtrace.join("\n") - ensure - self.monitor.kill if self.monitor - end - - # Shut down the SSH session if requested - if(rcleanup) - rssh.close - end - end - end - - # - # Prevent a race condition - # - def verify_channel - while ! self.channel - raise EOFError if ! self.thread.alive? - ::IO.select(nil, nil, nil, 0.10) - end - end - -end -end -end - diff --git a/lib/net/ssh/config.rb b/lib/net/ssh/config.rb index c518cf1b98808..46d83734d1258 100644 --- a/lib/net/ssh/config.rb +++ b/lib/net/ssh/config.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH # The Net::SSH::Config class is used to parse OpenSSH configuration files, @@ -9,6 +8,8 @@ module Net; module SSH # # Only a subset of OpenSSH configuration options are understood: # + # * ChallengeResponseAuthentication => maps to the :auth_methods option challenge-response (then coleasced into keyboard-interactive) + # * KbdInteractiveAuthentication => maps to the :auth_methods keyboard-interactive # * Ciphers => maps to the :encryption option # * Compression => :compression # * CompressionLevel => :compression_level @@ -20,20 +21,30 @@ module Net; module SSH # * HostKeyAlias => :host_key_alias # * HostName => :host_name # * IdentityFile => maps to the :keys option + # * IdentitiesOnly => :keys_only # * Macs => maps to the :hmac option - # * PasswordAuthentication => maps to the :auth_methods option + # * PasswordAuthentication => maps to the :auth_methods option password # * Port => :port # * PreferredAuthentications => maps to the :auth_methods option + # * ProxyCommand => maps to the :proxy option + # * ProxyJump => maps to the :proxy option + # * PubKeyAuthentication => maps to the :auth_methods option # * RekeyLimit => :rekey_limit # * User => :user # * UserKnownHostsFile => :user_known_hosts_file + # * NumberOfPasswordPrompts => :number_of_password_prompts # # Note that you will never need to use this class directly--you can control # whether the OpenSSH configuration files are read by passing the :config # option to Net::SSH.start. (They are, by default.) class Config - class < 0 + hash[:keepalive] = true + hash[:keepalive_interval] = value.to_i + else + hash[:keepalive] = false + end + when :passwordauthentication + if value + (hash[:auth_methods] << 'password').uniq! + else + hash[:auth_methods].delete('password') + end + when :challengeresponseauthentication + if value + (hash[:auth_methods] << 'challenge-response').uniq! + else + hash[:auth_methods].delete('challenge-response') + end + when :kbdinteractiveauthentication + if value + (hash[:auth_methods] << 'keyboard-interactive').uniq! + else + hash[:auth_methods].delete('keyboard-interactive') + end + when :preferredauthentications + hash[:auth_methods] = value.split(/,/) # TODO we should place to preferred_auth_methods rather than auth_methods + when :proxycommand + if value and !(value =~ /^none$/) + require 'net/ssh/proxy/command' + hash[:proxy] = Net::SSH::Proxy::Command.new(value) + end + when :proxyjump + if value + require 'net/ssh/proxy/jump' + hash[:proxy] = Net::SSH::Proxy::Jump.new(value) + end + when :pubkeyauthentication + if value + (hash[:auth_methods] << 'publickey').uniq! + else + hash[:auth_methods].delete('publickey') + end + when :rekeylimit + hash[:rekey_limit] = interpret_size(value) + when :sendenv + multi_send_env = value.to_s.split(/\s+/) + hash[:send_env] = multi_send_env.map { |e| Regexp.new pattern2regex(e).source, false } + when :numberofpasswordprompts + hash[:number_of_password_prompts] = value.to_i + when *rename.keys + hash[rename[key]] = value + end + end + # Converts an ssh_config pattern into a regex for matching against # host names. def pattern2regex(pattern) - pattern = "^" + pattern.to_s.gsub(/\./, "\\."). - gsub(/\?/, '.'). - gsub(/\*/, '.*') + "$" - Regexp.new(pattern, true) + tail = pattern + prefix = "" + while !tail.empty? do + head,sep,tail = tail.partition(/[\*\?]/) + prefix = prefix + Regexp.quote(head) + case sep + when '*' + prefix += '.*' + when '?' + prefix += '.' + when '' + else + fail "Unpexpcted sep:#{sep}" + end + end + Regexp.new("^" + prefix + "$", true) end # Converts the given size into an integer number of bytes. @@ -176,6 +303,26 @@ def interpret_size(size) else size.to_i end end + + def merge_challenge_response_with_keyboard_interactive(hash) + if hash[:auth_methods].include?('challenge-response') + hash[:auth_methods].delete('challenge-response') + (hash[:auth_methods] << 'keyboard-interactive').uniq! + end + hash + end + + def included_file_paths(base_dir, config_paths) + tokenize_config_value(config_paths).flat_map do |path| + Dir.glob(File.expand_path(path, base_dir)).select { |f| File.file?(f) } + end + end + + # Tokenize string into tokens. + # A token is a word or a quoted sequence of words, separated by whitespaces. + def tokenize_config_value(str) + str.scan(/([^"\s]+)?(?:"([^"]+)")?\s*/).map(&:join) + end end end diff --git a/lib/net/ssh/connection/channel.rb b/lib/net/ssh/connection/channel.rb index cd6044fad9223..cac7211c06ad5 100644 --- a/lib/net/ssh/connection/channel.rb +++ b/lib/net/ssh/connection/channel.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/loggable' require 'net/ssh/connection/constants' require 'net/ssh/connection/term' @@ -108,15 +107,15 @@ class Channel # that time (see #do_open_confirmation). # # This also sets the default maximum packet size and maximum window size. - def initialize(connection, type, local_id, &on_confirm_open) + def initialize(connection, type, local_id, max_pkt_size = 0x8000, max_win_size = 0x20000, &on_confirm_open) self.logger = connection.logger @connection = connection @type = type @local_id = local_id - @local_maximum_packet_size = 0x10000 - @local_window_size = @local_maximum_window_size = 0x20000 + @local_maximum_packet_size = max_pkt_size + @local_window_size = @local_maximum_window_size = max_win_size @on_confirm_open = on_confirm_open @@ -127,7 +126,7 @@ def initialize(connection, type, local_id, &on_confirm_open) @pending_requests = [] @on_open_failed = @on_data = @on_extended_data = @on_process = @on_close = @on_eof = nil @on_request = {} - @closing = @eof = false + @closing = @eof = @sent_eof = @local_closed = @remote_closed = false end # A shortcut for accessing properties of the channel (see #properties). @@ -190,12 +189,12 @@ def env(variable_name, variable_value, &block) end # A hash of the valid PTY options (see #request_pty). - VALID_PTY_OPTIONS = { :term => "xterm", - :chars_wide => 80, - :chars_high => 24, - :pixels_wide => 640, - :pixels_high => 480, - :modes => {} } + VALID_PTY_OPTIONS = { term: "xterm", + chars_wide: 80, + chars_high: 24, + pixels_wide: 640, + pixels_high: 480, + modes: {} } # Requests that a pseudo-tty (or "pty") be made available for this channel. # This is useful when you want to invoke and interact with some kind of @@ -270,24 +269,33 @@ def wait connection.loop { active? } end - # Returns true if the channel is currently closing, but not actually - # closed. A channel is closing when, for instance, #close has been - # invoked, but the server has not yet responded with a CHANNEL_CLOSE - # packet of its own. + # True if close() has been called; NOTE: if the channel has data waiting to + # be sent then the channel will close after all the data is sent. See + # closed?() to determine if we have actually sent CHANNEL_CLOSE to server. + # This may be true for awhile before closed? returns true if we are still + # sending buffered output to server. def closing? @closing end - # Requests that the channel be closed. If the channel is already closing, - # this does nothing, nor does it do anything if the channel has not yet - # been confirmed open (see #do_open_confirmation). Otherwise, it sends a - # CHANNEL_CLOSE message and marks the channel as closing. + # True if we have sent CHANNEL_CLOSE to the remote server. + def local_closed? + @local_closed + end + + def remote_closed? + @remote_closed + end + + def remote_closed! + @remote_closed = true + end + + # Requests that the channel be closed. It only marks the channel to be closed + # the CHANNEL_CLOSE message will be sent from event loop def close return if @closing - if remote_id - @closing = true - connection.send_message(Buffer.from(:byte, CHANNEL_CLOSE, :long, remote_id)) - end + @closing = true end # Returns true if the local end of the channel has declared that no more @@ -299,10 +307,10 @@ def eof? # Tells the remote end of the channel that no more data is forthcoming # from this end of the channel. The remote end may still send data. + # The CHANNEL_EOF packet will be sent once the output buffer is empty. def eof! return if eof? @eof = true - connection.send_message(Buffer.from(:byte, CHANNEL_EOF, :long, remote_id)) end # If an #on_process handler has been set up, this will cause it to be @@ -311,6 +319,17 @@ def eof! def process @on_process.call(self) if @on_process enqueue_pending_output + + if @eof and not @sent_eof and output.empty? and remote_id and not @local_closed + connection.send_message(Buffer.from(:byte, CHANNEL_EOF, :long, remote_id)) + @sent_eof = true + end + + if @closing and not @local_closed and output.empty? and remote_id + connection.send_message(Buffer.from(:byte, CHANNEL_CLOSE, :long, remote_id)) + @local_closed = true + connection.cleanup_channel(self) + end end # Registers a callback to be invoked when data packets are received by the @@ -431,9 +450,9 @@ def on_open_failed(&block) # data, via data.read_string. (Not all SSH servers support this channel # request type.) # - # channel.on_request "exit-status" do |ch, data| - # puts "process terminated with exit status: #{data.read_long}" - # end + # channel.on_request "exit-status" do |ch, data| + # puts "process terminated with exit status: #{data.read_long}" + # end def on_request(type, &block) old, @on_request[type] = @on_request[type], block old @@ -463,6 +482,7 @@ def on_request(type, &block) # convenient helper methods (see #exec and #subsystem). def send_channel_request(request_name, *data, &callback) info { "sending channel request #{request_name.inspect}" } + fail "Channel open not yet confirmed, please call send_channel_request(or exec) from block of open_channel" unless remote_id msg = Buffer.from(:byte, CHANNEL_REQUEST, :long, remote_id, :string, request_name, :bool, !callback.nil?, *data) @@ -506,6 +526,7 @@ def do_open_confirmation(remote_id, max_window, max_packet) #:nodoc: @remote_window_size = @remote_maximum_window_size = max_window @remote_maximum_packet_size = max_packet connection.forward.agent(self) if connection.options[:forward_agent] && type == "session" + forward_local_env(connection.options[:send_env]) if connection.options[:send_env] @on_confirm_open.call(self) if @on_confirm_open end @@ -518,7 +539,7 @@ def do_open_failed(reason_code, description) @on_open_failed.call(self, reason_code, description) else raise ChannelOpenFailed.new(reason_code, description) - end + end end # Invoked when the server sends a CHANNEL_WINDOW_ADJUST packet, and @@ -592,7 +613,7 @@ def do_failure if callback = pending_requests.shift callback.call(self, false) else - error { "channel failure recieved with no pending request to handle it (bug?)" } + error { "channel failure received with no pending request to handle it (bug?)" } end end @@ -602,12 +623,18 @@ def do_success if callback = pending_requests.shift callback.call(self, true) else - error { "channel success recieved with no pending request to handle it (bug?)" } + error { "channel success received with no pending request to handle it (bug?)" } end end private + # Runs the SSH event loop until the remote confirmed channel open + # experimental api + def wait_until_open_confirmed + connection.loop { !remote_id } + end + # Updates the local window size by the given amount. If the window # size drops to less than half of the local maximum (an arbitrary # threshold), a CHANNEL_WINDOW_ADJUST message will be sent to the @@ -621,6 +648,25 @@ def update_local_window_size(size) @local_maximum_window_size += 0x20000 end end + + # Gets an +Array+ of local environment variables in the remote process' + # environment. + # A variable name can either be described by a +Regexp+ or +String+. + # + # channel.forward_local_env [/^GIT_.*$/, "LANG"] + def forward_local_env(env_variable_patterns) + Array(env_variable_patterns).each do |env_variable_pattern| + matched_variables = ENV.find_all do |env_name, _| + case env_variable_pattern + when Regexp then env_name =~ env_variable_pattern + when String then env_name == env_variable_pattern + end + end + matched_variables.each do |env_name, env_value| + self.env(env_name, env_value) + end + end + end end end; end; end diff --git a/lib/net/ssh/connection/constants.rb b/lib/net/ssh/connection/constants.rb index fa23803feb369..2c3df5af05cad 100644 --- a/lib/net/ssh/connection/constants.rb +++ b/lib/net/ssh/connection/constants.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH; module Connection # Definitions of constants that are specific to the connection layer of the @@ -31,4 +30,4 @@ module Constants end -end; end end +end; end end \ No newline at end of file diff --git a/lib/net/ssh/connection/event_loop.rb b/lib/net/ssh/connection/event_loop.rb new file mode 100644 index 0000000000000..f9d95eadb9e9d --- /dev/null +++ b/lib/net/ssh/connection/event_loop.rb @@ -0,0 +1,114 @@ +require 'net/ssh/loggable' +require 'net/ssh/ruby_compat' + +module Net; module SSH; module Connection + # EventLoop can be shared across multiple sessions + # + # one issue is with blocks passed to loop, etc. + # they should get current session as parameter, but in + # case you're using multiple sessions in an event loop it doesnt makes sense + # and we don't pass session. + class EventLoop + include Loggable + + def initialize(logger=nil) + self.logger = logger + @sessions = [] + end + + def register(session) + @sessions << session + end + + # process until timeout + # if a block is given a session will be removed from loop + # if block returns false for that session + def process(wait = nil, &block) + return false unless ev_preprocess(&block) + + ev_select_and_postprocess(wait) + end + + # process the event loop but only for the sepcified session + def process_only(session, wait = nil) + orig_sessions = @sessions + begin + @sessions = [session] + return false unless ev_preprocess + ev_select_and_postprocess(wait) + ensure + @sessions = orig_sessions + end + end + + # Call preprocess on each session. If block given and that + # block retuns false then we exit the processing + def ev_preprocess(&block) + return false if block_given? && !yield(self) + @sessions.each(&:ev_preprocess) + return false if block_given? && !yield(self) + return true + end + + def ev_select_and_postprocess(wait) + owners = {} + r = [] + w = [] + minwait = nil + @sessions.each do |session| + sr,sw,actwait = session.ev_do_calculate_rw_wait(wait) + minwait = actwait if actwait && (minwait.nil? || actwait < minwait) + r.push(*sr) + w.push(*sw) + sr.each { |ri| owners[ri] = session } + sw.each { |wi| owners[wi] = session } + end + + readers, writers, = Net::SSH::Compat.io_select(r, w, nil, minwait) + + fired_sessions = {} + + if readers + readers.each do |reader| + session = owners[reader] + (fired_sessions[session] ||= {r: [],w: []})[:r] << reader + end + end + if writers + writers.each do |writer| + session = owners[writer] + (fired_sessions[session] ||= {r: [],w: []})[:w] << writer + end + end + + fired_sessions.each do |s,rw| + s.ev_do_handle_events(rw[:r],rw[:w]) + end + + @sessions.each { |s| s.ev_do_postprocess(fired_sessions.key?(s)) } + true + end + end + + # optimized version for a single session + class SingleSessionEventLoop < EventLoop + # Compatibility for original single session event loops: + # we call block with session as argument + def ev_preprocess(&block) + return false if block_given? && !yield(@sessions.first) + @sessions.each(&:ev_preprocess) + return false if block_given? && !yield(@sessions.first) + return true + end + + def ev_select_and_postprocess(wait) + raise "Only one session expected" unless @sessions.count == 1 + session = @sessions.first + sr,sw,actwait = session.ev_do_calculate_rw_wait(wait) + readers, writers, = Net::SSH::Compat.io_select(sr, sw, nil, actwait) + + session.ev_do_handle_events(readers,writers) + session.ev_do_postprocess(!((readers.nil? || readers.empty?) && (writers.nil? || writers.empty?))) + end + end +end; end; end diff --git a/lib/net/ssh/connection/keepalive.rb b/lib/net/ssh/connection/keepalive.rb new file mode 100644 index 0000000000000..641ae57bfaf88 --- /dev/null +++ b/lib/net/ssh/connection/keepalive.rb @@ -0,0 +1,55 @@ +require 'net/ssh/loggable' +module Net; module SSH; module Connection + +class Keepalive + include Loggable + + def initialize(session) + @last_keepalive_sent_at = nil + @unresponded_keepalive_count = 0 + @session = session + self.logger = session.logger + end + + def options + @session.options + end + + def enabled? + options[:keepalive] + end + + def interval + options[:keepalive_interval] || Session::DEFAULT_IO_SELECT_TIMEOUT + end + + def should_send? + return false unless enabled? + return true unless @last_keepalive_sent_at + Time.now - @last_keepalive_sent_at >= interval + end + + def keepalive_maxcount + (options[:keepalive_maxcount] || 3).to_i + end + + def send_as_needed(was_events) + return if was_events + return unless should_send? + info { "sending keepalive #{@unresponded_keepalive_count}" } + + @unresponded_keepalive_count += 1 + @session.send_global_request("keepalive@openssh.com") { |success, response| + debug { "keepalive response successful. Missed #{@unresponded_keepalive_count-1} keepalives" } + @unresponded_keepalive_count = 0 + } + @last_keepalive_sent_at = Time.now + if keepalive_maxcount > 0 && @unresponded_keepalive_count > keepalive_maxcount + error { "Timeout, server #{@session.host} not responding. Missed #{@unresponded_keepalive_count-1} timeouts." } + @unresponded_keepalive_count = 0 + raise Net::SSH::Timeout, "Timeout, server #{@session.host} not responding." + end + end +end + +end; end; end diff --git a/lib/net/ssh/connection/session.rb b/lib/net/ssh/connection/session.rb index d63a77ae58c9a..a69b4f6e0c5ba 100644 --- a/lib/net/ssh/connection/session.rb +++ b/lib/net/ssh/connection/session.rb @@ -1,8 +1,10 @@ -# -*- coding: binary -*- require 'net/ssh/loggable' +require 'net/ssh/ruby_compat' require 'net/ssh/connection/channel' require 'net/ssh/connection/constants' require 'net/ssh/service/forward' +require 'net/ssh/connection/keepalive' +require 'net/ssh/connection/event_loop' module Net; module SSH; module Connection @@ -25,6 +27,9 @@ module Net; module SSH; module Connection class Session include Constants, Loggable + # Default IO.select timeout threshold + DEFAULT_IO_SELECT_TIMEOUT = 300 + # The underlying transport layer abstraction (see Net::SSH::Transport::Session). attr_reader :transport @@ -47,9 +52,6 @@ class Session # The list of callbacks for pending requests. See #send_global_request. attr_reader :pending_requests #:nodoc: - # when a successful auth is made, note the auth info if session.options[:record_auth_info] - attr_accessor :auth_info - class NilChannel def initialize(session) @session = session @@ -75,6 +77,14 @@ def initialize(transport, options={}) @channel_open_handlers = {} @on_global_request = {} @properties = (options[:properties] || {}).dup + + @max_pkt_size = (options.key?(:max_pkt_size) ? options[:max_pkt_size] : 0x8000) + @max_win_size = (options.key?(:max_win_size) ? options[:max_win_size] : 0x20000) + + @keepalive = Keepalive.new(self) + + @event_loop = options[:event_loop] || SingleSessionEventLoop.new + @event_loop.register(self) end # Retrieves a custom property from this instance. This can be used to @@ -110,7 +120,11 @@ def closed? def close info { "closing remaining channels (#{channels.length} open)" } channels.each { |id, channel| channel.close } - loop { channels.any? } + begin + loop(0.1) { channels.any? } + rescue Net::SSH::Disconnect + raise unless channels.empty? + end transport.close end @@ -162,6 +176,15 @@ def busy?(include_invisible=false) def loop(wait=nil, &block) running = block || Proc.new { busy? } loop_forever { break unless process(wait, &running) } + begin + process(0) + rescue IOError => e + if e.message =~ /closed/ + debug { "stream was closed after loop => shallowing exception so it will be re-raised in next loop" } + else + raise + end + end end # The core of the event loop. It processes a single iteration of the event @@ -180,6 +203,8 @@ def loop(wait=nil, &block) # This will also cause all active channels to be processed once each (see # Net::SSH::Connection::Channel#on_process). # + # TODO revise example + # # # process multiple Net::SSH connections in parallel # connections = [ # Net::SSH.start("host1", ...), @@ -197,13 +222,10 @@ def loop(wait=nil, &block) # break if connections.empty? # end def process(wait=nil, &block) - return false unless preprocess(&block) - - r = listeners.keys - w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? } - readers, writers, = IO.select(r, w, nil, wait) - - postprocess(readers, writers) + @event_loop.process(wait, &block) + rescue + force_channel_cleanup_on_close if closed? + raise end # This is called internally as part of #process. It dispatches any @@ -211,19 +233,38 @@ def process(wait=nil, &block) # for any active channels. If a block is given, it is invoked at the # start of the method and again at the end, and if the block ever returns # false, this method returns false. Otherwise, it returns true. - def preprocess + def preprocess(&block) return false if block_given? && !yield(self) - dispatch_incoming_packets - channels.each { |id, channel| channel.process unless channel.closing? } + ev_preprocess(&block) return false if block_given? && !yield(self) return true end - # This is called internally as part of #process. It loops over the given - # arrays of reader IO's and writer IO's, processing them as needed, and + # Called by event loop to process available data before going to + # event multiplexing + def ev_preprocess(&block) + dispatch_incoming_packets(raise_disconnect_errors: false) + each_channel { |id, channel| channel.process unless channel.local_closed? } + end + + # Returns the file descriptors the event loop should wait for read/write events, + # we also return the max wait + def ev_do_calculate_rw_wait(wait) + r = listeners.keys + w = r.select { |w2| w2.respond_to?(:pending_write?) && w2.pending_write? } + [r,w,io_select_wait(wait)] + end + + # This is called internally as part of #process. + def postprocess(readers, writers) + ev_do_handle_events(readers, writers) + end + + # It loops over the given arrays of reader IO's and writer IO's, + # processing them as needed, and # then calls Net::SSH::Transport::Session#rekey_as_needed to allow the # transport layer to rekey. Then returns true. - def postprocess(readers, writers) + def ev_do_handle_events(readers, writers) Array(readers).each do |reader| if listeners[reader] listeners[reader].call(reader) @@ -238,10 +279,14 @@ def postprocess(readers, writers) Array(writers).each do |writer| writer.send_pending end + end + # calls Net::SSH::Transport::Session#rekey_as_needed to allow the + # transport layer to rekey + def ev_do_postprocess(was_events) + @keepalive.send_as_needed(was_events) transport.rekey_as_needed - - return true + true end # Send a global request of the given type. The +extra+ parameters must @@ -289,8 +334,8 @@ def send_global_request(type, *extra, &callback) # channel.wait def open_channel(type="session", *extra, &on_confirm) local_id = get_next_channel_id - channel = Channel.new(self, type, local_id, &on_confirm) + channel = Channel.new(self, type, local_id, @max_pkt_size, @max_win_size, &on_confirm) msg = Buffer.from(:byte, CHANNEL_OPEN, :string, type, :long, local_id, :long, channel.local_maximum_window_size, :long, channel.local_maximum_packet_size, *extra) @@ -299,6 +344,15 @@ def open_channel(type="session", *extra, &on_confirm) channels[local_id] = channel end + class StringWithExitstatus < String + def initialize(str, exitstatus) + super(str) + @exitstatus = exitstatus + end + + attr_reader :exitstatus + end + # A convenience method for executing a command and interacting with it. If # no block is given, all output is printed via $stdout and $stderr. Otherwise, # the block is called for each data and extended data packet, with three @@ -319,11 +373,21 @@ def open_channel(type="session", *extra, &on_confirm) # puts data # end # end - def exec(command, &block) + def exec(command, status: nil, &block) open_channel do |channel| channel.exec(command) do |ch, success| raise "could not execute command: #{command.inspect}" unless success - + + if status + channel.on_request("exit-status") do |ch2,data| + status[:exit_code] = data.read_long + end + + channel.on_request("exit-signal") do |ch2, data| + status[:exit_signal] = data.read_long + end + end + channel.on_data do |ch2, data| if block block.call(ch2, :stdout, data) @@ -344,20 +408,26 @@ def exec(command, &block) end # Same as #exec, except this will block until the command finishes. Also, - # if a block is not given, this will return all output (stdout and stderr) + # if no block is given, this will return all output (stdout and stderr) # as a single string. # # matches = ssh.exec!("grep something /some/files") - def exec!(command, &block) - block ||= Proc.new do |ch, type, data| + # + # the returned string has an exitstatus method to query it's exit satus + def exec!(command, status: nil, &block) + block_or_concat = block || Proc.new do |ch, type, data| ch[:result] ||= "" ch[:result] << data end - channel = exec(command, &block) + status ||= {} + channel = exec(command, status: status, &block_or_concat) channel.wait - return channel[:result] + channel[:result] ||= "" unless block + channel[:result] &&= channel[:result].force_encoding("UTF-8") unless block + + StringWithExitstatus.new(channel[:result], status[:exit_code]) if channel[:result] end # Enqueues a message to be sent to the server as soon as the socket is @@ -387,7 +457,7 @@ def send_message(message) # ch.exec "/some/process/that/wants/input" do |ch, success| # abort "can't execute!" unless success # - # io = Rex::Socket::Tcp.create( ... somewhere, ... port ... ) + # io = TCPSocket.new(somewhere, port) # io.extend(Net::SSH::BufferedIo) # ssh.listen_to(io) # @@ -446,11 +516,31 @@ def on_global_request(type, &block) old end + def cleanup_channel(channel) + if channel.local_closed? and channel.remote_closed? + info { "#{host} delete channel #{channel.local_id} which closed locally and remotely" } + channels.delete(channel.local_id) + end + end + + # If the #preprocess and #postprocess callbacks for this session need to run + # periodically, this method returns the maximum number of seconds which may + # pass between callbacks. + def max_select_wait_time + @keepalive.interval if @keepalive.enabled? + end + + private + # iterate channels with the posibility of callbacks opening new channels during the iteration + def each_channel(&block) + channels.dup.each(&block) + end + # Read all pending packets from the connection and dispatch them as # appropriate. Returns as soon as there are no more pending packets. - def dispatch_incoming_packets + def dispatch_incoming_packets(raise_disconnect_errors: true) while packet = transport.poll_message unless MAP.key?(packet.type) raise Net::SSH::Exception, "unexpected response #{packet.type} (#{packet.inspect})" @@ -458,6 +548,9 @@ def dispatch_incoming_packets send(MAP[packet.type], packet) end + rescue + force_channel_cleanup_on_close if closed? + raise if raise_disconnect_errors || !$!.is_a?(Net::SSH::Disconnect) end # Returns the next available channel id to be assigned, and increments @@ -466,6 +559,20 @@ def get_next_channel_id @channel_id_counter += 1 end + def force_channel_cleanup_on_close + channels.each do |id, channel| + channel_closed(channel) + end + end + + def channel_closed(channel) + channel.remote_closed! + channel.close + + cleanup_channel(channel) + channel.do_close + end + # Invoked when a global request is received. The registered global # request callback will be invoked, if one exists, and the necessary # reply returned. @@ -506,7 +613,8 @@ def channel_open(packet) info { "channel open #{packet[:channel_type]}" } local_id = get_next_channel_id - channel = Channel.new(self, packet[:channel_type], local_id) + + channel = Channel.new(self, packet[:channel_type], local_id, @max_pkt_size, @max_win_size) channel.do_open_confirmation(packet[:remote_id], packet[:window_size], packet[:packet_size]) callback = channel_open_handlers[packet[:channel_type]] @@ -573,10 +681,7 @@ def channel_close(packet) info { "channel_close: #{packet[:local_id]}" } channel = channels[packet[:local_id]] - channel.close - - channels.delete(packet[:local_id]) - channel.do_close + channel_closed(channel) end def channel_success(packet) @@ -589,6 +694,10 @@ def channel_failure(packet) channels[packet[:local_id]].do_failure end + def io_select_wait(wait) + [wait, max_select_wait_time].compact.min + end + MAP = Constants.constants.inject({}) do |memo, name| value = const_get(name) next unless Integer === value diff --git a/lib/net/ssh/connection/term.rb b/lib/net/ssh/connection/term.rb index efecea71246ab..3e1caa5d0c966 100644 --- a/lib/net/ssh/connection/term.rb +++ b/lib/net/ssh/connection/term.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH; module Connection # These constants are used when requesting a pseudo-terminal (via diff --git a/lib/net/ssh/errors.rb b/lib/net/ssh/errors.rb index 009f896ea36ac..84bcbdf99c98d 100644 --- a/lib/net/ssh/errors.rb +++ b/lib/net/ssh/errors.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH # A general exception class, to act as the ancestor of all other Net::SSH # exception classes. @@ -6,25 +5,32 @@ class Exception < ::RuntimeError; end # This exception is raised when authentication fails (whether it be # public key authentication, password authentication, or whatever). - class AuthenticationFailed < Exception; end + class AuthenticationFailed < Net::SSH::Exception; end + + # This exception is raised when a connection attempt times out. + class ConnectionTimeout < Net::SSH::Exception; end # This exception is raised when the remote host has disconnected # unexpectedly. - class Disconnect < Exception; end + class Disconnect < Net::SSH::Exception; end + + # This exception is raised when the remote host has disconnected/ + # timeouted unexpectedly. + class Timeout < Disconnect; end # This exception is primarily used internally, but if you have a channel # request handler (see Net::SSH::Connection::Channel#on_request) that you # want to fail in such a way that the server knows it failed, you can # raise this exception in the handler and Net::SSH will translate that into # a "channel failure" message. - class ChannelRequestFailed < Exception; end + class ChannelRequestFailed < Net::SSH::Exception; end # This is exception is primarily used internally, but if you have a channel # open handler (see Net::SSH::Connection::Session#on_open_channel) and you # want to fail in such a way that the server knows it failed, you can # raise this exception in the handler and Net::SSH will translate that into # a "channel open failed" message. - class ChannelOpenFailed < Exception + class ChannelOpenFailed < Net::SSH::Exception attr_reader :code, :reason def initialize(code, reason) @@ -33,12 +39,10 @@ def initialize(code, reason) end end - # Raised when the cached key for a particular host does not match the - # key given by the host, which can be indicative of a man-in-the-middle - # attack. When rescuing this exception, you can inspect the key fingerprint - # and, if you want to proceed anyway, simply call the remember_host! - # method on the exception, and then retry. - class HostKeyMismatch < Exception + # Base class for host key exceptions. When rescuing this exception, you can + # inspect the key fingerprint and, if you want to proceed anyway, simply call + # the remember_host! method on the exception, and then retry. + class HostKeyError < Net::SSH::Exception # the callback to use when #remember_host! is called attr_writer :callback #:nodoc: @@ -83,4 +87,18 @@ def remember_host! @callback.call end end + + # Raised when the cached key for a particular host does not match the + # key given by the host, which can be indicative of a man-in-the-middle + # attack. When rescuing this exception, you can inspect the key fingerprint + # and, if you want to proceed anyway, simply call the remember_host! + # method on the exception, and then retry. + class HostKeyMismatch < HostKeyError; end + + # Raised when there is no cached key for a particular host, which probably + # means that the host has simply not been seen before. + # When rescuing this exception, you can inspect the key fingerprint and, if + # you want to proceed anyway, simply call the remember_host! method on the + # exception, and then retry. + class HostKeyUnknown < HostKeyError; end end; end diff --git a/lib/net/ssh/key_factory.rb b/lib/net/ssh/key_factory.rb index 9d5d461aec478..a108dea49a64c 100644 --- a/lib/net/ssh/key_factory.rb +++ b/lib/net/ssh/key_factory.rb @@ -1,7 +1,8 @@ -# -*- coding: binary -*- require 'net/ssh/transport/openssl' require 'net/ssh/prompt' +require 'net/ssh/authentication/ed25519_loader' + module Net; module SSH # A factory class for returning new Key classes. It is used for obtaining @@ -12,18 +13,20 @@ module Net; module SSH # klass = Net::SSH::KeyFactory.get("rsa") # assert klass.is_a?(OpenSSL::PKey::RSA) # - # key = Net::SSH::KeyFacory.load_public_key("~/.ssh/id_dsa.pub") + # key = Net::SSH::KeyFactory.load_public_key("~/.ssh/id_dsa.pub") class KeyFactory # Specifies the mapping of SSH names to OpenSSL key classes. MAP = { "dh" => OpenSSL::PKey::DH, "rsa" => OpenSSL::PKey::RSA, - "dsa" => OpenSSL::PKey::DSA + "dsa" => OpenSSL::PKey::DSA, } + if defined?(OpenSSL::PKey::EC) + MAP["ecdsa"] = OpenSSL::PKey::EC + MAP["ed25519"] = Net::SSH::Authentication::ED25519::PrivKey if defined? Net::SSH::Authentication::ED25519 + end class < e - if encrypted_key && ask_passphrase - tries += 1 - if tries <= 3 - passphrase = prompt("Enter passphrase for #{filename}:", false) - retry + prompter = nil + result = + begin + key_read[data, passphrase || 'invalid'] + rescue *error_classes + if encrypted_key && ask_passphrase + tries += 1 + if tries <= 3 + prompter ||= prompt.start(type: 'private_key', filename: filename, sha: Digest::SHA256.digest(data)) + passphrase = prompter.ask("Enter passphrase for #{filename}:", false) + retry + else + raise + end else raise end - else - raise end - end + prompter.success if prompter + result end # Loads a public key from a file. It will correctly determine whether @@ -88,7 +88,13 @@ def load_public_key(filename) # the file describes an RSA or DSA key, and will load it # appropriately. The new public key is returned. def load_data_public_key(data, filename="") - type, blob = data.split(/ /) + fields = data.split(/ /) + + blob = nil + begin + blob = fields.shift + end while !blob.nil? && !/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match(blob) + blob = fields.shift raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil? @@ -96,6 +102,29 @@ def load_data_public_key(data, filename="") reader = Net::SSH::Buffer.new(blob) reader.read_key or raise OpenSSL::PKey::PKeyError, "not a public key #{filename.inspect}" end + + private + + # Determine whether the file describes an RSA or DSA key, and return how load it + # appropriately. + def classify_key(data, filename) + if data.match(/-----BEGIN OPENSSH PRIVATE KEY-----/) + Net::SSH::Authentication::ED25519Loader.raiseUnlessLoaded("OpenSSH keys only supported if ED25519 is available") + return ->(key_data, passphrase) { Net::SSH::Authentication::ED25519::PrivKey.read(key_data, passphrase) }, [ArgumentError] + elsif OpenSSL::PKey.respond_to?(:read) + return ->(key_data, passphrase) { OpenSSL::PKey.read(key_data, passphrase) }, [ArgumentError, OpenSSL::PKey::PKeyError] + elsif data.match(/-----BEGIN DSA PRIVATE KEY-----/) + return ->(key_data, passphrase) { OpenSSL::PKey::DSA.new(key_data, passphrase) }, [OpenSSL::PKey::DSAError] + elsif data.match(/-----BEGIN RSA PRIVATE KEY-----/) + return ->(key_data, passphrase) { OpenSSL::PKey::RSA.new(key_data, passphrase) }, [OpenSSL::PKey::RSAError] + elsif data.match(/-----BEGIN EC PRIVATE KEY-----/) && defined?(OpenSSL::PKey::EC) + return ->(key_data, passphrase) { OpenSSL::PKey::EC.new(key_data, passphrase) }, [OpenSSL::PKey::ECError] + elsif data.match(/-----BEGIN (.+) PRIVATE KEY-----/) + raise OpenSSL::PKey::PKeyError, "not a supported key type '#{$1}'" + else + raise OpenSSL::PKey::PKeyError, "not a private key (#{filename})" + end + end end end diff --git a/lib/net/ssh/known_hosts.rb b/lib/net/ssh/known_hosts.rb index be0870ab4d0e5..58a1e414b806a 100644 --- a/lib/net/ssh/known_hosts.rb +++ b/lib/net/ssh/known_hosts.rb @@ -1,9 +1,37 @@ -# -*- coding: binary -*- require 'strscan' +require 'openssl' +require 'base64' require 'net/ssh/buffer' module Net; module SSH + # Represents the result of a search in known hosts + # see search_for + class HostKeys + include Enumerable + attr_reader :host + + def initialize(host_keys, host, known_hosts, options = {}) + @host_keys = host_keys + @host = host + @known_hosts = known_hosts + @options = options + end + + def add_host_key(key) + @known_hosts.add(@host, key, @options) + @host_keys.push(key) + end + + def each(&block) + @host_keys.each(&block) + end + + def empty? + @host_keys.empty? + end + end + # Searches an OpenSSH-style known-host file for a given host, and returns all # matching keys. This is used to implement host-key verification, as well as # to determine what key a user prefers to use for a given host. @@ -11,11 +39,23 @@ module Net; module SSH # This is used internally by Net::SSH, and will never need to be used directly # by consumers of the library. class KnownHosts + + if defined?(OpenSSL::PKey::EC) + SUPPORTED_TYPE = %w(ssh-rsa ssh-dss + ecdsa-sha2-nistp256 + ecdsa-sha2-nistp384 + ecdsa-sha2-nistp521) + else + SUPPORTED_TYPE = %w(ssh-rsa ssh-dss) + end + + class < proxy) do |ssh| + # ... + # end + class Command + + # The command line template + attr_reader :command_line_template + + # The command line for the session + attr_reader :command_line + + # Create a new socket factory that tunnels via a command executed + # with the user's shell, which is composed from the given command + # template. In the command template, `%h' will be substituted by + # the host name to connect and `%p' by the port. + def initialize(command_line_template) + @command_line_template = command_line_template + @command_line = nil + end + + # Return a new socket connected to the given host and port via the + # proxy that was requested when the socket factory was instantiated. + def open(host, port, connection_options = nil) + command_line = @command_line_template.gsub(/%(.)/) { + case $1 + when 'h' + host + when 'p' + port.to_s + when 'r' + remote_user = connection_options && connection_options[:remote_user] + if remote_user + remote_user + else + raise ArgumentError, "remote user name not available" + end + when '%' + '%' + else + raise ArgumentError, "unknown key: #{$1}" + end + } + begin + io = IO.popen(command_line, "r+") + if result = Net::SSH::Compat.io_select([io], nil, [io], 60) + if result.last.any? || io.eof? + io.close + raise "command failed" + end + else + raise "command timed out" + end + rescue => e + raise ConnectError, "#{e}: #{command_line}" + end + @command_line = command_line + if Gem.win_platform? + # read_nonblock and write_nonblock are not available on Windows + # pipe. Use sysread and syswrite as a replacement works. + def io.send(data, flag) + syswrite(data) + end + + def io.recv(size) + sysread(size) + end + else + def io.send(data, flag) + begin + result = write_nonblock(data) + rescue IO::WaitWritable, Errno::EINTR + IO.select(nil, [self]) + retry + end + result + end + + def io.recv(size) + begin + result = read_nonblock(size) + rescue IO::WaitReadable, Errno::EINTR + timeout_in_seconds = 20 + if IO.select([self], nil, [self], timeout_in_seconds) == nil + raise "Unexpected spurious read wakeup" + end + retry + end + result + end + end + io + end + end + +end; end; end diff --git a/lib/net/ssh/proxy/errors.rb b/lib/net/ssh/proxy/errors.rb index 7decc82596a47..6eb3501a93afa 100644 --- a/lib/net/ssh/proxy/errors.rb +++ b/lib/net/ssh/proxy/errors.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/errors' module Net; module SSH; module Proxy diff --git a/lib/net/ssh/proxy/http.rb b/lib/net/ssh/proxy/http.rb index 4d2757df5852d..49b169573f46f 100644 --- a/lib/net/ssh/proxy/http.rb +++ b/lib/net/ssh/proxy/http.rb @@ -1,5 +1,4 @@ -# -*- coding: binary -*- -require 'rex/socket' +require 'socket' require 'net/ssh/proxy/errors' module Net; module SSH; module Proxy @@ -9,7 +8,7 @@ module Net; module SSH; module Proxy # # require 'net/ssh/proxy/http' # - # proxy = Net::SSH::Proxy::HTTP.new('proxy.host', proxy_port) + # proxy = Net::SSH::Proxy::HTTP.new('proxy_host', proxy_port) # Net::SSH.start('host', 'user', :proxy => proxy) do |ssh| # ... # end @@ -17,7 +16,7 @@ module Net; module SSH; module Proxy # If the proxy requires authentication, you can pass :user and :password # to the proxy's constructor: # - # proxy = Net::SSH::Proxy::HTTP.new('proxy.host', proxy_port, + # proxy = Net::SSH::Proxy::HTTP.new('proxy_host', proxy_port, # :user => "user", :password => "password") # # Note that HTTP digest authentication is not supported; Basic only at @@ -49,19 +48,8 @@ def initialize(proxy_host, proxy_port=80, options={}) # Return a new socket connected to the given host and port via the # proxy that was requested when the socket factory was instantiated. - def open(host, port) - socket = Rex::Socket::Tcp.create( - 'PeerHost' => proxy_host, - 'PeerPort' => proxy_port, - 'Context' => { - 'Msf' => options[:msframework], - 'MsfExploit' => options[:msfmodule] - } - ) - # Tell MSF to automatically close this socket on error or completion... - # This prevents resource leaks. - options[:msfmodule].add_socket(@socket) if options[:msfmodule] - + def open(host, port, connection_options) + socket = establish_connection(connection_options[:timeout]) socket.write "CONNECT #{host}:#{port} HTTP/1.0\r\n" if options[:user] @@ -79,13 +67,18 @@ def open(host, port) raise ConnectError, resp.inspect end - private + protected + + def establish_connection(connect_timeout) + Socket.tcp(proxy_host, proxy_port, nil, nil, + connect_timeout: connect_timeout) + end def parse_response(socket) version, code, reason = socket.gets.chomp.split(/ /, 3) headers = {} - while (line = socket.gets.chomp) != "" + while (line = socket.gets) && (line.chomp! != "") name, value = line.split(/:/, 2) headers[name.strip] = value.strip end @@ -94,13 +87,12 @@ def parse_response(socket) body = socket.read(headers["Content-Length"].to_i) end - return { :version => version, - :code => code.to_i, - :reason => reason, - :headers => headers, - :body => body } + return { version: version, + code: code.to_i, + reason: reason, + headers: headers, + body: body } end - end end; end; end diff --git a/lib/net/ssh/proxy/https.rb b/lib/net/ssh/proxy/https.rb new file mode 100644 index 0000000000000..e36ea0a4971c8 --- /dev/null +++ b/lib/net/ssh/proxy/https.rb @@ -0,0 +1,49 @@ +require 'socket' +require 'openssl' +require 'net/ssh/proxy/errors' +require 'net/ssh/proxy/http' + +module Net; module SSH; module Proxy + + # A specialization of the HTTP proxy which encrypts the whole connection + # using OpenSSL. This has the advantage that proxy authentication + # information is not sent in plaintext. + class HTTPS < HTTP + + # Create a new socket factory that tunnels via the given host and + # port. The +options+ parameter is a hash of additional settings that + # can be used to tweak this proxy connection. In addition to the options + # taken by Net::SSH::Proxy::HTTP it supports: + # + # * :ssl_context => the SSL configuration to use for the connection + def initialize(proxy_host, proxy_port=80, options={}) + @ssl_context = options.delete(:ssl_context) || + OpenSSL::SSL::SSLContext.new + super(proxy_host, proxy_port, options) + end + + protected + + # Shim to make OpenSSL::SSL::SSLSocket behave like a regular TCPSocket + # for all intents and purposes of Net::SSH::BufferedIo + module SSLSocketCompatibility + def self.extended(object) #:nodoc: + object.define_singleton_method(:recv, object.method(:sysread)) + object.sync_close = true + end + + def send(data, _opts) + syswrite(data) + end + end + + def establish_connection(connect_timeout) + plain_socket = super(connect_timeout) + OpenSSL::SSL::SSLSocket.new(plain_socket, @ssl_context).tap do |socket| + socket.extend(SSLSocketCompatibility) + socket.connect + end + end + end + +end; end; end diff --git a/lib/net/ssh/proxy/jump.rb b/lib/net/ssh/proxy/jump.rb new file mode 100644 index 0000000000000..01aefeff9381d --- /dev/null +++ b/lib/net/ssh/proxy/jump.rb @@ -0,0 +1,53 @@ +require 'uri' +require 'net/ssh/proxy/command' + +module Net; module SSH; module Proxy + + # An implementation of a jump proxy. To use it, instantiate it, + # then pass the instantiated object via the :proxy key to + # Net::SSH.start: + # + # require 'net/ssh/proxy/jump' + # + # proxy = Net::SSH::Proxy::Jump.new('user@proxy') + # Net::SSH.start('host', 'user', :proxy => proxy) do |ssh| + # ... + # end + class Jump < Command + + # The jump proxies + attr_reader :jump_proxies + + # Create a new socket factory that tunnels via multiple jump proxes as + # [user@]host[:port]. + def initialize(jump_proxies) + @jump_proxies = jump_proxies + end + + # Return a new socket connected to the given host and port via the jump + # proxy that was requested when the socket factory was instantiated. + def open(host, port, connection_options = nil) + build_proxy_command_equivalent(connection_options) + super + end + + # We cannot build the ProxyCommand template until we know if the :config + # option was specified during `Net::SSH.start`. + def build_proxy_command_equivalent(connection_options = nil) + first_jump, extra_jumps = jump_proxies.split(",", 2) + config = connection_options && connection_options[:config] + uri = URI.parse("ssh://#{first_jump}") + + template = "ssh" + template << " -l #{uri.user}" if uri.user + template << " -p #{uri.port}" if uri.port + template << " -J #{extra_jumps}" if extra_jumps + template << " -F #{config}" if config != true && config + template << " -W %h:%p " + template << uri.host + + @command_line_template = template + end + end + +end; end; end diff --git a/lib/net/ssh/proxy/socks4.rb b/lib/net/ssh/proxy/socks4.rb index 4e69bb0ca51a2..b678399ca1b38 100644 --- a/lib/net/ssh/proxy/socks4.rb +++ b/lib/net/ssh/proxy/socks4.rb @@ -1,5 +1,4 @@ -# -*- coding: binary -*- -require 'rex/socket' +require 'socket' require 'resolv' require 'ipaddr' require 'net/ssh/proxy/errors' @@ -48,19 +47,9 @@ def initialize(proxy_host, proxy_port=1080, options={}) # Return a new socket connected to the given host and port via the # proxy that was requested when the socket factory was instantiated. - def open(host, port) - socket = Rex::Socket::Tcp.create( - 'PeerHost' => proxy_host, - 'PeerPort' => proxy_port, - 'Context' => { - 'Msf' => options[:msframework], - 'MsfExploit' => options[:msfmodule] - } - ) - # Tell MSF to automatically close this socket on error or completion... - # This prevents resource leaks. - options[:msfmodule].add_socket(@socket) if options[:msfmodule] - + def open(host, port, connection_options) + socket = Socket.tcp(proxy_host, proxy_port, nil, nil, + connect_timeout: connection_options[:timeout]) ip_addr = IPAddr.new(Resolv.getaddress(host)) packet = [VERSION, CONNECT, port.to_i, ip_addr.to_i, options[:user]].pack("CCnNZ*") diff --git a/lib/net/ssh/proxy/socks5.rb b/lib/net/ssh/proxy/socks5.rb index 0cab0cbb12923..8ffb4c192616a 100644 --- a/lib/net/ssh/proxy/socks5.rb +++ b/lib/net/ssh/proxy/socks5.rb @@ -1,5 +1,4 @@ -# -*- coding: binary -*- -require 'rex/socket' +require 'socket' require 'net/ssh/ruby_compat' require 'net/ssh/proxy/errors' @@ -63,18 +62,9 @@ def initialize(proxy_host, proxy_port=1080, options={}) # Return a new socket connected to the given host and port via the # proxy that was requested when the socket factory was instantiated. - def open(host, port) - socket = Rex::Socket::Tcp.create( - 'PeerHost' => proxy_host, - 'PeerPort' => proxy_port, - 'Context' => { - 'Msf' => options[:msframework], - 'MsfExploit' => options[:msfmodule] - } - ) - # Tell MSF to automatically close this socket on error or completion... - # This prevents resource leaks. - options[:msfmodule].add_socket(@socket) if options[:msfmodule] + def open(host, port, connection_options) + socket = Socket.tcp(proxy_host, proxy_port, nil, nil, + connect_timeout: connection_options[:timeout]) methods = [METHOD_NO_AUTH] methods << METHOD_PASSWD if options[:user] @@ -105,11 +95,24 @@ def open(host, port) packet << [port].pack("n") socket.send packet, 0 - - version, reply, = socket.recv(4).unpack("C*") - len = socket.recv(1).getbyte(0) - socket.recv(len + 2) - + + version, reply, = socket.recv(2).unpack("C*") + socket.recv(1) + address_type = socket.recv(1).getbyte(0) + case address_type + when 1 + socket.recv(4) # get four bytes for IPv4 address + when 3 + len = socket.recv(1).getbyte(0) + hostname = socket.recv(len) + when 4 + ipv6addr hostname = socket.recv(16) + else + socket.close + raise ConnectError, "Illegal response type" + end + portnum = socket.recv(2) + unless reply == SUCCESS socket.close raise ConnectError, "#{reply}" diff --git a/lib/net/ssh/ruby_compat.rb b/lib/net/ssh/ruby_compat.rb index a1092f1d65257..d4abeb4755664 100644 --- a/lib/net/ssh/ruby_compat.rb +++ b/lib/net/ssh/ruby_compat.rb @@ -1,8 +1,24 @@ -# -*- coding: binary -*- +require 'thread' + class String if RUBY_VERSION < "1.9" def getbyte(index) self[index] end + def setbyte(index, c) + self[index] = c + end end end + +module Net; module SSH + + # This class contains miscellaneous patches and workarounds + # for different ruby implementations. + class Compat + def self.io_select(*params) + IO.select(*params) + end + end + +end; end diff --git a/lib/net/ssh/service/forward.rb b/lib/net/ssh/service/forward.rb index 52e87efea3bf9..e64440bc176c9 100644 --- a/lib/net/ssh/service/forward.rb +++ b/lib/net/ssh/service/forward.rb @@ -1,4 +1,4 @@ -# -*- coding: binary -*- +# -*- coding: utf-8 -*- require 'net/ssh/loggable' module Net; module SSH; module Service @@ -28,6 +28,7 @@ def initialize(session) @remote_forwarded_ports = {} @local_forwarded_ports = {} @agent_forwarded = false + @local_forwarded_sockets = {} session.on_open_channel('forwarded-tcpip', &method(:forwarded_tcpip)) session.on_open_channel('auth-agent', &method(:auth_agent_channel)) @@ -47,44 +48,59 @@ def initialize(session) # If three arguments are given, it is as if the local bind address is # "127.0.0.1", and the rest are applied as above. # + # To request an ephemeral port on the remote server, provide 0 (zero) for + # the port number. In all cases, this method will return the port that + # has been assigned. + # # ssh.forward.local(1234, "www.capify.org", 80) - # ssh.forward.local("0.0.0.0", 1234, "www.capify.org", 80) + # assigned_port = ssh.forward.local("0.0.0.0", 0, "www.capify.org", 80) def local(*args) if args.length < 3 || args.length > 4 raise ArgumentError, "expected 3 or 4 parameters, got #{args.length}" end - bind_address = "127.0.0.1" - bind_address = args.shift if args.first.is_a?(String) && args.first =~ /\D/ + local_port_type = :long + + socket = begin + if defined?(UNIXServer) and args.first.class == UNIXServer + local_port_type = :string + args.shift + else + bind_address = "127.0.0.1" + bind_address = args.shift if args.first.is_a?(String) && args.first =~ /\D/ + local_port = args.shift.to_i + local_port_type = :long + TCPServer.new(bind_address, local_port) + end + end - local_port = args.shift.to_i + local_port = socket.addr[1] if local_port == 0 # ephemeral port was requested remote_host = args.shift remote_port = args.shift.to_i - socket = TCPServer.new(bind_address, local_port) - @local_forwarded_ports[[local_port, bind_address]] = socket session.listen_to(socket) do |server| client = server.accept - debug { "received connection on #{bind_address}:#{local_port}" } + debug { "received connection on #{socket}" } - channel = session.open_channel("direct-tcpip", :string, remote_host, :long, remote_port, :string, bind_address, :long, local_port) do |achannel| + channel = session.open_channel("direct-tcpip", :string, remote_host, :long, remote_port, :string, bind_address, local_port_type, local_port) do |achannel| achannel.info { "direct channel established" } end prepare_client(client, channel, :local) - + channel.on_open_failed do |ch, code, description| channel.error { "could not establish direct channel: #{description} (#{code})" } + session.stop_listening_to(channel[:socket]) channel[:socket].close end end + + local_port end - # Terminates an active local forwarded port. If no such forwarded port - # exists, this will raise an exception. Otherwise, the forwarded connection - # is terminated. + # Terminates an active local forwarded port. # # ssh.forward.cancel_local(1234) # ssh.forward.cancel_local(1234, "0.0.0.0") @@ -103,6 +119,57 @@ def active_locals @local_forwarded_ports.keys end + # Starts listening for connections on the local host, and forwards them + # to the specified remote socket via the SSH connection. This will + # (re)create the local socket file. The remote server needs to have the + # socket file already available. + # + # ssh.forward.local_socket('/tmp/local.sock', '/tmp/remote.sock') + def local_socket(local_socket_path, remote_socket_path) + File.delete(local_socket_path) if File.exist?(local_socket_path) + socket = Socket.unix_server_socket(local_socket_path) + + @local_forwarded_sockets[local_socket_path] = socket + + session.listen_to(socket) do |server| + client = server.accept[0] + debug { "received connection on #{socket}" } + + channel = session.open_channel("direct-streamlocal@openssh.com", + :string, remote_socket_path, + :string, nil, + :long, 0) do |achannel| + achannel.info { "direct channel established" } + end + + prepare_client(client, channel, :local) + + channel.on_open_failed do |ch, code, description| + channel.error { "could not establish direct channel: #{description} (#{code})" } + session.stop_listening_to(channel[:socket]) + channel[:socket].close + end + end + + local_socket_path + end + + # Terminates an active local forwarded socket. + # + # ssh.forward.cancel_local_socket('/tmp/foo.sock') + def cancel_local_socket(local_socket_path) + socket = @local_forwarded_sockets.delete(local_socket_path) + socket.shutdown rescue nil + socket.close rescue nil + session.stop_listening_to(socket) + end + + # Returns a list of all active locally forwarded sockets. The returned value + # is an array of Unix domain socket file paths. + def active_local_sockets + @local_forwarded_sockets.keys + end + # Requests that all connections on the given remote-port be forwarded via # the local host to the given port/host. The last argument describes the # bind address on the remote host, and defaults to 127.0.0.1. @@ -111,20 +178,56 @@ def active_locals # forwarded immediately. If the remote server is not able to begin the # listener for this request, an exception will be raised asynchronously. # - # If you want to know when the connection is active, it will show up in the - # #active_remotes list. If you want to block until the port is active, you - # could do something like this: + # To request an ephemeral port on the remote server, provide 0 (zero) for + # the port number. The assigned port will show up in the # #active_remotes + # list. + # + # remote_host is interpreted by the server per RFC 4254, which has these + # special values: + # + # - "" means that connections are to be accepted on all protocol + # families supported by the SSH implementation. + # - "0.0.0.0" means to listen on all IPv4 addresses. + # - "::" means to listen on all IPv6 addresses. + # - "localhost" means to listen on all protocol families supported by + # the SSH implementation on loopback addresses only ([RFC3330] and + # [RFC3513]). + # - "127.0.0.1" and "::1" indicate listening on the loopback + # interfaces for IPv4 and IPv6, respectively. + # + # You may pass a block that will be called when the the port forward + # request receives a response. This block will be passed the remote_port + # that was actually bound to, or nil if the binding failed. If the block + # returns :no_exception, the "failed binding" exception will not be thrown. + # + # If you want to block until the port is active, you could do something + # like this: + # + # got_remote_port = nil + # remote(port, host, remote_port, remote_host) do |actual_remote_port| + # got_remote_port = actual_remote_port || :error + # :no_exception # will yield the exception on my own thread + # end + # session.loop { !got_remote_port } + # if got_remote_port == :error + # raise Net::SSH::Exception, "remote forwarding request failed" + # end # - # ssh.forward.remote(80, "www.google.com", 1234, "0.0.0.0") - # ssh.loop { !ssh.forward.active_remotes.include?([1234, "0.0.0.0"]) } def remote(port, host, remote_port, remote_host="127.0.0.1") session.send_global_request("tcpip-forward", :string, remote_host, :long, remote_port) do |success, response| if success + remote_port = response.read_long if remote_port == 0 debug { "remote forward from remote #{remote_host}:#{remote_port} to #{host}:#{port} established" } @remote_forwarded_ports[[remote_port, remote_host]] = Remote.new(host, port) + yield remote_port, remote_host if block_given? else - error { "remote forwarding request failed" } - raise Net::SSH::Exception, "remote forwarding request failed" + instruction = if block_given? + yield :error + end + unless instruction == :no_exception + error { "remote forwarding request failed" } + raise Net::SSH::Exception, "remote forwarding request failed" + end end end end @@ -161,6 +264,16 @@ def active_remotes @remote_forwarded_ports.keys end + # Returns all active remote forwarded ports and where they forward to. The + # returned value is a hash from [, ] + # to [, ]. + def active_remote_destinations + @remote_forwarded_ports.inject({}) do |result, (remote, local)| + result[[local.port, local.host]] = remote + result + end + end + # Enables SSH agent forwarding on the given channel. The forwarded agent # will remain active even after the channel closes--the channel is only # used as the transport for enabling the forwarded connection. You should @@ -168,7 +281,7 @@ def active_remotes # time a session channel is opened, when the connection was created with # :forward_agent set to true: # - # Net::SSH.start("remote.host", "me", :forwrd_agent => true) do |ssh| + # Net::SSH.start("remote.host", "me", :forward_agent => true) do |ssh| # ssh.open_channel do |ch| # # agent will be automatically forwarded by this point # end @@ -200,25 +313,41 @@ def agent(channel) # and +type+ is an arbitrary string describing the type of the channel. def prepare_client(client, channel, type) client.extend(Net::SSH::BufferedIo) + client.extend(Net::SSH::ForwardedBufferedIo) client.logger = logger session.listen_to(client) channel[:socket] = client channel.on_data do |ch, data| + debug { "data:#{data.length} on #{type} forwarded channel" } ch[:socket].enqueue(data) end + channel.on_eof do |ch| + debug { "eof #{type} on #{type} forwarded channel" } + begin + ch[:socket].send_pending + ch[:socket].shutdown Socket::SHUT_WR + rescue IOError => e + if e.message =~ /closed/ then + debug { "epipe in on_eof => shallowing exception:#{e}" } + else + raise + end + rescue Errno::EPIPE => e + debug { "epipe in on_eof => shallowing exception:#{e}" } + rescue Errno::ENOTCONN => e + debug { "enotconn in on_eof => shallowing exception:#{e}" } + end + end + channel.on_close do |ch| debug { "closing #{type} forwarded channel" } ch[:socket].close if !client.closed? session.stop_listening_to(ch[:socket]) end - channel.on_eof do |ch| - ch.close - end - channel.on_process do |ch| if ch[:socket].closed? ch.info { "#{type} forwarded connection closed" } @@ -231,6 +360,24 @@ def prepare_client(client, channel, type) end end + # not a real socket, so use a simpler behaviour + def prepare_simple_client(client, channel, type) + channel[:socket] = client + + channel.on_data do |ch, data| + ch.debug { "data:#{data.length} on #{type} forwarded channel" } + ch[:socket].send(data) + end + + channel.on_process do |ch| + data = ch[:socket].read(8192) + if data + ch.debug { "read #{data.length} bytes from client, sending over #{type} forwarded connection" } + ch.send_data(data) + end + end + end + # The callback used when a new "forwarded-tcpip" channel is requested # by the server. This will open a new socket to the host/port specified # when the forwarded connection was first requested. @@ -246,16 +393,7 @@ def forwarded_tcpip(session, channel, packet) raise Net::SSH::ChannelOpenFailed.new(1, "unknown request from remote forwarded connection on #{connected_address}:#{connected_port}") end - client = Rex::Socket::Tcp.create( - 'PeerHost' => remote.host, - 'PeerPort' => remote.port, - 'Context' => { - 'Msf' => session.options[:msframework], - 'MsfExploit' => session.options[:msfmodule] - } - ) - session.options[:msfmodule].add_socket(client) if session.options[:msfmodule] - + client = TCPSocket.new(remote.host, remote.port) info { "connected #{connected_address}:#{connected_port} originator #{originator_address}:#{originator_port}" } prepare_client(client, channel, :remote) @@ -269,8 +407,12 @@ def auth_agent_channel(session, channel, packet) channel[:invisible] = true begin - agent = Authentication::Agent.connect(logger) - prepare_client(agent.socket, channel, :agent) + agent = Authentication::Agent.connect(logger, session.options[:agent_socket_factory]) + if (agent.socket.is_a? ::IO) + prepare_client(agent.socket, channel, :agent) + else + prepare_simple_client(agent.socket, channel, :agent) + end rescue Exception => e error { "attempted to connect to agent but failed: #{e.class.name} (#{e.message})" } raise Net::SSH::ChannelOpenFailed.new(2, "could not connect to authentication agent") diff --git a/lib/net/ssh/test.rb b/lib/net/ssh/test.rb index 11cfd31aac98d..2714c8a3959e9 100644 --- a/lib/net/ssh/test.rb +++ b/lib/net/ssh/test.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/transport/session' require 'net/ssh/connection/session' require 'net/ssh/test/kex' @@ -11,10 +10,10 @@ module Net; module SSH # typically include this module in your unit test class, and then build a # "story" of expected sends and receives: # - # require 'test/unit' + # require 'minitest/autorun' # require 'net/ssh/test' # - # class MyTest < Test::Unit::TestCase + # class MyTest < Minitest::Test # include Net::SSH::Test # # def test_exec_via_channel_works @@ -51,7 +50,7 @@ module Test # If a block is given, yields the script for the test socket (#socket). # Otherwise, simply returns the socket's script. See Net::SSH::Test::Script. def story - yield socket.script if block_given? + Net::SSH::Test::Extensions::IO.with_test_extension { yield socket.script if block_given? } return socket.script end @@ -72,7 +71,10 @@ def connection(options={}) # in these tests. It is a fully functional SSH transport session, operating # over a mock socket (#socket). def transport(options={}) - @transport ||= Net::SSH::Transport::Session.new(options[:host] || "localhost", options.merge(:kex => "test", :host_key => "ssh-rsa", :paranoid => false, :proxy => socket(options))) + @transport ||= Net::SSH::Transport::Session.new( + options[:host] || "localhost", + options.merge(kex: "test", host_key: "ssh-rsa", verify_host_key: false, proxy: socket(options)) + ) end # First asserts that a story has been described (see #story). Then yields, @@ -82,7 +84,7 @@ def transport(options={}) # the block passed to this assertion. def assert_scripted raise "there is no script to be processed" if socket.script.events.empty? - yield + Net::SSH::Test::Extensions::IO.with_test_extension { yield } assert socket.script.events.empty?, "there should not be any remaining scripted events, but there are still #{socket.script.events.length} pending" end end diff --git a/lib/net/ssh/test/channel.rb b/lib/net/ssh/test/channel.rb index 8483550628864..2eb7a1220615f 100644 --- a/lib/net/ssh/test/channel.rb +++ b/lib/net/ssh/test/channel.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH; module Test # A mock channel, used for scripting actions in tests. It wraps a @@ -11,6 +10,7 @@ module Net; module SSH; module Test # channel = session.opens_channel # channel.sends_exec "ls" # channel.gets_data "result of ls" + # channel.gets_extended_data "some error coming from ls" # channel.gets_close # channel.sends_close # end @@ -98,6 +98,13 @@ def sends_close script.sends_channel_close(self) end + # Scripts the sending of a "request pty" request packet across the channel. + # + # channel.sends_request_pty + def sends_request_pty + script.sends_channel_request_pty(self) + end + # Scripts the reception of a channel data packet from the remote end. # # channel.gets_data "bar" @@ -105,6 +112,14 @@ def gets_data(data) script.gets_channel_data(self, data) end + # Scripts the reception of a channel extended data packet from the remote + # end. + # + # channel.gets_extended_data "whoops" + def gets_extended_data(data) + script.gets_channel_extended_data(self, data) + end + # Scripts the reception of an "exit-status" channel request packet. # # channel.gets_exit_status(127) @@ -127,4 +142,4 @@ def gets_close end end -end; end; end +end; end; end \ No newline at end of file diff --git a/lib/net/ssh/test/extensions.rb b/lib/net/ssh/test/extensions.rb index af085af2d0cbe..b3b188a1da969 100644 --- a/lib/net/ssh/test/extensions.rb +++ b/lib/net/ssh/test/extensions.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/buffer' require 'net/ssh/packet' require 'net/ssh/buffered_io' @@ -114,6 +113,22 @@ def self.included(base) #:nodoc: base.extend(ClassMethods) end + @extension_enabled = false + + def self.with_test_extension(&block) + orig_value = @extension_enabled + @extension_enabled = true + begin + yield + ensure + @extension_enabled = orig_value + end + end + + def self.extension_enabled? + @extension_enabled + end + module ClassMethods def self.extended(obj) #:nodoc: class < "abc-xyz", - :server_key => OpenSSL::PKey::RSA.new(32), - :shared_secret => OpenSSL::BN.new("1234567890", 10), - :hashing_algorithm => OpenSSL::Digest::SHA1 } + { session_id: "abc-xyz", + server_key: OpenSSL::PKey::RSA.new(512), + shared_secret: OpenSSL::BN.new("1234567890", 10), + hashing_algorithm: OpenSSL::Digest::SHA1 } end end diff --git a/lib/net/ssh/test/local_packet.rb b/lib/net/ssh/test/local_packet.rb index b1555527ccff0..ac2e3d4d8307d 100644 --- a/lib/net/ssh/test/local_packet.rb +++ b/lib/net/ssh/test/local_packet.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/packet' require 'net/ssh/test/packet' @@ -34,19 +33,19 @@ def process(packet) type = packet.read_byte raise "expected #{@type}, but got #{type}" if @type != type - @data.zip(types).each do |expected, type| - type ||= case expected + @data.zip(types).each do |expected, _type| + _type ||= case expected when nil then break when Numeric then :long when String then :string when TrueClass, FalseClass then :bool end - actual = packet.send("read_#{type}") + actual = packet.send("read_#{_type}") next if expected.nil? - raise "expected #{type} #{expected.inspect} but got #{actual.inspect}" unless expected == actual + raise "expected #{_type} #{expected.inspect} but got #{actual.inspect}" unless expected == actual end end end -end; end; end +end; end; end \ No newline at end of file diff --git a/lib/net/ssh/test/packet.rb b/lib/net/ssh/test/packet.rb index c3eeff9030527..ca3ff2be90e40 100644 --- a/lib/net/ssh/test/packet.rb +++ b/lib/net/ssh/test/packet.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/connection/constants' require 'net/ssh/transport/constants' @@ -18,6 +17,17 @@ class Packet include Net::SSH::Transport::Constants include Net::SSH::Connection::Constants + # Register a custom channel request. extra_parts is an array of types + # of extra parameters + def self.register_channel_request(request, extra_parts) + @registered_requests ||= {} + @registered_requests[request] = {extra_parts: extra_parts} + end + + def self.registered_channel_requests(request) + @registered_requests && @registered_requests[request] + end + # Ceate a new packet of the given +type+, and with +args+ being a list of # data elements in the order expected for packets of the given +type+ # (see #types). @@ -66,17 +76,23 @@ def types when CHANNEL_OPEN then [:string, :long, :long, :long] when CHANNEL_OPEN_CONFIRMATION then [:long, :long, :long, :long] when CHANNEL_DATA then [:long, :string] + when CHANNEL_EXTENDED_DATA then [:long, :long, :string] when CHANNEL_EOF, CHANNEL_CLOSE, CHANNEL_SUCCESS, CHANNEL_FAILURE then [:long] when CHANNEL_REQUEST parts = [:long, :string, :bool] case @data[1] - when "exec", "subsystem" then parts << :string + when "exec", "subsystem","shell" then parts << :string when "exit-status" then parts << :long - else raise "don't know what to do about #{@data[1]} channel request" + when "pty-req" then parts.concat([:string, :long, :long, :long, :long, :string]) + when "env" then parts.contact([:string,:string]) + else + request = Packet.registered_channel_requests(@data[1]) + raise "don't know what to do about #{@data[1]} channel request" unless request + parts.concat(request[:extra_parts]) end else raise "don't know how to parse packet type #{@type}" end end end -end; end; end +end; end; end \ No newline at end of file diff --git a/lib/net/ssh/test/remote_packet.rb b/lib/net/ssh/test/remote_packet.rb index 844d4ac6fd7c3..c09d750ade3a3 100644 --- a/lib/net/ssh/test/remote_packet.rb +++ b/lib/net/ssh/test/remote_packet.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/buffer' require 'net/ssh/test/packet' @@ -36,4 +35,4 @@ def to_s end end -end; end; end +end; end; end \ No newline at end of file diff --git a/lib/net/ssh/test/script.rb b/lib/net/ssh/test/script.rb index 6fec8930f89be..9014c7d128529 100644 --- a/lib/net/ssh/test/script.rb +++ b/lib/net/ssh/test/script.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/test/channel' require 'net/ssh/test/local_packet' require 'net/ssh/test/remote_packet' @@ -64,7 +63,8 @@ def gets(type, *args) # indicating whether a response to this packet is required , and +data+ # is any additional request-specific data that this packet should send. # +success+ indicates whether the response (if one is required) should be - # success or failure. + # success or failure. If +data+ is an array it will be treated as multiple + # data. # # If a reply is desired, a remote packet will also be queued, :channel_success # if +success+ is true, or :channel_failure if +success+ is false. @@ -72,7 +72,11 @@ def gets(type, *args) # This will typically be called via Net::SSH::Test::Channel#sends_exec or # Net::SSH::Test::Channel#sends_subsystem. def sends_channel_request(channel, request, reply, data, success=true) - events << LocalPacket.new(:channel_request, channel.remote_id, request, reply, data) + if data.is_a? Array + events << LocalPacket.new(:channel_request, channel.remote_id, request, reply, *data) + else + events << LocalPacket.new(:channel_request, channel.remote_id, request, reply, data) + end if reply if success events << RemotePacket.new(:channel_success, channel.local_id) @@ -105,6 +109,15 @@ def sends_channel_close(channel) events << LocalPacket.new(:channel_close, channel.remote_id) end + # Scripts the sending of a channel request pty packets from the given + # Net::SSH::Test::Channel +channel+. This will typically be called via + # Net::SSH::Test::Channel#sends_request_pty. + def sends_channel_request_pty(channel) + data = ['pty-req', false] + data += Net::SSH::Connection::Channel::VALID_PTY_OPTIONS.merge(modes: "\0").values + events << LocalPacket.new(:channel_request, channel.remote_id, *data) + end + # Scripts the reception of a channel data packet from the remote host by # the given Net::SSH::Test::Channel +channel+. This will typically be # called via Net::SSH::Test::Channel#gets_data. @@ -112,6 +125,15 @@ def gets_channel_data(channel, data) events << RemotePacket.new(:channel_data, channel.local_id, data) end + # Scripts the reception of a channel extended data packet from the remote + # host by the given Net::SSH::Test::Channel +channel+. This will typically + # be called via Net::SSH::Test::Channel#gets_extended_data. + # + # Currently the only extended data type is stderr == 1. + def gets_channel_extended_data(channel, data) + events << RemotePacket.new(:channel_extended_data, channel.local_id, 1, data) + end + # Scripts the reception of a channel request packet from the remote host by # the given Net::SSH::Test::Channel +channel+. This will typically be # called via Net::SSH::Test::Channel#gets_exit_status. @@ -155,4 +177,4 @@ def process(packet) end end -end; end; end +end; end; end \ No newline at end of file diff --git a/lib/net/ssh/test/socket.rb b/lib/net/ssh/test/socket.rb index 5356731d4fa91..877aad4df9afb 100644 --- a/lib/net/ssh/test/socket.rb +++ b/lib/net/ssh/test/socket.rb @@ -1,5 +1,4 @@ -# -*- coding: binary -*- -require 'rex/socket' +require 'socket' require 'stringio' require 'net/ssh/test/extensions' require 'net/ssh/test/script' @@ -26,8 +25,8 @@ def initialize @script = Script.new - script.gets(:kexinit, 1, 2, 3, 4, "test", "ssh-rsa", "none", "none", "none", "none", "none", "none", "", "", false) script.sends(:kexinit) + script.gets(:kexinit, 1, 2, 3, 4, "test", "ssh-rsa", "none", "none", "none", "none", "none", "none", "", "", false) script.sends(:newkeys) script.gets(:newkeys) end @@ -40,7 +39,7 @@ def write(data) # Allows the socket to also mimic a socket factory, simply returning # +self+. - def open(host, port) + def open(host, port, options={}) @host, @port = host, port self end @@ -55,6 +54,11 @@ def getpeername def recv(n) read(n) || "" end + + def readpartial(n) + recv(n) + end + end end; end; end diff --git a/lib/net/ssh/transport/algorithms.rb b/lib/net/ssh/transport/algorithms.rb index cef241f6cac82..bc9848ca4fc1f 100644 --- a/lib/net/ssh/transport/algorithms.rb +++ b/lib/net/ssh/transport/algorithms.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/buffer' require 'net/ssh/known_hosts' require 'net/ssh/loggable' @@ -7,6 +6,7 @@ require 'net/ssh/transport/hmac' require 'net/ssh/transport/kex' require 'net/ssh/transport/server_version' +require 'net/ssh/authentication/ed25519_loader' module Net; module SSH; module Transport @@ -23,16 +23,38 @@ class Algorithms # Define the default algorithms, in order of preference, supported by # Net::SSH. ALGORITHMS = { - :host_key => %w(ssh-rsa ssh-dss), - :kex => %w(diffie-hellman-group-exchange-sha1 - diffie-hellman-group1-sha1), - :encryption => %w(aes128-cbc 3des-cbc blowfish-cbc cast128-cbc - aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se - idea-cbc none arcfour128 arcfour256), - :hmac => %w(hmac-sha1 hmac-md5 hmac-sha1-96 hmac-md5-96 none), - :compression => %w(none zlib@openssh.com zlib), - :language => %w() + host_key: %w(ssh-rsa ssh-dss + ssh-rsa-cert-v01@openssh.com + ssh-rsa-cert-v00@openssh.com), + kex: %w(diffie-hellman-group-exchange-sha1 + diffie-hellman-group1-sha1 + diffie-hellman-group14-sha1 + diffie-hellman-group-exchange-sha256), + encryption: %w(aes128-cbc 3des-cbc blowfish-cbc cast128-cbc + aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se + idea-cbc arcfour128 arcfour256 arcfour + aes128-ctr aes192-ctr aes256-ctr + cast128-ctr blowfish-ctr 3des-ctr none), + + hmac: %w(hmac-sha1 hmac-md5 hmac-sha1-96 hmac-md5-96 + hmac-ripemd160 hmac-ripemd160@openssh.com + hmac-sha2-256 hmac-sha2-512 hmac-sha2-256-96 + hmac-sha2-512-96 none), + + compression: %w(none zlib@openssh.com zlib), + language: %w() } + if defined?(OpenSSL::PKey::EC) + ALGORITHMS[:host_key] += %w(ecdsa-sha2-nistp256 + ecdsa-sha2-nistp384 + ecdsa-sha2-nistp521) + if Net::SSH::Authentication::ED25519Loader::LOADED + ALGORITHMS[:host_key] += %w(ssh-ed25519) + end + ALGORITHMS[:kex] += %w(ecdh-sha2-nistp256 + ecdh-sha2-nistp384 + ecdh-sha2-nistp521) + end # The underlying transport layer session that supports this object attr_reader :session @@ -97,6 +119,12 @@ def initialize(session, options={}) prepare_preferred_algorithms! end + # Start the algorithm negotation + def start + raise ArgumentError, "Cannot call start if it's negotiation started or done" if @pending || @initialized + send_kexinit + end + # Request a rekey operation. This will return immediately, and does not # actually perform the rekey operation. It does cause the session to change # state, however--until the key exchange finishes, no new packets will be @@ -107,7 +135,7 @@ def rekey! send_kexinit end - # Called by the transport layer when a KEXINIT packet is recieved, indicating + # Called by the transport layer when a KEXINIT packet is received, indicating # that the server wants to exchange keys. This can be spontaneous, or it # can be in response to a client-initiated rekey request (see #rekey!). Either # way, this will block until the key exchange completes. @@ -183,20 +211,8 @@ def proceed! def prepare_preferred_algorithms! options[:compression] = %w(zlib@openssh.com zlib) if options[:compression] == true - ALGORITHMS.each do |algorithm, list| - algorithms[algorithm] = list.dup - - # apply the preferred algorithm order, if any - if options[algorithm] - algorithms[algorithm] = Array(options[algorithm]).compact.uniq - invalid = algorithms[algorithm].detect { |name| !ALGORITHMS[algorithm].include?(name) } - raise NotImplementedError, "unsupported #{algorithm} algorithm: `#{invalid}'" if invalid - - # make sure all of our supported algorithms are tacked onto the - # end, so that if the user tries to give a list of which none are - # supported, we can still proceed. - list.each { |name| algorithms[algorithm] << name unless algorithms[algorithm].include?(name) } - end + ALGORITHMS.each do |algorithm, supported| + algorithms[algorithm] = compose_algorithm_list(supported, options[algorithm], options[:append_all_supported_algorithms]) end # for convention, make sure our list has the same keys as the server @@ -207,11 +223,11 @@ def prepare_preferred_algorithms! algorithms[:compression_client] = algorithms[:compression_server] = algorithms[:compression] algorithms[:language_client ] = algorithms[:language_server ] = algorithms[:language] - if !options.key?(:host_key) and options[:config] + if !options.key?(:host_key) # make sure the host keys are specified in preference order, where any # existing known key for the host has preference. - existing_keys = KnownHosts.search_for(options[:host_key_alias] || session.host_as_string, options) + existing_keys = session.host_keys host_keys = existing_keys.map { |key| key.ssh_type }.uniq algorithms[:host_key].each do |name| host_keys << name unless host_keys.include?(name) @@ -220,9 +236,41 @@ def prepare_preferred_algorithms! end end + # Composes the list of algorithms by taking supported algorithms and matching with supplied options. + def compose_algorithm_list(supported, option, append_all_supported_algorithms = false) + return supported.dup unless option + + list = [] + option = Array(option).compact.uniq + + if option.first && option.first.start_with?('+') + list = supported.dup + list << option.first[1..-1] + list.concat(option[1..-1]) + list.uniq! + else + list = option + + if append_all_supported_algorithms + supported.each { |name| list << name unless list.include?(name) } + end + end + + unsupported = [] + list.select! do |name| + is_supported = supported.include?(name) + unsupported << name unless is_supported + is_supported + end + + lwarn { %(unsupported algorithm: `#{unsupported}') } unless unsupported.empty? + + list + end + # Parses a KEXINIT packet from the server. def parse_server_algorithm_packet(packet) - data = { :raw => packet.content } + data = { raw: packet.content } packet.read(16) # skip the cookie value @@ -240,7 +288,7 @@ def parse_server_algorithm_packet(packet) # TODO: if first_kex_packet_follows, we need to try to skip the # actual kexinit stuff and try to guess what the server is doing... # need to read more about this scenario. - first_kex_packet_follows = packet.read_bool + # first_kex_packet_follows = packet.read_bool return data end @@ -258,8 +306,8 @@ def build_client_algorithm_packet Net::SSH::Buffer.from(:byte, KEXINIT, :long, [rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF)], - :string, [kex, host_key, encryption, encryption, hmac, hmac], - :string, [compression, compression, language, language], + :mstring, [kex, host_key, encryption, encryption, hmac, hmac], + :mstring, [compression, compression, language, language], :bool, false, :long, 0) end @@ -323,12 +371,13 @@ def exchange_keys debug { "exchanging keys" } algorithm = Kex::MAP[kex].new(self, session, - :client_version_string => Net::SSH::Transport::ServerVersion::PROTO_VERSION, - :server_version_string => session.server_version.version, - :server_algorithm_packet => @server_packet, - :client_algorithm_packet => @client_packet, - :need_bytes => kex_byte_requirement, - :logger => logger) + client_version_string: Net::SSH::Transport::ServerVersion::PROTO_VERSION, + server_version_string: session.server_version.version, + server_algorithm_packet: @server_packet, + client_algorithm_packet: @client_packet, + need_bytes: kex_byte_requirement, + minimum_dh_bits: options[:minimum_dh_bits], + logger: logger) result = algorithm.exchange_keys secret = result[:shared_secret].to_ssh @@ -338,7 +387,7 @@ def exchange_keys @session_id ||= hash key = Proc.new { |salt| digester.digest(secret + hash + salt + @session_id) } - + iv_client = key["A"] iv_server = key["B"] key_client = key["C"] @@ -346,27 +395,26 @@ def exchange_keys mac_key_client = key["E"] mac_key_server = key["F"] - parameters = { :iv => iv_client, :key => key_client, :shared => secret, - :hash => hash, :digester => digester } - - cipher_client = CipherFactory.get(encryption_client, parameters.merge(:encrypt => true)) - cipher_server = CipherFactory.get(encryption_server, parameters.merge(:iv => iv_server, :key => key_server, :decrypt => true)) - - mac_client = HMAC.get(hmac_client, mac_key_client) - mac_server = HMAC.get(hmac_server, mac_key_server) - - session.configure_client :cipher => cipher_client, :hmac => mac_client, - :compression => normalize_compression_name(compression_client), - :compression_level => options[:compression_level], - :rekey_limit => options[:rekey_limit], - :max_packets => options[:rekey_packet_limit], - :max_blocks => options[:rekey_blocks_limit] - - session.configure_server :cipher => cipher_server, :hmac => mac_server, - :compression => normalize_compression_name(compression_server), - :rekey_limit => options[:rekey_limit], - :max_packets => options[:rekey_packet_limit], - :max_blocks => options[:rekey_blocks_limit] + parameters = { shared: secret, hash: hash, digester: digester } + + cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: key_client, encrypt: true)) + cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: key_server, decrypt: true)) + + mac_client = HMAC.get(hmac_client, mac_key_client, parameters) + mac_server = HMAC.get(hmac_server, mac_key_server, parameters) + + session.configure_client cipher: cipher_client, hmac: mac_client, + compression: normalize_compression_name(compression_client), + compression_level: options[:compression_level], + rekey_limit: options[:rekey_limit], + max_packets: options[:rekey_packet_limit], + max_blocks: options[:rekey_blocks_limit] + + session.configure_server cipher: cipher_server, hmac: mac_server, + compression: normalize_compression_name(compression_server), + rekey_limit: options[:rekey_limit], + max_packets: options[:rekey_packet_limit], + max_blocks: options[:rekey_blocks_limit] @initialized = true end diff --git a/lib/net/ssh/transport/cipher_factory.rb b/lib/net/ssh/transport/cipher_factory.rb index 2946aa0314bd0..e7e39d1bae4c2 100644 --- a/lib/net/ssh/transport/cipher_factory.rb +++ b/lib/net/ssh/transport/cipher_factory.rb @@ -1,5 +1,6 @@ -# -*- coding: binary -*- require 'openssl' +require 'net/ssh/transport/ctr.rb' +require 'net/ssh/transport/key_expander' require 'net/ssh/transport/identity_cipher' module Net; module SSH; module Transport @@ -18,9 +19,28 @@ class CipherFactory "rijndael-cbc@lysator.liu.se" => "aes-256-cbc", "arcfour128" => "rc4", "arcfour256" => "rc4", - "none" => "none" + "arcfour512" => "rc4", + "arcfour" => "rc4", + + "3des-ctr" => "des-ede3", + "blowfish-ctr" => "bf-ecb", + "aes256-ctr" => "aes-256-ecb", + "aes192-ctr" => "aes-192-ecb", + "aes128-ctr" => "aes-128-ecb", + "cast128-ctr" => "cast5-ecb", + + "none" => "none", + } + + # Ruby's OpenSSL bindings always return a key length of 16 for RC4 ciphers + # resulting in the error: OpenSSL::CipherError: key length too short. + # The following ciphers will override this key length. + KEY_LEN_OVERRIDE = { + "arcfour256" => 32, + "arcfour512" => 64 } + # Returns true if the underlying OpenSSL library supports the given cipher, # and false otherwise. def self.supported?(name) @@ -37,15 +57,19 @@ def self.supported?(name) def self.get(name, options={}) ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'" return IdentityCipher if ossl_name == "none" + cipher = OpenSSL::Cipher.new(ossl_name) - cipher = OpenSSL::Cipher::Cipher.new(ossl_name) cipher.send(options[:encrypt] ? :encrypt : :decrypt) cipher.padding = 0 - cipher.iv = make_key(cipher.iv_len, options[:iv], options) if ossl_name != "rc4" - cipher.key_len = 32 if name == "arcfour256" - cipher.key = make_key(cipher.key_len, options[:key], options) - cipher.update(" " * 1536) if ossl_name == "rc4" + + cipher.extend(Net::SSH::Transport::CTR) if (name =~ /-ctr(@openssh.org)?$/) + cipher.iv = Net::SSH::Transport::KeyExpander.expand_key(cipher.iv_len, options[:iv], options) if ossl_name != "rc4" + + key_len = KEY_LEN_OVERRIDE[name] || cipher.key_len + cipher.key_len = key_len + cipher.key = Net::SSH::Transport::KeyExpander.expand_key(key_len, options[:key], options) + cipher.update(" " * 1536) if (ossl_name == "rc4" && name != "arcfour") return cipher end @@ -54,32 +78,22 @@ def self.get(name, options={}) # block-size ] for the named cipher algorithm. If the cipher # algorithm is unknown, or is "none", 0 is returned for both elements # of the tuple. - def self.get_lengths(name) + # if :iv_len option is supplied the third return value will be ivlen + def self.get_lengths(name, options = {}) ossl_name = SSH_TO_OSSL[name] - return [0, 0] if ossl_name.nil? || ossl_name == "none" + if ossl_name.nil? || ossl_name == "none" + result = [0, 0] + result << 0 if options[:iv_len] + else + cipher = OpenSSL::Cipher.new(ossl_name) + key_len = KEY_LEN_OVERRIDE[name] || cipher.key_len + cipher.key_len = key_len - cipher = OpenSSL::Cipher::Cipher.new(ossl_name) - return [cipher.key_len, ossl_name=="rc4" ? 8 : cipher.block_size] - end - - private - - # Generate a key value in accordance with the SSH2 specification. - def self.make_key(bytes, start, options={}) - k = start[0, bytes] - - digester = options[:digester] - shared = options[:shared] - hash = options[:hash] - - while k.length < bytes - step = digester.digest(shared + hash + k) - bytes_needed = bytes - k.length - k << step[0, bytes_needed] - end - - return k + result = [key_len, ossl_name=="rc4" ? 8 : cipher.block_size] + result << cipher.iv_len if options[:iv_len] end + result + end end end; end; end diff --git a/lib/net/ssh/transport/constants.rb b/lib/net/ssh/transport/constants.rb index ba450dd98ae6b..0a204304ff0bc 100644 --- a/lib/net/ssh/transport/constants.rb +++ b/lib/net/ssh/transport/constants.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH; module Transport module Constants @@ -27,5 +26,7 @@ module Constants KEXDH_INIT = 30 KEXDH_REPLY = 31 + KEXECDH_INIT = 30 + KEXECDH_REPLY = 31 end end; end; end diff --git a/lib/net/ssh/transport/ctr.rb b/lib/net/ssh/transport/ctr.rb new file mode 100644 index 0000000000000..8b446b5777193 --- /dev/null +++ b/lib/net/ssh/transport/ctr.rb @@ -0,0 +1,93 @@ +require 'openssl' + +module Net::SSH::Transport + + # Pure-Ruby implementation of Stateful Decryption Counter(SDCTR) Mode + # for Block Ciphers. See RFC4344 for detail. + module CTR + def self.extended(orig) + orig.instance_eval { + @remaining = "" + @counter = nil + @counter_len = orig.block_size + orig.encrypt + orig.padding = 0 + + singleton_class.send(:alias_method, :_update, :update) + singleton_class.send(:private, :_update) + singleton_class.send(:undef_method, :update) + + def iv + @counter + end + + def iv_len + block_size + end + + def iv=(iv_s) + @counter = iv_s if @counter.nil? + end + + def encrypt + # DO NOTHING (always set to "encrypt") + end + + def decrypt + # DO NOTHING (always set to "encrypt") + end + + def padding=(pad) + # DO NOTHING (always 0) + end + + def reset + @remaining = "" + end + + def update(data) + @remaining += data + + encrypted = "" + + while @remaining.bytesize >= block_size + encrypted += xor!(@remaining.slice!(0, block_size), + _update(@counter)) + increment_counter! + end + + encrypted + end + + def final + unless @remaining.empty? + s = xor!(@remaining, _update(@counter)) + else + s = "" + end + + @remaining = "" + + s + end + + def xor!(s1, s2) + s = [] + s1.unpack('Q*').zip(s2.unpack('Q*')) {|a,b| s.push(a^b) } + s.pack('Q*') + end + singleton_class.send(:private, :xor!) + + def increment_counter! + c = @counter_len + while ((c -= 1) > 0) + if @counter.setbyte(c, (@counter.getbyte(c) + 1) & 0xff) != 0 + break + end + end + end + singleton_class.send(:private, :increment_counter!) + } + end + end +end diff --git a/lib/net/ssh/transport/hmac.rb b/lib/net/ssh/transport/hmac.rb index 9721475ba67fb..af929f76385a8 100644 --- a/lib/net/ssh/transport/hmac.rb +++ b/lib/net/ssh/transport/hmac.rb @@ -1,8 +1,13 @@ -# -*- coding: binary -*- +require 'net/ssh/transport/key_expander' require 'net/ssh/transport/hmac/md5' require 'net/ssh/transport/hmac/md5_96' require 'net/ssh/transport/hmac/sha1' require 'net/ssh/transport/hmac/sha1_96' +require 'net/ssh/transport/hmac/sha2_256' +require 'net/ssh/transport/hmac/sha2_256_96' +require 'net/ssh/transport/hmac/sha2_512' +require 'net/ssh/transport/hmac/sha2_512_96' +require 'net/ssh/transport/hmac/ripemd160' require 'net/ssh/transport/hmac/none' # Implements a simple factory interface for fetching hmac implementations, or @@ -10,18 +15,26 @@ module Net::SSH::Transport::HMAC # The mapping of SSH hmac algorithms to their implementations MAP = { - 'hmac-md5' => MD5, - 'hmac-md5-96' => MD5_96, - 'hmac-sha1' => SHA1, - 'hmac-sha1-96' => SHA1_96, - 'none' => None + 'hmac-md5' => MD5, + 'hmac-md5-96' => MD5_96, + 'hmac-sha1' => SHA1, + 'hmac-sha1-96' => SHA1_96, + 'hmac-ripemd160' => RIPEMD160, + 'hmac-ripemd160@openssh.com' => RIPEMD160, + 'none' => None } + # add mapping to sha2 hmac algorithms if they're available + MAP['hmac-sha2-256'] = SHA2_256 if defined?(::Net::SSH::Transport::HMAC::SHA2_256) + MAP['hmac-sha2-256-96'] = SHA2_256_96 if defined?(::Net::SSH::Transport::HMAC::SHA2_256_96) + MAP['hmac-sha2-512'] = SHA2_512 if defined?(::Net::SSH::Transport::HMAC::SHA2_512) + MAP['hmac-sha2-512-96'] = SHA2_512_96 if defined?(::Net::SSH::Transport::HMAC::SHA2_512_96) + # Retrieves a new hmac instance of the given SSH type (+name+). If +key+ is # given, the new instance will be initialized with that key. - def self.get(name, key="") + def self.get(name, key="", parameters = {}) impl = MAP[name] or raise ArgumentError, "hmac not found: #{name.inspect}" - impl.new(key) + impl.new(Net::SSH::Transport::KeyExpander.expand_key(impl.key_length, key, parameters)) end # Retrieves the key length for the hmac of the given SSH type (+name+). diff --git a/lib/net/ssh/transport/hmac/abstract.rb b/lib/net/ssh/transport/hmac/abstract.rb index 1020070394968..b3e3eaab79948 100644 --- a/lib/net/ssh/transport/hmac/abstract.rb +++ b/lib/net/ssh/transport/hmac/abstract.rb @@ -1,5 +1,5 @@ -# -*- coding: binary -*- require 'openssl' +require 'openssl/digest' module Net; module SSH; module Transport; module HMAC diff --git a/lib/net/ssh/transport/hmac/md5.rb b/lib/net/ssh/transport/hmac/md5.rb index 59bd034846237..66b78ca42eebc 100644 --- a/lib/net/ssh/transport/hmac/md5.rb +++ b/lib/net/ssh/transport/hmac/md5.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/transport/hmac/abstract' module Net::SSH::Transport::HMAC diff --git a/lib/net/ssh/transport/hmac/md5_96.rb b/lib/net/ssh/transport/hmac/md5_96.rb index 8759b70ff0d17..826b70a0d05f4 100644 --- a/lib/net/ssh/transport/hmac/md5_96.rb +++ b/lib/net/ssh/transport/hmac/md5_96.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/transport/hmac/md5' module Net::SSH::Transport::HMAC diff --git a/lib/net/ssh/transport/hmac/none.rb b/lib/net/ssh/transport/hmac/none.rb index cad4af51c1b68..191373e873acc 100644 --- a/lib/net/ssh/transport/hmac/none.rb +++ b/lib/net/ssh/transport/hmac/none.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/transport/hmac/abstract' module Net::SSH::Transport::HMAC diff --git a/lib/net/ssh/transport/hmac/ripemd160.rb b/lib/net/ssh/transport/hmac/ripemd160.rb new file mode 100644 index 0000000000000..a77e4cd2375dd --- /dev/null +++ b/lib/net/ssh/transport/hmac/ripemd160.rb @@ -0,0 +1,13 @@ +require 'net/ssh/transport/hmac/abstract' + +module Net::SSH::Transport::HMAC + + # The RIPEMD-160 HMAC algorithm. This has a mac and key length of 20, and + # uses the RIPEMD-160 digest algorithm. + class RIPEMD160 < Abstract + mac_length 20 + key_length 20 + digest_class OpenSSL::Digest::RIPEMD160 + end + +end diff --git a/lib/net/ssh/transport/hmac/sha1.rb b/lib/net/ssh/transport/hmac/sha1.rb index d1fdecc7a5ed1..b40d32fe910dd 100644 --- a/lib/net/ssh/transport/hmac/sha1.rb +++ b/lib/net/ssh/transport/hmac/sha1.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/transport/hmac/abstract' module Net::SSH::Transport::HMAC diff --git a/lib/net/ssh/transport/hmac/sha1_96.rb b/lib/net/ssh/transport/hmac/sha1_96.rb index d04e2e20e902b..6b0b3c282e075 100644 --- a/lib/net/ssh/transport/hmac/sha1_96.rb +++ b/lib/net/ssh/transport/hmac/sha1_96.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/transport/hmac/sha1' module Net::SSH::Transport::HMAC diff --git a/lib/net/ssh/transport/hmac/sha2_256.rb b/lib/net/ssh/transport/hmac/sha2_256.rb new file mode 100644 index 0000000000000..8191a909c734f --- /dev/null +++ b/lib/net/ssh/transport/hmac/sha2_256.rb @@ -0,0 +1,15 @@ +require 'net/ssh/transport/hmac/abstract' + +if defined?(OpenSSL::Digest::SHA256) # need openssl support + module Net::SSH::Transport::HMAC + + # The SHA-256 HMAC algorithm. This has a mac and key length of 32, and + # uses the SHA-256 digest algorithm. + class SHA2_256 < Abstract + mac_length 32 + key_length 32 + digest_class OpenSSL::Digest::SHA256 + end + + end +end diff --git a/lib/net/ssh/transport/hmac/sha2_256_96.rb b/lib/net/ssh/transport/hmac/sha2_256_96.rb new file mode 100644 index 0000000000000..15e36ffd17f54 --- /dev/null +++ b/lib/net/ssh/transport/hmac/sha2_256_96.rb @@ -0,0 +1,13 @@ +require 'net/ssh/transport/hmac/abstract' + +module Net::SSH::Transport::HMAC + + if defined?(SHA2_256) # need openssl support + # The SHA256-96 HMAC algorithm. This returns only the first 12 bytes of + # the digest. + class SHA2_256_96 < SHA2_256 + mac_length 12 + end + end + +end diff --git a/lib/net/ssh/transport/hmac/sha2_512.rb b/lib/net/ssh/transport/hmac/sha2_512.rb new file mode 100644 index 0000000000000..e68df19626542 --- /dev/null +++ b/lib/net/ssh/transport/hmac/sha2_512.rb @@ -0,0 +1,14 @@ +require 'net/ssh/transport/hmac/abstract' + +module Net::SSH::Transport::HMAC + + if defined?(OpenSSL::Digest::SHA512) # need openssl support + # The SHA-512 HMAC algorithm. This has a mac and key length of 64, and + # uses the SHA-512 digest algorithm. + class SHA2_512 < Abstract + mac_length 64 + key_length 64 + digest_class OpenSSL::Digest::SHA512 + end + end +end diff --git a/lib/net/ssh/transport/hmac/sha2_512_96.rb b/lib/net/ssh/transport/hmac/sha2_512_96.rb new file mode 100644 index 0000000000000..5cc7aeda32c64 --- /dev/null +++ b/lib/net/ssh/transport/hmac/sha2_512_96.rb @@ -0,0 +1,13 @@ +require 'net/ssh/transport/hmac/abstract' + +module Net::SSH::Transport::HMAC + + if defined?(SHA2_512) # need openssl support + # The SHA2-512-96 HMAC algorithm. This returns only the first 12 bytes of + # the digest. + class SHA2_512_96 < SHA2_512 + mac_length 12 + end + end + +end diff --git a/lib/net/ssh/transport/identity_cipher.rb b/lib/net/ssh/transport/identity_cipher.rb index 0a0b4034a5dfa..856c2ed6675aa 100644 --- a/lib/net/ssh/transport/identity_cipher.rb +++ b/lib/net/ssh/transport/identity_cipher.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH; module Transport # A cipher that does nothing but pass the data through, unchanged. This diff --git a/lib/net/ssh/transport/kex.rb b/lib/net/ssh/transport/kex.rb index 23f55df9c7f2a..1cd059aa5c936 100644 --- a/lib/net/ssh/transport/kex.rb +++ b/lib/net/ssh/transport/kex.rb @@ -1,6 +1,7 @@ -# -*- coding: binary -*- require 'net/ssh/transport/kex/diffie_hellman_group1_sha1' +require 'net/ssh/transport/kex/diffie_hellman_group14_sha1' require 'net/ssh/transport/kex/diffie_hellman_group_exchange_sha1' +require 'net/ssh/transport/kex/diffie_hellman_group_exchange_sha256' module Net::SSH::Transport module Kex @@ -8,7 +9,20 @@ module Kex # to their corresponding implementors. MAP = { 'diffie-hellman-group-exchange-sha1' => DiffieHellmanGroupExchangeSHA1, - 'diffie-hellman-group1-sha1' => DiffieHellmanGroup1SHA1 + 'diffie-hellman-group1-sha1' => DiffieHellmanGroup1SHA1, + 'diffie-hellman-group14-sha1' => DiffieHellmanGroup14SHA1, } + if defined?(DiffieHellmanGroupExchangeSHA256) + MAP['diffie-hellman-group-exchange-sha256'] = DiffieHellmanGroupExchangeSHA256 + end + if defined?(OpenSSL::PKey::EC) + require 'net/ssh/transport/kex/ecdh_sha2_nistp256' + require 'net/ssh/transport/kex/ecdh_sha2_nistp384' + require 'net/ssh/transport/kex/ecdh_sha2_nistp521' + + MAP['ecdh-sha2-nistp256'] = EcdhSHA2NistP256 + MAP['ecdh-sha2-nistp384'] = EcdhSHA2NistP384 + MAP['ecdh-sha2-nistp521'] = EcdhSHA2NistP521 + end end end diff --git a/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb b/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb new file mode 100644 index 0000000000000..28a8553a48cec --- /dev/null +++ b/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb @@ -0,0 +1,44 @@ +require 'net/ssh/transport/kex/diffie_hellman_group1_sha1' + +module Net; module SSH; module Transport; module Kex + + # A key-exchange service implementing the "diffie-hellman-group14-sha1" + # key-exchange algorithm. (defined in RFC 4253) + class DiffieHellmanGroup14SHA1 < DiffieHellmanGroup1SHA1 + include Constants, Loggable + + # The value of 'P', as a string, in hexadecimal + P_s = "FFFFFFFF" "FFFFFFFF" "C90FDAA2" "2168C234" + + "C4C6628B" "80DC1CD1" "29024E08" "8A67CC74" + + "020BBEA6" "3B139B22" "514A0879" "8E3404DD" + + "EF9519B3" "CD3A431B" "302B0A6D" "F25F1437" + + "4FE1356D" "6D51C245" "E485B576" "625E7EC6" + + "F44C42E9" "A637ED6B" "0BFF5CB6" "F406B7ED" + + "EE386BFB" "5A899FA5" "AE9F2411" "7C4B1FE6" + + "49286651" "ECE45B3D" "C2007CB8" "A163BF05" + + "98DA4836" "1C55D39A" "69163FA8" "FD24CF5F" + + "83655D23" "DCA3AD96" "1C62F356" "208552BB" + + "9ED52907" "7096966D" "670C354E" "4ABC9804" + + "F1746C08" "CA18217C" "32905E46" "2E36CE3B" + + "E39E772C" "180E8603" "9B2783A2" "EC07A28F" + + "B5C55DF0" "6F4C52C9" "DE2BCBF6" "95581718" + + "3995497C" "EA956AE5" "15D22618" "98FA0510" + + "15728E5A" "8AACAA68" "FFFFFFFF" "FFFFFFFF" + + # The radix in which P_s represents the value of P + P_r = 16 + + # The group constant + G = 2 + + private + + def get_p + OpenSSL::BN.new(P_s, P_r) + end + + def get_g + G + end + end +end; end; end; end diff --git a/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb b/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb index 2ea524d0b43fa..88e8cbac45cc2 100644 --- a/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +++ b/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/buffer' require 'net/ssh/errors' require 'net/ssh/loggable' @@ -41,8 +40,8 @@ class DiffieHellmanGroup1SHA1 # required by this algorithm, which was acquired during earlier # processing. def initialize(algorithms, connection, data) - @p = OpenSSL::BN.new(P_s, P_r) - @g = G + @p = get_p + @g = get_g @digester = OpenSSL::Digest::SHA1 @algorithms = algorithms @@ -70,14 +69,22 @@ def exchange_keys session_id = verify_signature(result) confirm_newkeys - return { :session_id => session_id, - :server_key => result[:server_key], - :shared_secret => result[:shared_secret], - :hashing_algorithm => digester } + return { session_id: session_id, + server_key: result[:server_key], + shared_secret: result[:shared_secret], + hashing_algorithm: digester } end private - + + def get_p + OpenSSL::BN.new(P_s, P_r) + end + + def get_g + G + end + # Returns the DH key parameters for the current connection. def get_parameters [p, g] @@ -108,11 +115,22 @@ def build_signature_buffer(result) def generate_key #:nodoc: dh = OpenSSL::PKey::DH.new - dh.p, dh.g = get_parameters - dh.priv_key = OpenSSL::BN.rand(data[:need_bytes] * 8) - - dh.generate_key! until dh.valid? + if dh.respond_to?(:set_pqg) + p, g = get_parameters + dh.set_pqg(p, nil, g) + else + dh.p, dh.g = get_parameters + end + dh.generate_key! + until dh.valid? && dh.priv_key.num_bytes == data[:need_bytes] + if dh.respond_to?(:set_key) + dh.set_key(nil, OpenSSL::BN.rand(data[:need_bytes] * 8)) + else + dh.priv_key = OpenSSL::BN.rand(data[:need_bytes] * 8) + end + dh.generate_key! + end dh end @@ -163,7 +181,7 @@ def verify_server_key(key) #:nodoc: blob, fingerprint = generate_key_fingerprint(key) - unless connection.host_key_verifier.verify(:key => key, :key_blob => blob, :fingerprint => fingerprint, :session => connection) + unless connection.host_key_verifier.verify(key: key, key_blob: blob, fingerprint: fingerprint, session: connection) raise Net::SSH::Exception, "host key verification failed" end end diff --git a/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb b/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb index 07643042794d9..8f9613e8de45d 100644 --- a/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +++ b/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/errors' require 'net/ssh/transport/constants' require 'net/ssh/transport/kex/diffie_hellman_group1_sha1' @@ -20,9 +19,14 @@ class DiffieHellmanGroupExchangeSHA1 < DiffieHellmanGroup1SHA1 # Compute the number of bits needed for the given number of bytes. def compute_need_bits - need_bits = data[:need_bytes] * 8 - if need_bits < MINIMUM_BITS - need_bits = MINIMUM_BITS + + # for Compatibility: OpenSSH requires (need_bits * 2 + 1) length of parameter + need_bits = data[:need_bytes] * 8 * 2 + 1 + + data[:minimum_dh_bits] ||= MINIMUM_BITS + + if need_bits < data[:minimum_dh_bits] + need_bits = data[:minimum_dh_bits] elsif need_bits > MAXIMUM_BITS need_bits = MAXIMUM_BITS end @@ -36,7 +40,7 @@ def get_parameters compute_need_bits # request the DH key parameters for the given number of bits. - buffer = Net::SSH::Buffer.from(:byte, KEXDH_GEX_REQUEST, :long, MINIMUM_BITS, + buffer = Net::SSH::Buffer.from(:byte, KEXDH_GEX_REQUEST, :long, data[:minimum_dh_bits], :long, data[:need_bits], :long, MAXIMUM_BITS) connection.send_message(buffer) diff --git a/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb b/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb new file mode 100644 index 0000000000000..656254c26c50e --- /dev/null +++ b/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb @@ -0,0 +1,15 @@ +require 'net/ssh/transport/kex/diffie_hellman_group_exchange_sha1' + +module Net::SSH::Transport::Kex + if defined?(OpenSSL::Digest::SHA256) + # A key-exchange service implementing the + # "diffie-hellman-group-exchange-sha256" key-exchange algorithm. + class DiffieHellmanGroupExchangeSHA256 < DiffieHellmanGroupExchangeSHA1 + def initialize(*args) + super(*args) + + @digester = OpenSSL::Digest::SHA256 + end + end + end +end diff --git a/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb b/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb new file mode 100644 index 0000000000000..953b2da106002 --- /dev/null +++ b/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb @@ -0,0 +1,93 @@ +require 'net/ssh/transport/constants' +require 'net/ssh/transport/kex/diffie_hellman_group1_sha1' + +module Net; module SSH; module Transport; module Kex + + # A key-exchange service implementing the "ecdh-sha2-nistp256" + # key-exchange algorithm. (defined in RFC 5656) + class EcdhSHA2NistP256 < DiffieHellmanGroup1SHA1 + include Constants, Loggable + + attr_reader :ecdh + + def digester + OpenSSL::Digest::SHA256 + end + + def curve_name + OpenSSL::PKey::EC::CurveNameAlias['nistp256'] + end + + def initialize(algorithms, connection, data) + @algorithms = algorithms + @connection = connection + + @digester = digester + @data = data.dup + @ecdh = generate_key + @logger = @data.delete(:logger) + end + + private + + def get_message_types + [KEXECDH_INIT, KEXECDH_REPLY] + end + + def build_signature_buffer(result) + response = Net::SSH::Buffer.new + response.write_string data[:client_version_string], + data[:server_version_string], + data[:client_algorithm_packet], + data[:server_algorithm_packet], + result[:key_blob], + ecdh.public_key.to_bn.to_s(2), + result[:server_ecdh_pubkey] + response.write_bignum result[:shared_secret] + response + end + + def generate_key #:nodoc: + OpenSSL::PKey::EC.new(curve_name).generate_key + end + + def send_kexinit #:nodoc: + init, reply = get_message_types + + # send the KEXECDH_INIT message + ## byte SSH_MSG_KEX_ECDH_INIT + ## string Q_C, client's ephemeral public key octet string + buffer = Net::SSH::Buffer.from(:byte, init, :mstring, ecdh.public_key.to_bn.to_s(2)) + connection.send_message(buffer) + + # expect the following KEXECDH_REPLY message + ## byte SSH_MSG_KEX_ECDH_REPLY + ## string K_S, server's public host key + ## string Q_S, server's ephemeral public key octet string + ## string the signature on the exchange hash + buffer = connection.next_message + raise Net::SSH::Exception, "expected REPLY" unless buffer.type == reply + + result = Hash.new + result[:key_blob] = buffer.read_string + result[:server_key] = Net::SSH::Buffer.new(result[:key_blob]).read_key + result[:server_ecdh_pubkey] = buffer.read_string + + # compute shared secret from server's public key and client's private key + pk = OpenSSL::PKey::EC::Point.new(OpenSSL::PKey::EC.new(curve_name).group, + OpenSSL::BN.new(result[:server_ecdh_pubkey], 2)) + result[:shared_secret] = OpenSSL::BN.new(ecdh.dh_compute_key(pk), 2) + + sig_buffer = Net::SSH::Buffer.new(buffer.read_string) + sig_type = sig_buffer.read_string + if sig_type != algorithms.host_key + raise Net::SSH::Exception, + "host key algorithm mismatch for signature " + + "'#{sig_type}' != '#{algorithms.host_key}'" + end + result[:server_sig] = sig_buffer.read_string + + return result + end + end +end; end; end; end diff --git a/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb b/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb new file mode 100644 index 0000000000000..b792410642772 --- /dev/null +++ b/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb @@ -0,0 +1,13 @@ +module Net; module SSH; module Transport; module Kex + + # A key-exchange service implementing the "ecdh-sha2-nistp256" + # key-exchange algorithm. (defined in RFC 5656) + class EcdhSHA2NistP384 < EcdhSHA2NistP256 + def digester + OpenSSL::Digest::SHA384 + end + def curve_name + OpenSSL::PKey::EC::CurveNameAlias['nistp384'] + end + end +end; end; end; end diff --git a/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb b/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb new file mode 100644 index 0000000000000..6623559c47d14 --- /dev/null +++ b/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb @@ -0,0 +1,13 @@ +module Net; module SSH; module Transport; module Kex + + # A key-exchange service implementing the "ecdh-sha2-nistp521" + # key-exchange algorithm. (defined in RFC 5656) + class EcdhSHA2NistP521 < EcdhSHA2NistP256 + def digester + OpenSSL::Digest::SHA512 + end + def curve_name + OpenSSL::PKey::EC::CurveNameAlias['nistp521'] + end + end +end; end; end; end diff --git a/lib/net/ssh/transport/key_expander.rb b/lib/net/ssh/transport/key_expander.rb new file mode 100644 index 0000000000000..9e6a018a54236 --- /dev/null +++ b/lib/net/ssh/transport/key_expander.rb @@ -0,0 +1,27 @@ +module Net; module SSH; module Transport + module KeyExpander + + # Generate a key value in accordance with the SSH2 specification. + # (RFC4253 7.2. "Output from Key Exchange") + def self.expand_key(bytes, start, options={}) + if bytes == 0 + return "" + end + + k = start[0, bytes] + return k if k.length >= bytes + + digester = options[:digester] or raise 'No digester supplied' + shared = options[:shared] or raise 'No shared secret supplied' + hash = options[:hash] or raise 'No hash supplied' + + while k.length < bytes + step = digester.digest(shared + hash + k) + bytes_needed = bytes - k.length + k << step[0, bytes_needed] + end + + return k + end + end +end; end; end diff --git a/lib/net/ssh/transport/openssl.rb b/lib/net/ssh/transport/openssl.rb index cd07e41c67b9b..1e355001daddb 100644 --- a/lib/net/ssh/transport/openssl.rb +++ b/lib/net/ssh/transport/openssl.rb @@ -1,6 +1,5 @@ -# -*- coding: binary -*- +# -*- coding: utf-8 -*- require 'openssl' -require 'net/ssh/buffer' module OpenSSL @@ -61,6 +60,10 @@ def ssh_type "ssh-rsa" end + def ssh_signature_type + ssh_type + end + # Converts the key to a blob, according to the SSH2 protocol. def to_blob @blob ||= Net::SSH::Buffer.from(:string, ssh_type, :bignum, e, :bignum, n).to_s @@ -88,6 +91,10 @@ def ssh_type "ssh-dss" end + def ssh_signature_type + ssh_type + end + # Converts the key to a blob, according to the SSH2 protocol. def to_blob @blob ||= Net::SSH::Buffer.from(:string, ssh_type, @@ -124,6 +131,119 @@ def ssh_do_sign(data) end end - end + if defined?(OpenSSL::PKey::EC) + # This class is originally defined in the OpenSSL module. As needed, methods + # have been added to it by the Net::SSH module for convenience in dealing + # with SSH functionality. + class EC + CurveNameAlias = { + "nistp256" => "prime256v1", + "nistp384" => "secp384r1", + "nistp521" => "secp521r1", + } + + CurveNameAliasInv = { + "prime256v1" => "nistp256", + "secp384r1" => "nistp384", + "secp521r1" => "nistp521", + } + + def self.read_keyblob(curve_name_in_type, buffer) + curve_name_in_key = buffer.read_string + unless curve_name_in_type == curve_name_in_key + raise Net::SSH::Exception, "curve name mismatched (`#{curve_name_in_key}' with `#{curve_name_in_type}')" + end + public_key_oct = buffer.read_string + begin + key = OpenSSL::PKey::EC.new(OpenSSL::PKey::EC::CurveNameAlias[curve_name_in_key]) + group = key.group + point = OpenSSL::PKey::EC::Point.new(group, OpenSSL::BN.new(public_key_oct, 2)) + key.public_key = point + + return key + rescue OpenSSL::PKey::ECError + raise NotImplementedError, "unsupported key type `#{type}'" + end + + end + + # Returns the description of this key type used by the + # SSH2 protocol, like "ecdsa-sha2-nistp256" + def ssh_type + "ecdsa-sha2-#{CurveNameAliasInv[self.group.curve_name]}" + end + def ssh_signature_type + ssh_type + end + + def digester + if self.group.curve_name =~ /^[a-z]+(\d+)\w*\z/ + curve_size = $1.to_i + if curve_size <= 256 + OpenSSL::Digest::SHA256.new + elsif curve_size <= 384 + OpenSSL::Digest::SHA384.new + else + OpenSSL::Digest::SHA512.new + end + else + OpenSSL::Digest::SHA256.new + end + end + private :digester + + # Converts the key to a blob, according to the SSH2 protocol. + def to_blob + @blob ||= Net::SSH::Buffer.from(:string, ssh_type, + :string, CurveNameAliasInv[self.group.curve_name], + :mstring, self.public_key.to_bn.to_s(2)).to_s + @blob + end + + # Verifies the given signature matches the given data. + def ssh_do_verify(sig, data) + digest = digester.digest(data) + a1sig = nil + + begin + sig_r_len = sig[0,4].unpack("H*")[0].to_i(16) + sig_l_len = sig[4+sig_r_len,4].unpack("H*")[0].to_i(16) + + sig_r = sig[4,sig_r_len].unpack("H*")[0] + sig_s = sig[4+sig_r_len+4,sig_l_len].unpack("H*")[0] + + a1sig = OpenSSL::ASN1::Sequence([ + OpenSSL::ASN1::Integer(sig_r.to_i(16)), + OpenSSL::ASN1::Integer(sig_s.to_i(16)), + ]) + rescue + end + + if a1sig == nil + return false + else + dsa_verify_asn1(digest, a1sig.to_der) + end + end + + # Returns the signature for the given data. + def ssh_do_sign(data) + digest = digester.digest(data) + sig = dsa_sign_asn1(digest) + a1sig = OpenSSL::ASN1.decode( sig ) + + sig_r = a1sig.value[0].value + sig_s = a1sig.value[1].value + + return Net::SSH::Buffer.from(:bignum, sig_r, :bignum, sig_s).to_s + end + end + else + class OpenSSL::PKey::ECError < RuntimeError + # for compatibility with interpreters + # without EC support (i.e. JRuby) + end + end + end end diff --git a/lib/net/ssh/transport/packet_stream.rb b/lib/net/ssh/transport/packet_stream.rb index b8c966b988cde..6555fc140a04d 100644 --- a/lib/net/ssh/transport/packet_stream.rb +++ b/lib/net/ssh/transport/packet_stream.rb @@ -1,11 +1,12 @@ -# -*- coding: binary -*- require 'net/ssh/buffered_io' require 'net/ssh/errors' require 'net/ssh/packet' +require 'net/ssh/ruby_compat' require 'net/ssh/transport/cipher_factory' require 'net/ssh/transport/hmac' require 'net/ssh/transport/state' + module Net; module SSH; module Transport # A module that builds additional functionality onto the Net::SSH::BufferedIo @@ -13,6 +14,8 @@ module Net; module SSH; module Transport # per the SSH2 protocol. It also adds an abstraction for polling packets, # to allow for both blocking and non-blocking reads. module PacketStream + PROXY_COMMAND_HOST_IP = ''.freeze + include BufferedIo def self.extended(object) @@ -56,14 +59,20 @@ def client_name end # The IP address of the peer (remote) end of the socket, as reported by - # the Rex socket. + # the socket. def peer_ip - @peer_ip ||= getpeername[1] - end + @peer_ip ||= + if respond_to?(:getpeername) + addr = getpeername + Socket.getnameinfo(addr, Socket::NI_NUMERICHOST | Socket::NI_NUMERICSERV).first + else + PROXY_COMMAND_HOST_IP + end + end # Returns true if the IO is available for reading, and false otherwise. def available_for_read? - result = IO.select([self], nil, nil, 0) + result = Net::SSH::Compat.io_select([self], nil, nil, 0) result && result.first.any? end @@ -75,7 +84,19 @@ def available_for_read? def next_packet(mode=:nonblock) case mode when :nonblock then - fill if available_for_read? + packet = poll_next_packet + return packet if packet + + if available_for_read? + if fill <= 0 + result = poll_next_packet + if result.nil? + raise Net::SSH::Disconnect, "connection closed by remote host" + else + return result + end + end + end poll_next_packet when :block then @@ -84,7 +105,7 @@ def next_packet(mode=:nonblock) return packet if packet loop do - result = IO.select([self]) or next + result = Net::SSH::Compat.io_select([self]) or next break if result.first.any? end @@ -113,18 +134,18 @@ def enqueue_packet(payload) payload = client.compress(payload) # the length of the packet, minus the padding - actual_length = 4 + payload.length + 1 + actual_length = 4 + payload.bytesize + 1 # compute the padding length padding_length = client.block_size - (actual_length % client.block_size) padding_length += client.block_size if padding_length < 4 # compute the packet length (sans the length field itself) - packet_length = payload.length + padding_length + 1 + packet_length = payload.bytesize + padding_length + 1 if packet_length < 16 padding_length += client.block_size - packet_length = payload.length + padding_length + 1 + packet_length = payload.bytesize + padding_length + 1 end padding = Array.new(padding_length) { rand(256) }.pack("C*") @@ -208,7 +229,6 @@ def poll_next_packet padding_length = @packet.read_byte payload = @packet.read(@packet_length - padding_length - 1) - padding = @packet.read(padding_length) if padding_length > 0 my_computed_hmac = server.hmac.digest([server.sequence_number, @packet.content].pack("NA*")) raise Net::SSH::Exception, "corrupted mac detected" if real_hmac != my_computed_hmac diff --git a/lib/net/ssh/transport/server_version.rb b/lib/net/ssh/transport/server_version.rb index 037b448bd9151..9faa1a1b865aa 100644 --- a/lib/net/ssh/transport/server_version.rb +++ b/lib/net/ssh/transport/server_version.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/errors' require 'net/ssh/loggable' require 'net/ssh/version' @@ -16,7 +15,7 @@ class ServerVersion include Loggable # The SSH version string as reported by Net::SSH - PROTO_VERSION = "SSH-2.0-OpenSSH_5.0" + PROTO_VERSION = "SSH-2.0-Ruby/Net::SSH_#{Net::SSH::Version::CURRENT} #{RUBY_PLATFORM}" # Any header text sent by the server prior to sending the version. attr_reader :header @@ -26,11 +25,11 @@ class ServerVersion # Instantiates a new ServerVersion and immediately (and synchronously) # negotiates the SSH protocol in effect, using the given socket. - def initialize(socket, logger) + def initialize(socket, logger, timeout = nil) @header = "" @version = nil @logger = logger - negotiate!(socket) + negotiate!(socket, timeout) end private @@ -38,16 +37,24 @@ def initialize(socket, logger) # Negotiates the SSH protocol to use, via the given socket. If the server # reports an incompatible SSH version (e.g., SSH1), this will raise an # exception. - def negotiate!(socket) + def negotiate!(socket, timeout) info { "negotiating protocol version" } + debug { "local is `#{PROTO_VERSION}'" } + socket.write "#{PROTO_VERSION}\r\n" + socket.flush + + if timeout && !IO.select([socket], nil, nil, timeout) + raise Net::SSH::ConnectionTimeout, "timeout during server version negotiating" + end loop do @version = "" loop do - version_timeout = (9000/1000.0)+3 # (3 to 12 seconds) - b = socket.get_once(1,version_timeout) - if b.nil? - raise Net::SSH::Disconnect, "connection timed out or closed by remote host" + begin + b = socket.readpartial(1) + raise Net::SSH::Disconnect, "connection closed by remote host" if b.nil? + rescue EOFError + raise Net::SSH::Disconnect, "connection closed by remote host" end @version << b break if b == "\n" @@ -63,10 +70,9 @@ def negotiate!(socket) raise Net::SSH::Exception, "incompatible SSH version `#{@version}'" end - debug { "local is `#{PROTO_VERSION}'" } - socket.write "#{PROTO_VERSION}\r\n" - socket.flush + if timeout && !IO.select(nil, [socket], nil, timeout) + raise Net::SSH::ConnectionTimeout, "timeout during client version negotiating" + end end end end; end; end - diff --git a/lib/net/ssh/transport/session.rb b/lib/net/ssh/transport/session.rb index 3bd4e33f66a52..d02e64becc796 100644 --- a/lib/net/ssh/transport/session.rb +++ b/lib/net/ssh/transport/session.rb @@ -1,6 +1,4 @@ -# -*- coding: binary -*- -require 'rex/socket' -require 'timeout' +require 'socket' require 'net/ssh/errors' require 'net/ssh/loggable' @@ -10,6 +8,7 @@ require 'net/ssh/transport/packet_stream' require 'net/ssh/transport/server_version' require 'net/ssh/verifiers/null' +require 'net/ssh/verifiers/secure' require 'net/ssh/verifiers/strict' require 'net/ssh/verifiers/lenient' @@ -59,29 +58,18 @@ def initialize(host, options={}) @host = host @port = options[:port] || DEFAULT_PORT + @bind_address = options[:bind_address] || nil @options = options - debug { "establishing connection to #{@host}:#{@port}" } - factory = options[:proxy] - - if (factory) - @socket = timeout(options[:timeout] || 0) { factory.open(@host, @port) } - else - @socket = timeout(options[:timeout] || 0) { - Rex::Socket::Tcp.create( - 'PeerHost' => @host, - 'PeerPort' => @port, - 'Proxies' => options[:proxies], - 'Context' => { - 'Msf' => options[:msframework], - 'MsfExploit' => options[:msfmodule] - } - ) - } - # Tell MSF to automatically close this socket on error or completion... - # This prevents resource leaks. - options[:msfmodule].add_socket(@socket) if options[:msfmodule] - end + @socket = + if (factory = options[:proxy]) + debug { "establishing connection to #{@host}:#{@port} through proxy" } + factory.open(@host, @port, options) + else + debug { "establishing connection to #{@host}:#{@port}" } + Socket.tcp(@host, @port, @bind_address, nil, + connect_timeout: options[:timeout]) + end @socket.extend(PacketStream) @socket.logger = @logger @@ -90,12 +78,23 @@ def initialize(host, options={}) @queue = [] - @host_key_verifier = select_host_key_verifier(options[:paranoid]) + @host_key_verifier = select_host_key_verifier(options[:verify_host_key]) - @server_version = ServerVersion.new(socket, logger) + + @server_version = ServerVersion.new(socket, logger, options[:timeout]) @algorithms = Algorithms.new(self, options) + @algorithms.start wait { algorithms.initialized? } + rescue Errno::ETIMEDOUT + raise Net::SSH::ConnectionTimeout + end + + def host_keys + @host_keys ||= begin + known_hosts = options.fetch(:known_hosts, KnownHosts) + known_hosts.search_for(options[:host_key_alias] || host_as_string, options) + end end # Returns the host (and possibly IP address) in a format compatible with @@ -104,11 +103,16 @@ def host_as_string @host_as_string ||= begin string = "#{host}" string = "[#{string}]:#{port}" if port != DEFAULT_PORT - if socket.peer_ip != host - string2 = socket.peer_ip + + peer_ip = socket.peer_ip + + if peer_ip != Net::SSH::Transport::PacketStream::PROXY_COMMAND_HOST_IP && + peer_ip != host + string2 = peer_ip string2 = "[#{string2}]:#{port}" if port != DEFAULT_PORT string << "," << string2 end + string end end @@ -160,7 +164,7 @@ def rekey_as_needed # Returns a hash of information about the peer (remote) side of the socket, # including :ip, :port, :host, and :canonized (see #host_as_string). def peer - @peer ||= { :ip => socket.peer_ip, :port => @port.to_i, :host => @host, :canonized => host_as_string } + @peer ||= { ip: socket.peer_ip, port: @port.to_i, host: @host, canonized: host_as_string } end # Blocks until a new packet is available to be read, and returns that @@ -194,7 +198,7 @@ def poll_message(mode=:nonblock, consume_queue=true) raise Net::SSH::Disconnect, "disconnected: #{packet[:description]} (#{packet[:reason_code]})" when IGNORE - debug { "IGNORE packet recieved: #{packet[:data].inspect}" } + debug { "IGNORE packet received: #{packet[:data].inspect}" } when UNIMPLEMENTED lwarn { "UNIMPLEMENTED: #{packet[:number]}" } @@ -273,25 +277,31 @@ def hint(which, value=true) # Instantiates a new host-key verification class, based on the value of # the parameter. When true or nil, the default Lenient verifier is # returned. If it is false, the Null verifier is returned, and if it is - # :very, the Strict verifier is returned. If the argument happens to - # respond to :verify, it is returned directly. Otherwise, an exception + # :very, the Strict verifier is returned. If it is :secure, the even more + # strict Secure verifier is returned. If the argument happens to respond + # to :verify, it is returned directly. Otherwise, an exception # is raised. - def select_host_key_verifier(paranoid) - case paranoid + def select_host_key_verifier(verify_host_key) + case verify_host_key when true, nil then Net::SSH::Verifiers::Lenient.new when false then Net::SSH::Verifiers::Null.new when :very then Net::SSH::Verifiers::Strict.new + when :secure then + Net::SSH::Verifiers::Secure.new else - if paranoid.respond_to?(:verify) - paranoid + if verify_host_key.respond_to?(:verify) + verify_host_key else - raise ArgumentError, "argument to :paranoid is not valid: #{paranoid.inspect}" + raise( + ArgumentError, + "Invalid argument to :verify_host_key (or deprecated " \ + ":paranoid): #{verify_host_key.inspect}" + ) end end end end end; end; end - diff --git a/lib/net/ssh/transport/state.rb b/lib/net/ssh/transport/state.rb index 165f0cce95489..31f06c5c81ec6 100644 --- a/lib/net/ssh/transport/state.rb +++ b/lib/net/ssh/transport/state.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'zlib' require 'net/ssh/transport/cipher_factory' require 'net/ssh/transport/hmac' @@ -193,7 +192,7 @@ def needs_rekey? def update_next_iv(data, reset=false) @next_iv << data - @next_iv = @next_iv[-cipher.iv_len..-1] + @next_iv = @next_iv[@next_iv.size-cipher.iv_len..-1] if reset cipher.reset diff --git a/lib/net/ssh/utils.rb b/lib/net/ssh/utils.rb deleted file mode 100644 index 32d4138f7bba5..0000000000000 --- a/lib/net/ssh/utils.rb +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: binary -*- -require 'net/ssh' -require 'rex' - -module Net -module SSH - -# A place to define convenience utils for Net:SSH -module Utils -class Key - class << self - - # Returns the fingerprint of a key file or key data. Usage: - # Net::SSH::Utils::Key.fingerprint(:file => "id_rsa") - # => "af:76:e4:f8:37:7b:52:8c:77:61:5b:d3:b0:d3:05:e4" - # - # If both :file and :data are provided, :data will be read. - # :format may be one of :binary, :compact, or nil (in which case colon-delimited will be returned) - # If the key is a public key, it must be declared as such by :public => true. Default is private. - def fingerprint(args={}) - file = args[:file] || args[:f] - data = args[:data] || args[:d] - method = ((args[:public] || args[:pub]) ? :load_public_key : :load_private_key) - format = args[:format] - if data - fd = Tempfile.new("msf3-sshkey-temp-") - fd.binmode - fd.write data - fd.flush - file = fd.path - end - key = KeyFactory.send method,file - fp = key.fingerprint - case args[:format] - when :binary,:bin,:b - return fp.split(":").map {|x| x.to_i(16)}.pack("C16") - when :compact,:com,:c - return fp.split(":").join - else - return fp - end - end - -end -end -end -end -end diff --git a/lib/net/ssh/verifiers/lenient.rb b/lib/net/ssh/verifiers/lenient.rb index 9ac6143846dad..1fcdd58345674 100644 --- a/lib/net/ssh/verifiers/lenient.rb +++ b/lib/net/ssh/verifiers/lenient.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- require 'net/ssh/verifiers/strict' module Net; module SSH; module Verifiers @@ -28,4 +27,4 @@ def tunnelled?(args) end end -end; end; end +end; end; end \ No newline at end of file diff --git a/lib/net/ssh/verifiers/null.rb b/lib/net/ssh/verifiers/null.rb index 313159503c780..c2bda3a0dfbef 100644 --- a/lib/net/ssh/verifiers/null.rb +++ b/lib/net/ssh/verifiers/null.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH; module Verifiers # The Null host key verifier simply allows every key it sees, without @@ -10,4 +9,4 @@ def verify(arguments) end end -end; end; end +end; end; end \ No newline at end of file diff --git a/lib/net/ssh/verifiers/secure.rb b/lib/net/ssh/verifiers/secure.rb new file mode 100644 index 0000000000000..b5b88239526ad --- /dev/null +++ b/lib/net/ssh/verifiers/secure.rb @@ -0,0 +1,52 @@ +require 'net/ssh/errors' +require 'net/ssh/known_hosts' + +module Net; module SSH; module Verifiers + + # Does a strict host verification, looking the server up in the known + # host files to see if a key has already been seen for this server. If this + # server does not appear in any host file, an exception will be raised + # (HostKeyUnknown). This is in contrast to the "Strict" class, which will + # silently add the key to your known_hosts file. If the server does appear at + # least once, but the key given does not match any known for the server, an + # exception will be raised (HostKeyMismatch). + # Otherwise, this returns true. + class Secure + def verify(arguments) + host_keys = arguments[:session].host_keys + + # We've never seen this host before, so raise an exception. + if host_keys.empty? + process_cache_miss(host_keys, arguments, HostKeyUnknown, "is unknown") + end + + # If we found any matches, check to see that the key type and + # blob also match. + found = host_keys.any? do |key| + key.ssh_type == arguments[:key].ssh_type && + key.to_blob == arguments[:key].to_blob + end + + # If a match was found, return true. Otherwise, raise an exception + # indicating that the key was not recognized. + unless found + process_cache_miss(host_keys, arguments, HostKeyMismatch, "does not match") + end + + found + end + + private + + def process_cache_miss(host_keys, args, exc_class, message) + exception = exc_class.new("fingerprint #{args[:fingerprint]} " + + "#{message} for #{host_keys.host.inspect}") + exception.data = args + exception.callback = Proc.new do + host_keys.add_host_key(args[:key]) + end + raise exception + end + end + +end; end; end diff --git a/lib/net/ssh/verifiers/strict.rb b/lib/net/ssh/verifiers/strict.rb index 5e16248ac44bb..2beb7fe152caa 100644 --- a/lib/net/ssh/verifiers/strict.rb +++ b/lib/net/ssh/verifiers/strict.rb @@ -1,6 +1,6 @@ -# -*- coding: binary -*- require 'net/ssh/errors' require 'net/ssh/known_hosts' +require 'net/ssh/verifiers/secure' module Net; module SSH; module Verifiers @@ -10,51 +10,15 @@ module Net; module SSH; module Verifiers # server. If the server does appear at least once, but the key given does # not match any known for the server, an exception will be raised (HostKeyMismatch). # Otherwise, this returns true. - class Strict + class Strict < Secure def verify(arguments) - options = arguments[:session].options - host = options[:host_key_alias] || arguments[:session].host_as_string - matches = [] - if options[:config] - matches = Net::SSH::KnownHosts.search_for(host, arguments[:session].options) - end - # we've never seen this host before, so just automatically add the key. - # not the most secure option (since the first hit might be the one that - # is hacked), but since almost nobody actually compares the key - # fingerprint, this is a reasonable compromise between usability and - # security. - if matches.empty? - ip = arguments[:session].peer[:ip] - if options[:config] - Net::SSH::KnownHosts.add(host, arguments[:key], arguments[:session].options) - end + begin + super + rescue HostKeyUnknown => err + err.remember_host! return true end - - # If we found any matches, check to see that the key type and - # blob also match. - found = matches.any? do |key| - key.ssh_type == arguments[:key].ssh_type && - key.to_blob == arguments[:key].to_blob - end - - # If a match was found, return true. Otherwise, raise an exception - # indicating that the key was not recognized. - found || process_cache_miss(host, arguments) end - - private - - def process_cache_miss(host, args) - exception = HostKeyMismatch.new("fingerprint #{args[:fingerprint]} does not match for #{host.inspect}") - exception.data = args - if options[:config] - exception.callback = Proc.new do - Net::SSH::KnownHosts.add(host, args[:key], args[:session].options) - end - end - raise exception - end end end; end; end diff --git a/lib/net/ssh/version.rb b/lib/net/ssh/version.rb index e27bb0df0f51c..e380c6065f39c 100644 --- a/lib/net/ssh/version.rb +++ b/lib/net/ssh/version.rb @@ -1,4 +1,3 @@ -# -*- coding: binary -*- module Net; module SSH # A class for describing the current version of a library. The version # consists of three parts: the +major+ number, the +minor+ number, and the @@ -17,16 +16,15 @@ class Version # A convenience method for instantiating a new Version instance with the # given +major+, +minor+, and +tiny+ components. - def self.[](major, minor, tiny) - new(major, minor, tiny) + def self.[](major, minor, tiny, pre = nil) + new(major, minor, tiny, pre) end - attr_reader :major, :minor, :tiny, :msf3 + attr_reader :major, :minor, :tiny # Create a new Version object with the given components. - def initialize(major, minor, tiny) - @major, @minor, @tiny = major, minor, tiny - @msf3 = true + def initialize(major, minor, tiny, pre = nil) + @major, @minor, @tiny, @pre = major, minor, tiny, pre end # Compare this version to the given +version+ object. @@ -37,7 +35,7 @@ def <=>(version) # Converts this version object to a string, where each of the three # version components are joined by the '.' character. E.g., 2.0.0. def to_s - @to_s ||= [@major, @minor, @tiny].join(".") + @to_s ||= [@major, @minor, @tiny, @pre].compact.join(".") end # Converts this version to a canonical integer that may be compared @@ -47,16 +45,20 @@ def to_i end # The major component of this version of the Net::SSH library - MAJOR = 2 + MAJOR = 4 # The minor component of this version of the Net::SSH library - MINOR = 0 + MINOR = 2 # The tiny component of this version of the Net::SSH library - TINY = 12 + TINY = 0 + + # The prerelease component of this version of the Net::SSH library + # nil allowed + PRE = nil # The current version of the Net::SSH library as a Version instance - CURRENT = new(MAJOR, MINOR, TINY) + CURRENT = new(*[MAJOR, MINOR, TINY, PRE].compact) # The current version of the Net::SSH library as a String STRING = CURRENT.to_s diff --git a/lib/snmp/ber.rb b/lib/snmp/ber.rb index b750acd3b6609..4299b39def0e9 100644 --- a/lib/snmp/ber.rb +++ b/lib/snmp/ber.rb @@ -8,14 +8,16 @@ # # -# Add ord method to Fixnum for forward compatibility with Ruby 1.9 +# Add ord method to Integer for forward compatibility with Ruby 1.9 # -if "a"[0].kind_of? Fixnum - unless Fixnum.methods.include? :ord - class Fixnum - def ord; self; end - end +if "a"[0].is_a?(Integer) + unless Integer.methods.include? :ord + class Integer + def ord + self + end end + end end # @@ -41,17 +43,17 @@ module BER #:nodoc:all GetBulkRequest_PDU_TAG = 0xa5 InformRequest_PDU_TAG = 0xa6 SNMPv2_Trap_PDU_TAG = 0xa7 - Report_PDU_TAG = 0xa8 # Note: Usage not defined - not supported + Report_PDU_TAG = 0xa8 # Note: Usage not defined - not supported # Primitive ASN.1 data types INTEGER_TAG = 0x02 OCTET_STRING_TAG = 0x04 NULL_TAG = 0x05 OBJECT_IDENTIFIER_TAG = 0x06 - + # Constructed ASN.1 data type SEQUENCE_TAG = 0x30 - + # SNMP application data types # See RFC 1155 for SNMPv1 # See RFC 1902 for SNMPv2c @@ -62,14 +64,14 @@ module BER #:nodoc:all TimeTicks_TAG = 0x43 Opaque_TAG = 0x44 Counter64_TAG = 0x46 - + # VarBind response exceptions NoSuchObject_TAG = 0x80 NoSuchInstance_TAG = 0x81 EndOfMibView_TAG = 0x82 - + # Exceptions thrown in this module - class OutOfData < RuntimeError; end + class OutOfData < RuntimeError; end class InvalidLength < RuntimeError; end class InvalidTag < RuntimeError; end class InvalidObjectId < RuntimeError; end @@ -77,7 +79,7 @@ class InvalidObjectId < RuntimeError; end def assert_no_remainder(remainder) raise ParseError, remainder.inspect if (remainder and remainder != "") end - + # # Decode tag-length-value data. The data is assumed to be a string of # bytes in network byte order. This format is returned by Socket#recv. @@ -110,7 +112,7 @@ def decode_tlv(data) end return tag, value, remainder end - + # # Decode TLV data for an ASN.1 integer. # @@ -129,7 +131,7 @@ def decode_timeticks(data) raise InvalidTag, tag.to_s if tag != TimeTicks_TAG return decode_uinteger_value(value), remainder end - + def decode_integer_value(value) result = build_integer(value, 0, value.length) if value[0].ord[7] == 1 @@ -137,7 +139,7 @@ def decode_integer_value(value) end result end - + ## # Decode an integer, ignoring the sign bit. Some agents insist on # encoding 32 bit unsigned integers with four bytes even though it @@ -146,7 +148,7 @@ def decode_integer_value(value) def decode_uinteger_value(value) build_integer(value, 0, value.length) end - + def build_integer(data, start, num_octets) number = 0 num_octets.times { |i| number = number<<8 | data[start+i].ord } @@ -159,26 +161,26 @@ def build_integer(data, start, num_octets) # Throws an InvalidTag exception if the tag is incorrect. # # Returns a tuple containing a string and any remaining unprocessed data. - # + # def decode_octet_string(data) tag, value, remainder = decode_tlv(data) raise InvalidTag, tag.to_s if tag != OCTET_STRING_TAG return value, remainder end - + def decode_ip_address(data) tag, value, remainder = decode_tlv(data) raise InvalidTag, tag.to_s if tag != IpAddress_TAG raise InvalidLength, tag.to_s if value.length != 4 return value, remainder end - + # # Decode TLV data for an ASN.1 sequence. # # Throws an InvalidTag exception if the tag is incorrect. # - # Returns a tuple containing the sequence data and any remaining + # Returns a tuple containing the sequence data and any remaining # unprocessed data that follows the sequence. # def decode_sequence(data) @@ -186,7 +188,7 @@ def decode_sequence(data) raise InvalidTag, tag.to_s if tag != SEQUENCE_TAG return value, remainder end - + # # Unwrap TLV data for an ASN.1 object identifier. This method extracts # the OID value as a character string but does not decode it further. @@ -202,7 +204,7 @@ def decode_object_id(data) raise InvalidTag, tag.to_s if tag != OBJECT_IDENTIFIER_TAG return decode_object_id_value(value), remainder end - + def decode_object_id_value(value) if value.length == 0 object_id = [] @@ -222,12 +224,12 @@ def decode_object_id_value(value) if value[i].ord < 0x80 object_id << n n = 0 - end + end end end return object_id end - + # # Encode the length field for TLV data. Returns the length octets # as a string. @@ -248,21 +250,21 @@ def encode_length(length) def encode_integer(value) encode_tagged_integer(INTEGER_TAG, value) end - + def encode_tagged_integer(tag, value) if value > 0 && value < 0x80 data = value.chr else data = integer_to_octets(value) if value > 0 && data[0].ord > 0x7f - data = "\000" << data + data = "\000" << data elsif value < 0 && data[0].ord < 0x80 data = "\377" << data end end encode_tlv(tag, data) end - + # # Helper method for encoding integer-like things. # @@ -279,11 +281,11 @@ def integer_to_octets(i) end until i == done octets end - + def encode_null NULL_TAG.chr << "\000" end - + # # Encode an exception. The encoding is simply the exception tag with # no data, similar to NULL. @@ -291,7 +293,7 @@ def encode_null def encode_exception(tag) tag.chr << "\000" end - + # # Wraps value in a tag and length. This method expects an # integer tag and a string value. @@ -301,21 +303,21 @@ def encode_tlv(tag, value) data = data << value if value.length > 0 data end - + # # Wrap string in a octet string tag and length. # def encode_octet_string(value) encode_tlv(OCTET_STRING_TAG, value) end - + # # Wrap value in a sequence tag and length. # def encode_sequence(value) encode_tlv(SEQUENCE_TAG, value) end - + # # Encode an object id. The input is assumed to be an array of integers # representing the object id. @@ -346,7 +348,7 @@ def encode_object_id(value) end encode_tlv(OBJECT_IDENTIFIER_TAG, data) end - + end end diff --git a/lib/windows_console_color_support.rb b/lib/windows_console_color_support.rb index 2b626cc078c7b..3fa9ba51f2164 100644 --- a/lib/windows_console_color_support.rb +++ b/lib/windows_console_color_support.rb @@ -6,17 +6,17 @@ class WindowsConsoleColorSupport STD_OUTPUT_HANDLE = -11 COLORS = [0, 4, 2, 6, 1, 5, 3, 7] - + def initialize(origstream) @origstream = origstream - + # initialize API @GetStdHandle = Win32API.new("kernel32","GetStdHandle",['L'],'L') @GetConsoleScreenBufferInfo = Win32API.new("kernel32","GetConsoleScreenBufferInfo",['L','P'],'L') @SetConsoleTextAttribute = Win32API.new("kernel32","SetConsoleTextAttribute",['L','l'],'L') @hConsoleHandle = @GetStdHandle.Call(STD_OUTPUT_HANDLE) end - + def write(msg) rest = msg while (rest =~ Regexp.new("([^\e]*)\e\\[([0-9;]+)m")) @@ -28,7 +28,7 @@ def write(msg) end @origstream.write(rest) end - + def flush @origstream.flush end @@ -37,7 +37,7 @@ def setcolor(color) csbi = 0.chr * 24 @GetConsoleScreenBufferInfo.Call(@hConsoleHandle,csbi) wAttr = csbi[8,2].unpack('v').first - + case color when 0 # reset wAttr = 0x07 @@ -54,7 +54,7 @@ def setcolor(color) when 40 .. 47 # background colors wAttr = (wAttr & ~0x70) | (COLORS[color - 40] << 4) end - + @SetConsoleTextAttribute.Call(@hConsoleHandle, wAttr) - end + end end diff --git a/metasploit-framework-db.gemspec b/metasploit-framework-db.gemspec index 5b261dbf35d36..63dfac650b088 100644 --- a/metasploit-framework-db.gemspec +++ b/metasploit-framework-db.gemspec @@ -28,10 +28,6 @@ Gem::Specification.new do |spec| spec.files = [] spec.add_runtime_dependency 'activerecord', *Metasploit::Framework::RailsVersionConstraint::RAILS_VERSION - # Metasploit::Credential database models - spec.add_runtime_dependency 'metasploit-credential', '~> 1.0' - # Database models shared between framework and Pro. - spec.add_runtime_dependency 'metasploit_data_models', '~> 1.0' # depend on metasploit-framewrok as the optional gems are useless with the actual code spec.add_runtime_dependency 'metasploit-framework', "= #{spec.version}" # Needed for module caching in Mdm::ModuleDetails diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index 41d93d8c7c4ff..84c976fd708d7 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -58,11 +58,6 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'jsobfu', '~> 0.2.0' # Needed for some admin modules (scrutinizer_add_user.rb) spec.add_runtime_dependency 'json' - # Metasploit::Concern hooks - spec.add_runtime_dependency 'metasploit-concern', '~> 1.0' - # 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', '~> 1.0' # Needed for Meterpreter on Windows, soon others. spec.add_runtime_dependency 'metasploit-payloads', '0.0.7' # Needed by msfgui and other rpc components