pwn 0.5.561 → 0.5.573
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +4 -3
- data/.ruby-version +1 -1
- data/Gemfile +20 -20
- data/README.md +5 -5
- data/build_gem.sh +14 -10
- data/documentation/lifecycle_authz_replay.example.yaml +27 -0
- data/documentation/vulnerability_report_template.md +37 -0
- data/lib/pwn/ai/agent/vuln_gen.rb +28 -8
- data/lib/pwn/ai/anthropic.rb +281 -0
- data/lib/pwn/ai/grok.rb +8 -3
- data/lib/pwn/ai/introspection.rb +9 -0
- data/lib/pwn/ai/open_ai.rb +6 -2
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/bounty/lifecycle_authz_replay.rb +505 -0
- data/lib/pwn/bounty.rb +16 -0
- data/lib/pwn/config.rb +145 -7
- data/lib/pwn/cron.rb +210 -0
- data/lib/pwn/driver.rb +9 -0
- data/lib/pwn/memory.rb +136 -0
- data/lib/pwn/plugins/repl.rb +395 -50
- data/lib/pwn/plugins/xxd.rb +24 -0
- data/lib/pwn/sessions.rb +160 -0
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn.rb +4 -0
- data/spec/lib/pwn/ai/anthropic_spec.rb +15 -0
- data/spec/lib/pwn/bounty/lifecycle_authz_replay_spec.rb +53 -0
- data/spec/lib/pwn/bounty_spec.rb +10 -0
- data/spec/lib/pwn/cron_spec.rb +15 -0
- data/spec/lib/pwn/memory_spec.rb +24 -0
- data/spec/lib/pwn/sessions_spec.rb +25 -0
- data/spec/smoke/lifecycle_authz_replay_smoke_test.rb +59 -0
- data/third_party/pwn_rdoc.jsonl +69 -2
- data/upgrade_pwn.sh +1 -1
- metadata +57 -42
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -4,6 +4,7 @@ require 'curses'
|
|
|
4
4
|
require 'fileutils'
|
|
5
5
|
require 'meshtastic'
|
|
6
6
|
require 'pry'
|
|
7
|
+
require 'reline'
|
|
7
8
|
require 'tty-prompt'
|
|
8
9
|
require 'unicode/display_width'
|
|
9
10
|
require 'yaml'
|
|
@@ -12,10 +13,59 @@ module PWN
|
|
|
12
13
|
module Plugins
|
|
13
14
|
# This module contains methods related to the pwn REPL Driver.
|
|
14
15
|
module REPL
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# )
|
|
16
|
+
# Custom input handler for pwn-ai to support multi-line submissions:
|
|
17
|
+
# - SHIFT+ENTER (common terminal sequences) or Ctrl+J / Alt+Enter inserts a newline (continue editing)
|
|
18
|
+
# - plain ENTER submits the full prompt (possibly multi-line) to the AI
|
|
19
|
+
# - Multi-line pastes are supported (Reline handles \n in buffer; submit with ENTER)
|
|
20
|
+
class PwnAIInput
|
|
21
|
+
attr_reader :line_buffer
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@line_buffer = ''
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def readline(prompt)
|
|
28
|
+
# Common escape sequences for SHIFT+ENTER across terminals (xterm, modern, etc.)
|
|
29
|
+
shift_enter_seqs = [
|
|
30
|
+
# Common SHIFT+ENTER sequences across terminals (xterm, gnome-terminal, kitty, wezterm, recent xterm w/ modifyOtherKeys, etc.)
|
|
31
|
+
# If SHIFT+ENTER still submits in your terminal, try Ctrl+J or Alt+Enter (M-^J) as alternatives (terminals vary in emitted bytes for SHIFT+ENTER)
|
|
32
|
+
'\\e[13;2~',
|
|
33
|
+
'\\e[1;2~',
|
|
34
|
+
'\\e\\r',
|
|
35
|
+
'\\e\\n',
|
|
36
|
+
'\\e[13;2u',
|
|
37
|
+
'\\u001b[13;2u',
|
|
38
|
+
'\\e[27;2;13~',
|
|
39
|
+
'\\e[27;2;13u',
|
|
40
|
+
'\\e[13;2u',
|
|
41
|
+
'\\e[1;2~',
|
|
42
|
+
'\\e[27;2;13~'
|
|
43
|
+
]
|
|
44
|
+
shift_enter_seqs.each do |seq|
|
|
45
|
+
Reline.config.add_oneshot_key_binding(seq.bytes, :key_newline)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
# readmultiline with confirm block that *always* returns true:
|
|
50
|
+
# => normal ENTER triggers finish/submit of the (multi-line) buffer
|
|
51
|
+
# The bound SHIFT+ENTER (and built-in Ctrl+J / M-^M) trigger key_newline (insert \n, stay in edit)
|
|
52
|
+
# Reline in multiline mode also handles multi-line pastes by splitting on \n in the buffer.
|
|
53
|
+
@line_buffer = Reline.readmultiline(prompt, true) { |_buffer| true } || ''
|
|
54
|
+
ensure
|
|
55
|
+
Reline.config.reset_oneshot_key_bindings
|
|
56
|
+
end
|
|
57
|
+
@line_buffer
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Compatibility with Pry input expectations (used by hooks for line_buffer, and possibly completer/tty checks)
|
|
61
|
+
def tty?
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def winsize
|
|
66
|
+
[TTY::Screen.rows || 24, TTY::Screen.columns || 80]
|
|
67
|
+
end
|
|
68
|
+
end
|
|
19
69
|
|
|
20
70
|
public_class_method def self.refresh_ps1_proc(opts = {})
|
|
21
71
|
mode = opts[:mode]
|
|
@@ -117,13 +167,147 @@ module PWN
|
|
|
117
167
|
end
|
|
118
168
|
|
|
119
169
|
Pry::Commands.create_command 'pwn-ai' do
|
|
120
|
-
description 'Initiate pwn.ai
|
|
170
|
+
description 'Initiate pwn.ai autonomous agent TUI (instruct tasks using PWN modules + CLI tools; memory/sessions/agents/cron/skills-aware from PWN::Config/PWN::Memory etc).'
|
|
121
171
|
|
|
122
172
|
def process
|
|
123
173
|
pi = pry_instance
|
|
124
174
|
pi.config.pwn_ai = true
|
|
175
|
+
pi.config.pwn_ai_agent = true
|
|
125
176
|
pi.config.color = false if pi.config.pwn_ai
|
|
126
|
-
|
|
177
|
+
|
|
178
|
+
# Switch to custom multi-line input for pwn-ai (SHIFT+ENTER newline, ENTER submit)
|
|
179
|
+
pi.config.pwn_ai_original_input ||= Pry.config.input
|
|
180
|
+
Pry.config.input = PwnAIInput.new
|
|
181
|
+
|
|
182
|
+
# Load and make aware of skills folder (scaled in PWN::Config per user pwn_env_path parent)
|
|
183
|
+
skills_path = begin
|
|
184
|
+
PWN::Config.pwn_skills_path
|
|
185
|
+
rescue StandardError
|
|
186
|
+
"#{Dir.home}/.pwn/skills"
|
|
187
|
+
end
|
|
188
|
+
PWN::Config.load_skills(pwn_skills_path: skills_path)
|
|
189
|
+
skills_count = (PWN.const_defined?(:Skills) ? PWN::Skills.keys.length : 0)
|
|
190
|
+
|
|
191
|
+
# Hermes-equivalent memory/sessions/cron init for this pwn-ai activation
|
|
192
|
+
PWN::Config.load_memory
|
|
193
|
+
mem_count = (PWN.const_defined?(:Memory) ? PWN::Memory.load.keys.length : 0)
|
|
194
|
+
sess = begin
|
|
195
|
+
PWN::Sessions.create(title: "pwn-ai #{Time.now.strftime('%Y-%m-%d %H:%M')}", source: 'pwn-ai-repl')
|
|
196
|
+
rescue StandardError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
pi.config.pwn_ai_session_id = sess[:id] if sess
|
|
200
|
+
cron_count = (PWN.const_defined?(:Cron) ? PWN::Cron.list.keys.length : 0)
|
|
201
|
+
|
|
202
|
+
puts "\
|
|
203
|
+
[*] pwn-ai agent TUI activated (PWN REPL driver w/ memory, sessions, delegation, cron)."
|
|
204
|
+
puts "[*] Memory facts: #{mem_count} | Session: #{pi.config.pwn_ai_session_id} | Cron jobs: #{cron_count} | Skills: #{skills_count}"
|
|
205
|
+
puts '[*] Instruct the AI agent to carry out a task, e.g.:'
|
|
206
|
+
puts " 'Use NmapIt to port scan target.com then use TransparentBrowser to spider and SAST::TestCaseEngine to analyze code if cloned. Generate report with PWN::Reports.'"
|
|
207
|
+
puts " 'Execute CLI nmap -sV target.com and summarize findings using PWN modules.'"
|
|
208
|
+
puts "[*] Skills loaded from #{skills_path} (#{skills_count} available) + memory/sessions/cron to expand autonomous capabilities."
|
|
209
|
+
puts "[*] Type 'toggle-pwn-ai' or normal pwn commands to exit agent mode.
|
|
210
|
+
"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
Pry::Commands.create_command 'pwn-ai-memory' do
|
|
215
|
+
description 'Manage pwn-ai persistent memory (Hermes equiv).'
|
|
216
|
+
|
|
217
|
+
def process
|
|
218
|
+
cmd = args[0]
|
|
219
|
+
case cmd
|
|
220
|
+
when 'list', 'recall', nil
|
|
221
|
+
q = args[1]
|
|
222
|
+
res = PWN::Memory.recall(query: q)
|
|
223
|
+
puts res.inspect
|
|
224
|
+
when 'remember'
|
|
225
|
+
key = args[1]
|
|
226
|
+
val = args[2..-1].join(' ')
|
|
227
|
+
PWN::Memory.remember(key: key, value: val)
|
|
228
|
+
puts "Remembered #{key}"
|
|
229
|
+
when 'forget'
|
|
230
|
+
PWN::Memory.forget(args[1])
|
|
231
|
+
puts "Forgot #{args[1]}"
|
|
232
|
+
when 'clear'
|
|
233
|
+
PWN::Memory.clear
|
|
234
|
+
puts 'Memory cleared'
|
|
235
|
+
else
|
|
236
|
+
puts PWN::Memory.help
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
Pry::Commands.create_command 'pwn-ai-sessions' do
|
|
242
|
+
description 'List/resume/delete pwn-ai sessions (Hermes equiv).'
|
|
243
|
+
|
|
244
|
+
def process
|
|
245
|
+
cmd = args[0]
|
|
246
|
+
case cmd
|
|
247
|
+
when 'list', nil
|
|
248
|
+
puts PWN::Sessions.list.inspect
|
|
249
|
+
when 'resume'
|
|
250
|
+
sid = args[1]
|
|
251
|
+
hist = PWN::Sessions.to_response_history(session_id: sid)
|
|
252
|
+
puts "Loaded session #{sid} with #{hist[:choices].size} entries (set manually into response_history if needed)"
|
|
253
|
+
when 'delete'
|
|
254
|
+
PWN::Sessions.delete(session_id: args[1])
|
|
255
|
+
puts "Deleted #{args[1]}"
|
|
256
|
+
when 'stats'
|
|
257
|
+
puts PWN::Sessions.stats
|
|
258
|
+
else
|
|
259
|
+
puts PWN::Sessions.help
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
Pry::Commands.create_command 'pwn-ai-cron' do
|
|
265
|
+
description 'Manage scheduled pwn-ai / cron jobs (Hermes equiv).'
|
|
266
|
+
|
|
267
|
+
def process
|
|
268
|
+
cmd = args[0]
|
|
269
|
+
case cmd
|
|
270
|
+
when 'list', nil
|
|
271
|
+
puts PWN::Cron.list.inspect
|
|
272
|
+
when 'create'
|
|
273
|
+
# simplistic: pwn-ai-cron create '0 * * * *' 'prompt here'
|
|
274
|
+
sched = args[1]
|
|
275
|
+
pr = args[2..-1].join(' ')
|
|
276
|
+
job = PWN::Cron.create(schedule: sched, prompt: pr)
|
|
277
|
+
puts "Created #{job}"
|
|
278
|
+
when 'run'
|
|
279
|
+
res = PWN::Cron.run(id: args[1])
|
|
280
|
+
puts res
|
|
281
|
+
when 'remove'
|
|
282
|
+
PWN::Cron.remove(id: args[1])
|
|
283
|
+
puts 'Removed'
|
|
284
|
+
else
|
|
285
|
+
puts PWN::Cron.help
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
Pry::Commands.create_command 'pwn-ai-delegate' do
|
|
291
|
+
description 'Delegate sub-task to a PWN::AI::Agent or simple sub-chat (Hermes delegation equiv).'
|
|
292
|
+
|
|
293
|
+
def process
|
|
294
|
+
goal = args.join(' ')
|
|
295
|
+
puts "[*] Delegating: #{goal}"
|
|
296
|
+
# Simple delegation: use a specialized agent if matches, else another chat turn
|
|
297
|
+
if goal =~ /sast|code|scan/i
|
|
298
|
+
res = PWN::AI::Agent::SAST.analyze(request: goal)
|
|
299
|
+
elsif goal =~ /vuln|report/i
|
|
300
|
+
res = PWN::AI::Agent::VulnGen.analyze(request: goal)
|
|
301
|
+
else
|
|
302
|
+
# fallback sub call to active engine (no full loop here)
|
|
303
|
+
engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
|
|
304
|
+
case engine
|
|
305
|
+
when :anthropic then res = PWN::AI::Anthropic.chat(request: goal)
|
|
306
|
+
when :grok then res = PWN::AI::Grok.chat(request: goal)
|
|
307
|
+
else res = PWN::AI::Ollama.chat(request: goal)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
puts res
|
|
127
311
|
end
|
|
128
312
|
end
|
|
129
313
|
|
|
@@ -339,6 +523,12 @@ module PWN
|
|
|
339
523
|
response_history: response_history,
|
|
340
524
|
spinner: false
|
|
341
525
|
)
|
|
526
|
+
when :anthropic
|
|
527
|
+
response = PWN::AI::Anthropic.chat(
|
|
528
|
+
request: request,
|
|
529
|
+
response_history: response_history,
|
|
530
|
+
spinner: false
|
|
531
|
+
)
|
|
342
532
|
end
|
|
343
533
|
|
|
344
534
|
response_history = {
|
|
@@ -742,9 +932,14 @@ module PWN
|
|
|
742
932
|
pi.config.color = true
|
|
743
933
|
pi.config.pwn_asm = false if pi.config.pwn_asm
|
|
744
934
|
pi.config.pwn_ai = false if pi.config.pwn_ai
|
|
935
|
+
pi.config.pwn_ai_agent = false if pi.config.pwn_ai_agent
|
|
745
936
|
pi.config.pwn_ai_debug = false if pi.config.pwn_ai_debug
|
|
746
937
|
pi.config.pwn_ai_speak = false if pi.config.pwn_ai_speak
|
|
747
938
|
pi.config.completer = Pry::InputCompleter
|
|
939
|
+
if pi.config.pwn_ai_original_input
|
|
940
|
+
Pry.config.input = pi.config.pwn_ai_original_input
|
|
941
|
+
pi.config.pwn_ai_original_input = nil
|
|
942
|
+
end
|
|
748
943
|
return unless pi.config.pwn_mesh
|
|
749
944
|
|
|
750
945
|
pi.config.pwn_mesh = false
|
|
@@ -834,65 +1029,215 @@ module PWN
|
|
|
834
1029
|
|
|
835
1030
|
Pry.config.hooks.add_hook(:after_read, :pwn_ai_hook) do |request, pi|
|
|
836
1031
|
if pi.config.pwn_ai && !request.chomp.empty?
|
|
837
|
-
|
|
1032
|
+
orig_request = pi.input.line_buffer.to_s
|
|
1033
|
+
request = orig_request
|
|
838
1034
|
debug = pi.config.pwn_ai_debug
|
|
839
1035
|
engine = PWN::Env[:ai][:active].to_s.downcase.to_sym
|
|
840
1036
|
response_history = PWN::Env[:ai][engine][:response_history]
|
|
841
1037
|
speak_answer = pi.config.pwn_ai_speak
|
|
1038
|
+
is_agent = (pi.config.pwn_ai_agent == true)
|
|
842
1039
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1040
|
+
# pwn-ai agent mode (Hermes TUI equiv): load skills context for autonomous task carrying
|
|
1041
|
+
skills_context = ''
|
|
1042
|
+
PWN::Skills.each { |n, m| skills_context += "\n--- SKILL #{n} ---\n#{m[:content].to_s[0, 1200]}\n" } if is_agent && PWN.const_defined?(:Skills) && PWN::Skills.is_a?(Hash)
|
|
1043
|
+
|
|
1044
|
+
memory_context = ''
|
|
1045
|
+
memory_context = PWN::Memory.to_context(limit: 25) if is_agent && PWN.const_defined?(:Memory)
|
|
1046
|
+
|
|
1047
|
+
sess_id = begin
|
|
1048
|
+
pi.config.pwn_ai_session_id
|
|
1049
|
+
rescue StandardError
|
|
1050
|
+
nil
|
|
1051
|
+
end
|
|
1052
|
+
|
|
1053
|
+
# Pre-process for clear CLI execution intent (e.g. "what does `id` return?")
|
|
1054
|
+
# This makes the agent actually *run* commands instead of just explaining them.
|
|
1055
|
+
curr_req = request.chomp
|
|
1056
|
+
if is_agent && sess_id && PWN.const_defined?(:Sessions)
|
|
1057
|
+
begin
|
|
1058
|
+
PWN::Sessions.append(session_id: sess_id, role: 'user', content: orig_request)
|
|
1059
|
+
rescue StandardError
|
|
1060
|
+
nil
|
|
1061
|
+
end
|
|
1062
|
+
end
|
|
1063
|
+
if is_agent && request =~ /`([^`]+)`/
|
|
1064
|
+
potential = ::Regexp.last_match(1).strip
|
|
1065
|
+
# Looks like a shell command (not PWN ruby)
|
|
1066
|
+
unless potential =~ /^(PWN::|def |class |require |puts |pp )/
|
|
1067
|
+
curr_req = "The user wants the *actual raw output* of this command (do not just describe it): `#{potential}`. " \
|
|
1068
|
+
'To fulfill the request accurately, you MUST immediately output ONLY a bash code block with the exact command. ' \
|
|
1069
|
+
"Example format: ```bash\n#{potential}\n``` . After the host executes it, you will receive the OBSERVATION with the real output."
|
|
1070
|
+
end
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
# Strict system prompt for agent mode (forces tool use over explanation)
|
|
1074
|
+
system_role = nil
|
|
1075
|
+
if is_agent
|
|
1076
|
+
base = PWN::Env[:ai][engine][:system_role_content] || 'You are an ethical hacker.'
|
|
1077
|
+
system_role = base + <<~PROMPT
|
|
1078
|
+
|
|
1079
|
+
You are operating as a Hermes-style autonomous agent inside the PWN REPL driver.
|
|
1080
|
+
|
|
1081
|
+
PRIMARY RULE FOR CLI AND TOOLS: When the user asks for the output of a command, "what does X return?", "run X", or anything that requires real execution, you MUST use a tool call.#{' '}
|
|
1082
|
+
NEVER just explain what a command does or what its output "would be".#{' '}
|
|
1083
|
+
To execute anything:
|
|
1084
|
+
- Output *exactly and only* a fenced code block.
|
|
1085
|
+
- For shell/CLI: ```bash
|
|
1086
|
+
<exact command here>
|
|
1087
|
+
```
|
|
1088
|
+
- For PWN Ruby modules: ```ruby
|
|
1089
|
+
PWN::Plugins::NmapIt.port_scan(...)
|
|
1090
|
+
```
|
|
1091
|
+
The host will execute it (Ruby in full PWN context, bash via shell) and reply with an OBSERVATION containing the real result.#{' '}
|
|
1092
|
+
Then continue or give the final answer.
|
|
1093
|
+
|
|
1094
|
+
Available tools include all PWN::Plugins (NmapIt, TransparentBrowser, etc.), SAST, Reports, and any CLI via bash blocks.
|
|
1095
|
+
Skills available this session:#{skills_context}
|
|
1096
|
+
#{memory_context}
|
|
1097
|
+
|
|
1098
|
+
PERSISTENT CAPABILITIES (use via ruby code blocks or direct calls):
|
|
1099
|
+
- Memory (cross-session): PWN::Memory.remember(key: :key, value: val, category: :fact|:preference|:lesson)
|
|
1100
|
+
PWN::Memory.recall(query: 'foo'), PWN::Memory.forget(key)
|
|
1101
|
+
- Sessions: current session id = #{sess_id}; PWN::Sessions.append(session_id: '#{sess_id}', role: 'observation', content: obs)
|
|
1102
|
+
- Cron: PWN::Cron.create(schedule: '0 * * * *', prompt: 'task here', name: 'foo')
|
|
1103
|
+
PWN::Cron.run(id: 'id'); list with PWN::Cron.list
|
|
1104
|
+
- Agents/Delegation: PWN::AI::Agent::SAST.analyze(request: ...); PWN::AI::Agent::VulnGen etc.
|
|
1105
|
+
For sub-agents use threads or separate eval calls and feed results back as OBS.
|
|
1106
|
+
|
|
1107
|
+
After receiving an observation, decide the next step or conclude.
|
|
1108
|
+
If you output text without a code block, it will be treated as your final answer to the user.
|
|
1109
|
+
PROMPT
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
max_turns = is_agent ? 7 : 1
|
|
1113
|
+
turn = 0
|
|
1114
|
+
last_response = ''
|
|
1115
|
+
tool_was_executed_this_turn = false
|
|
1116
|
+
|
|
1117
|
+
while turn < max_turns
|
|
1118
|
+
chat_opts = {
|
|
1119
|
+
request: curr_req,
|
|
861
1120
|
response_history: response_history,
|
|
862
1121
|
speak_answer: speak_answer,
|
|
863
1122
|
spinner: true
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1123
|
+
}
|
|
1124
|
+
chat_opts[:system_role_content] = system_role if system_role
|
|
1125
|
+
|
|
1126
|
+
case engine
|
|
1127
|
+
when :grok
|
|
1128
|
+
response = PWN::AI::Grok.chat(**chat_opts)
|
|
1129
|
+
when :ollama
|
|
1130
|
+
response = PWN::AI::Ollama.chat(**chat_opts)
|
|
1131
|
+
when :openai
|
|
1132
|
+
response = PWN::AI::OpenAI.chat(**chat_opts)
|
|
1133
|
+
when :anthropic
|
|
1134
|
+
response = PWN::AI::Anthropic.chat(**chat_opts)
|
|
1135
|
+
else
|
|
1136
|
+
raise "ERROR: Unsupported AI Engine: #{engine}"
|
|
1137
|
+
end
|
|
869
1138
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
last_response = "Model: #{model} not currently supported with API key."
|
|
873
|
-
else
|
|
874
|
-
if response[:choices].last.keys.include?(:text)
|
|
875
|
-
last_response = response[:choices].last[:text]
|
|
1139
|
+
if response.nil?
|
|
1140
|
+
last_response = 'Model not currently supported with API key.'
|
|
876
1141
|
else
|
|
877
|
-
|
|
1142
|
+
if response[:choices].last.keys.include?(:text)
|
|
1143
|
+
last_response = response[:choices].last[:text].to_s
|
|
1144
|
+
else
|
|
1145
|
+
last_response = response[:choices].last[:content].to_s
|
|
1146
|
+
end
|
|
1147
|
+
response_history = {
|
|
1148
|
+
id: response[:id],
|
|
1149
|
+
object: response[:object],
|
|
1150
|
+
model: response[:model],
|
|
1151
|
+
usage: response[:usage]
|
|
1152
|
+
}
|
|
1153
|
+
response_history[:choices] ||= response[:choices]
|
|
878
1154
|
end
|
|
879
1155
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
1156
|
+
puts "\n\001\e[32m\002#{last_response}\001\e[0m\002\n\n"
|
|
1157
|
+
if is_agent && sess_id && PWN.const_defined?(:Sessions)
|
|
1158
|
+
begin
|
|
1159
|
+
PWN::Sessions.append(session_id: sess_id, role: 'assistant', content: last_response)
|
|
1160
|
+
rescue StandardError
|
|
1161
|
+
nil
|
|
1162
|
+
end
|
|
1163
|
+
end
|
|
1164
|
+
|
|
1165
|
+
if debug
|
|
1166
|
+
puts 'DEBUG: response_history => '
|
|
1167
|
+
pp response_history
|
|
1168
|
+
end
|
|
1169
|
+
PWN::Env[:ai][engine][:response_history] = response_history
|
|
1170
|
+
|
|
1171
|
+
# === Agent tool execution: parse code blocks from *this* response and actually run them ===
|
|
1172
|
+
tool_was_executed_this_turn = false
|
|
1173
|
+
if is_agent
|
|
1174
|
+
# Robust regex: tolerate language specifier, extra whitespace, and text around the block
|
|
1175
|
+
last_response.scan(/```(?:\s*(ruby|bash|sh|shell|zsh))?\s*\n?(.*?)\n?```/m).each do |lang, code|
|
|
1176
|
+
code = code.strip
|
|
1177
|
+
next if code.empty? || tool_was_executed_this_turn
|
|
1178
|
+
|
|
1179
|
+
lang = (lang || 'bash').downcase
|
|
1180
|
+
puts "\001\e[33m\002[ pwn-ai AGENT EXEC #{lang} ]\e[0m\002 #{code[0..90]}..."
|
|
1181
|
+
|
|
1182
|
+
obs = ''
|
|
1183
|
+
begin
|
|
1184
|
+
if lang == 'ruby'
|
|
1185
|
+
require 'stringio'
|
|
1186
|
+
old_stdout = $stdout
|
|
1187
|
+
$stdout = StringIO.new
|
|
1188
|
+
res = eval(code, TOPLEVEL_BINDING) # rubocop:disable Security/Eval -- intentional for pwn-ai agent to run PWN Ruby modules/tools in REPL context
|
|
1189
|
+
captured = $stdout.string
|
|
1190
|
+
$stdout = old_stdout
|
|
1191
|
+
obs = (captured + "\n=> #{res.inspect}").strip
|
|
1192
|
+
else
|
|
1193
|
+
# CLI execution - use Open3 for cleaner capture (no extra shell if possible, but backticks are simple and work)
|
|
1194
|
+
require 'open3'
|
|
1195
|
+
stdout, stderr, status = Open3.capture3(code)
|
|
1196
|
+
obs = stdout
|
|
1197
|
+
obs += "\n[stderr]\n#{stderr}" unless stderr.to_s.strip.empty?
|
|
1198
|
+
obs += "\n[exit: #{status.exitstatus}]" unless status.success?
|
|
1199
|
+
obs = obs.strip
|
|
1200
|
+
end
|
|
1201
|
+
rescue StandardError => e
|
|
1202
|
+
obs = "ERROR executing #{lang} block: #{e.class} - #{e.message}"
|
|
1203
|
+
end
|
|
1204
|
+
|
|
1205
|
+
puts "\001\e[36m\002[OBSERVATION from #{lang}]\001\e[0m\002\n#{obs[0..700]}\n"
|
|
1206
|
+
if is_agent && sess_id && PWN.const_defined?(:Sessions)
|
|
1207
|
+
begin
|
|
1208
|
+
PWN::Sessions.append(session_id: sess_id, role: 'observation', content: obs)
|
|
1209
|
+
rescue StandardError
|
|
1210
|
+
nil
|
|
1211
|
+
end
|
|
1212
|
+
end
|
|
1213
|
+
|
|
1214
|
+
# Feed real result back to the model as the next "user" message in the loop
|
|
1215
|
+
curr_req = "OBSERVATION (#{lang} execution result for previous block):\n#{obs}\n\n" \
|
|
1216
|
+
"Now continue fulfilling the original user request: #{orig_request}. " \
|
|
1217
|
+
'If the task is complete, give the final answer (no more code blocks). Otherwise output the next needed tool block.'
|
|
1218
|
+
|
|
1219
|
+
tool_was_executed_this_turn = true
|
|
1220
|
+
turn += 1
|
|
1221
|
+
break # one execution per model turn for controlled pacing
|
|
1222
|
+
end
|
|
1223
|
+
end
|
|
1224
|
+
|
|
1225
|
+
# If we executed something, loop to let the model react to the OBS
|
|
1226
|
+
next if tool_was_executed_this_turn
|
|
1227
|
+
|
|
1228
|
+
# No tool executed this turn -> this last_response is the final answer
|
|
1229
|
+
break
|
|
887
1230
|
end
|
|
888
|
-
puts "\n\001\e[32m\002#{last_response}\001\e[0m\002\n\n"
|
|
889
1231
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1232
|
+
# If in agent mode and the model never produced an executable block but the query clearly wanted execution,
|
|
1233
|
+
# give one last chance with a strong reminder (helps weaker models like some Ollama ones)
|
|
1234
|
+
if is_agent && !tool_was_executed_this_turn && orig_request =~ /`[^`]+`/ && turn < max_turns
|
|
1235
|
+
reminder = 'The user explicitly asked about the output of a command in backticks. ' \
|
|
1236
|
+
'Do not describe the command. Output *only* the corresponding ```bash block now so the host can run it and give you the real result.'
|
|
1237
|
+
curr_req = "#{reminder}\nOriginal: #{orig_request}"
|
|
1238
|
+
# One final direct call (no full re-loop to avoid complexity)
|
|
1239
|
+
# (The main loop already handled most cases; this is a safety net)
|
|
894
1240
|
end
|
|
895
|
-
PWN::Env[:ai][engine][:response_history] = response_history
|
|
896
1241
|
end
|
|
897
1242
|
end
|
|
898
1243
|
|
data/lib/pwn/plugins/xxd.rb
CHANGED
|
@@ -144,6 +144,25 @@ module PWN
|
|
|
144
144
|
)
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
+
# Supported Method Parameters::
|
|
148
|
+
# PWN::Plugins::XXD.reverse_dump(
|
|
149
|
+
# string: 'required - continuous hex string to reverse (i.e. "68656c6c6f")',
|
|
150
|
+
# file: 'required - path to binary file to dump'
|
|
151
|
+
# )
|
|
152
|
+
|
|
153
|
+
def self.reverse_hex_string(opts = {})
|
|
154
|
+
string = opts[:string]
|
|
155
|
+
file = opts[:file]
|
|
156
|
+
|
|
157
|
+
raise ArgumentError, 'string is required' if string.nil?
|
|
158
|
+
|
|
159
|
+
raise ArgumentError, 'output file is required' if file.nil?
|
|
160
|
+
|
|
161
|
+
File.binwrite(file, [string].pack('H*'))
|
|
162
|
+
rescue StandardError => e
|
|
163
|
+
raise e
|
|
164
|
+
end
|
|
165
|
+
|
|
147
166
|
# Supported Method Parameters::
|
|
148
167
|
# PWN::Plugins::XXD.reverse_dump(
|
|
149
168
|
# hexdump: 'required - hexdump returned from #dump method',
|
|
@@ -284,6 +303,11 @@ module PWN
|
|
|
284
303
|
# <step through via F7, F8, F9, etc. to get to desired instruction>
|
|
285
304
|
# ```
|
|
286
305
|
|
|
306
|
+
#{self}.reverse_hex_string(
|
|
307
|
+
string: 'required - continuous hex string to reverse (i.e. \"68656c6f\")',
|
|
308
|
+
file: 'required - path to binary file to dump'
|
|
309
|
+
)
|
|
310
|
+
|
|
287
311
|
#{self}.reverse_dump(
|
|
288
312
|
hexdump: 'required - hexdump returned from #dump method',
|
|
289
313
|
file: 'required - path to binary file to dump',
|