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