IDOR with MongoDB: understanding ObjectID
During a recent engagement, I discovered a flaw allowing to directly access some personal information.
The vulnerable API endpoint was not properly checking the authorization to access to objects in a MongoDB database designated by their _id field. The format of the endpoint was the following:
hxxp://www.my-vulnerable-site.com/api/endpoint/1.0/?param1=foo¶m2=bar
The param1 and param2 parameters are useless for us, but the id was interesting. At first I didn’t know if it really was a MongoDB ID. The IDs fetched were similar to 507f1f77bcf86cd799439011
.
So at the first view, nothing special and this seems to be random. But not exactly.
When an object is created in MongoDB, a unique ID is assigned. Some parts of this ID are random whereas others are not, and here we – as evil attackers – can act here to determine other IDs.
First of all, the ID size is fixed to 12 bytes. If it is not 12-byte long, the ID used is probably not an ObjectID, the ID format used in MongoDB (but MongoDB may still be in use in the back-end).
At first, I was looking in the documentation of the latest version of MongoDB (at time of the writing, version 4.0). I determined that my ID was constituted of the following parts:
- a 4-byte value representing the seconds since the Unix epoch,
- a 5-byte random value,
- a 3-byte counter, starting with a random value.
This is directly specified by the MongoDB documentation: https://docs.mongodb.com/manual/reference/method/ObjectId/
With these rules, the ID shown before has the following form:
- 507f1f77: the timestamp of the object creation (1350508407 in decimal and something on the 12th of October 2012 in human-readable form),
- bcf86cd799: a random value,
- 439011: an incremental value, starting at a random value.
The last part in particular has an interesting form. In fact, by incrementing or decrementing this part from a known valid ID, we could access, in the case of an Insecure Direct Object Reference, to objects that are created in the same second as our valid object.
The random value is not regenerated for the same second. In my case, I was able to access several dozens of objects belonging to other users. If my own object has the ObjectID 507f1f77bcf86cd799439011
, chances are that 507f1f77bcf86cd799439010
and 507f1f77bcf86cd799439012
are also existing objects, maybe belonging to other users.
But 2 things were bothering me:
- I couldn’t determine IDs outside the set of contiguous ones around my own object (the entropy of the last 8 bytes is too huge to explicitly search for them online),
- I had IDs, corresponding to objects belonging to my profile, generated at other dates (like one month before) that were similar with the first I looked for.
I decided to get some help from Maxime Beugnet (@Mbeugnet, thank you guy!) and he pointed to me that the ObjectID format has changed. Diving again in the MongoDB documentation, I found that it effectively changed between versions 3.2 and 3.4.
According to the documentation we have the following format for version 3.2 and below (https://docs.mongodb.com/v3.2/reference/method/ObjectId/):
- a 4-byte value representing the seconds since the Unix epoch,
- a 3-byte machine identifier,
- a 2-byte process id,
- a 3-byte counter, starting with a random value.
There is much less entropy in this one. And this corresponds better to the format of the IDs I had because the machine identifier was limited to 2 or 3 values (suggesting the size of the MongoDB cluster).
The two last parts are still static for objects created the same second. Moreover, the process id should only slightly change or stay constant from one second to another (and all the PID pool is still limited to 65 535) for a given machine ID. That lets us with the randomness of the last 3 bytes, 16 777 216 possibilities to try. Really?
If the database running is creating several objects in one second, which is not a mad assumption, we have some dozens of objects in this pool. By randomly generating the last 3 bytes, we only need one match to find them all because they would be continuous at this step and we only have to increment and decrement the obtained ID.
Even with this method we have to realize over 100 000 requests for a second of object creation to have chances to get a match.
Another method, maybe a bit more aggressive, is to force object creation in our user scope to have access each second to a valid ObjectID. Given these ObjectIDs, we can access all the objects by exploiting the IDOR by incrementing and decrementing our own ID. This method works for the 2 formats: pre-3.4 and post-3.2.
Conclusion
As a developer, don’t rely on the apparent complexity of the ObjectID to limit unauthorized access to the objects in database fetched by their _id field when using MongoDB. It is fully guessable by passive or aggressive methods.
As usual, in the case of Insecure Direct Object References, always check if a given user has an authorization to access a specific object. Even if the ID is nearly not guessable. In the case of MongoDB, you could be surprised…