foundryvtt unauthenticated rce part2/3 - dumping creds with facs n' logic
FYI: THIS HAS BEEN FIXED IN FOUNDRY 0.7.10+ AND 0.8.2+. So please update your foundry instance!
This is only part 2 of a 3 part series of blog posts about getting unauthenticated remote code execution in foundryVTT. The last post talked about writing arbitrary files given an authenticated admin session. So is this the post i’ll surely talk about the auth bypass to become admin, right? Nope, not yet :p. I promise the next post will be about the super interesting auth bypass but i really want to get something else out of the way first: a weird bug allowing us to dump all users (and gm’s) passwords given an authenticated user by just asking nicely.
Baby steps
So after finding the previous exploit, i was looking for a way to bypass authentication. The first thing i thought about was checking if there was some sort of database injection i could use to dump passwords.
But catnip
i hear you ask,
the adminkey isn’t even stored in the database
But there is one little thing you need to understand. And that is of course, the fact that i am an idiot sandwitch. Yes, i didn’t even think about that at the time. The admin key is not stored anywhere in any database so this exploit get’s us no further in becomming an admin. On the bright side of things, that means this post now has 3 parts instead of 2 :p.
Querying data
How do players even get data from the foundry database?
I noticed that when i opened a compendium
(just some sort of ingame documentation don’t worry about it), the following websocket request
showed up:
[“modifyCompendium”,{“type”:“pf1.bestiary_3”,“action”:“get”,“data”:{},“options”:{“returnType”:“index”}}]
Interesting; they’re using the modifyCompendium
request but with get
as the
action to fetch the compendium data from the server.
So who’s processing this websocket request?
sockets.js:activate
:
|
|
This function assigns a handler function to each of the different types of websocket requests. To be more specific, this function calls other functions which assign those handlers to the websocket requests. Im guessing compendium websocket handlers get initialized in
compendium_1.default.socketListeners(ctx, handleEvent);
so let’s go there and see.
|
|
Well that was easy enough.
Seems like our modifyCompendium request will be processed within this._onModifyDocument
.
Wait but that function doesn’t exist?
class BaseCompendium extends document_1.default { …
Ooooooh, riiiight.
So the this._onModifyDocument
method is inheritet from the parent class
document_1.default
.
AKA
const document_1 = __importDefault(require(’../odm/document’));
So once agaaaain we go there and find the implementation: database/odm/document.js:_onModifyDocument
|
|
First it’s getting a table of the type we provided, then it just calls _onGet
on that table because the action
we provided was get
.
Sounds easy enough.
What does _onGet
do tho and can we attack it?
database/documents/compendium.js:_onGet
:
|
|
Ok so this.find
just searches the database table for some search criteria
we dont have much control over.
Every class (that inherits fromDocument
) has its own datastore, which is its own database (kinda).
So instead of tables we have datastores.
This means we cant just do some sort of injection to query another table (like the users table) inside the get method of our compendium; and dumping the compendiums datastore wouldnt be all that interesting would it?
What database are they actualy using?
const {datastores, NeDBDatastore} = require(’./odm/datastore’);
AHA, nedb an open-source javascript database not updated in over 5 years.
That does sound pretty bad but i couldn’t find any obvious netdb problems at
first glance. At this point i realize i need to go back a step. Ive analyzed the compendiums
onGet
method, but what about the others?
|
|
Damn. There aren’t many of em to exploit.
But 3 is still more than one.
database/odm/document.js:_onGet
:
|
|
Ok this one seems pretty blank; standart you might say. It just filters by the data we provide. I mean it makes sense, afterall document
is prolly the baseclass.
Wait. What.
Remember that the compendium is using document
as it’s base class?
Judging by the name, i feel like document is the baseclass for more than just
the compendium, right? Well now i don’t even care about whos implementing (more specifically overriding)
_onGet
. Quite the opposite actually; i want to know who doesn’t.
Finding the vuln
Every class that inherits document
but doesn’t have it’s own _onGet
will
inherit document:_onGet
, a method that returns the whole dataset no questions
asked. We already know that only 2 other classes implement _onGet
so what
inherits from document?
|
|
We know about the compendium, but entity sounds like another baseclass. Another round of grepping aaand
|
|
Oh. Oh my. Seems like 2 is the lucky number today.
After checking i realize user.js
is exactly what it sounds like.
Its got the role, the permissions, the name, color, character, password.
And because it inherits from Entity
and entity inherits from Document
it
indirectly inherits the _onGet
method that just yeets everything into
my arms.
Doing a quick test indeed confirms that:
[“modifyDocument”,{“type”:“User”,“action”:“get”,“data”:{}}]
leads to
|
|
Ah myes. Excellent.
You may be wondering why i can even see the passwords.
The answer is: plaintext passwords.
There is an ongoing issue discussing this problem
as some people are understandably not happy with the way foundry handles their access keys
.
Interestingly, in contrast to the users access keys, the adminKey
is actually
hashed and not stored anywhere near the database but more on that in the next
post.
I think this could be considered a logic bug? But honestly all i did was ask the server nicely and it just gave me the passwords.
POC or GTFO
Ok listen. I used this exploit as part of the full admin auth bypass i’ll be releasing next post. So i just quickly copy pasted some code from that exploit into a standalone POC. You use it like this:
python checksess.py <foundry url> <any user session whatsoever>
here’s n example:
|
|
You can see the session in your browser in any request in the f12 network tab. In the request headers there will be a cookie like:
Cookie session=a57md90in5eccr0jdi3q8c15
Since (as previously mentioned) this is part of a bigger exploit, i wont put the standalone on github. Instead, here you go: (note that it depends on the socketio library 4.6.1). The version is important because it needs to match up with foundries socketio version.
|
|
2021-06-01 11:01:24 +0200 +0200