diff --git a/data/vuln_envs/jenkins.yml b/data/vuln_envs/jenkins.yml new file mode 100644 index 0000000000000..b72e66fc3539a --- /dev/null +++ b/data/vuln_envs/jenkins.yml @@ -0,0 +1,49 @@ +name: jenkins +description: Jenkins CI server with Groovy Script Console enabled + +versions: + "2.361": + image: vulnhub/jenkins:2.361 + build_args: + JENKINS_VERSION: "2.361" + "2.375": + image: vulnhub/jenkins:2.375 + build_args: + JENKINS_VERSION: "2.375" + +shared: + ports: + http: 8080 + + volumes: + jenkins_home: + container_path: /var/jenkins_home + persist: false + + health_check: + type: http + path: /login + expected_status: 200 + interval: 5 + timeout: 2 + retries: 12 + + credentials: + default: + username: admin + password: admin + + datastore_defaults: + TARGETURI: /script + + ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 diff --git a/docs/ci_workflow.md b/docs/ci_workflow.md new file mode 100644 index 0000000000000..f4c9a90a137af --- /dev/null +++ b/docs/ci_workflow.md @@ -0,0 +1,194 @@ +# CI Workflow: Automated Exploit Verification + + + +This document defines how `test_env` will be used in GitHub Actions to automatically: +- Provision vulnerable environments from shared definitions +- Execute exploits with pre-configured datastore options +- Validate expected outcomes (session creation, command output) +- Clean up all containers to prevent resource leaks + +**Key principle:** CI consumes the same environment definitions used for local testing. No duplicated container configuration. + + +## Directory Structure + +This directory structure will created as part of the project: + +``` +metasploit-framework/ +├── ci/ +│ ├── test_activemq.rc +│ ├── test_jenkins.rc +│ └── test_drupal.rc +├── .github/ +│ └── workflows/ +│ └── vuln-env-test.yml +├── data/ +│ └── vuln_envs/ +│ ├── activemq.yml +│ ├── jenkins.yml +│ └── drupal.yml +└── docs/ + └── ci_workflow.md +``` + + + +A **resource script** with a `.rc` extension that contains msfconsole commands. Instead of typing commands one by one into msfconsole, they will be saved in a file and run: + +```bash +./msfconsole -q -r path/to/script.rc +``` + +Metasploit reads the file and executes each line automatically, as if it's typed. + + + +### Example: ci/test_jenkins.rc + +**What it is:** A text file containing msfconsole commands to test the Jenkins module automatically. + +**What it contains:** +```text +load test_env +use exploit/multi/http/jenkins_script_console +test_env build VERSION=2.361 +test_env exec 1 +test_env remove-all +exit +``` + +**What each line does:** +| Line | Command | Purpose | +|------|---------|---------| +| 1 | `load test_env` | Load the test_env plugin | +| 2 | `use exploit/multi/http/jenkins_script_console` | Select the exploit module | +| 3 | `test_env build VERSION=2.361` | Build environment using Jenkins version 2.361 | +| 4 | `test_env exec 1` | Execute exploit against environment ID 1 | +| 5 | `test_env remove-all` | Stop and remove all containers | +| 6 | `exit` | Close msfconsole | + +**How to run it manually (for testing):** +```bash +./msfconsole -q -r ci/test_jenkins.rc +``` +--- + +## GitHub Actions Workflow + +**What this is:** A YAML file that tells GitHub Actions what to do on every push or pull request. + +**File:** `.github/workflows/vuln-env-test.yml` + +**What it contains:** +```yaml +name: Vulnerable Environment Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test-jenkins: + name: Test Jenkins Script Console + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Run Jenkins exploit test + run: | + ./msfconsole -q -r ci/test_jenkins.rc + + - name: Verify session was created + run: | + grep "Session.*opened" ~/.msf4/logs/framework.log || echo "WARNING: No session log found" + + - name: Verify no containers left behind + run: | + REMAINING=$(docker ps -q | wc -l) + if [ "$REMAINING" -eq 0 ]; then + echo "Clean: No containers remaining" + else + echo "FAIL: $REMAINING container(s) still running" + docker ps + exit 1 + fi +``` + +**What each step does:** +| Step | Action | Purpose | +|------|--------|---------| +| Checkout | `actions/checkout@v4` | Download your code | +| Set up Ruby | `ruby/setup-ruby@v1` | Install Ruby 3.2 and gems | +| Set up Docker | `docker/setup-buildx-action@v3` | Install Docker | +| Cache Docker layers | `actions/cache@v3` | Speed up image pulls | +| Run exploit test | `./msfconsole -q -r ci/test_jenkins.rc` | Execute the Jenkins resource script | +| Verify session | `grep "Session.*opened"` | Confirm exploit succeeded | +| Verify cleanup | `docker ps -q` | Confirm no leaked containers | + +--- + +## Validation Criteria + +| Step | Expected Result | How It Is Checked | On Failure | +|------|----------------|-------------------|------------| +| `test_env build` | Container starts, health check passes | Console output contains "Environment ready" | Workflow fails | +| `test_env exec 1` | Session opens | `framework.log` contains "Session.*opened" | Workflow fails | +| `test_env remove-all` | All containers removed | `docker ps -q` returns empty | Workflow fails | +| Post-cleanup | Zero `msf.vulnenv` containers remain | `docker ps -a --filter "label=msf.vulnenv.managed_by=test_env"` returns empty | Workflow fails | + +--- + +## CI Metadata in Environment Definitions + +Environment definitions include a `ci` section so the automation knows what payload to use and what to validate: + +```yaml +# data/vuln_envs/jenkins.yml +ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + TARGETURI: /script + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 +``` + +### Schema Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `ci.exploit.payload` | String | Yes | Payload to use for automated execution | +| `ci.exploit.options` | Hash | No | Datastore options: `LHOST`, `LPORT`, `TARGETURI`, etc. | +| `ci.validation.expected_session` | Boolean | Yes | Whether a session must be created | +| `ci.validation.session_type` | String | No | Expected session type: `meterpreter`, `shell` | +| `ci.validation.expected_output` | String | No | Substring to verify in session output | +| `ci.validation.timeout` | Integer | Yes | Max seconds to wait for validation | + diff --git a/docs/reference_modules.md b/docs/reference_modules.md new file mode 100644 index 0000000000000..eb8d7e5e96516 --- /dev/null +++ b/docs/reference_modules.md @@ -0,0 +1,48 @@ +# Reference Modules for test_env + +## Selection Criteria +- Cover different service types: Java message broker (ActiveMQ), CI server (Jenkins), CMS (Drupal) +- Have clear, single-port (or well-defined multi-port) mappings +- Have existing Docker images with known vulnerable versions +- Demonstrate different health check patterns (API endpoint, login page, root page) +- Include both authenticated and unauthenticated exploit scenarios + +--- + +## Module 1: Apache ActiveMQ Jolokia RCE (Mentor Suggested) +- **Path:** `exploit/multi/http/apache_activemq_jolokia_rce` +- **Type:** Java web application (JMX-over-HTTP) +- **Ports:** 8161 (web console / Jolokia API), 61616 (OpenWire broker) +- **Health Check:** HTTP GET `/api/jolokia/` expecting 200, or GET `/` expecting 200 +- **Why:** h00die suggested PR #21497. Has a verified Docker one-liner. Real-world CVE-2026-34197. +- **VulnEnv Definition:** `activemq` +- **Docker Image:** `apache/activemq-classic:5.18.6` +- **Docker Run:** `docker run -d --name activemq -p 8161:8161 -p 61616:61616 apache/activemq-classic:5.18.6` +- **Credentials:** admin / admin +- **Exploit Context:** Requires authenticated Jolokia access; `TARGETURI` typically `/api/jolokia/` + +--- + +## Module 2: Jenkins Script Console +- **Path:** `exploit/multi/http/jenkins_script_console` +- **Type:** Web application / CI server +- **Port:** 8080 +- **Health Check:** HTTP GET `/login` expecting 200 +- **Why:** Well-documented, multiple versions exist, clear RPORT→8080 mapping, widely used in exploit development tutorials +- **VulnEnv Definition:** `jenkins` +- **Docker Image:** `vulnhub/jenkins:2.361` +- **Credentials:** admin / admin +- **Exploit Context:** Script Console at `/script` allows Groovy execution; `TARGETURI` typically `/script` + +--- + +## Module 3: Drupal Drupalgeddon2 +- **Path:** `exploit/unix/webapp/drupal_drupalgeddon2` +- **Type:** Web application / CMS +- **Port:** 80 +- **Health Check:** HTTP GET `/` expecting 200 +- **Why:** Simple single-port setup, unauthenticated exploit, different architecture from ActiveMQ/Jenkins, large community interest +- **VulnEnv Definition:** `drupal` +- **Docker Image:** `vulnhub/drupal:CVE-2018-7600` +- **Credentials:** None required (unauthenticated) +- **Exploit Context:** SA-CORE-2018-002 (CVE-2018-7600); remote code execution via form API diff --git a/docs/test_env/README.md b/docs/test_env/README.md new file mode 100644 index 0000000000000..bda6e89f923fa --- /dev/null +++ b/docs/test_env/README.md @@ -0,0 +1,50 @@ +# test_env Design Documentation + +This directory contains the architecture and workflow design for the `test_env` (VulnEnv) plugin. + +## Architecture Documents + +| Document | Description | +|----------|-------------| +| [01-command-dispatcher.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/architecture/01-command-dispatcher.md) | How `test_env` is added to msfconsole via plugin dispatcher | +| [02-module-metadata.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/architecture/02-module-metadata.md) | How modules expose `VulnEnv` metadata and how the plugin reads it | +| [03-database-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/architecture/03-database-schema.md) | Registry persistence: in-memory Phase 1, PostgreSQL Phase 2 | +| [04-environment-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/architecture/04-environment-schema.md) | YAML schema for shared environment definitions in `data/vuln_envs/` | +| [05-runtime-adapter.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/05-runtime-adapter.md) | Docker/Podman abstraction, port allocation, container labels | + +## Workflow & Planning Documents + +| Document | Description | +|----------|-------------| +| [reference_modules.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/reference_modules.md) | 3 reference modules selected for implementation (ActiveMQ, Jenkins, Drupal) | +| [workflow.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/workflow.md) | Target user workflows and console transcripts (acceptance criteria) | +| [ci_workflow.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/test_env/ci_workflow.md) | GitHub Actions CI integration with resource scripts | + +## Plugin File + +- [plugins/test_env.rb](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/plugins/test_env.rb) — Main plugin implementation (Week 1 skeleton) + +``` +nayera@Nero:~/git/metasploit-framework$ ./msfconsole -q -x "load test_env; exit" +[*] VulnEnv plugin loaded. +[*] Successfully loaded plugin: vulnenv + +nayera@Nero:~/git/metasploit-framework$ ./msfconsole -q -x "load test_env; test_env help; exit" +[*] VulnEnv plugin loaded. +[*] Successfully loaded plugin: vulnenv +Usage: test_env + +Commands: + build Build and launch environment for active module + list List tracked environments + stop Stop a running environment + start Restart a stopped environment + remove Tear down an environment + remove-all Tear down all environments + exec Execute exploit against environment + help Show this help +``` + +## Data Files + +- [data/vuln_envs/jenkins.ym](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/data/vuln_envs/jenkins.yml) — Reference environment definition (Week 1 draft) diff --git a/docs/test_env/architecture/.gitkeep b/docs/test_env/architecture/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docs/test_env/architecture/01-command-dispatcher.md b/docs/test_env/architecture/01-command-dispatcher.md new file mode 100644 index 0000000000000..32075f0fa94ad --- /dev/null +++ b/docs/test_env/architecture/01-command-dispatcher.md @@ -0,0 +1,154 @@ +# Command Dispatcher Architecture + +## What This Document Is +This defines how the `test_env` command will be added to msfconsole. + +## How Commands Work in Metasploit (From Source Code I Read) + +### 1. Plugin Registration +From `plugins/sample.rb`, I saw: +- Plugin inherits from `Msf::Plugin` +- Plugin has an inner `ConsoleCommandDispatcher` class +- Dispatcher `include Msf::Ui::Console::CommandDispatcher` +- `commands` method returns a hash: `{ 'command_name' => 'description' }` +- `initialize` calls `add_console_dispatcher(ConsoleCommandDispatcher)` +- `cleanup` calls `remove_console_dispatcher('Name')` + +### 2. Command Routing +From `lib/rex/ui/text/dispatcher_shell.rb` line 576, I saw: +```ruby +def run_command(dispatcher, method, arguments) +``` +When I type `test_env build`, this happens: +1. Shell parses the line into `["test_env", "build"]` +2. Shell finds my plugin's dispatcher in `dispatcher_stack` +3. Shell calls `run_command(my_dispatcher, "test_env", ["build"])` +4. Which calls `my_dispatcher.cmd_test_env("build")` + +### 3. Multi-Command Pattern +From `lib/msf/ui/console/command_dispatcher/jobs.rb`, I saw: +- One dispatcher can handle multiple commands via `commands` hash +- `cmd_jobs(*args)` uses `args.shift` to get the subcommand +- `cmd_rename_job_tabs` provides tab completion +- `cmd_jobs_help` prints usage information + +## My Design: test_env Command Dispatcher + +### Class Structure +``` +Msf::Plugin +└── Msf::Plugin::VulnEnv + └── Msf::Plugin::VulnEnv::ConsoleCommandDispatcher + └── (includes Msf::Ui::Console::CommandDispatcher) +``` + +### Commands Hash +| Command | Description | +|---------|-------------| +| `test_env` | Manage vulnerable test environments | + +### Subcommands (Handled Inside cmd_test_env) +| Subcommand | Handler Method | What It Does | +|-----------|---------------|--------------| +| `build` | `cmd_test_env_build(args)` | Build and launch environment for active module | +| `list` | `cmd_test_env_list(args)` | Show all tracked environments | +| `stop ` | `cmd_test_env_stop(args)` | Stop a running container | +| `start ` | `cmd_test_env_start(args)` | Restart a stopped container | +| `remove ` | `cmd_test_env_remove(args)` | Tear down a container | +| `remove-all` | `cmd_test_env_remove_all(args)` | Tear down all containers | +| `exec ` | `cmd_test_env_exec(args)` | Run exploit against environment | +| `help` | `cmd_test_env_help` | Show usage | + +## Sample code for solid clarification +### Argument Parsing Logic +```ruby +def cmd_test_env(*args) + # If no args or help requested, show help + if args.empty? || args.first == '-h' || args.first == '--help' + cmd_test_env_help + return + end + + # First argument is the subcommand + subcommand = args.shift + + # Route to appropriate handler + case subcommand + when 'build' then cmd_test_env_build(args) + when 'list' then cmd_test_env_list(args) + when 'stop' then cmd_test_env_stop(args) + when 'start' then cmd_test_env_start(args) + when 'remove' then cmd_test_env_remove(args) + when 'remove-all' then cmd_test_env_remove_all(args) + when 'exec' then cmd_test_env_exec(args) + when 'help' then cmd_test_env_help + else + print_error("Unknown subcommand: #{subcommand}") + cmd_test_env_help + end +end +``` + +### Tab Completion +```ruby +def cmd_test_env_tabs(str, words) + # If only "test_env" has been typed, suggest subcommands + if words.length == 1 + return %w[build list stop start remove remove-all exec help] + end + + # If subcommand is stop/start/remove/exec, suggest environment IDs + if words.length == 2 + case words[0] + when 'stop', 'start', 'remove', 'exec' + # TODO: Return IDs from registry (Week 6) + return [] + end + end + + [] +end +``` + +### Error Handling Pattern +Every subcommand follows this pattern: +```ruby +def cmd_test_env_build(args) + begin + # 1. Validate preconditions + mod = driver.active_module + raise "No active module. Use 'use ' first." unless mod + + # 2. Execute logic + # ... (implementation in later weeks) + + # 3. Report success + print_good("Environment built successfully") + + rescue => e + # 4. Report error + print_error("test_env build failed: #{e.message}") + elog("test_env build error: #{e.class} - #{e.message}") + elog(e.backtrace.join("\n")) + end +end +``` + +## Integration Points + +| What I Need | Where It Comes From | How I Access It | +|-------------|-------------------|---------------| +| Framework instance | `Msf::Plugin#initialize` | `framework` (instance variable) | +| Active module | `Msf::Ui::Console::Driver#active_module` | `driver.active_module` | +| Database | `framework.db.active` | Check before DB operations | +| Console output | `Msf::Ui::Console::CommandDispatcher` | `print_status`, `print_error`, `print_good` | + +## Decisions Made + +| Decision | Choice | Reason | +|----------|--------|--------| +| Single command or multiple? | Single `test_env` with subcommands | Matches `jobs` pattern; cleaner namespace | +| How to parse subcommands? | `case` statement on `args.shift` | Same as `cmd_jobs` | +| Tab completion? | `cmd_test_env_tabs` method | For good UX | +| Error handling? | `begin/rescue` with `print_error` | Consistent with framework style | + diff --git a/docs/test_env/architecture/02-module-metadata.md b/docs/test_env/architecture/02-module-metadata.md new file mode 100644 index 0000000000000..bcf8ed09fb41c --- /dev/null +++ b/docs/test_env/architecture/02-module-metadata.md @@ -0,0 +1,209 @@ +# Module Metadata Integration + +## What I Verified + +I wrote and ran `test_final.rb` to verify how to access module metadata. Here is the test script I used: + +```ruby +#!/usr/bin/env ruby + +$LOAD_PATH.unshift(File.expand_path('lib', __dir__)) +require 'msfenv' +require 'msf/core' + +framework = Msf::Simple::Framework.create +mod = framework.modules.create('exploit/multi/http/jenkins_script_console') + +puts "Module: #{mod.fullname}" +puts "" + +# Test: Can we read module_info via send? +puts "=== Reading module_info via send ===" +info = mod.send(:module_info) +puts "Type: #{info.class}" +puts "Keys count: #{info.keys.length}" +puts "Has Name? #{info.key?('Name')}" +puts "Name: #{info['Name']}" +puts "" + +# Test: Can we add a custom key? +puts "=== Adding custom key ===" +info['VulnEnv'] = { + 'definition' => 'jenkins', + 'default_version' => '2.361', + 'port_mapping' => { 8080 => 'RPORT' } +} + +puts "Added VulnEnv" +puts "Has VulnEnv? #{info.key?('VulnEnv')}" +puts "VulnEnv: #{info['VulnEnv'].inspect}" +puts "" + +# Test: Can we read it back? +puts "=== Reading back ===" +info2 = mod.send(:module_info) +puts "Same object? #{info.equal?(info2)}" +puts "Has VulnEnv? #{info2.key?('VulnEnv')}" +puts "VulnEnv: #{info2['VulnEnv'].inspect}" +``` + +### Output I Got + +``` +Module: exploit/multi/http/jenkins_script_console + +=== Reading module_info via send === +Type: Hash +Keys count: 18 +Has Name? true +Name: Jenkins-CI Script-Console Java Execution + +=== Adding custom key === +Added VulnEnv +Has VulnEnv? true +VulnEnv: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} + +=== Reading back === +Same object? true +Has VulnEnv? true +VulnEnv: {"definition"=>"jenkins", "default_version"=>"2.361", "port_mapping"=>{8080=>"RPORT"}} +``` + +### What This Proves + +| Test | Result | +|------|--------| +| `mod.send(:module_info)` returns a Hash | ✅ Yes | +| The Hash contains standard keys like `Name` | ✅ Yes | +| I can write a custom key `VulnEnv` to it | ✅ Yes | +| The custom key persists when read back | ✅ Yes | +| It's the same object (not a copy) | ✅ Yes | + +## Source Code Evidence + +From `lib/msf/core/module/module_info.rb` line 69: + +```ruby +protected + +# @!attribute module_info +attr_accessor :module_info +``` + +`module_info` is a **protected** `attr_accessor`. That is why: +- `mod.module_info` raises `NoMethodError` (protected method) +- `mod.send(:module_info)` works (bypasses access control) + +The framework itself uses `module_info` internally in `lib/msf/core/module.rb`: + +```ruby +# Line 116 +self.module_info = info + +# Line 129-132 +self.author = Msf::Author.transform(merge_module_info_with_target_info(module_info, 'Author')) +self.arch = Rex::Transformer.transform(merge_module_info_with_target_info(module_info, 'Arch'), Array, [ String ], 'Arch') +``` + +And in `lib/msf/core/module/module_info.rb`: + +```ruby +# Line 41 +def name + module_info['Name'] +end +``` + +## The Correct Way to Access Module Metadata + +```ruby +# In plugin's command dispatcher: +def cmd_test_env_build(args) + # 1. Get active module from driver + mod = driver.active_module + raise "No active module. Use 'use ' first." unless mod + + # 2. Access module_info via send (protected accessor) + info = mod.send(:module_info) + + # 3. Read VulnEnv configuration + vuln_env = info['VulnEnv'] + raise "Module has no VulnEnv config" unless vuln_env + + # 4. Extract values + definition_name = vuln_env['definition'] # 'jenkins' + version = vuln_env['default_version'] # '2.361' + port_mapping = vuln_env['port_mapping'] # {8080 => 'RPORT'} + + # 5. Load YAML definition file + yaml_path = File.join(Msf::Config.data_directory, 'vuln_envs', "#{definition_name}.yml") +end +``` + +## Why send(:module_info) Is Acceptable + +- `module_info` is **protected**, not private — meant for subclass/extension access +- Metasploit plugins are **framework extensions**, not external code +- The framework itself accesses `module_info` directly in the `ModuleInfo` mixin +- This is a **standard Ruby pattern** for working with protected framework internals + +## Alternative: Encapsulate in Helper Method (Is it better to do or not?) + +If It's not preferable not to use `send` directly everywhere: + +```ruby +class Plugin::VulnEnv < Msf::Plugin + class ConsoleCommandDispatcher + # Encapsulate the send call for clarity + def get_module_vuln_env(mod) + mod.send(:module_info)['VulnEnv'] + end + + def cmd_test_env_build(args) + mod = driver.active_module + vuln_env = get_module_vuln_env(mod) + # ... + end + end +end +``` + +## VulnEnv Schema + +```ruby +'VulnEnv' => { + 'definition' => String, # e.g., 'jenkins' → data/vuln_envs/jenkins.yml + 'default_version' => String, # e.g., '2.361' + 'port_mapping' => Hash, # { container_port => 'RPORT' } + 'datastore_overrides' => Hash # optional: { 'TARGETURI' => '/script' } +} +``` + +## Resolution Flow + +``` +test_env build called + ↓ +driver.active_module → Msf::Module instance + ↓ +mod.send(:module_info)['VulnEnv'] → Hash or nil + ↓ +if nil: print_error("Module has no VulnEnv configuration") + ↓ +if present: + definition = vuln_env['definition'] # 'jenkins' + yaml_path = File.join(Msf::Config.data_directory, 'vuln_envs', "#{definition}.yml") + definition_data = YAML.load_file(yaml_path) + version = vuln_env['default_version'] # '2.361' + env_config = definition_data['versions'][version] + shared_config = definition_data['shared'] +``` + +## Error Cases + +| Condition | Error Message | +|-----------|--------------| +| No active module | "No active module. Use 'use ' first." | +| Module has no VulnEnv | "Module does not define a vulnerable environment configuration." | +| Definition file not found | "Environment definition not found: data/vuln_envs/{name}.yml" | +| Version not found in definition | "Version '{version}' not defined for '{name}'" | diff --git a/docs/test_env/architecture/03-database-schema.md b/docs/test_env/architecture/03-database-schema.md new file mode 100644 index 0000000000000..af3a02693260f --- /dev/null +++ b/docs/test_env/architecture/03-database-schema.md @@ -0,0 +1,287 @@ +# Database Schema & Persistence + +## What I Learned From Metasploit Source + +I investigated the database architecture and found: + +### Migration System +- `db/migrate/` exists but is **empty** in the framework repo +- Migrations are gathered from **Rails engines** via `gather_engine_migration_paths` +- `lib/msf/core/db_manager/migration.rb` uses `ActiveRecord::MigrationContext` +- `schema.rb` is auto-generated, not edited directly + +### Key Code From `lib/msf/core/db_manager/migration.rb` + +```ruby +def gather_engine_migration_paths + paths = ActiveRecord::Migrator.migrations_paths + ::Rails::Engine.subclasses.map(&:instance).each do |engine| + migrations_paths = engine.paths['db/migrate'].existent_directories + migrations_paths.each do |migrations_path| + unless paths.include? migrations_path + paths << migrations_path + end + end + end + paths +end +``` + +### Database Configuration +- `config/database.yml` does not exist in the framework +- Database config is passed via `DatabaseYAML` option +- `framework.db.active` checks if database is connected + +## Phase 1: Plugin-Only (Weeks 1-6) — In-Memory Registry + +**Decision:** For the initial plugin implementation, use **in-memory storage only**. +No database migrations, no schema changes. + +### Why In-Memory First? +1. No framework modifications required +2. Plugin loads/unloads cleanly +3. Container labels provide cross-session identification +4. Database integration is Phase 2 (Week 6) + +### In-Memory Registry Design + +```ruby +class BuiltEnvironmentRegistry + attr_reader :environments, :framework + + def initialize(framework) + @framework = framework + @environments = {} # local_id => Hash + @next_id = 1 + end + + def register(container_id:, module_fullname:, rhost:, rport:, + version: nil, runtime: 'docker', image_ref:, + exploit_command:, datastore: {}) + id = @next_id + @next_id += 1 + + @environments[id] = { + local_id: id, + container_id: container_id, + module_fullname: module_fullname, + env_version: version, + rhost: rhost, + rport: rport, + runtime: runtime, + image_ref: image_ref, + status: 'running', + exploit_command: exploit_command, + datastore: datastore, + created_at: Time.now, + started_at: Time.now + } + + id + end + + def get(id) + @environments[id] + end + + def list + @environments.values.sort_by { |e| e[:local_id] } + end + + def update_status(id, status) + return unless @environments[id] + @environments[id][:status] = status + @environments[id][:updated_at] = Time.now + @environments[id][:stopped_at] = Time.now if status == 'stopped' + @environments[id][:started_at] = Time.now if status == 'running' + end + + def remove(id) + return unless @environments[id] + @environments[id][:status] = 'removed' + @environments[id][:removed_at] = Time.now + @environments.delete(id) + end + + def remove_all + @environments.each_value do |env| + env[:status] = 'removed' + env[:removed_at] = Time.now + end + @environments.clear + @next_id = 1 + end + + def find_by_container(container_id) + @environments.values.find { |e| e[:container_id] == container_id } + end + + def find_by_module(module_fullname) + @environments.values.select { |e| e[:module_fullname] == module_fullname } + end + + def used_ports + @environments.values.map { |e| e[:rport] } + end + + def running? + @environments.values.any? { |e| e[:status] == 'running' } + end +end +``` + +### Container Labels (Cross-Session Identification) + +Since in-memory data is lost on msfconsole restart, use **OCI container labels** +to identify and reconstruct environments: + +```bash +docker run -d \ + --label "msf.vulnenv.instance_id=msf-$(hostname)-$$" \ + --label "msf.vulnenv.module=exploit/multi/http/jenkins_script_console" \ + --label "msf.vulnenv.version=2.361" \ + --label "msf.vulnenv.env_id=1" \ + --label "msf.vulnenv.created_at=2024-06-25T17:37:00Z" \ + vulnhub/jenkins:2.361 +``` + +**Label Schema:** +| Label | Value | Purpose | +|-------|-------|---------| +| `msf.vulnenv.instance_id` | `msf-{hostname}-{pid}` | Identify msfconsole instance | +| `msf.vulnenv.module` | Module fullname | Link to exploit module | +| `msf.vulnenv.version` | Environment version | Track which version | +| `msf.vulnenv.env_id` | Internal registry ID | Cross-reference | +| `msf.vulnenv.created_at` | ISO8601 timestamp | Audit trail | +| `msf.vulnenv.managed_by` | `test_env` | Identify framework-managed | + +### State Reconstruction From Labels (Future Enhancement) + +```ruby +def reconstruct_from_labels(runtime) + containers = runtime.list(filters: { 'label' => 'msf.vulnenv.managed_by=test_env' }) + containers.each do |container| + labels = container['Labels'] + # Rebuild registry entry from labels + # (Week 6 enhancement) + end +end +``` + +## Phase 2: Database Integration (Week 6+) + +When adding PostgreSQL persistence: + +### Migration File +```ruby +# db/migrate/20240624000001_create_vuln_environments.rb +class CreateVulnEnvironments < ActiveRecord::Migration[8.0] + def change + create_table :vuln_environments, id: :serial do |t| + t.string :container_id, null: false + t.string :image_ref, null: false + t.string :module_fullname, null: false + t.string :env_version + t.string :rhost, default: '127.0.0.1' + t.integer :rport, null: false + t.text :datastore + t.string :runtime, default: 'docker', null: false + t.string :msf_instance_id + t.string :status, null: false, default: 'running' + t.text :exploit_command + t.timestamps + t.datetime :started_at + t.datetime :stopped_at + t.datetime :removed_at + end + + add_index :vuln_environments, :module_fullname + add_index :vuln_environments, :status + add_index :vuln_environments, :container_id, unique: true + add_index :vuln_environments, :msf_instance_id + add_index :vuln_environments, [:status, :module_fullname] + end +end +``` + +### ActiveRecord Model +```ruby +class VulnEnvironment < ActiveRecord::Base + self.table_name = 'vuln_environments' + serialize :datastore, JSON + + scope :active, -> { where(status: ['running', 'stopped']) } + scope :running, -> { where(status: 'running') } + scope :by_module, ->(name) { where(module_fullname: name) } + + validates :container_id, presence: true, uniqueness: true + validates :module_fullname, presence: true + validates :rport, presence: true, numericality: { only_integer: true } + validates :status, inclusion: { in: %w[running stopped removed orphaned error] } +end +``` + +### Integration With In-Memory Registry + +```ruby +class BuiltEnvironmentRegistry + def initialize(framework) + @framework = framework + @environments = {} + @next_id = 1 + load_from_database if database_available? + end + + private + + def database_available? + framework.db.active && defined?(VulnEnvironment) + end + + def load_from_database + VulnEnvironment.active.each do |db_env| + @environments[@next_id] = { + local_id: @next_id, + db_id: db_env.id, + container_id: db_env.container_id, + # ... map all fields ... + } + @next_id += 1 + end + end + + def persist_to_database(record) + VulnEnvironment.create!(...) + end +end +``` + +## Reference: sessions Table Pattern + +From `db/schema.rb`: +```ruby +create_table "sessions", id: :serial, force: :cascade do |t| + t.integer "host_id" + t.string "stype" + t.string "via_exploit" # Module association + t.string "via_payload" + t.string "desc" + t.integer "port" + t.string "platform" + t.text "datastore" # Serialized hash + t.datetime "opened_at", precision: nil, null: false + t.datetime "closed_at", precision: nil + t.string "close_reason" + t.integer "local_id" # In-memory mapping + t.datetime "last_seen", precision: nil + t.integer "module_run_id" + t.index ["module_run_id"], name: "index_sessions_on_module_run_id" +end +``` + +My `vuln_environments` table follows this exact pattern: +- `id: :serial` primary key +- `module_fullname` like `via_exploit` +- `datastore` serialized text +- `local_id` equivalent via `env_id` label +- Lifecycle timestamps (`created_at`, `started_at`, `stopped_at`, `removed_at`) diff --git a/docs/test_env/architecture/04-environment-schema.md b/docs/test_env/architecture/04-environment-schema.md new file mode 100644 index 0000000000000..87822fa797053 --- /dev/null +++ b/docs/test_env/architecture/04-environment-schema.md @@ -0,0 +1,233 @@ +# Environment Definition YAML Schema + +## What I Verified + +I created `data/vuln_envs/jenkins.yml` and validated it with Ruby: + +```bash +ruby -e " +require 'yaml' +data = YAML.safe_load(File.read('data/vuln_envs/jenkins.yml'), permitted_classes: [Symbol]) +puts 'Name: ' + data['name'] +puts 'Versions: ' + data['versions'].keys.inspect +puts 'Ports: ' + data['shared']['ports'].inspect +puts 'Health check type: ' + data['shared']['health_check']['type'] +" +``` + +Output: +``` +Name: jenkins +Versions: ["2.361", "2.375"] +Ports: {"http"=>8080} +Health check type: http +``` + +## Directory Structure + +``` +data/ + vuln_envs/ + README.md # Schema documentation + jenkins.yml # Jenkins environments (reference implementation) +``` + +## File Location +`data/vuln_envs/{name}.yml` + +The `{name}` must match the `name` field inside the file. + +## Schema + +### Top-Level Keys + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `name` | String | Yes | Machine-friendly identifier (matches filename) | +| `description` | String | Yes | Human-readable description | +| `versions` | Hash | Yes | Map of version strings to configurations | +| `shared` | Hash | Yes | Configuration shared across all versions | + +### versions Section + +Each version is a key-value pair: +- **Key**: Version string (e.g., `"2.361"`) +- **Value**: Hash with version-specific configuration + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `image` | String | Yes | OCI image reference | +| `build_args` | Hash | No | Docker build arguments | + +Example: +```yaml +versions: + "2.361": + image: vulnhub/jenkins:2.361 + build_args: + JENKINS_VERSION: "2.361" +``` + +### shared Section + +#### ports (Required) +```yaml +shared: + ports: + http: 8080 +``` + +#### health_check (Required) +```yaml +shared: + health_check: + type: http + path: /login + expected_status: 200 + interval: 5 + timeout: 2 + retries: 12 +``` + +| Key | Type | Required | Description | +|-----|------|----------|-------------| +| `type` | String | Yes | `http`, `tcp`, or `command` | +| `path` | String | If type=http | HTTP path to check | +| `expected_status` | Integer | No | Default: 200 | +| `command` | String | If type=command | Command to execute | +| `expected_output` | String | If type=command | Substring to match | +| `interval` | Integer | No | Seconds between checks. Default: 5 | +| `timeout` | Integer | No | Seconds to wait. Default: 2 | +| `retries` | Integer | No | Max attempts. Default: 12 | + +#### credentials (Optional) +```yaml +shared: + credentials: + default: + username: admin + password: admin +``` + +#### datastore_defaults (Optional) +```yaml +shared: + datastore_defaults: + TARGETURI: /script +``` + +#### ci (Optional) +```yaml +shared: + ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 +``` + +## Validation Rules + +1. `name` must match filename (without `.yml`) +2. `versions` must have at least one entry +3. Each version must have an `image` +4. `shared.ports` must have at least one entry +5. `shared.health_check` must have valid `type` +6. If `type` is `http`, `path` is required +7. If `type` is `command`, `command` and `expected_output` are required + +## Loader Implementation (Week 3) + +```ruby +class EnvironmentDefinitionLoader + DEFINITIONS_PATH = File.join(Msf::Config.data_directory, 'vuln_envs') + + def self.load(name) + require 'yaml' + path = File.join(DEFINITIONS_PATH, "#{name}.yml") + raise "Definition not found: #{path}" unless File.exist?(path) + + begin + YAML.safe_load(File.read(path), permitted_classes: [Symbol]) + rescue Psych::SyntaxError => e + raise "Invalid YAML in #{path}: #{e.message}" + end + end + + def self.available_definitions + Dir.glob(File.join(DEFINITIONS_PATH, '*.yml')).map do |f| + File.basename(f, '.yml') + end.sort + end +end +``` + +## Integration With Registry + +Environment definitions are loaded by the plugin and used to: +1. Build/pull container images +2. Map container ports to host ports +3. Configure health checks +4. Set module datastore defaults + +See [03-database-schema.md](https://github.com/Nayeraneru/metasploit-framework/blob/vulnenv-week1/docs/architecture/03-database-schema.md) for registry design. + +## Reference: jenkins.yml + +```yaml +name: jenkins +description: Jenkins CI server with Groovy Script Console enabled + +versions: + "2.361": + image: vulnhub/jenkins:2.361 + build_args: + JENKINS_VERSION: "2.361" + "2.375": + image: vulnhub/jenkins:2.375 + build_args: + JENKINS_VERSION: "2.375" + +shared: + ports: + http: 8080 + + volumes: + jenkins_home: + container_path: /var/jenkins_home + persist: false + + health_check: + type: http + path: /login + expected_status: 200 + interval: 5 + timeout: 2 + retries: 12 + + credentials: + default: + username: admin + password: admin + + datastore_defaults: + TARGETURI: /script + + ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 +``` diff --git a/docs/test_env/architecture/05-runtime-adapter.md b/docs/test_env/architecture/05-runtime-adapter.md new file mode 100644 index 0000000000000..e3d81ca6e0879 --- /dev/null +++ b/docs/test_env/architecture/05-runtime-adapter.md @@ -0,0 +1,209 @@ +# Runtime Adapter & Port Allocation + +## What I Verified on My Machine + +### Docker and Podman Availability + +**Both runtimes are installed.** Docker will be primary, Podman fallback. + +### Docker Inspect Test + +I ran a test container (port binding failed due to conflict, but inspect worked): + +```bash +$ docker run -d --name test-nginx --label msf.test=1 -p 127.0.0.1:8080:80 nginx +# Port 8080 was in use — container created but not started +# This proves: FIXED PORTS FAIL, dynamic allocation is required + +$ docker inspect test-nginx > /tmp/docker-inspect.json +``` + +Key fields from `docker inspect` output: + +```json +{ + "Id": "59721d248c4ff4be689ae3fedf8dc947f53d06c657ac0b1418205621a1ee6f44", + "State": { + "Status": "created", + "Running": false + }, + "Config": { + "Labels": { + "msf.test": "1" + } + }, + "HostConfig": { + "PortBindings": { + "80/tcp": [ + { + "HostIp": "127.0.0.1", + "HostPort": "8080" + } + ] + } + } +} +``` + +**Important findings:** +- Labels are stored in `Config.Labels`, not top-level +- `State.Status` shows "created" when container fails to start +- `HostConfig.PortBindings` shows the requested mapping +- **Port 8080 was already in use — fixed ports are unreliable** + +### Port Allocation Test + +I wrote and ran `test_port.rb`: + +```ruby +#!/usr/bin/env ruby + +require 'socket' + +def port_available?(port) + server = TCPServer.new('127.0.0.1', port) + server.close + true +rescue Errno::EADDRINUSE + false +end + +def find_port(preferred = nil, used = []) + if preferred + return preferred if port_available?(preferred) && !used.include?(preferred) + puts "Port #{preferred} unavailable, finding alternative..." + end + + (49152..65535).each do |p| + next if used.include?(p) + return p if port_available?(p) + end + + raise "No available ports" +end + +# Test +puts "Port 8080 available? #{port_available?(8080)}" +puts "Found port: #{find_port}" +puts "Found port (prefer 9999): #{find_port(9999)}" +``` + +Output: +``` +Port 8080 available? true +Found port: 49152 +Found port (prefer 9999): 9999 +``` + +**Port allocation works.** `TCPServer.new('127.0.0.1', port)` correctly tests availability. + +## Critical Design Decision: Dynamic Port Allocation + +From the Docker test failure: +- **Never assume a port is available** +- **Always test before binding** +- **Fallback to ephemeral range (49152-65535)** +- **Inform user when fallback occurs** + +## Runtime Adapter Design + +### Interface + +```ruby +module RuntimeAdapter + def self.detect + return DockerRuntime.new if DockerRuntime.available? + return PodmanRuntime.new if PodmanRuntime.available? + nil + end +end + +class BaseRuntime + def available?; raise NotImplementedError; end + def name; raise NotImplementedError; end + + def pull(image); raise NotImplementedError; end + def run(image:, ports:, labels:, volumes: [], env: {}, name: nil) + raise NotImplementedError + end + def stop(container_id); raise NotImplementedError; end + def start(container_id); raise NotImplementedError; end + def remove(container_id); raise NotImplementedError; end + def inspect(container_id); raise NotImplementedError; end + def exec(container_id, command); raise NotImplementedError; end + def list(filters: {}); raise NotImplementedError; end +end +``` + +## Container Label Schema + +All containers created by test_env receive these labels: + +| Label | Value | Purpose | +|-------|-------|---------| +| `msf.vulnenv.instance_id` | `msf-{hostname}-{pid}` | Isolate msfconsole instances | +| `msf.vulnenv.module` | Module fullname | Link to exploit module | +| `msf.vulnenv.version` | Environment version | Track which version | +| `msf.vulnenv.env_id` | Internal registry ID | Cross-reference | +| `msf.vulnenv.created_at` | ISO8601 timestamp | Audit trail | +| `msf.vulnenv.managed_by` | `test_env` | Identify framework-managed | + +## Docker vs Podman Differences + +| Feature | Docker | Podman | Impact on Design | +|---------|--------|--------|----------------| +| Daemon | Required (`dockerd`) | Daemonless | Podman simpler for rootless | +| Rootless | Complex setup | Default | Podman more secure | +| CLI syntax | `docker ...` | `podman ...` | Simple binary substitution | +| Networking | Bridge by default | `slirp4netns` for rootless | Slight performance difference | +| Image storage | Central (`/var/lib/docker`) | Per-user (`~/.local/share/containers`) | Not shared between users | + +## Auto-Detection Strategy + +## How test_env build Handles Port Conflicts + +### Problem + +From my test: +```bash +docker run -d -p 127.0.0.1:8080:80 nginx +# Error: ports are not available: exposing port TCP 127.0.0.1:8080 +``` + +Port 8080 was already in use. Fixed ports fail. + +### Solution: Dynamic Port Allocation with Automatic Fallback + +The `PortAllocator` class above handles this by: +1. Testing if preferred port is available via `TCPServer.new` +2. If not, scanning ephemeral range for available port +3. Tracking used ports to avoid duplicates + + +When `test_env build` runs, it: +- Reads the vulnerability environment's `port_mapping` (e.g., `{8080 => 'RPORT'}`) +- Checks if the user passed an override like `RPORT=8081` +- Allocates each required port, falling back to the ephemeral range if the preferred port is taken +- Starts the container with the allocated mappings +- Reports actual ports to the user +- Auto-sets the corresponding module datastore options +- +### Key Design Principles + +| Principle | Implementation | +|-----------|---------------| +| Never assume a port is available | `TCPServer.new` test before binding | +| Always provide fallback | Ephemeral range scan | +| Respect user preference | Try requested port first | +| Inform user of changes | Print status when fallback occurs | +| Auto-configure module | Set datastore options automatically | + +## Error Handling + +| Error Condition | Message | +|-----------------|---------| +| No runtime available | "No container runtime found. Install Docker or Podman." | +| Image pull failed | "Failed to pull image: {image}" | +| Container start failed (port conflict) | "Failed to start container: {error}. Try without RPORT override." | +| No available ports | "No available ports in range 49152-65535" | +| Container not found | "Container {id} not found" | diff --git a/docs/test_env/ci_workflow.md b/docs/test_env/ci_workflow.md new file mode 100644 index 0000000000000..f4c9a90a137af --- /dev/null +++ b/docs/test_env/ci_workflow.md @@ -0,0 +1,194 @@ +# CI Workflow: Automated Exploit Verification + + + +This document defines how `test_env` will be used in GitHub Actions to automatically: +- Provision vulnerable environments from shared definitions +- Execute exploits with pre-configured datastore options +- Validate expected outcomes (session creation, command output) +- Clean up all containers to prevent resource leaks + +**Key principle:** CI consumes the same environment definitions used for local testing. No duplicated container configuration. + + +## Directory Structure + +This directory structure will created as part of the project: + +``` +metasploit-framework/ +├── ci/ +│ ├── test_activemq.rc +│ ├── test_jenkins.rc +│ └── test_drupal.rc +├── .github/ +│ └── workflows/ +│ └── vuln-env-test.yml +├── data/ +│ └── vuln_envs/ +│ ├── activemq.yml +│ ├── jenkins.yml +│ └── drupal.yml +└── docs/ + └── ci_workflow.md +``` + + + +A **resource script** with a `.rc` extension that contains msfconsole commands. Instead of typing commands one by one into msfconsole, they will be saved in a file and run: + +```bash +./msfconsole -q -r path/to/script.rc +``` + +Metasploit reads the file and executes each line automatically, as if it's typed. + + + +### Example: ci/test_jenkins.rc + +**What it is:** A text file containing msfconsole commands to test the Jenkins module automatically. + +**What it contains:** +```text +load test_env +use exploit/multi/http/jenkins_script_console +test_env build VERSION=2.361 +test_env exec 1 +test_env remove-all +exit +``` + +**What each line does:** +| Line | Command | Purpose | +|------|---------|---------| +| 1 | `load test_env` | Load the test_env plugin | +| 2 | `use exploit/multi/http/jenkins_script_console` | Select the exploit module | +| 3 | `test_env build VERSION=2.361` | Build environment using Jenkins version 2.361 | +| 4 | `test_env exec 1` | Execute exploit against environment ID 1 | +| 5 | `test_env remove-all` | Stop and remove all containers | +| 6 | `exit` | Close msfconsole | + +**How to run it manually (for testing):** +```bash +./msfconsole -q -r ci/test_jenkins.rc +``` +--- + +## GitHub Actions Workflow + +**What this is:** A YAML file that tells GitHub Actions what to do on every push or pull request. + +**File:** `.github/workflows/vuln-env-test.yml` + +**What it contains:** +```yaml +name: Vulnerable Environment Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test-jenkins: + name: Test Jenkins Script Console + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: '3.2' + bundler-cache: true + + - name: Set up Docker + uses: docker/setup-buildx-action@v3 + + - name: Cache Docker layers + uses: actions/cache@v3 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Run Jenkins exploit test + run: | + ./msfconsole -q -r ci/test_jenkins.rc + + - name: Verify session was created + run: | + grep "Session.*opened" ~/.msf4/logs/framework.log || echo "WARNING: No session log found" + + - name: Verify no containers left behind + run: | + REMAINING=$(docker ps -q | wc -l) + if [ "$REMAINING" -eq 0 ]; then + echo "Clean: No containers remaining" + else + echo "FAIL: $REMAINING container(s) still running" + docker ps + exit 1 + fi +``` + +**What each step does:** +| Step | Action | Purpose | +|------|--------|---------| +| Checkout | `actions/checkout@v4` | Download your code | +| Set up Ruby | `ruby/setup-ruby@v1` | Install Ruby 3.2 and gems | +| Set up Docker | `docker/setup-buildx-action@v3` | Install Docker | +| Cache Docker layers | `actions/cache@v3` | Speed up image pulls | +| Run exploit test | `./msfconsole -q -r ci/test_jenkins.rc` | Execute the Jenkins resource script | +| Verify session | `grep "Session.*opened"` | Confirm exploit succeeded | +| Verify cleanup | `docker ps -q` | Confirm no leaked containers | + +--- + +## Validation Criteria + +| Step | Expected Result | How It Is Checked | On Failure | +|------|----------------|-------------------|------------| +| `test_env build` | Container starts, health check passes | Console output contains "Environment ready" | Workflow fails | +| `test_env exec 1` | Session opens | `framework.log` contains "Session.*opened" | Workflow fails | +| `test_env remove-all` | All containers removed | `docker ps -q` returns empty | Workflow fails | +| Post-cleanup | Zero `msf.vulnenv` containers remain | `docker ps -a --filter "label=msf.vulnenv.managed_by=test_env"` returns empty | Workflow fails | + +--- + +## CI Metadata in Environment Definitions + +Environment definitions include a `ci` section so the automation knows what payload to use and what to validate: + +```yaml +# data/vuln_envs/jenkins.yml +ci: + exploit: + payload: java/meterpreter/reverse_tcp + options: + LHOST: 127.0.0.1 + LPORT: 4444 + TARGETURI: /script + validation: + expected_session: true + session_type: meterpreter + expected_output: "uid=" + timeout: 120 +``` + +### Schema Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `ci.exploit.payload` | String | Yes | Payload to use for automated execution | +| `ci.exploit.options` | Hash | No | Datastore options: `LHOST`, `LPORT`, `TARGETURI`, etc. | +| `ci.validation.expected_session` | Boolean | Yes | Whether a session must be created | +| `ci.validation.session_type` | String | No | Expected session type: `meterpreter`, `shell` | +| `ci.validation.expected_output` | String | No | Substring to verify in session output | +| `ci.validation.timeout` | Integer | Yes | Max seconds to wait for validation | + diff --git a/docs/test_env/reference_modules.md b/docs/test_env/reference_modules.md new file mode 100644 index 0000000000000..eb8d7e5e96516 --- /dev/null +++ b/docs/test_env/reference_modules.md @@ -0,0 +1,48 @@ +# Reference Modules for test_env + +## Selection Criteria +- Cover different service types: Java message broker (ActiveMQ), CI server (Jenkins), CMS (Drupal) +- Have clear, single-port (or well-defined multi-port) mappings +- Have existing Docker images with known vulnerable versions +- Demonstrate different health check patterns (API endpoint, login page, root page) +- Include both authenticated and unauthenticated exploit scenarios + +--- + +## Module 1: Apache ActiveMQ Jolokia RCE (Mentor Suggested) +- **Path:** `exploit/multi/http/apache_activemq_jolokia_rce` +- **Type:** Java web application (JMX-over-HTTP) +- **Ports:** 8161 (web console / Jolokia API), 61616 (OpenWire broker) +- **Health Check:** HTTP GET `/api/jolokia/` expecting 200, or GET `/` expecting 200 +- **Why:** h00die suggested PR #21497. Has a verified Docker one-liner. Real-world CVE-2026-34197. +- **VulnEnv Definition:** `activemq` +- **Docker Image:** `apache/activemq-classic:5.18.6` +- **Docker Run:** `docker run -d --name activemq -p 8161:8161 -p 61616:61616 apache/activemq-classic:5.18.6` +- **Credentials:** admin / admin +- **Exploit Context:** Requires authenticated Jolokia access; `TARGETURI` typically `/api/jolokia/` + +--- + +## Module 2: Jenkins Script Console +- **Path:** `exploit/multi/http/jenkins_script_console` +- **Type:** Web application / CI server +- **Port:** 8080 +- **Health Check:** HTTP GET `/login` expecting 200 +- **Why:** Well-documented, multiple versions exist, clear RPORT→8080 mapping, widely used in exploit development tutorials +- **VulnEnv Definition:** `jenkins` +- **Docker Image:** `vulnhub/jenkins:2.361` +- **Credentials:** admin / admin +- **Exploit Context:** Script Console at `/script` allows Groovy execution; `TARGETURI` typically `/script` + +--- + +## Module 3: Drupal Drupalgeddon2 +- **Path:** `exploit/unix/webapp/drupal_drupalgeddon2` +- **Type:** Web application / CMS +- **Port:** 80 +- **Health Check:** HTTP GET `/` expecting 200 +- **Why:** Simple single-port setup, unauthenticated exploit, different architecture from ActiveMQ/Jenkins, large community interest +- **VulnEnv Definition:** `drupal` +- **Docker Image:** `vulnhub/drupal:CVE-2018-7600` +- **Credentials:** None required (unauthenticated) +- **Exploit Context:** SA-CORE-2018-002 (CVE-2018-7600); remote code execution via form API diff --git a/docs/test_env/workflow.md b/docs/test_env/workflow.md new file mode 100644 index 0000000000000..fd04ab6163fff --- /dev/null +++ b/docs/test_env/workflow.md @@ -0,0 +1,280 @@ +# test_env User Workflow (Design Specification) + +This document specifies the intended user interaction with `test_env`. Every transcript below is the **target behavior** that implementation must achieve. These are our acceptance criteria. + +--- + +## Scenario 1: Build an Environment for the Active Module + +**Precondition:** The user has selected a module that defines a `VulnEnv` key in its metadata. + +**Input:** +``` +msf > use exploit/multi/http/apache_activemq_jolokia_rce +msf exploit(apache_activemq_jolokia_rce) > test_env build +``` + +**Expected behavior:** +- The plugin detects the active module and reads `mod.info['VulnEnv']` +- It resolves the definition name (`activemq`) and loads `data/vuln_envs/activemq.yml` +- It selects the default version (`5.18.6`) +- It auto-detects the container runtime (Docker preferred, Podman fallback) +- It pulls `apache/activemq-classic:5.18.6` if not already cached +- It starts the container with localhost binding and dynamic port allocation +- It waits for the health check defined in the YAML (`GET /api/jolokia/` → 200) +- Once ready, it prints the mapped host port and suggests datastore settings + +**Expected output:** +``` +[*] Resolving environment for apache_activemq_jolokia_rce... +[*] Definition: activemq | Version: 5.18.6 | Runtime: docker +[*] Pulling image apache/activemq-classic:5.18.6... +[*] Starting container... +[*] Waiting for health check (GET /api/jolokia/)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49152 +[*] TARGETURI => /api/jolokia/ +[*] USERNAME => admin +[*] PASSWORD => admin +[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49152; exploit +``` + +**Postcondition:** A container is running. The module's datastore options (`RHOSTS`, `RPORT`, etc.) are automatically populated. The environment is registered in the in-memory registry with a unique ID. + +--- + +## Scenario 2: Build with a Specific Version + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env build VERSION=2.375 +``` + +**Expected behavior:** +- The `VERSION=2.375` argument overrides the `default_version` from the module's `VulnEnv` +- The plugin loads `jenkins.yml` and selects the `2.375` entry under `versions` +- If the version does not exist, the command fails immediately with a list of available versions + +**Expected output (success):** +``` +[*] Selected version: 2.375 +[*] Pulling image vulnhub/jenkins:2.375... +[*] Starting container... +[*] Waiting for health check (GET /login)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49153 +[*] TARGETURI => /script +[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49153; exploit +``` + +**Expected output (failure — version not found):** +``` +[-] Version '9.99' not defined for 'jenkins'. Available: 2.361, 2.375 +``` + +--- + +## Scenario 3: Request a Specific Port (with Automatic Fallback) + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env build RPORT=8080 +``` + +**Expected behavior:** +- The plugin attempts to bind host port 8080 to the container's exposed port +- If 8080 is already in use on the host, the `PortAllocator` scans the ephemeral range (49152–65535) for an available port +- The user is informed of the fallback. The allocated port is stored in the registry. + +**Expected output:** +``` +[*] Requested port 8080 is unavailable. Using dynamically allocated port 49154. +[*] Starting container... +[*] Waiting for health check (GET /login)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49154 +[*] TARGETURI => /script +``` + +--- + +## Scenario 4: List Active Environments + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env list +``` + +**Expected behavior:** +- The plugin queries the in-memory registry and prints a table using `Rex::Ui::Text::Table` +- Each row shows: ID, container ID (truncated), module fullname, RHOST, RPORT, status, version +- The table is sorted by ID + +**Expected output:** +``` +Environments +============ + +ID Container Module RHOST RPORT Status Version +-- --------- ------ ----- ----- ------ ------- +1 4f3a2b1c... exploit/multi/http/jenkins_script_console 127.0.0.1 49153 running 2.375 +2 9e8d7c6b... exploit/multi/http/apache_activemq_jolokia 127.0.0.1 49152 running 5.18.6 +``` + +--- + +## Scenario 5: Execute the Stored Exploit + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env exec 1 +``` + +**Expected behavior:** +- The plugin looks up environment ID 1 in the registry +- It verifies the container is running via `docker inspect` or `podman inspect` +- It automatically sets the module's datastore options (`RHOSTS`, `RPORT`, `TARGETURI`, etc.) from the registry record +- It invokes the module's `exploit` method (or `run_simple`) with these options +- If the environment is stopped, it prints an error telling the user to start it first + +**Expected output (success):** +``` +[*] Executing exploit against environment 1... +[*] Set RHOSTS 127.0.0.1 +[*] Set RPORT 49153 +[*] Set TARGETURI /script +[*] Started reverse TCP handler on 127.0.0.1:4444 +[+] Session 1 opened (127.0.0.1:4444 -> 127.0.0.1:49153) +``` + +**Expected output (failure — environment stopped):** +``` +[-] Environment 1 is not running. Start it with: test_env start 1 +``` + +--- + +## Scenario 6: Stop and Restart an Environment + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env stop 1 +``` + +**Expected behavior:** +- The plugin calls `docker stop` (or `podman stop`) on the container ID stored in the registry +- It updates the registry status to `stopped` +- It preserves the registry record so the environment can be restarted + +**Expected output:** +``` +[*] Stopping container 4f3a2b1c... +[+] Environment 1 stopped. +``` + +**Input (restart):** +``` +msf exploit(multi/http/jenkins_script_console) > test_env start 1 +``` + +**Expected behavior:** +- The plugin calls `docker start` on the container +- It re-runs the health check defined in the environment definition +- It updates the registry status to `running` + +**Expected output:** +``` +[*] Starting container 4f3a2b1c... +[*] Waiting for health check... +[+] Environment 1 running. RPORT=49153 +``` + +--- + +## Scenario 7: Remove a Single Environment + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env remove 1 +``` + +**Expected behavior:** +- The plugin stops the container if it is running +- It calls `docker rm` (or `podman rm`) to remove the container +- It removes the record from the in-memory registry +- If the database is active (Phase 2), it updates the DB status to `removed` + +**Expected output:** +``` +[*] Stopping container 4f3a2b1c... +[*] Removing container 4f3a2b1c... +[+] Environment 1 removed. +``` + +--- + +## Scenario 8: Remove All Environments + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env remove-all +``` + +**Expected behavior:** +- The plugin iterates all entries in the registry +- For each, it stops and removes the container +- It clears the in-memory registry +- If any container fails to stop/remove, it prints a warning but continues + +**Expected output:** +``` +[*] Tearing down 2 environment(s)... +[*] Stopping container 4f3a2b1c... +[*] Removing container 4f3a2b1c... +[*] Stopping container 9e8d7c6b... +[*] Removing container 9e8d7c6b... +[+] All environments removed. +``` + +--- + +## Quick Reference: Command Summary + +| Command | Arguments | Description | +|---------|-----------|-------------| +| `test_env build` | `[VERSION=x]` `[RPORT=y]` | Build and launch environment for active module | +| `test_env list` | none | Show all tracked environments | +| `test_env exec` | `` | Execute exploit against environment `` | +| `test_env stop` | `` | Stop a running environment | +| `test_env start` | `` | Restart a stopped environment | +| `test_env remove` | `` | Tear down and remove one environment | +| `test_env remove-all` | none | Tear down all environments | +| `test_env help` | none | Show usage information | + +--- + +## Error Handling Specification + +| Error Condition | Expected Output | Implementation Notes | +|-----------------|----------------|---------------------| +| No active module | `[-] No active module. Use 'use ' first.` | Check `driver.active_module` before any other logic | +| Module has no `VulnEnv` | `[-] Module does not define a vulnerable environment configuration.` | Check `mod.info['VulnEnv']` after resolving active module | +| No container runtime | `[-] No container runtime found. Install Docker or Podman.` | `RuntimeAdapter.detect` returns `nil` | +| Image pull fails | `[-] Failed to pull image: ` | Check exit status of `docker pull` | +| Container start fails | `[-] Failed to start container: ` | Catch `RuntimeAdapter#run` exceptions | +| No available ports | `[-] No available ports in range 49152-65535` | `PortAllocator` raises after exhausting range | +| Health check timeout | `[-] Health check timed out after seconds` | `HealthManager` exceeds `retries * interval` | +| Environment ID not found | `[-] Environment not found` | Registry lookup returns `nil` | +| Environment not running | `[-] Environment is not running. Start it with: test_env start ` | Check `status` field before `exec` | + +--- + +## Notes for Implementers + +- All output prefixes (`[*]`, `[+]`, `[-]`) must use `print_status`, `print_good`, `print_error` respectively +- Table output must use `Rex::Ui::Text::Table` for consistency with built-in commands like `sessions`, `jobs` +- The `test_env` command must be available regardless of whether a database is connected (Phase 1 is in-memory only) +- Container labels must be applied on every `run` so that orphaned containers can be identified even if the registry is lost diff --git a/docs/workflow.md b/docs/workflow.md new file mode 100644 index 0000000000000..fd04ab6163fff --- /dev/null +++ b/docs/workflow.md @@ -0,0 +1,280 @@ +# test_env User Workflow (Design Specification) + +This document specifies the intended user interaction with `test_env`. Every transcript below is the **target behavior** that implementation must achieve. These are our acceptance criteria. + +--- + +## Scenario 1: Build an Environment for the Active Module + +**Precondition:** The user has selected a module that defines a `VulnEnv` key in its metadata. + +**Input:** +``` +msf > use exploit/multi/http/apache_activemq_jolokia_rce +msf exploit(apache_activemq_jolokia_rce) > test_env build +``` + +**Expected behavior:** +- The plugin detects the active module and reads `mod.info['VulnEnv']` +- It resolves the definition name (`activemq`) and loads `data/vuln_envs/activemq.yml` +- It selects the default version (`5.18.6`) +- It auto-detects the container runtime (Docker preferred, Podman fallback) +- It pulls `apache/activemq-classic:5.18.6` if not already cached +- It starts the container with localhost binding and dynamic port allocation +- It waits for the health check defined in the YAML (`GET /api/jolokia/` → 200) +- Once ready, it prints the mapped host port and suggests datastore settings + +**Expected output:** +``` +[*] Resolving environment for apache_activemq_jolokia_rce... +[*] Definition: activemq | Version: 5.18.6 | Runtime: docker +[*] Pulling image apache/activemq-classic:5.18.6... +[*] Starting container... +[*] Waiting for health check (GET /api/jolokia/)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49152 +[*] TARGETURI => /api/jolokia/ +[*] USERNAME => admin +[*] PASSWORD => admin +[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49152; exploit +``` + +**Postcondition:** A container is running. The module's datastore options (`RHOSTS`, `RPORT`, etc.) are automatically populated. The environment is registered in the in-memory registry with a unique ID. + +--- + +## Scenario 2: Build with a Specific Version + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env build VERSION=2.375 +``` + +**Expected behavior:** +- The `VERSION=2.375` argument overrides the `default_version` from the module's `VulnEnv` +- The plugin loads `jenkins.yml` and selects the `2.375` entry under `versions` +- If the version does not exist, the command fails immediately with a list of available versions + +**Expected output (success):** +``` +[*] Selected version: 2.375 +[*] Pulling image vulnhub/jenkins:2.375... +[*] Starting container... +[*] Waiting for health check (GET /login)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49153 +[*] TARGETURI => /script +[*] Suggested: set RHOSTS 127.0.0.1; set RPORT 49153; exploit +``` + +**Expected output (failure — version not found):** +``` +[-] Version '9.99' not defined for 'jenkins'. Available: 2.361, 2.375 +``` + +--- + +## Scenario 3: Request a Specific Port (with Automatic Fallback) + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env build RPORT=8080 +``` + +**Expected behavior:** +- The plugin attempts to bind host port 8080 to the container's exposed port +- If 8080 is already in use on the host, the `PortAllocator` scans the ephemeral range (49152–65535) for an available port +- The user is informed of the fallback. The allocated port is stored in the registry. + +**Expected output:** +``` +[*] Requested port 8080 is unavailable. Using dynamically allocated port 49154. +[*] Starting container... +[*] Waiting for health check (GET /login)... +[+] Environment ready. +[*] RHOSTS => 127.0.0.1 +[*] RPORT => 49154 +[*] TARGETURI => /script +``` + +--- + +## Scenario 4: List Active Environments + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env list +``` + +**Expected behavior:** +- The plugin queries the in-memory registry and prints a table using `Rex::Ui::Text::Table` +- Each row shows: ID, container ID (truncated), module fullname, RHOST, RPORT, status, version +- The table is sorted by ID + +**Expected output:** +``` +Environments +============ + +ID Container Module RHOST RPORT Status Version +-- --------- ------ ----- ----- ------ ------- +1 4f3a2b1c... exploit/multi/http/jenkins_script_console 127.0.0.1 49153 running 2.375 +2 9e8d7c6b... exploit/multi/http/apache_activemq_jolokia 127.0.0.1 49152 running 5.18.6 +``` + +--- + +## Scenario 5: Execute the Stored Exploit + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env exec 1 +``` + +**Expected behavior:** +- The plugin looks up environment ID 1 in the registry +- It verifies the container is running via `docker inspect` or `podman inspect` +- It automatically sets the module's datastore options (`RHOSTS`, `RPORT`, `TARGETURI`, etc.) from the registry record +- It invokes the module's `exploit` method (or `run_simple`) with these options +- If the environment is stopped, it prints an error telling the user to start it first + +**Expected output (success):** +``` +[*] Executing exploit against environment 1... +[*] Set RHOSTS 127.0.0.1 +[*] Set RPORT 49153 +[*] Set TARGETURI /script +[*] Started reverse TCP handler on 127.0.0.1:4444 +[+] Session 1 opened (127.0.0.1:4444 -> 127.0.0.1:49153) +``` + +**Expected output (failure — environment stopped):** +``` +[-] Environment 1 is not running. Start it with: test_env start 1 +``` + +--- + +## Scenario 6: Stop and Restart an Environment + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env stop 1 +``` + +**Expected behavior:** +- The plugin calls `docker stop` (or `podman stop`) on the container ID stored in the registry +- It updates the registry status to `stopped` +- It preserves the registry record so the environment can be restarted + +**Expected output:** +``` +[*] Stopping container 4f3a2b1c... +[+] Environment 1 stopped. +``` + +**Input (restart):** +``` +msf exploit(multi/http/jenkins_script_console) > test_env start 1 +``` + +**Expected behavior:** +- The plugin calls `docker start` on the container +- It re-runs the health check defined in the environment definition +- It updates the registry status to `running` + +**Expected output:** +``` +[*] Starting container 4f3a2b1c... +[*] Waiting for health check... +[+] Environment 1 running. RPORT=49153 +``` + +--- + +## Scenario 7: Remove a Single Environment + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env remove 1 +``` + +**Expected behavior:** +- The plugin stops the container if it is running +- It calls `docker rm` (or `podman rm`) to remove the container +- It removes the record from the in-memory registry +- If the database is active (Phase 2), it updates the DB status to `removed` + +**Expected output:** +``` +[*] Stopping container 4f3a2b1c... +[*] Removing container 4f3a2b1c... +[+] Environment 1 removed. +``` + +--- + +## Scenario 8: Remove All Environments + +**Input:** +``` +msf exploit(multi/http/jenkins_script_console) > test_env remove-all +``` + +**Expected behavior:** +- The plugin iterates all entries in the registry +- For each, it stops and removes the container +- It clears the in-memory registry +- If any container fails to stop/remove, it prints a warning but continues + +**Expected output:** +``` +[*] Tearing down 2 environment(s)... +[*] Stopping container 4f3a2b1c... +[*] Removing container 4f3a2b1c... +[*] Stopping container 9e8d7c6b... +[*] Removing container 9e8d7c6b... +[+] All environments removed. +``` + +--- + +## Quick Reference: Command Summary + +| Command | Arguments | Description | +|---------|-----------|-------------| +| `test_env build` | `[VERSION=x]` `[RPORT=y]` | Build and launch environment for active module | +| `test_env list` | none | Show all tracked environments | +| `test_env exec` | `` | Execute exploit against environment `` | +| `test_env stop` | `` | Stop a running environment | +| `test_env start` | `` | Restart a stopped environment | +| `test_env remove` | `` | Tear down and remove one environment | +| `test_env remove-all` | none | Tear down all environments | +| `test_env help` | none | Show usage information | + +--- + +## Error Handling Specification + +| Error Condition | Expected Output | Implementation Notes | +|-----------------|----------------|---------------------| +| No active module | `[-] No active module. Use 'use ' first.` | Check `driver.active_module` before any other logic | +| Module has no `VulnEnv` | `[-] Module does not define a vulnerable environment configuration.` | Check `mod.info['VulnEnv']` after resolving active module | +| No container runtime | `[-] No container runtime found. Install Docker or Podman.` | `RuntimeAdapter.detect` returns `nil` | +| Image pull fails | `[-] Failed to pull image: ` | Check exit status of `docker pull` | +| Container start fails | `[-] Failed to start container: ` | Catch `RuntimeAdapter#run` exceptions | +| No available ports | `[-] No available ports in range 49152-65535` | `PortAllocator` raises after exhausting range | +| Health check timeout | `[-] Health check timed out after seconds` | `HealthManager` exceeds `retries * interval` | +| Environment ID not found | `[-] Environment not found` | Registry lookup returns `nil` | +| Environment not running | `[-] Environment is not running. Start it with: test_env start ` | Check `status` field before `exec` | + +--- + +## Notes for Implementers + +- All output prefixes (`[*]`, `[+]`, `[-]`) must use `print_status`, `print_good`, `print_error` respectively +- Table output must use `Rex::Ui::Text::Table` for consistency with built-in commands like `sessions`, `jobs` +- The `test_env` command must be available regardless of whether a database is connected (Phase 1 is in-memory only) +- Container labels must be applied on every `run` so that orphaned containers can be identified even if the registry is lost diff --git a/plugins/test_env.rb b/plugins/test_env.rb new file mode 100644 index 0000000000000..79cb4162dbbbd --- /dev/null +++ b/plugins/test_env.rb @@ -0,0 +1,89 @@ +module Msf + class Plugin::VulnEnv < Msf::Plugin + + class ConsoleCommandDispatcher + include Msf::Ui::Console::CommandDispatcher + + def name + 'VulnEnv' + end + + def commands + { + 'test_env' => 'Manage vulnerable test environments' + } + end + + def cmd_test_env(*args) + if args.empty? || args.first == '-h' || args.first == '--help' + cmd_test_env_help + return + end + + subcommand = args.shift + + case subcommand + when 'build' + print_status("TODO: test_env build") + when 'list' + print_status("TODO: test_env list") + when 'stop' + print_status("TODO: test_env stop") + when 'start' + print_status("TODO: test_env start") + when 'remove' + print_status("TODO: test_env remove") + when 'remove-all' + print_status("TODO: test_env remove-all") + when 'exec' + print_status("TODO: test_env exec") + when 'help' + cmd_test_env_help + else + print_error("Unknown subcommand: #{subcommand}") + cmd_test_env_help + end + end + + def cmd_test_env_help + print_line("Usage: test_env ") + print_line + print_line("Commands:") + print_line(" build Build and launch environment for active module") + print_line(" list List tracked environments") + print_line(" stop Stop a running environment") + print_line(" start Restart a stopped environment") + print_line(" remove Tear down an environment") + print_line(" remove-all Tear down all environments") + print_line(" exec Execute exploit against environment") + print_line(" help Show this help") + print_line + end + + def cmd_test_env_tabs(str, words) + if words.length == 1 + return %w[build list stop start remove remove-all exec help] + end + [] + end + end + + def initialize(framework, opts) + super + add_console_dispatcher(ConsoleCommandDispatcher) + print_status("VulnEnv plugin loaded.") + end + + def cleanup + remove_console_dispatcher('VulnEnv') + end + + def name + 'vulnenv' + end + + def desc + 'Automated vulnerable environment provisioning' + end + end +end