21 Dec 2017

From YAML Deserialization to RCE in Ruby on Rails Applications

It’s not uncommon for me to find unsafe YAML deserialization while reviewing Ruby on Rails applications. For those who aren’t familiar with the dangers of arbitrary YAML deserialization, the short of it is that deserializing YAML can lead to code execution. This is possible because YAML deserialization allocates a new Ruby object without initializing it, and then calls a callback method named init_with if defined. If an object of a particular class were to be cleverly serialized with a particular set of instance variables then maybe, just maybe, a callback made on deserialization will end up executing dangerous code. This is why it is unsafe to pass user input to YAML.load.

If this sounds like a strange thing for a web application to do, consider that serializing objects can be a lazy way persist complex objects between multiple requests, a data store, a cache, etc. For example, the last instance of this I found was in functionality for persisting user search preferences in a cookie. Instead of using JSON, I imagine a developer somewhere thought it would be clever to just dump the entire search object and load it again on subsequent requests.

As you might have already guessed, crafting a reliable payload that exploits this is no small feat. The object state and callbacks have to be just right, constrained to only the classes made available to the application. Charliesome has a fantastic writeup of a payload he and several others wrote. This was especially interesting at the time because of CVE-2013-0156, which caused vulnerable Ruby on Rails applications to perform YAML deserialization when handling XML HTTP requests. This was a critical bug in the framework itself.

Anyway, this was all back in 2013. We’re now heading into 2018 and I’m pretty confident that we’re going to continue to see developers calling YAML.load with user-supplied YAML. Unfortunately for us penetration testers, Charliesome et al’s payload hasn’t worked since code changes were made to Rack’s SessionHash class. Without a working payload, we’ve been forced to report unsafe YAML deserialization to clients without a proof of concept reproduction. That’s a bit of a buzz kill.

Then about a month ago, NiklasB published an updated version of Charliesome’s payload generator. This is kind of a big deal, because as far as I know there aren’t any other working payload generators that have been publicly released. Interestingly, the payload it generates is very similar to the original one. Apparently not that much has changed since 2013.

You can find his payload generator here.