Skip to content

Commit 4f12e40

Browse files
authored
Merge pull request #7 from andreashappe/pentest_task_tree
Pentest task tree
2 parents 41a6a2c + 88b3977 commit 4f12e40

File tree

2 files changed

+237
-12
lines changed

2 files changed

+237
-12
lines changed

src/helper/ui.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,35 @@
44
from rich.panel import Panel
55
from rich.pretty import Pretty
66

7-
def print_event(console: Console, event):
7+
def get_panels_from_event(console: Console, event):
8+
panels = []
9+
810
if "messages" in event:
911
message = event["messages"][-1]
1012
if isinstance(message, HumanMessage):
11-
console.print(Panel(str(message.content), title="Punny Human says"))
13+
panels.append(Panel(str(message.content), title="Input to the LLM"))
1214
elif isinstance(message, ToolMessage):
13-
console.print(Panel(str(message.content), title=f"Tool Reponse from {message.name}"))
15+
panels.append(Panel(str(message.content), title=f"Tool Reponse from {message.name}"))
1416
elif isinstance(message, AIMessage):
1517
if message.content != '':
16-
console.print(Panel(str(message.content), title="AI says"))
17-
elif len(message.tool_calls) == 1:
18-
tool = message.tool_calls[0]
19-
console.print(Panel(Pretty(tool["args"]), title=f"Tool Call to {tool["name"]}"))
18+
panels.append(Panel(str(message.content), title="Output from the LLM"))
19+
elif len(message.tool_calls) >= 1:
20+
for tool in message.tool_calls:
21+
panels.append(Panel(Pretty(tool["args"]), title=f"Tool Call to {tool["name"]}"))
2022
else:
21-
print("WHAT do you want?")
22-
console.log(message)
23+
panels.append(Panel(Pretty(message), title='unknown message type'))
2324
else:
24-
print("WHAT message are you?")
25-
console.log(message)
25+
raise Exception("Unknown message type: " + str(message))
2626
else:
27-
print("WHAT ARE YOU??????")
27+
console.log("no messages in event?")
2828
console.log(event)
29+
return panels
30+
31+
def print_event(console: Console, event):
32+
panels = get_panels_from_event(console, event)
33+
34+
for panel in panels:
35+
console.print(panel)
2936

3037
def print_event_stream(console: Console, events):
3138
for event in events:

src/pentest_task_tree.py

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
2+
import operator
3+
4+
from dotenv import load_dotenv
5+
from rich.console import Console
6+
from rich.panel import Panel
7+
8+
from langchain_core.prompts import PromptTemplate
9+
from langchain_openai import ChatOpenAI
10+
11+
from helper.common import get_or_fail
12+
from helper.ui import print_event
13+
from tools.ssh import get_ssh_connection_from_env, SshTestCredentialsTool, SshExecuteTool
14+
from graphs.initial_version import create_chat_tool_agent_graph
15+
16+
from typing import Annotated, List, Tuple, Union
17+
from typing_extensions import TypedDict
18+
from pydantic import BaseModel, Field
19+
20+
from langchain_core.prompts import ChatPromptTemplate
21+
from langgraph.graph import StateGraph, START, END
22+
23+
# setup configuration from environment variables
24+
load_dotenv()
25+
get_or_fail("OPENAI_API_KEY") # langgraph will use this env variable itself
26+
conn = get_ssh_connection_from_env()
27+
conn.connect()
28+
29+
# prepare console for debug output
30+
console = Console()
31+
32+
# the shared graph data structure
33+
class PlanExecute(TypedDict):
34+
input: str # the initial user-given objective
35+
plan: str # the current task plan
36+
next_step: str # the next operation to be tested by the agent
37+
past_steps: Annotated[List[Tuple], operator.add] # past steps of the agent, also including a summary
38+
response: str # response from the agent to the user
39+
40+
# This is the common prefix used by both planner and replanner
41+
# I used gelei's pentestGPT prompts from https://github.com/GreyDGL/PentestGPT/blob/main/pentestgpt/prompts/prompt_class_v2.py as a starting point. Mostly
42+
# I removed tool-specific examples to not frame the LLM to move into a specific
43+
# direction and tried to make it more generic.
44+
COMMON_PREFIX = """You are given an objective by the user. You are required to strategize and create a tree-structured task plan that will allow to successfully solve the objective. Another worker will follow your task plan to complete the objective, and will report after each finished task back to you. You should use this feedback to update the task plan.
45+
46+
When creating the task plan you must follow the following requirements:
47+
48+
1. You need to maintain a task plan, which contains all potential tasks that should be investigated to solve the objective. The tasks should be in a tree structure because one task can be considered as a sub-task to another.
49+
You can display the tasks in a layer structure, such as 1, 1.1, 1.1.1, etc. Initially, you should only generate the root tasks based on the initial information. In addition select the next task (as next_step) that should be executed by the tester.
50+
"""
51+
52+
# The Planner Prompt
53+
planner_prompt = ChatPromptTemplate.from_messages(
54+
[
55+
(
56+
"system", COMMON_PREFIX + """This plan should involve individual tasks, that if executed correctly will yield the correct answer. Do not add any superfluous steps but make sure that each step has all the information needed - do not skip steps.""",
57+
),
58+
("placeholder", "{messages}"),
59+
]
60+
)
61+
62+
# The Replanner Prompt
63+
replanner_prompt = ChatPromptTemplate.from_template(
64+
COMMON_PREFIX + """2. Each time you receive results from the worker you should
65+
2.1 Analyze the message and see identify useful key information
66+
2.2 Decide to add a new task or update a task information according to the findings. Only add steps to the plan that still NEED to be done.
67+
2.3 Decide to delete a task if necessary. Do this if the task is not relevant for reaching the objective anymore.
68+
2.4 From all the tasks, identify those that can be performed next. Analyze those tasks and decide which one should be performed next based on their likelihood to a successful exploit. Name this task as 'next_step'.
69+
70+
Your objective was this:
71+
{input}
72+
73+
Your original task-plan was this:
74+
{plan}
75+
76+
You have currently done the follow tasks:
77+
{past_steps}
78+
79+
If no more steps are needed to solve the objective, then respond with that. Otherwise, return a new task-plan and the next step to execute. If you were not able to complete the task, stop after 15 planning steps and give a summary to the user.
80+
"""
81+
)
82+
83+
# we are using structured responses so that we do not have to manually parse
84+
# the output of the LLM
85+
86+
class Plan(BaseModel):
87+
"""Plan to follow in future"""
88+
89+
steps: str = Field(
90+
description="the hierarchical task plan"
91+
)
92+
93+
next_step: str = Field(
94+
description = "The next task to perform."
95+
)
96+
97+
class Response(BaseModel):
98+
"""Response to user."""
99+
response: str
100+
101+
class Act(BaseModel):
102+
"""Action to perform."""
103+
104+
action: Union[Response, Plan] = Field(
105+
description="Action to perform. If you want to respond to user, use Response. "
106+
"If you need to further use tools to get the answer, use Plan."
107+
)
108+
109+
# The executor agent that actually performs tasks on the connected target
110+
111+
# configure a second LLM connection for the executor/agent_step
112+
# this would allow us to use different LLMs for high-level planning
113+
# and low-level execution. Also we could use different tools for the
114+
# the different models, e.g., allow the high-level model to browse the web
115+
# for newer attack ideas.
116+
llm2 = ChatOpenAI(model="gpt-4o", temperature=0)
117+
tools = [SshExecuteTool(conn), SshTestCredentialsTool(conn)]
118+
llm2_with_tools = llm2.bind_tools(tools)
119+
120+
# this will be called as a separate sub-graph/agent with it's own state
121+
# and output
122+
def execute_step(state: PlanExecute):
123+
124+
task = state["next_step"]
125+
126+
prompt = PromptTemplate.from_template("""
127+
You are a low-privilege user {username} with password {password} on a linux system and your goal is to become the root user by executing commands on the remote system.
128+
129+
To achieve this, focus upon {task}
130+
131+
Do not repeat already tried escalation attacks. You should focus upon enumeration and privilege escalation. If you were able to become root, describe the used method as final message. Stop after 5 executions. If not successful until then, give a summary of gathered facts.
132+
""").format(username=conn.username, password=conn.password,task=task)
133+
134+
# create our command executor/agent graph
135+
graph_builder = create_chat_tool_agent_graph(llm2_with_tools, tools)
136+
graph = graph_builder.compile()
137+
138+
events = graph.stream(
139+
{"messages": [("user", prompt)]},
140+
stream_mode='values'
141+
)
142+
143+
agent_response = None
144+
for event in events:
145+
print_event(console, event)
146+
agent_response = event
147+
148+
return {
149+
"past_steps": [(task, agent_response["messages"][-1].content)],
150+
}
151+
152+
# create the graph
153+
def create_plan_and_execute_graph(llm, execute_step):
154+
155+
def should_end(state: PlanExecute):
156+
if "response" in state and state["response"]:
157+
return END
158+
else:
159+
return "agent"
160+
161+
def plan_step(state: PlanExecute):
162+
planner = planner_prompt | llm.with_structured_output(Plan)
163+
plan = planner.invoke({"messages": [("user", state["input"])]})
164+
return {"plan": plan.steps, "next_step": plan.next_step}
165+
166+
def replan_step(state: PlanExecute):
167+
168+
console.print(Panel(state["plan"], title="Plan"))
169+
170+
replanner = replanner_prompt | llm.with_structured_output(Act)
171+
output = replanner.invoke(state)
172+
if isinstance(output.action, Response):
173+
return {"response": output.action.response}
174+
else:
175+
console.print(Panel(output.action.next_step, title="Next Step"))
176+
console.print(Panel(output.action.steps, title="New Plan"))
177+
return {"plan": output.action.steps, "next_step": output.action.next_step}
178+
179+
workflow = StateGraph(PlanExecute)
180+
181+
# Add the nodes
182+
workflow.add_node("planner", plan_step)
183+
workflow.add_node("agent", execute_step)
184+
workflow.add_node("replan", replan_step)
185+
186+
# set the start node
187+
workflow.add_edge(START, "planner")
188+
189+
# configure links between nodes
190+
workflow.add_edge("planner", "agent")
191+
workflow.add_edge("agent", "replan")
192+
workflow.add_conditional_edges("replan", should_end)
193+
194+
return workflow
195+
196+
# create the graph
197+
llm = ChatOpenAI(model="gpt-4o", temperature=0)
198+
workflow = create_plan_and_execute_graph(llm, execute_step)
199+
app = workflow.compile()
200+
print(app.get_graph(xray=True).draw_ascii())
201+
202+
# now start our agent and give it our initial prompt ('get root')
203+
template = PromptTemplate.from_template("""
204+
You are a low-privilege user {username} with password {password} on a linux system and your goal is to become the root user by executing commands on the remote system.
205+
206+
Do not repeat already tried escalation attacks. You should focus upon enumeration and privilege escalation. If you were able to become root, describe the used method as final message.
207+
""").format(username=conn.username, password=conn.password)
208+
209+
# start everything
210+
events = app.stream(
211+
input = {"input": template },
212+
config = {"recursion_limit": 50},
213+
stream_mode = "values"
214+
)
215+
216+
# output all occurring events
217+
for event in events:
218+
print_event(console, event)

0 commit comments

Comments
 (0)