Vastly improved communicate as new user PBA code structure, also not leaking any more process or thread handles.

This commit is contained in:
Shay Nehmad 2019-09-05 20:56:48 +03:00
parent e9cd20a345
commit e618378c95
4 changed files with 134 additions and 81 deletions

View File

@ -0,0 +1,64 @@
import logging
import subprocess
from infection_monkey.post_breach.actions.add_user import BackdoorUser
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
logger = logging.getLogger(__name__)
class NewUserError(Exception):
pass
class NewUser(object):
"""
RAII object to use for creating and using a new user in Windows. Use with `with`.
User will be created when the instance is instantiated.
User will log on start of `with` scope.
User will log off on end of `with` scope.
Example:
# Created # Logged on
with NewUser("user", "pass") as new_user:
...
...
# Logged off
...
"""
def __init__(self, username, password):
"""
Creates a user with the username + password.
:raises: subprocess.CalledProcessError if failed to add the user.
"""
self.username = username
self.password = password
windows_cmds = BackdoorUser.get_windows_commands_to_add_user(self.username, self.password, True)
logger.debug("Trying these commands: {}".format(str(windows_cmds)))
_ = subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True)
def __enter__(self):
# Importing these only on windows, as they won't exist on linux.
import win32security
import win32con
try:
# Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera
self.logon_handle = win32security.LogonUser(
self.username,
".", # Use current domain.
self.password,
win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user).
win32con.LOGON32_PROVIDER_DEFAULT) # Which logon provider to use - whatever Windows offers.
except Exception as err:
raise NewUserError("Can't logon as {}. Error: {}".format(self.username, str(err)))
return self
def get_logon_handle(self):
return self.logon_handle
def __exit__(self, exc_type, exc_val, exc_tb):
self.logon_handle.Close()
# TODO Delete user

View File

@ -5,6 +5,7 @@ import string
import subprocess
from common.data.post_breach_consts import POST_BREACH_COMMUNICATE_AS_NEW_USER
from infection_monkey.monkey_utils.windows.new_user import NewUser, NewUserError
from infection_monkey.post_breach.actions.add_user import BackdoorUser
from infection_monkey.post_breach.pba import PBA
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
@ -31,88 +32,76 @@ class CommunicateAsNewUser(PBA):
def run(self):
username = USERNAME + ''.join(random.choice(string.ascii_lowercase) for _ in range(5))
if is_windows_os():
# Importing these only on windows, as they won't exist on linux.
import win32con
import win32process
import win32security
if not self.try_to_create_user_windows(username, PASSWORD):
return # no point to continue if failed creating the user.
try:
# Logon as new user: https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonusera
new_user_logon_token_handle = win32security.LogonUser(
username,
".", # use current domain
PASSWORD,
win32con.LOGON32_LOGON_INTERACTIVE, # logon type - interactive (normal user)
win32con.LOGON32_PROVIDER_DEFAULT) # logon provider
except Exception as e:
PostBreachTelem(
self,
("Can't logon as {}. Error: {}".format(username, e.message), False)
).send()
return # no point to continue if can't log on.
# Using os.path is OK, as this is on windows for sure
ping_app_path = os.path.join(os.environ["WINDIR"], "system32", "PING.exe")
if not os.path.exists(ping_app_path):
PostBreachTelem(self, ("{} not found.".format(ping_app_path), False)).send()
return # Can't continue without ping.
try:
# Open process as that user:
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera
commandline = "{} {}".format(ping_app_path, "google.com")
_ = win32process.CreateProcessAsUser(
new_user_logon_token_handle, # A handle to the primary token that represents a user.
None, # The name of the module to be executed.
commandline, # The command line to be executed.
None, # Process attributes
None, # Thread attributes
True, # Should inherit handles
win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process.
None, # An environment block for the new process. If this parameter is NULL, the new process
# uses the environment of the calling process.
None, # CWD. If this parameter is NULL, the new process will have the same current drive and
# directory as the calling process.
win32process.STARTUPINFO() # STARTUPINFO structure.
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
)
PostBreachTelem(self, (
CREATED_PROCESS_AS_USER_WINDOWS_FORMAT.format(commandline, username), True)).send()
return
except Exception as e:
# TODO: if failed on 1314, we can try to add elevate the rights of the current user with the "Replace a
# process level token" right, using Local Security Policy editing. Worked, but only after reboot. So:
# 1. need to decide if worth it, and then
# 2. need to find how to do this using python...
PostBreachTelem(self, (
"Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send()
return
self.communicate_as_new_user_windows(username)
else:
try:
linux_cmds = BackdoorUser.get_linux_commands_to_add_user(username)
commandline = "ping -c 2 google.com"
linux_cmds.extend([";", "sudo", "-u", username, commandline])
final_command = ' '.join(linux_cmds)
logger.debug("Trying to execute these commands: {}".format(final_command))
output = subprocess.check_output(final_command, stderr=subprocess.STDOUT, shell=True)
PostBreachTelem(self, (
CREATED_PROCESS_AS_USER_LINUX_FORMAT.format(commandline, username, output[:150]), True)).send()
return
except subprocess.CalledProcessError as e:
PostBreachTelem(self, (e.output, False)).send()
return
self.communicate_as_new_user_linux(username)
def try_to_create_user_windows(self, username, password):
def communicate_as_new_user_linux(self, username):
try:
windows_cmds = BackdoorUser.get_windows_commands_to_add_user(username, password, True)
logger.debug("Trying these commands: {}".format(str(windows_cmds)))
subprocess.check_output(windows_cmds, stderr=subprocess.STDOUT, shell=True)
return True
except subprocess.CalledProcessError as e:
linux_cmds = BackdoorUser.get_linux_commands_to_add_user(username)
commandline = "ping -c 2 google.com"
linux_cmds.extend([";", "sudo", "-u", username, commandline])
final_command = ' '.join(linux_cmds)
logger.debug("Trying to execute these commands: {}".format(final_command))
output = subprocess.check_output(final_command, stderr=subprocess.STDOUT, shell=True)
PostBreachTelem(self, (
"Couldn't create the user '{}'. Error output is: '{}'".format(username, e.output), False)).send()
return False
CREATED_PROCESS_AS_USER_LINUX_FORMAT.format(commandline, username, output[:150]), True)).send()
except subprocess.CalledProcessError as e:
PostBreachTelem(self, (e.output, False)).send()
def communicate_as_new_user_windows(self, username):
# Importing these only on windows, as they won't exist on linux.
import win32con
import win32process
import win32api
try:
with NewUser(username, PASSWORD) as new_user:
# Using os.path is OK, as this is on windows for sure
ping_app_path = os.path.join(os.environ["WINDIR"], "system32", "PING.exe")
if not os.path.exists(ping_app_path):
PostBreachTelem(self, ("{} not found.".format(ping_app_path), False)).send()
return # Can't continue without ping.
try:
# Open process as that user:
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera
commandline = "{} {} {} {}".format(ping_app_path, "google.com", "-n", "2")
process_handle = win32process.CreateProcessAsUser(
new_user.get_logon_handle(), # A handle to the primary token that represents a user.
None, # The name of the module to be executed.
commandline, # The command line to be executed.
None, # Process attributes
None, # Thread attributes
True, # Should inherit handles
win32con.NORMAL_PRIORITY_CLASS, # The priority class and the creation of the process.
None, # An environment block for the new process. If this parameter is NULL, the new process
# uses the environment of the calling process.
None, # CWD. If this parameter is NULL, the new process will have the same current drive and
# directory as the calling process.
win32process.STARTUPINFO() # STARTUPINFO structure.
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
)
PostBreachTelem(self,
(CREATED_PROCESS_AS_USER_WINDOWS_FORMAT.format(commandline, username), True)).send()
win32api.CloseHandle(process_handle[0]) # Process handle
win32api.CloseHandle(process_handle[1]) # Thread handle
except Exception as e:
# TODO: if failed on 1314, we can try to add elevate the rights of the current user with the
# "Replace a process level token" right, using Local Security Policy editing. Worked, but only
# after reboot. So:
# 1. need to decide if worth it, and then
# 2. need to find how to do this using python...
PostBreachTelem(self, (
"Failed to open process as user {}. Error: {}".format(username, str(e)), False)).send()
# Nothing more we can do. Leak the process handle.
except subprocess.CalledProcessError as err:
PostBreachTelem(self, (
"Couldn't create the user '{}'. Error output is: '{}'".format(username, str(err)),
False)).send()
except NewUserError as e:
PostBreachTelem(self, (str(e), False)).send()