Agent, Common: Refactor pydantic credentials to use SecretStr
This commit is contained in:
parent
17e3b3d205
commit
ece4d9383e
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
from pydantic import SecretStr
|
||||
|
||||
from ..base_models import InfectionMonkeyBaseModel
|
||||
|
||||
|
||||
class Password(InfectionMonkeyBaseModel):
|
||||
password: str
|
||||
password: SecretStr
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue