Skip to content

Commit ea9b0a8

Browse files
karlkrJaanusKaap
authored andcommitted
Added TuoniPayload, TuoniPayloadTemplate classes
1 parent 3fbf872 commit ea9b0a8

7 files changed

Lines changed: 350 additions & 5 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
__pycache__
2+
.python-version
3+
.venv
4+
dist/

tuoni/TuoniCommandPlugin.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import random
2+
from tuoni.TuoniCommandTemplate import *
3+
4+
5+
class TuoniCommandPlugin:
6+
"""
7+
A class that provides data and functionality for a command plugin.
8+
9+
Attributes:
10+
name (str): The name of the command plugin.
11+
vendor (str): The vendor of the command plugin.
12+
description (str): A description of the command plugin.
13+
plugin_id (str): The unique identifier of the command plugin.
14+
commands (list[TuoniCommandTemplate]): A list of command templates associated with the plugin.
15+
16+
"""
17+
18+
def __init__(self, conf, c2):
19+
"""
20+
Constructor for the command plugin class.
21+
22+
Args:
23+
conf (dict): Data from the server.
24+
c2 (TuoniC2): The related server object that manages communication.
25+
"""
26+
self.name = conf["info"]["name"]
27+
self.vendor = conf["info"]["vendor"]
28+
self.description = conf["info"]["description"]
29+
self.plugin_id = conf["identifier"]["id"]
30+
self.c2 = c2
31+
self.commands = []
32+
for command_name in conf["commands"]:
33+
self.commands.append(TuoniCommandTemplate(
34+
conf["commands"][command_name], c2))

tuoni/TuoniCommandTemplate.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import random
2+
from tuoni.TuoniListener import *
3+
4+
5+
class TuoniCommandTemplate:
6+
"""
7+
A class that provides data and functionality for a command template.
8+
9+
Attributes:
10+
id (str): The unique identifier of the command template.
11+
name (str): The name of the command template.
12+
plugin_id (str): The unique identifier of the command plugin.
13+
scope (str): The scope of the command template.
14+
qualifiedName (str): The qualified name of the command template.
15+
fullyQualifiedName (str): The fully qualified name of the command template.
16+
description (str): A description of the command template.
17+
conf_schema (dict): The configuration schema for the command template.
18+
conf_examples (dict): Examples of valid configurations for the command template.
19+
"""
20+
21+
def __init__(self, conf, c2):
22+
"""
23+
Constructor for the command template class.
24+
25+
Args:
26+
conf (dict): Data from the server.
27+
c2 (TuoniC2): The related server object that manages communication.
28+
"""
29+
self.id = conf["id"]
30+
self.name = conf["name"]
31+
self.plugin_id = conf["pluginId"]
32+
self.scope = conf["scope"]
33+
self.qualifiedName = conf["qualifiedName"]
34+
self.fullyQualifiedName = conf["fullyQualifiedName"]
35+
self.description = conf["description"]
36+
self.conf_schema = conf["configurationSchema"]
37+
self.conf_examples = {}
38+
if "defaultConfiguration" in conf:
39+
self.conf_examples["default"] = conf["defaultConfiguration"]
40+
if "exampleConfigurations" in conf:
41+
for example in conf["exampleConfigurations"]:
42+
self.conf_examples[example["name"]] = example["configuration"]
43+
self.c2 = c2
44+
45+
def get_default_conf(self):
46+
"""
47+
Retrieve the default configuration for the command template.
48+
49+
Returns:
50+
dict: The default configuration settings, or an empty dictionary if none are defined.
51+
"""
52+
if "default" in self.conf_examples:
53+
return self.conf_examples["default"]
54+
return {} # Might change but let's say for now that if no "default" conf then empty conf is same
55+
56+
def get_minimal_conf(self):
57+
"""
58+
Retrieve the minimal configuration for the command template.
59+
60+
Returns:
61+
dict: The minimal configuration settings, or an empty dictionary if none are defined.
62+
"""
63+
return self.get_default_conf()

tuoni/TuoniListener.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class TuoniListener:
1212
plugin (str): The plugin associated with the listener.
1313
configuration (dict): The configuration settings for the listener.
1414
"""
15-
15+
1616
def __init__(self, conf, c2):
1717
"""
1818
Constructor for the listener class.
@@ -31,6 +31,8 @@ def _load_conf(self, conf):
3131
self.status = conf["status"]
3232
self.plugin = conf["plugin"]
3333
self.configuration = conf["configuration"]
34+
self.configuration_files = conf.get("configurationFiles", {})
35+
3436

3537
def stop(self):
3638
"""
@@ -68,14 +70,15 @@ def reload(self):
6870
data = self.c2.request_get(f"/api/v1/listeners/{self.listener_id}")
6971
self._load_conf(data)
7072

71-
def update(self,):
73+
def update(self):
7274
"""
7375
Update listener on the server.
7476
"""
7577
if self.listener_id is None:
7678
raise ExceptionTuoniDeleted("")
7779
req = {
7880
"configuration": self.configuration,
81+
"configurationFiles": self.configuration_files,
7982
"name": self.name
8083
}
8184
self.c2.request_patch(f"/api/v1/listeners/{self.listener_id}", req)

tuoni/TuoniPayload.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
class TuoniPayload:
2+
"""
3+
A class that provides data and functionality for a sent payload.
4+
5+
Attributes:
6+
payload_id (int): The unique identifier of the payload.
7+
template_id (str): The unique identifier of the payload template used to create the payload.
8+
template_name (str): The name of the payload template used to create the payload.
9+
configuration (dict): The configuration settings for the payload.
10+
listeners (list): The listeners associated with the payload.
11+
os (str): The operating system for the payload.
12+
architecture (str): The architecture for the payload.
13+
status (str): The status of the payload.
14+
encrypted_communication (bool): Indicates if the communication is encrypted.
15+
16+
Examples:
17+
Create a payload from a conf dict and save it to disk:
18+
19+
>>> payload = TuoniPayload(
20+
... conf={
21+
... "templateId": "shelldot.payload.windows-x64",
22+
... "configuration": {"type": "executable"}
23+
... },
24+
... c2=tuoni_c2
25+
... )
26+
>>> payload.create(listener_id=1)
27+
>>> print(f"Created payload with ID: {payload.payload_id}")
28+
>>>
29+
>>> # Download and save the payload binary
30+
>>> data = payload.download()
31+
>>> with open("agent.exe", "wb") as f:
32+
... f.write(data)
33+
>>>
34+
>>> # Delete the payload when done
35+
>>> payload.delete()
36+
"""
37+
38+
def __init__(self, conf=None, c2=None):
39+
"""
40+
Constructor for the payload class.
41+
42+
Args:
43+
conf (dict): A dict to initialize the payload from. When passed a server
44+
response all fields are populated. When passed a user-provided creation
45+
dict, only ``templateId`` and ``configuration`` are required; ``name``,
46+
``encryptedCommunication`` (default ``True``), and
47+
``configurationFiles`` (default ``[]``) are optional.
48+
c2 (TuoniC2): The related server object that manages communication.
49+
"""
50+
self.payload_id = None
51+
self.name = None
52+
self.template_id = None
53+
self.configuration = {}
54+
self.configuration_files = []
55+
self.encrypted_communication = True
56+
self.c2 = c2
57+
if conf is not None:
58+
self._load_conf(conf)
59+
60+
def _load_conf(self, conf):
61+
self.payload_id = conf.get("id", None)
62+
self.name = conf.get("name", None)
63+
self.template_id = conf.get("templateId", self.template_id)
64+
self.configuration = conf.get("configuration", self.configuration)
65+
self.listeners = conf.get("listeners", [])
66+
self.os = conf.get("os", None)
67+
self.architecture = conf.get("architecture", None)
68+
self.status = conf.get("status", None)
69+
self.encrypted_communication = conf.get("encryptedCommunication", self.encrypted_communication)
70+
self.configuration_files = conf.get("configurationFiles", self.configuration_files)
71+
72+
def load(self, id):
73+
"""
74+
Load the payload data from the C2 server using the payload ID.
75+
76+
Args:
77+
id (int): The unique identifier of the payload to load.
78+
"""
79+
data = self.c2.request_get(f"/api/v1/payloads/{id}")
80+
self._load_conf(data)
81+
82+
def create(self, listener_id):
83+
"""
84+
Create the payload on the C2 server.
85+
86+
Args:
87+
listener_id (int): The ID of the listener to associate with this payload.
88+
89+
Examples:
90+
>>> payload = TuoniPayload(
91+
... conf={
92+
... "templateId": "shelldot.payload.windows-x64",
93+
... "configuration": {"type": "executable"}
94+
... },
95+
... c2=tuoni_c2
96+
... )
97+
>>> payload.create(listener_id=1)
98+
"""
99+
if self.payload_id is not None:
100+
raise Exception("Payload already created.")
101+
data = self.c2.request_post("/api/v1/payloads", {
102+
"payloadTemplateId": self.template_id,
103+
"name": self.name,
104+
"configuration": self.configuration,
105+
"configurationFiles": self.configuration_files,
106+
"listenerId": listener_id,
107+
"encrypted": self.encrypted_communication
108+
})
109+
self._load_conf(data)
110+
111+
def delete(self):
112+
"""
113+
Delete the payload from the C2 server.
114+
"""
115+
if self.payload_id is None:
116+
raise Exception("Payload not created.")
117+
self.c2.request_delete(f"/api/v1/payloads/{self.payload_id}")
118+
self.payload_id = None
119+
120+
def update(self):
121+
"""
122+
Update the payload configuration on the C2 server.
123+
124+
Args:
125+
new_configuration (dict): The new configuration settings for the payload.
126+
"""
127+
if self.payload_id is None:
128+
raise Exception("Payload not created.")
129+
data = self.c2.request_patch(f"/api/v1/payloads/{self.payload_id}", {
130+
"name": self.name
131+
})
132+
self._load_conf(data)
133+
134+
def download(self):
135+
"""
136+
Download the payload from the C2 server.
137+
138+
Returns:
139+
bytes: The binary data of the downloaded payload.
140+
"""
141+
if self.payload_id is None:
142+
raise Exception("Payload not created.")
143+
return self.c2.request_get(f"/api/v1/payloads/{self.payload_id}/download", result_as_json=False, result_as_bytes=True)

tuoni/TuoniPayloadPlugin.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import random
22
from tuoni.TuoniListener import *
3+
from tuoni.TuoniPayloadTemplate import *
34

45
class TuoniPayloadPlugin:
56
"""
@@ -11,8 +12,34 @@ class TuoniPayloadPlugin:
1112
description (str): A description of the payload plugin.
1213
plugin_id (str): The unique identifier of the payload plugin.
1314
templates (list): A list of available payload templates.
15+
16+
Examples:
17+
Iterate over payload plugins, inspect their templates, and create a payload
18+
for each type defined in the template's configuration schema:
19+
20+
>>> payload_plugins = tuoni_c2.load_payload_plugins()
21+
>>> for plugin in payload_plugins.values():
22+
... print(f"Plugin: {plugin.name} ({plugin.plugin_id})")
23+
... for template in plugin.templates:
24+
... print(f" Template: {template.id}")
25+
... type_values = (
26+
... template.conf_schema
27+
... .get("properties", {})
28+
... .get("type", {})
29+
... .get("enum", [])
30+
... )
31+
... for type_value in type_values:
32+
... payload = TuoniPayload(
33+
... conf={
34+
... "templateId": template.id,
35+
... "configuration": {"type": type_value}
36+
... },
37+
... c2=tuoni_c2
38+
... )
39+
... payload.create(listener_id=1)
40+
... print(f" Created payload ID: {payload.payload_id}")
1441
"""
15-
42+
1643
def __init__(self, conf, c2):
1744
"""
1845
Constructor for the payload plugin class.
@@ -26,7 +53,7 @@ def __init__(self, conf, c2):
2653
self.description = conf["info"]["description"]
2754
self.plugin_id = conf["identifier"]["id"]
2855
self.templates = []
29-
for payloadTemplate in conf["payloads"]:
30-
self.templates.append(payloadTemplate)
56+
for payloadTemplateName, payloadTemplate in conf["payloads"].items():
57+
self.templates.append(TuoniPayloadTemplate(payloadTemplate, c2))
3158
self.c2 = c2
3259

0 commit comments

Comments
 (0)