Ruby on Rails
A common web framework for the Ruby Programming Language
Note: A lot of content here is taken from this gist talking about Ruby on Rails applications. Be sure to check it out for a lot of attack techniques
Command Execution
In Ruby, if you can execute any code a simple `
will allow you to execute system commands:
Security Pitfalls
In Ruby, Kernel
is the standard module, and its functions do not need to be prefixed with Kernel.
, meaning Kernel.open()
and open()
are equivalent. A different function however is File.open()
, which sounds like it should do the same thing.
One important difference however is that the open()
function allows subprocesses to be created by prefixing with the |
pipe symbol:
If you have control over the start of such a path, you can inject a |
pipe symbol to execute commands. While you often also have Directory Traversal when starting a path with /
, this attack does not require the /
slash character and may get through a filter that tries to prevent it.
Regular Expressions
In ruby you can match a string to some regex in two simple ways:
These two ways are identical to each other, but not the same as many other programming languages. The change is that Regular Expressions are multi-line by default. In some languages, this is represented as another argument, or /m
at the end, but in Ruby, this is the default.
The multi-line mode makes ^
and $
act differently. Normally, these would represent the start and end of the whole string. But in multi-line mode, these are the start and end of the line. This means that if there are newline characters in the string, the ^
for example could match against a line earlier or later in the string.
Many regular expressions use these characters to sanitize user input and to try to if the whole string follows a particular pattern. But in Ruby, if you can get a newline into the string, any of the lines just need to match. So one line can follow the pattern, but another line can be some arbitrary payload that gets past the check.
Here's an example:
See the whole chapter on Regular Expressions (RegEx) for more details on the syntax.
URL Parameters
Similarly to PHP, Ruby on Rails allows you to put arrays in query parameters:
This will result in a params
variable like this:
You can even use named array keys to create objects inside:
Finally, you can create nil
values by not providing a value, which might break some things:
See 1.1.2 - Multiparameter attributes and 1.1.3 - POST/PUT text/xml for more input tricks like these.
Sessions
You can do a lot if you can find the Secret Key used for verifying sessions. Some common locations are:
config/environment.rb
config/initializers/secret_token.rb
config/secrets.yml
/proc/self/environ
(if it's just given via an environment variable)
Forging sessions
First of all, you can of course sign your own data to create arbitrary objects that might bypass authentication or anything else. See this code as an example to serialize your own data.
Insecure Deserialization
Ruby on Rails cookies use Marshal serialization to turn objects into strings, and then back into objects for deserialization.
For Ruby 3 you can use a piece of code like this to create a marshal payload executing any Ruby code:
More References
Ransack Data Exfiltration
The popular Ransack Ruby library allows developers to query a database in the form of objects. On version < 4.0.0 (Released: Feb 9, 2023), there is a big risk of mass assignment in query parameters that perform these filters. The client often provides a query where they can specify what attributes to filter for with conditions like cont
(contains) or start
(starts with). These can be pointed to sensitive data like password reset tokens by an attacker and exfiltrated character-by-character by the named filters.
The reason versions after 4.0.0 are often safe, is because an explicit whitelist is required to be filled out per class to select all queryable attributes. Of course, a developer could still include sensitive fields here by mistake, but it is safe by default.
Take this vulnerable code example:
Here the search
page uses params[:q]
from the client to query the Post
class, which is indended to be searched for a title
or content
.
Then, a URL like /search?q[title_cont]=hacking
will respond with all posts with a title containing "hacking". First is the path to the attribute: title
, and then comes the Predictate: cont
, separated by an _
underscore.
The vulnerability here however, is when we provide a sensitive attribute, which is easy as the path to the attribute can be deeper by separating them by underscores. If we want to find the reset_password_token
for example, this is inside of the user
:
/search?q=[user_reset_password_token_cont]=hacking
. This query will return something if there is a user with "hacking" in their password reset token, but this can be abused by doing a character-by-character brute-force attack where we provide all possible starting characters and find which give a response back, indicating it was found:
Afterward, we know a token starts with 2
, and we can simply try all other characters after it:
By continually doing this, eventually, we find for example q[user_reset_password_token]=2dd0571e439813f7
which shows the entire token is correct, and we have leaked it in only a few requests.
Leaking such hexadecimal token can look something like this:
In this case, we found the sensitive user
and reset_password_token
attributes by reading the code, but in a more black-box scenario where you only notice the pattern of
?q[attr_predicate]=
some guessing is required. Tools like ffuf
can fuzz for these attributes by providing the FUZZ
keyword in the correct part of a URL:
In the example above, we try to find an object and property that when _eq
is put on it, returns false because it is not found. Then the size is smaller and different from when the attribute is wrong, as then it is ignored returning all results in a static size.
This behavior depends, as in some cases a wrong guess will instead give no results, requiring you to change the fuzzing. A strategy for this would be to create a (mostly) always-true query like
?q[OBJ_PROP_cont]=_
asking for the property to contain at least one character (_
= wildcard).
Case Insensitive Predicates
An important note, however, is the fact that the start
predicate is case-insensitive, meaning using just this technique we won't know the casing of a token. For a hexadecimal token, this is no problem, but for a Base64 token, it is important to get this correct.
There is no easy way to make start
case-sensitive, but there are alternative predicates that are case-sensitive like eq
(SQL =
) or cont
(SQL LIKE
). Not all databases perform LIKE
case-sensitively, some popular ones that do include PostgreSQL and Oracle DB.
While MySQL/MariaDB, SQLite, or Microsoft SQL do not. It is easy to test if this is the case by searching for a string with the wrong casing using the eq
or cont
predicates.
Commonly eq
will work case-insensitively making it possible to guess all different combinations of casing for a token. If you found a token like a2b
, you can try a2b
, A2b
, a2B
, and A2B
to find the correct one. Then, use this correct token to reset the password, or whatever else the sensitive data lets you do. Here is an implementation:
Binary Search
If the targetted data is numeric, it is possible to use the lt
(less than) or lteq
(less than or equal) predicates to compare a range of values all at once. This algorithm is called Binary Search and can drastically speed up your attack. Here is a simple implementation that leaks the number
attribute from user
:
A more advanced example is achieving Binary Search for string attributes. We require a way to test multiple values at once, to test a range (half of the possible values) at once. It turns out, the start_any
predicate (similar to cont_any
) can do this for us! It requires an array and performs the regular start
predicate with all the strings in that array, and if one is found, it is successful.
We can make use of this by specifying half of the possible continuations as an array in the query parameters, which will return results if the next character is in any of them, achieving Binary Search once again.
Some important things to note are firstly the fact that Ruby (and many other frameworks) accept arrays as query parameters by duplicating the names and appending []
like
?array[]=1&array[]=2
to create array=["1","2"]
. We use this to generate the required strings. These strings need to be the known prefix so far, and half of the possible characters. If we know prefix="se"
the guesses will be ["sea", "seb", "sec", ...]
.
Here is an example implementation:
In every situation, binary search will be faster than linear search, but the difference is largest when ALPHABET
is largest. If this is N
, the average time for both will be:
Linear Search:
N/2
(N=50 -> 25 attempts)Binary Search:
log2(N)
(N=50 -> 6 attempts)
Last updated