Merge pull request #517 from guardicore/feature/515-add-curl-pba

Replaced ping with curl/wget and Invoke-WebRequest
This commit is contained in:
Shay Nehmad 2019-12-25 12:52:56 +02:00 committed by GitHub
commit 52eced33e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 42 additions and 35 deletions

View File

@ -10,10 +10,10 @@ from infection_monkey.post_breach.pba import PBA
from infection_monkey.telemetry.post_breach_telem import PostBreachTelem
from infection_monkey.utils.environment import is_windows_os
PING_TEST_DOMAIN = "google.com"
INFECTION_MONKEY_WEBSITE_URL = "https://infectionmonkey.com/"
CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT = "Created process '{}' as user '{}', and successfully pinged."
CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT = "Created process '{}' as user '{}', but failed to ping (exit status {})."
CREATED_PROCESS_AS_USER_SUCCESS_FORMAT = "Created process '{}' as user '{}' and the process succeeded."
CREATED_PROCESS_AS_USER_FAILED_FORMAT = "Created process '{}' as user '{}', but the process failed (exit status {}:{})."
USERNAME_PREFIX = "somenewuser"
PASSWORD = "N3WPa55W0rD!1"
@ -23,8 +23,8 @@ logger = logging.getLogger(__name__)
class CommunicateAsNewUser(PBA):
"""
This PBA creates a new user, and then pings google as that user. This is used for a Zero Trust test of the People
pillar. See the relevant telemetry processing to see what findings are created.
This PBA creates a new user, and then creates HTTPS requests as that user. This is used for a Zero Trust test of the
People pillar. See the relevant telemetry processing to see what findings are created.
"""
def __init__(self):
@ -34,9 +34,9 @@ class CommunicateAsNewUser(PBA):
username = CommunicateAsNewUser.get_random_new_user_name()
try:
with create_auto_new_user(username, PASSWORD) as new_user:
ping_commandline = CommunicateAsNewUser.get_commandline_for_ping()
exit_status = new_user.run_as(ping_commandline)
self.send_ping_result_telemetry(exit_status, ping_commandline, username)
http_request_commandline = CommunicateAsNewUser.get_commandline_for_http_request(INFECTION_MONKEY_WEBSITE_URL)
exit_status = new_user.run_as(http_request_commandline)
self.send_result_telemetry(exit_status, http_request_commandline, username)
except subprocess.CalledProcessError as e:
PostBreachTelem(self, (e.output.decode(), False)).send()
except NewUserError as e:
@ -47,21 +47,33 @@ class CommunicateAsNewUser(PBA):
return USERNAME_PREFIX + ''.join(random.choice(string.ascii_lowercase) for _ in range(5))
@staticmethod
def get_commandline_for_ping(domain=PING_TEST_DOMAIN, is_windows=is_windows_os()):
format_string = "PING.exe {domain} -n 1" if is_windows else "ping -c 1 {domain}"
return format_string.format(domain=domain)
def get_commandline_for_http_request(url, is_windows=is_windows_os()):
if is_windows:
format_string = 'powershell.exe -command "Invoke-WebRequest {url}" -UseBasicParsing'
else:
# true || false -> 0. false || true -> 0. false || false -> 1. So:
# if curl works, we're good.
# If curl doesn't exist or fails and wget work, we're good.
# And if both don't exist: we'll call it a win.
format_string = "curl {url} || wget -O/dev/null -q {url}"
return format_string.format(url=url)
def send_ping_result_telemetry(self, exit_status, commandline, username):
def send_result_telemetry(self, exit_status, commandline, username):
"""
Parses the result of ping and sends telemetry accordingly.
Parses the result of the command and sends telemetry accordingly.
:param exit_status: In both Windows and Linux, 0 exit code from Ping indicates success.
:param exit_status: In both Windows and Linux, 0 exit code indicates success.
:param commandline: Exact commandline which was executed, for reporting back.
:param username: Username from which the command was executed, for reporting back.
"""
if exit_status == 0:
PostBreachTelem(self, (
CREATED_PROCESS_AS_USER_PING_SUCCESS_FORMAT.format(commandline, username), True)).send()
CREATED_PROCESS_AS_USER_SUCCESS_FORMAT.format(commandline, username), True)).send()
else:
PostBreachTelem(self, (
CREATED_PROCESS_AS_USER_PING_FAILED_FORMAT.format(commandline, username, exit_status), False)).send()
CREATED_PROCESS_AS_USER_FAILED_FORMAT.format(
commandline, username, exit_status, twos_complement(exit_status)), False)).send()
def twos_complement(exit_status):
return hex(exit_status & (2 ** 32 - 1))

View File

@ -13,3 +13,4 @@ wmi
pywin32 ; sys_platform == 'win32'
pymssql<3.0
pyftpdlib
WinSys-3.x

View File

@ -5,7 +5,7 @@ from infection_monkey.utils.auto_new_user import AutoNewUser
from infection_monkey.utils.new_user_error import NewUserError
ACTIVE_NO_NET_USER = '/ACTIVE:NO'
WAIT_TIMEOUT_IN_MILLISECONDS = 20 * 1000
WAIT_TIMEOUT_IN_MILLISECONDS = 60 * 1000
logger = logging.getLogger(__name__)
@ -65,8 +65,7 @@ class AutoNewWindowsUser(AutoNewUser):
self.username,
".", # Use current domain.
self.password,
win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user). Need this to open ping
# using a shell.
win32con.LOGON32_LOGON_INTERACTIVE, # Logon type - interactive (normal user), since we're using a shell.
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)))
@ -78,33 +77,28 @@ class AutoNewWindowsUser(AutoNewUser):
import win32process
import win32api
import win32event
from winsys import _advapi32
exit_code = -1
process_handle = None
thread_handle = None
try:
# Open process as that user:
# https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessasusera
process_handle, thread_handle, _, _ = win32process.CreateProcessAsUser(
self.get_logon_handle(), # A handle to the primary token that represents a user.
None, # The name of the module to be executed.
command, # 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
# Open process as that user
# https://github.com/tjguk/winsys/blob/master/winsys/_advapi32.py
proc_info = _advapi32.CreateProcessWithLogonW(
username=self.username,
domain=".",
password=self.password,
command_line=command
)
process_handle = proc_info.hProcess
thread_handle = proc_info.hThread
logger.debug(
"Waiting for process to finish. Timeout: {}ms".format(WAIT_TIMEOUT_IN_MILLISECONDS))
# https://social.msdn.microsoft.com/Forums/vstudio/en-US/b6d6a7ae-71e9-4edb-ac8f-408d2a41750d/what-events-on-a-process-handle-signal-satisify-waitforsingleobject?forum=vcgeneral
# Ignoring return code, as we'll use `GetExitCode` to determine the state of the process later.
_ = win32event.WaitForSingleObject( # Waits until the specified object is signaled, or time-out.
process_handle, # Ping process handle