pwn 0.5.712 → 0.5.714
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/etc/default_skills/pwn/ai/agent/learning/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/agent/mistakes/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +16 -0
- data/etc/default_skills/pwn/ai/agent/tools/artifacts/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/binary_triage/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/decompile/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/exploitdev/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/finding_record/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/fuzz_campaign/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/intel_lookup/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/job_run/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/pty_session/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/session_export/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/swarm_map/SKILL.md +45 -0
- data/etc/default_skills/pwn/memory_index/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/binary_parser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/detect_os/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/ghidra/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/log/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/preflight_checker/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +2 -0
- data/etc/default_skills/pwn/sessions/SKILL.md +3 -0
- data/etc/default_skills/pwn/setup/references/urls.md +1 -0
- data/etc/intel/files_exploits.csv +2 -0
- data/lib/pwn/ai/agent/dispatch.rb +5 -0
- data/lib/pwn/ai/agent/learning.rb +72 -0
- data/lib/pwn/ai/agent/loop.rb +62 -13
- data/lib/pwn/ai/agent/mistakes.rb +21 -1
- data/lib/pwn/ai/agent/prompt_builder.rb +16 -13
- data/lib/pwn/ai/agent/registry.rb +21 -1
- data/lib/pwn/ai/agent/reward.rb +4 -2
- data/lib/pwn/ai/agent/swarm.rb +26 -0
- data/lib/pwn/ai/agent/tool_guard.rb +201 -1
- data/lib/pwn/ai/agent/tools/artifacts.rb +61 -0
- data/lib/pwn/ai/agent/tools/binary_triage.rb +20 -0
- data/lib/pwn/ai/agent/tools/decompile.rb +27 -0
- data/lib/pwn/ai/agent/tools/exploitdev.rb +45 -0
- data/lib/pwn/ai/agent/tools/finding_record.rb +48 -0
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -0
- data/lib/pwn/ai/agent/tools/intel_lookup.rb +24 -0
- data/lib/pwn/ai/agent/tools/job_run.rb +56 -0
- data/lib/pwn/ai/agent/tools/pty_session.rb +73 -0
- data/lib/pwn/ai/agent/tools/session_export.rb +20 -0
- data/lib/pwn/ai/agent/tools/shell.rb +5 -0
- data/lib/pwn/ai/agent/tools/swarm_map.rb +26 -0
- data/lib/pwn/config.rb +10 -1
- data/lib/pwn/memory_index.rb +50 -0
- data/lib/pwn/plugins/aflplusplus.rb +30 -1
- data/lib/pwn/plugins/artifact_registry.rb +43 -0
- data/lib/pwn/plugins/binary_parser.rb +31 -0
- data/lib/pwn/plugins/detect_os.rb +109 -2
- data/lib/pwn/plugins/exploit_db.rb +38 -1
- data/lib/pwn/plugins/findings.rb +105 -0
- data/lib/pwn/plugins/ghidra.rb +75 -0
- data/lib/pwn/plugins/jobs.rb +12 -1
- data/lib/pwn/plugins/log.rb +18 -3
- data/lib/pwn/plugins/nmap_it.rb +29 -0
- data/lib/pwn/plugins/preflight_checker.rb +36 -0
- data/lib/pwn/plugins/transparent_browser.rb +29 -0
- data/lib/pwn/plugins.rb +2 -0
- data/lib/pwn/sessions.rb +92 -1
- data/lib/pwn/setup.rb +41 -15
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/fixtures/error_class_docker.json +7 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +57 -2
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +21 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +11 -0
- data/spec/lib/pwn/ai/agent/reward_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/swarm_spec.rb +6 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/tools/artifacts_spec.rb +16 -0
- data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/decompile_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/intel_lookup_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +16 -0
- data/spec/lib/pwn/ai/agent/tools/pty_session_spec.rb +17 -0
- data/spec/lib/pwn/ai/agent/tools/session_export_spec.rb +14 -0
- data/spec/lib/pwn/ai/agent/tools/swarm_map_spec.rb +14 -0
- data/spec/lib/pwn/cron_spec.rb +2 -2
- data/spec/lib/pwn/memory_index_spec.rb +9 -0
- data/spec/lib/pwn/plugins/aflplusplus_spec.rb +9 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +12 -0
- data/spec/lib/pwn/plugins/detect_os_spec.rb +40 -0
- data/spec/lib/pwn/plugins/exploit_db_spec.rb +12 -0
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +23 -0
- data/spec/lib/pwn/plugins/findings_spec.rb +30 -0
- data/spec/lib/pwn/plugins/frida_spec.rb +5 -0
- data/spec/lib/pwn/plugins/fuzz_spec.rb +20 -0
- data/spec/lib/pwn/plugins/gdb_spec.rb +15 -0
- data/spec/lib/pwn/plugins/ghidra_spec.rb +14 -0
- data/spec/lib/pwn/plugins/k8s_spec.rb +5 -0
- data/spec/lib/pwn/plugins/log_spec.rb +10 -5
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +4 -0
- data/spec/lib/pwn/plugins/nuclei_spec.rb +6 -0
- data/spec/lib/pwn/plugins/preflight_checker_spec.rb +24 -0
- data/spec/lib/pwn/plugins/process_tube_spec.rb +23 -0
- data/spec/lib/pwn/plugins/semgrep_spec.rb +4 -0
- data/spec/lib/pwn/plugins/sock_spec.rb +7 -0
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +15 -0
- data/spec/lib/pwn/sessions_spec.rb +53 -0
- data/spec/lib/pwn/setup_spec.rb +32 -0
- data/third_party/pwn_rdoc.jsonl +64 -2
- metadata +42 -3
- data/spec/lib/pwn/req_queue_spec.rb +0 -168
- data/tmp_pwn_critic_skills_flaw.txt +0 -9
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pwn/ai/agent/registry'
|
|
4
|
+
|
|
5
|
+
PWN::AI::Agent::Registry.register(
|
|
6
|
+
name: 'swarm_map',
|
|
7
|
+
toolset: 'pwn',
|
|
8
|
+
schema: {
|
|
9
|
+
name: 'swarm_map',
|
|
10
|
+
description: 'Fan-out TCP/port map of a CIDR or host list. Sequential when swarm is off.',
|
|
11
|
+
parameters: {
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
targets: { type: 'string' },
|
|
15
|
+
ports: { type: 'string' }
|
|
16
|
+
},
|
|
17
|
+
required: %w[targets]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
handler: lambda { |args|
|
|
21
|
+
PWN::AI::Agent::Swarm.map_targets(
|
|
22
|
+
targets: args[:targets] || args['targets'],
|
|
23
|
+
ports: args[:ports] || args['ports'] || '80,443'
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
)
|
data/lib/pwn/config.rb
CHANGED
|
@@ -193,7 +193,16 @@ module PWN
|
|
|
193
193
|
# Advisory only: never replaces TaskSummarizer / plan_first.
|
|
194
194
|
policy: true,
|
|
195
195
|
toolsets: nil,
|
|
196
|
-
operator_account: nil
|
|
196
|
+
operator_account: nil,
|
|
197
|
+
sessions_keep_days: 90,
|
|
198
|
+
artifacts_keep_days: 90,
|
|
199
|
+
max_total_mb: 512,
|
|
200
|
+
model_routes: {
|
|
201
|
+
summarize: nil,
|
|
202
|
+
judge: nil,
|
|
203
|
+
act: nil,
|
|
204
|
+
sensitive: nil
|
|
205
|
+
}
|
|
197
206
|
# multi-agent personas : ~/.pwn/agents.yml (see PWN::AI::Agent::Swarm.help)
|
|
198
207
|
# swarm bus : ~/.pwn/swarm/<swarm_id>/bus.jsonl
|
|
199
208
|
}
|
data/lib/pwn/memory_index.rb
CHANGED
|
@@ -272,6 +272,50 @@ module PWN
|
|
|
272
272
|
[]
|
|
273
273
|
end
|
|
274
274
|
|
|
275
|
+
public_class_method def self.bm25(opts = {})
|
|
276
|
+
query = opts[:query].to_s.downcase
|
|
277
|
+
docs = Array(opts[:docs]).map(&:to_s)
|
|
278
|
+
qtok = expand_query(query: query)
|
|
279
|
+
return docs.map { |d| { doc: d, score: 0.0 } } if qtok.empty?
|
|
280
|
+
|
|
281
|
+
df = Hash.new(0)
|
|
282
|
+
tokenized = docs.map do |d|
|
|
283
|
+
toks = d.downcase.scan(/[a-z0-9]{3,}/)
|
|
284
|
+
toks.uniq.each { |t| df[t] += 1 }
|
|
285
|
+
toks
|
|
286
|
+
end
|
|
287
|
+
n = docs.length.to_f
|
|
288
|
+
avgdl = tokenized.sum(&:length) / [n, 1].max
|
|
289
|
+
k1 = 1.2
|
|
290
|
+
b = 0.75
|
|
291
|
+
ranked = docs.each_with_index.map do |doc, i|
|
|
292
|
+
toks = tokenized[i]
|
|
293
|
+
tf = Hash.new(0)
|
|
294
|
+
toks.each { |t| tf[t] += 1 }
|
|
295
|
+
dl = toks.length.to_f
|
|
296
|
+
score = qtok.sum do |t|
|
|
297
|
+
f = tf[t]
|
|
298
|
+
next 0.0 if f.zero?
|
|
299
|
+
|
|
300
|
+
idf = Math.log(((n - df[t] + 0.5) / (df[t] + 0.5)) + 1)
|
|
301
|
+
idf * ((f * (k1 + 1)) / (f + (k1 * (1 - b + (b * dl / avgdl)))))
|
|
302
|
+
end
|
|
303
|
+
{ doc: doc, score: score }
|
|
304
|
+
end
|
|
305
|
+
ranked.sort_by { |h| -h[:score] }
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
private_class_method def self.expand_query(opts = {})
|
|
309
|
+
toks = opts[:query].to_s.downcase.scan(/[a-z0-9]{3,}/)
|
|
310
|
+
table = {
|
|
311
|
+
'bypass' => %w[evasion evade],
|
|
312
|
+
'rate' => %w[throttle],
|
|
313
|
+
'limit' => %w[throttle],
|
|
314
|
+
'login' => %w[auth]
|
|
315
|
+
}
|
|
316
|
+
toks.flat_map { |t| [t] + Array(table[t]) }.uniq
|
|
317
|
+
end
|
|
318
|
+
|
|
275
319
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
276
320
|
|
|
277
321
|
public_class_method def self.authors
|
|
@@ -308,6 +352,12 @@ module PWN
|
|
|
308
352
|
texts: 'required - Array texts value consumed by #embed'
|
|
309
353
|
)
|
|
310
354
|
|
|
355
|
+
# Rank documents by BM25 (pure Ruby; no embedder required).
|
|
356
|
+
#{self}.bm25(
|
|
357
|
+
query: 'required - search text',
|
|
358
|
+
docs: 'required - Array of document strings'
|
|
359
|
+
)
|
|
360
|
+
|
|
311
361
|
# Run reset and return its result
|
|
312
362
|
#{self}.reset
|
|
313
363
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'open3'
|
|
4
4
|
require 'digest'
|
|
5
|
+
require 'json'
|
|
5
6
|
|
|
6
7
|
module PWN
|
|
7
8
|
module Plugins
|
|
@@ -24,16 +25,38 @@ module PWN
|
|
|
24
25
|
{ stdout: stdout, stderr: stderr, exit: status.exitstatus }
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
public_class_method def self.parse_stats(opts = {})
|
|
29
|
+
out_dir = (opts[:out_dir] || opts[:output]).to_s
|
|
30
|
+
raise 'ERROR: out_dir is required' if out_dir.empty?
|
|
31
|
+
|
|
32
|
+
path = File.join(out_dir, 'fuzzer_stats')
|
|
33
|
+
path = File.join(out_dir, 'default', 'fuzzer_stats') unless File.file?(path)
|
|
34
|
+
return {} unless File.file?(path)
|
|
35
|
+
|
|
36
|
+
File.read(path).each_line.with_object({}) do |line, acc|
|
|
37
|
+
key, val = line.split(':', 2)
|
|
38
|
+
next if key.to_s.strip.empty?
|
|
39
|
+
|
|
40
|
+
acc[key.strip.to_sym] = val.to_s.strip
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
27
44
|
public_class_method def self.crash_triage(opts = {})
|
|
28
45
|
out_dir = (opts[:out_dir] || opts[:output]).to_s
|
|
29
46
|
raise 'ERROR: out_dir is required' if out_dir.empty?
|
|
30
47
|
|
|
31
48
|
crashes = Dir[File.join(out_dir, '**/crashes/id:*')]
|
|
32
49
|
hashes = crashes.map { |p| [p, Digest::SHA256.file(p).hexdigest] }
|
|
50
|
+
rows = hashes.map do |path, sha|
|
|
51
|
+
art = File.join(out_dir, "crash-#{sha[0, 12]}.json")
|
|
52
|
+
File.write(art, JSON.generate(path: path, sha256: sha))
|
|
53
|
+
{ path: path, sha256: sha, artifact: art }
|
|
54
|
+
end
|
|
33
55
|
{
|
|
34
56
|
crashes: crashes,
|
|
35
57
|
unique: hashes.map(&:last).uniq,
|
|
36
|
-
asan: crashes.any? { |p| File.binread(p).include?('AddressSanitizer') }
|
|
58
|
+
asan: crashes.any? { |p| File.binread(p).include?('AddressSanitizer') },
|
|
59
|
+
artifacts: rows
|
|
37
60
|
}
|
|
38
61
|
end
|
|
39
62
|
|
|
@@ -56,6 +79,12 @@ module PWN
|
|
|
56
79
|
args: 'optional - Array args value consumed by #fuzz'
|
|
57
80
|
)
|
|
58
81
|
|
|
82
|
+
# Parse AFL fuzzer_stats (execs_per_sec, paths, crashes) from out_dir.
|
|
83
|
+
#{self}.parse_stats(
|
|
84
|
+
out_dir: 'required - AFL output directory containing fuzzer_stats',
|
|
85
|
+
output: 'optional - alias for out_dir'
|
|
86
|
+
)
|
|
87
|
+
|
|
59
88
|
# Dedup AFL++ crash files by sha256 and note AddressSanitizer hits.
|
|
60
89
|
#{self}.crash_triage(
|
|
61
90
|
out_dir: 'required - AFL output directory containing crashes/',
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'json'
|
|
5
|
+
require 'digest'
|
|
5
6
|
|
|
6
7
|
module PWN
|
|
7
8
|
module Plugins
|
|
@@ -40,6 +41,35 @@ module PWN
|
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
public_class_method def self.get(opts = {})
|
|
45
|
+
path = opts[:path].to_s
|
|
46
|
+
raise 'ERROR: path is required' if path.empty?
|
|
47
|
+
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
48
|
+
|
|
49
|
+
{ path: path, sha256: Digest::SHA256.file(path).hexdigest, bytes: File.size(path), body: File.binread(path)[0, 65_536] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
public_class_method def self.read_page(opts = {})
|
|
53
|
+
path = opts[:path].to_s
|
|
54
|
+
raise 'ERROR: path is required' if path.empty?
|
|
55
|
+
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
56
|
+
|
|
57
|
+
offset = opts[:offset].to_i
|
|
58
|
+
length = (opts[:length] || 4_096).to_i
|
|
59
|
+
length = 4_096 if length <= 0
|
|
60
|
+
mode = (opts[:mode] || 'text').to_s
|
|
61
|
+
data = File.binread(path, length, offset).to_s
|
|
62
|
+
case mode
|
|
63
|
+
when 'hex'
|
|
64
|
+
{ path: path, offset: offset, bytes: data.bytesize, hex: data.unpack1('H*') }
|
|
65
|
+
when 'base64'
|
|
66
|
+
require 'base64'
|
|
67
|
+
{ path: path, offset: offset, bytes: data.bytesize, body: Base64.strict_encode64(data) }
|
|
68
|
+
else
|
|
69
|
+
{ path: path, offset: offset, bytes: data.bytesize, body: data.force_encoding('UTF-8').scrub }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
43
73
|
public_class_method def self.authors
|
|
44
74
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
45
75
|
end
|
|
@@ -61,6 +91,19 @@ module PWN
|
|
|
61
91
|
session_id: 'optional - session id value consumed by #list'
|
|
62
92
|
)
|
|
63
93
|
|
|
94
|
+
# Read an artifact file and return sha256 plus a body cap.
|
|
95
|
+
#{self}.get(
|
|
96
|
+
path: 'required - filesystem path of a registered artifact'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Page an artifact as text, hex, or base64 from an offset.
|
|
100
|
+
#{self}.read_page(
|
|
101
|
+
path: 'required - filesystem path of the artifact',
|
|
102
|
+
offset: 'optional - byte offset (defaults to 0)',
|
|
103
|
+
length: 'optional - byte count (defaults to 4096)',
|
|
104
|
+
mode: 'optional - text, hex, or base64 (defaults to text)'
|
|
105
|
+
)
|
|
106
|
+
|
|
64
107
|
# Print the AUTHOR(S) string for this module.
|
|
65
108
|
#{self}.authors
|
|
66
109
|
"
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'metasm'
|
|
4
|
+
require 'digest'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
4
7
|
|
|
5
8
|
module PWN
|
|
6
9
|
module Plugins
|
|
@@ -78,6 +81,29 @@ module PWN
|
|
|
78
81
|
[]
|
|
79
82
|
end
|
|
80
83
|
|
|
84
|
+
public_class_method def self.triage(opts = {})
|
|
85
|
+
path = opts[:path].to_s
|
|
86
|
+
inf = info(opts)
|
|
87
|
+
secs = sections(opts)
|
|
88
|
+
imps = Array(imports(opts)).first(40)
|
|
89
|
+
exps = Array(exports(opts)).first(40)
|
|
90
|
+
summary = {
|
|
91
|
+
format: inf[:format],
|
|
92
|
+
arch: inf[:cpu],
|
|
93
|
+
bits: inf[:cpu].to_s[/\d+/],
|
|
94
|
+
imports: imps,
|
|
95
|
+
exports: exps,
|
|
96
|
+
function_count: Array(symbols(opts)).length,
|
|
97
|
+
section_entropy_map: secs.to_h { |s| [s[:name], s[:size]] }
|
|
98
|
+
}
|
|
99
|
+
sha = Digest::SHA256.file(path).hexdigest
|
|
100
|
+
dir = File.join(Dir.home, '.pwn', 'artifacts', 'triage')
|
|
101
|
+
FileUtils.mkdir_p(dir)
|
|
102
|
+
art = File.join(dir, "#{sha[0, 16]}.json")
|
|
103
|
+
File.write(art, JSON.pretty_generate(summary.merge(path: path, sha256: sha)))
|
|
104
|
+
summary.merge(artifact: art)
|
|
105
|
+
end
|
|
106
|
+
|
|
81
107
|
public_class_method def self.authors
|
|
82
108
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
83
109
|
end
|
|
@@ -114,6 +140,11 @@ module PWN
|
|
|
114
140
|
limit: 'optional - limit value consumed by #relocations'
|
|
115
141
|
)
|
|
116
142
|
|
|
143
|
+
# Structured binary triage JSON plus an artifact path under ~/.pwn/artifacts/triage.
|
|
144
|
+
#{self}.triage(
|
|
145
|
+
path: 'required - filesystem path of the binary to triage'
|
|
146
|
+
)
|
|
147
|
+
|
|
117
148
|
# Print the AUTHOR(S) string for this module.
|
|
118
149
|
#{self}.authors
|
|
119
150
|
"
|
|
@@ -31,10 +31,17 @@ module PWN
|
|
|
31
31
|
os = :cygwin if OS.cygwin?
|
|
32
32
|
os = :freebsd if OS.freebsd?
|
|
33
33
|
os = :linux if OS.linux?
|
|
34
|
-
os = :netbsd if OS.host_os.include?('netbsd')
|
|
35
|
-
os = :openbsd if OS.host_os.include?('openbsd')
|
|
34
|
+
os = :netbsd if OS.host_os.to_s.include?('netbsd')
|
|
35
|
+
os = :openbsd if OS.host_os.to_s.include?('openbsd')
|
|
36
36
|
os = :osx if OS.osx?
|
|
37
37
|
os = :windows if OS.windows?
|
|
38
|
+
if os.nil?
|
|
39
|
+
uname = `uname -s 2>/dev/null`.to_s.downcase
|
|
40
|
+
os = :openbsd if uname.include?('openbsd')
|
|
41
|
+
os = :freebsd if uname.include?('freebsd')
|
|
42
|
+
os = :netbsd if uname.include?('netbsd')
|
|
43
|
+
os = :linux if uname.include?('linux')
|
|
44
|
+
end
|
|
38
45
|
|
|
39
46
|
os
|
|
40
47
|
rescue StandardError => e
|
|
@@ -151,6 +158,56 @@ module PWN
|
|
|
151
158
|
osr['VERSION'].to_s
|
|
152
159
|
end
|
|
153
160
|
|
|
161
|
+
# Supported Method Parameters::
|
|
162
|
+
# PWN::Plugins::DetectOS.living_off_the_land(
|
|
163
|
+
# os_release: 'optional - /etc/os-release body (defaults to reading the file)',
|
|
164
|
+
# which: 'optional - Hash of binary name => present Boolean (skips live PATH)',
|
|
165
|
+
# bins: 'optional - extra binary names to probe besides plugin required_bins'
|
|
166
|
+
# )
|
|
167
|
+
|
|
168
|
+
public_class_method def self.living_off_the_land(opts = {})
|
|
169
|
+
os_type = opts[:type] || type
|
|
170
|
+
flavor = distro(opts)
|
|
171
|
+
ver = version(opts)
|
|
172
|
+
cpu = opts[:arch] || arch
|
|
173
|
+
endn = opts[:endian] || endian
|
|
174
|
+
names = inventory_names(bins: opts[:bins])
|
|
175
|
+
which = opts[:which]
|
|
176
|
+
present = []
|
|
177
|
+
missing = []
|
|
178
|
+
names.each do |name|
|
|
179
|
+
ok = if which.is_a?(Hash)
|
|
180
|
+
which[name] || which[name.to_sym]
|
|
181
|
+
else
|
|
182
|
+
bin_present?(name: name)
|
|
183
|
+
end
|
|
184
|
+
(ok ? present : missing) << name
|
|
185
|
+
end
|
|
186
|
+
cap = if opts.key?(:cap_net_raw)
|
|
187
|
+
opts[:cap_net_raw]
|
|
188
|
+
else
|
|
189
|
+
cap_net_raw_live
|
|
190
|
+
end
|
|
191
|
+
docker = if opts.key?(:docker)
|
|
192
|
+
opts[:docker]
|
|
193
|
+
else
|
|
194
|
+
docker_live
|
|
195
|
+
end
|
|
196
|
+
summary = "#{flavor} #{ver} #{cpu} present=#{present.first(24).join(',')} missing=#{missing.first(12).join(',')}"
|
|
197
|
+
{
|
|
198
|
+
type: os_type,
|
|
199
|
+
distro: flavor,
|
|
200
|
+
version: ver,
|
|
201
|
+
arch: cpu.to_s,
|
|
202
|
+
endian: endn,
|
|
203
|
+
present: present,
|
|
204
|
+
missing: missing,
|
|
205
|
+
cap_net_raw: cap,
|
|
206
|
+
docker: docker,
|
|
207
|
+
summary: summary
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
154
211
|
public_class_method def self.authors
|
|
155
212
|
"AUTHOR(S):
|
|
156
213
|
0day Inc. <support@0dayinc.com>
|
|
@@ -193,12 +250,62 @@ module PWN
|
|
|
193
250
|
version: 'optional - force a version string instead of detecting'
|
|
194
251
|
)
|
|
195
252
|
|
|
253
|
+
# Profile this host: distro, arch, and which plugin bins are on PATH.
|
|
254
|
+
#{self}.living_off_the_land(
|
|
255
|
+
os_release: 'optional - /etc/os-release body (defaults to reading the file)',
|
|
256
|
+
lsb_release: 'optional - /etc/lsb-release body',
|
|
257
|
+
build_prop: 'optional - Android /system/build.prop body',
|
|
258
|
+
type: 'optional - OS family from #type',
|
|
259
|
+
which: 'optional - Hash of binary name => true/false (skips live PATH when set)',
|
|
260
|
+
bins: 'optional - extra binary names to probe besides plugin required_bins',
|
|
261
|
+
arch: 'optional - CPU arch override',
|
|
262
|
+
endian: 'optional - :little or :big override',
|
|
263
|
+
cap_net_raw: 'optional - Boolean CAP_NET_RAW override',
|
|
264
|
+
docker: 'optional - Boolean docker.sock override'
|
|
265
|
+
)
|
|
266
|
+
|
|
196
267
|
# Print the AUTHOR(S) string for this module.
|
|
197
268
|
#{self}.authors
|
|
198
269
|
"
|
|
199
270
|
constants.sort
|
|
200
271
|
end
|
|
201
272
|
|
|
273
|
+
private_class_method def self.inventory_names(opts = {})
|
|
274
|
+
extra = Array(opts[:bins]).map(&:to_s).reject(&:empty?)
|
|
275
|
+
deps = if defined?(PWN::Plugins::PreflightChecker::PLUGIN_DEPS)
|
|
276
|
+
PWN::Plugins::PreflightChecker::PLUGIN_DEPS.values.flat_map { |row| Array(row[:bins]) }
|
|
277
|
+
else
|
|
278
|
+
[]
|
|
279
|
+
end
|
|
280
|
+
(deps + extra).map(&:to_s).reject(&:empty?).uniq
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
private_class_method def self.bin_present?(opts = {})
|
|
284
|
+
name = opts[:name].to_s
|
|
285
|
+
return false if name.empty?
|
|
286
|
+
return PWN::Plugins::PreflightChecker.bin?(name: name) if defined?(PWN::Plugins::PreflightChecker)
|
|
287
|
+
|
|
288
|
+
ENV.fetch('PATH', '').split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, name)) }
|
|
289
|
+
rescue StandardError
|
|
290
|
+
false
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
private_class_method def self.cap_net_raw_live
|
|
294
|
+
return unless defined?(PWN::Plugins::PreflightChecker)
|
|
295
|
+
|
|
296
|
+
PWN::Plugins::PreflightChecker.cap_net_raw?
|
|
297
|
+
rescue StandardError
|
|
298
|
+
nil
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
private_class_method def self.docker_live
|
|
302
|
+
return unless defined?(PWN::Plugins::PreflightChecker)
|
|
303
|
+
|
|
304
|
+
PWN::Plugins::PreflightChecker.service?(name: 'docker', path: '/var/run/docker.sock')
|
|
305
|
+
rescue StandardError
|
|
306
|
+
nil
|
|
307
|
+
end
|
|
308
|
+
|
|
202
309
|
private_class_method def self.normalize_id(opts = {})
|
|
203
310
|
raw = opts[:id].to_s.downcase.strip.gsub(/["']/, '')
|
|
204
311
|
return :unknown if raw.empty?
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'json'
|
|
4
4
|
require 'open3'
|
|
5
|
+
require 'csv'
|
|
6
|
+
require 'fileutils'
|
|
5
7
|
|
|
6
8
|
module PWN
|
|
7
9
|
module Plugins
|
|
@@ -15,7 +17,9 @@ module PWN
|
|
|
15
17
|
q = (opts[:query] || opts[:q]).to_s
|
|
16
18
|
raise 'ERROR: query is required' if q.empty?
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
local = local_hits(query: q)
|
|
21
|
+
return { query: q, results: local, source: 'local' } unless local.empty?
|
|
22
|
+
return { error: 'searchsploit missing', query: q, results: [] } unless PWN::Plugins::PreflightChecker.bin?(name: 'searchsploit')
|
|
19
23
|
|
|
20
24
|
stdout, stderr, status = Open3.capture3('searchsploit', '--json', q)
|
|
21
25
|
parsed = begin
|
|
@@ -37,6 +41,39 @@ module PWN
|
|
|
37
41
|
search(query: q).merge(cpe: cpe, product: product, version: version)
|
|
38
42
|
end
|
|
39
43
|
|
|
44
|
+
private_class_method def self.local_hits(opts = {})
|
|
45
|
+
q = opts[:query].to_s.downcase
|
|
46
|
+
path = intel_csv
|
|
47
|
+
return [] unless File.file?(path)
|
|
48
|
+
|
|
49
|
+
CSV.read(path, headers: true).filter_map do |row|
|
|
50
|
+
blob = row.to_h.values.join(' ').downcase
|
|
51
|
+
next unless blob.include?(q) || q.split.any? { |tok| tok.length > 2 && blob.include?(tok) }
|
|
52
|
+
|
|
53
|
+
{
|
|
54
|
+
cve: row['cve'],
|
|
55
|
+
edb_id: row['id'],
|
|
56
|
+
description: row['description'],
|
|
57
|
+
local_path: row['file'],
|
|
58
|
+
cvss: row['cvss']
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
rescue StandardError
|
|
62
|
+
[]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private_class_method def self.intel_csv
|
|
66
|
+
dest = File.join(Dir.home, '.pwn', 'intel', 'files_exploits.csv')
|
|
67
|
+
return dest if File.file?(dest)
|
|
68
|
+
|
|
69
|
+
bundled = File.expand_path('../../../etc/intel/files_exploits.csv', __dir__)
|
|
70
|
+
if File.file?(bundled)
|
|
71
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
72
|
+
FileUtils.cp(bundled, dest)
|
|
73
|
+
end
|
|
74
|
+
dest
|
|
75
|
+
end
|
|
76
|
+
|
|
40
77
|
public_class_method def self.authors
|
|
41
78
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
42
79
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module Plugins
|
|
9
|
+
# Persistent finding rows for pwn-ai (title, severity, host, evidence, PoC).
|
|
10
|
+
module Findings
|
|
11
|
+
FILE = File.join(Dir.home, '.pwn', 'findings.jsonl')
|
|
12
|
+
|
|
13
|
+
public_class_method def self.required_bins
|
|
14
|
+
[]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
public_class_method def self.record(opts = {})
|
|
18
|
+
title = opts[:title].to_s
|
|
19
|
+
raise 'ERROR: title is required' if title.empty?
|
|
20
|
+
|
|
21
|
+
arts = Array(opts[:poc_artifacts]).map(&:to_s).reject(&:empty?)
|
|
22
|
+
arts << opts[:poc].to_s unless opts[:poc].to_s.empty?
|
|
23
|
+
raise 'ERROR: poc_artifacts are required' if arts.empty?
|
|
24
|
+
|
|
25
|
+
row = {
|
|
26
|
+
id: SecureRandom.hex(6),
|
|
27
|
+
title: title,
|
|
28
|
+
severity: (opts[:severity] || 'info').to_s,
|
|
29
|
+
host: opts[:host].to_s,
|
|
30
|
+
evidence: opts[:evidence].to_s,
|
|
31
|
+
poc: opts[:poc].to_s,
|
|
32
|
+
poc_artifacts: arts,
|
|
33
|
+
session_id: opts[:session_id].to_s,
|
|
34
|
+
at: Time.now.utc.iso8601
|
|
35
|
+
}
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(FILE))
|
|
37
|
+
File.open(FILE, 'a') { |f| f.puts(JSON.generate(row)) }
|
|
38
|
+
row
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
public_class_method def self.report(opts = {})
|
|
42
|
+
return [] unless File.file?(FILE)
|
|
43
|
+
|
|
44
|
+
rows = File.readlines(FILE).filter_map do |ln|
|
|
45
|
+
JSON.parse(ln, symbolize_names: true)
|
|
46
|
+
rescue JSON::ParserError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
host = opts[:host].to_s
|
|
50
|
+
rows = rows.select { |r| r[:host].to_s == host } unless host.empty?
|
|
51
|
+
rows
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
public_class_method def self.render(opts = {})
|
|
55
|
+
dir = opts[:dir_path].to_s
|
|
56
|
+
dir = File.join(Dir.home, '.pwn', 'exports') if dir.empty?
|
|
57
|
+
name = opts[:report_name].to_s
|
|
58
|
+
name = 'findings' if name.empty?
|
|
59
|
+
payload = { title: 'Findings', findings: report }
|
|
60
|
+
{
|
|
61
|
+
markdown: PWN::Reports::Markdown.generate(results_hash: payload, dir_path: dir, report_name: name),
|
|
62
|
+
html: PWN::Reports::HTML.generate(results_hash: payload, dir_path: dir, report_name: name),
|
|
63
|
+
json: PWN::Reports::JSON.generate(results_hash: payload, dir_path: dir, report_name: name)
|
|
64
|
+
}
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
public_class_method def self.authors
|
|
68
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
public_class_method def self.help
|
|
72
|
+
puts "USAGE:
|
|
73
|
+
# List host binaries this module expects to be installed.
|
|
74
|
+
#{self}.required_bins
|
|
75
|
+
|
|
76
|
+
# Append a finding row to ~/.pwn/findings.jsonl.
|
|
77
|
+
#{self}.record(
|
|
78
|
+
title: 'required - short finding title',
|
|
79
|
+
severity: 'optional - info|low|medium|high|critical (defaults to info)',
|
|
80
|
+
host: 'optional - affected host or URL',
|
|
81
|
+
evidence: 'optional - proof text or path',
|
|
82
|
+
poc: 'optional - filesystem path of a PoC',
|
|
83
|
+
poc_artifacts: 'required - Array of artifact paths proving the issue',
|
|
84
|
+
session_id: 'optional - pwn-ai session id'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# List recorded findings, optionally filtered by host.
|
|
88
|
+
#{self}.report(
|
|
89
|
+
host: 'optional - only rows whose host matches this string'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Render findings as markdown, html, and json reports.
|
|
93
|
+
#{self}.render(
|
|
94
|
+
dir_path: 'optional - output directory (defaults to ~/.pwn/exports)',
|
|
95
|
+
report_name: 'optional - basename without extension (defaults to findings)'
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Print the AUTHOR(S) string for this module.
|
|
99
|
+
#{self}.authors
|
|
100
|
+
"
|
|
101
|
+
constants.sort
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module PWN
|
|
8
|
+
module Plugins
|
|
9
|
+
# Headless Ghidra analyzeHeadless wrapper with r2 fallback.
|
|
10
|
+
module Ghidra
|
|
11
|
+
public_class_method def self.required_bins
|
|
12
|
+
%w[analyzeHeadless]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
public_class_method def self.analyze(opts = {})
|
|
16
|
+
path = (opts[:bin] || opts[:path]).to_s
|
|
17
|
+
raise 'ERROR: bin is required' if path.empty?
|
|
18
|
+
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
19
|
+
|
|
20
|
+
if PWN::Plugins::PreflightChecker.bin?(name: 'analyzeHeadless')
|
|
21
|
+
proj = opts[:project_dir].to_s
|
|
22
|
+
proj = File.join(Dir.home, '.pwn', 'ghidra') if proj.empty?
|
|
23
|
+
FileUtils.mkdir_p(proj)
|
|
24
|
+
stdout, stderr, status = Open3.capture3('analyzeHeadless', proj, 'pwn', '-import', path, '-deleteProject')
|
|
25
|
+
return { stdout: stdout, stderr: stderr, exit: status.exitstatus, engine: 'ghidra' }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return r2_fallback(path: path) if PWN::Plugins::PreflightChecker.bin?(name: 'r2')
|
|
29
|
+
|
|
30
|
+
{ error: 'analyzeHeadless missing', hint: 'install Ghidra or r2; pwn setup --profile re' }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
public_class_method def self.decompile(opts = {})
|
|
34
|
+
analyze(opts)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private_class_method def self.r2_fallback(opts = {})
|
|
38
|
+
path = opts[:path].to_s
|
|
39
|
+
stdout, stderr, status = Open3.capture3('r2', '-q', '-c', 'aaa;s main;pdg', path)
|
|
40
|
+
{ stdout: stdout, stderr: stderr, exit: status.exitstatus, engine: 'r2' }
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
{ error: e.message, engine: 'r2' }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
public_class_method def self.authors
|
|
46
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
public_class_method def self.help
|
|
50
|
+
puts "USAGE:
|
|
51
|
+
# List host binaries this module expects to be installed.
|
|
52
|
+
#{self}.required_bins
|
|
53
|
+
|
|
54
|
+
# Run analyzeHeadless (or r2 pdg) against a binary.
|
|
55
|
+
#{self}.analyze(
|
|
56
|
+
bin: 'required - filesystem path of the binary',
|
|
57
|
+
path: 'optional - alias for bin',
|
|
58
|
+
project_dir: 'optional - Ghidra project directory under ~/.pwn/ghidra'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Decompile a binary; same as #analyze when Ghidra/r2 is the engine.
|
|
62
|
+
#{self}.decompile(
|
|
63
|
+
bin: 'required - filesystem path of the binary',
|
|
64
|
+
path: 'optional - alias for bin',
|
|
65
|
+
project_dir: 'optional - Ghidra project directory'
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Print the AUTHOR(S) string for this module.
|
|
69
|
+
#{self}.authors
|
|
70
|
+
"
|
|
71
|
+
constants.sort
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|