Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f68b1e8
initial structure to add vnc executor
thorinaboenke Aug 2, 2024
da905d8
add command parameters and model validation
thorinaboenke Aug 5, 2024
4b2bc74
first set of vnc commands in vncexecutor
thorinaboenke Aug 5, 2024
38175a4
add vnc session store
thorinaboenke Oct 17, 2024
083b3fc
add expect screen command
thorinaboenke Nov 8, 2024
326a58b
Merge remote-tracking branch 'origin/development' into feature_62_vnc…
thorinaboenke Nov 8, 2024
24f4491
add vnc command to loop
thorinaboenke Nov 8, 2024
c35c208
merge origin/development
thorinaboenke Feb 7, 2025
de9b43a
add vncdotool to dependencies
thorinaboenke Feb 7, 2025
7483de4
refactor
thorinaboenke Feb 7, 2025
0f7260c
close vnc connection
thorinaboenke Feb 7, 2025
833a49d
handle failed connection
thorinaboenke Feb 9, 2025
32f823a
loop variable substitution
thorinaboenke Feb 9, 2025
7c6df36
deal with no connection established
thorinaboenke Feb 9, 2025
98164c2
check for connection before returning client
thorinaboenke Feb 10, 2025
014de51
use placeholders in break condition
thorinaboenke Feb 10, 2025
d123f7a
skip variable subsitution for break_if
thorinaboenke Feb 10, 2025
984084f
return Result_Stdout on sucessfull vnc connection
thorinaboenke Feb 10, 2025
3cb8f3b
add docs
thorinaboenke Feb 10, 2025
f09d3a3
add docs
thorinaboenke Feb 11, 2025
0a9a3d3
Merge branch 'development' into feature_62_vnc_executor
thorinaboenke Feb 11, 2025
f3397c7
add vnc executor unit tests
thorinaboenke Feb 11, 2025
67821aa
add test for break_if
thorinaboenke Feb 11, 2025
9a09e98
use fixtures
thorinaboenke Feb 11, 2025
51d4fa8
add pytest-mocker
thorinaboenke Feb 11, 2025
d15fba3
add docs how to integrate new commands
thorinaboenke Feb 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions docs/source/developing/integration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
.. _integration:

=========================================
Integrating AttackMate in a Python Script
=========================================

It is possible to integrate AttackMate into a Python script for automation and custom attack scenarios.
This feature is under development and may change in the future. Bug reports are welcome.

Installation
============

Before using AttackMate in a script, ensure it is installed.

Example Script
==============

Below is an example of how to integrate AttackMate into a Python script.
Configs and Variable Store can be passed as a dictionary or as a Config object.
Commands can be created with the Command.create() method and passed to the run_command() method.

::

from attackmate.attackmate import AttackMate
from attackmate.command import Command
from attackmate.variablestore import VariableStore
from attackmate.schemas.config import Config

def main():
### Optional: define config manually
config = Config(
sliver_config={"config_file": "path/to/config/file"},
msf_config={"password": "your_password", "ssl": True, "port": 55553},
cmd_config={"loop_sleep": 10}
)

### Optional: varstore can be passed as a dictionary
varstore = {"TEST": "test"}

attackmate = AttackMate(config=config, varstore=varstore)

command1 = Command.create(type="sleep", cmd="sleep", seconds="1")
command2 = Command.create(type="debug", cmd="$TEST", varstore=True)

result1 = attackmate.run_command(command1)
result2 = attackmate.run_command(command2)

print(result1)
print(result2)

if __name__ == "__main__":
main()

Running the Script
==================

To execute the script, save it as `attackmate_script.py` and run:

::

$ python attackmate_script.py

If AttackMate is configured correctly, it will execute the commands and print the results.

.. note::
Reguler Commands return a Result object.
Commands that run in Background Mode do not return a Result object.

Understanding the Result Object
===============================

When executing a command with AttackMate, the result is returned as an instance of the `Result` class. This object contains the standard output (`stdout`) and the return code (`returncode`) of the executed command.
Commands that run in the Background return Result(None,None)

Attributes
----------

- **stdout (str)**: The standard output of the executed command.
- **returncode (int)**: The return code indicating the success or failure of the command.

Example Usage
-------------

The `Result` object can be used to check the output and status of a command execution:

::

result = attackmate.run_command(command)

print("Command Output:", result.stdout)
print("Return Code:", result.returncode)

Handling Results
----------------

The return code can be used to determine if the command was successful:

::

if result.returncode == 0:
print("Command executed successfully.")
else:
print(f"Command failed with return code {result.returncode}")
139 changes: 139 additions & 0 deletions docs/source/playbook/commands/vnc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
===
vnc
===

Execute commands on a remote server via VNC. Uses the `vncdotool <https://github.com/sibson/vncdotool>` library.

.. note::

This command caches all the settings so
that they only need to be defined once.

.. code-block:: yaml

vars:
$SERVER_ADDRESS: 192.42.0.254
$PORT: 5900
$DISPLAY: 1
$PASSWORD: password

commands:
# creates new vnc-connection and session. If creates_session is omitted, a session named "default" is created
- type: vnc
cmd: capture
filename: screenshot.png
hostname: $SERVER_ADDRESS
port: $PORT
display: $DISPLAY
password: $PASSWORD
creates_session: my_session

# reuses existing session "my_session
- type: vnc
cmd: type
input: "echo hello world"
session: my_session

# closes existing session "my_session" and deletes from session store
- type: vnc
cmd: close
session: my_session


.. confval:: cmd

One of ``type``, ``key``, ``capture``, ``move``, ``expectscreen``, ``close``

:type: str

.. confval:: hostname

This option sets the hostname or ip-address of the
remote ssh-server.

:type: str

.. confval:: port

Port to connect to on the remote host.

:type: int
:default: ``5900``

.. confval:: display

Specifies the display to use on the remote machine.

:type: str
:default: ``1``

.. confval:: password

Specifies the password to use

:type: str

.. confval:: filename

Path where a screeshot ``capture`` should be saved, or file to compare a screenshot with ``expext screen``.

:type: str

.. confval:: maxrms

Metric to compare a screen with ``expext screen``. Only continue if the sceen matches.
Maximum RMS (root mean square) error allowed (set a small value for near-exact match)

:type: float

.. confval:: input

text to type with the command ``type``

:type: str

.. confval:: key

key to press with the command ``key``

:type: str


.. confval:: x

x position ot move the cursor to with the command ``move``

:type: int

.. confval:: y

y position ot move the cursor to ``move``

:type: int


.. confval:: creates_session

A session name that identifies the session that is created when
executing this command. This session name can be used by using the
option ``session`` in another vnc-command.
If no ``creates_session`` name is defined and no previous session is used a session named ``default`` is created.

:type: str

.. confval:: session

Reuse an existing session. This setting works only if another
vnc-command was executed with the command-option "creates_session"

:type: str


.. note::

The vnc connection needs to be closed with the command ``close`` explicitely, otherwise attackmate will keep running.





4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ dependencies = [
"tabulate",
"python-magic",
"httpx",
"httpx[http2]"
"httpx[http2]",
"vncdotool",
"pytest-mock"
]
dynamic = ["version"]

Expand Down
2 changes: 2 additions & 0 deletions src/attackmate/executors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .father.fatherexecutor import FatherExecutor
from .http.webservexecutor import WebServExecutor
from .http.httpclientexecutor import HttpClientExecutor
from .vnc.vncexecutor import VncExecutor
from .common.setvarexecutor import SetVarExecutor
from .common.sleepexecutor import SleepExecutor
from .common.tempfileexecutor import TempfileExecutor
Expand Down Expand Up @@ -37,6 +38,7 @@
'DebugExecutor',
'IncludeExecutor',
'RegExExecutor',
'VncExecutor',
'LoopExecutor',
'JsonExecutor',
]
17 changes: 10 additions & 7 deletions src/attackmate/executors/common/loopexecutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ def __init__(
def log_command(self, command: LoopCommand):
self.logger.info('Looping commands')

def break_condition_met(self, command: LoopCommand) -> bool:
def break_condition_met(self, command: LoopCommand, placeholders) -> bool:
if not command.break_if:
return False
condition = Template(command.break_if).safe_substitute(**self.varstore.variables)
condition = Template(command.break_if).safe_substitute(placeholders)

if Conditional.test(condition):
self.logger.warning('Breaking out of loop due to condition: %s', command.break_if)
return True
Expand All @@ -65,9 +66,10 @@ def loop_range(self, command: LoopCommand, start: int, end: int) -> None:
'LOOP_INDEX': x,
**self.varstore.variables,
}
self.substitute_variables_in_command(template_cmd, placeholders)
if self.break_condition_met(command):

if self.break_condition_met(command, placeholders):
return
self.substitute_variables_in_command(template_cmd, placeholders)
self.runfunc([template_cmd])

def loop_items(self, command: LoopCommand, varname: str, iterable: list[str]) -> None:
Expand All @@ -78,9 +80,10 @@ def loop_items(self, command: LoopCommand, varname: str, iterable: list[str]) ->
'LOOP_ITEM': x,
**self.varstore.variables,
}
self.substitute_variables_in_command(template_cmd, placeholders)
if self.break_condition_met(command):

if self.break_condition_met(command, placeholders):
return
self.substitute_variables_in_command(template_cmd, placeholders)
self.runfunc([template_cmd])

def loop_until(self, command: LoopCommand, condition: str) -> None:
Expand Down Expand Up @@ -133,4 +136,4 @@ def _exec_cmd(self, command: LoopCommand) -> Result:
# runfunc will replace global variables then
self.execute_loop(command)
self.logger.info('Loop execution complete')
return Result('', 0)
return Result('', 0)
9 changes: 6 additions & 3 deletions src/attackmate/executors/features/cmdvars.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,11 @@ def substitute_template_vars(self, command: BaseCommand, substitute_cmd_vars: bo
for attr_name in command.list_template_vars():

# Skip variable substitution for "cmd"
if attr_name == 'cmd' and not substitute_cmd_vars:
continue
if attr_name == 'cmd' and not substitute_cmd_vars:
continue

if attr_name == "break_if":
continue

attr_value = getattr(command, attr_name)

Expand All @@ -54,7 +57,7 @@ def substitute_template_vars(self, command: BaseCommand, substitute_cmd_vars: bo
setattr(substituted_command, attr_name, substituted_dict)

elif isinstance(attr_value, list):
# copy the dict to avoid referencing the original list
# copy the list to avoid referencing the original list
substituted_list = [i for i in attr_value]
for v in substituted_list:
if isinstance(v, str):
Expand Down
35 changes: 35 additions & 0 deletions src/attackmate/executors/vnc/sessionstore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from vncdotool.api import ThreadedVNCClientProxy as VncClient


class SessionStore:
def __init__(self):
self.store: dict[str, VncClient] = {}

def __getstate__(self):
"""
store contains the clients of the vnc connections.
"""
state = self.__dict__.copy()
state['store'] = None
return state

def has_session(self, session_name: str) -> bool:
if session_name in self.store:
return True
else:
return False

def get_client_by_session(self, session_name: str) -> VncClient:
if session_name in self.store:
return self.store[session_name]
else:
raise KeyError('Session not found in Sessionstore')

def set_session(self, session_name: str, client: VncClient):
self.store[session_name] = client

def remove_session(self, session_name: str):
if session_name in self.store:
del self.store[session_name]
else:
raise KeyError('Session not found in Sessionstore')
Loading