diff --git a/monkey/common/credentials/credentials.py b/monkey/common/credentials/credentials.py index 053d7f33d..ae8de63d1 100644 --- a/monkey/common/credentials/credentials.py +++ b/monkey/common/credentials/credentials.py @@ -2,6 +2,8 @@ from __future__ import annotations from typing import Optional, Union +from pydantic import SecretBytes, SecretStr + from ..base_models import InfectionMonkeyBaseModel from . import LMHash, NTHash, Password, SSHKeypair, Username @@ -9,6 +11,25 @@ Secret = Union[Password, LMHash, NTHash, SSHKeypair] Identity = Username +def get_plain_text(secret: Union[SecretStr, SecretBytes, None, str]) -> Optional[str]: + if secret: + return secret.get_secret_value() + else: + return secret + + class Credentials(InfectionMonkeyBaseModel): + """Represents a credential pair (some form of identity and a secret)""" + identity: Optional[Identity] + """Identity part of credentials, like a username or an email""" + secret: Optional[Secret] + """Secret part of credentials, like a password or a hash""" + + class Config: + json_encoders = { + # This makes secrets dumpable to json, but not loggable + SecretStr: lambda v: v.get_secret_value() if v else None, + SecretBytes: lambda v: v.get_secret_value() if v else None, + } diff --git a/monkey/common/credentials/lm_hash.py b/monkey/common/credentials/lm_hash.py index 9dd201536..2bace0c84 100644 --- a/monkey/common/credentials/lm_hash.py +++ b/monkey/common/credentials/lm_hash.py @@ -1,16 +1,16 @@ import re -from pydantic import validator -from pydantic.main import BaseModel +from pydantic import SecretStr, validator +from ..base_models import InfectionMonkeyBaseModel from .validators import ntlm_hash_regex -class LMHash(BaseModel): - lm_hash: str +class LMHash(InfectionMonkeyBaseModel): + lm_hash: SecretStr @validator("lm_hash") - def validate_hash_format(cls, nt_hash): - if not re.match(ntlm_hash_regex, nt_hash): - raise ValueError(f"Invalid LM hash provided: {nt_hash}") - return nt_hash + def validate_hash_format(cls, lm_hash): + if not re.match(ntlm_hash_regex, lm_hash.get_secret_value()): + raise ValueError("Invalid LM hash provided") + return lm_hash diff --git a/monkey/common/credentials/nt_hash.py b/monkey/common/credentials/nt_hash.py index 5a5dacd97..5c901fa9b 100644 --- a/monkey/common/credentials/nt_hash.py +++ b/monkey/common/credentials/nt_hash.py @@ -1,16 +1,16 @@ import re -from pydantic import validator +from pydantic import SecretStr, validator from ..base_models import InfectionMonkeyBaseModel from .validators import ntlm_hash_regex class NTHash(InfectionMonkeyBaseModel): - nt_hash: str + nt_hash: SecretStr @validator("nt_hash") def validate_hash_format(cls, nt_hash): - if not re.match(ntlm_hash_regex, nt_hash): - raise ValueError(f"Invalid NT hash provided: {nt_hash}") + if not re.match(ntlm_hash_regex, nt_hash.get_secret_value()): + raise ValueError("Invalid NT hash provided") return nt_hash diff --git a/monkey/common/credentials/password.py b/monkey/common/credentials/password.py index a9c1f2016..1745de170 100644 --- a/monkey/common/credentials/password.py +++ b/monkey/common/credentials/password.py @@ -1,5 +1,7 @@ +from pydantic import SecretStr + from ..base_models import InfectionMonkeyBaseModel class Password(InfectionMonkeyBaseModel): - password: str + password: SecretStr diff --git a/monkey/common/credentials/ssh_keypair.py b/monkey/common/credentials/ssh_keypair.py index 49d886b8b..651716965 100644 --- a/monkey/common/credentials/ssh_keypair.py +++ b/monkey/common/credentials/ssh_keypair.py @@ -1,6 +1,8 @@ +from pydantic import SecretStr + from ..base_models import InfectionMonkeyBaseModel class SSHKeypair(InfectionMonkeyBaseModel): - private_key: str + private_key: SecretStr public_key: str diff --git a/monkey/infection_monkey/exploit/mssqlexec.py b/monkey/infection_monkey/exploit/mssqlexec.py index 6c155f468..b1d700945 100644 --- a/monkey/infection_monkey/exploit/mssqlexec.py +++ b/monkey/infection_monkey/exploit/mssqlexec.py @@ -6,6 +6,7 @@ from typing import Sequence, Tuple import pymssql from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT +from common.credentials.credentials import get_plain_text from common.utils.exceptions import FailedExploitationError from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_agent_dst_path @@ -111,7 +112,7 @@ class MSSQLExploiter(HostExploiter): conn = pymssql.connect( host, user, - password, + get_plain_text(password), port=port, login_timeout=self.LOGIN_TIMEOUT, timeout=self.QUERY_TIMEOUT, diff --git a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py index df2cf65b1..8113026e7 100644 --- a/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py +++ b/monkey/infection_monkey/exploit/powershell_utils/powershell_client.py @@ -11,6 +11,7 @@ from pypsrp.powershell import PowerShell, RunspacePool from typing_extensions import Protocol from urllib3 import connectionpool +from common.credentials.credentials import get_plain_text from infection_monkey.exploit.powershell_utils.auth_options import AuthOptions from infection_monkey.exploit.powershell_utils.credentials import Credentials, SecretType @@ -42,14 +43,16 @@ def format_password(credentials: Credentials) -> Optional[str]: if credentials.secret_type == SecretType.CACHED: return None + plaintext_secret = get_plain_text(credentials.secret) + if credentials.secret_type == SecretType.PASSWORD: - return credentials.secret + return plaintext_secret if credentials.secret_type == SecretType.LM_HASH: - return f"{credentials.secret}:00000000000000000000000000000000" + return f"{plaintext_secret}:00000000000000000000000000000000" if credentials.secret_type == SecretType.NT_HASH: - return f"00000000000000000000000000000000:{credentials.secret}" + return f"00000000000000000000000000000000:{plaintext_secret}" raise ValueError(f"Unknown secret type {credentials.secret_type}") diff --git a/monkey/infection_monkey/exploit/smbexec.py b/monkey/infection_monkey/exploit/smbexec.py index 80c09547f..778a4fb36 100644 --- a/monkey/infection_monkey/exploit/smbexec.py +++ b/monkey/infection_monkey/exploit/smbexec.py @@ -4,6 +4,7 @@ from impacket.dcerpc.v5 import scmr, transport from impacket.dcerpc.v5.scmr import DCERPCSessionError from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT +from common.credentials.credentials import get_plain_text from common.utils.attack_utils import ScanStatus, UsageEnum from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_agent_dst_path @@ -107,7 +108,14 @@ class SMBExploiter(HostExploiter): rpctransport.setRemoteHost(self.host.ip_addr) if hasattr(rpctransport, "set_credentials"): # This method exists only for selected protocol sequences. - rpctransport.set_credentials(user, password, "", lm_hash, ntlm_hash, None) + rpctransport.set_credentials( + user, + get_plain_text(password), + "", + get_plain_text(lm_hash), + get_plain_text(ntlm_hash), + None, + ) rpctransport.set_kerberos(SMBExploiter.USE_KERBEROS) scmr_rpc = rpctransport.get_dce_rpc() @@ -116,7 +124,8 @@ class SMBExploiter(HostExploiter): scmr_rpc.connect() except Exception as exc: logger.debug( - f"Can't connect to SCM on exploited machine {self.host}, port {port} : {exc}" + f"Can't connect to SCM on exploited machine {self.host}, port {port} : " + f"{exc}" ) continue diff --git a/monkey/infection_monkey/exploit/sshexec.py b/monkey/infection_monkey/exploit/sshexec.py index 8b10c8c44..ab96794a2 100644 --- a/monkey/infection_monkey/exploit/sshexec.py +++ b/monkey/infection_monkey/exploit/sshexec.py @@ -5,6 +5,7 @@ from pathlib import PurePath import paramiko from common.common_consts.timeouts import LONG_REQUEST_TIMEOUT, MEDIUM_REQUEST_TIMEOUT +from common.credentials.credentials import get_plain_text from common.utils import Timer from common.utils.attack_utils import ScanStatus from common.utils.exceptions import FailedExploitationError @@ -59,7 +60,7 @@ class SSHExploiter(HostExploiter): for user, ssh_key_pair in ssh_key_pairs_iterator: # Creating file-like private key for paramiko - pkey = io.StringIO(ssh_key_pair["private_key"]) + pkey = io.StringIO(get_plain_text(ssh_key_pair["private_key"])) ssh_string = "%s@%s" % (user, self.host.ip_addr) ssh = paramiko.SSHClient() diff --git a/monkey/infection_monkey/exploit/tools/smb_tools.py b/monkey/infection_monkey/exploit/tools/smb_tools.py index d90997b24..d143da84e 100644 --- a/monkey/infection_monkey/exploit/tools/smb_tools.py +++ b/monkey/infection_monkey/exploit/tools/smb_tools.py @@ -8,7 +8,9 @@ from typing import Optional from impacket.dcerpc.v5 import srvs, transport from impacket.smb3structs import SMB2_DIALECT_002, SMB2_DIALECT_21 from impacket.smbconnection import SMB_DIALECT, SMBConnection +from pydantic import SecretStr +from common.credentials.credentials import get_plain_text from common.utils.attack_utils import ScanStatus from infection_monkey.network.tools import get_interface_to_target from infection_monkey.telemetry.attack.t1105_telem import T1105Telem @@ -26,8 +28,8 @@ class SmbTools(object): host, agent_file: BytesIO, dst_path: PurePath, - username, - password, + username: str, + password: SecretStr, lm_hash="", ntlm_hash="", timeout=30, @@ -190,7 +192,9 @@ class SmbTools(object): return remote_full_path @staticmethod - def new_smb_connection(host, username, password, lm_hash="", ntlm_hash="", timeout=30): + def new_smb_connection( + host, username: str, password: SecretStr, lm_hash="", ntlm_hash="", timeout=30 + ): try: smb = SMBConnection(host.ip_addr, host.ip_addr, sess_port=445) except Exception as exc: @@ -212,7 +216,13 @@ class SmbTools(object): # we know this should work because the WMI connection worked try: - smb.login(username, password, "", lm_hash, ntlm_hash) + smb.login( + username, + get_plain_text(password), + "", + get_plain_text(lm_hash), + get_plain_text(ntlm_hash), + ) except Exception as exc: logger.error(f'Error while logging into {host} using user "{username}": {exc}') return None, dialect diff --git a/monkey/infection_monkey/exploit/wmiexec.py b/monkey/infection_monkey/exploit/wmiexec.py index cdbb3b63f..e79d18d25 100644 --- a/monkey/infection_monkey/exploit/wmiexec.py +++ b/monkey/infection_monkey/exploit/wmiexec.py @@ -5,6 +5,7 @@ import traceback from impacket.dcerpc.v5.rpcrt import DCERPCException +from common.credentials.credentials import get_plain_text from infection_monkey.exploit.HostExploiter import HostExploiter from infection_monkey.exploit.tools.helpers import get_agent_dst_path from infection_monkey.exploit.tools.smb_tools import SmbTools @@ -44,7 +45,14 @@ class WmiExploiter(HostExploiter): wmi_connection = WmiTools.WmiConnection() try: - wmi_connection.connect(self.host, user, password, None, lm_hash, ntlm_hash) + wmi_connection.connect( + self.host, + user, + get_plain_text(password), + None, + get_plain_text(lm_hash), + get_plain_text(ntlm_hash), + ) except AccessDeniedException: self.report_login_attempt(False, user, password, lm_hash, ntlm_hash) logger.debug(f"Failed connecting to {self.host} using WMI") diff --git a/monkey/infection_monkey/exploit/zerologon.py b/monkey/infection_monkey/exploit/zerologon.py index f43a61069..de19956c8 100644 --- a/monkey/infection_monkey/exploit/zerologon.py +++ b/monkey/infection_monkey/exploit/zerologon.py @@ -299,8 +299,8 @@ class ZerologonExploiter(HostExploiter): self, user: str, lmhash: str, nthash: str ) -> None: extracted_credentials = [ - Credentials(Username(user), LMHash(lmhash)), - Credentials(Username(user), NTHash(nthash)), + Credentials(identity=Username(username=user), secret=LMHash(lm_hash=lmhash)), + Credentials(identity=Username(username=user), secret=NTHash(nt_hash=nthash)), ] self.telemetry_messenger.send_telemetry(CredentialsTelem(extracted_credentials)) diff --git a/monkey/tests/data_for_tests/propagation_credentials.py b/monkey/tests/data_for_tests/propagation_credentials.py index a1d783f78..a23334e14 100644 --- a/monkey/tests/data_for_tests/propagation_credentials.py +++ b/monkey/tests/data_for_tests/propagation_credentials.py @@ -1,16 +1,18 @@ from itertools import product +from pydantic import SecretStr + from common.credentials import Credentials, LMHash, NTHash, Password, SSHKeypair, Username USERNAME = "m0nk3y_user" SPECIAL_USERNAME = "m0nk3y.user" -NT_HASH = "C1C58F96CDF212B50837BC11A00BE47C" -LM_HASH = "299BD128C1101FD6299BD128C1101FD6" -PASSWORD_1 = "trytostealthis" -PASSWORD_2 = "password!" -PASSWORD_3 = "rubberbabybuggybumpers" +NT_HASH = SecretStr("C1C58F96CDF212B50837BC11A00BE47C") +LM_HASH = SecretStr("299BD128C1101FD6299BD128C1101FD6") +PASSWORD_1 = SecretStr("trytostealthis") +PASSWORD_2 = SecretStr("password!") +PASSWORD_3 = SecretStr("rubberbabybuggybumpers") PUBLIC_KEY = "MY_PUBLIC_KEY" -PRIVATE_KEY = "MY_PRIVATE_KEY" +PRIVATE_KEY = SecretStr("MY_PRIVATE_KEY") IDENTITIES = [Username(username=USERNAME), None, Username(username=SPECIAL_USERNAME)] IDENTITY_DICTS = [{"username": USERNAME}, None] diff --git a/monkey/tests/unit_tests/common/credentials/test_credentials.py b/monkey/tests/unit_tests/common/credentials/test_credentials.py index 89238b839..30f27bbdb 100644 --- a/monkey/tests/unit_tests/common/credentials/test_credentials.py +++ b/monkey/tests/unit_tests/common/credentials/test_credentials.py @@ -1,6 +1,11 @@ +import logging + import pytest +from pydantic import SecretBytes +from pydantic.types import SecretStr from tests.data_for_tests.propagation_credentials import CREDENTIALS, CREDENTIALS_DICTS +from common.base_models import InfectionMonkeyBaseModel from common.credentials import Credentials @@ -12,3 +17,28 @@ def test_credentials_serialization_json(credentials, expected_credentials_dict): deserialized_credentials = Credentials.parse_raw(serialized_credentials) assert credentials == deserialized_credentials + + +logger = logging.getLogger() +logger.level = logging.DEBUG + + +def test_credentials_secrets_not_logged(caplog): + class TestSecret(InfectionMonkeyBaseModel): + some_secret: SecretStr + some_secret_in_bytes: SecretBytes + + class TestCredentials(Credentials): + secret: TestSecret + + sensitive = "super_secret" + creds = TestCredentials( + identity=None, + secret=TestSecret(some_secret=sensitive, some_secret_in_bytes=sensitive.encode()), + ) + + logging.getLogger().info( + f"{creds.secret.some_secret} and" f" {creds.secret.some_secret_in_bytes}" + ) + + assert sensitive not in caplog.text diff --git a/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py b/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py index 5ff9547a0..c6ae98f11 100644 --- a/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py +++ b/monkey/tests/unit_tests/infection_monkey/credential_store/test_aggregating_propagation_credentials_repository.py @@ -1,6 +1,7 @@ from unittest.mock import MagicMock import pytest +from pydantic import SecretStr from tests.data_for_tests.propagation_credentials import ( CREDENTIALS, LM_HASH, @@ -30,14 +31,14 @@ EMPTY_CHANNEL_CREDENTIALS = [] STOLEN_USERNAME_1 = "user1" STOLEN_USERNAME_2 = "user2" STOLEN_USERNAME_3 = "user3" -STOLEN_PASSWORD_1 = "abcdefg" -STOLEN_PASSWORD_2 = "super_secret" +STOLEN_PASSWORD_1 = SecretStr("abcdefg") +STOLEN_PASSWORD_2 = SecretStr("super_secret") STOLEN_PUBLIC_KEY_1 = "some_public_key_1" STOLEN_PUBLIC_KEY_2 = "some_public_key_2" -STOLEN_LM_HASH = "AAD3B435B51404EEAAD3B435B51404EE" -STOLEN_NT_HASH = "C0172DFF622FE29B5327CB79DC12D24C" -STOLEN_PRIVATE_KEY_1 = "some_private_key_1" -STOLEN_PRIVATE_KEY_2 = "some_private_key_2" +STOLEN_LM_HASH = SecretStr("AAD3B435B51404EEAAD3B435B51404EE") +STOLEN_NT_HASH = SecretStr("C0172DFF622FE29B5327CB79DC12D24C") +STOLEN_PRIVATE_KEY_1 = SecretStr("some_private_key_1") +STOLEN_PRIVATE_KEY_2 = SecretStr("some_private_key_2") STOLEN_CREDENTIALS = [ Credentials( identity=Username(username=STOLEN_USERNAME_1), diff --git a/vulture_allowlist.py b/vulture_allowlist.py index e6550b8f9..caf7c3e24 100644 --- a/vulture_allowlist.py +++ b/vulture_allowlist.py @@ -7,7 +7,7 @@ from common.agent_configuration.agent_sub_configurations import ( CustomPBAConfiguration, ScanTargetConfiguration, ) -from common.credentials import LMHash, NTHash +from common.credentials import Credentials, LMHash, NTHash from infection_monkey.exploit.log4shell_utils.ldap_server import LDAPServerFactory from monkey_island.cc.event_queue import IslandEventTopic, PyPubSubIslandEventQueue from monkey_island.cc.models import Report @@ -167,6 +167,7 @@ strict_slashes # unused attribute (monkey/monkey_island/cc/app.py:96) post_breach_actions # unused variable (monkey\infection_monkey\config.py:95) LMHash.validate_hash_format NTHash.validate_hash_format +Credentials.Config.json_encoders # Deployments DEVELOP # unused variable (monkey/monkey/monkey_island/cc/deployment.py:5)