pwn 0.5.709 → 0.5.710
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/documentation/Installation.md +1 -1
- data/etc/default_skills/exploitdev/SKILL.md +38 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/credential_attack/SKILL.md +7 -0
- data/etc/default_skills/pwn/plugins/credential_attack/references/urls.md +3 -0
- data/etc/default_skills/pwn/plugins/exploit_db/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/exploit_dev/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/frida/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/fuzz/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/gdb/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/k8s/SKILL.md +51 -0
- data/etc/default_skills/pwn/plugins/nuclei/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/{doctor → preflight_checker}/SKILL.md +13 -11
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/recon/references/urls.md +1 -0
- data/etc/default_skills/pwn/plugins/semgrep/SKILL.md +48 -0
- data/etc/default_skills/pwn/plugins/volatility/SKILL.md +1 -0
- data/lib/pwn/ai/agent/prompt_builder.rb +1 -1
- data/lib/pwn/ai/agent/tools/sessions.rb +8 -1
- data/lib/pwn/ffi/stdio.rb +3 -1
- data/lib/pwn/plugins/aflplusplus.rb +21 -1
- data/lib/pwn/plugins/android.rb +9 -3
- data/lib/pwn/plugins/credential_attack.rb +62 -3
- data/lib/pwn/plugins/exploit_db.rb +69 -0
- data/lib/pwn/plugins/exploit_dev.rb +69 -3
- data/lib/pwn/plugins/frida.rb +60 -9
- data/lib/pwn/plugins/fuzz.rb +33 -0
- data/lib/pwn/plugins/gdb.rb +25 -5
- data/lib/pwn/plugins/jobs.rb +1 -0
- data/lib/pwn/plugins/k8s.rb +73 -0
- data/lib/pwn/plugins/nuclei.rb +36 -3
- data/lib/pwn/plugins/packet.rb +1 -1
- data/lib/pwn/plugins/{doctor.rb → preflight_checker.rb} +22 -5
- data/lib/pwn/plugins/process_tube.rb +20 -1
- data/lib/pwn/plugins/radare2.rb +1 -1
- data/lib/pwn/plugins/recon.rb +39 -4
- data/lib/pwn/plugins/semgrep.rb +61 -0
- data/lib/pwn/plugins/sock.rb +1 -0
- data/lib/pwn/plugins/sqlmap.rb +1 -1
- data/lib/pwn/plugins/volatility.rb +21 -2
- data/lib/pwn/plugins.rb +4 -1
- data/lib/pwn/setup.rb +13 -1
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/help_contract_spec.rb +2 -0
- data/spec/lib/pwn/plugins/exploit_db_spec.rb +13 -0
- data/spec/lib/pwn/plugins/k8s_spec.rb +13 -0
- data/spec/lib/pwn/plugins/{doctor_spec.rb → preflight_checker_spec.rb} +1 -1
- data/spec/lib/pwn/plugins/semgrep_spec.rb +13 -0
- data/spec/lib/pwn/req_queue_spec.rb +168 -0
- data/third_party/pwn_rdoc.jsonl +43 -9
- metadata +20 -8
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
module PWN
|
|
7
|
+
module Plugins
|
|
8
|
+
# searchsploit / ExploitDB plus a CPE string builder for nmap versions.
|
|
9
|
+
module ExploitDB
|
|
10
|
+
public_class_method def self.required_bins
|
|
11
|
+
%w[searchsploit]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
public_class_method def self.search(opts = {})
|
|
15
|
+
q = (opts[:query] || opts[:q]).to_s
|
|
16
|
+
raise 'ERROR: query is required' if q.empty?
|
|
17
|
+
|
|
18
|
+
return { error: 'searchsploit missing', query: q } unless PWN::Plugins::PreflightChecker.bin?(name: 'searchsploit')
|
|
19
|
+
|
|
20
|
+
stdout, stderr, status = Open3.capture3('searchsploit', '--json', q)
|
|
21
|
+
parsed = begin
|
|
22
|
+
JSON.parse(stdout)
|
|
23
|
+
rescue JSON::ParserError
|
|
24
|
+
{}
|
|
25
|
+
end
|
|
26
|
+
{ query: q, results: parsed['RESULTS_EXPLOIT'] || parsed['results'] || [], stderr: stderr, exit: status.exitstatus }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
public_class_method def self.cve_for_cpe(opts = {})
|
|
30
|
+
cpe = opts[:cpe].to_s
|
|
31
|
+
product = opts[:product].to_s
|
|
32
|
+
version = opts[:version].to_s
|
|
33
|
+
q = cpe
|
|
34
|
+
q = "#{product} #{version}".strip if q.empty?
|
|
35
|
+
raise 'ERROR: cpe or product is required' if q.empty?
|
|
36
|
+
|
|
37
|
+
search(query: q).merge(cpe: cpe, product: product, version: version)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
public_class_method def self.authors
|
|
41
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
public_class_method def self.help
|
|
45
|
+
puts "USAGE:
|
|
46
|
+
# List host binaries this module expects to be installed.
|
|
47
|
+
#{self}.required_bins
|
|
48
|
+
|
|
49
|
+
# Search local ExploitDB via searchsploit --json.
|
|
50
|
+
#{self}.search(
|
|
51
|
+
query: 'required - product, CVE, or keyword',
|
|
52
|
+
q: 'optional - alias for query'
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Build a search from an nmap CPE/product/version tuple.
|
|
56
|
+
#{self}.cve_for_cpe(
|
|
57
|
+
cpe: 'optional - CPE string (e.g. cpe:/a:apache:http_server:2.4.49)',
|
|
58
|
+
product: 'optional - product name when no CPE',
|
|
59
|
+
version: 'optional - version string from nmap_it'
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Print the AUTHOR(S) string for this module.
|
|
63
|
+
#{self}.authors
|
|
64
|
+
"
|
|
65
|
+
constants.sort
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
3
5
|
module PWN
|
|
4
6
|
module Plugins
|
|
5
7
|
# p8/p16/p32/p64, cyclic, flat, fmt writes, gadget search.
|
|
@@ -83,6 +85,8 @@ module PWN
|
|
|
83
85
|
public_class_method def self.shellcode(opts = {})
|
|
84
86
|
arch = (opts[:arch] || 'x86_64').to_s
|
|
85
87
|
kind = (opts[:kind] || opts[:payload] || 'nop').to_s
|
|
88
|
+
return execve_bin_sh_bytes(arch: arch) if kind == 'execve_bin_sh'
|
|
89
|
+
|
|
86
90
|
asm = case kind
|
|
87
91
|
when 'nop' then 'nop'
|
|
88
92
|
when 'ret' then 'ret'
|
|
@@ -93,6 +97,40 @@ module PWN
|
|
|
93
97
|
PWN::Plugins::Assembly.asm_to_opcodes(asm: asm, arch: arch, endian: opts[:endian])
|
|
94
98
|
end
|
|
95
99
|
|
|
100
|
+
public_class_method def self.one_gadget(opts = {})
|
|
101
|
+
path = (opts[:path] || opts[:libc]).to_s
|
|
102
|
+
raise 'ERROR: path is required' if path.empty?
|
|
103
|
+
|
|
104
|
+
return { error: 'one_gadget missing', hint: 'pwn setup --profile re', path: path } unless PWN::Plugins::PreflightChecker.bin?(name: 'one_gadget')
|
|
105
|
+
|
|
106
|
+
stdout, stderr, status = Open3.capture3('one_gadget', path)
|
|
107
|
+
{
|
|
108
|
+
path: path,
|
|
109
|
+
stdout: stdout,
|
|
110
|
+
stderr: stderr,
|
|
111
|
+
exit: status.exitstatus,
|
|
112
|
+
gadgets: stdout.scan(/0x[0-9a-fA-F]+/)
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
public_class_method def self.libc_offsets(opts = {})
|
|
117
|
+
path = opts[:path].to_s
|
|
118
|
+
raise 'ERROR: path is required' if path.empty?
|
|
119
|
+
|
|
120
|
+
wanted = Array(opts[:names] || %w[system execve __libc_start_main])
|
|
121
|
+
syms = PWN::Plugins::BinaryParser.symbols(path: path, limit: 20_000)
|
|
122
|
+
map = {}
|
|
123
|
+
Array(syms).each do |s|
|
|
124
|
+
n = s[:name].to_s
|
|
125
|
+
next if n.empty?
|
|
126
|
+
|
|
127
|
+
wanted.each do |w|
|
|
128
|
+
map[w.to_s] = s[:value] if n == w.to_s || n.end_with?(w.to_s)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
{ path: path, symbols: map }
|
|
132
|
+
end
|
|
133
|
+
|
|
96
134
|
public_class_method def self.fmt_writes(opts = {})
|
|
97
135
|
addr = (opts[:addr] || 0).to_i
|
|
98
136
|
value = (opts[:value] || 0).to_i
|
|
@@ -109,14 +147,14 @@ module PWN
|
|
|
109
147
|
path = opts[:path].to_s
|
|
110
148
|
raise 'ERROR: path is required' if path.empty?
|
|
111
149
|
|
|
112
|
-
if PWN::Plugins::
|
|
150
|
+
if PWN::Plugins::PreflightChecker.bin?(name: 'r2')
|
|
113
151
|
sid = PWN::Plugins::Radare2.open(path: path)
|
|
114
152
|
begin
|
|
115
153
|
PWN::Plugins::Radare2.cmd(session: sid, cmd: '/R')
|
|
116
154
|
ensure
|
|
117
155
|
PWN::Plugins::Radare2.close(session: sid)
|
|
118
156
|
end
|
|
119
|
-
elsif PWN::Plugins::
|
|
157
|
+
elsif PWN::Plugins::PreflightChecker.bin?(name: 'ROPgadget')
|
|
120
158
|
`ROPgadget --binary #{path}`.to_s
|
|
121
159
|
else
|
|
122
160
|
{ error: 'r2 and ROPgadget missing', hint: 'pwn setup --profile re' }
|
|
@@ -230,7 +268,19 @@ module PWN
|
|
|
230
268
|
|
|
231
269
|
# Run gadgets and return its result
|
|
232
270
|
#{self}.gadgets(
|
|
233
|
-
path: 'required - filesystem path to
|
|
271
|
+
path: 'required - filesystem path of the binary to search for gadgets'
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Resolve one_gadget offsets in a libc (needs the one_gadget gem/bin).
|
|
275
|
+
#{self}.one_gadget(
|
|
276
|
+
path: 'required - filesystem path to libc.so',
|
|
277
|
+
libc: 'optional - alias for path'
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Map named libc symbols to virtual addresses via BinaryParser.
|
|
281
|
+
#{self}.libc_offsets(
|
|
282
|
+
path: 'required - filesystem path to libc or any ELF',
|
|
283
|
+
names: 'optional - Array of symbol names (defaults to system, execve, __libc_start_main)'
|
|
234
284
|
)
|
|
235
285
|
|
|
236
286
|
# Print the AUTHOR(S) string for this module.
|
|
@@ -259,6 +309,22 @@ module PWN
|
|
|
259
309
|
template = { 1 => 'C', 2 => little ? 'v' : 'n', 4 => little ? 'V' : 'N', 8 => little ? 'Q<' : 'Q>' }[bytes]
|
|
260
310
|
buf.unpack1(template)
|
|
261
311
|
end
|
|
312
|
+
|
|
313
|
+
private_class_method def self.execve_bin_sh_bytes(opts = {})
|
|
314
|
+
arch = opts[:arch].to_s
|
|
315
|
+
if arch.match?(/64|amd64/i)
|
|
316
|
+
return [
|
|
317
|
+
0x48, 0x31, 0xd2, 0x48, 0xbb, 0x2f, 0x62, 0x69, 0x6e,
|
|
318
|
+
0x2f, 0x73, 0x68, 0x00, 0x53, 0x48, 0x89, 0xe7, 0x50,
|
|
319
|
+
0x57, 0x48, 0x89, 0xe6, 0xb0, 0x3b, 0x0f, 0x05
|
|
320
|
+
].pack('C*')
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
[
|
|
324
|
+
0x31, 0xc0, 0x50, 0x68, 0x2f, 0x2f, 0x73, 0x68, 0x68, 0x2f, 0x62, 0x69, 0x6e,
|
|
325
|
+
0x89, 0xe3, 0x50, 0x53, 0x89, 0xe1, 0xb0, 0x0b, 0xcd, 0x80
|
|
326
|
+
].pack('C*')
|
|
327
|
+
end
|
|
262
328
|
end
|
|
263
329
|
end
|
|
264
330
|
end
|
data/lib/pwn/plugins/frida.rb
CHANGED
|
@@ -6,18 +6,39 @@ module PWN
|
|
|
6
6
|
module Plugins
|
|
7
7
|
# Frida attach/spawn + script injection.
|
|
8
8
|
module Frida
|
|
9
|
+
SSL_PINNING = <<~JS
|
|
10
|
+
Java.perform(function () {
|
|
11
|
+
try {
|
|
12
|
+
var TrustManager = Java.use('javax.net.ssl.X509TrustManager');
|
|
13
|
+
var SSLContext = Java.use('javax.net.ssl.SSLContext');
|
|
14
|
+
var TrustManagers = [Java.registerClass({
|
|
15
|
+
name: 'pwn.TrustAll',
|
|
16
|
+
implements: [TrustManager],
|
|
17
|
+
methods: {
|
|
18
|
+
checkClientTrusted: function () {},
|
|
19
|
+
checkServerTrusted: function () {},
|
|
20
|
+
getAcceptedIssuers: function () { return []; }
|
|
21
|
+
}
|
|
22
|
+
}).$new()];
|
|
23
|
+
var ctx = SSLContext.getInstance('TLS');
|
|
24
|
+
ctx.init(null, TrustManagers, null);
|
|
25
|
+
SSLContext.getDefault.implementation = function () { return ctx; };
|
|
26
|
+
} catch (e) {}
|
|
27
|
+
});
|
|
28
|
+
JS
|
|
29
|
+
|
|
9
30
|
public_class_method def self.required_bins
|
|
10
31
|
%w[frida]
|
|
11
32
|
end
|
|
12
33
|
|
|
13
34
|
public_class_method def self.ps(opts = {})
|
|
14
|
-
PWN::Plugins::
|
|
35
|
+
PWN::Plugins::PreflightChecker.require_bin!(name: 'frida')
|
|
15
36
|
stdout, stderr, status = Open3.capture3('frida-ps', *Array(opts[:extra]))
|
|
16
37
|
{ stdout: stdout, stderr: stderr, exit: status.exitstatus }
|
|
17
38
|
end
|
|
18
39
|
|
|
19
40
|
public_class_method def self.attach(opts = {})
|
|
20
|
-
PWN::Plugins::
|
|
41
|
+
PWN::Plugins::PreflightChecker.require_bin!(name: 'frida')
|
|
21
42
|
target = opts[:target] || opts[:name]
|
|
22
43
|
script = opts[:script].to_s
|
|
23
44
|
raise 'ERROR: target is required' if target.to_s.empty?
|
|
@@ -29,6 +50,23 @@ module PWN
|
|
|
29
50
|
{ stdout: stdout, stderr: stderr, exit: status.exitstatus }
|
|
30
51
|
end
|
|
31
52
|
|
|
53
|
+
public_class_method def self.spawn(opts = {})
|
|
54
|
+
PWN::Plugins::PreflightChecker.require_bin!(name: 'frida')
|
|
55
|
+
path = (opts[:path] || opts[:file]).to_s
|
|
56
|
+
raise 'ERROR: path is required' if path.empty?
|
|
57
|
+
|
|
58
|
+
cmd = ['frida', '-f', path]
|
|
59
|
+
cmd += ['-l', opts[:script_path].to_s] if opts[:script_path]
|
|
60
|
+
cmd += ['-e', opts[:script].to_s] unless opts[:script].to_s.empty?
|
|
61
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
62
|
+
{ stdout: stdout, stderr: stderr, exit: status.exitstatus }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
public_class_method def self.ssl_pinning_script(opts = {})
|
|
66
|
+
opts[:android]
|
|
67
|
+
SSL_PINNING
|
|
68
|
+
end
|
|
69
|
+
|
|
32
70
|
public_class_method def self.authors
|
|
33
71
|
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
34
72
|
end
|
|
@@ -38,17 +76,30 @@ module PWN
|
|
|
38
76
|
# List host binaries this module expects to be installed.
|
|
39
77
|
#{self}.required_bins
|
|
40
78
|
|
|
41
|
-
#
|
|
79
|
+
# List processes via frida-ps.
|
|
42
80
|
#{self}.ps(
|
|
43
|
-
extra: 'optional -
|
|
81
|
+
extra: 'optional - extra frida-ps argv'
|
|
44
82
|
)
|
|
45
83
|
|
|
46
|
-
#
|
|
84
|
+
# Attach to a running process and optionally inject a script.
|
|
47
85
|
#{self}.attach(
|
|
48
|
-
target: 'required -
|
|
49
|
-
name: 'optional -
|
|
50
|
-
script: '
|
|
51
|
-
script_path: 'optional -
|
|
86
|
+
target: 'required - process name (defaults to opts[:name])',
|
|
87
|
+
name: 'optional - alias for target',
|
|
88
|
+
script: 'optional - JavaScript source to pass with -e',
|
|
89
|
+
script_path: 'optional - path to a .js file for -l'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Spawn a binary under Frida and inject a script.
|
|
93
|
+
#{self}.spawn(
|
|
94
|
+
path: 'required - filesystem path to the binary',
|
|
95
|
+
file: 'optional - alias for path',
|
|
96
|
+
script: 'optional - JavaScript source to pass with -e',
|
|
97
|
+
script_path: 'optional - path to a .js file for -l'
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Return an SSL-pinning bypass JavaScript template.
|
|
101
|
+
#{self}.ssl_pinning_script(
|
|
102
|
+
android: 'optional - currently always returns the Java TrustManager template'
|
|
52
103
|
)
|
|
53
104
|
|
|
54
105
|
# Print the AUTHOR(S) string for this module.
|
data/lib/pwn/plugins/fuzz.rb
CHANGED
|
@@ -171,6 +171,25 @@ module PWN
|
|
|
171
171
|
sock_obj = PWN::Plugins::Sock.disconnect(sock_obj: sock_obj) unless sock_obj.nil?
|
|
172
172
|
end
|
|
173
173
|
|
|
174
|
+
public_class_method def self.http(opts = {})
|
|
175
|
+
template = opts[:request].to_s
|
|
176
|
+
raise 'ERROR: request is required' if template.empty?
|
|
177
|
+
|
|
178
|
+
token = (opts[:token] || 'FUZZ').to_s
|
|
179
|
+
Array(opts[:dictionary] || opts[:payloads] || ['A']).map do |p|
|
|
180
|
+
template.gsub(token, p.to_s)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
public_class_method def self.file_format(opts = {})
|
|
185
|
+
path = opts[:path].to_s
|
|
186
|
+
raise 'ERROR: path is required' if path.empty?
|
|
187
|
+
|
|
188
|
+
seed = File.binread(path)
|
|
189
|
+
dict = Array(opts[:dictionary] || ["\xff"])
|
|
190
|
+
dict.map { |blob| seed + blob.to_s }
|
|
191
|
+
end
|
|
192
|
+
|
|
174
193
|
# Author(s):: 0day Inc. <support@0dayinc.com>
|
|
175
194
|
|
|
176
195
|
public_class_method def self.authors
|
|
@@ -199,6 +218,20 @@ module PWN
|
|
|
199
218
|
char_encoding: 'optional - character encoding returned by PWN::Plugins::Char.list_encoders (defaults to UTF-8)'
|
|
200
219
|
)
|
|
201
220
|
|
|
221
|
+
# Expand an HTTP request template by substituting FUZZ with dictionary payloads.
|
|
222
|
+
#{self}.http(
|
|
223
|
+
request: 'required - HTTP request template containing FUZZ (or token:)',
|
|
224
|
+
dictionary: 'optional - Array of payload strings',
|
|
225
|
+
payloads: 'optional - alias for dictionary',
|
|
226
|
+
token: 'optional - placeholder to replace (defaults to FUZZ)'
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Append dictionary blobs to a seed file for coverage-guided or dumb file fuzzing.
|
|
230
|
+
#{self}.file_format(
|
|
231
|
+
path: 'required - filesystem path to the seed file',
|
|
232
|
+
dictionary: 'optional - Array of binary/string mutations'
|
|
233
|
+
)
|
|
234
|
+
|
|
202
235
|
# Print the AUTHOR(S) string for this module.
|
|
203
236
|
#{self}.authors
|
|
204
237
|
"
|
data/lib/pwn/plugins/gdb.rb
CHANGED
|
@@ -11,7 +11,7 @@ module PWN
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
public_class_method def self.batch(opts = {})
|
|
14
|
-
PWN::Plugins::
|
|
14
|
+
PWN::Plugins::PreflightChecker.require_bin!(name: 'gdb')
|
|
15
15
|
binary = opts[:binary].to_s
|
|
16
16
|
cmds = Array(opts[:commands] || opts[:cmds])
|
|
17
17
|
args = Array(opts[:args])
|
|
@@ -38,7 +38,7 @@ module PWN
|
|
|
38
38
|
batch(opts.merge(commands: ['checksec']))
|
|
39
39
|
rescue StandardError
|
|
40
40
|
path = opts[:binary].to_s
|
|
41
|
-
return { error: 'gdb missing' } unless PWN::Plugins::
|
|
41
|
+
return { error: 'gdb missing' } unless PWN::Plugins::PreflightChecker.bin?(name: 'gdb')
|
|
42
42
|
|
|
43
43
|
{ hint: 'pwndbg/gef checksec not loaded', binary: path }
|
|
44
44
|
end
|
|
@@ -48,7 +48,18 @@ module PWN
|
|
|
48
48
|
binary = opts[:binary].to_s
|
|
49
49
|
raise 'ERROR: core is required' if core.empty?
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
PWN::Plugins::PreflightChecker.require_bin!(name: 'gdb')
|
|
52
|
+
binary = opts[:binary].to_s
|
|
53
|
+
argv = ['gdb', '--batch', '--quiet', '-c', core, '-ex', 'set pagination off', '-ex', 'bt', '-ex', 'info registers']
|
|
54
|
+
argv << binary unless binary.empty?
|
|
55
|
+
stdout, stderr, status = Open3.capture3(*argv)
|
|
56
|
+
{ stdout: stdout, stderr: stderr, exit: status.exitstatus }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
public_class_method def self.breakpoints(opts = {})
|
|
60
|
+
bps = Array(opts[:breakpoints] || opts[:bp])
|
|
61
|
+
cmds = bps.map { |b| "break #{b}" } + Array(opts[:commands]) + %w[run bt]
|
|
62
|
+
batch(opts.merge(commands: cmds))
|
|
52
63
|
end
|
|
53
64
|
|
|
54
65
|
public_class_method def self.authors
|
|
@@ -81,8 +92,17 @@ module PWN
|
|
|
81
92
|
|
|
82
93
|
# Run core and return its result
|
|
83
94
|
#{self}.core(
|
|
84
|
-
core: 'required -
|
|
85
|
-
binary: 'optional - binary
|
|
95
|
+
core: 'required - filesystem path to the core dump',
|
|
96
|
+
binary: 'optional - matching binary for symbols'
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Plant breakpoints then run and dump a backtrace.
|
|
100
|
+
#{self}.breakpoints(
|
|
101
|
+
binary: 'optional - binary to debug',
|
|
102
|
+
breakpoints: 'optional - Array of break specs (e.g. main or *0x401000)',
|
|
103
|
+
bp: 'optional - alias for breakpoints',
|
|
104
|
+
commands: 'optional - extra gdb commands after the breaks',
|
|
105
|
+
args: 'optional - Array of inferior argv'
|
|
86
106
|
)
|
|
87
107
|
|
|
88
108
|
# Print the AUTHOR(S) string for this module.
|
data/lib/pwn/plugins/jobs.rb
CHANGED
|
@@ -34,6 +34,7 @@ module PWN
|
|
|
34
34
|
started_at: Time.now.utc.iso8601
|
|
35
35
|
}
|
|
36
36
|
File.write(meta, JSON.generate(row))
|
|
37
|
+
PWN::Plugins::ArtifactRegistry.register(session_id: opts[:session_id], path: log, kind: 'job-log') if opts[:session_id] && defined?(PWN::Plugins::ArtifactRegistry) && File.file?(log)
|
|
37
38
|
row
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module PWN
|
|
6
|
+
module Plugins
|
|
7
|
+
# trivy / kube-hunter wrappers plus docker-socket preflight.
|
|
8
|
+
module K8s
|
|
9
|
+
public_class_method def self.required_bins
|
|
10
|
+
%w[trivy]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
public_class_method def self.trivy(opts = {})
|
|
14
|
+
PWN::Plugins::PreflightChecker.require_bin!(name: 'trivy')
|
|
15
|
+
target = opts[:target] || opts[:image] || opts[:path]
|
|
16
|
+
raise 'ERROR: target is required' if target.to_s.empty?
|
|
17
|
+
|
|
18
|
+
mode = (opts[:mode] || :image).to_s
|
|
19
|
+
cmd = ['trivy', mode, '--format', 'json', target.to_s]
|
|
20
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
21
|
+
{ stdout: stdout, stderr: stderr, exit: status.exitstatus }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
public_class_method def self.kube_hunter(opts = {})
|
|
25
|
+
return { error: 'kube-hunter missing', hint: 'pip install kube-hunter' } unless PWN::Plugins::PreflightChecker.bin?(name: 'kube-hunter')
|
|
26
|
+
|
|
27
|
+
cmd = ['kube-hunter']
|
|
28
|
+
cmd += ['--remote', opts[:remote].to_s] if opts[:remote]
|
|
29
|
+
cmd << '--report' << 'json'
|
|
30
|
+
stdout, stderr, status = Open3.capture3(*cmd)
|
|
31
|
+
{ stdout: stdout, stderr: stderr, exit: status.exitstatus }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
public_class_method def self.docker_socket?(opts = {})
|
|
35
|
+
path = (opts[:path] || '/var/run/docker.sock').to_s
|
|
36
|
+
PWN::Plugins::PreflightChecker.service?(name: 'docker', path: path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
public_class_method def self.authors
|
|
40
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
public_class_method def self.help
|
|
44
|
+
puts "USAGE:
|
|
45
|
+
# List host binaries this module expects to be installed.
|
|
46
|
+
#{self}.required_bins
|
|
47
|
+
|
|
48
|
+
# Scan a container image or filesystem with trivy.
|
|
49
|
+
#{self}.trivy(
|
|
50
|
+
target: 'required - image name or path',
|
|
51
|
+
image: 'optional - alias for target',
|
|
52
|
+
path: 'optional - alias for target',
|
|
53
|
+
mode: 'optional - trivy subcommand (defaults to image)'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Run kube-hunter (remote cluster optional).
|
|
57
|
+
#{self}.kube_hunter(
|
|
58
|
+
remote: 'optional - API server host for --remote'
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# True when the docker unix socket is present.
|
|
62
|
+
#{self}.docker_socket?(
|
|
63
|
+
path: 'optional - socket path (defaults to /var/run/docker.sock)'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Print the AUTHOR(S) string for this module.
|
|
67
|
+
#{self}.authors
|
|
68
|
+
"
|
|
69
|
+
constants.sort
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
data/lib/pwn/plugins/nuclei.rb
CHANGED
|
@@ -12,7 +12,7 @@ module PWN
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
public_class_method def self.scan(opts = {})
|
|
15
|
-
PWN::Plugins::
|
|
15
|
+
PWN::Plugins::PreflightChecker.require_bin!(name: 'nuclei')
|
|
16
16
|
target = opts[:target] || opts[:url]
|
|
17
17
|
raise 'ERROR: target is required' if target.to_s.empty?
|
|
18
18
|
|
|
@@ -26,7 +26,28 @@ module PWN
|
|
|
26
26
|
rescue JSON::ParserError
|
|
27
27
|
nil
|
|
28
28
|
end
|
|
29
|
-
{ findings: findings, stderr: stderr, exit: status.exitstatus }
|
|
29
|
+
{ findings: to_findings(rows: findings), raw: findings, stderr: stderr, exit: status.exitstatus }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
public_class_method def self.to_findings(opts = {})
|
|
33
|
+
Array(opts[:rows] || opts[:findings]).map do |r|
|
|
34
|
+
r = r.transform_keys(&:to_s) if r.is_a?(Hash)
|
|
35
|
+
info = r['info'] || {}
|
|
36
|
+
info = info.transform_keys(&:to_s) if info.is_a?(Hash)
|
|
37
|
+
{
|
|
38
|
+
title: info['name'] || r['template-id'] || 'nuclei finding',
|
|
39
|
+
severity: (info['severity'] || 'info').to_s,
|
|
40
|
+
url: r['matched-at'] || r['host'],
|
|
41
|
+
template: r['template-id'],
|
|
42
|
+
description: info['description']
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
public_class_method def self.to_defectdojo(opts = {})
|
|
48
|
+
to_findings(opts).map do |f|
|
|
49
|
+
{ title: f[:title], severity: f[:severity], description: "#{f[:url]} #{f[:template]}".strip }
|
|
50
|
+
end
|
|
30
51
|
end
|
|
31
52
|
|
|
32
53
|
public_class_method def self.authors
|
|
@@ -44,7 +65,19 @@ module PWN
|
|
|
44
65
|
url: 'optional - HTTP(S) URL',
|
|
45
66
|
severity: 'optional - severity value consumed by #scan',
|
|
46
67
|
templates: 'optional - templates value consumed by #scan',
|
|
47
|
-
rate_limit: 'optional -
|
|
68
|
+
rate_limit: 'optional - requests per second passed to nuclei -rate-limit'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Map nuclei JSONL rows into report-shaped finding hashes.
|
|
72
|
+
#{self}.to_findings(
|
|
73
|
+
rows: 'optional - Array of parsed nuclei JSONL objects',
|
|
74
|
+
findings: 'optional - alias for rows'
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Shape findings for a DefectDojo import payload.
|
|
78
|
+
#{self}.to_defectdojo(
|
|
79
|
+
rows: 'optional - Array of parsed nuclei JSONL objects',
|
|
80
|
+
findings: 'optional - alias for rows'
|
|
48
81
|
)
|
|
49
82
|
|
|
50
83
|
# Print the AUTHOR(S) string for this module.
|
data/lib/pwn/plugins/packet.rb
CHANGED
|
@@ -1085,7 +1085,7 @@ module PWN
|
|
|
1085
1085
|
# )
|
|
1086
1086
|
|
|
1087
1087
|
public_class_method def self.send(opts = {})
|
|
1088
|
-
PWN::Plugins::
|
|
1088
|
+
PWN::Plugins::PreflightChecker.require_cap_net_raw! if defined?(PWN::Plugins::PreflightChecker)
|
|
1089
1089
|
pkt = opts[:pkt]
|
|
1090
1090
|
|
|
1091
1091
|
if opts[:iface]
|
|
@@ -5,9 +5,10 @@ module PWN
|
|
|
5
5
|
# Plugin/binary/capability preflight. Plugins declare required_bins /
|
|
6
6
|
# required_caps; HOST summaries list degraded modules without autoloading
|
|
7
7
|
# the whole plugin tree.
|
|
8
|
-
module
|
|
8
|
+
module PreflightChecker
|
|
9
9
|
class MissingBinary < StandardError; end
|
|
10
10
|
class MissingCapability < StandardError; end
|
|
11
|
+
class MissingService < StandardError; end
|
|
11
12
|
|
|
12
13
|
PLUGIN_DEPS = {
|
|
13
14
|
'PWN::Plugins::Radare2' => { bins: %w[r2] },
|
|
@@ -21,7 +22,8 @@ module PWN
|
|
|
21
22
|
'PWN::Plugins::Metasploit' => { bins: %w[msfconsole] },
|
|
22
23
|
'PWN::Plugins::BurpSuite' => { bins: %w[burpsuite] },
|
|
23
24
|
'PWN::Plugins::Zaproxy' => { bins: %w[zaproxy] },
|
|
24
|
-
'PWN::Plugins::Packet' => { bins: [], caps: %w[CAP_NET_RAW] }
|
|
25
|
+
'PWN::Plugins::Packet' => { bins: [], caps: %w[CAP_NET_RAW] },
|
|
26
|
+
'PWN::Plugins::K8s' => { bins: %w[trivy], services: [{ name: 'docker', path: '/var/run/docker.sock' }] }
|
|
25
27
|
}.freeze
|
|
26
28
|
|
|
27
29
|
public_class_method def self.required_bins
|
|
@@ -65,6 +67,13 @@ module PWN
|
|
|
65
67
|
'or run as root. Fallback: PWN::Plugins::Sock.connect (no raw).'
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
public_class_method def self.service?(opts = {})
|
|
71
|
+
path = opts[:path].to_s
|
|
72
|
+
return File.socket?(path) unless path.empty?
|
|
73
|
+
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
68
77
|
public_class_method def self.check(opts = {})
|
|
69
78
|
deps = PLUGIN_DEPS
|
|
70
79
|
if opts[:names]
|
|
@@ -74,17 +83,19 @@ module PWN
|
|
|
74
83
|
deps.map do |plugin, dep|
|
|
75
84
|
bins = Array(dep[:bins])
|
|
76
85
|
caps = Array(dep[:caps])
|
|
86
|
+
services = Array(dep[:services])
|
|
77
87
|
missing_bins = bins.reject { |b| bin?(name: b) }
|
|
78
88
|
missing_caps = []
|
|
79
89
|
missing_caps << 'CAP_NET_RAW' if caps.include?('CAP_NET_RAW') && !cap_net_raw?
|
|
80
|
-
|
|
90
|
+
missing_services = services.reject { |s| service?(name: s[:name] || s['name'], path: s[:path] || s['path']) }.map { |s| s[:name] || s['name'] }
|
|
91
|
+
status = if missing_bins.empty? && missing_caps.empty? && missing_services.empty?
|
|
81
92
|
:ok
|
|
82
93
|
elsif bins.any? && missing_bins.length == bins.length
|
|
83
94
|
:dead
|
|
84
95
|
else
|
|
85
96
|
:degraded
|
|
86
97
|
end
|
|
87
|
-
{ plugin: plugin, status: status, missing_bins: missing_bins, missing_caps: missing_caps }
|
|
98
|
+
{ plugin: plugin, status: status, missing_bins: missing_bins, missing_caps: missing_caps, missing_services: missing_services }
|
|
88
99
|
end
|
|
89
100
|
end
|
|
90
101
|
|
|
@@ -95,7 +106,7 @@ module PWN
|
|
|
95
106
|
degraded = rows.select { |r| r[:status] == :degraded }
|
|
96
107
|
lines = ["HOST plugins ok=#{rows.count { |r| r[:status] == :ok }} degraded=#{degraded.length} dead=#{dead.length}"]
|
|
97
108
|
(degraded + dead).first(cap).each do |r|
|
|
98
|
-
miss = (r[:missing_bins] + r[:missing_caps]).join(',')
|
|
109
|
+
miss = (Array(r[:missing_bins]) + Array(r[:missing_caps]) + Array(r[:missing_services])).join(',')
|
|
99
110
|
lines << " #{r[:status]} #{r[:plugin]} missing=#{miss}"
|
|
100
111
|
end
|
|
101
112
|
lines.join("\n")
|
|
@@ -128,6 +139,12 @@ module PWN
|
|
|
128
139
|
# Run require cap net raw and return its result
|
|
129
140
|
#{self}.require_cap_net_raw!
|
|
130
141
|
|
|
142
|
+
# True when a unix socket/service path exists.
|
|
143
|
+
#{self}.service?(
|
|
144
|
+
name: 'optional - service name for the report row',
|
|
145
|
+
path: 'required - unix socket path (e.g. /var/run/docker.sock)'
|
|
146
|
+
)
|
|
147
|
+
|
|
131
148
|
# Run check and return its result
|
|
132
149
|
#{self}.check(
|
|
133
150
|
names: 'optional - Array names value consumed by #check'
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'pty'
|
|
4
4
|
require 'timeout'
|
|
5
|
+
require 'socket'
|
|
5
6
|
|
|
6
7
|
module PWN
|
|
7
8
|
module Plugins
|
|
@@ -24,6 +25,17 @@ module PWN
|
|
|
24
25
|
{ id: id, pid: pid }
|
|
25
26
|
end
|
|
26
27
|
|
|
28
|
+
public_class_method def self.connect(opts = {})
|
|
29
|
+
host = (opts[:host] || opts[:target]).to_s
|
|
30
|
+
port = opts[:port].to_i
|
|
31
|
+
raise 'ERROR: host and port are required' if host.empty? || port <= 0
|
|
32
|
+
|
|
33
|
+
sock = TCPSocket.new(host, port)
|
|
34
|
+
id = "sock_#{sock.object_id}"
|
|
35
|
+
@tubes[id] = { r: sock, w: sock, pid: nil, buf: +'' }
|
|
36
|
+
{ id: id, host: host, port: port }
|
|
37
|
+
end
|
|
38
|
+
|
|
27
39
|
public_class_method def self.write_line(opts = {})
|
|
28
40
|
t = tube!(opts)
|
|
29
41
|
line = opts[:line] || opts[:data] || ''
|
|
@@ -59,7 +71,7 @@ module PWN
|
|
|
59
71
|
t = tube!(opts)
|
|
60
72
|
t[:w].close
|
|
61
73
|
t[:r].close
|
|
62
|
-
Process.kill('TERM', t[:pid])
|
|
74
|
+
Process.kill('TERM', t[:pid]) if t[:pid]
|
|
63
75
|
@tubes.delete(opts[:id].to_s)
|
|
64
76
|
true
|
|
65
77
|
rescue StandardError
|
|
@@ -82,6 +94,13 @@ module PWN
|
|
|
82
94
|
command: 'optional - command value consumed by #spawn'
|
|
83
95
|
)
|
|
84
96
|
|
|
97
|
+
# Connect a TCP tube with the same write_line/recvuntil API as spawn.
|
|
98
|
+
#{self}.connect(
|
|
99
|
+
host: 'required - hostname or IP address (defaults to opts[:target])',
|
|
100
|
+
target: 'optional - hostname, IP, or CIDR to scan',
|
|
101
|
+
port: 'required - TCP/UDP port number'
|
|
102
|
+
)
|
|
103
|
+
|
|
85
104
|
# Run write line and return its result
|
|
86
105
|
#{self}.write_line(
|
|
87
106
|
line: 'optional - line value consumed by #write_line',
|