blob: 1fe8457bb6bc91d2ca733b9b6eb7d99bf2d6a499 [file]
import re
from base64 import b64decode
from datetime import datetime, timezone
from urllib.parse import urlparse
import marshmallow as ma
from app.objects.interfaces.i_object import FirstClassObjectInterface
from app.objects.secondclass.c_link import Link, LinkSchema
from app.objects.secondclass.c_fact import OriginType
from app.utility.base_object import BaseObject
from app.utility.base_planning_svc import BasePlanningService
from app.utility.base_service import BaseService
class AgentFieldsSchema(ma.Schema):
paw = ma.fields.String(allow_none=True)
sleep_min = ma.fields.Integer()
sleep_max = ma.fields.Integer()
watchdog = ma.fields.Integer()
group = ma.fields.String()
architecture = ma.fields.String()
platform = ma.fields.String()
server = ma.fields.String()
upstream_dest = ma.fields.String(allow_none=True)
username = ma.fields.String()
location = ma.fields.String()
pid = ma.fields.Integer()
ppid = ma.fields.Integer()
trusted = ma.fields.Boolean()
executors = ma.fields.List(ma.fields.String())
privilege = ma.fields.String()
exe_name = ma.fields.String()
host = ma.fields.String()
contact = ma.fields.String()
proxy_receivers = ma.fields.Dict(keys=ma.fields.String(), values=ma.fields.List(ma.fields.String()),
allow_none=True)
proxy_chain = ma.fields.List(ma.fields.List(ma.fields.String()), allow_none=True)
origin_link_id = ma.fields.String()
deadman_enabled = ma.fields.Boolean(allow_none=True)
available_contacts = ma.fields.List(ma.fields.String(), allow_none=True)
host_ip_addrs = ma.fields.List(ma.fields.String(), allow_none=True)
display_name = ma.fields.String(dump_only=True)
created = ma.fields.DateTime(format=BaseObject.TIME_FORMAT, dump_only=True)
last_seen = ma.fields.DateTime(format=BaseObject.TIME_FORMAT, dump_only=True)
links = ma.fields.List(ma.fields.Nested(LinkSchema), dump_only=True)
pending_contact = ma.fields.String()
status = ma.fields.String(dump_only=True)
@ma.pre_load
def remove_nulls(self, in_data, **_):
return {k: v for k, v in in_data.items() if v is not None}
@ma.pre_load
def remove_properties(self, data, **_):
data.pop('display_name', None)
data.pop('created', None)
data.pop('last_seen', None)
data.pop('links', None)
data.pop('status', None)
return data
class AgentSchema(AgentFieldsSchema):
@ma.post_load
def build_agent(self, data, **kwargs):
return None if kwargs.get('partial') is True else Agent(**data)
class Agent(FirstClassObjectInterface, BaseObject):
schema = AgentSchema()
load_schema = AgentSchema(partial=['paw', 'origin_link_id'])
RESERVED = dict(server='#{server}', group='#{group}', agent_paw='#{paw}', location='#{location}',
exe_name='#{exe_name}', upstream_dest='#{upstream_dest}',
payload=re.compile('#{payload:(.*?)}', flags=re.DOTALL))
@property
def unique(self):
return self.hash(self.paw)
@property
def display_name(self):
return '{}${}'.format(self.host, self.username)
@property
def status(self):
now = datetime.now(timezone.utc)
untrusted_buffer = int(self.get_config(name='agents', prop='untrusted_timer'))
time_diff = (now - self.last_seen).total_seconds()
expired = time_diff > int(self.sleep_max) + untrusted_buffer
if self._marked_for_stop:
# If agent hasn't received the stop instruction yet in a beacon response, it's still pending stop
# Otherwise, if agent has received the stop instruction or takes too long to beacon back, mark as dead
return 'dead' if self._stop_delivered or expired else 'pending kill'
else:
# If agent hasn't beaconed in since max beacon time + untrusted timer, mark as dead
return 'dead' if expired else 'alive'
@classmethod
def is_global_variable(cls, variable):
if variable.startswith('payload:'):
return True
if variable == 'payload':
return False
if variable in cls.RESERVED:
return True
return False
def __init__(self, sleep_min=30, sleep_max=60, watchdog=0, platform='unknown', server='unknown', host='unknown',
username='unknown', architecture='unknown', group='red', location='unknown', pid=0, ppid=0,
trusted=True, executors=(), privilege='User', exe_name='unknown', contact='unknown', paw=None,
proxy_receivers=None, proxy_chain=None, origin_link_id='', deadman_enabled=False,
available_contacts=None, host_ip_addrs=None, upstream_dest=None, pending_contact=None):
super().__init__()
self.paw = paw if paw else self.generate_name(size=6)
self.host = host
self.username = username
self.group = group
self.architecture = architecture
self.platform = platform.lower()
url = urlparse(server)
self.server = '%s://%s:%s' % (url.scheme, url.hostname, url.port)
self.location = location
self.pid = pid
self.ppid = ppid
self.trusted = trusted
self.created = datetime.now(timezone.utc)
self.last_seen = self.created
self.last_trusted_seen = self.created
self.executors = executors
self.privilege = privilege
self.exe_name = exe_name
self.sleep_min = int(sleep_min)
self.sleep_max = int(sleep_max)
self.watchdog = int(watchdog)
self.contact = contact
self.links = []
self.access = self.Access.BLUE if group == 'blue' else self.Access.RED
self.proxy_receivers = proxy_receivers if proxy_receivers else dict()
self.proxy_chain = proxy_chain if proxy_chain else []
self.origin_link_id = origin_link_id
self.deadman_enabled = deadman_enabled
self.available_contacts = available_contacts if available_contacts else [self.contact]
self.pending_contact = pending_contact if pending_contact else contact
self.host_ip_addrs = host_ip_addrs if host_ip_addrs else []
if upstream_dest:
upstream_url = urlparse(upstream_dest)
self.upstream_dest = '%s://%s:%s' % (upstream_url.scheme, upstream_url.hostname, upstream_url.port)
else:
self.upstream_dest = self.server
self._executor_change_to_assign = None
self.log = self.create_logger('agent')
self._marked_for_stop = False
self._stop_delivered = False
def store(self, ram):
existing = self.retrieve(ram['agents'], self.unique)
if not existing:
ram['agents'].append(self)
return self.retrieve(ram['agents'], self.unique)
existing.update('group', self.group)
existing.update('trusted', self.trusted)
existing.update('sleep_min', self.sleep_min)
existing.update('sleep_max', self.sleep_max)
existing.update('watchdog', self.watchdog)
existing.update('pending_contact', self.pending_contact)
return existing
async def calculate_sleep(self):
return self.jitter('%d/%d' % (self.sleep_min, self.sleep_max))
async def capabilities(self, abilities):
"""Get abilities that the agent is capable of running
:param abilities: List of abilities to check agent capability
:type abilities: List[Ability]
:return: List of abilities the agents is capable of running
:rtype: List[Ability]
"""
capabilities = []
for ability in abilities:
if self.privileged_to_run(ability) and ability.find_executors(self.executors, self.platform):
capabilities.append(ability)
return capabilities
async def get_preferred_executor(self, ability):
"""Get preferred executor for ability
Will return None if the agent is not capable of running any
executors in the given ability.
:param ability: Ability to get preferred executor for
:type ability: Ability
:return: Preferred executor or None
:rtype: Union[Executor, None]
"""
potential_executors = ability.find_executors(self.executors, self.platform)
if not potential_executors:
return None
preferred_executor_name = self._get_preferred_executor_name()
for executor in potential_executors:
if executor.name == preferred_executor_name:
return executor
return potential_executors[0]
async def heartbeat_modification(self, **kwargs):
now = datetime.now(timezone.utc)
self.last_seen = now
if self.trusted:
self.last_trusted_seen = now
self.update('pid', kwargs.get('pid'))
self.update('ppid', kwargs.get('ppid'))
self.update('server', kwargs.get('server'))
self.update('exe_name', kwargs.get('exe_name'))
self.update('location', kwargs.get('location'))
self.update('privilege', kwargs.get('privilege'))
self.update('host', kwargs.get('host'))
self.update('username', kwargs.get('username'))
self.update('architecture', kwargs.get('architecture'))
self.update('platform', kwargs.get('platform'))
self.update('proxy_receivers', kwargs.get('proxy_receivers'))
self.update('proxy_chain', kwargs.get('proxy_chain'))
self.update('deadman_enabled', kwargs.get('deadman_enabled'))
self.update('contact', kwargs.get('contact'))
self.update('host_ip_addrs', kwargs.get('host_ip_addrs'))
self.update('upstream_dest', kwargs.get('upstream_dest'))
if not self._executor_change_to_assign:
# Don't update executors if we're waiting to assign an executor change to the agent.
self.update('executors', kwargs.get('executors'))
# Check if agent has been marked to stop
if self._marked_for_stop and not self._stop_delivered:
self._stop_delivered = True
async def gui_modification(self, **kwargs):
loaded = AgentFieldsSchema(only=('group', 'trusted', 'sleep_min', 'sleep_max', 'watchdog', 'pending_contact')).load(kwargs)
for k, v in loaded.items():
self.update(k, v)
async def kill(self):
self.update('watchdog', 1)
self.update('sleep_min', 3)
self.update('sleep_max', 3)
self._marked_for_stop = True
self._stop_delivered = False
def replace(self, encoded_cmd, file_svc):
decoded_cmd = b64decode(encoded_cmd).decode('utf-8', errors='ignore').replace('\n', '')
decoded_cmd = decoded_cmd.replace(self.RESERVED['server'], self.server)
decoded_cmd = decoded_cmd.replace(self.RESERVED['group'], self.group)
decoded_cmd = decoded_cmd.replace(self.RESERVED['agent_paw'], self.paw)
decoded_cmd = decoded_cmd.replace(self.RESERVED['location'], self.location)
decoded_cmd = decoded_cmd.replace(self.RESERVED['exe_name'], self.exe_name)
decoded_cmd = decoded_cmd.replace(self.RESERVED['upstream_dest'], self.upstream_dest)
decoded_cmd = self._replace_payload_data(decoded_cmd, file_svc)
return decoded_cmd
def privileged_to_run(self, ability):
if not ability.privilege or self.Privileges[self.privilege].value >= self.Privileges[ability.privilege].value:
return True
return False
async def bootstrap(self, data_svc):
abilities = []
for i in self.get_config(name='agents', prop='bootstrap_abilities'):
for a in await data_svc.locate('abilities', match=dict(ability_id=i)):
abilities.append(a)
await self.task(abilities, obfuscator='plain-text')
async def deadman(self, data_svc):
abilities = []
deadman_abilities = self.get_config(name='agents', prop='deadman_abilities')
if deadman_abilities:
for i in deadman_abilities:
for a in await data_svc.locate('abilities', match=dict(ability_id=i)):
abilities.append(a)
await self.task(abilities, obfuscator='plain-text', deadman=True)
async def task(self, abilities, obfuscator, facts=(), deadman=False):
if not self.executors:
return []
bps = BasePlanningService()
preferred_executor_name = self._get_preferred_executor_name()
links = []
for ability in await self.capabilities(abilities):
executors = ability.find_executors(self.executors, self.platform)
executors = sorted(executors, key=lambda ex: ex.name == preferred_executor_name, reverse=True)
for executor in executors:
ex_links = [Link.load(dict(command=self.encode_string(executor.test), paw=self.paw, ability=ability,
executor=executor, deadman=deadman))]
valid_links = await bps.add_test_variants(links=ex_links, agent=self, facts=facts, trim_unset_variables=True)
if valid_links:
links.extend(valid_links)
break
links = await bps.obfuscate_commands(self, obfuscator, links)
knowledge_svc_handle = BaseService.get_service('knowledge_svc')
for fact in facts:
fact.source = self.paw
fact.origin_type = OriginType.SEEDED
await knowledge_svc_handle.add_fact(fact)
self.links.extend(links)
return links
async def all_facts(self):
knowledge_svc_handle = BaseService.get_service('knowledge_svc')
return await knowledge_svc_handle.get_facts(dict(source=self.paw))
@property
def executor_change_to_assign(self):
return self._executor_change_to_assign
def set_pending_executor_removal(self, executor_name):
"""Mark specified executor to remove.
:param executor_name: name of executor for agent to remove
:type executor_name: str
"""
if executor_name and isinstance(executor_name, str):
if executor_name in self.executors:
# Remove the executor server-side so planners can generate appropriate links immediately.
self.executors.remove(executor_name)
self._executor_change_to_assign = dict(action='remove', executor=executor_name)
else:
self.log.error('Paw %s: Invalid executor name. Please provide non-empty string. Provided value: %s',
self.paw, executor_name)
def set_pending_executor_path_update(self, executor_name, new_binary_path):
"""Mark specified executor to update its binary path to the new path.
:param executor_name: name of executor for agent to update binary path
:type executor_name: str
:param new_binary_path: new binary path for executor to reference
:type new_binary_path: str
"""
if executor_name and new_binary_path and isinstance(executor_name, str) and isinstance(new_binary_path, str):
if executor_name in self.executors:
self._executor_change_to_assign = dict(action='update_path', executor=executor_name,
value=new_binary_path)
else:
self.log.error('Paw %s: Invalid format for executor name or new binary path. '
'Please provide non-empty strings. Provided values: %s, %s',
self.paw, executor_name, new_binary_path)
def assign_pending_executor_change(self):
"""Return the executor change dict and remove pending change to assign.
:return: Dict representing the executor change that is assigned.
:rtype: dict(str, str)
"""
executor_change = self.executor_change_to_assign
self._executor_change_to_assign = None
return executor_change
def _replace_payload_data(self, decoded_cmd, file_svc):
for uuid in re.findall(self.RESERVED['payload'], decoded_cmd):
if self.is_uuid4(uuid):
_, display_name = file_svc.get_payload_name_from_uuid(uuid)
decoded_cmd = decoded_cmd.replace('#{payload:%s}' % uuid, display_name)
return decoded_cmd
def _get_preferred_executor_name(self):
if 'psh' in self.executors:
return 'psh'
elif 'sh' in self.executors:
return 'sh'
return self.executors[0]