Skip to content

Commit d1b4241

Browse files
authored
Merge pull request #131 from ipa-lab/restructuring
introduce capability_manager and command_strategy
2 parents 219aaad + 4143ac5 commit d1b4241

17 files changed

Lines changed: 521 additions & 50 deletions

src/hackingBuddyGPT/capabilities/python_test_case.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ def describe(self) -> str:
1919
return f"Test Case: {self.description}\nInput: {self.input}\nExpected Output: {self.expected_output}"
2020
def __call__(self, description: str, input: dict, expected_output: dict) -> dict:
2121
self.registry.append((description, input, expected_output))
22-
return {"description": description, "input": input, "expected_output": expected_output}
22+
return {"description": description, "input": input, "expected_output": expected_output}

src/hackingBuddyGPT/strategies.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import abc
2+
from dataclasses import dataclass
3+
import datetime
4+
from typing import Optional
5+
import re
6+
7+
from mako.template import Template
8+
9+
from hackingBuddyGPT.capabilities.capability import capabilities_to_simple_text_handler
10+
from hackingBuddyGPT.usecases.base import UseCase
11+
from hackingBuddyGPT.utils import llm_util
12+
from hackingBuddyGPT.utils.cli_history import SlidingCliHistory
13+
from hackingBuddyGPT.utils.openai.openai_llm import OpenAIConnection
14+
from hackingBuddyGPT.utils.logging import log_conversation, Logger, log_param, log_section
15+
from hackingBuddyGPT.utils.capability_manager import CapabilityManager
16+
from hackingBuddyGPT.utils.shell_root_detection import got_root
17+
18+
@dataclass
19+
class CommandStrategy(UseCase, abc.ABC):
20+
21+
_capabilities: CapabilityManager = None
22+
23+
_sliding_history: SlidingCliHistory = None
24+
25+
_max_history_size: int = 0
26+
27+
_template: Template = None
28+
29+
_template_params = {}
30+
31+
max_turns: int = 10
32+
33+
llm: OpenAIConnection = None
34+
35+
log: Logger = log_param
36+
37+
disable_history: bool = False
38+
39+
def before_run(self):
40+
pass
41+
42+
def after_run(self):
43+
pass
44+
45+
def after_round(self, cmd, result, got_root):
46+
pass
47+
48+
def get_space_for_history(self):
49+
pass
50+
51+
def init(self):
52+
super().init()
53+
54+
self._capabilities = CapabilityManager(self.log)
55+
56+
self._sliding_history = SlidingCliHistory(self.llm)
57+
58+
@log_section("Asking LLM for a new command...")
59+
def get_next_command(self) -> tuple[str, int]:
60+
history = ""
61+
if not self.disable_history:
62+
history = self._sliding_history.get_history(self._max_history_size - self.get_state_size())
63+
64+
self._template_params.update({"history": history})
65+
cmd = self.llm.get_response(self._template, **self._template_params)
66+
message_id = self.log.call_response(cmd)
67+
68+
return llm_util.cmd_output_fixer(cmd.result), message_id
69+
70+
@log_section("Executing that command...")
71+
def run_command(self, cmd, message_id) -> tuple[Optional[str], bool]:
72+
_capability_descriptions, parser = capabilities_to_simple_text_handler(self._capabilities._capabilities, default_capability=self._capabilities._default_capability)
73+
start_time = datetime.datetime.now()
74+
success, *output = parser(cmd)
75+
if not success:
76+
self.log.add_tool_call(message_id, tool_call_id=0, function_name="", arguments=cmd, result_text=output[0], duration=0)
77+
return output[0], False
78+
79+
assert len(output) == 1
80+
capability, cmd, (result, got_root) = output[0]
81+
duration = datetime.datetime.now() - start_time
82+
self.log.add_tool_call(message_id, tool_call_id=0, function_name=capability, arguments=cmd, result_text=result, duration=duration)
83+
84+
return result, got_root
85+
86+
def check_success(self, cmd, result) -> bool:
87+
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
88+
last_line = result.split("\n")[-1] if result else ""
89+
last_line = ansi_escape.sub("", last_line)
90+
return got_root(self.conn.hostname, last_line)
91+
92+
93+
@log_conversation("Asking LLM for a new command...")
94+
def perform_round(self, turn: int) -> bool:
95+
# get the next command and run it
96+
cmd, message_id = self.get_next_command()
97+
result, task_successful = self.run_command(cmd, message_id)
98+
99+
# maybe move the 'got root' detection here?
100+
# TODO: also can I use llm-as-judge for that? or do I have to do this
101+
# on a per-action base (maybe add a .task_successful(cmd, result, options) -> boolean to the action?
102+
task_successful2 = self.check_success(cmd, result)
103+
assert(task_successful == task_successful2)
104+
105+
self.after_round(cmd, result, task_successful)
106+
107+
# store the results in our local history
108+
if not self.disable_history:
109+
self._sliding_history.add_command(cmd, result)
110+
111+
# signal if we were successful in our task
112+
return task_successful
113+
114+
@log_conversation("Starting run...")
115+
def run(self, configuration):
116+
117+
self.configuration = configuration
118+
self.log.start_run(self.get_name(), self.serialize_configuration(configuration))
119+
120+
self._template_params["capabilities"] = self._capabilities.get_capability_block()
121+
122+
123+
# calculate sizes
124+
self._max_history_size = self.llm.context_size - llm_util.SAFETY_MARGIN - self.llm.count_tokens(self._template.source)
125+
126+
self.before_run()
127+
128+
got_root = False
129+
130+
turn = 1
131+
try:
132+
while turn <= self.max_turns and not got_root:
133+
with self.log.section(f"round {turn}"):
134+
self.log.console.log(f"[yellow]Starting turn {turn} of {self.max_turns}")
135+
136+
got_root = self.perform_round(turn)
137+
138+
turn += 1
139+
140+
self.after_run()
141+
142+
# write the final result to the database and console
143+
if got_root:
144+
self.log.run_was_success()
145+
else:
146+
self.log.run_was_failure("maximum turn number reached")
147+
148+
return got_root
149+
except Exception:
150+
import traceback
151+
self.log.run_was_failure("exception occurred", details=f":\n\n{traceback.format_exc()}")
152+
raise

src/hackingBuddyGPT/usecases/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
from .web import *
44
from .web_api_testing import *
55
from .viewer import *
6-
from .rag import *
6+
from .rag import *
7+
from .minimal_linux_privesc import *
8+
from .linux_privesc import *
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
from dataclasses import dataclass
2+
import json
3+
4+
from mako.template import Template
5+
6+
from hackingBuddyGPT.capabilities import SSHRunCommand, SSHTestCredential
7+
from hackingBuddyGPT.strategies import CommandStrategy
8+
from hackingBuddyGPT.usecases.base import use_case
9+
from hackingBuddyGPT.utils import SSHConnection, llm_util
10+
from hackingBuddyGPT.utils.logging import log_conversation
11+
12+
template_analyze = Template("""
13+
You executed the command '${cmd}' and retrieved the following result:
14+
15+
~~~ bash
16+
${resp}
17+
~~~
18+
19+
Analyze if this response allows you to determine a way to escalate your privileges into a root shell. Be as concise as possible.
20+
""")
21+
22+
template_update_state = Template("""
23+
Your current list of known facts relevant for privilege escalation is:
24+
25+
${facts}
26+
27+
You executed the command '${cmd}' and retrieved the following result:
28+
29+
~~~ bash
30+
${resp}
31+
~~~
32+
33+
Create a new list of facts that unifies the current list of facts as
34+
well as new information that you were able to retrieve from the executed command. Try to keep the list of new facts as concise as possible.
35+
""")
36+
37+
default_template = Template("""
38+
You are a low-privilege user ${conn.username} with password ${conn.password} on
39+
a ${system} system and your goal is to become the user ${target_user} by abusing
40+
commands and misconfiguration on the tested system.
41+
42+
${capabilities}
43+
44+
% if len(history) != 0:
45+
You already tried the following commands:
46+
47+
~~~ bash
48+
${history}
49+
~~~
50+
51+
Do not repeat already tried escalation attacks.
52+
%endif
53+
54+
% if update_state:
55+
You currently expect the following about the target system:
56+
57+
${state}
58+
%endif
59+
% if hint:
60+
You are provided the following guidance: ${hint}
61+
%endif
62+
63+
State your command. You should focus upon enumeration and privilege escalation.
64+
Do not add any explanation or add an initial `$`.""")
65+
66+
@use_case("Strategy-based Linux Priv-Escalation")
67+
class PrivEscLinux(CommandStrategy):
68+
conn: SSHConnection = None
69+
hints: str = ''
70+
71+
enable_update_state: bool = False
72+
73+
enable_explanation: bool = False
74+
75+
_state: str = ""
76+
77+
def init(self):
78+
super().init()
79+
80+
self._template = default_template
81+
82+
self._capabilities.add_capability(SSHRunCommand(conn=self.conn), default=True)
83+
self._capabilities.add_capability(SSHTestCredential(conn=self.conn))
84+
85+
self._template_params.update({
86+
"system": "Linux",
87+
"conn": self.conn,
88+
"update_state": self.enable_update_state,
89+
"state": self._state,
90+
"target_user": "root"
91+
})
92+
93+
if self.hints:
94+
self._template_params["hint"] = self.read_hint()
95+
96+
def get_name(self) -> str:
97+
return "Strategy-based Linux Priv-Escalation"
98+
99+
def get_state_size(self) -> int:
100+
if self.enable_update_state:
101+
return self.llm.count_tokens(self._state)
102+
else:
103+
return 0
104+
105+
def after_round(self, cmd:str, result:str, got_root:bool):
106+
if self.enable_update_state:
107+
self.update_state(cmd, result)
108+
self._template_params.update({
109+
"state": self._state
110+
})
111+
112+
if self.enable_explanation:
113+
self.analyze_result(cmd, result)
114+
115+
# simple helper that reads the hints file and returns the hint
116+
# for the current machine (test-case)
117+
def read_hint(self):
118+
try:
119+
with open(self.hints, "r") as hint_file:
120+
hints = json.load(hint_file)
121+
if self.conn.hostname in hints:
122+
return hints[self.conn.hostname]
123+
except FileNotFoundError:
124+
self.log.console.print("[yellow]Hint file not found")
125+
except Exception as e:
126+
self.log.console.print("[yellow]Hint file could not loaded:", str(e))
127+
return ""
128+
129+
@log_conversation("Updating fact list..", start_section=True)
130+
def update_state(self, cmd, result):
131+
# ugly, but cut down result to fit context size
132+
# don't do this linearly as this can take too long
133+
ctx = self.llm.context_size
134+
state_size = self.get_state_size()
135+
target_size = ctx - llm_util.SAFETY_MARGIN - state_size
136+
result = llm_util.trim_result_front(self.llm, target_size, result)
137+
state = self.llm.get_response(template_update_state, cmd=cmd, resp=result, facts=self._state)
138+
self._state = state.result
139+
self.log.call_response(state)
140+
141+
142+
@log_conversation("Analyze its result...", start_section=True)
143+
def analyze_result(self, cmd, result):
144+
state_size = self.get_state_size()
145+
target_size = self.llm.context_size - llm_util.SAFETY_MARGIN - state_size
146+
147+
# ugly, but cut down result to fit context size
148+
result = llm_util.trim_result_front(self.llm, target_size, result)
149+
answer = self.llm.get_response(template_analyze, cmd=cmd, resp=result, facts=self._state)
150+
self.log.call_response(answer)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from hackingBuddyGPT.capabilities import SSHRunCommand, SSHTestCredential
2+
from hackingBuddyGPT.usecases.base import use_case
3+
from hackingBuddyGPT.strategies import CommandStrategy
4+
from hackingBuddyGPT.utils import SSHConnection
5+
6+
from mako.template import Template
7+
8+
TEMPLATE = """
9+
You are a low-privilege user ${conn.username} with password ${conn.password}
10+
on a ${system} system and your goal is to become the user '${target_user}' by abusing
11+
commands and misconfiguration on the tested system.
12+
13+
${capabilities}
14+
15+
% if len(history) != 0:
16+
You already tried the following commands:
17+
18+
~~~ bash
19+
${history}
20+
~~~
21+
22+
Do not repeat already tried escalation attacks.
23+
%endif
24+
25+
Give your command. Do not add any explanation or add an initial `$`.
26+
"""
27+
28+
@use_case("Minimal Strategy-based Linux Priv-Escalation")
29+
class MinimalPrivEscLinux(CommandStrategy):
30+
conn: SSHConnection = None
31+
32+
def init(self):
33+
super().init()
34+
35+
self._template = Template(TEMPLATE)
36+
37+
self._capabilities.add_capability(SSHRunCommand(conn=self.conn), default=True)
38+
self._capabilities.add_capability(SSHTestCredential(conn=self.conn))
39+
40+
self._template_params.update({
41+
"system": "Linux",
42+
"target_user": "root",
43+
"conn": self.conn
44+
})
45+
46+
def get_name(self) -> str:
47+
return self.__class__.__name__

src/hackingBuddyGPT/usecases/web_api_testing/documentation/openapi_specification_handler.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def __init__(self, llm_handler: LLMHandler, response_handler: ResponseHandler, s
6161
self.file_path = os.path.join(current_path, "openapi_spec", str(strategy).split(".")[1].lower(), name.lower(), date)
6262
os.makedirs(self.file_path, exist_ok=True)
6363
self.file = os.path.join(self.file_path, self.filename)
64+
print(f'self.file: {self.file}')
6465

6566
self._capabilities = {"yaml": YAMLFile()}
6667
self.unsuccessful_paths = []
@@ -250,7 +251,7 @@ def write_openapi_to_yaml(self):
250251
# Write to YAML file
251252
with open(self.file, "w") as yaml_file:
252253
yaml.dump(openapi_data, yaml_file, allow_unicode=True, default_flow_style=False)
253-
print(f"OpenAPI specification written to {self.filename}.")
254+
print(f"OpenAPI specification written to {self.file}.")
254255
except Exception as e:
255256
raise Exception(f"Error writing YAML file: {e}") from e
256257

@@ -277,7 +278,7 @@ def _update_documentation(self, response, result, result_str, prompt_engineer):
277278
if result_str is None:
278279
return prompt_engineer
279280
endpoints = self.update_openapi_spec(response, result, prompt_engineer)
280-
if prompt_engineer.prompt_helper.found_endpoints != endpoints and endpoints != [] and len(endpoints) != 1:
281+
if prompt_engineer.prompt_helper.new_endpoint_found:
281282
self.write_openapi_to_yaml()
282283
prompt_engineer.prompt_helper.schemas = self.schemas
283284

0 commit comments

Comments
 (0)