Detecting and exploiting mass-assignments in order to manipulate user columns and read private messages
Published on 05.02.2019
Mass assignments are often overlooked yet extremely critical vulnerabilities, depending on the context. I've been asked about details on how I found certain vulnerabilities a couple of times now and I've never really seen this on the HackerOne Hacktivity or security blogs. I think the reason for this is that it might not be as straightforward as a Local File Inclusion for example, but it really isn't any harder.
Mass assignment vulnerabilities
Mass assignments are vulnerabilities in applications, that automatically bind HTTP request parameters into program code variables or objects as described by OWASP. Automatically binding data in an array from request input is especially convenient for developers that use a framework and/or an ORM.
In Eloquent, which is the ORM used by the Laravel PHP Framework, you have models for your objects. These models have a static
create method, that takes an array of data as its first parameter. The keys in the array are the database columns and the values the corresponding data, that should be saved in the respective column.
Let's say we have users in our application. The database has the following columns:
id is an AUTO INCREMENT column and
is_admin will be set to
0 by default.
Our registration form looks like the following:
<form method="POST" action="https://evilcorp.com/register"> <input type="hidden" name="csrf" value="RWFzdGVyIGVnZyEgWW91J3JlIHJlYWxseSBjdXJpb3VzLCBhcmVuJ3QgeW91PyBLZWVwIGNoZWNraW5nIGV2ZXJ5IGRhdGEgZXZlcnl3aGVyZSwgd2VsbCBkb25lIQ=="> <input type="text" name="user[name]" value="Paul"> <input type="email" name="user[email]" value="firstname.lastname@example.org"> </form>
After submitting the form, the backend can access a key
user in the request data (PHP example
[ 'name' => 'Paul', 'email' => 'email@example.com', ]
With an ORM, that will persist the representation of this user in the database automatically, we can just pass this array to a method like
create on the model and the library will do the rest:
With no previous user, our database will look like this now:
id, name, email, is_admin 1, Paul, firstname.lastname@example.org, 0
Conveniently arranging and persisting data this way is called a mass assignment. Without a validation or filtering of the input data (columns and values), an attacker can set any columns to any value.
Exploiting this is trivial with an additional input:
<input type="text" name="user[is_admin]" value="1">
A fresh database would look like this after the creation process:
id, name, email, is_admin 1, Paul, email@example.com, 1
Detecting mass assignments
Detecting mass assignments can be rather simple but may also require analysis and interpretation of the corresponding application response to different requests with various values. Interesting aspects of the response can be its code (500 for example), headers (such as a redirect to an error page) and actual content (such as error messages).
The most simple indicator for mass assignments is the bracket syntax for input parameter names, which I explained in the previous section. When sending an additional input with a column name like
<input name="user[thisdoesnotexist]" type="text">) and any value, it is very likely that an application with mass assignments and no column whitelist will respond with an error, most commonly response code 500. The error will be caused by the database as the passed column does not exist and the INSERT query therefore fails. Keep in mind, that indicators are no definitive confirmation and an internal server error could also be caused by a MassAssignmentException thrown via Laravel for example.
Another approach - also important for the exploitation part - is gathering information about the database structure of the model in question. This can be achieved by viewing the source of generated pages. You are looking for a (partly) dump of the current user. Also, there might be API requests returning the current user, rather when creating a user or just a GET request retrieving the logged in user. On top of that, in applications with frontend frameworks like VueJS, the whole user with all attributes could be passed as an object to the Vue component, which can be found in the source code aswell. With knowledge about columns that might be manipulable, you can try exploiting the mass assignment by passing it in the request in question and verifying a change of the value at the place where you found the column.
Exploitation, case studies and impact
In order to automatically gather response data and behaviour of the application quickly, you can use the Burp Suite Intruder. I do not own Burp Suite Pro unfortunately, which makes the Intruder unusable after a couple of seconds because of Community Edition limitations. Also make sure that automated tools are allowed and keep in mind that this can possibly cause damage.
Intercept the request, right click on it and choose
Send to Intruder in the context menu. Add an additional input like
user[column], which you could set to 1. Highlight
column and click
Add § at the right side of the Burp interface to add an intrusion point, where the program will add the entries in the wordlist one by one afterwards.
sqlmap comes with a
common-columns.txt wordlist. Go to the
Payloads tab in the Intruder module and load the wordlist under
Payload Options. With these basic settings, you can start the attack and conveniently order the responses by status code, length and more.
Burp will try to set all the columns in the wordlist to 1. Of course you can also choose other values. Also note, that, besides causing damage, this might not just escalate your privileges to an admin account with columns like
is_admin, but also get you banned if setting columns to 1/0.
As an alternative to the Burp Intruder, you can use fuzzers like wfuzz. However, wfuzz does not log the actual response content. In order to distinguish between the typical responses you are receiving for the request and anomalies, filters blacklisting/whitelisting specific response codes and amount of lines, words or characters will be very helpful.
Impact: Denial of Service
In a scenario with an auto incremental identifier column, you can trigger a DoS. Let's say the user table has a column named
id, which is an auto incremental
UNSIGNED INT(10). The highest possible id will be 4294967295. When inserting a user, the application does normally not choose the id, therefore it will make the database take a look at the current auto increment index. The problem with passing the id to the database is that the auto increment functionality will use the inserted id as its new starting point. Putting this all together, an attacker can send an additional input
user[id] with the value
4294967295. That will cause all future inserts into the user table to fail.
Why? When inserting a row with the highest possible number, the auto increment index will be stuck at that number, as it reached the columns limit. This means that the database will try to insert the next row with the same id. Primary keys uniquely identify a specific row in tables. As a result, the query is going to fail because of
Duplicate entry '4294967295' for key 'PRIMARY'.
You can test this on my database fiddle.
Impact/Case study: Manipulate created user and read private messages
With the concepts outlined in the sections above in mind, I was going through the source code of a signup page in a bug bounty program. I noticed the form input names, that indicated mass assignments. So I started out with requests including columns like...
with a value of
1. The application did respond to this with status code 500 directly. I started being more curious, so I tried the most popular column name
id with different values.
Sending the column
id with a value of
1, which is a user that does probably already exist if not deleted, the application did respond with a redirect. It was a redirect to an error page. As the error above was a 500 response directly and not a redirect, I was sure, that in this case the column does in fact exist. The reason why it redirected to an error page is the value, which most likely caused duplicate entries for the primary key column id. So I did alter the
id value to something similar to
133333337. Response code 200, the user has been created. Fine.
Unfortunately, I did not manage to find my user id in the source code. Without a reflection of the manipulated column, I was just very certain about the success of this attack, not absolutely sure. However, I found the id of my profile, which belongs to my user. Upon registration, there is an array for the user and one for the profile. So I tried sending an additional column
id for the profile array aswell. Going to the places in the source code, at which my profile id has been reflected, I've seen the id I passed in the creation request. The mass assignment vulnerability has been confirmed at this point. If there is no blacklist for columns, in which
id was missing, this was certainly a critical flaw. We can at least trigger a denial of service. I contacted the security team before reporting. I explained the scenario, my current theory and asked for information on the database structure. The team was super responsive and gave me an insight into the column names.
However, in the meantime I did browse the rest of the application to see if I can find another occurrence of this in order to demonstrate a more critical and straightforward scenario for reporting. One reason for this is that the program is managed and it will be very hard to get past the triaging state with something that is not a single one-liner command or request.
I investigated the form for sending message. This time, there was a hidden input already, which was not set by default; basically this vulnerability is somewhere between an insecure direct object reference and a mass assignment. The input name was similar to
mail[conversation_id]. Setting the value to a random number and sending the message to my other test account worked fine. Opening the message in the
Sent folder of my inbox did show a third-party message below the test message I sent. It did not belong to my two test accounts in the conversation. I suspected that
conversation_id is meant to be set to the message you are answering in a reply.
I stopped testing right away at this point, documented everything and filed the report. Even with the id column manipulation in combination with the analysis of the application responses and the demonstration of reading other peoples messages, the report has been flagged with
Needs more info by the platform team. Luckily, I was still in contact with the great team of the company, which did confirm the vulnerability on their own and took over the report.
After the report was resolved, I've been provided with additional insights into what happened after my tests. I did purposely choose an id, which is not too high, so the database won't reach the column integer value limit any time soon, as this results in a DoS; see Denial of Service above. In fact, the database was fine. What actually triggered a DoS nonetheless was a maximum number limitation in their runtime engine at
2^32. This underlines the fact that you should carefully run your tests and think about possible problems the system might run into when processing the data you are sending.
To sum up, all applications are different. An interpretation of the behaviour in respond to different input parameters might be required to detect an unvalidated mass assignment, but it is worth the time. Depending on the applications context, the specific form and object being modified by it, the impact can range from a simple DoS to reading sensitive private messages, privilege escalation and account takeovers. The core concept of this vulnerability is still very simple. With different applications, there are most likely million other ways of detecting and exploiting them. However, I hope you could take away some aspects, that help you securing web applications in the future, happy bounties!