80 lines
2.2 KiB
Ruby
80 lines
2.2 KiB
Ruby
# ©AngelaMos | 2026
|
|
# exploit_probe.rb
|
|
# frozen_string_literal: true
|
|
|
|
require "fileutils"
|
|
require "marshalsea"
|
|
|
|
CANARY_PATH = "/tmp/marshalsea-canary"
|
|
CANARY_MARKER = "fired"
|
|
|
|
RESULT_FIRED = "FIRED"
|
|
RESULT_BLOCKED = "BLOCKED"
|
|
RESULT_INERT = "INERT"
|
|
|
|
def canary_present?
|
|
File.exist?(CANARY_PATH) && File.read(CANARY_PATH) == CANARY_MARKER
|
|
end
|
|
|
|
def clear_canary
|
|
FileUtils.rm_f(CANARY_PATH)
|
|
end
|
|
|
|
def classify(fired, detail)
|
|
return RESULT_FIRED if fired
|
|
return RESULT_BLOCKED if detail.start_with?("ArgumentError")
|
|
|
|
RESULT_INERT
|
|
end
|
|
|
|
def observe
|
|
clear_canary
|
|
detail = begin
|
|
yield
|
|
"returned"
|
|
rescue StandardError => e
|
|
"#{e.class}: #{e.message}"
|
|
end
|
|
[canary_present?, detail]
|
|
end
|
|
|
|
erb_version = Gem::Specification.find_all_by_name("erb").map(&:version).max.to_s
|
|
image = ENV.fetch("MATRIX_IMAGE", "?")
|
|
|
|
chain = Marshalsea::Chains::ErbDefModule.canary(CANARY_PATH, CANARY_MARKER)
|
|
primitive = Marshalsea::Chains::ErbDefMethod.canary(CANARY_PATH, CANARY_MARKER)
|
|
|
|
chain_blob = chain.serialize
|
|
primitive_blob = primitive.serialize
|
|
|
|
built_without_firing = !canary_present?
|
|
|
|
chain_fired, chain_detail = observe { Marshal.load(chain_blob) }
|
|
chain_outcome = classify(chain_fired, chain_detail)
|
|
|
|
primitive_load_fired, = observe { Marshal.load(primitive_blob) }
|
|
primitive_fired, primitive_detail = observe do
|
|
Marshal.load(primitive_blob).def_method(Module.new, "marshalsea_probe")
|
|
end
|
|
primitive_outcome = classify(primitive_fired, primitive_detail)
|
|
|
|
clear_canary
|
|
|
|
predicted = Marshalsea::Chains::ErbDefModule.affects?(erb_version) ? RESULT_FIRED : RESULT_BLOCKED
|
|
inspection = Marshalsea::Marshal::Parser.new(chain_blob).parse
|
|
|
|
puts format("%-9s erb=%-9s chain=%-8s primitive=%-8s predicted=%-8s sinks=%d %s",
|
|
image, erb_version, chain_outcome, primitive_outcome, predicted,
|
|
inspection.sinks.length, chain_detail[0, 60])
|
|
|
|
checks = {
|
|
"builder_did_not_execute_its_own_payload" => built_without_firing,
|
|
"chain_matches_prediction" => chain_outcome == predicted,
|
|
"primitive_matches_prediction" => primitive_outcome == predicted,
|
|
"primitive_is_inert_until_the_application_calls_it" => !primitive_load_fired,
|
|
"chain_carries_no_sink_tag" => inspection.sinks.empty?
|
|
}
|
|
|
|
checks.each { |name, ok| puts "#{name}=#{ok}" }
|
|
exit(checks.values.all? ? 0 : 1)
|