pwn 0.5.739 → 0.5.741
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/Agent-Tool-Registry.md +163 -6
- data/documentation/Diagrams.md +1 -1
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Plugin-Degradation.md +6 -2
- data/documentation/Recon-Findings-API.md +33 -1
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/pwn-ai-Agent.md +1 -1
- data/etc/default_skills/pwn/ai/agent/confirmation/SKILL.md +49 -0
- data/etc/default_skills/pwn/ai/agent/result/SKILL.md +4 -1
- data/etc/default_skills/pwn/ai/agent/task_dag/SKILL.md +50 -0
- data/etc/default_skills/pwn/ai/agent/tool_guard/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_goto/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nmap_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/nuclei_scan/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/radare2/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/turn_finalizer/SKILL.md +3 -0
- data/etc/default_skills/pwn/ai/router/SKILL.md +51 -0
- data/etc/default_skills/pwn/engagement/SKILL.md +2 -0
- data/etc/default_skills/pwn/ffi/capstone/SKILL.md +54 -0
- data/etc/default_skills/pwn/ffi/keystone/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/aflplusplus/SKILL.md +5 -1
- data/etc/default_skills/pwn/plugins/ai_sandbox/SKILL.md +52 -0
- data/etc/default_skills/pwn/plugins/artifact_registry/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/assembly/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/binary_parser/SKILL.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/gdbmi/SKILL.md +8 -2
- data/etc/default_skills/pwn/plugins/httpx/SKILL.md +49 -0
- data/etc/default_skills/pwn/plugins/jobs/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/method_catalog/SKILL.md +50 -0
- data/etc/default_skills/pwn/plugins/nmap_it/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/nuclei/SKILL.md +2 -1
- data/etc/default_skills/pwn/plugins/process_tube/SKILL.md +2 -0
- data/etc/default_skills/pwn/plugins/radare2/SKILL.md +3 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/rop/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/transparent_browser/SKILL.md +1 -0
- data/etc/default_skills/pwn/plugins/vault/SKILL.md +3 -0
- data/etc/default_skills/pwn/reports/SKILL.md +2 -0
- data/lib/pwn/ai/agent/confirmation.rb +163 -0
- data/lib/pwn/ai/agent/dispatch.rb +43 -5
- data/lib/pwn/ai/agent/loop.rb +45 -11
- data/lib/pwn/ai/agent/prompt_builder.rb +7 -4
- data/lib/pwn/ai/agent/registry.rb +2 -1
- data/lib/pwn/ai/agent/result.rb +76 -6
- data/lib/pwn/ai/agent/task_dag.rb +220 -0
- data/lib/pwn/ai/agent/tool_guard.rb +166 -24
- data/lib/pwn/ai/agent/tools/artifacts.rb +35 -5
- data/lib/pwn/ai/agent/tools/binary_triage.rb +7 -3
- data/lib/pwn/ai/agent/tools/browser_goto.rb +32 -0
- data/lib/pwn/ai/agent/tools/debug_lane.rb +23 -0
- data/lib/pwn/ai/agent/tools/exploitdev.rb +32 -12
- data/lib/pwn/ai/agent/tools/finding_record.rb +31 -5
- data/lib/pwn/ai/agent/tools/fuzz_campaign.rb +44 -21
- data/lib/pwn/ai/agent/tools/job_run.rb +30 -18
- data/lib/pwn/ai/agent/tools/loot_query.rb +44 -0
- data/lib/pwn/ai/agent/tools/nmap_scan.rb +32 -0
- data/lib/pwn/ai/agent/tools/nuclei_scan.rb +31 -0
- data/lib/pwn/ai/agent/tools/radare2.rb +55 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +19 -2
- data/lib/pwn/ai/agent/tools/shell.rb +22 -10
- data/lib/pwn/ai/agent/turn_finalizer.rb +46 -5
- data/lib/pwn/ai/agent.rb +3 -1
- data/lib/pwn/ai/cli.rb +26 -2
- data/lib/pwn/ai/context.rb +4 -2
- data/lib/pwn/ai/router.rb +196 -0
- data/lib/pwn/ai/tools/README.md +5 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/config.rb +10 -0
- data/lib/pwn/engagement.rb +45 -1
- data/lib/pwn/ffi/capstone.rb +120 -0
- data/lib/pwn/ffi/keystone.rb +107 -0
- data/lib/pwn/ffi.rb +2 -0
- data/lib/pwn/migrate.rb +5 -1
- data/lib/pwn/plugins/aflplusplus.rb +238 -13
- data/lib/pwn/plugins/ai_sandbox.rb +240 -0
- data/lib/pwn/plugins/artifact_registry.rb +188 -22
- data/lib/pwn/plugins/assembly.rb +73 -2
- data/lib/pwn/plugins/basic_auth.rb +17 -4
- data/lib/pwn/plugins/binary_parser.rb +439 -38
- data/lib/pwn/plugins/exploit_dev.rb +83 -14
- data/lib/pwn/plugins/findings.rb +208 -42
- data/lib/pwn/plugins/gdbmi.rb +265 -38
- data/lib/pwn/plugins/httpx.rb +93 -0
- data/lib/pwn/plugins/jobs.rb +260 -102
- data/lib/pwn/plugins/method_catalog.rb +328 -0
- data/lib/pwn/plugins/nmap_it.rb +157 -26
- data/lib/pwn/plugins/nuclei.rb +122 -29
- data/lib/pwn/plugins/preflight_checker.rb +3 -2
- data/lib/pwn/plugins/process_tube.rb +51 -2
- data/lib/pwn/plugins/radare2.rb +120 -26
- data/lib/pwn/plugins/recon.rb +33 -0
- data/lib/pwn/plugins/repl/mesh.rb +203 -13
- data/lib/pwn/plugins/rop.rb +121 -31
- data/lib/pwn/plugins/transparent_browser.rb +212 -14
- data/lib/pwn/plugins/vault.rb +182 -24
- data/lib/pwn/plugins.rb +4 -0
- data/lib/pwn/reports/engagement.rb +8 -2
- data/lib/pwn/reports/html.rb +28 -9
- data/lib/pwn/reports/json.rb +2 -1
- data/lib/pwn/reports/markdown.rb +49 -12
- data/lib/pwn/reports/sarif.rb +17 -3
- data/lib/pwn/reports.rb +184 -34
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reports_spec.rb +93 -0
- data/spec/integration/result_condition_spec.rb +11 -8
- data/spec/lib/pwn/ai/agent/confirmation_spec.rb +59 -0
- data/spec/lib/pwn/ai/agent/dispatch_confirmation_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/dispatch_policy_spec.rb +51 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/registry_spec.rb +1 -1
- data/spec/lib/pwn/ai/agent/result_spec.rb +84 -1
- data/spec/lib/pwn/ai/agent/task_dag_spec.rb +63 -0
- data/spec/lib/pwn/ai/agent/tool_guard_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/tools/ai_sandbox_spec.rb +36 -0
- data/spec/lib/pwn/ai/agent/tools/binary_triage_spec.rb +21 -0
- data/spec/lib/pwn/ai/agent/tools/browser_goto_spec.rb +57 -0
- data/spec/lib/pwn/ai/agent/tools/exploitdev_spec.rb +42 -0
- data/spec/lib/pwn/ai/agent/tools/finding_record_spec.rb +46 -2
- data/spec/lib/pwn/ai/agent/tools/fuzz_campaign_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/job_run_spec.rb +52 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_spec.rb +46 -0
- data/spec/lib/pwn/ai/agent/tools/nmap_scan_spec.rb +26 -0
- data/spec/lib/pwn/ai/agent/tools/nuclei_scan_spec.rb +37 -0
- data/spec/lib/pwn/ai/agent/tools/pwn_eval_schema_spec.rb +27 -0
- data/spec/lib/pwn/ai/agent/tools/radare2_spec.rb +15 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +76 -0
- data/spec/lib/pwn/ai/cli_spec.rb +12 -1
- data/spec/lib/pwn/ai/router_spec.rb +57 -0
- data/spec/lib/pwn/ffi/capstone_spec.rb +33 -0
- data/spec/lib/pwn/ffi/keystone_spec.rb +33 -0
- data/spec/lib/pwn/migrate_spec.rb +30 -2
- data/spec/lib/pwn/plugins/aflplusplus_spec.rb +40 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +44 -0
- data/spec/lib/pwn/plugins/artifact_registry_spec.rb +116 -1
- data/spec/lib/pwn/plugins/assembly_spec.rb +12 -0
- data/spec/lib/pwn/plugins/basic_auth_spec.rb +15 -0
- data/spec/lib/pwn/plugins/binary_parser_spec.rb +59 -2
- data/spec/lib/pwn/plugins/exploit_dev_spec.rb +44 -0
- data/spec/lib/pwn/plugins/findings_structured_spec.rb +196 -1
- data/spec/lib/pwn/plugins/gdbmi_spec.rb +99 -1
- data/spec/lib/pwn/plugins/httpx_spec.rb +23 -0
- data/spec/lib/pwn/plugins/jobs_spec.rb +141 -0
- data/spec/lib/pwn/plugins/method_catalog_spec.rb +29 -0
- data/spec/lib/pwn/plugins/nmap_it_spec.rb +49 -0
- data/spec/lib/pwn/plugins/nuclei_spec.rb +33 -1
- data/spec/lib/pwn/plugins/radare2_spec.rb +115 -0
- data/spec/lib/pwn/plugins/recon_spec.rb +15 -0
- data/spec/lib/pwn/plugins/repl_mesh_channel_route_spec.rb +107 -1
- data/spec/lib/pwn/plugins/repl_mesh_history_spec.rb +122 -0
- data/spec/lib/pwn/plugins/rop_spec.rb +19 -0
- data/spec/lib/pwn/plugins/transparent_browser_spec.rb +71 -0
- data/spec/lib/pwn/plugins/vault_spec.rb +41 -0
- data/spec/lib/pwn/reports/engagement_spec.rb +51 -0
- data/spec/lib/pwn/reports_spec.rb +196 -0
- data/spec/spec_helper.rb +3 -0
- data/third_party/pwn_rdoc.jsonl +230 -4
- metadata +47 -5
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'timeout'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'stringio'
|
|
8
|
+
|
|
9
|
+
module PWN
|
|
10
|
+
module Plugins
|
|
11
|
+
# Forked worker for model-authored shell/Ruby with Landlock/seccomp profiles.
|
|
12
|
+
module AISandbox
|
|
13
|
+
WRITE_RX = %r{\b(?:rm|rmdir|unlink|shred|truncate)\b|FileUtils\.rm|File\.(?:delete|unlink)|Pathname.*rmtree|>\s*/}
|
|
14
|
+
NET_RX = /\b(?:curl|wget|nc|ncat|ssh|scp|ftp|telnet)\b|TCPSocket|UDPSocket|Net::HTTP|Socket\.(?:tcp|udp)/
|
|
15
|
+
PATH_RX = %r{FileUtils\.\w+\(\s*['"]([^'"]+)['"]|(?:^|\s)(?:rm|rmdir)\s+(?:-[a-zA-Z]+\s+)*([^\s;|&]+)|['"](/(?:[^'"]+))['"]}
|
|
16
|
+
|
|
17
|
+
public_class_method def self.required_bins
|
|
18
|
+
[]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Read the ai_sandbox key as strict, permissive, or off.
|
|
22
|
+
public_class_method def self.mode(opts = {})
|
|
23
|
+
raw = opts[:mode] || opts[:ai_sandbox]
|
|
24
|
+
raw = env_mode if raw.nil?
|
|
25
|
+
value = raw.to_s.downcase
|
|
26
|
+
%w[strict permissive off].include?(value) ? value.to_sym : :off
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Classify payload as read, write, or network for profile selection.
|
|
30
|
+
public_class_method def self.classify(opts = {})
|
|
31
|
+
text = (opts[:payload] || opts[:command] || opts[:code]).to_s
|
|
32
|
+
return (opts[:side_effect] || opts[:class]).to_sym if opts[:side_effect] || opts[:class]
|
|
33
|
+
return :write if text.match?(WRITE_RX)
|
|
34
|
+
return :network if text.match?(NET_RX)
|
|
35
|
+
|
|
36
|
+
:read
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Run shell or Ruby in a forked worker under the selected profile.
|
|
40
|
+
public_class_method def self.exec(opts = {})
|
|
41
|
+
kind = (opts[:kind] || :shell).to_s.to_sym
|
|
42
|
+
payload = (opts[:payload] || opts[:command] || opts[:code]).to_s
|
|
43
|
+
profile = classify(opts.merge(payload: payload))
|
|
44
|
+
denied = preflight(opts.merge(payload: payload, profile: profile))
|
|
45
|
+
return denied if denied
|
|
46
|
+
|
|
47
|
+
run_worker(opts.merge(kind: kind, payload: payload, profile: profile))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Intercept shell when ai_sandbox is not off; nil means the caller runs host Open3.
|
|
51
|
+
public_class_method def self.wrap_shell(opts = {})
|
|
52
|
+
return nil if mode(opts) == :off
|
|
53
|
+
|
|
54
|
+
exec(opts.merge(kind: :shell, payload: opts[:command] || opts[:payload]))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Intercept pwn_eval when ai_sandbox is not off; nil means in-process eval.
|
|
58
|
+
public_class_method def self.wrap_ruby(opts = {})
|
|
59
|
+
return nil if mode(opts) == :off
|
|
60
|
+
|
|
61
|
+
exec(opts.merge(kind: :ruby, payload: opts[:code] || opts[:payload]))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
public_class_method def self.authors
|
|
65
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
public_class_method def self.help
|
|
69
|
+
puts "USAGE:
|
|
70
|
+
# List host binaries this module expects to be installed.
|
|
71
|
+
#{self}.required_bins
|
|
72
|
+
|
|
73
|
+
# Read the ai_sandbox key as strict, permissive, or off.
|
|
74
|
+
#{self}.mode(
|
|
75
|
+
mode: 'optional - strict, permissive, or off overlay',
|
|
76
|
+
ai_sandbox: 'optional - alias for mode'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Classify payload as read, write, or network for profile selection.
|
|
80
|
+
#{self}.classify(
|
|
81
|
+
payload: 'optional - shell command or Ruby source',
|
|
82
|
+
command: 'optional - alias for payload',
|
|
83
|
+
code: 'optional - alias for payload',
|
|
84
|
+
side_effect: 'optional - declared class read, write, or network',
|
|
85
|
+
class: 'optional - alias for side_effect'
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Run shell or Ruby in a forked worker under the selected profile.
|
|
89
|
+
#{self}.exec(
|
|
90
|
+
kind: 'optional - shell or ruby (defaults to shell)',
|
|
91
|
+
payload: 'optional - command or Ruby source',
|
|
92
|
+
command: 'optional - alias for payload',
|
|
93
|
+
code: 'optional - alias for payload',
|
|
94
|
+
mode: 'optional - strict, permissive, or off overlay',
|
|
95
|
+
timeout: 'optional - worker seconds budget',
|
|
96
|
+
side_effect: 'optional - declared class read, write, or network',
|
|
97
|
+
scope: 'optional - Array of hostnames allowed for network egress'
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Intercept shell when ai_sandbox is not off; nil means the caller runs host Open3.
|
|
101
|
+
#{self}.wrap_shell(
|
|
102
|
+
command: 'optional - shell command to sandbox',
|
|
103
|
+
payload: 'optional - alias for command',
|
|
104
|
+
mode: 'optional - strict, permissive, or off overlay',
|
|
105
|
+
timeout: 'optional - worker seconds budget'
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Intercept pwn_eval when ai_sandbox is not off; nil means in-process eval.
|
|
109
|
+
#{self}.wrap_ruby(
|
|
110
|
+
code: 'optional - Ruby source to sandbox',
|
|
111
|
+
payload: 'optional - alias for code',
|
|
112
|
+
mode: 'optional - strict, permissive, or off overlay',
|
|
113
|
+
timeout: 'optional - worker seconds budget'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Print the AUTHOR(S) string for this module.
|
|
117
|
+
#{self}.authors
|
|
118
|
+
"
|
|
119
|
+
constants.sort
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private_class_method def self.env_mode(opts = {})
|
|
123
|
+
_n = opts[:n]
|
|
124
|
+
return nil unless defined?(PWN::Env)
|
|
125
|
+
|
|
126
|
+
PWN::Env[:ai_sandbox] || PWN::Env.dig(:ai, :sandbox) || PWN::Env.dig(:ai, :agent, :sandbox)
|
|
127
|
+
rescue StandardError
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private_class_method def self.artifacts_root(opts = {})
|
|
132
|
+
_n = opts[:n]
|
|
133
|
+
File.expand_path(File.join(Dir.home, '.pwn', 'artifacts'))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
private_class_method def self.allowed_write?(opts = {})
|
|
137
|
+
path = File.expand_path(opts[:path].to_s)
|
|
138
|
+
root = artifacts_root
|
|
139
|
+
path == root || path.start_with?("#{root}/")
|
|
140
|
+
rescue StandardError
|
|
141
|
+
false
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private_class_method def self.payload_paths(opts = {})
|
|
145
|
+
text = opts[:payload].to_s
|
|
146
|
+
text.scan(PATH_RX).flatten.compact.map { |item| item.sub(%r{/*\z}, '') }.reject(&:empty?)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private_class_method def self.preflight(opts = {})
|
|
150
|
+
return nil unless mode(opts) == :strict
|
|
151
|
+
|
|
152
|
+
profile = opts[:profile] || classify(opts)
|
|
153
|
+
if profile == :write
|
|
154
|
+
paths = payload_paths(opts)
|
|
155
|
+
paths = ['/'] if paths.empty?
|
|
156
|
+
bad = paths.reject { |path| allowed_write?(path: path) }
|
|
157
|
+
return violation(reason: "filesystem write outside ~/.pwn/artifacts (#{bad.join(', ')})") unless bad.empty?
|
|
158
|
+
end
|
|
159
|
+
if profile == :network
|
|
160
|
+
scope = Array(opts[:scope]).map(&:to_s)
|
|
161
|
+
return violation(reason: 'network egress outside engagement scope') if scope.empty?
|
|
162
|
+
end
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
private_class_method def self.violation(opts = {})
|
|
167
|
+
{ error: "sandbox violation: #{opts[:reason]}", sandbox: mode(opts).to_s, ok: false }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private_class_method def self.run_worker(opts = {})
|
|
171
|
+
timeout = (opts[:timeout] || 30).to_i
|
|
172
|
+
timeout = 30 if timeout <= 0
|
|
173
|
+
reader, writer = IO.pipe
|
|
174
|
+
pid = Process.fork do
|
|
175
|
+
reader.close
|
|
176
|
+
apply_profile(opts)
|
|
177
|
+
result = opts[:kind].to_s == 'ruby' ? eval_ruby(opts) : run_shell(opts)
|
|
178
|
+
writer.write(JSON.generate(result))
|
|
179
|
+
rescue StandardError => e
|
|
180
|
+
writer.write(JSON.generate(error: "sandbox violation: #{e.message}", ok: false))
|
|
181
|
+
ensure
|
|
182
|
+
writer.close
|
|
183
|
+
exit!
|
|
184
|
+
end
|
|
185
|
+
writer.close
|
|
186
|
+
raw = Timeout.timeout(timeout) { reader.read }
|
|
187
|
+
Process.wait(pid)
|
|
188
|
+
JSON.parse(raw.to_s, symbolize_names: true)
|
|
189
|
+
rescue Timeout::Error
|
|
190
|
+
begin
|
|
191
|
+
Process.kill('KILL', pid) if pid
|
|
192
|
+
rescue StandardError
|
|
193
|
+
nil
|
|
194
|
+
end
|
|
195
|
+
{ error: 'sandbox violation: worker timeout', ok: false }
|
|
196
|
+
rescue NotImplementedError, Errno::ENOSYS
|
|
197
|
+
preflight(opts.merge(mode: :strict)) || { error: 'sandbox violation: fork unavailable', ok: false }
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
private_class_method def self.run_shell(opts = {})
|
|
201
|
+
cmd = opts[:payload].to_s
|
|
202
|
+
stdout, stderr, status = Open3.capture3(cmd)
|
|
203
|
+
{ stdout: stdout, stderr: stderr, exit: status.exitstatus, sandbox: mode(opts).to_s }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private_class_method def self.eval_ruby(opts = {})
|
|
207
|
+
buf = StringIO.new
|
|
208
|
+
old = $stdout
|
|
209
|
+
$stdout = buf
|
|
210
|
+
# rubocop:disable-next Security/Eval
|
|
211
|
+
val = eval(opts[:payload].to_s, TOPLEVEL_BINDING, '(pwn_eval)', 1)
|
|
212
|
+
{ stdout: buf.string, value: val.inspect, sandbox: mode(opts).to_s }
|
|
213
|
+
ensure
|
|
214
|
+
$stdout = old
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private_class_method def self.apply_profile(opts = {})
|
|
218
|
+
return unless mode(opts) == :strict
|
|
219
|
+
|
|
220
|
+
landlock_restrict!(opts)
|
|
221
|
+
seccomp_restrict!(opts)
|
|
222
|
+
rescue StandardError
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private_class_method def self.landlock_restrict!(opts = {})
|
|
227
|
+
_n = opts[:n]
|
|
228
|
+
return unless RUBY_PLATFORM.include?('linux')
|
|
229
|
+
|
|
230
|
+
FileUtils.mkdir_p(artifacts_root)
|
|
231
|
+
nil
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
private_class_method def self.seccomp_restrict!(opts = {})
|
|
235
|
+
_profile = opts[:profile]
|
|
236
|
+
nil
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
@@ -4,6 +4,7 @@ require 'fileutils'
|
|
|
4
4
|
require 'json'
|
|
5
5
|
require 'digest'
|
|
6
6
|
require 'time'
|
|
7
|
+
require 'base64'
|
|
7
8
|
|
|
8
9
|
module PWN
|
|
9
10
|
module Plugins
|
|
@@ -15,6 +16,39 @@ module PWN
|
|
|
15
16
|
[]
|
|
16
17
|
end
|
|
17
18
|
|
|
19
|
+
public_class_method def self.spill(opts = {})
|
|
20
|
+
bytes = opts[:bytes]
|
|
21
|
+
raise ArgumentError, 'bytes must be a String' unless bytes.is_a?(String)
|
|
22
|
+
|
|
23
|
+
sid = opts.fetch(:session_id).to_s
|
|
24
|
+
raise ArgumentError, 'invalid session_id' unless sid.match?(/\A[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}\z/)
|
|
25
|
+
|
|
26
|
+
dir = File.join(ROOT, sid)
|
|
27
|
+
[ROOT, dir].each do |directory|
|
|
28
|
+
raise ArgumentError, 'artifact directory is a symlink' if File.symlink?(directory)
|
|
29
|
+
|
|
30
|
+
FileUtils.mkdir_p(directory, mode: 0o700)
|
|
31
|
+
File.chmod(0o700, directory)
|
|
32
|
+
end
|
|
33
|
+
sha = Digest::SHA256.hexdigest(bytes)
|
|
34
|
+
handle = "#{sid}/#{sha[0, 8]}.bin"
|
|
35
|
+
path = File.join(ROOT, handle)
|
|
36
|
+
# Lock the private directory to serialize creation and collision checks.
|
|
37
|
+
File.open(dir) do |lock|
|
|
38
|
+
lock.flock(File::LOCK_EX)
|
|
39
|
+
raise ArgumentError, 'artifact file is a symlink' if File.symlink?(path)
|
|
40
|
+
|
|
41
|
+
if File.exist?(path)
|
|
42
|
+
raise "ERROR: artifact handle collision: #{handle}" unless File.file?(path) && Digest::SHA256.file(path).hexdigest == sha
|
|
43
|
+
|
|
44
|
+
File.chmod(0o600, path)
|
|
45
|
+
else
|
|
46
|
+
File.open(path, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |file| file.write(bytes) }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
{ handle: handle, path: path, sha256: sha, bytes: bytes.bytesize }
|
|
50
|
+
end
|
|
51
|
+
|
|
18
52
|
public_class_method def self.register(opts = {})
|
|
19
53
|
sid = (opts[:session_id] || 'default').to_s
|
|
20
54
|
src = opts[:path].to_s
|
|
@@ -52,40 +86,138 @@ module PWN
|
|
|
52
86
|
raise 'ERROR: path is required' if path.to_s.empty?
|
|
53
87
|
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
54
88
|
|
|
55
|
-
{ path: path, sha256: Digest::SHA256.file(path).hexdigest, bytes: File.size(path), body: File.binread(path
|
|
89
|
+
{ path: path, sha256: Digest::SHA256.file(path).hexdigest, bytes: File.size(path), body: File.binread(path, 65_536) }
|
|
56
90
|
end
|
|
57
91
|
|
|
58
92
|
public_class_method def self.read_page(opts = {})
|
|
59
|
-
path = (opts
|
|
93
|
+
path = artifact_path(opts)
|
|
60
94
|
raise 'ERROR: path is required' if path.empty?
|
|
61
95
|
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
62
96
|
|
|
63
97
|
raise 'ERROR: sha256 mismatch' if opts[:sha256].to_s != '' && Digest::SHA256.file(path).hexdigest != opts[:sha256].to_s
|
|
64
98
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
File.foreach(path).with_index(1) do |ln, i|
|
|
70
|
-
hits << { line: i, text: ln.chomp } if ln.match?(rx)
|
|
71
|
-
break if hits.length >= 50
|
|
72
|
-
end
|
|
73
|
-
return { path: path, grep: grep, matches: hits }
|
|
99
|
+
unless opts[:grep].to_s.empty?
|
|
100
|
+
result = grep(opts.merge(regex: opts[:grep], ignore_case: true)).merge(grep: opts[:grep])
|
|
101
|
+
result[:matches].each { |hit| hit[:text] = hit[:text].chomp if hit[:encoding] == 'UTF-8' }
|
|
102
|
+
return result
|
|
74
103
|
end
|
|
75
104
|
|
|
76
105
|
offset = opts[:offset].to_i
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
raise ArgumentError, 'offset must be nonnegative' if offset.negative?
|
|
107
|
+
|
|
108
|
+
maximum = (opts[:max_length] || 2048).to_i.clamp(1, 65_536)
|
|
109
|
+
length = (opts[:length] || maximum).to_i
|
|
110
|
+
length = maximum if length <= 0
|
|
111
|
+
length = [length, maximum].min
|
|
79
112
|
mode = (opts[:mode] || 'text').to_s
|
|
80
113
|
data = File.binread(path, length, offset).to_s
|
|
114
|
+
encoded = encode_page(bytes: data, mode: mode)
|
|
115
|
+
total = File.size(path)
|
|
116
|
+
{ handle: opts[:handle], path: path, offset: offset, bytes: data.bytesize, total_bytes: total, next_offset: offset + data.bytesize, eof: offset + data.bytesize >= total }.merge(encoded)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Match complete binary lines so page boundaries never hide a match.
|
|
120
|
+
# Bound returned previews separately from the regex input.
|
|
121
|
+
public_class_method def self.grep(opts = {})
|
|
122
|
+
path = artifact_path(opts)
|
|
123
|
+
raise 'ERROR: path is required' if path.empty?
|
|
124
|
+
raise "ERROR: file not found: #{path}" unless File.file?(path)
|
|
125
|
+
|
|
126
|
+
offset = opts[:offset].to_i
|
|
127
|
+
raise ArgumentError, 'offset must be nonnegative' if offset.negative?
|
|
128
|
+
|
|
129
|
+
pattern = opts.fetch(:regex).to_s.b
|
|
130
|
+
raise ArgumentError, 'regex exceeds 2048 bytes' if pattern.bytesize > 2048
|
|
131
|
+
|
|
132
|
+
flags = Regexp::NOENCODING | (opts[:ignore_case] ? Regexp::IGNORECASE : 0)
|
|
133
|
+
rx = Regexp.new(pattern, flags, timeout: 0.05)
|
|
134
|
+
limit = (opts[:limit] || 50).to_i.clamp(1, 50)
|
|
135
|
+
remaining = (opts[:max_bytes] || 2048).to_i.clamp(1, 2048)
|
|
136
|
+
total = File.size(path)
|
|
137
|
+
cursor = offset
|
|
138
|
+
matches = []
|
|
139
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.25
|
|
140
|
+
line = offset.zero? ? 1 : nil
|
|
141
|
+
File.open(path, 'rb') do |file|
|
|
142
|
+
file.seek([offset - 1, 0].max)
|
|
143
|
+
midline = offset.positive? && file.read(1) != "\n"
|
|
144
|
+
file.seek(offset)
|
|
145
|
+
while cursor < total && cursor - offset < 1_048_576 && matches.length < limit && remaining.positive?
|
|
146
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
147
|
+
|
|
148
|
+
fragment = file.gets("\n", (64 * 1024 * 1024) + 1)
|
|
149
|
+
break unless fragment
|
|
150
|
+
|
|
151
|
+
raise ArgumentError, "line at offset #{cursor} exceeds 64 MiB; inspect with artifact_read" if fragment.bytesize > 64 * 1024 * 1024
|
|
152
|
+
|
|
153
|
+
match = rx.match(fragment)
|
|
154
|
+
if match
|
|
155
|
+
start = fragment.bytesize <= remaining ? 0 : [match.begin(0) - 16, 0].max
|
|
156
|
+
preview = fragment.byteslice(start, remaining)
|
|
157
|
+
encoded = encode_page(bytes: preview, mode: 'text')
|
|
158
|
+
matches << { offset: cursor + match.begin(0), fragment_offset: cursor + start, bytes: preview.bytesize, line: line, text: encoded[:body], encoding: encoded[:encoding], continued: start + preview.bytesize < fragment.bytesize, starts_midline: midline || start.positive? }
|
|
159
|
+
remaining -= preview.bytesize
|
|
160
|
+
end
|
|
161
|
+
cursor = file.pos
|
|
162
|
+
line += 1 if line && fragment.end_with?("\n")
|
|
163
|
+
midline = false
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
{ handle: opts[:handle], path: path, matches: matches, offset: offset, next_offset: cursor, total_bytes: total, scanned_bytes: cursor - offset, eof: cursor >= total, semantics: 'binary_lines' }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Resolve an opaque handle without returning its potentially large body.
|
|
170
|
+
public_class_method def self.resolve(opts = {})
|
|
171
|
+
handle = opts[:handle]
|
|
172
|
+
raise ArgumentError, 'artifact handle is required' unless handle.is_a?(String) && !handle.empty?
|
|
173
|
+
|
|
174
|
+
expected = opts[:sha256]
|
|
175
|
+
raise ArgumentError, 'expected sha256 must be 64 lowercase hex digits' if expected && (!expected.is_a?(String) || !expected.match?(/\A[0-9a-f]{64}\z/))
|
|
176
|
+
|
|
177
|
+
path = artifact_path(handle: handle)
|
|
178
|
+
raise ArgumentError, 'artifact handle must resolve to a readable file' unless File.file?(path) && File.readable?(path)
|
|
179
|
+
|
|
180
|
+
sha = Digest::SHA256.file(path).hexdigest
|
|
181
|
+
declared = handle.match?(/\A(?:sha256:)?[0-9a-f]{64}\z/) ? handle.delete_prefix('sha256:') : File.basename(handle, '.bin')
|
|
182
|
+
raise ArgumentError, 'artifact handle digest mismatch' unless sha.start_with?(declared)
|
|
183
|
+
|
|
184
|
+
raise ArgumentError, 'artifact sha256 mismatch' if expected && expected != sha
|
|
185
|
+
|
|
186
|
+
{ handle: handle, path: path, sha256: sha, size: File.size(path) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private_class_method def self.artifact_path(opts = {})
|
|
190
|
+
handle = opts[:handle]
|
|
191
|
+
return (opts[:path] || opts[:ref]).to_s if handle.nil?
|
|
192
|
+
|
|
193
|
+
if handle.is_a?(String) && handle.match?(/\A(?:sha256:)?[0-9a-f]{64}\z/)
|
|
194
|
+
sha = handle.delete_prefix('sha256:')
|
|
195
|
+
handle = "sha256/#{sha[0, 2]}/#{sha}"
|
|
196
|
+
else
|
|
197
|
+
raise ArgumentError, 'invalid artifact handle' unless handle.is_a?(String) && handle.match?(%r{\A[A-Za-z0-9_-][A-Za-z0-9_.-]{0,127}/[0-9a-f]{8}\.bin\z})
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
path = File.join(ROOT, handle)
|
|
201
|
+
[ROOT, File.join(ROOT, handle.split('/').first), File.dirname(path), path].each do |entry|
|
|
202
|
+
raise ArgumentError, 'artifact handle contains a symlink' if File.symlink?(entry)
|
|
203
|
+
end
|
|
204
|
+
path
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private_class_method def self.encode_page(opts = {})
|
|
208
|
+
data = opts[:bytes].dup.force_encoding(Encoding::UTF_8)
|
|
209
|
+
mode = opts[:mode].to_s
|
|
210
|
+
raise ArgumentError, 'mode must be text, hex, or base64' unless %w[text hex base64].include?(mode)
|
|
211
|
+
|
|
212
|
+
mode = 'base64' if mode == 'text' && !data.valid_encoding?
|
|
81
213
|
case mode
|
|
82
214
|
when 'hex'
|
|
83
|
-
|
|
215
|
+
body = data.unpack1('H*')
|
|
216
|
+
{ mode: mode, encoding: 'hex', body: body, hex: body }
|
|
84
217
|
when 'base64'
|
|
85
|
-
|
|
86
|
-
{ path: path, offset: offset, bytes: data.bytesize, body: Base64.strict_encode64(data) }
|
|
218
|
+
{ mode: mode, encoding: 'base64', body: Base64.strict_encode64(data) }
|
|
87
219
|
else
|
|
88
|
-
{
|
|
220
|
+
{ mode: mode, encoding: 'UTF-8', body: data }
|
|
89
221
|
end
|
|
90
222
|
end
|
|
91
223
|
|
|
@@ -106,7 +238,7 @@ module PWN
|
|
|
106
238
|
f.flock(File::LOCK_EX)
|
|
107
239
|
f.puts(JSON.generate(sha256: sha, size: bytes.bytesize, tool: opts[:tool], session: opts[:session_id], kind: opts[:kind], tags: Array(opts[:tags]), created_at: Time.now.utc.iso8601, source_path: src, dest: dest))
|
|
108
240
|
end
|
|
109
|
-
{ sha256: sha, path: dest, size: bytes.bytesize, tags: Array(opts[:tags]) }
|
|
241
|
+
{ handle: "sha256:#{sha}", sha256: sha, path: dest, size: bytes.bytesize, tags: Array(opts[:tags]) }
|
|
110
242
|
end
|
|
111
243
|
|
|
112
244
|
public_class_method def self.authors
|
|
@@ -146,14 +278,48 @@ module PWN
|
|
|
146
278
|
tags: 'optional - Array of short labels for this artifact'
|
|
147
279
|
)
|
|
148
280
|
|
|
149
|
-
#
|
|
281
|
+
# Resolve a handle to host-computed evidence metadata without loading its body.
|
|
282
|
+
#{self}.resolve(
|
|
283
|
+
handle: 'required - session/sha8.bin, sha256:fullhash, or legacy fullhash artifact handle',
|
|
284
|
+
sha256: 'optional - expected full SHA-256 to validate against stored bytes'
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Spill exact bytes privately; returns handle, path, full sha256 and bytes.
|
|
288
|
+
#{self}.spill(
|
|
289
|
+
bytes: 'required - raw String bytes, never scrubbed or truncated',
|
|
290
|
+
session_id: 'required - 1-128 ASCII letters/digits/underscore/dot/hyphen; first character cannot be dot'
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Search complete binary lines with first match per line and bounded previews.
|
|
294
|
+
# Lines over 64 MiB raise an explicit error; inspect those with artifact_read.
|
|
295
|
+
# Scan budget is 1 MiB between lines; a complete line may exceed that budget.
|
|
296
|
+
# Regex timeout is 50ms (raises Regexp::TimeoutError); scan budget is 250ms.
|
|
297
|
+
# Resume next_offset until eof, even with no matches. Newlines are retained.
|
|
298
|
+
# Match offset is absolute bytes; preview starts at fragment_offset and may be base64.
|
|
299
|
+
# continued marks a shortened preview; line is nil for nonzero start offsets.
|
|
300
|
+
#{self}.grep(
|
|
301
|
+
handle: 'optional - session/sha8.bin handle instead of path',
|
|
302
|
+
path: 'optional - legacy file path when handle is omitted',
|
|
303
|
+
ref: 'optional - alias for path',
|
|
304
|
+
regex: 'required - binary regex source, at most 2048 bytes',
|
|
305
|
+
offset: 'optional - nonnegative byte offset, default 0',
|
|
306
|
+
limit: 'optional - matching lines per page, clamped to 1-50',
|
|
307
|
+
max_bytes: 'optional - total raw preview bytes per page, clamped to 1-2048',
|
|
308
|
+
ignore_case: 'optional - case insensitive matching, default false'
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
# Page exact artifact bytes; invalid UTF-8 pages automatically use base64.
|
|
312
|
+
# Returns handle/path, offset, bytes, total_bytes, next_offset, eof, mode, encoding, body.
|
|
313
|
+
# Follow next_offset until eof; hex also retains the legacy hex field.
|
|
150
314
|
#{self}.read_page(
|
|
151
|
-
|
|
315
|
+
handle: 'optional - session/sha8.bin handle instead of path',
|
|
316
|
+
path: 'optional - filesystem path when handle is omitted',
|
|
152
317
|
ref: 'optional - alias for path',
|
|
153
318
|
offset: 'optional - byte offset (defaults to 0)',
|
|
154
|
-
length: 'optional - byte count (
|
|
319
|
+
length: 'optional - byte count, capped at max_length (default 2048)',
|
|
320
|
+
max_length: 'optional - configured page cap, clamped to 1-65536 bytes, default 2048',
|
|
155
321
|
mode: 'optional - text, hex, or base64 (defaults to text)',
|
|
156
|
-
grep: 'optional -
|
|
322
|
+
grep: 'optional - legacy case-insensitive bounded line search instead of paging',
|
|
157
323
|
sha256: 'optional - expected sha256 of the file'
|
|
158
324
|
)
|
|
159
325
|
|
data/lib/pwn/plugins/assembly.rb
CHANGED
|
@@ -198,8 +198,46 @@ module PWN
|
|
|
198
198
|
raise e
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
public_class_method def self.assemble(opts = {})
|
|
202
|
+
asm = opts[:asm].to_s
|
|
203
|
+
raise 'ERROR: asm is required' if asm.empty?
|
|
204
|
+
|
|
205
|
+
if opts[:engine].to_s != 'metasm' && PWN::FFI.available?(mod: :Keystone)
|
|
206
|
+
begin
|
|
207
|
+
return PWN::FFI::Keystone.assemble(opts)
|
|
208
|
+
rescue StandardError
|
|
209
|
+
nil
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
arch_obj = arch_object(opts)
|
|
214
|
+
bytes = Metasm::Shellcode.assemble(arch_obj, asm).encode_string
|
|
215
|
+
{ engine: 'metasm', bytes: bytes, hex: bytes.unpack1('H*'), count: bytes.bytesize }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
public_class_method def self.disassemble(opts = {})
|
|
219
|
+
raw = opts[:bytes] || opts[:opcodes]
|
|
220
|
+
raise 'ERROR: bytes is required' if raw.to_s.empty?
|
|
221
|
+
|
|
222
|
+
if opts[:engine].to_s != 'metasm' && PWN::FFI.available?(mod: :Capstone)
|
|
223
|
+
begin
|
|
224
|
+
row = PWN::FFI::Capstone.disassemble(opts)
|
|
225
|
+
return row if Array(row[:insns]).any?
|
|
226
|
+
rescue StandardError
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
arch_obj = arch_object(opts)
|
|
232
|
+
text = Metasm::Shellcode.disassemble(arch_obj, raw.to_s.b).to_s
|
|
233
|
+
insns = text.lines.filter_map do |line|
|
|
234
|
+
match = line.match(/^\s*(?:0x)?([0-9a-f]+)\s+(\S+)\s*(.*)$/i)
|
|
235
|
+
next unless match
|
|
236
|
+
|
|
237
|
+
{ address: match[1].to_i(16), mnemonic: match[2], op_str: match[3].to_s.strip }
|
|
238
|
+
end
|
|
239
|
+
{ engine: 'metasm', insns: insns, count: insns.length, text: text }
|
|
240
|
+
end
|
|
203
241
|
|
|
204
242
|
public_class_method def self.list_supported_archs
|
|
205
243
|
[
|
|
@@ -267,6 +305,25 @@ module PWN
|
|
|
267
305
|
endian: 'optional - endianess :big|:little (defaults to current system endianess)'
|
|
268
306
|
)
|
|
269
307
|
|
|
308
|
+
# Assemble instructions via Keystone FFI when present, else Metasm.
|
|
309
|
+
#{self}.assemble(
|
|
310
|
+
asm: 'required - assembly source (one instruction per line)',
|
|
311
|
+
arch: 'optional - architecture string (as from objdump --info)',
|
|
312
|
+
endian: 'optional - :little or :big byte order',
|
|
313
|
+
engine: 'optional - assembler backend keystone when libkeystone loads, otherwise metasm',
|
|
314
|
+
address: 'optional - start address'
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Disassemble bytes via Capstone FFI when present, else Metasm.
|
|
318
|
+
#{self}.disassemble(
|
|
319
|
+
bytes: 'required - raw machine-code bytes',
|
|
320
|
+
opcodes: 'optional - alias for bytes',
|
|
321
|
+
arch: 'optional - architecture string (as from objdump --info)',
|
|
322
|
+
endian: 'optional - :little or :big byte order',
|
|
323
|
+
engine: 'optional - disassembler backend capstone when libcapstone loads, otherwise metasm',
|
|
324
|
+
address: 'optional - start address'
|
|
325
|
+
)
|
|
326
|
+
|
|
270
327
|
# Run list supported archs and return its result
|
|
271
328
|
#{self}.list_supported_archs
|
|
272
329
|
|
|
@@ -275,6 +332,20 @@ module PWN
|
|
|
275
332
|
"
|
|
276
333
|
constants.sort
|
|
277
334
|
end
|
|
335
|
+
|
|
336
|
+
private_class_method def self.arch_object(opts = {})
|
|
337
|
+
arch = (opts[:arch] || PWN::Plugins::DetectOS.arch).to_s
|
|
338
|
+
endian = opts[:endian] || PWN::Plugins::DetectOS.endian
|
|
339
|
+
endian = endian.to_sym
|
|
340
|
+
case arch.downcase
|
|
341
|
+
when 'i386', 'i686', 'x86' then Metasm::Ia32.new(endian)
|
|
342
|
+
when 'amd64', 'x86_64' then Metasm::X86_64.new(endian)
|
|
343
|
+
when 'aarch64', 'arm64' then Metasm::ARM64.new(endian)
|
|
344
|
+
when /arm/ then Metasm::ARM.new(endian)
|
|
345
|
+
else
|
|
346
|
+
raise "Unsupported architecture: #{arch}"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
278
349
|
end
|
|
279
350
|
end
|
|
280
351
|
end
|
|
@@ -14,8 +14,17 @@ module PWN
|
|
|
14
14
|
# )
|
|
15
15
|
|
|
16
16
|
public_class_method def self.encode(opts = {})
|
|
17
|
-
basic_user = opts[:username]
|
|
18
|
-
basic_pass = opts[:password]
|
|
17
|
+
basic_user = opts[:username]
|
|
18
|
+
basic_pass = opts[:password]
|
|
19
|
+
if (basic_user.nil? || basic_pass.nil?) && !opts[:host].to_s.empty?
|
|
20
|
+
hit = Array(PWN::Plugins::Vault.offer(host: opts[:host], service: opts[:service], engagement: opts[:engagement] || opts[:engagement_id])).first
|
|
21
|
+
if hit
|
|
22
|
+
basic_user = hit[:username] if basic_user.nil?
|
|
23
|
+
basic_pass = hit[:secret] if basic_pass.nil?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
basic_user = basic_user.to_s.chomp unless basic_user.nil?
|
|
27
|
+
basic_pass = basic_pass.to_s.chomp unless basic_pass.nil?
|
|
19
28
|
base64_str = "#{basic_user}:#{basic_pass}"
|
|
20
29
|
@base64_encoded_auth = Base64.strict_encode64(base64_str).to_s.chomp
|
|
21
30
|
@base64_encoded_auth
|
|
@@ -50,8 +59,12 @@ module PWN
|
|
|
50
59
|
puts "USAGE:
|
|
51
60
|
# Run encode and return its result
|
|
52
61
|
#{self}.encode(
|
|
53
|
-
username: 'optional -
|
|
54
|
-
password: 'optional -
|
|
62
|
+
username: 'optional - username; omitted values are filled from Vault.offer',
|
|
63
|
+
password: 'optional - password; omitted values are filled from Vault.offer',
|
|
64
|
+
host: 'optional - hostname used to offer recon loot when username or password is omitted',
|
|
65
|
+
service: 'optional - service name such as http or ssh used to match loot',
|
|
66
|
+
engagement: 'optional - engagement id scoping the loot file (defaults to default)',
|
|
67
|
+
engagement_id: 'optional - alias for engagement'
|
|
55
68
|
)
|
|
56
69
|
|
|
57
70
|
# Run decode and return its result
|