mirror of https://github.com/avast/PurpleDome
Documentation can be built now
parent
b5fdb52fee
commit
d25676032e
@ -0,0 +1,18 @@
|
||||
# Makefile for standard actions
|
||||
#
|
||||
#
|
||||
|
||||
.PHONY: test init deinit shipit pylint
|
||||
|
||||
test: tox.ini
|
||||
tox;
|
||||
coverage html;
|
||||
coverage report;
|
||||
|
||||
shipit: test
|
||||
cd doc; make html; cd ..
|
||||
python3 tools/shipit.py
|
||||
|
||||
# More detailed pylint tests.
|
||||
pylint:
|
||||
pylint --rcfile=pylint.rc *.py app/*.py plugins/base/*.py
|
@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" Logger for the attack side. Output must be flexible, because we want to be able to feed it into many different processes. From ML to analysts """
|
||||
|
||||
import json
|
||||
import datetime
|
||||
|
||||
# TODO: Collect caldera attacks: Source, target, type of attack. Start/Stop. Results. Parameters
|
||||
|
||||
# TODO: Collect kali attacks: Source, target, type of attack. Start/Stop. Results. Parameters. Settings
|
||||
|
||||
# TODO: Export data
|
||||
|
||||
# TODO: Add TTP and similar metadata
|
||||
|
||||
|
||||
def __get_timestamp__():
|
||||
return datetime.datetime.now().strftime("%H:%M:%S.%f")
|
||||
|
||||
|
||||
def __mitre_fix_ttp__(ttp):
|
||||
""" enforce some systematic naming scheme for MITRE TTPs """
|
||||
|
||||
if ttp is None:
|
||||
return ""
|
||||
|
||||
if ttp.startswith("MITRE_"):
|
||||
return ttp
|
||||
else:
|
||||
return "MITRE_" + ttp
|
||||
|
||||
|
||||
class AttackLog():
|
||||
""" A specific logger class to log the progress of the attack steps """
|
||||
|
||||
def __init__(self):
|
||||
self.log = []
|
||||
|
||||
def start_caldera_attack(self, source, paw, group, ability_id, ttp=None, name=None, description=None): # pylint: disable=too-many-arguments
|
||||
""" Mark the start of a caldera attack
|
||||
|
||||
@param source: source of the attack. Attack IP
|
||||
@param paw: Caldera oaw of the targets being attacked
|
||||
@param group: Caldera group of the targets being attacked
|
||||
@param ability_id: Caldera ability id of the attack
|
||||
@param ttp: TTP of the attack (as stated by Caldera internal settings)
|
||||
@param name: Name of the attack. Data source is Caldera internal settings
|
||||
@param description: Descirption of the attack. Caldera is the source
|
||||
"""
|
||||
|
||||
data = {"timestamp": __get_timestamp__(),
|
||||
"event": "start",
|
||||
"type": "attack",
|
||||
"sub-type": "caldera",
|
||||
"source": source,
|
||||
"target_paw": paw,
|
||||
"target_group": group,
|
||||
"ability_id": ability_id,
|
||||
"hunting_tag": __mitre_fix_ttp__(ttp),
|
||||
"name": name or "",
|
||||
"description": description or ""
|
||||
}
|
||||
|
||||
self.log.append(data)
|
||||
|
||||
# TODO: Add parameter
|
||||
# TODO: Add config
|
||||
# TODO: Add results
|
||||
|
||||
def stop_caldera_attack(self, source, paw, group, ability_id, ttp=None, name=None, description=None): # pylint: disable=too-many-arguments
|
||||
""" Mark the end of a caldera attack
|
||||
|
||||
@param source: source of the attack. Attack IP
|
||||
@param paw: Caldera oaw of the targets being attacked
|
||||
@param group: Caldera group of the targets being attacked
|
||||
@param ability_id: Caldera ability id of the attack
|
||||
@param ttp: TTP of the attack (as stated by Caldera internal settings)
|
||||
@param name: Name of the attack. Data source is Caldera internal settings
|
||||
@param description: Descirption of the attack. Caldera is the source
|
||||
"""
|
||||
|
||||
data = {"timestamp": __get_timestamp__(),
|
||||
"event": "stop",
|
||||
"type": "attack",
|
||||
"sub-type": "caldera",
|
||||
"source": source,
|
||||
"target_paw": paw,
|
||||
"target_group": group,
|
||||
"ability_id": ability_id,
|
||||
"hunting_tag": __mitre_fix_ttp__(ttp),
|
||||
"name": name or "",
|
||||
"description": description or ""
|
||||
}
|
||||
self.log.append(data)
|
||||
|
||||
def start_kali_attack(self, source, target, attack_name, ttp=None):
|
||||
""" Mark the start of a Kali based attack
|
||||
|
||||
@param source: source of the attack. Attack IP
|
||||
@param target: Target machine of the attack
|
||||
@param attack_name: Name of the attack. From plugin
|
||||
@param ttp: TTP of the attack. From plugin
|
||||
"""
|
||||
|
||||
data = {"timestamp": __get_timestamp__(),
|
||||
"event": "start",
|
||||
"type": "attack",
|
||||
"sub-type": "kali",
|
||||
"source": source,
|
||||
"target": target,
|
||||
"kali_name": attack_name,
|
||||
"hunting_tag": __mitre_fix_ttp__(ttp),
|
||||
}
|
||||
self.log.append(data)
|
||||
|
||||
# TODO: Add parameter
|
||||
# TODO: Add config
|
||||
# TODO: Add results
|
||||
|
||||
def stop_kali_attack(self, source, target, attack_name, ttp=None):
|
||||
""" Mark the end of a Kali based attack
|
||||
|
||||
@param source: source of the attack. Attack IP
|
||||
@param target: Target machine of the attack
|
||||
@param attack_name: Name of the attack. From plugin
|
||||
@param ttp: TTP of the attack. From plugin
|
||||
"""
|
||||
|
||||
data = {"timestamp": __get_timestamp__(),
|
||||
"event": "stop",
|
||||
"type": "attack",
|
||||
"sub-type": "kali",
|
||||
"source": source,
|
||||
"target": target,
|
||||
"kali_name": attack_name,
|
||||
"hunting_tag": __mitre_fix_ttp__(ttp),
|
||||
}
|
||||
self.log.append(data)
|
||||
|
||||
def write_json(self, filename):
|
||||
""" Write the json data for this log
|
||||
|
||||
@param filename: Name of the json file
|
||||
"""
|
||||
with open(filename, "wt") as fh:
|
||||
json.dump(self.get_dict(), fh)
|
||||
|
||||
def get_dict(self):
|
||||
""" Return logged data in dict format """
|
||||
|
||||
return self.log
|
@ -0,0 +1,547 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" Remote control a caldera server """
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
import requests
|
||||
import simplejson
|
||||
|
||||
from app.exceptions import CalderaError
|
||||
from app.interface_sfx import CommandlineColors
|
||||
from app.attack_log import AttackLog
|
||||
|
||||
|
||||
# TODO: Ability deserves an own class.
|
||||
|
||||
|
||||
class CalderaControl():
|
||||
""" Remote control Caldera through REST api """
|
||||
|
||||
def __init__(self, server, config=None, apikey=None):
|
||||
"""
|
||||
|
||||
|
||||
@param server: Caldera server url/ip
|
||||
@param config: The configuration
|
||||
"""
|
||||
# print(server)
|
||||
self.url = server if server.endswith("/") else server + "/"
|
||||
|
||||
self.config = config
|
||||
|
||||
if self.config:
|
||||
self.apikey = self.config.caldera_apikey()
|
||||
else:
|
||||
self.apikey = apikey
|
||||
|
||||
def fetch_client(self, platform="windows", file="sandcat.go", target_dir=".", extension=""):
|
||||
""" Downloads the appropriate Caldera client
|
||||
|
||||
@param platform: Platform to download the agent for
|
||||
@param file: file to download from caldera. This defines the agent type
|
||||
@param target_dir: directory to drop the new file into
|
||||
@param extension: File extension to add to the downloaded file
|
||||
"""
|
||||
header = {"platform": platform,
|
||||
"file": file}
|
||||
fullurl = self.url + "file/download"
|
||||
request = requests.get(fullurl, headers=header)
|
||||
filename = request.headers["FILENAME"] + extension
|
||||
open(os.path.join(target_dir, filename), "wb").write(request.content)
|
||||
# print(r.headers)
|
||||
return filename
|
||||
|
||||
def __contact_server__(self, payload, rest_path="api/rest", method="post"):
|
||||
"""
|
||||
|
||||
@param payload: payload as dict to send to the server
|
||||
@param rest_path: specific path for this rest api
|
||||
@param method: http method to use
|
||||
"""
|
||||
url = self.url + rest_path
|
||||
header = {"KEY": self.apikey,
|
||||
"Content-Type": "application/json"}
|
||||
if method.lower() == "post":
|
||||
request = requests.post(url, headers=header, data=json.dumps(payload))
|
||||
elif method.lower() == "put":
|
||||
request = requests.put(url, headers=header, data=json.dumps(payload))
|
||||
elif method.lower() == "get":
|
||||
request = requests.get(url, headers=header, data=json.dumps(payload))
|
||||
elif method.lower() == "delete":
|
||||
request = requests.delete(url, headers=header, data=json.dumps(payload))
|
||||
else:
|
||||
raise ValueError
|
||||
try:
|
||||
res = request.json()
|
||||
except simplejson.errors.JSONDecodeError as exception:
|
||||
print("!!! Error !!!!")
|
||||
print(payload)
|
||||
print(request.text)
|
||||
print("!!! Error !!!!")
|
||||
raise exception
|
||||
|
||||
return res
|
||||
|
||||
# ############## List
|
||||
def list_links(self, opid):
|
||||
""" List links associated with an operation
|
||||
|
||||
@param opid: operation id to list links for
|
||||
"""
|
||||
|
||||
payload = {"index": "link",
|
||||
"op_id": opid}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
def list_results(self, linkid):
|
||||
""" List results for a link
|
||||
|
||||
@param linkid: ID of the link
|
||||
"""
|
||||
|
||||
payload = {"index": "result",
|
||||
"link_id": linkid}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
def list_operations(self):
|
||||
""" Return operations """
|
||||
|
||||
payload = {"index": "operations"}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
def list_abilities(self):
|
||||
""" Return all ablilities """
|
||||
# curl -H 'KEY: ADMIN123' http://192.168.178.102:8888/api/rest -H 'Content-Type: application/json' -d '{"index":"abilities"}'
|
||||
|
||||
payload = {"index": "abilities"}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
def list_agents(self):
|
||||
""" List running agents
|
||||
|
||||
"""
|
||||
# TODO: Add filters for specific platforms/executors : , platform_filter=None, executor_filter=None as parameters
|
||||
# curl -H 'KEY: ADMIN123' http://192.168.178.102:8888/api/rest -H 'Content-Type: application/json' -d '{"index":"agents"}'
|
||||
payload = {"index": "agents"}
|
||||
|
||||
agents = self.__contact_server__(payload)
|
||||
return agents
|
||||
|
||||
def list_adversaries(self):
|
||||
""" List registered adversaries """
|
||||
# curl -H 'KEY: ADMIN123' http://192.168.178.102:8888/api/rest -H 'Content-Type: application/json' -d '{"index":"adversaries"}'
|
||||
payload = {"index": "adversaries"}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
def list_objectives(self):
|
||||
""" List registered objectives """
|
||||
# curl -H 'KEY: ADMIN123' http://192.168.178.102:8888/api/rest -H 'Content-Type: application/json' -d '{"index":"objectives"}'
|
||||
payload = {"index": "objectives"}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
# ######### Get one specific item
|
||||
|
||||
def get_operation(self, name):
|
||||
""" Gets an operation by name
|
||||
|
||||
@param name: Name of the operation to look for
|
||||
"""
|
||||
|
||||
for operation in self.list_operations():
|
||||
if operation["name"] == name:
|
||||
return operation
|
||||
return None
|
||||
|
||||
def get_adversary(self, name):
|
||||
""" Gets a specific adversary by name
|
||||
|
||||
@param name: Name to look for
|
||||
"""
|
||||
for adversary in self.list_adversaries():
|
||||
if adversary["name"] == name:
|
||||
return adversary
|
||||
return None
|
||||
|
||||
def get_objective(self, name):
|
||||
""" Returns an objective with a given name
|
||||
|
||||
@param name: Name to filter for
|
||||
"""
|
||||
for objective in self.list_objectives():
|
||||
if objective["name"] == name:
|
||||
return objective
|
||||
return None
|
||||
|
||||
# ######### Get by id
|
||||
|
||||
def get_ability(self, abid):
|
||||
"""" Return an ability by id
|
||||
|
||||
@param abid: Ability id
|
||||
"""
|
||||
|
||||
res = []
|
||||
|
||||
for ability in self.list_abilities():
|
||||
if ability["ability_id"] == abid:
|
||||
res.append(ability)
|
||||
return res
|
||||
|
||||
def get_operation_by_id(self, op_id):
|
||||
""" Get operation by id
|
||||
|
||||
@param op_id: Operation id
|
||||
"""
|
||||
payload = {"index": "operations",
|
||||
"id": op_id}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
def get_result_by_id(self, linkid):
|
||||
""" Get the result from a link id
|
||||
|
||||
@param linkid: link id
|
||||
"""
|
||||
payload = {"index": "result",
|
||||
"link_id": linkid}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
def get_linkid(self, op_id, paw, ability_id):
|
||||
""" Get the id of a link identified by paw and ability_id
|
||||
|
||||
@param op_id: Operation id
|
||||
@param paw: Paw of the agent
|
||||
@param ability_id: Ability id to filter for
|
||||
"""
|
||||
operation = self.get_operation_by_id(op_id)
|
||||
|
||||
# print("Check for: {} {}".format(paw, ability_id))
|
||||
for alink in operation[0]["chain"]:
|
||||
# print("Lookup: PAW: {} Ability: {}".format(alink["paw"], alink["ability"]["ability_id"]))
|
||||
# print("In: " + str(alink))
|
||||
if alink["paw"] == paw and alink["ability"]["ability_id"] == ability_id:
|
||||
return alink["id"]
|
||||
|
||||
return None
|
||||
|
||||
# ######### View
|
||||
|
||||
def view_operation_report(self, opid):
|
||||
""" views the operation report
|
||||
|
||||
@param opid: Operation id to look for
|
||||
"""
|
||||
|
||||
# let postData = selectedOperationId ? {'index':'operation_report', 'op_id': selectedOperationId, 'agent_output': Number(agentOutput)} : null;
|
||||
# checking it (from snifffing protocol at the server): POST {'id': 539687}
|
||||
payload = {"index": "operation_report",
|
||||
"op_id": opid,
|
||||
'agent_output': 1
|
||||
}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
def view_operation_output(self, opid, paw, ability_id):
|
||||
""" Gets the output of an executed ability
|
||||
|
||||
@param opid: Id of the operation to look for
|
||||
@param paw: Paw of the agent to look up
|
||||
@param ability_id: if of the ability to extract the output from
|
||||
"""
|
||||
orep = self.view_operation_report(opid)
|
||||
|
||||
if paw not in orep["steps"]:
|
||||
print("Broken operation report:")
|
||||
print(orep)
|
||||
print(f"Could not find {paw} in {orep['steps']}")
|
||||
raise CalderaError
|
||||
# print("oprep: " + str(orep))
|
||||
for a_step in orep["steps"][paw]["steps"]:
|
||||
if a_step["ability_id"] == ability_id:
|
||||
try:
|
||||
# TODO There is no output if the state is for example -4 (untrusted). Fix that. Why is the caldera implant untrusted ?
|
||||
print("oprep: " + str(orep))
|
||||
return a_step["output"]
|
||||
except KeyError as exception:
|
||||
raise CalderaError from exception
|
||||
# print(f"Did not find ability {ability_id} in caldera operation output")
|
||||
return None
|
||||
|
||||
# ######### Add
|
||||
|
||||
def add_operation(self, name, advid, group="red", state="running"):
|
||||
""" Adds a new operation
|
||||
|
||||
@param name: Name of the operation
|
||||
@param advid: Adversary id
|
||||
@param group: agent group to attack
|
||||
@param state: state to initially set
|
||||
"""
|
||||
|
||||
# Add operation: curl -X PUT -H "KEY:$KEY" http://127.0.0.1:8888/api/rest -d '{"index":"operations","name":"testoperation1"}'
|
||||
# observed from GUI sniffing: PUT {'name': 'schnuffel2', 'group': 'red', 'adversary_id': '0f4c3c67-845e-49a0-927e-90ed33c044e0', 'state': 'running', 'planner': 'atomic', 'autonomous': '1', 'obfuscator': 'plain-text', 'auto_close': '1', 'jitter': '4/8', 'source': 'Alice Filters', 'visibility': '50'}
|
||||
payload = {"index": "operations",
|
||||
"name": name,
|
||||
"state": state,
|
||||
"autonomous": 1,
|
||||
'obfuscator': 'plain-text',
|
||||
'auto_close': '1',
|
||||
'jitter': '4/8',
|
||||
'source': 'Alice Filters',
|
||||
'visibility': '50',
|
||||
"group": group,
|
||||
#
|
||||
"planner": "atomic",
|
||||
"adversary_id": advid,
|
||||
}
|
||||
|
||||
return self.__contact_server__(payload, method="put")
|
||||
|
||||
def add_adversary(self, name, ability, description="created automatically"):
|
||||
""" Adds a new adversary
|
||||
|
||||
@param name: Name of the adversary
|
||||
@param ability: One ability for this adversary
|
||||
@param description: Description of this adversary
|
||||
"""
|
||||
|
||||
# Add operation: curl -X PUT -H "KEY:$KEY" http://127.0.0.1:8888/api/rest -d '{"index":"operations","name":"testoperation1"}'
|
||||
|
||||
# Sniffed from gui:
|
||||
# Rest core: PUT adversaries {'name': 'removeme', 'description': 'description', 'atomic_ordering': [{'id': 'bd527b63-9f9e-46e0-9816-b8434d2b8989'}], 'id': '558932cb-3ac6-43d2-b821-2db0fa8ad469', 'objective': ''}
|
||||
# Returns: [{'name': 'removeme', 'adversary_id': '558932cb-3ac6-43d2-b821-2db0fa8ad469', 'description': 'description', 'tags': [], 'atomic_ordering': ['bd527b63-9f9e-46e0-9816-b8434d2b8989'], 'objective': '495a9828-cab1-44dd-a0ca-66e58177d8cc'}]
|
||||
|
||||
payload = {"index": "adversaries",
|
||||
"name": name,
|
||||
"description": description,
|
||||
"atomic_ordering": [{"id": ability}],
|
||||
#
|
||||
"objective": '495a9828-cab1-44dd-a0ca-66e58177d8cc' # default objective
|
||||
}
|
||||
return self.__contact_server__(payload, method="put")
|
||||
|
||||
# ######### Execute
|
||||
|
||||
# TODO View the abilities a given agent could execute. curl -H "key:$API_KEY" -X POST localhost:8888/plugin/access/abilities -d '{"paw":"$PAW"}'
|
||||
|
||||
def execute_ability(self, paw, ability_id, obfuscator="plain-text"):
|
||||
""" Executes an ability on a target. This happens outside of the scop of an operation. You will get no result of the ability back
|
||||
|
||||
@param paw: Paw of the target
|
||||
@param ability_id: ability to execute
|
||||
@param obfuscator: Obfuscator to use
|
||||
"""
|
||||
|
||||
# curl -H "key:ADMIN123" -X POST localhost:8888/plugin/access/exploit -d '{"paw":"$PAW","ability_id":"$ABILITY_ID"}'```
|
||||
# You can optionally POST an obfuscator and/or a facts dictionary with key/value pairs to fill in any variables the chosen ability requires.
|
||||
# {"paw":"$PAW","ability_id":"$ABILITY_ID","obfuscator":"base64","facts":[{"trait":"username","value":"admin"},{"trait":"password", "value":"123"}]}
|
||||
payload = {"paw": paw,
|
||||
"ability_id": ability_id,
|
||||
"obfuscator": obfuscator}
|
||||
return self.__contact_server__(payload, rest_path="plugin/access/exploit_ex")
|
||||
|
||||
def execute_operation(self, operation_id, state="running"):
|
||||
""" Executes an operation on a server
|
||||
|
||||
@param operation_id: The operation to modify
|
||||
@param state: The state to set this operation into
|
||||
"""
|
||||
|
||||
# TODO: Change state of an operation: curl -X POST -H "KEY:ADMIN123" http://localhost:8888/api/rest -d '{"index":"operation", "op_id":123, "state":"finished"}'
|
||||
# curl -X POST -H "KEY:ADMIN123" http://localhost:8888/api/rest -d '{"index":"operation", "op_id":123, "state":"finished"}'
|
||||
|
||||
if state not in ["running", "finished", "paused", "run_one_link", "cleanup"]:
|
||||
raise ValueError
|
||||
|
||||
payload = {"index": "operation",
|
||||
"op_id": operation_id,
|
||||
"state": state}
|
||||
return self.__contact_server__(payload)
|
||||
|
||||
# ######### Delete
|
||||
|
||||
# TODO: Delete agent
|
||||
|
||||
# curl -X DELETE http://localhost:8888/api/rest -d '{"index":"operations","id":"$operation_id"}'
|
||||
def delete_operation(self, opid):
|
||||
""" Delete operation by id
|
||||
|
||||
@param opid: Operation id
|
||||
"""
|
||||
payload = {"index": "operations",
|
||||
"id": opid}
|
||||
return self.__contact_server__(payload, method="delete")
|
||||
|
||||
def delete_adversary(self, adid):
|
||||
""" Delete adversary by id
|
||||
|
||||
@param adid: Adversary id
|
||||
"""
|
||||
payload = {"index": "adversaries",
|
||||
"adversary_id": [{"adversary_id": adid}]}
|
||||
return self.__contact_server__(payload, method="delete")
|
||||
|
||||
# ######### File access
|
||||
|
||||
# TODO: Get uploaded files
|
||||
|
||||
#
|
||||
|
||||
# Link, chain and stuff
|
||||
|
||||
def is_operation_finished(self, opid):
|
||||
""" Checks if an operation finished - finished is not necessary successful !
|
||||
|
||||
@param opid: Operation id to check
|
||||
"""
|
||||
# An operation can run several Abilities vs several targets (agents). Each one is a link in the chain (see opperation report).
|
||||
# Those links can have the states:
|
||||
# return dict(HIGH_VIZ=-5,
|
||||
# UNTRUSTED=-4,
|
||||
# EXECUTE=-3,
|
||||
# DISCARD=-2,
|
||||
# PAUSE=-1)
|
||||
# Plus: 0 as "finished"
|
||||
#
|
||||
|
||||
operation = self.get_operation_by_id(opid)
|
||||
# print(f"Operation data {operation}")
|
||||
try:
|
||||
print(operation[0]["state"])
|
||||
if operation[0]["state"] == "finished":
|
||||
return True
|
||||
except KeyError as exception:
|
||||
raise CalderaError from exception
|
||||
except IndexError as exception:
|
||||
raise CalderaError from exception
|
||||
|
||||
return False
|
||||
# try:
|
||||
# for alink in operation[0]["chain"]:
|
||||
# if alink["status"] != 0:
|
||||
# return False
|
||||
# if alink["status"] == 0:
|
||||
# return True
|
||||
# except Exception as exception:
|
||||
# raise CalderaError from exception
|
||||
# return True
|
||||
|
||||
def is_operation_finished_multi(self, opid):
|
||||
""" Checks if an operation finished - finished is not necessary successful ! On several targets.
|
||||
|
||||
All links (~ abilities) on all targets must have the status 0 for this to be True.
|
||||
|
||||
@param opid: Operation id to check
|
||||
"""
|
||||
# An operation can run several Abilities vs several targets (agents). Each one is a link in the chain (see opperation report).
|
||||
# Those links can have the states:
|
||||
# return dict(HIGH_VIZ=-5,
|
||||
# UNTRUSTED=-4,
|
||||
# EXECUTE=-3,
|
||||
# DISCARD=-2,
|
||||
# PAUSE=-1)
|
||||
# Plus: 0 as "finished"
|
||||
#
|
||||
|
||||
operation = self.get_operation_by_id(opid)
|
||||
# print(f"Operation data {operation}")
|
||||
try:
|
||||
for host_group in operation[0]["host_group"]:
|
||||
for alink in host_group["links"]:
|
||||
if alink["status"] != 0:
|
||||
return False
|
||||
except Exception as exception:
|
||||
raise CalderaError from exception
|
||||
return True
|
||||
|
||||
# ######## All inclusive methods
|
||||
|
||||
def attack(self, attack_logger: AttackLog = None, paw="kickme", ability_id="bd527b63-9f9e-46e0-9816-b8434d2b8989", group="red"):
|
||||
""" Attacks a system and returns results
|
||||
|
||||
@param attack_logger: An attack logger class to log attacks with
|
||||
@param paw: Paw to attack
|
||||
@param group: Group to attack. Paw must be in the group
|
||||
@param ability_id: Ability to run against the target
|
||||
"""
|
||||
|
||||
adversary_name = "generated_adv__" + str(time.time())
|
||||
operation_name = "testoperation__" + str(time.time())
|
||||
|
||||
self.add_adversary(adversary_name, ability_id)
|
||||
adid = self.get_adversary(adversary_name)["adversary_id"]
|
||||
|
||||
if attack_logger:
|
||||
attack_logger.start_caldera_attack(source=self.url,
|
||||
paw=paw, group=group,
|
||||
ability_id=ability_id,
|
||||
ttp=self.get_ability(ability_id)[0]["technique_id"],
|
||||
name=self.get_ability(ability_id)[0]["name"],
|
||||
description=self.get_ability(ability_id)[0]["description"])
|
||||
|
||||
# ##### Create / Run Operation
|
||||
|
||||
print(f"New adversary generated. ID: {adid}, ability: {ability_id} group: {group}")
|
||||
self.add_operation(operation_name, advid=adid, group=group)
|
||||
|
||||
opid = self.get_operation(operation_name)["id"]
|
||||
print("New operation created. OpID: " + str(opid))
|
||||
|
||||
self.execute_operation(opid)
|
||||
retries = 20
|
||||
print(f"{CommandlineColors.OKGREEN}Executed attack operation{CommandlineColors.ENDC}")
|
||||
while not self.is_operation_finished(opid) and retries > 0:
|
||||
print(".... waiting for Caldera to finish")
|
||||
time.sleep(10)
|
||||
retries -= 1
|
||||
|
||||
# TODO: Handle outout from several clients
|
||||
|
||||
retries = 0
|
||||
output = None
|
||||
while retries < 10:
|
||||
try:
|
||||
output = self.view_operation_output(opid, paw, ability_id)
|
||||
except CalderaError:
|
||||
retries += 1
|
||||
time.sleep(10)
|
||||
else:
|
||||
break
|
||||
|
||||
if output is None:
|
||||
output = str(self.get_operation_by_id(opid))
|
||||
print(f"{CommandlineColors.FAIL}Failed getting operation data. We just have: {output} from get_operation_by_id{CommandlineColors.ENDC}")
|
||||
else:
|
||||
print("Output: " + str(output))
|
||||
|
||||
# ######## Cleanup
|
||||
self.execute_operation(opid, "cleanup")
|
||||
self.delete_adversary(adid)
|
||||
self.delete_operation(opid)
|
||||
if attack_logger:
|
||||
attack_logger.stop_caldera_attack(source=self.url,
|
||||
paw=paw,
|
||||
group=group,
|
||||
ability_id=ability_id,
|
||||
ttp=self.get_ability(ability_id)[0]["technique_id"],
|
||||
name=self.get_ability(ability_id)[0]["name"],
|
||||
description=self.get_ability(ability_id)[0]["description"]
|
||||
)
|
||||
|
||||
def pretty_print_ability(self, abi):
|
||||
""" Pretty pritns an ability
|
||||
|
||||
@param abi: A ability dict
|
||||
"""
|
||||
|
||||
print("""
|
||||
ID: {technique_id}
|
||||
Technique name: {technique_name}
|
||||
Tactic: {tactic}
|
||||
Name: {name}
|
||||
ID: {ability_id}
|
||||
Description: {description}
|
||||
Platform: {platform}/{executor}
|
||||
|
||||
""".format(**abi))
|
@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" Configuration loader for PurpleDome """
|
||||
|
||||
import yaml
|
||||
|
||||
from app.exceptions import ConfigurationError
|
||||
|
||||
|
||||
# TODO: Add attack scripts (that will be CACAO in the future !) and plugin config
|
||||
# So the config being read is distributed into several files and they will have different formats (yaml, CACAO)
|
||||
# Currently it is a single file and YAML only.
|
||||
# We want to be independent from file structure or number of config files
|
||||
|
||||
# TODO: Attack control also by config class. Used in experimentcontrol. Will change with scripts !
|
||||
|
||||
|
||||
class MachineConfig():
|
||||
""" Sub config for a specific machine"""
|
||||
|
||||
def __init__(self, machinedata):
|
||||
""" Init machine control config
|
||||
|
||||
@param machinedata: dict containing machine data
|
||||
"""
|
||||
if machinedata is None:
|
||||
raise ConfigurationError
|
||||
|
||||
self.raw_config = machinedata
|
||||
self.verify()
|
||||
|
||||
def verify(self):
|
||||
""" Verify essential data is present """
|
||||
try:
|
||||
self.vmname()
|
||||
operating_system = self.os()
|
||||
vmcontroller = self.vmcontroller()
|
||||
except KeyError as exception:
|
||||
raise ConfigurationError from exception
|
||||
|
||||
if operating_system not in ["linux", "windows"]:
|
||||
raise ConfigurationError
|
||||
|
||||
# TODO: Verify with plugins
|
||||
if vmcontroller not in ["vagrant", "running_vm"]:
|
||||
raise ConfigurationError
|
||||
|
||||
def vmname(self):
|
||||
""" Returns the vmname """
|
||||
|
||||
return self.raw_config["vm_name"]
|
||||
|
||||
def vmcontroller(self):
|
||||
""" Returns the vm controller. lowercase """
|
||||
|
||||
return self.raw_config["vm_controller"]["type"].lower()
|
||||
|
||||
def vm_ip(self):
|
||||
""" Return the configured ip/domain name (whatever is needed to reach the machine). Returns None if missing """
|
||||
try:
|
||||
return self.raw_config["vm_controller"]["ip"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def os(self): # pylint: disable=invalid-name
|
||||
""" returns the os. lowercase """
|
||||
|
||||
return self.raw_config["os"].lower()
|
||||
|
||||
def use_existing_machine(self):
|
||||
""" Returns if we want to use the existing machine """
|
||||
|
||||
return self.raw_config.get("use_existing_machine", False)
|
||||
|
||||
def machinepath(self):
|
||||
""" Returns the machine path. If not configured it will fall back to the vm_name """
|
||||
|
||||
return self.raw_config.get("machinepath", self.vmname())
|
||||
|
||||
def get_playground(self):
|
||||
""" Returns the machine specific playground where all the implants and tools will be installed """
|
||||
|
||||
return self.raw_config.get("playground", None)
|
||||
|
||||
def caldera_paw(self):
|
||||
""" Returns the paw (caldera id) of the machine """
|
||||
|
||||
return self.raw_config.get("paw", None)
|
||||
|
||||
def caldera_group(self):
|
||||
""" Returns the group (caldera group id) of the machine """
|
||||
|
||||
return self.raw_config.get("group", None)
|
||||
|
||||
def ssh_keyfile(self):
|
||||
""" Returns the configured SSH keyfile """
|
||||
|
||||
return self.raw_config.get("ssh_keyfile", None)
|
||||
|
||||
def ssh_user(self):
|
||||
""" Returns configured ssh user or "vagrant" as default """
|
||||
|
||||
return self.raw_config.get("ssh_user", "vagrant")
|
||||
|
||||
def halt_needs_force(self):
|
||||
""" Returns if halting the machine needs force False as default """
|
||||
|
||||
return self.raw_config.get("halt_needs_force", False)
|
||||
|
||||
def vagrantfilepath(self):
|
||||
""" Vagrant specific config: The vagrant file path """
|
||||
|
||||
if "vagrantfilepath" not in self.raw_config["vm_controller"]:
|
||||
raise ConfigurationError("Vagrantfilepath missing")
|
||||
return self.raw_config["vm_controller"]["vagrantfilepath"]
|
||||
|
||||
def sensors(self):
|
||||
""" Return a list of sensors configured for this machine """
|
||||
if "sensors" in self.raw_config:
|
||||
return self.raw_config["sensors"] or []
|
||||
return []
|
||||
|
||||
def vulnerabilities(self):
|
||||
""" Return a list of vulnerabilities configured for this machine """
|
||||
if "vulnerabilities" in self.raw_config:
|
||||
return self.raw_config["vulnerabilities"] or []
|
||||
return []
|
||||
|
||||
def is_active(self):
|
||||
""" Returns if this machine is set to active. Default is true """
|
||||
|
||||
return self.raw_config.get("active", True)
|
||||
|
||||
|
||||
class ExperimentConfig():
|
||||
""" Configuration class for a whole experiments """
|
||||
|
||||
def __init__(self, configfile):
|
||||
""" Init the config, process the file
|
||||
|
||||
@param configfile: The configuration file to process
|
||||
"""
|
||||
|
||||
self.raw_config = None
|
||||
self._targets = []
|
||||
self._attackers = []
|
||||
self.load(configfile)
|
||||
|
||||
def load(self, configfile):
|
||||
""" Loads the configuration file
|
||||
|
||||
@param configfile: The configuration file to process
|
||||
"""
|
||||
|
||||
with open(configfile) as fh:
|
||||
self.raw_config = yaml.safe_load(fh)
|
||||
|
||||
# Process targets
|
||||
for target in self.raw_config["targets"]:
|
||||
self._targets.append(MachineConfig(self.raw_config["targets"][target]))
|
||||
|
||||
# Process attackers
|
||||
for attacker in self.raw_config["attackers"]:
|
||||
self._attackers.append(MachineConfig(self.raw_config["attackers"][attacker]))
|
||||
|
||||
def targets(self) -> [MachineConfig]:
|
||||
""" Return config for targets as MachineConfig objects """
|
||||
|
||||
return self._targets
|
||||
|
||||
def attackers(self) -> [MachineConfig]:
|
||||
""" Return config for attackers as MachineConfig objects """
|
||||
|
||||
return self._attackers
|
||||
|
||||
def attacker(self, mid) -> MachineConfig:
|
||||
""" Return config for attacker as MachineConfig objects
|
||||
|
||||
@param mid: id of the attacker, 0 is main attacker
|
||||
"""
|
||||
|
||||
return self.attackers()[mid]
|
||||
|
||||
def caldera_apikey(self):
|
||||
""" Returns the caldera apikey """
|
||||
|
||||
return self.raw_config["caldera"]["apikey"]
|
||||
|
||||
def loot_dir(self):
|
||||
""" Returns the loot dir """
|
||||
|
||||
return self.raw_config["results"]["loot_dir"]
|
||||
|
||||
def kali_conf(self, attack):
|
||||
""" Get kali config for a specific kali attack
|
||||
|
||||
@param attack: Name of the attack to look up config for
|
||||
"""
|
||||
|
||||
try:
|
||||
res = self.raw_config["kali_conf"][attack]
|
||||
except KeyError as exception:
|
||||
raise ConfigurationError from exception
|
||||
|
||||
return res
|
||||
|
||||
def get_nap_time(self):
|
||||
""" Returns the attackers nap time between attack steps """
|
||||
|
||||
try:
|
||||
return self.raw_config["attacks"]["nap_time"]
|
||||
except KeyError:
|
||||
return 0
|
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3
|
||||
""" A collection of shared exceptions """
|
||||
|
||||
|
||||
class ServerError(Exception):
|
||||
""" An elemental server is not running """
|
||||
|
||||
|
||||
class ConfigurationError(Exception):
|
||||
""" An elemental server is not running """
|
||||
|
||||
|
||||
class PluginError(Exception):
|
||||
""" Some plugin core function is broken """
|
||||
|
||||
|
||||
class CalderaError(Exception):
|
||||
""" Caldera is broken """
|
||||
|
||||
|
||||
class NetworkError(Exception):
|
||||
""" Network connection (like ssh) can not be established """
|
@ -0,0 +1,229 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" A class to control a whole experiment. From setting up the machines to running the attacks """
|
||||
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
|
||||
from app.attack_log import AttackLog
|
||||
from app.config import ExperimentConfig
|
||||
from app.interface_sfx import CommandlineColors
|
||||
from caldera_control import CalderaControl
|
||||
from machine_control import Machine
|
||||
|
||||
|
||||
# TODO: Multi threading at least when starting machines
|
||||
|
||||
class Experiment():
|
||||
""" Class handling experiments """
|
||||
|
||||
def __init__(self, configfile):
|
||||
"""
|
||||
|
||||
@param configfile: Path to the configfile to load """
|
||||
|
||||
self.attacker_1 = None
|
||||
|
||||
self.experiment_control = ExperimentConfig(configfile)
|
||||
self.attack_logger = AttackLog()
|
||||
self.__start_attacker()
|
||||
|
||||
self.starttime = datetime.now().strftime("%Y_%m_%d___%H_%M_%S")
|
||||
self.lootdir = os.path.join(self.experiment_control.loot_dir(), self.starttime)
|
||||
os.makedirs(self.lootdir)
|
||||
|
||||
self.targets = []
|
||||
# start target machines
|
||||
for target_conf in self.experiment_control.targets():
|
||||
if not target_conf.is_active():
|
||||
continue
|
||||
|
||||
tname = target_conf.vmname()
|
||||
|
||||
print(f"{CommandlineColors.OKBLUE}preparing target {tname} ....{CommandlineColors.ENDC}")
|
||||
target_1 = Machine(target_conf)
|
||||
target_1.set_caldera_server(self.attacker_1.getip())
|
||||
try:
|
||||
if not target_conf.use_existing_machine():
|
||||
target_1.destroy()
|
||||
except subprocess.CalledProcessError:
|
||||
# Maybe the machine just does not exist yet
|
||||
pass
|
||||
target_1.install_caldera_service()
|
||||
target_1.up()
|
||||
# TODO prime sensors here
|
||||
needs_reboot = target_1.prime_sensors()
|
||||
if needs_reboot:
|
||||
target_1.reboot()
|
||||
print(f"{CommandlineColors.OKGREEN}Target is up: {tname} {CommandlineColors.ENDC}")
|
||||
target_1.start_caldera_client()
|
||||
print(f"{CommandlineColors.OKGREEN}Initial start of caldera client: {tname} {CommandlineColors.ENDC}")
|
||||
self.targets.append(target_1)
|
||||
|
||||
# TODO: Install vulnerabilities by plugin
|
||||
|
||||
print(f"{CommandlineColors.OKBLUE}Contacting caldera agents on all targets ....{CommandlineColors.ENDC}")
|
||||
time.sleep(20)
|
||||
# Wait until all targets are registered as Caldera targets
|
||||
for target_1 in self.targets:
|
||||
caldera_url = "http://" + self.attacker_1.getip() + ":8888"
|
||||
|
||||
caldera_control = CalderaControl(caldera_url, config=self.experiment_control)
|
||||
running_agents = [i["paw"] for i in caldera_control.list_agents()]
|
||||
while target_1.get_paw() not in running_agents:
|
||||
print(f"Connecting to caldera {caldera_url}, running agents are: {running_agents}")
|
||||
print(f"Missing agent: {target_1.get_paw()} ...")
|
||||
target_1.start_caldera_client()
|
||||
print(f"Restarted caldera agent: {target_1.get_paw()} ...")
|
||||
time.sleep(120) # Was 30, but maybe there are timing issues
|
||||
running_agents = [i["paw"] for i in caldera_control.list_agents()]
|
||||
print(f"{CommandlineColors.OKGREEN}Caldera agents reached{CommandlineColors.ENDC}")
|
||||
|
||||
# Install vulnerabilities
|
||||
for a_target in self.targets:
|
||||
print(f"Installing vulnerabilities on {a_target.get_paw()}")
|
||||
a_target.install_vulnerabilities()
|
||||
a_target.start_vulnerabilities()
|
||||
|
||||
# Install sensor plugins
|
||||
for a_target in self.targets:
|
||||
print(f"Installing sensors on {a_target.get_paw()}")
|
||||
a_target.install_sensors()
|
||||
a_target.start_sensors()
|
||||
|
||||
# Attack them
|
||||
print(f"{CommandlineColors.OKBLUE}Running Caldera attacks{CommandlineColors.ENDC}")
|
||||
for target_1 in self.targets:
|
||||
# Run caldera attacks
|
||||
caldera_attacks = self.experiment_control.raw_config["caldera_attacks"][target_1.get_os()]
|
||||
if caldera_attacks:
|
||||
for attack in caldera_attacks:
|
||||
# TODO: Work with snapshots
|
||||
# TODO: If we have several targets in the same group, it is nonsense to attack each one separately. Make this smarter
|
||||
print(f"Attacking machine with PAW: {target_1.get_paw()}")
|
||||
caldera_control = CalderaControl("http://" + self.attacker_1.getip() + ":8888", config=self.experiment_control)
|
||||
|
||||
caldera_control.attack(self.attack_logger, target_1.get_paw(), attack, target_1.get_group())
|
||||
|
||||
time.sleep(self.experiment_control.get_nap_time())
|
||||
print(f"{CommandlineColors.OKGREEN}Finished Caldera attacks{CommandlineColors.ENDC}")
|
||||
|
||||
# Run Kali attacks
|
||||
print(f"{CommandlineColors.OKBLUE}Running Kali attacks{CommandlineColors.ENDC}")
|
||||
for target_1 in self.targets:
|
||||
for attack in self.experiment_control.raw_config["kali_attacks"][target_1.get_os()]:
|
||||
# TODO: Work with snapshots
|
||||
|
||||
self.attacker_1.kali_attack(attack, target_1.getip(), self.experiment_control)
|
||||
|
||||
time.sleep(self.experiment_control.get_nap_time())
|
||||
|
||||
print(f"{CommandlineColors.OKGREEN}Finished Kali attacks{CommandlineColors.ENDC}")
|
||||
|
||||
# Stop sensor plugins
|
||||
# Collect data
|
||||
for a_target in self.targets:
|
||||
a_target.stop_sensors()
|
||||
a_target.collect_sensors(self.lootdir)
|
||||
|
||||
# Uninstall vulnerabilities
|
||||
for a_target in self.targets:
|
||||
print(f"Uninstalling vulnerabilities on {a_target.get_paw()}")
|
||||
a_target.stop_vulnerabilities()
|
||||
|
||||
# TODO: Zip result dir
|
||||
|
||||
# Stop target machines
|
||||
for target_1 in self.targets:
|
||||
target_1.halt()
|
||||
|
||||
self.__stop_attacker()
|
||||
self.attack_logger.write_json(os.path.join(self.lootdir, "attack.json"))
|
||||
self.zip_loot()
|
||||
|
||||
def zip_loot(self):
|
||||
""" Zip the loot together """
|
||||
|
||||
filename = os.path.join(self.lootdir, self.starttime + ".zip")
|
||||
globs = ["/**/*.json",
|
||||
"/**/*.proto",
|
||||
"/*/**/*.zip",
|
||||
|
||||
]
|
||||
|
||||
print(f"Creating zip file {filename}")
|
||||
|
||||
with zipfile.ZipFile(filename, "w") as zfh:
|
||||
for a_glob in globs:
|
||||
a_glob = self.lootdir + a_glob
|
||||
for a_file in glob.iglob(a_glob, recursive=True):
|
||||
if a_file != filename:
|
||||
print(a_file)
|
||||
zfh.write(a_file)
|
||||
|
||||
@staticmethod
|
||||
def __get_results_files(root):
|
||||
""" Yields a list of potential result files
|
||||
|
||||
@param root: Root dir of the machine to collect data from
|
||||
"""
|
||||
# TODO: Properly implement. Get proper root parameter
|
||||
|
||||
total = [os.path.join(root, "logstash", "filebeat.json")]
|
||||
for a_file in total:
|
||||
if os.path.exists(a_file):
|
||||
yield a_file
|
||||
|
||||
def __clean_result_files(self, root):
|
||||
""" Deletes result files
|
||||
|
||||
@param root: Root dir of the machine to collect data from
|
||||
"""
|
||||
|
||||
# TODO: Properly implement. Get proper root parameter
|
||||
|
||||
for a_file in self.__get_results_files(root):
|
||||
os.remove(a_file)
|
||||
|
||||
def __collect_loot(self, root):
|
||||
""" Collect results into loot dir
|
||||
|
||||
@param root: Root dir of the machine to collect data from
|
||||
"""
|
||||
|
||||
try:
|
||||
os.makedirs(os.path.abspath(self.experiment_control.loot_dir()))
|
||||
except FileExistsError:
|
||||
pass
|
||||
for a_file in self.__get_results_files(root):
|
||||
print("Copy {} {}".format(a_file, os.path.abspath(self.experiment_control.loot_dir())))
|
||||
|
||||
def __start_attacker(self):
|
||||
""" Start the attacking VM """
|
||||
|
||||
# Preparing attacker
|
||||
self.attacker_1 = Machine(self.experiment_control.attacker(0).raw_config)
|
||||
|
||||
if not self.experiment_control.attacker(0).use_existing_machine():
|
||||
try:
|
||||
self.attacker_1.destroy()
|
||||
except subprocess.CalledProcessError:
|
||||
# Machine does not exist
|
||||
pass
|
||||
self.attacker_1.create(reboot=False)
|
||||
self.attacker_1.up()
|
||||
self.attacker_1.install_caldera_server(cleanup=False)
|
||||
else:
|
||||
self.attacker_1.up()
|
||||
self.attacker_1.install_caldera_server(cleanup=False)
|
||||
|
||||
self.attacker_1.start_caldera_server()
|
||||
self.attacker_1.set_attack_logger(self.attack_logger)
|
||||
|
||||
def __stop_attacker(self):
|
||||
""" Stop the attacking VM """
|
||||
self.attacker_1.halt()
|
@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" Helper functions to improve the command line experiments """
|
||||
|
||||
# Colors to be used when printing text to the terminal
|
||||
# print(f"{CommandlineColors.WARNING}Warning {CommandlineColors.ENDC}")
|
||||
|
||||
|
||||
class CommandlineColors:
|
||||
""" A collection of command line colors """
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
HEADER = '\033[95m'
|
||||
OKBLUE = '\033[94m'
|
||||
OKCYAN = '\033[96m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
ATTACK = '\033[93m' # An attack is running
|
||||
MACHINE_CREATED = '\033[92m'
|
||||
MACHINE_STOPPED = '\033[96m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
BOLD = '\033[1m'
|
||||
UNDERLINE = '\033[4m'
|
@ -0,0 +1,563 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" (Virtual) machine handling. Start, stop, create and destroy. Starting remote commands on them. """
|
||||
|
||||
import os
|
||||
import time
|
||||
from glob import glob
|
||||
|
||||
import requests
|
||||
import straight.plugin
|
||||
|
||||
from app.config import MachineConfig, ExperimentConfig
|
||||
from app.exceptions import ServerError, ConfigurationError
|
||||
from app.calderacontrol import CalderaControl
|
||||
from app.interface_sfx import CommandlineColors
|
||||
from plugins.base.kali import KaliPlugin
|
||||
from plugins.base.machinery import MachineryPlugin
|
||||
from plugins.base.sensor import SensorPlugin
|
||||
from plugins.base.vulnerability_plugin import VulnerabilityPlugin
|
||||
|
||||
|
||||
class Machine():
|
||||
""" A virtual machine. Attacker or target. Abstracting stuff away. """
|
||||
|
||||
def __init__(self, config, calderakey="ADMIN123"):
|
||||
"""
|
||||
|
||||
@param config: The machine configuration as dict
|
||||
@param calderakey: Key to the caldera controller
|
||||
"""
|
||||
|
||||
self.vm_manager = None
|
||||
self.attack_logger = None
|
||||
|
||||
if isinstance(config, MachineConfig):
|
||||
self.config = config
|
||||
else:
|
||||
self.config = MachineConfig(config)
|
||||
|
||||
# TODO: Read config from plugin
|
||||
if self.config.vmcontroller() == "vagrant":
|
||||
self.__parse_vagrant_config__()
|
||||
if self.config.vmcontroller() == "running_vm":
|
||||
self.__parse_running_vm_config__()
|
||||
|
||||
self.caldera_server = None
|
||||
|
||||
self.abs_machinepath_external = None
|
||||
|
||||
self.abs_machinepath_external = os.path.join(self.vagrantfilepath, self.config.machinepath())
|
||||
# TODO Add internal machinepath path for within the VM (/vagrant/machinepath) for non-linux machines
|
||||
self.abs_machinepath_internal = os.path.join("/vagrant/", self.config.machinepath())
|
||||
|
||||
if not os.path.exists(self.abs_machinepath_external):
|
||||
raise ConfigurationError(f"machinepath does not exist: {self.abs_machinepath_external}")
|
||||
|
||||
self.load_machine_plugin()
|
||||
self.caldera_basedir = self.vm_manager.get_playground()
|
||||
|
||||
self.calderakey = calderakey
|
||||
self.sensors = [] # Sensor plugins
|
||||
self.vulnerabilities = [] # Vulnerability plugins
|
||||
|
||||
def __parse_vagrant_config__(self):
|
||||
""" Check if a file configured in the config is present """
|
||||
|
||||
self.vagrantfilepath = os.path.abspath(self.config.vagrantfilepath())
|
||||
self.vagrantfile = os.path.join(self.vagrantfilepath, "Vagrantfile")
|
||||
if not os.path.isfile(self.vagrantfile):
|
||||
raise ConfigurationError(f"Vagrantfile not existing: {self.vagrantfile}")
|
||||
|
||||
def __parse_running_vm_config__(self):
|
||||
""" Check if a file configured in the config is present """
|
||||
|
||||
self.vagrantfilepath = os.path.abspath(self.config.vagrantfilepath())
|
||||
self.vagrantfile = os.path.join(self.vagrantfilepath, "Vagrantfile")
|
||||
|
||||
def get_paw(self):
|
||||
""" Returns the paw of the current machine """
|
||||
return self.config.caldera_paw()
|
||||
|
||||
def get_group(self):
|
||||
""" Returns the group of the current machine """
|
||||
return self.config.caldera_group()
|
||||
|
||||
def destroy(self):
|
||||
""" Destroys the current machine """
|
||||
|
||||
self.vm_manager.__call_destroy__()
|
||||
|
||||
def create(self, reboot=True):
|
||||
""" Create a VM
|
||||
|
||||
@param reboot: Reboot the VM during installation. Required if you want to install software
|
||||
"""
|
||||
|
||||
self.vm_manager.__call_create__(reboot)
|
||||
|
||||
def reboot(self):
|
||||
""" Reboot a machine """
|
||||
|
||||
if self.get_os() == "windows":
|
||||
self.vm_manager.remote_run("shutdown /r")
|
||||
self.vm_manager.__call_disconnect__()
|
||||
time.sleep(60) # Shutdown can be slow....
|
||||
if self.get_os() == "linux":
|
||||
self.vm_manager.remote_run("reboot")
|
||||
self.vm_manager.__call_disconnect__()
|
||||
res = None
|
||||
while not res:
|
||||
time.sleep(5)
|
||||
res = self.vm_manager.__call_connect__()
|
||||
print("Re-connecting....")
|
||||
|
||||
def up(self): # pylint: disable=invalid-name
|
||||
""" Starts a VM. Creates it if not already created """
|
||||
|
||||
self.vm_manager.__call_up__()
|
||||
|
||||
def halt(self):
|
||||
""" Halts a VM """
|
||||
|
||||
self.vm_manager.__call_halt__()
|
||||
|
||||
def getuser(self):
|
||||
""" Gets the user of the current VM """
|
||||
|
||||
return "Result " + str(self.vm_manager.__call_remote_run__("echo $USER"))
|
||||
|
||||
def connect(self):
|
||||
""" command connection. establish it """
|
||||
|
||||
return self.vm_manager.__call_connect__()
|
||||
|
||||
def disconnect(self, connection):
|
||||
""" Command connection dis-connect """
|
||||
|
||||
self.vm_manager.__call_disconnect__(connection)
|
||||
|
||||
def remote_run(self, cmd, disown=False):
|
||||
""" Simplifies connect and run
|
||||
|
||||
@param cmd: Command to run as shell command
|
||||
@param disown: run in background
|
||||
"""
|
||||
|
||||
return self.vm_manager.__call_remote_run__(cmd, disown)
|
||||
|
||||
def kali_attack(self, attack, target, config: ExperimentConfig):
|
||||
""" Pick a Kali attack and run it
|
||||
|
||||
@param attack: Name of the attack to run
|
||||
@param target: IP address of the target
|
||||
@param config: A full experiment config object that has the methog "kali_conf" (just in case I want to split the config later)
|
||||
@returns: The output of the cmdline attacking tool
|
||||
"""
|
||||
|
||||
def get_handlers(plugin) -> [KaliPlugin]:
|
||||
return plugin.produce()
|
||||
|
||||
base = "plugins/**/*.py"
|
||||
|
||||
plugin_dirs = set()
|
||||
for a_glob in glob(base, recursive=True):
|
||||
plugin_dirs.add(os.path.dirname(a_glob))
|
||||
|
||||
for a_directory in plugin_dirs:
|
||||
plugins = straight.plugin.load(a_directory, subclasses=KaliPlugin)
|
||||
|
||||
handlers = get_handlers(plugins)
|
||||
|
||||
for plugin in handlers:
|
||||
name = plugin.get_name()
|
||||
if name == attack:
|
||||
print(f"{CommandlineColors.OKBLUE}Running Kali plugin {name}{CommandlineColors.ENDC}")
|
||||
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
|
||||
"abs_machinepath_external": self.abs_machinepath_external}
|
||||
plugin.set_sysconf(syscon)
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.__set_logger__(self.attack_logger)
|
||||
plugin.__execute__([target], config.kali_conf(name))
|
||||
|
||||
def load_machine_plugin(self):
|
||||
""" Loads the matching machine plugin """
|
||||
|
||||
def get_handlers(a_plugin) -> [MachineryPlugin]:
|
||||
return a_plugin.produce()
|
||||
|
||||
base = "plugins/**/*.py"
|
||||
|
||||
plugin_dirs = set()
|
||||
for a_glob in glob(base, recursive=True):
|
||||
plugin_dirs.add(os.path.dirname(a_glob))
|
||||
|
||||
for a_dir in plugin_dirs:
|
||||
plugins = straight.plugin.load(a_dir, subclasses=MachineryPlugin)
|
||||
|
||||
handlers = get_handlers(plugins)
|
||||
|
||||
for plugin in handlers:
|
||||
name = plugin.get_name()
|
||||
if name == self.config.vmcontroller():
|
||||
print(f"{CommandlineColors.OKBLUE}Installing sensor: {name}{CommandlineColors.ENDC}")
|
||||
|
||||
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
|
||||
"abs_machinepath_external": self.abs_machinepath_external}
|
||||
plugin.set_sysconf(syscon)
|
||||
plugin.__call_process_config__(self.config)
|
||||
self.vm_manager = plugin
|
||||
break
|
||||
|
||||
def prime_sensors(self):
|
||||
""" Prime sensors from plugins (hard core installs that could require a reboot)
|
||||
|
||||
A machine can have several sensors running. Those are defined in a list in the config. This primes the sensors
|
||||
|
||||
"""
|
||||
|
||||
def get_handlers(a_plugin) -> [SensorPlugin]:
|
||||
return a_plugin.produce()
|
||||
|
||||
base = "plugins/**/*.py"
|
||||
reboot = False
|
||||
|
||||
plugin_dirs = set()
|
||||
for a_glob in glob(base, recursive=True):
|
||||
plugin_dirs.add(os.path.dirname(a_glob))
|
||||
|
||||
for a_dir in plugin_dirs:
|
||||
plugins = straight.plugin.load(a_dir, subclasses=SensorPlugin)
|
||||
|
||||
handlers = get_handlers(plugins)
|
||||
|
||||
for plugin in handlers:
|
||||
name = plugin.get_name()
|
||||
if name in self.config.sensors():
|
||||
print(f"{CommandlineColors.OKBLUE}Priming sensor: {name}{CommandlineColors.ENDC}")
|
||||
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
|
||||
"abs_machinepath_external": self.abs_machinepath_external,
|
||||
"sensor_specific": self.config.raw_config.get(name, {})
|
||||
}
|
||||
plugin.set_sysconf(syscon)
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.setup()
|
||||
reboot |= plugin.prime()
|
||||
self.sensors.append(plugin)
|
||||
print(f"{CommandlineColors.OKGREEN}Primed sensor: {name}{CommandlineColors.ENDC}")
|
||||
return reboot
|
||||
|
||||
def install_sensors(self):
|
||||
""" Install sensors from plugins
|
||||
|
||||
A machine can have several sensors running. Those are defined in a list in the config. This installs the sensors
|
||||
|
||||
"""
|
||||
|
||||
for plugin in self.get_sensors():
|
||||
name = plugin.get_name()
|
||||
|
||||
print(f"{CommandlineColors.OKBLUE}Installing sensor: {name}{CommandlineColors.ENDC}")
|
||||
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
|
||||
"abs_machinepath_external": self.abs_machinepath_external,
|
||||
"sensor_specific": self.config.raw_config.get(name, {})}
|
||||
plugin.set_sysconf(syscon)
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.setup()
|
||||
plugin.install()
|
||||
print(f"{CommandlineColors.OKGREEN}Installed sensor: {name}{CommandlineColors.ENDC}")
|
||||
# self.sensors.append(plugin)
|
||||
|
||||
def get_sensors(self) -> [SensorPlugin]:
|
||||
""" Returns a list of running sensors """
|
||||
return self.sensors
|
||||
|
||||
def start_sensors(self):
|
||||
""" Start sensors
|
||||
|
||||
A machine can have several sensors running. Those are defined in a list in the config. This starts the sensors
|
||||
|
||||
"""
|
||||
for plugin in self.get_sensors():
|
||||
print(f"{CommandlineColors.OKBLUE}Starting sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.start()
|
||||
print(f"{CommandlineColors.OKGREEN}Started sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
|
||||
|
||||
def stop_sensors(self):
|
||||
""" Stop sensors
|
||||
|
||||
A machine can have several sensors running. Those are defined in a list in the config. This stops the sensors
|
||||
|
||||
"""
|
||||
|
||||
for plugin in self.get_sensors():
|
||||
print(f"{CommandlineColors.OKBLUE}Stopping sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.stop()
|
||||
print(f"{CommandlineColors.OKGREEN}Stopped sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
|
||||
|
||||
def collect_sensors(self, lootdir):
|
||||
""" Collect data from sensors
|
||||
|
||||
A machine can have several sensors running. Those are defined in a list in the config. This collects the data from the sensors
|
||||
|
||||
@param lootdir: Fresh created directory for loot
|
||||
"""
|
||||
|
||||
machine_specific_path = os.path.join(lootdir, self.config.vmname())
|
||||
os.mkdir(machine_specific_path)
|
||||
|
||||
for plugin in self.get_sensors():
|
||||
print(f"{CommandlineColors.OKBLUE}Collecting sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.__call_collect__(machine_specific_path)
|
||||
print(f"{CommandlineColors.OKGREEN}Collected sensor: {plugin.get_name()}{CommandlineColors.ENDC}")
|
||||
|
||||
############
|
||||
|
||||
def install_vulnerabilities(self):
|
||||
""" Install vulnerabilities from plugins: The machine is not yet modified ! For that call start_vulnerabilities next
|
||||
|
||||
A machine can have several vulnerabilities. Those are defined in a list in the config. This installs the vulnerabilities
|
||||
|
||||
"""
|
||||
|
||||
def get_handlers(a_plugin) -> [SensorPlugin]:
|
||||
return a_plugin.produce()
|
||||
|
||||
base = "plugins/**/*.py"
|
||||
|
||||
plugin_dirs = set()
|
||||
for a_glob in glob(base, recursive=True):
|
||||
plugin_dirs.add(os.path.dirname(a_glob))
|
||||
|
||||
for a_dir in plugin_dirs:
|
||||
plugins = straight.plugin.load(a_dir, subclasses=VulnerabilityPlugin)
|
||||
|
||||
handlers = get_handlers(plugins)
|
||||
|
||||
for plugin in handlers:
|
||||
name = plugin.get_name()
|
||||
print(f"Configured vulnerabilities: {self.config.vulnerabilities()}")
|
||||
if name in self.config.vulnerabilities():
|
||||
print(f"{CommandlineColors.OKBLUE}Installing vulnerability: {name}{CommandlineColors.ENDC}")
|
||||
syscon = {"abs_machinepath_internal": self.abs_machinepath_internal,
|
||||
"abs_machinepath_external": self.abs_machinepath_external}
|
||||
plugin.set_sysconf(syscon)
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.setup()
|
||||
plugin.install(self.vm_manager)
|
||||
self.vulnerabilities.append(plugin)
|
||||
|
||||
def get_vulnerabilities(self) -> [SensorPlugin]:
|
||||
""" Returns a list of installed vulnerabilities """
|
||||
return self.vulnerabilities
|
||||
|
||||
def start_vulnerabilities(self):
|
||||
""" Really install the vulnerabilities on the machine
|
||||
|
||||
A machine can have vulnerabilities installed. Those are defined in a list in the config. This starts the vulnerabilities
|
||||
|
||||
"""
|
||||
for plugin in self.get_vulnerabilities():
|
||||
print(f"{CommandlineColors.OKBLUE}Activating vulnerability: {plugin.get_name()}{CommandlineColors.ENDC}")
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.start()
|
||||
|
||||
def stop_vulnerabilities(self):
|
||||
""" Un-install the vulnerabilities on the machine
|
||||
|
||||
A machine can have vulnerabilities installed. Those are defined in a list in the config. This stops the vulnerabilities
|
||||
|
||||
"""
|
||||
for plugin in self.get_vulnerabilities():
|
||||
print(f"{CommandlineColors.OKBLUE}Uninstalling vulnerability: {plugin.get_name()}{CommandlineColors.ENDC}")
|
||||
plugin.set_machine_plugin(self.vm_manager)
|
||||
plugin.stop()
|
||||
|
||||
############
|
||||
|
||||
def getip(self):
|
||||
""" Returns the IP of the main ethernet interface of this machine """
|
||||
|
||||
# TODO: Create special code to extract windows IPs
|
||||
|
||||
# TODO: Find a smarter way to get the ip
|
||||
|
||||
return self.vm_manager.get_ip()
|
||||
|
||||
def install_caldera_server(self, cleanup=False, version="2.8.1"):
|
||||
""" Installs the caldera server on the VM
|
||||
|
||||
@param cleanup: Remove the old caldera version. Slow but reduces side effects
|
||||
@param version: Caldera version to use. Check Caldera git for potential branches to use
|
||||
"""
|
||||
# https://github.com/mitre/caldera.git
|
||||
print(f"{CommandlineColors.OKBLUE}Installing Caldera server {CommandlineColors.ENDC}")
|
||||
|
||||
if cleanup:
|
||||
cleanupcmd = "rm -rf caldera;"
|
||||
else:
|
||||
cleanupcmd = ""
|
||||
|
||||
cmd = f"cd {self.caldera_basedir}; {cleanupcmd} git clone https://github.com/mitre/caldera.git --recursive --branch {version}; cd caldera; pip3 install -r requirements.txt"
|
||||
print(f"{CommandlineColors.OKGREEN}Caldera server installed {CommandlineColors.ENDC}")
|
||||
res = self.vm_manager.__call_remote_run__(cmd)
|
||||
return "Result installing caldera server " + str(res)
|
||||
|
||||
def wait_for_caldera_server(self, timeout=6):
|
||||
""" Ping caldera server. return as soon as it is responding
|
||||
|
||||
@param timeout: timeout in seconds
|
||||
"""
|
||||
for i in range(timeout):
|
||||
time.sleep(10)
|
||||
caldera_url = "http://" + self.getip() + ":8888"
|
||||
caldera_control = CalderaControl(caldera_url, apikey=self.calderakey)
|
||||
print(f"{i} Trying to connect to {caldera_url} Caldera API")
|
||||
try:
|
||||
caldera_control.list_adversaries()
|
||||
except requests.exceptions.ConnectionError:
|
||||
pass
|
||||
else:
|
||||
print("Caldera: All systems nominal")
|
||||
return True
|
||||
raise ServerError
|
||||
|
||||
def start_caldera_server(self):
|
||||
""" Start the caldera server on the VM. Required for an attacker VM """
|
||||
# https://github.com/mitre/caldera.git
|
||||
|
||||
print(f"{CommandlineColors.OKBLUE}Starting Caldera server {CommandlineColors.ENDC}")
|
||||
|
||||
cmd = f"cd {self.caldera_basedir}; cd caldera ; nohup python3 server.py --insecure &"
|
||||
self.vm_manager.__call_remote_run__(cmd, disown=True)
|
||||
self.wait_for_caldera_server()
|
||||
print(f"{CommandlineColors.OKGREEN}Caldera server started. Confirmed it is running. {CommandlineColors.ENDC}")
|
||||
|
||||
def create_start_caldera_client_cmd(self):
|
||||
""" Creates a command to start the caldera client """
|
||||
|
||||
playground = self.vm_manager.get_playground()
|
||||
|
||||
if self.get_os() == "linux":
|
||||
cmd = f"""
|
||||
nohup {playground}/caldera_agent.sh start &
|
||||
"""
|
||||
elif self.get_os() == "windows":
|
||||
if playground:
|
||||
playground = playground + "\\" # Workaround for Windows: Can not set target dir for fabric-put in Windows. Only default (none=user) dir available.
|
||||
else:
|
||||
playground = ""
|
||||
# playground = self.vm_manager.get_playground()
|
||||
cmd = f"""
|
||||
{playground}caldera_agent.bat
|
||||
"""
|
||||
|
||||
return cmd
|
||||
|
||||
def start_caldera_client(self):
|
||||
""" Install caldera client. Required on targets """
|
||||
|
||||
name = self.vm_manager.get_vm_name()
|
||||
print(f"{CommandlineColors.OKBLUE}Starting Caldera client {name} {CommandlineColors.ENDC}")
|
||||
|
||||
if self.get_os() == "windows":
|
||||
# TODO: Do not mount but use ssh to copy
|
||||
|
||||
url = "http://" + self.caldera_server + ":8888"
|
||||
caldera_control = CalderaControl(url, apikey=self.calderakey)
|
||||
caldera_control.fetch_client(platform="windows",
|
||||
file="sandcat.go",
|
||||
target_dir=self.abs_machinepath_external,
|
||||
extension=".go")
|
||||
dst = self.vm_manager.get_playground()
|
||||
src = os.path.join(self.abs_machinepath_external, "caldera_agent.bat")
|
||||
self.vm_manager.put(src, dst)
|
||||
src = os.path.join(self.abs_machinepath_external, "splunkd.go") # sandcat.go local name
|
||||
self.vm_manager.put(src, dst)
|
||||
|
||||
cmd = self.__install_caldera_service_cmd().strip()
|
||||
print(cmd)
|
||||
self.vm_manager.remote_run(cmd, disown=False)
|
||||
|
||||
if self.get_os() == "linux":
|
||||
dst = self.vm_manager.get_playground()
|
||||
src = os.path.join(self.abs_machinepath_external, "caldera_agent.sh")
|
||||
self.vm_manager.put(src, dst)
|
||||
|
||||
cmd = self.create_start_caldera_client_cmd().strip()
|
||||
|
||||
print(cmd)
|
||||
self.vm_manager.remote_run(cmd, disown=True)
|
||||
|
||||
print(f"{CommandlineColors.OKGREEN}Caldera client started {CommandlineColors.ENDC}")
|
||||
|
||||
def get_os(self):
|
||||
""" Returns the OS of the machine """
|
||||
|
||||
return self.config.os()
|
||||
|
||||
def __install_caldera_service_cmd(self):
|
||||
playground = self.vm_manager.get_playground()
|
||||
|
||||
if self.get_os() == "linux":
|
||||
return f"""
|
||||
#!/bin/bash
|
||||
|
||||
# Installs and runs the caldera agent
|
||||
|
||||
# TODO: Respect start/stop commands
|
||||
|
||||
cd {playground}
|
||||
server="http://{self.caldera_server}:8888";
|
||||
curl -s -X POST -H "file:sandcat.go" -H "platform:linux" $server/file/download > sandcat.go;
|
||||
chmod +x sandcat.go;
|
||||
nohup ./sandcat.go -server $server -group {self.config.caldera_group()} -v -paw {self.config.caldera_paw()} &
|
||||
"""
|
||||
if self.get_os() == "windows":
|
||||
if playground: # Workaround for Windows: Can not set target dir for fabric-put in Windows. Only default (none=user) dir available.
|
||||
playground = playground + "\\"
|
||||
else:
|
||||
playground = ""
|
||||
url = "http://" + self.caldera_server + ":8888"
|
||||
caldera_control = CalderaControl(url, apikey=self.calderakey)
|
||||
filename = caldera_control.fetch_client(platform="windows",
|
||||
file="sandcat.go",
|
||||
target_dir=self.abs_machinepath_external,
|
||||
extension=".go")
|
||||
return f"""
|
||||
START {playground}{filename} -server {url} -group {self.config.caldera_group()} -paw {self.config.caldera_paw()}
|
||||
""".strip()
|
||||
|
||||
raise Exception # System type unknown
|
||||
|
||||
def install_caldera_service(self):
|
||||
""" Install the caldera client as a service. For linux targets """
|
||||
|
||||
# print("DELETEME ! " + sys._getframe().f_code.co_name)
|
||||
|
||||
content = self.__install_caldera_service_cmd()
|
||||
|
||||
print(f"{CommandlineColors.OKBLUE}Installing Caldera service {CommandlineColors.ENDC}")
|
||||
|
||||
if self.get_os() == "linux":
|
||||
filename = os.path.join(self.abs_machinepath_external, "caldera_agent.sh")
|
||||
elif self.get_os() == "windows":
|
||||
filename = os.path.join(self.abs_machinepath_external, "caldera_agent.bat")
|
||||
with open(filename, "wt") as fh:
|
||||
fh.write(content)
|
||||
print(f"{CommandlineColors.OKGREEN}Installed Caldera service {CommandlineColors.ENDC}")
|
||||
|
||||
def set_caldera_server(self, server):
|
||||
""" Set the local caldera server config """
|
||||
self.caldera_server = server
|
||||
|
||||
def set_attack_logger(self, attack_logger):
|
||||
""" Configure the attack logger for this server
|
||||
|
||||
@param attack_logger: The attack logger to set
|
||||
"""
|
||||
|
||||
self.attack_logger = attack_logger
|
@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" A command line tool to control a caldera server """
|
||||
|
||||
import argparse
|
||||
|
||||
from app.calderacontrol import CalderaControl
|
||||
|
||||
|
||||
# https://caldera.readthedocs.io/en/latest/The-REST-API.html
|
||||
|
||||
|
||||
# Arpgparse handling
|
||||
def list_agents(calcontrol, arguments): # pylint: disable=unused-argument
|
||||
""" Call list agents in caldera control
|
||||
|
||||
@param calcontrol: Connection to the caldera server
|
||||
@param arguments: Parser command line arguments
|
||||
"""
|
||||
# TODO: calcontrol.list_agents(arguments)
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
|
||||
def list_abilities(calcontrol, arguments):
|
||||
""" Call list abilities in caldera control
|
||||
|
||||
@param calcontrol: Connection to the caldera server
|
||||
@param arguments: Parser command line arguments
|
||||
"""
|
||||
|
||||
abilities = arguments.ability_ids
|
||||
|
||||
if arguments.all:
|
||||
abilities = [aid["ability_id"] for aid in calcontrol.list_abilities()]
|
||||
|
||||
for aid in abilities:
|
||||
for ability in calcontrol.get_ability(aid):
|
||||
calcontrol.pretty_print_ability(ability)
|
||||
|
||||
|
||||
def attack(calcontrol, arguments):
|
||||
""" Calling attack
|
||||
|
||||
@param calcontrol: Connection to the caldera server
|
||||
@param arguments: Parser command line arguments
|
||||
"""
|
||||
print("Running attack")
|
||||
print(arguments.paw)
|
||||
print(arguments.group)
|
||||
print(arguments.ability_id)
|
||||
calcontrol.attack(paw=arguments.paw, group=arguments.group, ability_id=arguments.ability_id)
|
||||
|
||||
|
||||
def create_parser():
|
||||
""" Creates the parser for the command line arguments"""
|
||||
|
||||
main_parser = argparse.ArgumentParser("Controls a Caldera server to attack other systems")
|
||||
subparsers = main_parser.add_subparsers(help="sub-commands")
|
||||
|
||||
# Sub parser for attacks
|
||||
parser_attack = subparsers.add_parser("attack", help="attack system")
|
||||
parser_attack.set_defaults(func=attack)
|
||||
parser_attack.add_argument("--paw", default="kickme", help="paw to attack and get specific results for")
|
||||
parser_attack.add_argument("--group", default="red", help="target group to attack")
|
||||
parser_attack.add_argument("--ability_id", default="bd527b63-9f9e-46e0-9816-b8434d2b8989",
|
||||
help="The ability to use for the attack")
|
||||
|
||||
# Sub parser to list abilities
|
||||
parser_abilities = subparsers.add_parser("abilities", help="abilities")
|
||||
# parser_abilities.add_argument("--abilityid", default=None, help="Id of the ability to list")
|
||||
parser_abilities.set_defaults(func=list_abilities)
|
||||
parser_abilities.add_argument("--ability_ids", default=[], nargs="+",
|
||||
help="The abilities to look up. One or more ids")
|
||||
parser_abilities.add_argument("--all", default=False, action="store_true",
|
||||
help="List all abilities")
|
||||
|
||||
# TODO: Add sub parser to list agents
|
||||
parser_agents = subparsers.add_parser("agents", help="agents")
|
||||
parser_agents.set_defaults(func=list_agents)
|
||||
|
||||
# For all parsers
|
||||
main_parser.add_argument("--caldera_url", help="caldera url, including port", default="http://192.168.178.97:8888/")
|
||||
main_parser.add_argument("--apikey", help="caldera api key", default="ADMIN123")
|
||||
|
||||
return main_parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = create_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
print(args.caldera_url)
|
||||
|
||||
caldera_control = CalderaControl(args.caldera_url, config=None, apikey=args.apikey)
|
||||
print("Caldera Control ready")
|
||||
|
||||
str(args.func(caldera_control, args))
|
@ -0,0 +1,232 @@
|
||||
|
||||
###
|
||||
# Caldera configuration
|
||||
caldera:
|
||||
###
|
||||
# API key for caldera. See caldera configuration. Default is ADMIN123
|
||||
apikey: ADMIN123
|
||||
|
||||
###
|
||||
# Attacks configuration
|
||||
attackers:
|
||||
###
|
||||
# Configuration for the first attacker. One should normally be enough
|
||||
attacker:
|
||||
|
||||
###
|
||||
# Defining VM controller settings for this machine
|
||||
vm_controller:
|
||||
###
|
||||
# Type of the VM controller, Options are "vagrant"
|
||||
type: vagrant
|
||||
###
|
||||
# # path where the vagrantfile is in
|
||||
vagrantfilepath: systems
|
||||
|
||||
###
|
||||
# Name of machine in Vagrantfile
|
||||
vm_name: attacker
|
||||
|
||||
###
|
||||
# machinepath is a path where the machine specific files and logs are stored. Relative to the Vagrantfile path
|
||||
# and will be mounted internally as /vagrant/<name>
|
||||
# If machinepath is not set PurpleDome will try "vm_name"
|
||||
machinepath: attacker1
|
||||
|
||||
###
|
||||
# OS of the VM guest. Options are so far "windows", "linux"
|
||||
os: linux
|
||||
|
||||
###
|
||||
# Do not destroy/create the machine: Set this to "yes".
|
||||
use_existing_machine: yes
|
||||
|
||||
###
|
||||
# List of targets
|
||||
targets:
|
||||
###
|
||||
# Specific target
|
||||
target1:
|
||||
vm_controller:
|
||||
type: vagrant
|
||||
vagrantfilepath: systems
|
||||
|
||||
###
|
||||
# simple switch if targets is used in attack simulation. Default is true. If set to false the machine will not be started
|
||||
active: no
|
||||
|
||||
vm_name: target1
|
||||
os: linux
|
||||
###
|
||||
# Targets need a unique PAW name for caldera
|
||||
paw: target1
|
||||
###
|
||||
# Targets need to be in a group for caldera
|
||||
group: red
|
||||
|
||||
machinepath: target1
|
||||
# Do not destroy/create the machine: Set this to "yes".
|
||||
use_existing_machine: yes
|
||||
|
||||
###
|
||||
# The folder all the implants will be installed into
|
||||
playground: /home/vagrant
|
||||
|
||||
# Sensors to run on this machine
|
||||
sensors:
|
||||
# - linux_idp
|
||||
|
||||
target2:
|
||||
#root: systems/target1
|
||||
vm_controller:
|
||||
type: vagrant
|
||||
vagrantfilepath: systems
|
||||
|
||||
###
|
||||
# simple switch if targets is used in attack simulation. Default is true. If set to false the machine will not be started
|
||||
active: yes
|
||||
|
||||
vm_name: target2
|
||||
os: windows
|
||||
paw: target2w
|
||||
group: red
|
||||
|
||||
machinepath: target2w
|
||||
|
||||
# Do not destroy/create the machine: Set this to "yes".
|
||||
use_existing_machine: yes
|
||||
###
|
||||
# Optional setting to activate force when halting the machine. Windows guests sometime get stuck
|
||||
halt_needs_force: yes
|
||||
|
||||
###
|
||||
# If SSH without vagrant support is used (Windows !) we need a user name (uppercase)
|
||||
ssh_user: PURPLEDOME
|
||||
|
||||
###
|
||||
# For non-vagrant ssh connections a ssh keyfile stored in the machinepath is required.
|
||||
ssh_keyfile: id_rsa.3
|
||||
|
||||
###
|
||||
# The folder all the implants will be installed into
|
||||
# Windows can only use default playground at the moment !
|
||||
# playground: C:\\Users\\PurpleDome
|
||||
|
||||
# Sensors to run on this machine
|
||||
sensors:
|
||||
- windows_idp
|
||||
|
||||
vulnerabilities:
|
||||
- weak_user_passwords
|
||||
- rdp_config_vul
|
||||
|
||||
|
||||
# Ubuntu 20.10 (Groovy)
|
||||
target3:
|
||||
vm_controller:
|
||||
type: vagrant
|
||||
vagrantfilepath: systems
|
||||
|
||||
###
|
||||
# simple switch if targets is used in attack simulation. Default is true. If set to false the machine will not be started
|
||||
active: no
|
||||
|
||||
vm_name: target3
|
||||
os: linux
|
||||
###
|
||||
# Targets need a unique PAW name for caldera
|
||||
paw: target3
|
||||
###
|
||||
# Targets need to be in a group for caldera
|
||||
group: red
|
||||
|
||||
machinepath: target3
|
||||
# Do not destroy/create the machine: Set this to "yes".
|
||||
use_existing_machine: no
|
||||
|
||||
###
|
||||
# The folder all the implants will be installed into
|
||||
playground: /home/vagrant
|
||||
|
||||
# Sensors to run on this machine
|
||||
sensors:
|
||||
- linux_idp
|
||||
|
||||
vulnerabilities:
|
||||
- sshd_config_vul
|
||||
- weak_user_passwords
|
||||
|
||||
###
|
||||
# General sensor config config
|
||||
sensors:
|
||||
###
|
||||
# Windows IDP plugin configuration
|
||||
windows_idp:
|
||||
###
|
||||
# Name of the dll to use. Must match AV version
|
||||
dll_name: aswidptestdll.dll
|
||||
|
||||
###
|
||||
# Folder where the IDP tool is located
|
||||
idp_tool_folder: C:\\capture
|
||||
|
||||
###
|
||||
# General attack config
|
||||
attacks:
|
||||
###
|
||||
# configure the seconds the system idles between the attacks. Makes it slower. But attack and defense logs will be simpler to match
|
||||
nap_time: 5
|
||||
|
||||
###
|
||||
# A list of caldera attacks to run against the targets.
|
||||
caldera_attacks:
|
||||
###
|
||||
# Linux specific attacks. A list of caldera ability IDs
|
||||
linux:
|
||||
- "bd527b63-9f9e-46e0-9816-b8434d2b8989"
|
||||
###
|
||||
# Windows specific attacks. A list of caldera ability IDs
|
||||
windows:
|
||||
- "bd527b63-9f9e-46e0-9816-b8434d2b8989"
|
||||
|
||||
###
|
||||
# Kali tool based attacks. Will result in kali commandline tools to be called. Currently supported are: "hydra"
|
||||
kali_attacks:
|
||||
###
|
||||
# Linux specific attacks, a list
|
||||
linux:
|
||||
- hydra
|
||||
- nmap
|
||||
###
|
||||
# Windows specific attacks, a list
|
||||
windows:
|
||||
- hydra
|
||||
- nmap
|
||||
|
||||
###
|
||||
# Configuration for the kali attack tools
|
||||
kali_conf:
|
||||
###
|
||||
# Hydra configuration
|
||||
hydra:
|
||||
###
|
||||
# A list of protocols to brute force against. Supported: "ssh"
|
||||
protocols:
|
||||
- ssh
|
||||
- rdp
|
||||
#- ftps
|
||||
###
|
||||
# A file containing potential user names
|
||||
userfile: users.txt
|
||||
###
|
||||
# A file containing potential passwords
|
||||
pwdfile: passwords.txt
|
||||
nmap:
|
||||
|
||||
|
||||
###
|
||||
# Settings for the results being harvested
|
||||
results:
|
||||
###
|
||||
# The directory the loot will be in
|
||||
loot_dir: loot
|
@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
""" The main tool to run experiments """
|
||||
|
||||
import argparse
|
||||
|
||||
from app.experimentcontrol import Experiment
|
||||
|
||||
|
||||
def explain(args): # pylint: disable=unused-argument
|
||||
""" Explain the tool"""
|
||||
|
||||
print("Please specify a command to execute. For a list see <help>")
|
||||
|
||||
|
||||
def run(args):
|
||||
""" Run experiments
|
||||
|
||||
@param args: arguments from the argparse parser
|
||||
"""
|
||||
Experiment(args.configfile)
|
||||
|
||||
|
||||
def create_parser():
|
||||
""" Creates the parser for the command line arguments"""
|
||||
parser = argparse.ArgumentParser("Controls an experiment on the configured systems")
|
||||
subparsers = parser.add_subparsers(help="sub-commands")
|
||||
|
||||
parser.set_defaults(func=explain)
|
||||
|
||||
# Sub parser for machine creation
|
||||
parser_run = subparsers.add_parser("run", help="run experiments")
|
||||
parser_run.set_defaults(func=run)
|
||||
parser_run.add_argument("--configfile", default="experiment.yaml", help="Config file to create from")
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
arguments = create_parser().parse_args()
|
||||
arguments.func(arguments)
|
@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Init the system
|
||||
|
||||
sudo apt-get -y install python3-venv
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
pip3 install -r requirements.txt
|
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
""" Demo program to set up and control the machines """
|
||||
|
||||
import argparse
|
||||
|
||||
import yaml
|
||||
|
||||
from app.calderacontrol import CalderaControl
|
||||
from app.machinecontrol import Machine
|
||||
|
||||
|
||||
def create_machines(arguments):
|
||||
"""
|
||||
|
||||
@param arguments: The arguments from argparse
|
||||
"""
|
||||
# TODO: Add argparse and make it flexible
|
||||
|
||||
with open(arguments.configfile) as fh:
|
||||
config = yaml.safe_load(fh)
|
||||
|
||||
target_ = Machine(config["targets"]["target1"])
|
||||
attacker_1 = Machine(config["attackers"]["attacker"])
|
||||
|
||||
print("Got them")
|
||||
|
||||
# TODO Automatically create all machines defined in config file
|
||||
|
||||
# attacker_1.destroy()
|
||||
print("destroyed")
|
||||
attacker_1.create(reboot=False)
|
||||
print("Attacker up")
|
||||
attacker_1.up()
|
||||
print(attacker_1.install_caldera_server())
|
||||
attacker_1.start_caldera_server()
|
||||
print("Attacker done")
|
||||
|
||||
target_.destroy()
|
||||
target_.set_caldera_server(attacker_1.getip())
|
||||
target_.install_caldera_service()
|
||||
target_.create()
|
||||
print("Target up")
|
||||
target_.up()
|
||||
target_.start_caldera_client()
|
||||
print("Target done")
|
||||
|
||||
print("Caldera server running at: http://{}:8888/".format(attacker_1.getip()))
|
||||
# target_.install_caldera_client(attacker_1.getip(), "target1elf")
|
||||
|
||||
|
||||
def download_caldera_client(arguments):
|
||||
""" Downloads the caldera client
|
||||
|
||||
@param arguments: The arguments from argparse
|
||||
"""
|
||||
|
||||
caldera_control = CalderaControl(args.ip, None)
|
||||
caldera_control.fetch_client(platform=arguments.platform,
|
||||
file=arguments.file,
|
||||
target_dir=arguments.target_dir,
|
||||
extension=".go")
|
||||
|
||||
|
||||
def create_parser():
|
||||
""" Creates the parser for the command line arguments"""
|
||||
|
||||
main_parser = argparse.ArgumentParser("Controls a Caldera server to attack other systems")
|
||||
subparsers = main_parser.add_subparsers(help="sub-commands")
|
||||
|
||||
# Sub parser for machine creation
|
||||
parser_create = subparsers.add_parser("create", help="create systems")
|
||||
parser_create.set_defaults(func=create_machines)
|
||||
parser_create.add_argument("--configfile", default="experiment.yaml", help="Config file to create from")
|
||||
|
||||
parser_download_caldera_client = subparsers.add_parser("fetch_client", help="download the caldera client")
|
||||
parser_download_caldera_client.set_defaults(func=download_caldera_client)
|
||||
parser_download_caldera_client.add_argument("--ip", default="192.168.178.189", help="Ip of Caldera to connect to")
|
||||
parser_download_caldera_client.add_argument("--platform", default="windows", help="platform to download the client for")
|
||||
parser_download_caldera_client.add_argument("--file", default="sandcat.go", help="The agent to download")
|
||||
parser_download_caldera_client.add_argument("--target_dir", default=".", help="The target dir to download the file to")
|
||||
|
||||
return main_parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = create_parser()
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
args.func(args)
|
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
""" Base class for Kali plugins """
|
||||
|
||||
from plugins.base.plugin_base import BasePlugin
|
||||
|
||||
|
||||
class KaliPlugin(BasePlugin):
|
||||
""" Class to execute a command on a kali system targeting another system """
|
||||
|
||||
# Boilerplate
|
||||
name = None
|
||||
description = None
|
||||
ttp = None
|
||||
references = None
|
||||
|
||||
required_files = []
|
||||
|
||||
# TODO: parse results
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.conf = {} # Plugin specific configuration
|
||||
self.sysconf = {} # System configuration. common for all plugins
|
||||
self.attack_logger = None
|
||||
|
||||
def teardown(self):
|
||||
""" Cleanup afterwards """
|
||||
pass # pylint: disable=unnecessary-pass
|
||||
|
||||
def run(self, targets, config):
|
||||
""" Run the command
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
@param config: dict with command specific configuration
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __execute__(self, targets, config):
|
||||
""" Execute the plugin. This is called by the code
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
@param config: dict with command specific configuration
|
||||
"""
|
||||
|
||||
self.attack_logger.start_kali_attack(self.machine_plugin.config.vmname(), targets, self.name, ttp=self.get_ttp())
|
||||
self.setup()
|
||||
res = self.run(targets, config)
|
||||
self.teardown()
|
||||
self.attack_logger.stop_kali_attack(self.machine_plugin.config.vmname(), targets, self.name, ttp=self.get_ttp())
|
||||
return res
|
||||
|
||||
def command(self, targets, config):
|
||||
""" Generate command
|
||||
|
||||
@param targets: A list of targets, ip addresses will do
|
||||
@param config: dict with command specific configuration
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def __set_logger__(self, attack_logger):
|
||||
""" Set the attack logger for this machine """
|
||||
self.attack_logger = attack_logger
|
||||
|
||||
def get_ttp(self):
|
||||
""" Returns the ttp of the plugin, please set in boilerplate """
|
||||
if self.ttp:
|
||||
return self.ttp
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def get_references(self):
|
||||
""" Returns the references of the plugin, please set in boilerplate """
|
||||
if self.references:
|
||||
return self.references
|
||||
|
||||
raise NotImplementedError
|
@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" Base class for classes to control any kind of machine: vm, bare metal, cloudified """
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from app.config import MachineConfig
|
||||
from app.interface_sfx import CommandlineColors
|
||||
from plugins.base.plugin_base import BasePlugin
|
||||
|
||||
|
||||
class MachineStates(Enum):
|
||||
""" Potential machine states """
|
||||
# TODO: maybe move state handling functions in here like "is running", "is broken"
|
||||
RUNNING = 1
|
||||
NOT_CREATED = 2
|
||||
POWEROFF = 3
|
||||
ABORTED = 4
|
||||
SAVED = 5
|
||||
STOPPED = 6
|
||||
FROZEN = 7
|
||||
SHUTOFF = 8
|
||||
|
||||
|
||||
class MachineryPlugin(BasePlugin):
|
||||
""" Class to control virtual machines, vagrant, .... """
|
||||
|
||||
# Boilerplate
|
||||
name = None
|
||||
|
||||
required_files = []
|
||||
|
||||
###############
|
||||
# This is stuff you might want to implement
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.connection = None # Connection
|
||||
self.config = None
|
||||
|
||||
def process_config(self, config: MachineConfig):
|
||||
""" Machine specific processing of configuration
|
||||
|
||||
@param config: configuration to do additional processing on
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def create(self, reboot=True):
|
||||
""" Create a machine
|
||||
|
||||
@param reboot: Optionally reboot the machine after creation
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def up(self): # pylint: disable=invalid-name
|
||||
""" Start a machine, create it if it does not exist """
|
||||
raise NotImplementedError
|
||||
|
||||
def halt(self):
|
||||
""" Halt a machine """
|
||||
raise NotImplementedError
|
||||
|
||||
def destroy(self):
|
||||
""" Destroy a machine """
|
||||
raise NotImplementedError
|
||||
|
||||
def connect(self):
|
||||
""" Connect to a machine """
|
||||
raise NotImplementedError
|
||||
|
||||
def remote_run(self, cmd, disown=False):
|
||||
""" Connects to the machine and runs a command there
|
||||
|
||||
@param cmd: command to run int he machine's shell
|
||||
@param disown: Send the connection into background
|
||||
"""
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def disconnect(self):
|
||||
""" Disconnect from a machine """
|
||||
raise NotImplementedError
|
||||
|
||||
def put(self, src, dst):
|
||||
""" Send a file to a machine
|
||||
|
||||
@param src: source dir
|
||||
@param dst: destination
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get(self, src, dst):
|
||||
""" Get a file to a machine
|
||||
|
||||
@param src: source dir
|
||||
@param dst: destination
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def is_running(self):
|
||||
""" Returns if the machine is running """
|
||||
return self.get_state() == MachineStates.RUNNING
|
||||
|
||||
def get_state(self):
|
||||
""" Get detailed state of a machine """
|
||||
raise NotImplementedError
|
||||
|
||||
def get_ip(self):
|
||||
""" Return the IP of the machine. If there are several it should be the one accepting ssh or similar. If a resolver is running, a domain is also ok. """
|
||||
raise NotImplementedError
|
||||
|
||||
def get_playground(self):
|
||||
""" path where all the attack tools will be copied to on a client. Your specific machine plugin can overwrite it. """
|
||||
|
||||
return self.config.get_playground()
|
||||
|
||||
def get_vm_name(self):
|
||||
""" Get the name of the machine """
|
||||
|
||||
return self.config.vmname()
|
||||
|
||||
###############
|
||||
# This is the interface from the main code to the plugin system. Do not touch
|
||||
def __call_halt__(self):
|
||||
""" Wrapper around halt """
|
||||
print(f"{CommandlineColors.OKBLUE}Stopping machine: {self.config.vmname()} {CommandlineColors.ENDC}")
|
||||
self.halt()
|
||||
print(f"{CommandlineColors.OKGREEN}Machine stopped: {self.config.vmname()}{CommandlineColors.ENDC}")
|
||||
|
||||
def __call_process_config__(self, config: MachineConfig):
|
||||
""" Wrapper around process_config """
|
||||
|
||||
# print("===========> Processing config")
|
||||
self.config = config
|
||||
self.process_config(config)
|
||||
|
||||
def __call_remote_run__(self, cmd, disown=False):
|
||||
""" Simplifies connect and run
|
||||
|
||||
@param cmd: Command to run as shell command
|
||||
@param disown: run in background
|
||||
"""
|
||||
|
||||
return self.remote_run(cmd, disown)
|
||||
|
||||
def __call_disconnect__(self):
|
||||
""" Command connection dis-connect """
|
||||
|
||||
self.disconnect()
|
||||
|
||||
def __call_connect__(self):
|
||||
""" command connection. establish it """
|
||||
|
||||
return self.connect()
|
||||
|
||||
def __call_up__(self):
|
||||
""" Starts a VM. Creates it if not already created """
|
||||
|
||||
self.up()
|
||||
|
||||
def __call_create__(self, reboot=True):
|
||||
""" Create a VM
|
||||
|
||||
@param reboot: Reboot the VM during installation. Required if you want to install software
|
||||
"""
|
||||
|
||||
self.create(reboot)
|
||||
|
||||
def __call_destroy__(self):
|
||||
""" Destroys the current machine """
|
||||
|
||||
self.destroy()
|
@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
""" Base class for all plugin types """
|
||||
|
||||
import os
|
||||
# from shutil import copy
|
||||
|
||||
|
||||
class BasePlugin():
|
||||
""" Base class for plugins """
|
||||
|
||||
required_files = None # a list of files shipped with the plugin to be installed
|
||||
name = None # The name of the plugin
|
||||
description = None # The description of this plugin
|
||||
|
||||
def __init__(self):
|
||||
# self.machine = None
|
||||
self.plugin_path = None
|
||||
self.machine_plugin = None
|
||||
self.sysconf = {}
|
||||
|
||||
def setup(self):
|
||||
""" Prepare everything for the plugin """
|
||||
|
||||
for a_file in self.required_files:
|
||||
src = os.path.join(os.path.dirname(self.plugin_path), a_file)
|
||||
print(src)
|
||||
self.copy_to_machine(src)
|
||||
|
||||
def set_machine_plugin(self, machine_plugin):
|
||||
""" Set the machine plugin class to communicate with
|
||||
|
||||
@param machine_plugin: Machine plugin to communicate with
|
||||
"""
|
||||
|
||||
self.machine_plugin = machine_plugin
|
||||
|
||||
def set_sysconf(self, config):
|
||||
""" Set system config
|
||||
|
||||
@param config: A dict with system configuration relevant for all plugins
|
||||
"""
|
||||
|
||||
self.sysconf["abs_machinepath_internal"] = config["abs_machinepath_internal"]
|
||||
self.sysconf["abs_machinepath_external"] = config["abs_machinepath_external"]
|
||||
|
||||
def copy_to_machine(self, filename):
|
||||
""" Copies a file shipped with the plugin to the machine share folder
|
||||
|
||||
@param filename: File from the plugin folder to copy to the machine share.
|
||||
"""
|
||||
|
||||
self.machine_plugin.put(filename, self.machine_plugin.get_playground())
|
||||
|
||||
# plugin_folder = os.path.dirname(os.path.realpath(self.plugin_path))
|
||||
# src = os.path.join(plugin_folder, filename)
|
||||
|
||||
# if os.path.commonprefix((os.path.realpath(src), plugin_folder)) != plugin_folder:
|
||||
# raise PluginError
|
||||
|
||||
# copy(src, self.sysconf["abs_machinepath_external"])
|
||||
|
||||
def run_cmd(self, command, warn=True, disown=False):
|
||||
""" Execute a command on the vm using the connection
|
||||
|
||||
@param command: Command to execute
|
||||
@param disown: Run in background
|
||||
"""
|
||||
|
||||
print(f" Plugin running command {command}")
|
||||
|
||||
res = self.machine_plugin.__call_remote_run__(command, disown=disown)
|
||||
return res
|
||||
|
||||
def get_name(self):
|
||||
""" Returns the name of the plugin, please set in boilerplate """
|
||||
if self.name:
|
||||
return self.name
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def get_description(self):
|
||||
""" Returns the description of the plugin, please set in boilerplate """
|
||||
if self.description:
|
||||
return self.description
|
||||
|
||||
raise NotImplementedError
|
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
""" A base plugin class for sensors. Anything installed on the target to collect system information and identify the attack """
|
||||
|
||||
import os
|
||||
from plugins.base.plugin_base import BasePlugin
|
||||
|
||||
|
||||
class SensorPlugin(BasePlugin):
|
||||
""" A sensor will be running on the target machine and monitor attacks. To remote control those sensors
|
||||
there are sensor plugins. This is the base class for them
|
||||
|
||||
"""
|
||||
|
||||
# Boilerplate
|
||||
name = None
|
||||
|
||||
required_files = []
|
||||
|
||||
def __init__(self):
|
||||
super().__init__() # pylint:disable=useless-super-delegation
|
||||
self.debugit = False
|
||||
# self.machine = None
|
||||
|
||||
def set_sysconf(self, config):
|
||||
""" Set system config
|
||||
|
||||
@param config: A dict with system configuration relevant for all plugins
|
||||
"""
|
||||
|
||||
super().set_sysconf(config)
|
||||
self.sysconf["sensor_specific"] = config["sensor_specific"]
|
||||
|
||||
def prime(self):
|
||||
""" prime sets hard core configs in the target. You can use it to call everything that permanently alters the OS by settings.
|
||||
If your prime function returns True the machine will be rebooted after prime-ing it. This is very likely what you want. Only use prime if install is not sufficient.
|
||||
"""
|
||||
|
||||
return False
|
||||
|
||||
def install_command(self):
|
||||
""" Generate the install command. Put everything you need that does not require a reboot in here. If you want to hard core alter the OS of the target, use the prime method """
|
||||
raise NotImplementedError
|
||||
|
||||
def install(self):
|
||||
""" Install the sensor. Executed on the target. Take the sensor from the share and (maybe) copy it to its destination. Do some setup
|
||||
"""
|
||||
|
||||
cmd = self.install_command()
|
||||
if cmd:
|
||||
self.machine_plugin.__call_remote_run__(cmd)
|
||||
|
||||
def start_command(self):
|
||||
""" Generate the start command """
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def start(self, disown=None):
|
||||
""" Start the sensor. The connection to the client is disowned here. = Sent to background. This keeps the process running.
|
||||
|
||||
@param disown: Send async into background
|
||||
"""
|
||||
|
||||
if disown is None:
|
||||
disown = not self.debugit
|
||||
cmd = self.start_command()
|
||||
if cmd:
|
||||
# self.run_cmd(cmd, disown=not self.debugit)
|
||||
self.machine_plugin.__call_remote_run__(cmd, disown=disown)
|
||||
|
||||
def stop_command(self):
|
||||
""" Generate the stop command """
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
""" Stop the sensor """
|
||||
cmd = self.stop_command()
|
||||
if cmd:
|
||||
# self.run_cmd(cmd)
|
||||
self.machine_plugin.__call_remote_run__(cmd)
|
||||
|
||||
def __call_collect__(self, machine_path):
|
||||
""" Generate the data collect command
|
||||
|
||||
@param machine_path: Machine specific path to collect data into
|
||||
"""
|
||||
|
||||
path = os.path.join(machine_path, "sensors", self.name)
|
||||
os.makedirs(path)
|
||||
self.collect(path)
|
||||
|
||||
def collect_command(self, path):
|
||||
""" Generate the data collect command
|
||||
|
||||
@param path: Path to put the data into
|
||||
"""
|
||||
|
||||
def collect(self, path):
|
||||
""" Collect data from sensor. Copy it from sensor collection dir on target OS to the share
|
||||
|
||||
@param path: The path to copy the data into
|
||||
"""
|
||||
raise NotImplementedError
|
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
""" This is a specific plugin type that installs a vulnerability into a VM. This can be a vulnerable application or a configuration setting """
|
||||
|
||||
from plugins.base.plugin_base import BasePlugin
|
||||
|
||||
|
||||
class VulnerabilityPlugin(BasePlugin):
|
||||
""" A plugin that installs a vulnerable application or does vulnerable configuration changes on the target VM
|
||||
"""
|
||||
|
||||
# Boilerplate
|
||||
name = None
|
||||
description = None
|
||||
ttp = None
|
||||
references = None
|
||||
|
||||
required_files = []
|
||||
|
||||
def __init__(self):
|
||||
super().__init__() # pylint:disable=useless-super-delegation
|
||||
self.debugit = False
|
||||
|
||||
def install(self, machine_plugin=None):
|
||||
""" This is setting up everything up to the point where the machine itself would be modified. But system
|
||||
modification is done by start
|
||||
|
||||
@param machine_plugin: Optional: you can already set the machine to use
|
||||
"""
|
||||
|
||||
if machine_plugin:
|
||||
self.machine_plugin = machine_plugin
|
||||
|
||||
def start(self):
|
||||
""" Modifying the target machine and add the vulnerability """
|
||||
|
||||
# It is ok if install is empty. But this function here is the core. So implement it !
|
||||
raise NotImplementedError
|
||||
|
||||
def stop(self):
|
||||
""" Modifying the target machine and remove the vulnerability """
|
||||
|
||||
# Must be implemented. If you want to leave a mess create an empty function and be honest :-)
|
||||
raise NotImplementedError
|
||||
|
||||
def get_ttp(self):
|
||||
""" Returns the ttp of the plugin, please set in boilerplate """
|
||||
if self.ttp:
|
||||
return self.ttp
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
def get_references(self):
|
||||
""" Returns the references of the plugin, please set in boilerplate """
|
||||
if self.references:
|
||||
return self.references
|
||||
|
||||
raise NotImplementedError
|
@ -0,0 +1,13 @@
|
||||
python-vagrant==0.5.15
|
||||
fabric==2.6.0
|
||||
requests==2.25.1
|
||||
simplejson==3.17.2
|
||||
tox==3.22.0
|
||||
sphinx-argparse==0.2.5
|
||||
sphinxcontrib-autoyaml==0.6.1
|
||||
sphinx-pyreverse==0.0.13
|
||||
coverage==5.4
|
||||
PyYAML==5.4.1
|
||||
straight.plugin==1.5.0
|
||||
sphinxcontrib.asciinema==0.3.1
|
||||
paramiko
|
@ -0,0 +1,13 @@
|
||||
python-vagrant>=0.5.15
|
||||
fabric>=2.6.0
|
||||
requests>=2.25.1
|
||||
simplejson>=3.17.2
|
||||
tox>=3.22.0
|
||||
sphinx-argparse>=0.2.5
|
||||
sphinxcontrib-autoyaml>=0.6.1
|
||||
sphinx-pyreverse>=0.0.13
|
||||
coverage>=5.4
|
||||
PyYAML>=5.4.1
|
||||
straight.plugin>=1.5.0
|
||||
sphinxcontrib.asciinema>=0.3.1
|
||||
paramiko
|
Loading…
Reference in New Issue