pwn 0.5.784 → 0.5.787
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/.ruby-version +1 -1
- data/Gemfile +6 -6
- data/README.md +37 -0
- data/bin/pwn +2 -1
- data/bin/pwn_web_cache_deception +35 -7
- data/build_gem.sh +1 -1
- data/documentation/AI-Integration.md +7 -5
- data/documentation/Agent-Tool-Registry.md +2 -2
- data/documentation/Backup-Retention.md +69 -15
- data/documentation/Configuration.md +10 -6
- data/documentation/Contributing.md +1 -1
- data/documentation/Cron.md +2 -2
- data/documentation/Diagrams.md +2 -2
- data/documentation/General-PWN-Usage.md +4 -3
- data/documentation/Harness-Evaluation.md +1 -1
- data/documentation/Home.md +6 -5
- data/documentation/How-PWN-Works.md +6 -5
- data/documentation/Installation.md +12 -9
- data/documentation/Mission-Cancellation.md +134 -0
- data/documentation/Operability.md +18 -0
- data/documentation/Persistence.md +7 -5
- data/documentation/Plugin-Degradation.md +14 -0
- data/documentation/Plugins.md +3 -2
- data/documentation/Policy-Benchmark.md +32 -33
- data/documentation/Runtime-Source-Protection.md +162 -0
- data/documentation/Transparent-Browser.md +11 -0
- data/documentation/Troubleshooting.md +3 -2
- data/documentation/What-is-PWN.md +2 -2
- data/documentation/YubiKey-Vaults.md +185 -0
- data/documentation/YubiKey.md +218 -0
- data/documentation/diagrams/cron-scheduling.svg +130 -130
- data/documentation/diagrams/dot/cron-scheduling.dot +3 -3
- data/documentation/diagrams/dot/persistence-filesystem.dot +5 -3
- data/documentation/diagrams/dot/plugin-ecosystem.dot +2 -2
- data/documentation/diagrams/dot/pwn-ai-feedback-learning-loop.dot +1 -1
- data/documentation/diagrams/dot/reinforcement-learning.dot +2 -2
- data/documentation/diagrams/persistence-filesystem.svg +172 -142
- data/documentation/diagrams/plugin-ecosystem.svg +64 -64
- data/documentation/diagrams/pwn-ai-feedback-learning-loop.svg +1 -1
- data/documentation/diagrams/reinforcement-learning.svg +89 -89
- data/documentation/pwn-ai-Agent.md +10 -4
- data/etc/default_cron/session_cleanup.rb +43 -27
- data/etc/default_skills/pwn/ai/agent/mission_namespace/SKILL.md +21 -0
- data/etc/default_skills/pwn/ai/agent/tools/browser_act/SKILL.md +45 -0
- data/etc/default_skills/pwn/ai/agent/tools/loot_query/SKILL.md +27 -26
- data/etc/default_skills/pwn/ai/agent/tools/recon_run/SKILL.md +25 -15
- data/etc/default_skills/pwn/ai/agent/tools/workspace_tool/SKILL.md +24 -0
- data/etc/default_skills/pwn/ai/server_tools/SKILL.md +25 -0
- data/etc/default_skills/pwn/backups/SKILL.md +54 -0
- data/etc/default_skills/pwn/plugins/recon/SKILL.md +12 -1
- data/etc/default_skills/pwn/plugins/yubikey/SKILL.md +157 -0
- data/etc/default_skills/pwn/plugins/yubikey/fido/SKILL.md +189 -0
- data/etc/default_skills/pwn/plugins/yubikey/hsmauth/SKILL.md +83 -0
- data/etc/default_skills/pwn/plugins/yubikey/management/SKILL.md +80 -0
- data/etc/default_skills/pwn/plugins/yubikey/oath/SKILL.md +115 -0
- data/etc/default_skills/pwn/plugins/yubikey/openpgp/SKILL.md +144 -0
- data/etc/default_skills/pwn/plugins/yubikey/otp/SKILL.md +97 -0
- data/etc/default_skills/pwn/plugins/yubikey/piv/SKILL.md +110 -0
- data/etc/default_skills/pwn/plugins/yubikey/protocol/SKILL.md +21 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/SKILL.md +75 -0
- data/etc/default_skills/pwn/plugins/yubikey/security_domain/scp/SKILL.md +46 -0
- data/etc/default_skills/pwn/plugins/yubikey/transport/SKILL.md +98 -0
- data/etc/default_skills/pwn/redaction/SKILL.md +13 -1
- data/lib/pwn/ai/agent/dispatch.rb +26 -15
- data/lib/pwn/ai/agent/learning.rb +14 -18
- data/lib/pwn/ai/agent/loop.rb +17 -1
- data/lib/pwn/ai/agent/mission_namespace.rb +331 -0
- data/lib/pwn/ai/agent/policy_evaluation.rb +334 -36
- data/lib/pwn/ai/agent/prompt_builder.rb +13 -2
- data/lib/pwn/ai/agent/request_runtime.rb +122 -18
- data/lib/pwn/ai/agent/reward.rb +5 -3
- data/lib/pwn/ai/agent/skill_consolidation.rb +3 -2
- data/lib/pwn/ai/agent/skill_review.rb +9 -5
- data/lib/pwn/ai/agent/swarm.rb +4 -0
- data/lib/pwn/ai/agent/tools/browser_act.rb +32 -0
- data/lib/pwn/ai/agent/tools/loot_query.rb +11 -1
- data/lib/pwn/ai/agent/tools/recon_run.rb +11 -1
- data/lib/pwn/ai/agent/tools/workspace_tool.rb +29 -0
- data/lib/pwn/ai/agent/turn_finalizer.rb +6 -2
- data/lib/pwn/ai/agent.rb +1 -0
- data/lib/pwn/ai/anthropic.rb +8 -33
- data/lib/pwn/ai/cli.rb +34 -5
- data/lib/pwn/ai/gemini.rb +1 -0
- data/lib/pwn/ai/grok.rb +39 -28
- data/lib/pwn/ai/ollama.rb +4 -2
- data/lib/pwn/ai/open_ai.rb +11 -32
- data/lib/pwn/ai/open_web_ui.rb +62 -4
- data/lib/pwn/ai/router.rb +8 -1
- data/lib/pwn/ai/server_tools.rb +231 -0
- data/lib/pwn/ai.rb +1 -0
- data/lib/pwn/backups.rb +349 -0
- data/lib/pwn/config.rb +70 -39
- data/lib/pwn/migrate.rb +85 -280
- data/lib/pwn/module_skills.rb +7 -0
- data/lib/pwn/plugins/ai_sandbox.rb +538 -1
- data/lib/pwn/plugins/authentication_helper.rb +74 -4
- data/lib/pwn/plugins/basic_auth.rb +2 -1
- data/lib/pwn/plugins/google_workspace.rb +4 -21
- data/lib/pwn/plugins/jobs.rb +1 -1
- data/lib/pwn/plugins/log.rb +3 -1
- data/lib/pwn/plugins/preflight_checker.rb +34 -9
- data/lib/pwn/plugins/recon.rb +4 -1
- data/lib/pwn/plugins/repl/ai.rb +293 -26
- data/lib/pwn/plugins/repl/ai_console.rb +268 -88
- data/lib/pwn/plugins/repl/ai_swarm.rb +177 -58
- data/lib/pwn/plugins/repl/mesh.rb +14 -19
- data/lib/pwn/plugins/repl/vault.rb +4 -15
- data/lib/pwn/plugins/repl.rb +9 -216
- data/lib/pwn/plugins/sandbox/driver.rb +4 -0
- data/lib/pwn/plugins/transparent_browser.rb +215 -0
- data/lib/pwn/plugins/vault.rb +412 -135
- data/lib/pwn/plugins/yubikey/fido.rb +1288 -0
- data/lib/pwn/plugins/yubikey/hsmauth.rb +439 -0
- data/lib/pwn/plugins/yubikey/management.rb +686 -0
- data/lib/pwn/plugins/yubikey/oath.rb +766 -0
- data/lib/pwn/plugins/yubikey/openpgp.rb +1063 -0
- data/lib/pwn/plugins/yubikey/otp.rb +897 -0
- data/lib/pwn/plugins/yubikey/piv.rb +1315 -0
- data/lib/pwn/plugins/yubikey/protocol.rb +341 -0
- data/lib/pwn/plugins/yubikey/security_domain/scp.rb +482 -0
- data/lib/pwn/plugins/yubikey/security_domain.rb +464 -0
- data/lib/pwn/plugins/yubikey/transport.rb +1017 -0
- data/lib/pwn/plugins/yubikey.rb +844 -0
- data/lib/pwn/plugins.rb +1 -0
- data/lib/pwn/redaction.rb +69 -12
- data/lib/pwn/session_trace.rb +4 -0
- data/lib/pwn/setup.rb +114 -24
- data/lib/pwn/version.rb +1 -1
- data/lib/pwn/www/app_cobalt_io.rb +16 -5
- data/lib/pwn/www/bug_crowd.rb +16 -5
- data/lib/pwn/www/coinbase_pro.rb +16 -5
- data/lib/pwn/www/github.rb +30 -14
- data/lib/pwn/www/paypal.rb +15 -4
- data/lib/pwn/www/synack.rb +16 -5
- data/lib/pwn/www/twitter.rb +17 -6
- data/lib/pwn.rb +1 -0
- data/spec/documentation/installation_md_spec.rb +1 -1
- data/spec/fixtures/yubikey/apdu_frames.json +162 -0
- data/spec/integration/authentication_helper_oath_spec.rb +117 -0
- data/spec/integration/controller_spill_spec.rb +48 -0
- data/spec/integration/harness_process_spec.rb +97 -0
- data/spec/integration/mission_cancel_spec.rb +79 -0
- data/spec/integration/mission_curses_pty_spec.rb +44 -0
- data/spec/integration/readme_security_spec.rb +47 -0
- data/spec/integration/web_cache_deception_yubikey_spec.rb +159 -0
- data/spec/lib/pwn/ai/agent/dispatch_spec.rb +6 -3
- data/spec/lib/pwn/ai/agent/dispatch_vault_safety_spec.rb +86 -0
- data/spec/lib/pwn/ai/agent/injection_guard_spec.rb +2 -0
- data/spec/lib/pwn/ai/agent/learning_spec.rb +4 -4
- data/spec/lib/pwn/ai/agent/loop_artifacts_spec.rb +5 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +104 -29
- data/spec/lib/pwn/ai/agent/mission_namespace_spec.rb +138 -0
- data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +10 -23
- data/spec/lib/pwn/ai/agent/request_runtime_spec.rb +48 -0
- data/spec/lib/pwn/ai/agent/shell_search_effect_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/solve_spec.rb +6 -4
- data/spec/lib/pwn/ai/agent/tools/browser_act_spec.rb +44 -0
- data/spec/lib/pwn/ai/agent/tools/loot_query_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/ai/agent/tools/recon_run_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/ai/agent/tools/workspace_tool_spec.rb +20 -0
- data/spec/lib/pwn/ai/agent/turn_finalizer_spec.rb +7 -0
- data/spec/lib/pwn/ai/anthropic_vault_enrollment_spec.rb +37 -0
- data/spec/lib/pwn/ai/cli_spec.rb +8 -16
- data/spec/lib/pwn/ai/oauth_vault_persistence_spec.rb +195 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +2 -2
- data/spec/lib/pwn/ai/open_web_ui_workspace_spec.rb +36 -0
- data/spec/lib/pwn/ai/router_spec.rb +1 -1
- data/spec/lib/pwn/ai/server_tools_spec.rb +39 -0
- data/spec/lib/pwn/backups_spec.rb +129 -0
- data/spec/lib/pwn/config_redaction_yubikey_spec.rb +28 -0
- data/spec/lib/pwn/config_yubikey_spec.rb +92 -0
- data/spec/lib/pwn/migrate_spec.rb +29 -81
- data/spec/lib/pwn/migrate_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/module_skills_spec.rb +8 -0
- data/spec/lib/pwn/plugins/ai_sandbox_spec.rb +230 -0
- data/spec/lib/pwn/plugins/authentication_helper_native_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/plugins/authentication_helper_yubikey_spec.rb +90 -0
- data/spec/lib/pwn/plugins/capability_broker/daemon_spec.rb +1 -1
- data/spec/lib/pwn/plugins/google_workspace_spec.rb +17 -35
- data/spec/lib/pwn/plugins/log_yubikey_spec.rb +47 -0
- data/spec/lib/pwn/plugins/preflight_checker_native_yubikey_spec.rb +19 -0
- data/spec/lib/pwn/plugins/preflight_checker_yubikey_spec.rb +55 -0
- data/spec/lib/pwn/plugins/recon_yubikey_spec.rb +43 -0
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +110 -75
- data/spec/lib/pwn/plugins/repl/ai_legacy_containment_spec.rb +200 -0
- data/spec/lib/pwn/plugins/repl/ai_swarm_spec.rb +19 -16
- data/spec/lib/pwn/plugins/repl/ai_vault_edit_yubikey_spec.rb +103 -0
- data/spec/lib/pwn/plugins/repl/ai_yubikey_spec.rb +48 -0
- data/spec/lib/pwn/plugins/repl/mesh_vault_persistence_spec.rb +149 -0
- data/spec/lib/pwn/plugins/repl/vault_yaml_serialization_spec.rb +11 -0
- data/spec/lib/pwn/plugins/repl/vault_yubikey_spec.rb +39 -0
- data/spec/lib/pwn/plugins/repl_mesh_layout_spec.rb +2 -0
- data/spec/lib/pwn/plugins/repl_spec.rb +56 -5
- data/spec/lib/pwn/plugins/transparent_browser_jev_spec.rb +46 -0
- data/spec/lib/pwn/plugins/vault_native_yubikey_spec.rb +29 -0
- data/spec/lib/pwn/plugins/vault_publication_safety_spec.rb +258 -0
- data/spec/lib/pwn/plugins/vault_store_safety_spec.rb +241 -0
- data/spec/lib/pwn/plugins/vault_yubikey_safety_spec.rb +168 -0
- data/spec/lib/pwn/plugins/vault_yubikey_spec.rb +128 -0
- data/spec/lib/pwn/plugins/vault_yubikey_vector_spec.rb +45 -0
- data/spec/lib/pwn/plugins/yubikey/fido_spec.rb +854 -0
- data/spec/lib/pwn/plugins/yubikey/hsmauth_spec.rb +235 -0
- data/spec/lib/pwn/plugins/yubikey/management_spec.rb +277 -0
- data/spec/lib/pwn/plugins/yubikey/oath_spec.rb +349 -0
- data/spec/lib/pwn/plugins/yubikey/openpgp_spec.rb +810 -0
- data/spec/lib/pwn/plugins/yubikey/otp_spec.rb +337 -0
- data/spec/lib/pwn/plugins/yubikey/piv_spec.rb +646 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_secure_spec.rb +28 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_spec.rb +149 -0
- data/spec/lib/pwn/plugins/yubikey/protocol_vectors_spec.rb +18 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/scp_spec.rb +256 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain/support.rb +153 -0
- data/spec/lib/pwn/plugins/yubikey/security_domain_spec.rb +296 -0
- data/spec/lib/pwn/plugins/yubikey/transport_spec.rb +760 -0
- data/spec/lib/pwn/plugins/yubikey_security_spec.rb +260 -0
- data/spec/lib/pwn/plugins/yubikey_spec.rb +209 -0
- data/spec/lib/pwn/plugins/yubikey_terminal_spec.rb +147 -0
- data/spec/lib/pwn/redaction_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/session_cleanup_default_spec.rb +7 -5
- data/spec/lib/pwn/session_cleanup_payload_spec.rb +11 -11
- data/spec/lib/pwn/session_cleanup_recovery_spec.rb +34 -20
- data/spec/lib/pwn/setup_native_yubikey_spec.rb +66 -0
- data/spec/lib/pwn/setup_vault_yubikey_spec.rb +61 -0
- data/spec/lib/pwn/setup_yubikey_spec.rb +40 -0
- data/spec/lib/pwn/www/login_yubikey_spec.rb +273 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/harness/cli_faults.rb +13 -9
- data/spec/support/harness/controller_spill.rb +26 -0
- data/spec/support/harness/controller_state.rb +54 -0
- data/spec/support/harness/evaluate.rb +12 -1
- data/spec/support/harness/experiments.rb +1 -1
- data/spec/support/harness/fake_provider.rb +22 -6
- data/spec/support/harness/fault_preload.rb +3 -1
- data/spec/support/harness/mission_console.rb +54 -0
- data/spec/support/harness/mission_curses_pty.rb +41 -0
- data/spec/support/harness/mission_dispatch.rb +30 -0
- data/spec/support/harness/mission_fixture_preload.rb +24 -0
- data/spec/support/harness/mission_job_lifetime.rb +25 -0
- data/spec/support/harness/mission_legacy.rb +25 -0
- data/spec/support/harness/mission_normal_exit.rb +34 -0
- data/spec/support/harness/mission_swarm.rb +23 -0
- data/spec/support/harness/mission_unsupported.rb +29 -0
- data/spec/support/harness/package_acceptance.rb +33 -5
- data/spec/support/harness/session_acceptance.rb +4 -4
- data/spec/support/harness/swarm_acceptance.rb +1 -1
- data/spec/support/harness/swarm_control.rb +44 -0
- data/spec/support/mission_namespace.rb +14 -0
- data/spec/support/mission_pipe_queue.rb +33 -0
- data/third_party/pwn_rdoc.jsonl +605 -19
- metadata +129 -16
- data/scripts/benchmark_policy.rb +0 -376
- data/scripts/build_gate.rb +0 -58
- data/spec/integration/build_gate_spec.rb +0 -73
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -990,6 +990,15 @@ module PWN
|
|
|
990
990
|
text.sub(%r{\s*\[/UNTRUSTED TOOL OUTPUT\]\s*\z}m, '')
|
|
991
991
|
end
|
|
992
992
|
|
|
993
|
+
private_class_method def self.terminal_effect_blocker(opts = {})
|
|
994
|
+
runtime = opts[:runtime]
|
|
995
|
+
diagnostic = runtime&.blocker
|
|
996
|
+
return nil unless diagnostic
|
|
997
|
+
|
|
998
|
+
runtime.finish!(status: 'blocked', unmet: runtime.unmet + [diagnostic])
|
|
999
|
+
"[pwn-ai] blocked: #{diagnostic}"
|
|
1000
|
+
end
|
|
1001
|
+
|
|
993
1002
|
private_class_method def self.declared_deliverables(opts = {})
|
|
994
1003
|
Array(declared_contract(request: opts[:request])[:paths])
|
|
995
1004
|
end
|
|
@@ -1004,7 +1013,7 @@ module PWN
|
|
|
1004
1013
|
end
|
|
1005
1014
|
# Paths are user-owned. Inference cannot invent a destination or
|
|
1006
1015
|
# convert the source document into a write obligation.
|
|
1007
|
-
contract = contract.merge(paths: literals
|
|
1016
|
+
contract = contract.merge(paths: literals)
|
|
1008
1017
|
Thread.current[:pwn_loop_deliverables] = contract if Thread.current[:pwn_loop_active]
|
|
1009
1018
|
contract
|
|
1010
1019
|
end
|
|
@@ -2091,6 +2100,10 @@ module PWN
|
|
|
2091
2100
|
model = opts[:model] || Thread.current[:pwn_swarm_model] || config[:model]
|
|
2092
2101
|
cwt_opts[:model] = model unless model.to_s.strip.empty?
|
|
2093
2102
|
cwt_opts[:temp] = opts[:temp] unless opts[:temp].nil?
|
|
2103
|
+
if PWN::AI::ServerTools.mixable?(engine: engine, model: cwt_opts[:model])
|
|
2104
|
+
hosted = Array(config[:server_tools]).map(&:to_s).reject(&:empty?)
|
|
2105
|
+
cwt_opts[:server_tools] = hosted if hosted.any?
|
|
2106
|
+
end
|
|
2094
2107
|
if tools && !tools.empty?
|
|
2095
2108
|
env_tc = begin
|
|
2096
2109
|
config[:tool_choice]
|
|
@@ -3637,6 +3650,9 @@ module PWN
|
|
|
3637
3650
|
return "[pwn-ai] #{status}: explicit request verification did not pass."
|
|
3638
3651
|
end
|
|
3639
3652
|
end
|
|
3653
|
+
effect_blocker = terminal_effect_blocker(runtime: runtime)
|
|
3654
|
+
return effect_blocker if effect_blocker
|
|
3655
|
+
|
|
3640
3656
|
# Completion is an evidence decision, never a prose classifier.
|
|
3641
3657
|
unless may_finalize?(
|
|
3642
3658
|
request: request,
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'socket'
|
|
4
|
+
require 'fiddle'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'date'
|
|
7
|
+
require 'rbconfig'
|
|
8
|
+
require 'timeout'
|
|
9
|
+
|
|
10
|
+
# Load root autoloads before opening nested namespaces on direct require.
|
|
11
|
+
require 'pwn'
|
|
12
|
+
|
|
13
|
+
module PWN
|
|
14
|
+
module AI
|
|
15
|
+
module Agent
|
|
16
|
+
# Exec-based user namespace bootstrap; the terminal never enters a namespace.
|
|
17
|
+
# Only typed descriptors cross the private socket. Ruby closures are permitted
|
|
18
|
+
# below an existing owner, where fork does not need a new user namespace.
|
|
19
|
+
class MissionNamespace # rubocop:disable Metrics/ClassLength -- one exec handshake and process ownership protocol
|
|
20
|
+
class Unavailable < StandardError; end
|
|
21
|
+
CONTEXT_KEYS = %i[pwn_swarm_engine pwn_swarm_model pwn_swarm_id pwn_swarm_depth pwn_solve_workspace pwn_solve_tools
|
|
22
|
+
pwn_session_id pwn_artifact_contract pwn_active_steering_request pwn_turn_finalizer_depth
|
|
23
|
+
pwn_reflect_depth pwn_request_intent pwn_loop_budget_s pwn_loop_mutations pwn_loop_tokens].freeze
|
|
24
|
+
attr_reader :status
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
attr_accessor :owned, :bootstrap_requires, :force_exec
|
|
28
|
+
|
|
29
|
+
def prepared?
|
|
30
|
+
rows = File.readlines('/proc/self/uid_map').map { |line| line.split.map(&:to_i) }
|
|
31
|
+
count = rows.length == 1 ? rows.first.last : nil
|
|
32
|
+
# The initial host map is the full 32-bit range. Any smaller single
|
|
33
|
+
# line is a user namespace, including a root test bootstrap that must
|
|
34
|
+
# still include a non-root account.
|
|
35
|
+
count.is_a?(Integer) && count.positive? && count < 4_294_967_295
|
|
36
|
+
rescue SystemCallError
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def io(number)
|
|
41
|
+
stream = Thread.current[:pwn_mission_fds]&.fetch(number, nil) || IO.for_fd(number)
|
|
42
|
+
stream.close_on_exec = true if number > 2
|
|
43
|
+
stream
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def native(name, arguments)
|
|
47
|
+
Fiddle::Function.new(Fiddle::Handle::DEFAULT[name], arguments, Fiddle::TYPE_INT)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def checked(result, operation)
|
|
51
|
+
raise Unavailable, "#{operation}: #{SystemCallError.new(operation, Fiddle.last_error).message}; install util-linux unshare and enable Linux user/PID namespaces and pidfds" if result.negative?
|
|
52
|
+
|
|
53
|
+
result
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Tagged JSON preserves Symbol hash keys and values without deserializing
|
|
57
|
+
# executable Ruby objects or placing credentials in argv or temporary files.
|
|
58
|
+
def encode(value, depth = 0)
|
|
59
|
+
raise ArgumentError, 'mission snapshot is too deep or cyclic' if depth > 30
|
|
60
|
+
|
|
61
|
+
case value
|
|
62
|
+
when Hash then ['hash', value.map { |key, item| [encode(key, depth + 1), encode(item, depth + 1)] }]
|
|
63
|
+
when Array then ['array', value.map { |item| encode(item, depth + 1) }]
|
|
64
|
+
when Time then ['time', value.to_i, value.nsec, value.utc_offset, value.utc?]
|
|
65
|
+
when DateTime then ['datetime', value.iso8601(9)]
|
|
66
|
+
when Date then ['date', value.iso8601]
|
|
67
|
+
when Symbol then ['symbol', value.to_s]
|
|
68
|
+
when String then ['string', [value.b].pack('m0'), value.encoding.name]
|
|
69
|
+
when NilClass, TrueClass, FalseClass, Numeric then ['scalar', value]
|
|
70
|
+
else raise ArgumentError, "mission snapshot cannot transfer #{value.class}; use a serializable descriptor"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def decode(value)
|
|
75
|
+
type, body, encoding = value
|
|
76
|
+
case type
|
|
77
|
+
when 'hash' then body.to_h { |key, item| [decode(key), decode(item)] }
|
|
78
|
+
when 'array' then body.map { |item| decode(item) }
|
|
79
|
+
when 'time' then value[4] ? Time.at(body, encoding, :nanosecond).utc : Time.at(body, encoding, :nanosecond).getlocal(value[3])
|
|
80
|
+
when 'datetime' then DateTime.iso8601(body)
|
|
81
|
+
when 'date' then Date.iso8601(body)
|
|
82
|
+
when 'symbol' then body.to_sym
|
|
83
|
+
when 'string' then body.unpack1('m0').force_encoding(encoding)
|
|
84
|
+
when 'scalar' then body
|
|
85
|
+
else raise ArgumentError, 'invalid mission snapshot'
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def supervise
|
|
90
|
+
channel = UNIXSocket.for_fd(3)
|
|
91
|
+
snapshot = decode(JSON.parse(channel.gets))
|
|
92
|
+
checked(native('unshare', [Fiddle::TYPE_INT]).call(0x20000000), 'unshare PID namespace')
|
|
93
|
+
reader, writer = IO.pipe
|
|
94
|
+
armed, arm = IO.pipe
|
|
95
|
+
init = fork do
|
|
96
|
+
armed.close
|
|
97
|
+
checked(native('prctl', [Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG]).call(1, Signal.list.fetch('KILL'), 0, 0, 0), 'PR_SET_PDEATHSIG')
|
|
98
|
+
arm.write('1')
|
|
99
|
+
arm.close
|
|
100
|
+
channel.close
|
|
101
|
+
writer.close
|
|
102
|
+
exit! unless reader.read(1) == '1'
|
|
103
|
+
|
|
104
|
+
reader.close
|
|
105
|
+
self.owned = true
|
|
106
|
+
self.bootstrap_requires = snapshot.fetch(:requires, [])
|
|
107
|
+
PWN::Env.replace(decode(encode(snapshot.fetch(:env))))
|
|
108
|
+
bootstrap_requires.each { |path| require path }
|
|
109
|
+
PWN::Env.replace(snapshot.fetch(:env))
|
|
110
|
+
PWN.const_set(:Skills, snapshot[:skills]) if snapshot[:skills] && !PWN.const_defined?(:Skills)
|
|
111
|
+
snapshot.fetch(:locals).each { |key, value| Thread.current[key] = value }
|
|
112
|
+
begin
|
|
113
|
+
Thread.handle_interrupt(Exception => :immediate) { execute(snapshot.fetch(:descriptor)) }
|
|
114
|
+
completed = true
|
|
115
|
+
ensure
|
|
116
|
+
exit!(completed ? 0 : 1)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
arm.close
|
|
120
|
+
raise Unavailable, 'namespace init exited before arming parent-death protection' unless armed.read(1) == '1'
|
|
121
|
+
|
|
122
|
+
armed.close
|
|
123
|
+
reader.close
|
|
124
|
+
fd = IO.new(checked(native('pidfd_open', [Fiddle::TYPE_INT, Fiddle::TYPE_INT]).call(init, 0), 'pidfd_open'))
|
|
125
|
+
channel.send_io(fd)
|
|
126
|
+
exit! unless channel.read(1) == '1'
|
|
127
|
+
|
|
128
|
+
writer.write('1')
|
|
129
|
+
writer.close
|
|
130
|
+
readable = IO.select([channel, fd]).first
|
|
131
|
+
native('pidfd_send_signal', [Fiddle::TYPE_INT, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT]).call(fd.fileno, Signal.list.fetch('KILL'), 0, 0) if readable.include?(channel)
|
|
132
|
+
_pid, status = Process.wait2(init)
|
|
133
|
+
init = nil
|
|
134
|
+
fd.close
|
|
135
|
+
status.exitstatus || (128 + status.termsig)
|
|
136
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- startup must fail closed
|
|
137
|
+
warn "Mission namespace unavailable: #{e.class}: #{e.message}"
|
|
138
|
+
ensure
|
|
139
|
+
if init
|
|
140
|
+
begin
|
|
141
|
+
Process.kill('KILL', init)
|
|
142
|
+
rescue Errno::ESRCH
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
begin
|
|
146
|
+
Process.wait(init)
|
|
147
|
+
rescue Errno::ECHILD
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
channel&.close unless channel&.closed?
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def execute(descriptor)
|
|
155
|
+
case descriptor.fetch(:operation).to_sym
|
|
156
|
+
when :command
|
|
157
|
+
_pid, status = Process.wait2(Process.spawn(*descriptor.fetch(:argv)))
|
|
158
|
+
exit!(status.exitstatus || (128 + status.termsig))
|
|
159
|
+
when :loop
|
|
160
|
+
require 'pwn/ai/cli'
|
|
161
|
+
PWN::AI::CLI.send(:mission_entry, descriptor)
|
|
162
|
+
when :protected
|
|
163
|
+
PWN::Plugins::AISandbox.send(:mission_entry, descriptor)
|
|
164
|
+
when :console, :slash
|
|
165
|
+
PWN::Plugins::REPL::AIConsole::Console.send(:mission_entry, descriptor: descriptor)
|
|
166
|
+
when :legacy, :steerable
|
|
167
|
+
PWN::Plugins::REPL.pwn_ai_mission_entry(descriptor)
|
|
168
|
+
when :swarm
|
|
169
|
+
PWN::Plugins::REPL::AISwarm::Controller.send(:mission_entry, descriptor: descriptor)
|
|
170
|
+
else
|
|
171
|
+
raise ArgumentError, 'unknown mission operation'
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def initialize(opts = {}, &operation)
|
|
177
|
+
@mutex = Mutex.new
|
|
178
|
+
raise Unavailable, 'mission containment requires Linux' unless RUBY_PLATFORM.include?('linux')
|
|
179
|
+
|
|
180
|
+
if self.class.prepared? && !self.class.force_exec && !(opts[:inherit_namespace] && self.class.owned)
|
|
181
|
+
start_prepared(opts, &operation)
|
|
182
|
+
return
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
if operation
|
|
186
|
+
raise Unavailable, 'host closures cannot cross an exec boundary; use a mission descriptor or explicitly bootstrap under unshare --user --map-current-user --keep-caps' unless self.class.owned
|
|
187
|
+
|
|
188
|
+
@pid = fork do
|
|
189
|
+
Thread.handle_interrupt(Exception => :immediate) { operation.call }
|
|
190
|
+
ensure
|
|
191
|
+
exit!
|
|
192
|
+
end
|
|
193
|
+
@pidfd = IO.new(self.class.checked(self.class.native('pidfd_open', [Fiddle::TYPE_INT, Fiddle::TYPE_INT]).call(@pid, 0), 'pidfd_open'))
|
|
194
|
+
return
|
|
195
|
+
end
|
|
196
|
+
snapshot = { requires: Array(self.class.bootstrap_requires), descriptor: opts.fetch(:descriptor), env: PWN::Env, locals: CONTEXT_KEYS.to_h { |key| [key, Thread.current[key]] } }
|
|
197
|
+
if PWN.const_defined?(:Skills)
|
|
198
|
+
snapshot[:skills] = PWN::Skills
|
|
199
|
+
ruby_skills = PWN::Skills.values.select { |skill| skill[:loaded] && skill[:type] == :ruby }
|
|
200
|
+
snapshot[:requires] += ruby_skills.flat_map do |skill|
|
|
201
|
+
skill[:format] == :legacy ? [skill.fetch(:path)] : Dir.glob(File.join(skill.fetch(:dir), 'scripts', '*.rb'))
|
|
202
|
+
end
|
|
203
|
+
snapshot[:requires].uniq!
|
|
204
|
+
end
|
|
205
|
+
payload = JSON.generate(self.class.encode(snapshot))
|
|
206
|
+
@channel, child = UNIXSocket.pair
|
|
207
|
+
mapping = opts.fetch(:fds, {}).merge(3 => child)
|
|
208
|
+
@pid = Process.spawn('unshare', '--user', '--map-current-user', '--keep-caps', RbConfig.ruby, '-I', $LOAD_PATH.join(File::PATH_SEPARATOR), '-rpwn', '-e', 'exit!(PWN::AI::Agent::MissionNamespace.supervise || 1)', mapping.merge(close_others: true))
|
|
209
|
+
child.close
|
|
210
|
+
Timeout.timeout(15) do
|
|
211
|
+
@channel.puts(payload)
|
|
212
|
+
@pidfd = @channel.recv_io
|
|
213
|
+
@channel.write('1')
|
|
214
|
+
end
|
|
215
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- failed startup cannot leak a child
|
|
216
|
+
close
|
|
217
|
+
raise Unavailable, "mission not started: #{e.message}; install util-linux unshare and enable Linux user/PID namespaces and pidfds"
|
|
218
|
+
ensure
|
|
219
|
+
child&.close unless child&.closed?
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def wait
|
|
223
|
+
@pidfd.wait_readable if @pidfd && !@pidfd.closed?
|
|
224
|
+
@mutex.synchronize { reap }
|
|
225
|
+
rescue IOError, Errno::EBADF
|
|
226
|
+
@mutex.synchronize { reap }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def close
|
|
230
|
+
return :reaped unless @mutex
|
|
231
|
+
|
|
232
|
+
@mutex.synchronize do
|
|
233
|
+
if @pidfd && !@pidfd.closed?
|
|
234
|
+
result = self.class.native('pidfd_send_signal', [Fiddle::TYPE_INT, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT]).call(@pidfd.fileno, Signal.list.fetch('KILL'), 0, 0)
|
|
235
|
+
self.class.checked(result, 'pidfd_send_signal') unless result.negative? && Fiddle.last_error == Errno::ESRCH::Errno
|
|
236
|
+
elsif @pid
|
|
237
|
+
begin
|
|
238
|
+
Process.kill('KILL', @pid)
|
|
239
|
+
rescue Errno::ESRCH
|
|
240
|
+
nil
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
reap
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
public_class_method def self.authors
|
|
248
|
+
"AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n"
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
public_class_method def self.help
|
|
252
|
+
puts <<~HELP
|
|
253
|
+
# Identify module authors.
|
|
254
|
+
#{self}.authors
|
|
255
|
+
|
|
256
|
+
# Launch a private Linux mission and retain its cancellation owner.
|
|
257
|
+
#{self}.new(
|
|
258
|
+
descriptor: 'required - serializable operation and request parameters',
|
|
259
|
+
fds: 'optional - explicitly inherited descriptor map'
|
|
260
|
+
)
|
|
261
|
+
HELP
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
private
|
|
265
|
+
|
|
266
|
+
def start_prepared(opts, &operation)
|
|
267
|
+
ready, announce = IO.pipe
|
|
268
|
+
release, gate = IO.pipe
|
|
269
|
+
owner = Process.pid
|
|
270
|
+
@pid = fork do
|
|
271
|
+
ready.close
|
|
272
|
+
gate.close
|
|
273
|
+
begin
|
|
274
|
+
self.class.checked(self.class.native('prctl', [Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG]).call(1, Signal.list.fetch('KILL'), 0, 0, 0), 'PR_SET_PDEATHSIG')
|
|
275
|
+
exit! unless Process.ppid == owner
|
|
276
|
+
self.class.checked(self.class.native('unshare', [Fiddle::TYPE_INT]).call(0x20000000), 'unshare PID namespace')
|
|
277
|
+
armed, arm = IO.pipe
|
|
278
|
+
init = fork do
|
|
279
|
+
armed.close
|
|
280
|
+
announce.close
|
|
281
|
+
self.class.checked(self.class.native('prctl', [Fiddle::TYPE_INT, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG, Fiddle::TYPE_LONG]).call(1, Signal.list.fetch('KILL'), 0, 0, 0), 'PR_SET_PDEATHSIG')
|
|
282
|
+
arm.write('1')
|
|
283
|
+
arm.close
|
|
284
|
+
exit! unless release.read(1) == '1'
|
|
285
|
+
release.close
|
|
286
|
+
self.class.owned = true
|
|
287
|
+
Thread.current[:pwn_mission_fds] = opts.fetch(:fds, {})
|
|
288
|
+
begin
|
|
289
|
+
Thread.handle_interrupt(Exception => :immediate) { operation ? operation.call : self.class.execute(opts.fetch(:descriptor)) }
|
|
290
|
+
completed = true
|
|
291
|
+
ensure
|
|
292
|
+
exit!(completed ? 0 : 1)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
arm.close
|
|
296
|
+
raise Unavailable, 'namespace init exited before arming parent-death protection' unless armed.read(1) == '1'
|
|
297
|
+
|
|
298
|
+
armed.close
|
|
299
|
+
announce.puts(JSON.generate(pid: init))
|
|
300
|
+
announce.close
|
|
301
|
+
release.close
|
|
302
|
+
_pid, status = Process.wait2(init)
|
|
303
|
+
exit!(status.exitstatus || (128 + status.termsig))
|
|
304
|
+
rescue Exception => e # rubocop:disable Lint/RescueException -- report child startup errors
|
|
305
|
+
announce.puts(JSON.generate(error: e.message)) unless announce.closed?
|
|
306
|
+
ensure
|
|
307
|
+
exit!
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
announce.close
|
|
311
|
+
release.close
|
|
312
|
+
message = JSON.parse(ready.gets || '{}')
|
|
313
|
+
raise Unavailable, message.fetch('error', 'namespace supervisor exited') unless message['pid']
|
|
314
|
+
|
|
315
|
+
@pidfd = IO.new(self.class.checked(self.class.native('pidfd_open', [Fiddle::TYPE_INT, Fiddle::TYPE_INT]).call(message.fetch('pid'), 0), 'pidfd_open'))
|
|
316
|
+
gate.write('1')
|
|
317
|
+
ensure
|
|
318
|
+
[ready, announce, release, gate].compact.each { |io| io.close unless io.closed? }
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def reap
|
|
322
|
+
_pid, @status = Process.wait2(@pid) if @pid
|
|
323
|
+
@pid = nil
|
|
324
|
+
@channel&.close unless @channel&.closed?
|
|
325
|
+
@pidfd&.close unless @pidfd&.closed?
|
|
326
|
+
:reaped
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|