Ayo. Im back with another virtual tabletop vulnerability post :). This time were taking a look at my favorite vtt platform foundryvtt. I’ll be making a series of posts covering the vulnerabilities i found within the software. Most notably an authentication bypass leading into an arbitrary file write. This first part will be about how i found a way to overwrite arbitrary directories on the target filesystem along with a small information leak at the end. The next post will be about how i managed to dump all the other users credentials as a regular user. Im saving the most interesting part for last (full admin authentication bypass) as thats by far the hardest to explain. With the posts i’ll also release the relevant exploits i wrote.
Foundry is a software you can use to host online tabletop games. A license costs about 50$ and like most paid software, it’s proprietary. When you purchase the software, you’ll get a server written in nodejs along with a guide on how to get your friends to join. As it’s fully self hosted, it’s reasonable to assume most people will be running this server on their personal computer.
So because we host the server, we have the servers code. Neat. Only problem: it’s obfuscated. So how do we make the code readable? We unobfuscate it. At the time i had no idea what obfuscator they used and i couldn’t find any public unobfuscator that works, so i made my own. Later on i found out (by asking the creator) that they’re actually using a public obfuscator but hey, now i know how to make my own js unobfuscator. I wont go into detail on how i made it because it’s really not that interesting. You can go look at the code if you want :).
Also a heads up: many variables and stuff in this post are named by me, i don’t actually have the original code.
How i found the first (and simplest) bug
First thing i did when i got the code was look for obvious fuckups / low hanging fruit. From using foundry alot i knew that it had lots of file uploads and because those tend to be problematic i was searching the sauce code for all file uploads i could find. Looking through the code it was clear to me that the foundry devs are not entirely ignorant about their programs security. There were alot of checks to make sure you couldn’t escape the directory foundry wants you to stay in for reading and writing files. But if you look hard enough you WILL find something that’s just how the game is played.
So what did i find?
Arbitrary directory overwrite
A way to overwrite arbitrary directories via the module installation process.
Installing modules in foundry
First i’ll quickly go over what modules are and how they’re installed in foundry. Modules are just plugins to extend the software. When a server admin installs them, they’ll just get hosted as files. The only code they’re allowed to have is js code executed on the players browsers. By installing an arbitrary module, there is no risk of code execution in of itself unless you happen to have a 0-day for the browser the players are using.
Installing them is done with a POST request to the /setup page. The request body has a JSON like this:
So esentially the only thing we do is direct foundry to a url where a manifest file is hosted. The manifests are json too. This is what they look like:
Now we know what data the server receives but the way more interesting question is what the server does with this data.
Discovering the bug
The post request is processed in
I condensed this method to the most relevant parts only.
First we get a
(thats what i put as the
type in my earler POST request).
This just means
loc_0 will be the module declared inside
There are different package types but theyr’e all just plugins for the software,
don’t worry about it too much.
The module inherits its
install function from
package.js is actually the file we’d be interested in.
After getting the type mapping, we “check the package” (line 6).
This will go
fetch the json from the manifest url we gave it:
and also does some stuff like checking if all the required fields are present.
This means the
pck variable is our json manifest.
The code then passes the download url and the name from inside our manifest to
loc_0.install (line 14), this is where the magic happens.
Can you spot the error?
This function just assumes that the
name parameter is
trusted but its completely user supplied and unchecked.
Remember that the name parameter is comming directly out of the
name field in
our json manifest.
const path_zip = path.join(this.baseDir, name + ‘.zip’);
If we supply
../../../../../../tmp/x as the module name inside the manifest,
path_zip would be
Ok we can write files anywhere on the system but they have to end with
.zip and also theyre deleted immediately after (line 48).
While we could probably make this exploitable on linux by finding a way to
crash the function before the file is deleted and abuse the fact linux doesn’t really care
about file extensions, there is a better way.
path_noext doesn’t append
.zip so we completely control that path.
const path_noext = path.join(this.baseDir, name);
The only 2 places this variable is used are:
First it removes the path and then it extracts the zip there. Which means we can replace an arbitrary directory with the contents of our zip. On windows we can simply put an exe in one of the many autostart dirs. On linux we can replace the .ssh dir or some motd or something else entirely. Going from arbitrary file writing to code execution is outside the scope of this post but it’s very far from impossible, especially in this scenario of people self hosting a server on their personal desktop.
I wrote a small POC exploit that works like this:
- zip up the
- make a module.json where the name field contains the target path ex:
- make a post request to /setup with action: installPackage and the manifest pointing to our evil module.json
This is how you use it:
- get an admin session with my other exploit (releasing soon TM)
- put some malicious data in the
- run exploit against target:
python ziphaxx.py http://localhost:30000
- start http server in out dir:
python -m http.server 1337
- specify target dir: ../kektop
The exploit will also guide you through those steps.
I’ll talk about getting the admin session soon but for now let’s not worry about that. I have another thing that makes this exploit just a bit more reliable.
A helping hand
While looking for a way to bypass authentication, i stumbled upon a small infoleak. Foundry uses WebSocket for most of the communication. On the node server, every incomming socket gets initialized with some handlers for certain api calls. But there are 2 handlers which get connected before the server checks if the client is even authenticated.
Eveyone who has an http connection, has a session so
if (!sess) return;
if (!usr) return; is the real show stopper.
But before that we have
getSetupData is a pretty cool data leak because it get’s us the absolute path
where foundry stores it’s config.
By default this is within the users home directory so it leaks the username
This is what we get:
And before you get too excited, no this doesn’t leak the adminKey, but it does tell us if there is an adminKey set. As it turns out foundry doesn’t even force you to set an admin key.
2021-02-16 20:39:33 +0100 CET