It is a well known security issue that allowing user controlled inputs in calls to the Ruby String#constantize, String#safe_constantize, Module#const_get and Module#qualified_const_get methods are insecure, since these methods allow the arbitrary loading of Ruby classes.For an example, the following code snippet would allow an attacker to initialise an object of any class:
This unsafe reflection has resulted in code execution vulnerabilities in the past, by initialising a new Logger object since the class previously used the Ruby Kernel#open method for opening the log file, which allowed the execution of terminal commands if the input begun with |.However, this method to achieve RCE has been patched since 2017 when the use of Kernel#open was changed to File.open to only allow the Logger class to create files.
It did pique our interest if the above dangerous construct could still result in arbitrary code execution on a minimal installation of Ruby on Rails, which resulted in the discovery of a new method by loading SQLite extension libraries via the SQLite3::Database class in the sqlite3 gem. Upon further investigation, this new SQLite3::Database reflection gadget could also be exploited in a new deserialisation gadget chain if user inputs were deserialised using Marshal.load on a Rails application with the sqlite3, activerecord and activesupport gems installed.
Although brakeman has the check_unsafe_reflection rule for detecting potentially unsafe uses of the Ruby constantize or similar methods, a wide variety of exploitation techniques are fairly limited. The following articles were two notable investigations that identified several reflection gadgets that could be abused to impact a Ruby/Rails application:
Logger command execution method that has been patched since 2017.constantize documented Gems that had an unsafe const_missing method that could be abused to cause memory a leak with only just params[:class].classify.constantize as the dangerous construct.The focus for this research was investigating if unsafe reflection or deserialisation of user controllable parameters on a Rails application could result in RCE. Testing was conducted on a minimal and default installation of Ruby on Rails with the following two endpoints, where the following Dockerfile (adapted from Luke Jahnke) was used to build the Docker image.
/reflection: Unsafe reflection of user controlled values to construct a new object./marshal: Unsafe deserialisation of a user controllable value using Marshal.load.The first task was to discover classes within installed gems and Ruby that could result in arbitrary code execution when an object is initialised. Using semgrep to assist with identifying the use of dangerous Ruby methods within the installed gems, over 1,000+ potential sinks were raised that were then manually investigated if input parameters for a constructor reached the sink.
This analysis resulted in the discovery of the following classes that were considered suitable candidates to investigate further.
RDoc::RI::Driveroptions[:extra_doc_dirs] option allows specifying extra folders for loading documentation, that initialises a new RDoc::Store object (source).load_cache method for the RDoc::Store object is then invoked, that appends cache.ri to the folder path that is then loaded using Marshal.load (source).SQLite3::Databaseoptions[:extensions] specifies additional file paths to SQLite extension libraries that are loaded when the database was initialised (source).A few other interesting constructor methods in the following classes were also discovered, but since the impact was not code execution they were not analysed further and are summarised below:
LoggerLogger.new('create/file/anywhere.txt')Gem::ConfigFile--debug, then it sets $DEBUG=true (source).Gem::ConfigFile.new(['--debug'])TCPServer and TCPSocketTCPSocket.new('127.0.0.1', 1337)ActiveRecord::ConnectionAdapters::SQLite3AdapterActiveRecord::ConnectionAdapters::SQLite3Adapter.new({database: 'created/anything'}) will create the folder ./created.ActiveSupport::ConfigurationFileto_json method and returning in a HTTP response).ActiveSupport::ConfigurationFile.new('/etc/passwd').to_jsonENVENV variable is a hash-like accessor for environment variables, depending on the implementation of the unsafe reflection it is possible to leak environment variables.'ENV'.constantize.to_jsonBoth the RDoc::RI::Driver and SQLite3::Database reflection gadgets depend on reading a user controllable file on the filesystem to leverage the arbitrary code execution sinks. The problem is that the test application does not have ActiveStorage or any other file upload functionality enabled. The TCPSocket and TCPServer (plus other networking classes) reflection gadgets could be used to open a file descriptor at /proc/self/fd/x, but the open syscall is unable to open these socket files.
The next option was to look into how Ruby on Rails handles multipart/form-data requests with uploaded files. Underneath the hood, Ruby on Rails uses Rack for handling the processing of web requests and responses. The following code snippet from rack/blob/main/lib/rack/multipart.rb shows the parse_multipart method that processes multipart/form-data requests with uploaded files.
Digging into Rack::Multipart::Parser class, it shows that a new TempFile is created with the contents of the uploaded file with the default prefix of RackMultipart.
This can be confirmed by sending a file in a POST request to a target Rails application, and then observing the temporary file in the /tmp folder (default location for temporary files).
NOTE: The / endpoint does not exist on the test application, but the file is still saved to the filesystem.
Example curl command to demonstrate uploading a file
Showing the generated temporary file on the filesystem inside the Rails container
One problem with this method of file upload is that although the extension value can be controlled by the user, the filename cannot be controlled so the RDoc::RI::Driver gadget is no longer a valid candidate for this scenario. This is because when a new RDoc::RI::Driver object is initialised with the options[:extra_doc_dirs] option, it deserialises the cache.ri file in the provided folder.
The other issue is determining the last six characters of the temporary filename before the extension. However, when a temporary file is created, the Rails process opens a file descriptor that is a symbolic link to the temporary file, where file descriptor numbers are significantly easier to enumerate than a Ruby temporary file name.
Example showing that /proc/self/fd/12 is a symboilic link to the temporary file
SQLite3::Database Reflection GadgetTo confirm if /proc/self/fd/{num} paths could be loaded as a SQLite extension, first a basic POC library was compiled using the following source code and gcc command.
C POC file that will create a file at /tmp/rce-conf.txt to confirm code execution
gcc command to compile the malicious SQLite extension
Uploading the payload.so to the application, Rack opened a new file descriptor to the temporary file at /proc/self/fd/13.
Finally, the below curl command would load /proc/self/fd/13 as a SQLite extension when the SQLite3::Database object is initialised.
The creation of the /tmp/rce-conf.txt confirms that the uploaded SQLite extension was executed
This confirms that any Rails application with the sqlite3 gem installed (which is installed by default) that allow the unsafe reflection of user inputs could result in RCE.Rack enables an attacker to upload a malicious SQLite extension to the filesystem that is loaded during the construction of a SQLite3::Database object by using /proc/self/fd/x filepaths, which an external attacker could easily enumerate.
SQLite3::Database Reflection Gadget into a Deserialisation GadgetThe next challenge was to investigate if SQLite3::Database objects could be leveraged in a deserialisation gadget chain to achieve RCE on a Rails application. The first step was to confirm if SQLite3::Database objects could be serialised, which is unfortunately not the case as it shows in the screenshot below.

However, it was discovered that the ActiveRecord::ConnectionAdapters::SQLite3Adapter class initialises a new SQLite3::Database object when the connect! method is invoked, as it shows in the following code snippet.
Using the infamous ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy gadget, a deserialised DeprecatedInstanceVariableProxy object could be used to invoke the connect! method of a deserialised ActiveRecord::ConnectionAdapters::SQLite3Adapter object, which was first utilised back in 2013 by Hailey Somerville to exploit CVE-2013-0156. Since 2013, the DeprecatedInstanceVariableProxy gadget now requires the @deprecator instance variable to be set, otherwise an exception would be raised before executing the deserialised payload.
Putting all these points together in a gadget chain, the following proof-of-concept script was developed:
To demonstrate this new deserialisation payload, the same payload.so from the previous section was uploaded to the test application that opened a file descriptor at /proc/self/fd/12.
The following curl command then exploits the Marshal.load vulnerability on the /marshal endpoint on the test application that results in the initialisation of the SQLite3::Database object that loads the SQLite extension.
Once again, the creation of the /tmp/rce-conf.txt file confirms that the SQLite extension was successfully executed, confirming RCE.
This new gadget chain could be exploited to achieve RCE on any Rails application that allows the unsafe deserialisation of user controlled inputs and has the sqlite3, activerecord and activesupport gems installed; where all three of these gems are installed by default on a minimal installation of Ruby on Rails. Considering that the DeprecatedInstanceVariableProxy gadget has been known about since 2013, plus this gadget chain exploits the legitimate requirement to allow the loading of SQLite3 extensions when initialising a database connection, it is speculated that this gadget chain could be viable for some time.
The SQLite3::Database reflection gadget that was demonstrated in this article showed that unsafe Ruby reflection and construction of new objects using user inputs could still result in arbitrary code execution if the sqlite3 gem installed, which is installed by default on a minimal installation of Rails. In addition, this article showcases a method to write files to the /tmp folder by abusing Rack’s file upload parsing and accessing the contents of the file through /proc/self/fd/x, which could be utilised in exploiting other vulnerabilities that depend on file write on a Rails application.
Using our research about the SQLite3::Database reflection gadget, a new gadget chain was discovered that could be exploited to achieve RCE if the activerecord and activesupport gems installed, which they are by default on Ruby on Rails.
In conclusion, this research could potentially result in new RCE vulnerabilities being discovered once again on Rails applications that reflect or deserialise user controllable inputs.