pwn 0.5.721 → 0.5.723
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/Gemfile +2 -2
- data/bin/pwn_setup +5 -5
- data/documentation/AI-Integration.md +34 -1
- data/documentation/Policy-Benchmark.md +151 -0
- data/documentation/Reinforcement-Learning.md +27 -1
- data/etc/default_skills/pwn/ai/agent/engagement/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/learning/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/metrics/SKILL.md +4 -0
- data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/policy/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/reward/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +6 -0
- data/etc/default_skills/pwn/ai/agent/tools/capabilities/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/context/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/context/SKILL.md +50 -0
- data/etc/default_skills/pwn/ai/http_retry/SKILL.md +7 -0
- data/etc/default_skills/pwn/ai/http_retry/references/urls.md +4 -0
- data/etc/default_skills/pwn/ai/open_ai/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/open_ai/references/urls.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/gdb/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/gdbmi/SKILL.md +55 -0
- data/etc/default_skills/pwn/plugins/ghidra_headless/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +6 -0
- data/etc/default_skills/pwn/plugins/packet/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/radare2/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/engagement/SKILL.md +3 -2
- data/lib/pwn/ai/agent/curriculum.rb +37 -45
- data/lib/pwn/ai/agent/dispatch.rb +9 -0
- data/lib/pwn/ai/agent/engagement.rb +59 -0
- data/lib/pwn/ai/agent/learning.rb +111 -44
- data/lib/pwn/ai/agent/loop.rb +50 -8
- data/lib/pwn/ai/agent/metrics.rb +70 -2
- data/lib/pwn/ai/agent/mistakes.rb +67 -1
- data/lib/pwn/ai/agent/policy.rb +142 -16
- data/lib/pwn/ai/agent/registry.rb +1 -1
- data/lib/pwn/ai/agent/reward.rb +153 -53
- data/lib/pwn/ai/agent/swarm.rb +235 -35
- data/lib/pwn/ai/agent/tool_guard.rb +13 -1
- data/lib/pwn/ai/agent/tools/artifacts.rb +50 -3
- data/lib/pwn/ai/agent/tools/capabilities.rb +19 -0
- data/lib/pwn/ai/agent/tools/context.rb +38 -0
- data/lib/pwn/ai/agent/tools/finding_record.rb +18 -0
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +10 -1
- data/lib/pwn/ai/agent/tools/job_run.rb +32 -0
- data/lib/pwn/ai/agent/tools/learning.rb +5 -6
- data/lib/pwn/ai/agent/tools/metrics.rb +16 -0
- data/lib/pwn/ai/agent/tools/pty_session.rb +4 -4
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +6 -5
- data/lib/pwn/ai/agent/tools/shell.rb +10 -1
- data/lib/pwn/ai/agent/tools/skills.rb +30 -0
- data/lib/pwn/ai/agent/tools/swarm.rb +8 -2
- data/lib/pwn/ai/context.rb +193 -0
- data/lib/pwn/ai/http_retry.rb +53 -7
- data/lib/pwn/ai/open_ai.rb +302 -45
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/migrate.rb +10 -1
- data/lib/pwn/plugins/artifact_registry.rb +40 -17
- data/lib/pwn/plugins/binary_parser.rb +4 -1
- data/lib/pwn/plugins/exploit_dev.rb +15 -0
- data/lib/pwn/plugins/findings.rb +48 -8
- data/lib/pwn/plugins/gdb.rb +17 -0
- data/lib/pwn/plugins/gdbmi.rb +128 -0
- data/lib/pwn/plugins/ghidra_headless.rb +104 -0
- data/lib/pwn/plugins/jobs.rb +72 -0
- data/lib/pwn/plugins/packet.rb +51 -0
- data/lib/pwn/plugins/preflight_checker.rb +29 -0
- data/lib/pwn/plugins/process_tube.rb +24 -7
- data/lib/pwn/plugins/radare2.rb +14 -2
- data/lib/pwn/plugins/repl.rb +6 -1
- data/lib/pwn/plugins/transparent_browser.rb +64 -0
- data/lib/pwn/plugins.rb +2 -0
- data/lib/pwn/reports/engagement.rb +19 -0
- data/lib/pwn/sessions.rb +3 -1
- data/lib/pwn/version.rb +1 -1
- data/scripts/benchmark_policy.rb +276 -0
- data/spec/documentation/installation_md_spec.rb +18 -4
- data/spec/integration/reinforced_feedback_loop_spec.rb +20 -13
- data/spec/lib/pwn/ai/agent/curriculum_spec.rb +267 -0
- data/spec/lib/pwn/ai/agent/engagement_spec.rb +12 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +81 -3
- data/spec/lib/pwn/ai/agent/loop_spec.rb +51 -0
- data/spec/lib/pwn/ai/agent/metrics_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +10 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +103 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +62 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +175 -12
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +121 -1
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +6 -0
- data/spec/lib/pwn/ai/agent/tools/capabilities_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/context_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/tools/learning_spec.rb +25 -0
- data/spec/lib/pwn/ai/context_spec.rb +48 -0
- data/spec/lib/pwn/ai/http_retry_spec.rb +27 -0
- data/spec/lib/pwn/ai/open_ai_oauth_transport_spec.rb +245 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +150 -0
- data/spec/lib/pwn/migrate_spec.rb +24 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +19 -0
- data/spec/lib/pwn/plugins/findings_spec.rb +2 -0
- data/spec/lib/pwn/plugins/gdbmi_spec.rb +17 -0
- data/spec/lib/pwn/plugins/ghidra_headless_spec.rb +17 -0
- data/third_party/pwn_rdoc.jsonl +86 -2
- metadata +24 -5
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'fileutils'
|
|
5
|
+
require 'json'
|
|
5
6
|
|
|
6
7
|
module PWN
|
|
7
8
|
module Plugins
|
|
@@ -197,6 +198,14 @@ module PWN
|
|
|
197
198
|
{ path: out, triage: triage, gadgets: gadgets.first(8) }
|
|
198
199
|
end
|
|
199
200
|
|
|
201
|
+
public_class_method def self.from_crash(opts = {})
|
|
202
|
+
path = (opts[:path] || opts[:from_crash]).to_s
|
|
203
|
+
raise 'ERROR: path is required' if path.empty?
|
|
204
|
+
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
205
|
+
|
|
206
|
+
JSON.parse(File.read(path), symbolize_names: true)
|
|
207
|
+
end
|
|
208
|
+
|
|
200
209
|
public_class_method def self.authors
|
|
201
210
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
202
211
|
end
|
|
@@ -344,6 +353,12 @@ module PWN
|
|
|
344
353
|
names: 'optional - Array of symbol names (defaults to system, execve, __libc_start_main)'
|
|
345
354
|
)
|
|
346
355
|
|
|
356
|
+
# Load a crash.json produced by a fuzz campaign.
|
|
357
|
+
#{self}.from_crash(
|
|
358
|
+
path: 'required - filesystem path of crash.json',
|
|
359
|
+
from_crash: 'optional - alias for path'
|
|
360
|
+
)
|
|
361
|
+
|
|
347
362
|
# Print the AUTHOR(S) string for this module.
|
|
348
363
|
#{self}.authors
|
|
349
364
|
"
|
data/lib/pwn/plugins/findings.rb
CHANGED
|
@@ -20,22 +20,37 @@ module PWN
|
|
|
20
20
|
raise 'ERROR: title is required' if title.empty?
|
|
21
21
|
|
|
22
22
|
arts = Array(opts[:poc_artifacts]).map(&:to_s).reject(&:empty?)
|
|
23
|
-
|
|
23
|
+
if opts[:poc].is_a?(Hash)
|
|
24
|
+
arts << opts[:poc][:path].to_s unless opts[:poc][:path].to_s.empty?
|
|
25
|
+
else
|
|
26
|
+
arts << opts[:poc].to_s unless opts[:poc].to_s.empty?
|
|
27
|
+
end
|
|
24
28
|
raise 'ERROR: poc_artifacts are required' if arts.empty?
|
|
25
29
|
|
|
26
|
-
ev = opts[:evidence]
|
|
27
|
-
|
|
30
|
+
ev = opts[:evidence]
|
|
31
|
+
ev_list = Array(ev).map(&:to_s)
|
|
32
|
+
ev_text = ev_list.join(' ')
|
|
33
|
+
raise 'ERROR: evidence must be at least 40 characters citing the PoC' if ev_text.length < 40
|
|
34
|
+
|
|
35
|
+
sha_ev = arts.filter_map do |p|
|
|
36
|
+
next unless File.file?(p)
|
|
28
37
|
|
|
38
|
+
Digest::SHA256.file(p).hexdigest
|
|
39
|
+
end
|
|
40
|
+
proven = sha_ev.any?
|
|
29
41
|
row = {
|
|
30
42
|
id: SecureRandom.hex(6),
|
|
31
43
|
title: title,
|
|
32
|
-
severity: (opts[:severity] || 'info').to_s,
|
|
44
|
+
severity: proven ? (opts[:severity] || 'info').to_s : 'unproven',
|
|
45
|
+
cvss_vector: (opts[:cvss_vector] || opts[:cvss]).to_s,
|
|
46
|
+
affected_asset: (opts[:affected_asset] || opts[:host]).to_s,
|
|
33
47
|
host: opts[:host].to_s,
|
|
34
|
-
evidence:
|
|
35
|
-
poc: opts[:poc].to_s,
|
|
48
|
+
evidence: sha_ev.any? ? sha_ev : ev_list,
|
|
49
|
+
poc: opts[:poc].is_a?(Hash) ? opts[:poc] : { type: 'file', path: arts.first, reproduction_steps: opts[:reproduction_steps].to_s },
|
|
36
50
|
poc_artifacts: arts,
|
|
51
|
+
chain_refs: Array(opts[:chain_refs] || opts[:chain_parent_id]).map(&:to_s).reject(&:empty?),
|
|
37
52
|
cvss: opts[:cvss].to_s,
|
|
38
|
-
status: (opts[:status] || 'open').to_s,
|
|
53
|
+
status: proven ? (opts[:status] || 'open').to_s : 'unproven',
|
|
39
54
|
engagement_id: opts[:engagement_id].to_s,
|
|
40
55
|
chain_parent_id: opts[:chain_parent_id].to_s,
|
|
41
56
|
session_id: opts[:session_id].to_s,
|
|
@@ -98,6 +113,21 @@ module PWN
|
|
|
98
113
|
child.merge(composite_severity: sev)
|
|
99
114
|
end
|
|
100
115
|
|
|
116
|
+
public_class_method def self.chain_score(opts = {})
|
|
117
|
+
ids = Array(opts[:ids] || opts[:chain_refs]).map(&:to_s)
|
|
118
|
+
rows = report.select { |r| ids.include?(r[:id].to_s) || ids.include?(r[:chain_parent_id].to_s) }
|
|
119
|
+
rows = report if rows.empty? && ids.empty?
|
|
120
|
+
ranks = { 'info' => 0, 'low' => 1, 'medium' => 2, 'high' => 3, 'critical' => 4, 'unproven' => 0 }
|
|
121
|
+
peak = rows.map { |r| ranks[r[:severity].to_s] || 0 }.max || 0
|
|
122
|
+
linked = rows.length >= 2
|
|
123
|
+
sev = if linked && peak >= 2
|
|
124
|
+
'critical'
|
|
125
|
+
else
|
|
126
|
+
%w[info low medium high critical][peak] || 'info'
|
|
127
|
+
end
|
|
128
|
+
{ chain_refs: rows.map { |r| r[:id] }, score: sev, combined_severity: sev, n: rows.length }
|
|
129
|
+
end
|
|
130
|
+
|
|
101
131
|
public_class_method def self.render(opts = {})
|
|
102
132
|
dir = opts[:dir_path].to_s
|
|
103
133
|
dir = File.join(Dir.home, '.pwn', 'exports') if dir.empty?
|
|
@@ -148,9 +178,13 @@ module PWN
|
|
|
148
178
|
severity: 'optional - info|low|medium|high|critical (defaults to info)',
|
|
149
179
|
host: 'optional - affected host or URL',
|
|
150
180
|
evidence: 'optional - proof text or path',
|
|
151
|
-
poc: 'optional - filesystem path of a PoC',
|
|
181
|
+
poc: 'optional - filesystem path of a PoC or Hash with type/path/reproduction_steps',
|
|
152
182
|
poc_artifacts: 'required - Array of artifact paths proving the issue',
|
|
153
183
|
cvss: 'optional - CVSS vector or score string',
|
|
184
|
+
cvss_vector: 'optional - CVSS 3.1 vector string (defaults to cvss)',
|
|
185
|
+
affected_asset: 'optional - host or URL the finding applies to (defaults to host)',
|
|
186
|
+
reproduction_steps: 'optional - how to replay the PoC',
|
|
187
|
+
chain_refs: 'optional - Array of related finding ids',
|
|
154
188
|
status: 'optional - open|closed (defaults to open)',
|
|
155
189
|
engagement_id: 'optional - engagement identifier',
|
|
156
190
|
session_id: 'optional - pwn-ai session id',
|
|
@@ -189,6 +223,12 @@ module PWN
|
|
|
189
223
|
name: 'optional - alias for engagement_id'
|
|
190
224
|
)
|
|
191
225
|
|
|
226
|
+
# Recompute combined severity for chained findings.
|
|
227
|
+
#{self}.chain_score(
|
|
228
|
+
ids: 'optional - Array of finding ids to score together',
|
|
229
|
+
chain_refs: 'optional - alias for ids'
|
|
230
|
+
)
|
|
231
|
+
|
|
192
232
|
# Print the AUTHOR(S) string for this module.
|
|
193
233
|
#{self}.authors
|
|
194
234
|
"
|
data/lib/pwn/plugins/gdb.rb
CHANGED
|
@@ -90,6 +90,18 @@ module PWN
|
|
|
90
90
|
crash_info(opts.merge(commands: script))
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
public_class_method def self.ptrace_preflight(opts = {})
|
|
94
|
+
_target = opts[:target]
|
|
95
|
+
scope = 0
|
|
96
|
+
path = '/proc/sys/kernel/yama/ptrace_scope'
|
|
97
|
+
scope = File.read(path).to_i if File.file?(path)
|
|
98
|
+
{
|
|
99
|
+
ptrace_scope: scope,
|
|
100
|
+
attach_ok: scope.zero?,
|
|
101
|
+
hint: scope.positive? ? 'spawn instead of attach' : 'ok'
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
93
105
|
public_class_method def self.authors
|
|
94
106
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
95
107
|
end
|
|
@@ -147,6 +159,11 @@ module PWN
|
|
|
147
159
|
script: 'optional - Array of gdb/MI commands'
|
|
148
160
|
)
|
|
149
161
|
|
|
162
|
+
# Probe yama ptrace_scope before gdb/frida attach.
|
|
163
|
+
#{self}.ptrace_preflight(
|
|
164
|
+
target: 'optional - pid or binary path being attached'
|
|
165
|
+
)
|
|
166
|
+
|
|
150
167
|
# Print the AUTHOR(S) string for this module.
|
|
151
168
|
#{self}.authors
|
|
152
169
|
"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module PWN
|
|
6
|
+
module Plugins
|
|
7
|
+
# GDB machine-interface bridge: breakpoints, stepping, registers,
|
|
8
|
+
# memory, backtraces, checksec. Pairs with ProcessTube for interactive
|
|
9
|
+
# sessions.
|
|
10
|
+
module GDBMI
|
|
11
|
+
public_class_method def self.required_bins
|
|
12
|
+
%w[gdb]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
public_class_method def self.open(opts = {})
|
|
16
|
+
binary = opts[:binary].to_s
|
|
17
|
+
PWN::Plugins::ProcessTube.spawn(cmd: ['gdb', '--interpreter=mi2', '--quiet', binary].reject(&:empty?))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
public_class_method def self.break(opts = {})
|
|
21
|
+
loc = (opts[:location] || opts[:addr] || opts[:symbol]).to_s
|
|
22
|
+
raise 'ERROR: location is required' if loc.empty?
|
|
23
|
+
|
|
24
|
+
mi(opts.merge(cmd: "-break-insert #{loc}"))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
public_class_method def self.step(opts = {})
|
|
28
|
+
mi(opts.merge(cmd: (opts[:into] ? '-exec-step' : '-exec-next')))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
public_class_method def self.registers(opts = {})
|
|
32
|
+
mi(opts.merge(cmd: '-data-list-register-values x'))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
public_class_method def self.read_memory(opts = {})
|
|
36
|
+
addr = (opts[:addr] || opts[:address]).to_s
|
|
37
|
+
raise 'ERROR: addr is required' if addr.empty?
|
|
38
|
+
|
|
39
|
+
n = (opts[:length] || 64).to_i
|
|
40
|
+
mi(opts.merge(cmd: "-data-read-memory-bytes #{addr} #{n}"))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
public_class_method def self.backtrace(opts = {})
|
|
44
|
+
mi(opts.merge(cmd: '-stack-list-frames'))
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
public_class_method def self.checksec(opts = {})
|
|
48
|
+
PWN::Plugins::GDB.mitigations(opts)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
public_class_method def self.mi(opts = {})
|
|
52
|
+
cmd = opts[:cmd].to_s
|
|
53
|
+
raise 'ERROR: cmd is required' if cmd.empty?
|
|
54
|
+
|
|
55
|
+
if opts[:id]
|
|
56
|
+
PWN::Plugins::ProcessTube.write_line(id: opts[:id], line: cmd)
|
|
57
|
+
PWN::Plugins::ProcessTube.recvuntil(id: opts[:id], until: "\n", timeout: opts[:timeout] || 5)
|
|
58
|
+
else
|
|
59
|
+
PWN::Plugins::GDB.batch(opts.merge(commands: [cmd.sub(/\A-/, '')]))
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
public_class_method def self.authors
|
|
64
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
public_class_method def self.help
|
|
68
|
+
puts "USAGE:
|
|
69
|
+
# List host binaries this module expects to be installed.
|
|
70
|
+
#{self}.required_bins
|
|
71
|
+
|
|
72
|
+
# Open gdb --interpreter=mi2 via ProcessTube.
|
|
73
|
+
#{self}.open(
|
|
74
|
+
binary: 'optional - filesystem path of the binary to debug'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Insert a breakpoint (MI -break-insert).
|
|
78
|
+
#{self}.break(
|
|
79
|
+
location: 'required - symbol or address',
|
|
80
|
+
addr: 'optional - alias for location',
|
|
81
|
+
symbol: 'optional - alias for location',
|
|
82
|
+
id: 'optional - ProcessTube id from #open'
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Step (into when into: true, otherwise next).
|
|
86
|
+
#{self}.step(
|
|
87
|
+
into: 'optional - true to step into (defaults to next)',
|
|
88
|
+
id: 'optional - ProcessTube id from #open'
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Read registers via MI.
|
|
92
|
+
#{self}.registers(
|
|
93
|
+
id: 'optional - ProcessTube id from #open'
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Read memory bytes at addr.
|
|
97
|
+
#{self}.read_memory(
|
|
98
|
+
addr: 'required - address to read',
|
|
99
|
+
address: 'optional - alias for addr',
|
|
100
|
+
length: 'optional - byte count (defaults to 64)',
|
|
101
|
+
id: 'optional - ProcessTube id from #open'
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Backtrace via MI -stack-list-frames.
|
|
105
|
+
#{self}.backtrace(
|
|
106
|
+
id: 'optional - ProcessTube id from #open'
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# checksec / mitigations via GDB.mitigations.
|
|
110
|
+
#{self}.checksec(
|
|
111
|
+
binary: 'optional - filesystem path of the binary'
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Send a raw MI command.
|
|
115
|
+
#{self}.mi(
|
|
116
|
+
cmd: 'required - MI command string',
|
|
117
|
+
id: 'optional - ProcessTube id from #open',
|
|
118
|
+
timeout: 'optional - seconds to wait for a reply'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Print the AUTHOR(S) string for this module.
|
|
122
|
+
#{self}.authors
|
|
123
|
+
"
|
|
124
|
+
constants.sort
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'digest'
|
|
6
|
+
require 'open3'
|
|
7
|
+
require 'tmpdir'
|
|
8
|
+
|
|
9
|
+
module PWN
|
|
10
|
+
module Plugins
|
|
11
|
+
# analyzeHeadless wrapper that exports decompiled C plus the symbol
|
|
12
|
+
# table as JSON, cached by binary SHA-256.
|
|
13
|
+
module GhidraHeadless
|
|
14
|
+
public_class_method def self.required_bins
|
|
15
|
+
%w[analyzeHeadless]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
public_class_method def self.analyze(opts = {})
|
|
19
|
+
path = (opts[:bin] || opts[:path]).to_s
|
|
20
|
+
raise 'ERROR: bin is required' if path.empty?
|
|
21
|
+
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
22
|
+
|
|
23
|
+
sha = Digest::SHA256.file(path).hexdigest
|
|
24
|
+
cache = File.join(Dir.home, '.pwn', 'cache', 'ghidra_headless', "#{sha}.json")
|
|
25
|
+
return JSON.parse(File.read(cache), symbolize_names: true).merge(cached: true) if File.file?(cache)
|
|
26
|
+
|
|
27
|
+
row = if PWN::Plugins::PreflightChecker.bin?(name: 'analyzeHeadless')
|
|
28
|
+
run_headless(path: path, sha: sha, function: opts[:function])
|
|
29
|
+
else
|
|
30
|
+
PWN::Plugins::Ghidra.decompile(opts.merge(bin: path))
|
|
31
|
+
end
|
|
32
|
+
FileUtils.mkdir_p(File.dirname(cache))
|
|
33
|
+
File.write(cache, JSON.generate(row))
|
|
34
|
+
row.merge(sha256: sha, cached: false)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
public_class_method def self.decompile(opts = {})
|
|
38
|
+
analyze(opts)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
public_class_method def self.authors
|
|
42
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
public_class_method def self.help
|
|
46
|
+
puts "USAGE:
|
|
47
|
+
# List host binaries this module expects to be installed.
|
|
48
|
+
#{self}.required_bins
|
|
49
|
+
|
|
50
|
+
# Run analyzeHeadless and cache decompiled C + symbols as JSON.
|
|
51
|
+
#{self}.analyze(
|
|
52
|
+
bin: 'required - filesystem path of the binary',
|
|
53
|
+
path: 'optional - alias for bin',
|
|
54
|
+
function: 'optional - function name to decompile'
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Alias of #analyze.
|
|
58
|
+
#{self}.decompile(
|
|
59
|
+
bin: 'required - filesystem path of the binary',
|
|
60
|
+
path: 'optional - alias for bin',
|
|
61
|
+
function: 'optional - function name to decompile'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Print the AUTHOR(S) string for this module.
|
|
65
|
+
#{self}.authors
|
|
66
|
+
"
|
|
67
|
+
constants.sort
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private_class_method def self.run_headless(opts = {})
|
|
71
|
+
path = opts[:path].to_s
|
|
72
|
+
Dir.mktmpdir('pwn-ghidra') do |dir|
|
|
73
|
+
script = File.join(dir, 'export.py')
|
|
74
|
+
out = File.join(dir, 'export.json')
|
|
75
|
+
File.write(script, <<~PY)
|
|
76
|
+
# @runtime PyGhidra
|
|
77
|
+
import json, os
|
|
78
|
+
from ghidra.app.decompiler import DecompInterface
|
|
79
|
+
from ghidra.util.task import ConsoleTaskMonitor
|
|
80
|
+
decomp = DecompInterface()
|
|
81
|
+
decomp.openProgram(currentProgram)
|
|
82
|
+
mon = ConsoleTaskMonitor()
|
|
83
|
+
fns = []
|
|
84
|
+
fm = currentProgram.getFunctionManager()
|
|
85
|
+
it = fm.getFunctions(True)
|
|
86
|
+
while it.hasNext():
|
|
87
|
+
fn = it.next()
|
|
88
|
+
res = decomp.decompileFunction(fn, 30, mon)
|
|
89
|
+
c = res.getDecompiledFunction().getC() if res and res.getDecompiledFunction() else ''
|
|
90
|
+
fns.append({'name': fn.getName(), 'entry': str(fn.getEntryPoint()), 'c': c})
|
|
91
|
+
open(#{out.inspect}, 'w').write(json.dumps({'functions': fns, 'symbols': [s.getName() for s in currentProgram.getSymbolTable().getAllSymbols(True)]}))
|
|
92
|
+
PY
|
|
93
|
+
stdout, stderr, status = Open3.capture3(
|
|
94
|
+
'analyzeHeadless', dir, 'pwn', '-import', path, '-postScript', script, '-deleteProject'
|
|
95
|
+
)
|
|
96
|
+
payload = File.file?(out) ? JSON.parse(File.read(out), symbolize_names: true) : {}
|
|
97
|
+
{ stdout: stdout, stderr: stderr, exit: status.exitstatus, engine: 'ghidra', decompiled: payload, sha256: opts[:sha] }
|
|
98
|
+
end
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
{ error: e.message, engine: 'ghidra', sha256: opts[:sha] }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
data/lib/pwn/plugins/jobs.rb
CHANGED
|
@@ -40,6 +40,39 @@ module PWN
|
|
|
40
40
|
row
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
public_class_method def self.run(opts = {})
|
|
44
|
+
start(opts)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
public_class_method def self.job_run(opts = {})
|
|
48
|
+
start(opts)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
public_class_method def self.job_status(opts = {})
|
|
52
|
+
status(id: opts[:handle] || opts[:id])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
public_class_method def self.job_tail(opts = {})
|
|
56
|
+
tail(id: opts[:handle] || opts[:id], lines: opts[:lines])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
public_class_method def self.job_kill(opts = {})
|
|
60
|
+
stop(id: opts[:handle] || opts[:id])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
public_class_method def self.watch(opts = {})
|
|
64
|
+
row = load_job(opts)
|
|
65
|
+
pattern = opts[:pattern].to_s
|
|
66
|
+
raise 'ERROR: pattern is required' if pattern.empty?
|
|
67
|
+
|
|
68
|
+
log = row[:log].to_s
|
|
69
|
+
return { id: row[:id], hits: [] } unless File.file?(log)
|
|
70
|
+
|
|
71
|
+
rx = Regexp.new(pattern)
|
|
72
|
+
hits = File.readlines(log).grep(rx).last(20)
|
|
73
|
+
{ id: row[:id], hits: hits.map(&:chomp), pattern: pattern }
|
|
74
|
+
end
|
|
75
|
+
|
|
43
76
|
public_class_method def self.status(opts = {})
|
|
44
77
|
row = load_job(opts)
|
|
45
78
|
alive = begin
|
|
@@ -144,6 +177,45 @@ module PWN
|
|
|
144
177
|
# Run stop and return its result
|
|
145
178
|
#{self}.stop
|
|
146
179
|
|
|
180
|
+
# Tail a job log for a regex (AFL new-crash lines).
|
|
181
|
+
#{self}.watch(
|
|
182
|
+
id: 'required - job id from #start',
|
|
183
|
+
pattern: 'required - regex to match in the job log'
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Alias of start that returns a job handle immediately.
|
|
187
|
+
#{self}.run(
|
|
188
|
+
command: 'required - command string to run',
|
|
189
|
+
cmd: 'optional - alias for command',
|
|
190
|
+
session_id: 'optional - pwn-ai session id',
|
|
191
|
+
max_runtime: 'optional - seconds after which a live job is killed'
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Alias of #run.
|
|
195
|
+
#{self}.job_run(
|
|
196
|
+
command: 'required - command string to run',
|
|
197
|
+
cmd: 'optional - alias for command'
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Status by handle or id.
|
|
201
|
+
#{self}.job_status(
|
|
202
|
+
handle: 'optional - job handle from #run',
|
|
203
|
+
id: 'optional - job id alias for handle'
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Tail a job log by handle.
|
|
207
|
+
#{self}.job_tail(
|
|
208
|
+
handle: 'optional - job handle from #run',
|
|
209
|
+
id: 'optional - job id alias for handle',
|
|
210
|
+
lines: 'optional - number of trailing lines'
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Kill a job by handle.
|
|
214
|
+
#{self}.job_kill(
|
|
215
|
+
handle: 'optional - job handle from #run',
|
|
216
|
+
id: 'optional - job id alias for handle'
|
|
217
|
+
)
|
|
218
|
+
|
|
147
219
|
# Print the AUTHOR(S) string for this module.
|
|
148
220
|
#{self}.authors
|
|
149
221
|
"
|
data/lib/pwn/plugins/packet.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'packetfu'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'fileutils'
|
|
4
6
|
require 'packetfu/protos/arp'
|
|
5
7
|
require 'packetfu/protos/eth'
|
|
6
8
|
require 'packetfu/protos/hsrp'
|
|
@@ -1212,6 +1214,43 @@ module PWN
|
|
|
1212
1214
|
{ results: rows, degraded: true, fidelity: 'connect-scan (no raw sockets / custom IP flags)' }
|
|
1213
1215
|
end
|
|
1214
1216
|
|
|
1217
|
+
public_class_method def self.cap_net_raw?(opts = {})
|
|
1218
|
+
_iface = opts[:iface]
|
|
1219
|
+
return true if File.readable?('/dev/bpf0')
|
|
1220
|
+
|
|
1221
|
+
File.read('/proc/self/status').to_s.include?('CapEff:') && begin
|
|
1222
|
+
line = File.readlines('/proc/self/status').find { |l| l.start_with?('CapEff:') }.to_s
|
|
1223
|
+
val = line.split.last.to_s.to_i(16)
|
|
1224
|
+
val.anybits?(0x2000)
|
|
1225
|
+
rescue StandardError
|
|
1226
|
+
false
|
|
1227
|
+
end
|
|
1228
|
+
end
|
|
1229
|
+
|
|
1230
|
+
public_class_method def self.capture(opts = {})
|
|
1231
|
+
iface = (opts[:iface] || 'eth0').to_s
|
|
1232
|
+
count = (opts[:count] || 8).to_i
|
|
1233
|
+
out = (opts[:path] || File.join(Dir.home, '.pwn', 'artifacts', "capture-#{Time.now.to_i}.pcap")).to_s
|
|
1234
|
+
FileUtils.mkdir_p(File.dirname(out))
|
|
1235
|
+
if cap_net_raw?(iface: iface)
|
|
1236
|
+
begin
|
|
1237
|
+
return { path: out, engine: 'packetfu', degraded: false } if defined?(PacketFu)
|
|
1238
|
+
rescue StandardError
|
|
1239
|
+
nil
|
|
1240
|
+
end
|
|
1241
|
+
end
|
|
1242
|
+
if PWN::Plugins::PreflightChecker.bin?(name: 'tcpdump')
|
|
1243
|
+
stdout, stderr, status = Open3.capture3('tcpdump', '-i', iface, '-c', count.to_s, '-w', out)
|
|
1244
|
+
return { path: out, engine: 'tcpdump', exit: status.exitstatus, stdout: stdout, stderr: stderr, degraded: true, hint: 'CAP_NET_RAW missing; used tcpdump' }
|
|
1245
|
+
end
|
|
1246
|
+
{
|
|
1247
|
+
error: 'CAP_NET_RAW missing',
|
|
1248
|
+
capability: 'CAP_NET_RAW',
|
|
1249
|
+
remediation: 'setcap cap_net_raw+ep $(command -v ruby) # or install tcpdump',
|
|
1250
|
+
degraded: true
|
|
1251
|
+
}
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1215
1254
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
1216
1255
|
|
|
1217
1256
|
public_class_method def self.authors
|
|
@@ -1450,6 +1489,18 @@ module PWN
|
|
|
1450
1489
|
timeout: 'optional - connect timeout seconds (defaults to 2)'
|
|
1451
1490
|
)
|
|
1452
1491
|
|
|
1492
|
+
# True when this process has CAP_NET_RAW (or /dev/bpf0 on BSD).
|
|
1493
|
+
#{self}.cap_net_raw?(
|
|
1494
|
+
iface: 'optional - interface name reserved for callers'
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
# Capture packets; falls back to tcpdump when CAP_NET_RAW is missing.
|
|
1498
|
+
#{self}.capture(
|
|
1499
|
+
iface: 'optional - interface (defaults to eth0)',
|
|
1500
|
+
count: 'optional - packet count (defaults to 8)',
|
|
1501
|
+
path: 'optional - output pcap path'
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1453
1504
|
# Print the AUTHOR(S) string for this module.
|
|
1454
1505
|
#{self}.authors
|
|
1455
1506
|
"
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
3
7
|
module PWN
|
|
4
8
|
module Plugins
|
|
5
9
|
# Plugin/binary/capability preflight. Plugins declare required_bins /
|
|
@@ -84,6 +88,26 @@ module PWN
|
|
|
84
88
|
CAPABILITIES.keys.map { |k| capability_coverage(capability: k) }
|
|
85
89
|
end
|
|
86
90
|
|
|
91
|
+
public_class_method def self.manifest(opts = {})
|
|
92
|
+
_refresh = opts[:refresh]
|
|
93
|
+
path = File.join(Dir.home, '.pwn', 'capabilities.json')
|
|
94
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
95
|
+
caps = {}
|
|
96
|
+
PLUGIN_DEPS.each do |mod, dep|
|
|
97
|
+
missing = Array(dep[:bins]).reject { |b| bin?(name: b) }
|
|
98
|
+
caps[mod] = { bins: dep[:bins], missing: missing, ok: missing.empty?, caps: Array(dep[:caps]) }
|
|
99
|
+
end
|
|
100
|
+
row = {
|
|
101
|
+
at: Time.now.utc.iso8601,
|
|
102
|
+
cap_net_raw: cap_net_raw?,
|
|
103
|
+
container: bin?(name: 'podman') || bin?(name: 'docker'),
|
|
104
|
+
plugins: caps,
|
|
105
|
+
coverage: capability_coverage
|
|
106
|
+
}
|
|
107
|
+
File.write(path, JSON.pretty_generate(row))
|
|
108
|
+
row.merge(path: path)
|
|
109
|
+
end
|
|
110
|
+
|
|
87
111
|
TASK_BINS = {
|
|
88
112
|
'proxy' => %w[burpsuite zaproxy],
|
|
89
113
|
'vuln-scan' => %w[nuclei openvas],
|
|
@@ -229,6 +253,11 @@ module PWN
|
|
|
229
253
|
limit: 'optional - limit value consumed by #host_summary'
|
|
230
254
|
)
|
|
231
255
|
|
|
256
|
+
# Refresh ~/.pwn/capabilities.json with bin/CAP_* presence.
|
|
257
|
+
#{self}.manifest(
|
|
258
|
+
refresh: 'optional - unused reserved refresh flag'
|
|
259
|
+
)
|
|
260
|
+
|
|
232
261
|
# Print the AUTHOR(S) string for this module.
|
|
233
262
|
#{self}.authors
|
|
234
263
|
"
|
|
@@ -20,9 +20,9 @@ module PWN
|
|
|
20
20
|
|
|
21
21
|
argv = cmd.is_a?(Array) ? cmd.map(&:to_s) : ['bash', '-lc', cmd.to_s]
|
|
22
22
|
r, w, pid = PTY.spawn(*argv)
|
|
23
|
-
|
|
24
|
-
@tubes[
|
|
25
|
-
{ id:
|
|
23
|
+
name = (opts[:name] || "tube_#{pid}").to_s
|
|
24
|
+
@tubes[name] = { r: r, w: w, pid: pid, buf: +'', started_at: Time.now, last_io: Time.now, name: name, scrollback: +'' }
|
|
25
|
+
{ id: name, pid: pid, name: name }
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
public_class_method def self.connect(opts = {})
|
|
@@ -41,6 +41,8 @@ module PWN
|
|
|
41
41
|
line = opts[:line] || opts[:data] || ''
|
|
42
42
|
t[:w].write("#{line}\n")
|
|
43
43
|
t[:w].flush
|
|
44
|
+
t[:last_io] = Time.now
|
|
45
|
+
t[:scrollback] = (t[:scrollback].to_s + "#{line}\n")[-65_536, 65_536] || (t[:scrollback].to_s + "#{line}\n")
|
|
44
46
|
line.to_s
|
|
45
47
|
end
|
|
46
48
|
|
|
@@ -69,13 +71,14 @@ module PWN
|
|
|
69
71
|
|
|
70
72
|
public_class_method def self.close(opts = {})
|
|
71
73
|
t = tube!(opts)
|
|
74
|
+
dump_scrollback(tube: t, id: opts[:id] || opts[:name])
|
|
72
75
|
t[:w].close
|
|
73
76
|
t[:r].close
|
|
74
77
|
Process.kill('TERM', t[:pid]) if t[:pid]
|
|
75
|
-
@tubes.delete(opts[:id].to_s)
|
|
78
|
+
@tubes.delete((opts[:id] || opts[:name]).to_s)
|
|
76
79
|
true
|
|
77
80
|
rescue StandardError
|
|
78
|
-
@tubes.delete(opts[:id].to_s)
|
|
81
|
+
@tubes.delete((opts[:id] || opts[:name]).to_s)
|
|
79
82
|
false
|
|
80
83
|
end
|
|
81
84
|
|
|
@@ -137,7 +140,8 @@ module PWN
|
|
|
137
140
|
# Run spawn and return its result
|
|
138
141
|
#{self}.spawn(
|
|
139
142
|
cmd: 'required - command string to run (defaults to opts[:command])',
|
|
140
|
-
command: 'optional - command value consumed by #spawn'
|
|
143
|
+
command: 'optional - command value consumed by #spawn',
|
|
144
|
+
name: 'optional - persistent session name reused across tool calls'
|
|
141
145
|
)
|
|
142
146
|
|
|
143
147
|
# Connect a TCP tube with the same write_line/recvuntil API as spawn.
|
|
@@ -202,12 +206,25 @@ module PWN
|
|
|
202
206
|
end
|
|
203
207
|
|
|
204
208
|
private_class_method def self.tube!(opts = {})
|
|
205
|
-
id = opts[:id].to_s
|
|
209
|
+
id = (opts[:id] || opts[:name]).to_s
|
|
206
210
|
t = @tubes[id]
|
|
207
211
|
raise 'ERROR: id is required / unknown tube' unless t
|
|
208
212
|
|
|
209
213
|
t
|
|
210
214
|
end
|
|
215
|
+
|
|
216
|
+
private_class_method def self.dump_scrollback(opts = {})
|
|
217
|
+
t = opts[:tube]
|
|
218
|
+
return unless t
|
|
219
|
+
|
|
220
|
+
body = t[:scrollback].to_s + t[:buf].to_s
|
|
221
|
+
return if body.empty?
|
|
222
|
+
return unless defined?(PWN::Plugins::ArtifactRegistry)
|
|
223
|
+
|
|
224
|
+
PWN::Plugins::ArtifactRegistry.put(bytes: body, kind: 'pty-scrollback', tags: ['pty', opts[:id].to_s])
|
|
225
|
+
rescue StandardError
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
211
228
|
end
|
|
212
229
|
end
|
|
213
230
|
end
|