pwn 0.5.716 → 0.5.718
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 +1 -1
- data/etc/default_skills/pwn/ai/agent/swarm/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/tools/verify/SKILL.md +45 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/exploit_db/SKILL.md +7 -0
- data/etc/default_skills/pwn/plugins/exploit_db/references/urls.md +4 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/findings/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/sarif/SKILL.md +51 -0
- data/etc/default_skills/pwn/reports/sarif/references/urls.md +3 -0
- data/lib/pwn/ai/agent/dispatch.rb +1 -1
- data/lib/pwn/ai/agent/loop.rb +46 -12
- data/lib/pwn/ai/agent/mistakes.rb +5 -2
- data/lib/pwn/ai/agent/prompt_builder.rb +12 -1
- data/lib/pwn/ai/agent/reward.rb +33 -5
- data/lib/pwn/ai/agent/swarm.rb +45 -0
- data/lib/pwn/ai/agent/tool_guard.rb +38 -6
- data/lib/pwn/ai/agent/tools/finding_record.rb +32 -11
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +3 -1
- data/lib/pwn/ai/agent/tools/intel_lookup.rb +7 -3
- data/lib/pwn/ai/agent/tools/pty_session.rb +43 -0
- data/lib/pwn/ai/agent/tools/session_export.rb +2 -2
- data/lib/pwn/ai/agent/tools/shell.rb +4 -1
- data/lib/pwn/ai/agent/tools/verify.rb +63 -0
- data/lib/pwn/memory.rb +56 -6
- data/lib/pwn/plugins/aflplusplus.rb +18 -0
- data/lib/pwn/plugins/binary_parser.rb +23 -5
- data/lib/pwn/plugins/detect_os.rb +2 -14
- data/lib/pwn/plugins/exploit_db.rb +155 -27
- data/lib/pwn/plugins/exploit_dev.rb +33 -5
- data/lib/pwn/plugins/findings.rb +41 -2
- data/lib/pwn/plugins/jobs.rb +43 -2
- data/lib/pwn/plugins/preflight_checker.rb +11 -1
- data/lib/pwn/plugins/process_tube.rb +44 -0
- data/lib/pwn/plugins/radare2.rb +9 -1
- data/lib/pwn/reports/sarif.rb +60 -0
- data/lib/pwn/reports.rb +1 -0
- data/lib/pwn/sessions.rb +22 -4
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/fixtures/error_class_docker.json +1 -1
- data/spec/lib/pwn/ai/agent/mistakes_spec.rb +2 -1
- data/spec/lib/pwn/ai/agent/reward_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +2 -1
- data/spec/lib/pwn/ai/agent/tools/pty_session_spec.rb +1 -0
- data/spec/lib/pwn/ai/agent/tools/verify_spec.rb +24 -0
- data/spec/lib/pwn/plugins/binary_parser_spec.rb +14 -0
- data/spec/lib/pwn/plugins/detect_os_spec.rb +0 -1
- data/spec/lib/pwn/plugins/exploit_db_spec.rb +45 -5
- data/spec/lib/pwn/plugins/findings_spec.rb +13 -1
- data/spec/lib/pwn/reports/sarif_spec.rb +28 -0
- data/third_party/pwn_rdoc.jsonl +28 -1
- metadata +12 -5
- data/etc/intel/files_exploits.csv +0 -2
|
@@ -13,12 +13,16 @@ PWN::AI::Agent::Registry.register(
|
|
|
13
13
|
properties: {
|
|
14
14
|
query: { type: 'string' },
|
|
15
15
|
cve: { type: 'string' },
|
|
16
|
-
cpe: { type: 'string' }
|
|
16
|
+
cpe: { type: 'string' },
|
|
17
|
+
kind: { type: 'string' },
|
|
18
|
+
binary: { type: 'string' },
|
|
19
|
+
technique: { type: 'string' }
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
},
|
|
20
23
|
handler: lambda { |args|
|
|
21
|
-
q = args[:query] || args['query'] || args[:cve] || args['cve'] || args[:cpe] || args['cpe']
|
|
22
|
-
|
|
24
|
+
q = args[:query] || args['query'] || args[:cve] || args['cve'] || args[:cpe] || args['cpe'] || args[:binary] || args['binary']
|
|
25
|
+
kind = args[:kind] || args['kind'] || 'cve'
|
|
26
|
+
PWN::Plugins::ExploitDB.lookup(kind: kind, query: q, binary: args[:binary] || args['binary'], technique: args[:technique] || args['technique'])
|
|
23
27
|
}
|
|
24
28
|
)
|
|
@@ -71,3 +71,46 @@ PWN::AI::Agent::Registry.register(
|
|
|
71
71
|
PWN::Plugins::ProcessTube.close(id: args[:id] || args['id'])
|
|
72
72
|
}
|
|
73
73
|
)
|
|
74
|
+
PWN::AI::Agent::Registry.register(
|
|
75
|
+
name: 'pty_session',
|
|
76
|
+
toolset: 'terminal',
|
|
77
|
+
schema: {
|
|
78
|
+
name: 'pty_session',
|
|
79
|
+
description: 'Interactive PTY: op spawn|expect|send|stream|close.',
|
|
80
|
+
parameters: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
op: { type: 'string' },
|
|
84
|
+
session_id: { type: 'string' },
|
|
85
|
+
id: { type: 'string' },
|
|
86
|
+
cmd: { type: 'string' },
|
|
87
|
+
command: { type: 'string' },
|
|
88
|
+
pattern: { type: 'string' },
|
|
89
|
+
until: { type: 'string' },
|
|
90
|
+
timeout: { type: 'integer' },
|
|
91
|
+
data: { type: 'string' },
|
|
92
|
+
line: { type: 'string' },
|
|
93
|
+
offset: { type: 'integer' }
|
|
94
|
+
},
|
|
95
|
+
required: %w[op]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
handler: lambda { |args|
|
|
99
|
+
op = (args[:op] || args['op']).to_s
|
|
100
|
+
id = args[:id] || args[:session_id] || args['id'] || args['session_id']
|
|
101
|
+
case op
|
|
102
|
+
when 'spawn'
|
|
103
|
+
PWN::Plugins::ProcessTube.spawn(cmd: args[:cmd] || args[:command] || args['cmd'])
|
|
104
|
+
when 'expect'
|
|
105
|
+
PWN::Plugins::ProcessTube.expect(id: id, until: args[:pattern] || args[:until] || args['pattern'], timeout: args[:timeout])
|
|
106
|
+
when 'send'
|
|
107
|
+
PWN::Plugins::ProcessTube.write_line(id: id, line: args[:data] || args[:line] || args['data'])
|
|
108
|
+
when 'stream'
|
|
109
|
+
PWN::Plugins::ProcessTube.stream(id: id, offset: args[:offset])
|
|
110
|
+
when 'close'
|
|
111
|
+
PWN::Plugins::ProcessTube.close(id: id)
|
|
112
|
+
else
|
|
113
|
+
{ error: "unknown op #{op}" }
|
|
114
|
+
end
|
|
115
|
+
}
|
|
116
|
+
)
|
|
@@ -10,11 +10,11 @@ PWN::AI::Agent::Registry.register(
|
|
|
10
10
|
description: 'Export a session transcript plus attached artifacts as a tar.gz under ~/.pwn/exports.',
|
|
11
11
|
parameters: {
|
|
12
12
|
type: 'object',
|
|
13
|
-
properties: { session_id: { type: 'string' } },
|
|
13
|
+
properties: { session_id: { type: 'string' }, engagement_id: { type: 'string' } },
|
|
14
14
|
required: %w[session_id]
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
handler: lambda { |args|
|
|
18
|
-
PWN::Sessions.export(session_id: args[:session_id] || args['session_id'])
|
|
18
|
+
PWN::Sessions.export(session_id: args[:session_id] || args['session_id'], engagement_id: args[:engagement_id] || args['engagement_id'])
|
|
19
19
|
}
|
|
20
20
|
)
|
|
@@ -59,7 +59,10 @@ PWN::AI::Agent::Registry.register(
|
|
|
59
59
|
if cmd.empty? || PWN::AI::Agent::ToolGuard.placeholder?(text: cmd)
|
|
60
60
|
return PWN::AI::Agent::ToolGuard.invalid_payload(
|
|
61
61
|
hint: 'command is required (string). Do not send ..., {...}, {…}, or empty. ' \
|
|
62
|
-
'Example: shell(command="uname -r").'
|
|
62
|
+
'Example: shell(command="uname -r").',
|
|
63
|
+
offending_token: '...',
|
|
64
|
+
code: 'SYNTAX_DENY',
|
|
65
|
+
suggestion: 'replace ellipsis or placeholders with a concrete command'
|
|
63
66
|
)
|
|
64
67
|
end
|
|
65
68
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'pwn/ai/agent/registry'
|
|
7
|
+
|
|
8
|
+
PWN::AI::Agent::Registry.register(
|
|
9
|
+
name: 'verify',
|
|
10
|
+
toolset: 'sessions',
|
|
11
|
+
schema: {
|
|
12
|
+
name: 'verify',
|
|
13
|
+
description: 'Run machine-checkable acceptance checks (file_exists, file_sha256, file_grep, port_open, http_status, cmd_exit0).',
|
|
14
|
+
parameters: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
checks: { type: 'array' }
|
|
18
|
+
},
|
|
19
|
+
required: %w[checks]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
handler: lambda { |args|
|
|
23
|
+
checks = Array(args[:checks] || args['checks'])
|
|
24
|
+
results = checks.map do |row|
|
|
25
|
+
row = {} unless row.is_a?(Hash)
|
|
26
|
+
type = (row[:type] || row['type']).to_s
|
|
27
|
+
a = row[:args] || row['args'] || row
|
|
28
|
+
case type
|
|
29
|
+
when 'file_exists'
|
|
30
|
+
path = (a[:path] || a['path']).to_s
|
|
31
|
+
{ type: type, passed: File.file?(path), path: path }
|
|
32
|
+
when 'file_sha256'
|
|
33
|
+
path = (a[:path] || a['path']).to_s
|
|
34
|
+
want = (a[:sha256] || a['sha256']).to_s
|
|
35
|
+
have = File.file?(path) ? Digest::SHA256.file(path).hexdigest : ''
|
|
36
|
+
{ type: type, passed: !want.empty? && have == want, path: path }
|
|
37
|
+
when 'file_grep'
|
|
38
|
+
path = (a[:path] || a['path']).to_s
|
|
39
|
+
rx = (a[:pattern] || a['pattern']).to_s
|
|
40
|
+
body = File.file?(path) ? File.read(path) : ''
|
|
41
|
+
{ type: type, passed: !rx.empty? && body.match?(Regexp.new(rx)), path: path }
|
|
42
|
+
when 'port_open'
|
|
43
|
+
host = (a[:host] || a['host'] || '127.0.0.1').to_s
|
|
44
|
+
port = (a[:port] || a['port']).to_i
|
|
45
|
+
ok = begin
|
|
46
|
+
TCPSocket.new(host, port).close
|
|
47
|
+
true
|
|
48
|
+
rescue StandardError
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
{ type: type, passed: ok, host: host, port: port }
|
|
52
|
+
when 'http_status'
|
|
53
|
+
{ type: type, passed: false, error: 'use TransparentBrowser for HTTP checks' }
|
|
54
|
+
when 'cmd_exit0'
|
|
55
|
+
cmd = (a[:command] || a['command']).to_s
|
|
56
|
+
{ type: type, passed: system(cmd) == true }
|
|
57
|
+
else
|
|
58
|
+
{ type: type, passed: false, error: 'unknown check type' }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
{ passed: results.all? { |r| r[:passed] }, results: results }
|
|
62
|
+
}
|
|
63
|
+
)
|
data/lib/pwn/memory.rb
CHANGED
|
@@ -15,6 +15,7 @@ module PWN
|
|
|
15
15
|
# via ruby code blocks during execution loops.
|
|
16
16
|
module Memory
|
|
17
17
|
MEMORY_FILE = File.join(Dir.home, '.pwn', 'memory.json')
|
|
18
|
+
ARCHIVE_FILE = File.join(Dir.home, '.pwn', 'memory.archive.json')
|
|
18
19
|
# Lean retention — keep RL-quality signal, drop ephemeral bulk.
|
|
19
20
|
VALUE_MAX_CHARS = 2_000
|
|
20
21
|
PROTECT_KEY_PREFIXES = %w[operator_pref_ process_sop_ mistake_fix_ memory_].freeze
|
|
@@ -111,7 +112,10 @@ module PWN
|
|
|
111
112
|
source: (opts[:source] || 'pwn-ai').to_s,
|
|
112
113
|
confidence: opts[:confidence]&.to_f&.clamp(0.0, 1.0),
|
|
113
114
|
importance: opts[:importance]&.to_f&.clamp(0.0, 1.0),
|
|
114
|
-
ttl: opts[:ttl]&.to_i
|
|
115
|
+
ttl: opts[:ttl]&.to_i,
|
|
116
|
+
namespace: (opts[:namespace] || 'default').to_s,
|
|
117
|
+
last_hit: Time.now.utc.iso8601,
|
|
118
|
+
hits: 1
|
|
115
119
|
}.compact
|
|
116
120
|
mem[key.to_sym] = entry
|
|
117
121
|
save(mem: mem)
|
|
@@ -510,17 +514,62 @@ module PWN
|
|
|
510
514
|
# context = PWN::Memory.to_context(limit: 20)
|
|
511
515
|
# (used internally by pwn-ai hook to inject into system prompt)
|
|
512
516
|
public_class_method def self.to_context(opts = {})
|
|
513
|
-
limit = opts[:limit] ||
|
|
514
|
-
|
|
515
|
-
|
|
517
|
+
limit = (opts[:limit] || 12).to_i
|
|
518
|
+
limit = 12 if limit <= 0
|
|
519
|
+
mem = load
|
|
520
|
+
now = Time.now.utc
|
|
521
|
+
live = {}
|
|
522
|
+
expired = {}
|
|
523
|
+
mem.each do |k, v|
|
|
524
|
+
next unless v.is_a?(Hash)
|
|
525
|
+
|
|
526
|
+
ttl = v[:ttl].to_i
|
|
527
|
+
ts = begin
|
|
528
|
+
Time.parse(v[:timestamp].to_s)
|
|
529
|
+
rescue StandardError
|
|
530
|
+
now
|
|
531
|
+
end
|
|
532
|
+
if ttl.positive? && (now - ts) > ttl
|
|
533
|
+
expired[k] = v
|
|
534
|
+
else
|
|
535
|
+
live[k] = v
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
archive_expired!(rows: expired) unless expired.empty?
|
|
539
|
+
ranked = live.sort_by do |_k, v|
|
|
540
|
+
hits = v[:hits].to_i
|
|
541
|
+
hits = 1 if hits <= 0
|
|
542
|
+
rec = begin
|
|
543
|
+
Time.parse((v[:last_hit] || v[:timestamp]).to_s).to_f
|
|
544
|
+
rescue StandardError
|
|
545
|
+
0
|
|
546
|
+
end
|
|
547
|
+
-(hits * rec)
|
|
548
|
+
end.first(limit)
|
|
549
|
+
return '' if ranked.empty?
|
|
516
550
|
|
|
517
551
|
ctx = "\n\nPERSISTENT MEMORY (cross-session facts, prefs, lessons - use PWN::Memory.remember to store new ones):\n"
|
|
518
|
-
|
|
552
|
+
ranked.each do |k, v|
|
|
519
553
|
ctx += "- #{k} [#{v[:category]} @ #{v[:timestamp]}]: #{v[:value].to_s[0, 300]}\n"
|
|
520
554
|
end
|
|
521
555
|
ctx
|
|
522
556
|
end
|
|
523
557
|
|
|
558
|
+
private_class_method def self.archive_expired!(opts = {})
|
|
559
|
+
rows = opts[:rows]
|
|
560
|
+
return if rows.nil? || rows.empty?
|
|
561
|
+
|
|
562
|
+
arch = File.file?(ARCHIVE_FILE) ? JSON.parse(File.read(ARCHIVE_FILE), symbolize_names: true) : {}
|
|
563
|
+
arch.merge!(rows)
|
|
564
|
+
FileUtils.mkdir_p(File.dirname(ARCHIVE_FILE))
|
|
565
|
+
File.write(ARCHIVE_FILE, JSON.pretty_generate(arch))
|
|
566
|
+
mem = load
|
|
567
|
+
rows.each_key { |k| mem.delete(k) }
|
|
568
|
+
save(mem: mem, force: mem.empty?)
|
|
569
|
+
rescue StandardError
|
|
570
|
+
nil
|
|
571
|
+
end
|
|
572
|
+
|
|
524
573
|
# True when a memory key must survive cap eviction / age GC.
|
|
525
574
|
public_class_method def self.protected_entry?(opts = {})
|
|
526
575
|
key = opts[:key].to_s
|
|
@@ -618,7 +667,8 @@ module PWN
|
|
|
618
667
|
source: 'optional - :human | :reflect | :heuristic | :resolve | :consolidate (M3 provenance)',
|
|
619
668
|
confidence: 'optional - 0.0..1.0 how sure the writer was (M3)',
|
|
620
669
|
importance: 'optional - 0.0..1.0 retrieval/eviction weight (M2/M3)',
|
|
621
|
-
ttl: 'optional - seconds until stale (M3; consolidate evicts stale low-conf first)'
|
|
670
|
+
ttl: 'optional - seconds until stale (M3; consolidate evicts stale low-conf first)',
|
|
671
|
+
namespace: 'optional - memory namespace string (defaults to default)'
|
|
622
672
|
)
|
|
623
673
|
|
|
624
674
|
# Run recall and return its result
|
|
@@ -60,6 +60,17 @@ module PWN
|
|
|
60
60
|
}
|
|
61
61
|
end
|
|
62
62
|
|
|
63
|
+
public_class_method def self.minimize(opts = {})
|
|
64
|
+
crash = opts[:crash].to_s
|
|
65
|
+
out = opts[:out].to_s
|
|
66
|
+
target = opts[:target].to_s
|
|
67
|
+
raise 'ERROR: crash, out, and target are required' if crash.empty? || out.empty? || target.empty?
|
|
68
|
+
return { error: 'afl-tmin missing', hint: 'pwn setup --profile re' } unless PWN::Plugins::PreflightChecker.bin?(name: 'afl-tmin')
|
|
69
|
+
|
|
70
|
+
stdout, stderr, status = Open3.capture3('afl-tmin', '-i', crash, '-o', out, '--', target)
|
|
71
|
+
{ stdout: stdout, stderr: stderr, exit: status.exitstatus, out: out }
|
|
72
|
+
end
|
|
73
|
+
|
|
63
74
|
public_class_method def self.authors
|
|
64
75
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
65
76
|
end
|
|
@@ -91,6 +102,13 @@ module PWN
|
|
|
91
102
|
output: 'optional - alias for out_dir'
|
|
92
103
|
)
|
|
93
104
|
|
|
105
|
+
# Minimize a crash input with afl-tmin when present.
|
|
106
|
+
#{self}.minimize(
|
|
107
|
+
crash: 'required - path to a crashing input',
|
|
108
|
+
out: 'required - path to write the minimized input',
|
|
109
|
+
target: 'required - fuzz target binary'
|
|
110
|
+
)
|
|
111
|
+
|
|
94
112
|
# Print the AUTHOR(S) string for this module.
|
|
95
113
|
#{self}.authors
|
|
96
114
|
"
|
|
@@ -91,17 +91,35 @@ module PWN
|
|
|
91
91
|
format: inf[:format],
|
|
92
92
|
arch: inf[:cpu],
|
|
93
93
|
bits: inf[:cpu].to_s[/\d+/],
|
|
94
|
+
endian: inf[:endian] || inf[:ei_data],
|
|
95
|
+
stripped: inf[:stripped],
|
|
96
|
+
static: inf[:static],
|
|
97
|
+
protections: {
|
|
98
|
+
nx: inf[:nx],
|
|
99
|
+
pie: inf[:pie],
|
|
100
|
+
relro: inf[:relro],
|
|
101
|
+
canary: inf[:canary],
|
|
102
|
+
fortify: inf[:fortify]
|
|
103
|
+
},
|
|
104
|
+
sections: secs,
|
|
94
105
|
imports: imps,
|
|
95
106
|
exports: exps,
|
|
107
|
+
strings_top: Array(inf[:strings]).first(20),
|
|
108
|
+
entry: inf[:entry] || inf[:entrypoint],
|
|
96
109
|
function_count: Array(symbols(opts)).length,
|
|
97
|
-
|
|
110
|
+
notes: []
|
|
98
111
|
}
|
|
99
112
|
sha = Digest::SHA256.file(path).hexdigest
|
|
100
|
-
dir = File.join(Dir.home, '.pwn', '
|
|
113
|
+
dir = File.join(Dir.home, '.pwn', 'cache', 'triage')
|
|
101
114
|
FileUtils.mkdir_p(dir)
|
|
102
|
-
art = File.join(dir, "#{sha
|
|
103
|
-
File.
|
|
104
|
-
|
|
115
|
+
art = File.join(dir, "#{sha}.json")
|
|
116
|
+
if File.file?(art)
|
|
117
|
+
cached = JSON.parse(File.read(art), symbolize_names: true)
|
|
118
|
+
return cached.merge(cached: true, artifact: art)
|
|
119
|
+
end
|
|
120
|
+
body = summary.merge(path: path, sha256: sha)
|
|
121
|
+
File.write(art, JSON.pretty_generate(body))
|
|
122
|
+
body.merge(artifact: art, cached: false)
|
|
105
123
|
end
|
|
106
124
|
|
|
107
125
|
public_class_method def self.authors
|
|
@@ -27,16 +27,6 @@ module PWN
|
|
|
27
27
|
'mswin' => :windows
|
|
28
28
|
}.freeze
|
|
29
29
|
|
|
30
|
-
PATH_NOISE = %w[
|
|
31
|
-
sh bash zsh fish csh tcsh dash true false echo test pwd ls cat cp mv rm
|
|
32
|
-
mkdir rmdir chmod chown chgrp ln touch head tail more less grep egrep fgrep
|
|
33
|
-
sed awk sort uniq wc find xargs tar gzip gunzip zcat bzip2 xz make
|
|
34
|
-
python python2 python3 ruby perl php node java git svn hg
|
|
35
|
-
vi vim nvi nano ed ex
|
|
36
|
-
apt apt-get dpkg rpm yum dnf pacman brew port pkg pkg_add
|
|
37
|
-
sudo doas su login id whoami uname hostname env export
|
|
38
|
-
].freeze
|
|
39
|
-
|
|
40
30
|
public_class_method def self.type
|
|
41
31
|
os = :cygwin if OS.cygwin?
|
|
42
32
|
os = :freebsd if OS.freebsd?
|
|
@@ -193,8 +183,7 @@ module PWN
|
|
|
193
183
|
found << name if ok
|
|
194
184
|
end
|
|
195
185
|
owned = pwn_installed_bins(pwn_bins: opts[:pwn_bins])
|
|
196
|
-
native = (found.map(&:to_s) - owned
|
|
197
|
-
native = native.first(256)
|
|
186
|
+
native = (found.map(&:to_s) - owned).reject(&:empty?).uniq.sort
|
|
198
187
|
cap = if opts.key?(:cap_net_raw)
|
|
199
188
|
opts[:cap_net_raw]
|
|
200
189
|
else
|
|
@@ -205,8 +194,7 @@ module PWN
|
|
|
205
194
|
else
|
|
206
195
|
docker_live
|
|
207
196
|
end
|
|
208
|
-
|
|
209
|
-
summary = "#{flavor} #{ver} #{cpu} native=#{sample}"
|
|
197
|
+
summary = "#{flavor} #{ver} #{cpu}"
|
|
210
198
|
{
|
|
211
199
|
type: os_type,
|
|
212
200
|
distro: flavor,
|
|
@@ -4,11 +4,19 @@ require 'json'
|
|
|
4
4
|
require 'open3'
|
|
5
5
|
require 'csv'
|
|
6
6
|
require 'fileutils'
|
|
7
|
+
require 'net/http'
|
|
8
|
+
require 'uri'
|
|
9
|
+
require 'stringio'
|
|
10
|
+
require 'zlib'
|
|
11
|
+
require 'rubygems/package'
|
|
7
12
|
|
|
8
13
|
module PWN
|
|
9
14
|
module Plugins
|
|
10
15
|
# searchsploit / ExploitDB plus a CPE string builder for nmap versions.
|
|
11
16
|
module ExploitDB
|
|
17
|
+
EXPLOITDB_CSV_URL = 'https://gitlab.com/exploit-database/exploitdb/-/raw/main/files_exploits.csv'
|
|
18
|
+
GTFOBINS_TAR_URL = 'https://codeload.github.com/GTFOBins/GTFOBins.github.io/tar.gz/master'
|
|
19
|
+
|
|
12
20
|
public_class_method def self.required_bins
|
|
13
21
|
%w[searchsploit]
|
|
14
22
|
end
|
|
@@ -41,37 +49,44 @@ module PWN
|
|
|
41
49
|
search(query: q).merge(cpe: cpe, product: product, version: version)
|
|
42
50
|
end
|
|
43
51
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
public_class_method def self.gtfobins(opts = {})
|
|
53
|
+
name = (opts[:binary] || opts[:query] || opts[:q]).to_s.downcase
|
|
54
|
+
raise 'ERROR: binary is required' if name.empty?
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
local_path: row['file'],
|
|
58
|
-
cvss: row['cvss']
|
|
59
|
-
}
|
|
60
|
-
end
|
|
61
|
-
rescue StandardError
|
|
62
|
-
[]
|
|
56
|
+
path = File.join(intel_dir, 'gtfobins.json')
|
|
57
|
+
data = File.file?(path) ? JSON.parse(File.read(path)) : {}
|
|
58
|
+
row = data[name] || data.values.find { |v| v.is_a?(Hash) && v['binary'].to_s.downcase == name }
|
|
59
|
+
{ binary: name, hit: row, source: 'local' }
|
|
63
60
|
end
|
|
64
61
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
public_class_method def self.lookup(opts = {})
|
|
63
|
+
kind = (opts[:kind] || 'cve').to_s
|
|
64
|
+
q = (opts[:query] || opts[:cve] || opts[:binary] || opts[:technique]).to_s
|
|
65
|
+
return gtfobins(binary: q) if %w[binary gtfobins].include?(kind)
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
dest
|
|
67
|
+
search(query: q).merge(kind: kind)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
public_class_method def self.sync(opts = {})
|
|
71
|
+
dest = opts[:dir].to_s
|
|
72
|
+
dest = intel_dir if dest.empty?
|
|
73
|
+
FileUtils.mkdir_p(dest)
|
|
74
|
+
fetched = []
|
|
75
|
+
csv_url = (opts[:csv_url] || EXPLOITDB_CSV_URL).to_s
|
|
76
|
+
bins_url = (opts[:gtfobins_url] || GTFOBINS_TAR_URL).to_s
|
|
77
|
+
return { dest: dest, dry_run: true, urls: [csv_url, bins_url], fetched: [] } if opts[:dry_run]
|
|
78
|
+
|
|
79
|
+
csv_body = http_get(url: csv_url, get: opts[:get], timeout: opts[:timeout])
|
|
80
|
+
csv_path = File.join(dest, 'files_exploits.csv')
|
|
81
|
+
File.binwrite(csv_path, csv_body)
|
|
82
|
+
fetched << 'files_exploits.csv'
|
|
83
|
+
|
|
84
|
+
bins_body = http_get(url: bins_url, get: opts[:get], timeout: opts[:timeout])
|
|
85
|
+
json_path = File.join(dest, 'gtfobins.json')
|
|
86
|
+
File.write(json_path, gtfobins_json(body: bins_body))
|
|
87
|
+
fetched << 'gtfobins.json'
|
|
88
|
+
|
|
89
|
+
{ dest: dest, fetched: fetched, dry_run: false }
|
|
75
90
|
end
|
|
76
91
|
|
|
77
92
|
public_class_method def self.authors
|
|
@@ -96,11 +111,124 @@ module PWN
|
|
|
96
111
|
version: 'optional - version string from nmap_it'
|
|
97
112
|
)
|
|
98
113
|
|
|
114
|
+
# Look up a local GTFOBins-style primitive by binary name.
|
|
115
|
+
#{self}.gtfobins(
|
|
116
|
+
binary: 'required - host binary name (defaults to query/q)',
|
|
117
|
+
query: 'optional - alias for binary',
|
|
118
|
+
q: 'optional - alias for binary'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Offline intel lookup by kind (cve, binary, technique).
|
|
122
|
+
#{self}.lookup(
|
|
123
|
+
kind: 'optional - cve|binary|technique (defaults to cve)',
|
|
124
|
+
query: 'optional - search text',
|
|
125
|
+
cve: 'optional - CVE id',
|
|
126
|
+
binary: 'optional - GTFOBins binary name',
|
|
127
|
+
technique: 'optional - technique keyword'
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Fetch latest ExploitDB CSV and GTFOBins into ~/.pwn/intel (not the pwn tree).
|
|
131
|
+
#{self}.sync(
|
|
132
|
+
dir: 'optional - destination directory (defaults to ~/.pwn/intel)',
|
|
133
|
+
dry_run: 'optional - true to list source URLs without writing',
|
|
134
|
+
timeout: 'optional - HTTP timeout seconds',
|
|
135
|
+
csv_url: 'optional - ExploitDB files_exploits.csv URL override',
|
|
136
|
+
gtfobins_url: 'optional - GTFOBins tarball or JSON URL override',
|
|
137
|
+
get: 'optional - proc taking a URL string and returning the response body'
|
|
138
|
+
)
|
|
139
|
+
|
|
99
140
|
# Print the AUTHOR(S) string for this module.
|
|
100
141
|
#{self}.authors
|
|
101
142
|
"
|
|
102
143
|
constants.sort
|
|
103
144
|
end
|
|
145
|
+
|
|
146
|
+
private_class_method def self.local_hits(opts = {})
|
|
147
|
+
q = opts[:query].to_s.downcase
|
|
148
|
+
path = File.join(intel_dir, 'files_exploits.csv')
|
|
149
|
+
return [] unless File.file?(path)
|
|
150
|
+
|
|
151
|
+
CSV.read(path, headers: true).filter_map do |row|
|
|
152
|
+
blob = row.to_h.values.join(' ').downcase
|
|
153
|
+
next unless blob.include?(q) || q.split.any? { |tok| tok.length > 2 && blob.include?(tok) }
|
|
154
|
+
|
|
155
|
+
{
|
|
156
|
+
cve: row['cve'],
|
|
157
|
+
edb_id: row['id'],
|
|
158
|
+
description: row['description'],
|
|
159
|
+
local_path: row['file'],
|
|
160
|
+
cvss: row['cvss']
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
rescue StandardError
|
|
164
|
+
[]
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private_class_method def self.intel_dir(opts = {})
|
|
168
|
+
custom = opts[:dir].to_s
|
|
169
|
+
return custom unless custom.empty?
|
|
170
|
+
|
|
171
|
+
File.join(Dir.home, '.pwn', 'intel')
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
private_class_method def self.http_get(opts = {})
|
|
175
|
+
url = opts[:url].to_s
|
|
176
|
+
getter = opts[:get]
|
|
177
|
+
return getter.call(url) if getter.respond_to?(:call)
|
|
178
|
+
|
|
179
|
+
timeout = (opts[:timeout] || 120).to_i
|
|
180
|
+
uri = URI.parse(url)
|
|
181
|
+
http_get_uri(uri: uri, timeout: timeout, hops: 0)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private_class_method def self.http_get_uri(opts = {})
|
|
185
|
+
uri = opts[:uri]
|
|
186
|
+
timeout = opts[:timeout].to_i
|
|
187
|
+
hops = opts[:hops].to_i
|
|
188
|
+
raise 'ERROR: too many HTTP redirects' if hops > 8
|
|
189
|
+
|
|
190
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
191
|
+
http.use_ssl = uri.scheme == 'https'
|
|
192
|
+
http.open_timeout = timeout
|
|
193
|
+
http.read_timeout = timeout
|
|
194
|
+
req = Net::HTTP::Get.new(uri.request_uri)
|
|
195
|
+
req['User-Agent'] = 'pwn-exploitdb-sync'
|
|
196
|
+
res = http.request(req)
|
|
197
|
+
case res
|
|
198
|
+
when Net::HTTPRedirection
|
|
199
|
+
nxt = URI.parse(res['location'].to_s)
|
|
200
|
+
nxt = uri + res['location'].to_s unless nxt.absolute?
|
|
201
|
+
http_get_uri(uri: nxt, timeout: timeout, hops: hops + 1)
|
|
202
|
+
else
|
|
203
|
+
raise "ERROR: fetch #{uri} failed: #{res.code}" unless res.is_a?(Net::HTTPSuccess)
|
|
204
|
+
|
|
205
|
+
res.body
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
private_class_method def self.gtfobins_json(opts = {})
|
|
210
|
+
body = opts[:body].to_s
|
|
211
|
+
stripped = body.lstrip
|
|
212
|
+
return body if stripped.start_with?('{', '[')
|
|
213
|
+
|
|
214
|
+
bins = {}
|
|
215
|
+
Zlib::GzipReader.wrap(StringIO.new(body)) do |gz|
|
|
216
|
+
Gem::Package::TarReader.new(gz) do |tar|
|
|
217
|
+
tar.each do |entry|
|
|
218
|
+
next unless entry.file?
|
|
219
|
+
next unless entry.full_name.include?('_gtfobins/') && entry.full_name.end_with?('.md')
|
|
220
|
+
|
|
221
|
+
name = File.basename(entry.full_name, '.md')
|
|
222
|
+
text = entry.read.to_s
|
|
223
|
+
funcs = text.scan(/^##\s+(.+)$/).flatten.map(&:strip)
|
|
224
|
+
bins[name] = { 'binary' => name, 'functions' => funcs }
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
JSON.generate(bins)
|
|
229
|
+
rescue StandardError
|
|
230
|
+
body.start_with?('{') ? body : JSON.generate({})
|
|
231
|
+
end
|
|
104
232
|
end
|
|
105
233
|
end
|
|
106
234
|
end
|
|
@@ -149,11 +149,7 @@ module PWN
|
|
|
149
149
|
|
|
150
150
|
if PWN::Plugins::PreflightChecker.bin?(name: 'r2')
|
|
151
151
|
sid = PWN::Plugins::Radare2.open(path: path)
|
|
152
|
-
|
|
153
|
-
PWN::Plugins::Radare2.cmd(session: sid, cmd: '/R')
|
|
154
|
-
ensure
|
|
155
|
-
PWN::Plugins::Radare2.close(session: sid)
|
|
156
|
-
end
|
|
152
|
+
PWN::Plugins::Radare2.cmd(session: sid, cmd: '/R')
|
|
157
153
|
elsif PWN::Plugins::PreflightChecker.bin?(name: 'ROPgadget')
|
|
158
154
|
`ROPgadget --binary #{path}`.to_s
|
|
159
155
|
else
|
|
@@ -161,6 +157,20 @@ module PWN
|
|
|
161
157
|
end
|
|
162
158
|
end
|
|
163
159
|
|
|
160
|
+
public_class_method def self.fmtstr(opts = {})
|
|
161
|
+
fmt_writes(opts)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
public_class_method def self.io(opts = {})
|
|
165
|
+
kind = (opts[:kind] || opts[:mode] || 'process').to_s
|
|
166
|
+
case kind
|
|
167
|
+
when 'remote'
|
|
168
|
+
PWN::Plugins::ProcessTube.connect(host: opts[:host] || opts[:target], port: opts[:port])
|
|
169
|
+
else
|
|
170
|
+
PWN::Plugins::ProcessTube.spawn(cmd: opts[:cmd] || opts[:command])
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
164
174
|
public_class_method def self.authors
|
|
165
175
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
166
176
|
end
|
|
@@ -266,6 +276,24 @@ module PWN
|
|
|
266
276
|
offset: 'optional - offset value consumed by #fmt_writes'
|
|
267
277
|
)
|
|
268
278
|
|
|
279
|
+
# Alias of fmt_writes for pwntools-style fmtstr helpers.
|
|
280
|
+
#{self}.fmtstr(
|
|
281
|
+
addr: 'optional - address or flag (e.g. main or 0x401000)',
|
|
282
|
+
value: 'optional - integer or string to pack/encode',
|
|
283
|
+
offset: 'optional - offset value consumed by #fmt_writes'
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Open a local process or remote TCP tube for exploit IO.
|
|
287
|
+
#{self}.io(
|
|
288
|
+
kind: 'optional - process|remote|pty_session (defaults to process)',
|
|
289
|
+
mode: 'optional - alias for kind',
|
|
290
|
+
cmd: 'optional - command for process/pty_session',
|
|
291
|
+
command: 'optional - alias for cmd',
|
|
292
|
+
host: 'optional - remote host',
|
|
293
|
+
target: 'optional - alias for host',
|
|
294
|
+
port: 'optional - remote TCP port'
|
|
295
|
+
)
|
|
296
|
+
|
|
269
297
|
# Run gadgets and return its result
|
|
270
298
|
#{self}.gadgets(
|
|
271
299
|
path: 'required - filesystem path of the binary to search for gadgets'
|