Skip to content

Commit 7436a5d

Browse files
authored
Merge pull request #15 from Neverbolt/main
Implements first version of modular capability system
2 parents 394d89b + d6fe107 commit 7436a5d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1003
-839
lines changed

‎.env.example‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
OPENAI_KEY="your-openai-key"
2-
MODEL="gpt-4"
3-
CONTEXT_SIZE=7000
1+
llm.api_key='your-openai-key'
2+
log_db.connection_string='log_db.sqlite3'
43

54
# exchange with the IP of your target VM
6-
TARGET_IP='enter-the-private-ip-of-some-vm.local'
5+
conn.host='enter-the-private-ip-of-some-vm.local'
6+
conn.hostname='the-hostname-of-the-vm-used-for-root-detection'
7+
conn.port=2222
78

89
# exchange with the user for your target VM
9-
TARGET_USER='bob'
10-
TARGET_PASSWORD='secret'
10+
conn.username='bob'
11+
conn.password='secret'
1112

12-
# which LLM driver to use (can be openai_rest or oobabooga for now)
13-
LLM_CONNECTION = "openai_rest"
13+
# which LLM model to use (can be anything openai supports, or if you use a custom llm.api_url, anything your api provides for the model parameter
14+
llm.model='gpt-3.5-turbo'
15+
llm.context_size=16385
1416

1517
# how many rounds should this thing go?
16-
MAX_ROUNDS = 20
18+
max_turns = 20

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ venv/
33
__pycache__/
44
*.swp
55
*.log
6+
.idea/
7+
*.sqlite3
8+
*.sqlite3-jounal

‎README.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ This work is partially based upon our empiric research into [how hackers work](h
6767

6868
This is a simple example run of `wintermute.py` using GPT-4 against a vulnerable VM. More example runs can be seen in [our collection of historic runs](docs/old_runs/old_runs.md).
6969

70-
![Example wintermute run](example_run_gpt4.png)
70+
![Example wintermute run](docs/example_run_gpt4.png)
7171

7272
Some things to note:
7373

@@ -105,8 +105,13 @@ $ cp .env.example .env
105105
# IMPORTANT: setup your OpenAI API key, the VM's IP and credentials within .env
106106
$ vi .env
107107

108-
# start wintermute, i.e., attack the configured virtual machine
108+
# if you start wintermute without parameters, it will list all available use cases
109109
$ python wintermute.py
110+
usage: wintermute.py [-h] {linux_privesc,windows privesc} ...
111+
wintermute.py: error: the following arguments are required: {linux_privesc,windows privesc}
112+
113+
# start wintermute, i.e., attack the configured virtual machine
114+
$ python wintermute.py linux_privesc --enable_explanation true --enable_update_state true
110115
~~~
111116

112117
# Disclaimers

‎args.py‎

Lines changed: 0 additions & 75 deletions
This file was deleted.

‎capabilities/__init__.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .capability import Capability
2+
from .psexec_test_credential import PSExecTestCredential
3+
from .psexec_run_command import PSExecRunCommand
4+
from .ssh_run_command import SSHRunCommand
5+
from .ssh_test_credential import SSHTestCredential

‎capabilities/capability.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import abc
2+
3+
4+
class Capability(abc.ABC):
5+
"""
6+
A capability is something that can be used by an LLM to perform a task.
7+
The method signature for the __call__ method is not yet defined, but it will probably be different for different
8+
types of capabilities (though it is recommended to have the same signature for capabilities, that accomplish the
9+
same task but slightly different / for a different target).
10+
11+
At the moment, this is not yet a very powerful class, but in the near-term future, this will provide an automated
12+
way of providing a json schema for the capabilities, which can then be used for function-calling LLMs.
13+
"""
14+
@abc.abstractmethod
15+
def describe(self, name: str = None) -> str:
16+
"""
17+
describe should return a string that describes the capability. This is used to generate the help text for the
18+
LLM.
19+
I don't like, that at the moment the name under which the capability is available to the LLM is allowed to be
20+
passed in, but it is necessary at the moment, to be backwards compatible. Please do not use the name if you
21+
don't really have to, then we can see if we can remove it in the future.
22+
23+
This is a method and not just a simple property on purpose (though it could become a @property in the future, if
24+
we don't need the name parameter anymore), so that it can template in some of the capabilities parameters into
25+
the description.
26+
"""
27+
pass
28+
29+
@abc.abstractmethod
30+
def __call__(self, *args, **kwargs):
31+
"""
32+
The actual execution of a capability, please make sure, that the parameters and return type of your
33+
implementation are well typed, as this will make it easier to support full function calling soon.
34+
"""
35+
pass

‎capabilities/psexec_run_command.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from dataclasses import dataclass
2+
from typing import Tuple
3+
4+
from utils import PSExecConnection
5+
from .capability import Capability
6+
7+
8+
@dataclass
9+
class PSExecRunCommand(Capability):
10+
conn: PSExecConnection
11+
12+
@property
13+
def describe(self, name: str = None) -> str:
14+
return f"give a command to be executed on the shell and I will respond with the terminal output when running this command on the windows machine. The given command must not require user interaction. Only state the to be executed command. The command should be used for enumeration or privilege escalation."
15+
16+
def __call__(self, command: str) -> Tuple[str, bool]:
17+
return self.conn.run(command)[0], False
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import warnings
2+
from dataclasses import dataclass
3+
from typing import Tuple
4+
5+
from utils import PSExecConnection
6+
from .capability import Capability
7+
8+
9+
@dataclass
10+
class PSExecTestCredential(Capability):
11+
conn: PSExecConnection
12+
13+
def describe(self, name: str = None) -> str:
14+
return f"give credentials to be tested by stating `{name} username password`"
15+
16+
def __call__(self, username: str, password: str) -> Tuple[str, bool]:
17+
try:
18+
test_conn = self.conn.new_with(username=username, password=password)
19+
test_conn.init()
20+
warnings.warn("full credential testing is not implemented yet for psexec, we have logged in, but do not know who we are, returning True for now")
21+
return "Login as root was successful\n", True
22+
except Exception:
23+
return "Authentication error, credentials are wrong\n", False

‎capabilities/ssh_run_command.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import re
2+
from dataclasses import dataclass
3+
from typing import Tuple
4+
5+
from invoke import Responder
6+
7+
from utils import SSHConnection
8+
from .capability import Capability
9+
10+
11+
GOT_ROOT_REXEXPs = [
12+
re.compile("^# $"),
13+
re.compile("^bash-[0-9]+.[0-9]# $")
14+
]
15+
16+
17+
@dataclass
18+
class SSHRunCommand(Capability):
19+
conn: SSHConnection
20+
21+
def describe(self, name: str = None) -> str:
22+
return f"give a command to be executed on the shell and I will respond with the terminal output when running this command on the linux server. The given command must not require user interaction. Only state the to be executed command. The command should be used for enumeration or privilege escalation."
23+
24+
def __call__(self, command: str) -> Tuple[str, bool]:
25+
got_root = False
26+
sudo_pass = Responder(
27+
pattern=r'\[sudo\] password for ' + self.conn.username + ':',
28+
response=self.conn.password + '\n',
29+
)
30+
31+
try:
32+
stdout, stderr, rc = self.conn.run(command, pty=True, warn=True, watchers=[sudo_pass], timeout=10)
33+
except Exception as e:
34+
print("TIMEOUT! Could we have become root?")
35+
stdout, stderr, rc = "", "", -1
36+
tmp = ""
37+
last_line = ""
38+
for line in stdout.splitlines():
39+
if not line.startswith('[sudo] password for ' + self.conn.username + ':'):
40+
last_line = line
41+
tmp = tmp + line
42+
43+
# remove ansi shell codes
44+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
45+
last_line = ansi_escape.sub('', last_line)
46+
47+
for i in GOT_ROOT_REXEXPs:
48+
if i.fullmatch(last_line):
49+
got_root = True
50+
if last_line.startswith(f'root@{self.conn.hostname}:'):
51+
got_root = True
52+
return tmp, got_root
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from dataclasses import dataclass
2+
from typing import Tuple
3+
4+
import paramiko
5+
6+
from utils import SSHConnection
7+
from .capability import Capability
8+
9+
10+
@dataclass
11+
class SSHTestCredential(Capability):
12+
conn: SSHConnection
13+
14+
def describe(self, name: str = None) -> str:
15+
return f"give credentials to be tested by stating `{name} username password`"
16+
17+
def __call__(self, command: str) -> Tuple[str, bool]:
18+
cmd_parts = command.split(" ")
19+
assert (cmd_parts[0] == "test_credential")
20+
21+
if len(cmd_parts) != 3:
22+
return "didn't provide username/password", False
23+
24+
test_conn = self.conn.new_with(username=cmd_parts[1], password=cmd_parts[2])
25+
try:
26+
test_conn.init()
27+
user = test_conn.run("whoami")[0].strip('\n\r ')
28+
if user == "root":
29+
return "Login as root was successful\n", True
30+
else:
31+
return "Authentication successful, but user is not root\n", False
32+
33+
except paramiko.ssh_exception.AuthenticationException:
34+
return "Authentication error, credentials are wrong\n", False

0 commit comments

Comments
 (0)