Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion data/vuln_envs/activemq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ name: activemq
description: Apache ActiveMQ Classic with Jolokia API

variants:
#Used by exploit/multi/http/apache_activemq_jolokia_rce
- name: "5.18.6"
version: "5.18.6"
image: docker.io/apache/activemq-classic:5.18.6
default: true

#Used by exploit/multi/misc/apache_activemq_rce_cve_2023_46604
- name: "5.18.2"
version: "5.18.2"
image: docker.io/dinifarb/activemq:5.18.2

shared:
ports:
web: 8161
Expand Down Expand Up @@ -48,4 +54,8 @@ shared:

profiles:
default:
description: Standard Apache ActiveMQ with web console and broker
description: Standard ActiveMQ; web console reachable via HTTP/Jolokia.
broker-only:
description: Web console not assumed reachable; only the broker port is health-checked
health_check:
type: tcp
28 changes: 28 additions & 0 deletions data/vuln_envs/httpd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: httpd
description: Apache HTTP Server for scanner testing

variants:
- name: "2.4.57"
version: "2.4.57"
image: docker.io/library/httpd:2.4.57
default: true

shared:
ports:
http: 80

health_check:
type: http
path: /
expected_status: 200
interval: 2
timeout: 2
retries: 10

ci:
validation:
expected_session: false

profiles:
default:
description: Standard Apache HTTP Server
32 changes: 32 additions & 0 deletions data/vuln_envs/openssh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: openssh
description: OpenSSH server for SSH scanner testing

variants:
- name: "7.2"
version: "7.2p2"
image: docker.io/rastasheep/ubuntu-sshd:16.04
default: true

shared:
ports:
ssh: 22

credentials:
default:
username: root
password: root

# TCP check is enough: if port 22 accepts a connection, SSH is ready
health_check:
type: tcp
interval: 2
timeout: 2
retries: 10

ci:
validation:
expected_session: false

profiles:
default:
description: Standard OpenSSH server
20 changes: 18 additions & 2 deletions docs/test_env/reference_modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
## 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)
- **Ports:** 8161 (web console / Jolokia API)
- **Profile:** `default`
- **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.
- **VulnerableEnvironment Definition:** `activemq`
Expand All @@ -33,4 +34,19 @@
- **Docker Image:** `eystsen/vulnerablewordpress`
- **Credentials:** admin / admin
- **Exploit Context:** Authenticated admin access; uploads PHP shell via theme/plugin editor
- **Provisioning:** This image does **not** start ready-to-use. The Dockerfile configures `wp-config.php` to point at a `wordpress` database but never creates the schema or an admin account — WordPress boots straight into the install wizard (`/wp-admin/install.php`), and stays there indefinitely with no admin/admin login until the wizard is submitted. `wordpress.yml` now defines a `provision` step (`type: http_post`, submits `install.php?step=2` with the credentials from `credentials.default`) that runs once the base health check passes, followed by the `verify` check above before the environment is registered as ready. See `04-environment-schema.md` for the general `provision`/`verify` schema this relies on.
- **Provisioning:** This image does **not** start ready-to-use. The Dockerfile configures `wp-config.php` to point at a `wordpress` database but never creates the schema or an admin account — WordPress boots straight into the install wizard (`/wp-admin/install.php`), and stays there indefinitely with no admin/admin login until the wizard is submitted. `wordpress.yml` now defines a `provision` step (`type: http_post`, submits `install.php?step=2` with the credentials from `credentials.default`) that runs once the base health check passes, followed by the `verify` check above before the environment is registered as ready. See `04-environment-schema.md` for the general `provision`/`verify` schema this relies on.

---

## Module 3: Apache ActiveMQ OpenWire RCE (CVE-2023-46604)
- **Path:** `exploit/multi/misc/apache_activemq_rce_cve_2023_46604`
- **Type:** Java message broker (raw OpenWire protocol, not HTTP)
- **Port:** 61616 (broker)
- **Profile:** `broker-only`
- **Health Check:** Uses activemq.yml's broker-only profile (tcp on 61616). The default HTTP check fails against this image due to AMQ-8018 (web console binds 127.0.0.1 since 5.16.0). Since the module only needs OpenWire, a TCP profile is accurate and matches the schema guidance to create a new profile when the health check type differs.
- **Why this module specifically:** it's the first real proof that `activemq.yml` works as a genuine shared definition across independent modules, not just independent files that happen to use the same schema. Two unrelated CVEs, two different attack surfaces (HTTP/Jolokia vs. raw OpenWire), one definition file.
- **VulnerableEnvironment Definition:** `activemq` (same file as Module 1 — nothing duplicated: image family, credentials, health check, and `ci.exploit`'s recommended payload are all inherited unchanged)
- **Docker Image:** `dinifarb/activemq:5.18.2`
- **Credentials:** none required — this CVE is unauthenticated
- **Exploit Context:** Unauthenticated; sends crafted OpenWire packet that loads attacker-hosted Spring XML config. Requires TARGET => 1 (Linux) override — default target is Windows.

20 changes: 12 additions & 8 deletions modules/auxiliary/scanner/http/http_header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def initialize(info = {})
'Reliability' => UNKNOWN_RELIABILITY,
'Stability' => UNKNOWN_STABILITY,
'SideEffects' => UNKNOWN_SIDE_EFFECTS
},
'VulnerableEnvironment' => {
'definition' => 'httpd',
'default_variant' => '2.4.57',
'port_mapping' => { 80 => 'RPORT' }
}
)
)
Expand Down Expand Up @@ -63,9 +68,8 @@ def run_host(ip)
end

# Header Names are case insensitive so convert them to upcase
headers_uppercase = headers.inject({}) do |hash, keys|
headers_uppercase = headers.each_with_object({}) do |keys, hash|
hash[keys[0].upcase] = keys[1]
hash
end

ignored_headers.each do |h|
Expand All @@ -76,18 +80,18 @@ def run_host(ip)
end
headers_uppercase.to_a.compact.sort

counter = 0;
counter = 0
headers_uppercase.each do |h|
header_string = "#{h[0]}: #{h[1]}"
print_good "#{peer}: #{header_string}"

report_note(
:type => "http.header.#{rport}.#{counter}",
:data => { :header_string => header_string },
:host => ip,
:port => rport
type: "http.header.#{rport}.#{counter}",
data: { header_string: header_string },
host: ip,
port: rport
)
counter = counter + 1
counter += 1
end
if counter == 0
print_warning "#{peer}: all detected headers are defined in IGN_HEADER and were ignored "
Expand Down
7 changes: 6 additions & 1 deletion modules/auxiliary/scanner/http/http_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ def initialize
'Name' => 'HTTP Version Detection',
'Description' => 'Display version information about each system.',
'Author' => 'hdm',
'License' => MSF_LICENSE
'License' => MSF_LICENSE,
'VulnerableEnvironment' => {
'definition' => 'httpd',
'default_variant' => '2.4.57',
'port_mapping' => {80 => 'RPORT'}
}
)

register_wmap_options({
Expand Down
31 changes: 18 additions & 13 deletions modules/auxiliary/scanner/http/robots_txt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ def initialize
'Name' => 'HTTP Robots.txt Content Scanner',
'Description' => 'Detect robots.txt files and analyze its content',
'Author' => ['et'],
'License' => MSF_LICENSE
'License' => MSF_LICENSE,
'VulnerableEnvironment' => {
'definition' => 'httpd',
'default_variant' => '2.4.57',
'port_mapping' => { 80 => 'RPORT' }
}
)

register_options(
[
OptString.new('PATH', [ true, "The test path to find robots.txt file", '/']),
OptString.new('PATH', [ true, 'The test path to find robots.txt file', '/']),

]
)
Expand All @@ -40,15 +45,15 @@ def run_host(target_host)
res = send_request_raw({
'uri' => turl,
'method' => 'GET',
'version' => '1.0',
'version' => '1.0'
}, 10)

if not res
if !res
print_error("[#{target_host}] #{tpath}robots.txt - No response")
return
end

if not res.body.include?("llow:")
if !res.body.include?('llow:')
vprint_status("[#{target_host}] #{tpath}robots.txt - Doesn't contain \"llow:\"")
return
end
Expand All @@ -57,20 +62,20 @@ def run_host(target_host)
print_good("Contents of Robots.txt:\n#{res.body}")

# short url regex
aregex = /llow:[ ]{0,2}(.*?)$/i
aregex = /llow: {0,2}(.*?)$/i

result = res.body.scan(aregex).flatten.map { |s| s.strip }.uniq

vprint_status("[#{target_host}] #{tpath}robots.txt - #{result.join(', ')}")
result.each do |u|
report_note(
:host => target_host,
:port => rport,
:proto => 'tcp',
:sname => (ssl ? 'https' : 'http'),
:type => 'ROBOTS_TXT',
:data => { :file => u },
:update => :unique_data
host: target_host,
port: rport,
proto: 'tcp',
sname: (ssl ? 'https' : 'http'),
type: 'ROBOTS_TXT',
data: { file: u },
update: :unique_data
)
end
rescue ::Rex::ConnectionRefused, ::Rex::HostUnreachable, ::Rex::ConnectionTimeout
Expand Down
7 changes: 6 additions & 1 deletion modules/auxiliary/scanner/ssh/ssh_version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ def initialize
'Daniel van Eeden <metasploit[at]myname.nl>', # original author
'h00die' # algorithms enhancements
],
'License' => MSF_LICENSE
'License' => MSF_LICENSE,
'VulnerableEnvironment' => {
'definition' => 'openssh',
'default_variant' => '7.2',
'port_mapping' => { 22 => 'RPORT' }
}
)

register_options(
Expand Down
15 changes: 15 additions & 0 deletions modules/exploits/multi/misc/apache_activemq_rce_cve_2023_46604.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,21 @@ def initialize(info = {})
# The maximum time in seconds to wait for a session.
'WfsDelay' => 30
},
'VulnerableEnvironment' => {
'definition' => 'activemq',
'default_variant' => '5.18.2',
'profile' => 'broker-only',
'port_mapping' => { 61616 => 'RPORT' },
'overrides' => {
'ci' => {
'exploit' => {
'options' => {
'TARGET' => 1
}
}
}
}
},
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
Expand Down
43 changes: 28 additions & 15 deletions plugins/test_env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1595,11 +1595,13 @@ def cmd_test_env_build(args)

# Step 17: apply datastore to the active module
datastore.each do |key, value|
mod.datastore[key] = value
if mod.options.include?(key)
mod.datastore[key] = value
end
end

# Step 18: display results to user
build_display_results(env_id, config, datastore)
build_display_results(env_id, config, datastore, mod)

rescue PortAllocator::NoPortsAvailable => e
print_error("No available ports: #{e.message}")
Expand Down Expand Up @@ -1682,7 +1684,7 @@ def build_resolve_environment(mod, env, options)
# this specific image/variant), apply it now, before the container is
# even started, so it's reflected if the user runs 'show options'.
recommended_payload = config.dig('ci', 'exploit', 'payload')
if recommended_payload && mod.datastore['PAYLOAD'] != recommended_payload
if recommended_payload && mod.options.include?('PAYLOAD') && mod.datastore['PAYLOAD'] != recommended_payload
print_status("Setting recommended payload for this environment: #{recommended_payload}")
mod.datastore['PAYLOAD'] = recommended_payload
end
Expand Down Expand Up @@ -1870,16 +1872,18 @@ def build_register_environment(runtime, container_id, mod, variant, config, allo
end

# Step 18: Display build results and suggested exploit command.
def build_display_results(env_id, config, datastore)
def build_display_results(env_id, config, datastore, mod)
print_good("Environment ready.")
print_status("Environment ID: #{env_id}")
datastore.each do |key, value|

applicable = datastore.select { |k, _v| mod.options.include?(k) }
applicable.each do |key, value|
print_status(" #{key.ljust(12)} => #{value}")
end


env = self.class.registry.get(env_id)
print_status("Suggested: #{env.exploit_command}")
action = mod.type == 'auxiliary' ? 'run' : 'exploit'
opts = applicable.map { |k, v| "#{k}=#{v}" }.join(' ')
print_status("Suggested: #{action} #{opts}")
end
def cmd_test_env_help
print_line("Usage: test_env <command>")
Expand Down Expand Up @@ -2030,7 +2034,7 @@ def cmd_test_env_exec(args)
config = loader.resolve(env_meta.definition, target.env_version, env_meta.profile, env_meta.overrides) rescue nil
ci_exploit = config&.dig('ci', 'exploit') || {}

if ci_exploit['payload']
if ci_exploit['payload'] && mod.options.include?('PAYLOAD')
print_status("Setting recommended payload for this environment: #{ci_exploit['payload']}")
driver.run_single("set PAYLOAD #{ci_exploit['payload']}")
end
Expand All @@ -2045,8 +2049,12 @@ def cmd_test_env_exec(args)
# RPORT/credentials by hand - single source of truth, and it's the
# exact same hash 'test_env build' already showed the user under
# "Suggested:", so what runs here always matches what was printed.
applied = {}
target.datastore.each do |key, value|
driver.run_single("set #{key} #{value}")
if mod.options.include?(key)
driver.run_single("set #{key} #{value}")
applied[key] = value
end
end

# --- Step D.5: avoid Rex::BindFailed from stale listeners on
Expand All @@ -2058,17 +2066,22 @@ def cmd_test_env_exec(args)
# kills the stale process. Picking a fresh free port each time
# removes the collision entirely rather than requiring cleanup.
%w[SRVPORT FETCH_SRVPORT].each do |opt|
free_port = free_local_port
driver.run_single("set #{opt} #{free_port}")
if mod.options.include?(opt)
free_port = free_local_port
driver.run_single("set #{opt} #{free_port}")
applied[opt] = free_port
end
end

# --- Step E: run it. driver.run_single("exploit") reuses the
# console's own exploit-execution path - AutoCheck, payload
# generation, session creation, and all success/failure messaging
# come from that well-tested path rather than being reimplemented
# here. See the design note above for why this matters.
print_status("Executing: #{target.exploit_command}")
driver.run_single("exploit")
action = mod.type == 'auxiliary' ? 'run' : 'exploit'
opts = applied.map { |k, v| "#{k}=#{v}" }.join(' ')
print_status("Executing: #{action} #{opts}")
driver.run_single(action)
rescue => e
print_error("test_env exec failed: #{e.class} - #{e.message}")
elog("test_env exec error: #{e.class} - #{e.message}")
Expand Down
Loading