foundryvtt unauthenticated rce part3/3 - bypassing auth with maffs
FYI: THIS HAS BEEN FIXED IN FOUNDRY 0.7.10+ AND 0.8.2+. So please update your foundry instance!
This is the final post on my foundry exploit chain. In this post i’ll talk about how i found a way to become an admin from the context of 0 prior authentication.
The admin login
What does being an admin mean in foundry?
When you authenticate to foundry with your adminkey, your session will be
marked as being an admin
session.
Getting more into the technical details: upon providing your key,
auth.js::authenticateAdmin
will validate your key against the
pbkdf2 sha512
hashed key created by the server owner.
This key is not stored in the database like the other passwords but rather
configured as either a commandline option (--adminKey
) or in the
Configuration Menu
within the software itself (then stored in an admin.txt).
When this check
|
|
is sucessfull, the current users session is getting its admin
flag set to
true.
sess.admin = success;
I was thinking about a timing attack at first but even if that works id only get the hashed password so i didn’t look into that. would still be a good idea to use a constant time comparison here. After staring at the authentication method for a bit i couldn’t find a way to break it. But what about the session itself? If there was a way to get into the session of an admin, that would work just as well wouldn’t it?
Sessions
This is how prettymuch every session authentication system works:
When you first request the website, a token is getting generated and used as a
browser cookie.
So in every subsequent request this token is sent by your browser and the
server will map that to your session.
Foundry internally calls those tokens rids
So how are rids generated?
auth.js::_create
const rid = util.randomID(24);
A 24 character randomID created by common/utils::randomID
:
|
|
What stands out right off the bat is the use of Math.random()
.
To some people this may look like an easy win, it certainly did to me at the
time because pseudorandom number generators
are prone to being predictable.
But it really, really isn’t that easy.
Let me explain what the fuck is going on.
First, were defining a method, lets call it loc_0
.
The method generates a pseudorandom number between 0.0 and 1.0 with Math.random()
They look like this:
0.21315554681349425
Next, were converting this number to a string.
The toString
parameter takes the base of the numbering system we want.
2 for binary, 16 for hex, 10 for decimal and so on.
Base36 is yet another numbering system
/ encoding.
To quote wikipedia:
The choice of 36 is convenient in that the digits can be represented using the Arabic numerals 0–9 and the Latin letters A–Z (the ISO basic Latin alphabet).
Base36 encoding a foating point value (a double to be specific) is a very
weird and imprecise process and decoding is even worse.
I’ll tell you a fun fact: calling .toString(36)
on some doubles will yield
different results on chrome and firefox; and sometimes 2 doubles will result in
the same string.
Anyways; because doing Math.random().toString(36)
:
0.7o8zgt4gqzs
still has the 0.
prefix, the method gets rid (rid haha funny) of the first 2 characters using substr(2)
.
So loc_0
will generate something like 7o8zgt4gqzs
.
The code repeatedly generates these strings and appends them until the combined
string has more characters or the same amount of characters than we requested
in arg_0
.
Then, incase we generated more character than we need, they get chopped of at
the end with lob_1.substr(0, arg_0)
.
With arg_0
being 24 in the token generation (session rids are 24 chars long) we end up with something like:
f5gaf57r1lcatwjqm5fbwguj
In theory, the gameplan is:
- find out where one
loc_0
iteration ends and the next one starts (seperate the different b36 strings) - decode the base36 into the original numbers
- reverse the prng seed from the numbers and generate our own tokens
The first 2 parts are actually impossible (or maybe my brain is to smoll); but sometimes being close enough is actually just as good ;).
Before we go on
Before we go on id like to give a disclaimer. The rest of this post will be concerned with algorithms and maffs n shit and well … i fucking suck at all of it. So feel free to shout at me for my small brain methodology. Seriously, feel free to educate me on my twitter, the reddit thread, discord or wherever; im really curious if there is a better way to approach this. Let us move on, shall we?
Base36
I chose the single base36 decode as my first challange. My thinking was that gaining a better understanding of b36 will help me with the first challange (splitting the combined b36 into the individual componments). So first i was like,
there is a b36 encode so let’s just call the b36 decode function
Yeah nice meme, there is none. I took a look at some base36 implementations in python, some documentation blah blah, just gaining a basic understanding. The basic theory is:
- define a map of the possible characters as an array (0123456789abcdefghijklmnopqrstuvwxyz)
- index into this array with the number we want to encode (x) MOD 36 (x % 36)
- divide x by 36 (x /= 36)
- repeat until number is 0
or in programming terms:
|
|
NodeJS uses V8 (the chrome js engine); so how does v8 do this? Issa wild ride. I’ll only show parts of the code because that thing is fucking massive At first they split the integer from the fraction:
|
|
Given the 52 fraction bits in a double we have a pretty large set of values between 0 and 1. Because of this, the chances of getting a 1 are next to impossible so im just gonna ignore that as an option alltogether and only look at the fraction part of b36 encoding.
The fraction part starts with getting half of the difference between the current and next
higher (possible) double value. aka delta
:
|
|
What does this accomplish? I don’t know leave me alone. Simmilar to the previously described encoding method, this is what happens next:
|
|
So far this is actually pretty similar to the division method but what do we
need delta
for?
Im just gonna throw this next part at you because sorry but there’s no way im able to explain this:
|
|
This goes on while (fraction >= delta);
.
I implemented the relevant parts in python with numpy (you don’t actually need to understand much of this):
|
|
If this looks like i know what im doing you’re wrong, i just took the original function and reimplemented it in python, gaining a little bit of understanding about how it works. Cool, now we have a functioning b36 encode function, let’s think about decoding this thing. Remember, the most basic form of the algo is (without delta n shit):
|
|
d
is the input and x
is the index into the 0-9a-z (alpha
) table.
We can easily get x
from any character in the b36 string with:
alpha.index(character)
which just means get the index of element "character"
in python.
easy enough; what about x
to d
tho.
I bet there is a facy maffs way of solving this but heres how i did it:
adding a bunch of print statements to look at the numbers at different points.
Stairing at those numbers for a bit and trying some things like dividing
numbers by 36 and adding number together until i just noticed a pattern.
To be totally honest, i don’t entirely understand how my solution works i just
instinctively did it.
Imagine this first iteration:
|
|
After talking to my girlfriend (love you),
i think it makes a bit more sense to me now.
Lemme try to explain.
I’ll use x
instead of d
in the below formulars.
Consider this set of conditions:
|
|
We can expand this into a single formular by inserting the previous formular for
the unknowns x
:
|
|
Or, the simplified form:
|
|
Or as a recursive function:
|
|
I don’t know how to notate stopping conditions but it would be if n > 0
.
Anyway; whats difficult about this is that we need to solve for floor(36n)
.
We know that f(n) = 0
because that’s the stop condition.
I could’ve used a theorem prover for this but floor is kinda painful to implement and i found this solution (assuming input is already converted with indexof alpha):
|
|
Or, a bit more elegant and with numpy and indexof applied (full solution):
|
|
We actually need the numpy stuff because python doesnt have sized number types but we need to be able to overflow the numbers. This neat little function will accurately decode v8s base36 encode.
Why does this work?
- Choose a number between 0 and 1 (exclusive); let’s say 0.7.
- Multiply that by 36; 0.7 * 36 = 25.2.
- Divide the integral part of the result by 36; 25 / 36 = 0.6944
Notice how thats pretty damn close to the original number? Floor looses alot of accuracy, but we can get that back if we have multiple related floored values.
What about this delta guy
Remember, so far we only focused on the basic algorythm without all the delta stuff. Interestingly, im pretty sure we can’t really account for that. Because we’re approximating the original value we don’t know the accurate intermediate values and thus can’t use those to do the same check as the encoder does. I mean maybe there is a way and im just not smart enough but it doesnt matter because as i said before: base36 encode is ambiguous. Paste this into the chrome console and see for yourself:
0.5635652429338151.toString(36) == 0.5635652429338150.toString(36)
And it really isnt hard to find those numbers either, all i did was generate a random number and play with the last digit. Now is this ambiguity because of the encoder, the delta stuff or the way doubles work in c? I have no clue. Another interesting thing is that firefox uses a simpler algorythm.
Firefox: 0.5635652429338151.toString(36) == “0.kadp7602flc”
Chrome: 0.5635652429338151.toString(36) == “0.kadp7602fla”
Anyways. Enough rambling about floats. We have a decently reliable b36decode function, the next challenge is decoding concatinated b36 strings.
Where to split?
Remember, the algo does Math.random().toString(36)
until it generated 24 or
more characters, then takes the first 24 only.
If you thought i had no idea what im doing in the last segment, wait till you
read this :p.
I was playing around with my new decode function.
Testing how accurate it was, just manipulating some characters n stuff.
Just decoding some random double
b36dec(‘kadp7602fln’) -> 0.5635652429338152
ok cool
b36dec(‘kadp7602flna’) -> 0.5635652429338152
wait what??
b36dec(‘kadp7602flnabc’) -> 0.5635652429338152
excuse me???
b36dec(‘kadp7602flnthefuckisgoingon’) -> 0.5635652429338152
If you haven’t noticed already: b36dec(‘kadp7602flna’) == b36dec(‘kadp7602flnthefuckisgoingon’). Nothing that comes after the encoded value matters at all! Why does this matter? Well if multiple b36 strings are getting concatinated, i can actually decode the first number by just decoding the whole concatinated thing. Because nothing after the first number matters! I can then reencode that first number, which tells me which part of the concatinated string contains the first number. That first number is removed and i move on with the rest of the string. Kinda cool right? If i had to guess why this works id say it’s a side effect on the approximation based method i used for decoding. The more characters we have the less it matters what we start with; it’s kinda self correcting. Or maybe it’s just because the float is always overflowing at a certain point? Let’s just say it’s the wizard jizz reality seems to be made of; what do i know, im a school dropout eating pizza in my basement appartment. Getting a small b36 string (like 0.5) would obviously stop this from working but that wont happen. With 52 bits between 0 and 1, the chances of getting a small fraction like .5 are abysmal. What about the last b36 string tho? Since the whole thing gets cut off at 24 characters, we dont get the last one. Do we ignore that? Yeah let’s ignore the last part for now and test this thing. Just a few manual tests confirm this isn’t very precise. So i get another idea. Sometimes the decoder is influenced a tiny bit by what comes after the first string. Weirdly, this influence is often times not big enough that it would prompt the encoder to generate an additional character (just a wrong one). So what if we just use the decoder to determine the length of the first string. This is what i thought of:
- decode whole string into first float
- encode first float again and get the length of that string
- decode the first LEN chars of the whole string
If that sounds insane it’s because it is. Just look at it:
|
|
While is pretty accurate, it’s still not perfect. How do i know how accurate it is?
Stonks.py
I calculated it.
In the script riddec_stonks.py
(i’ll be releasing this alongside everything
else) you can find the code.
This is what happens in the script:
- generate 100 000 rids (concatinated random b36 strings)
- decode rids into individual floats
- reinterpret float bits in memory as integers (easier to calulate with)
- calculate how many CONSECUTIVE bits are usually accurate
I called amounts of inaccurate ending bits delta bits. So delta 0 means perfectly decoded, delta 1 means last bit inaccurate and so on. This is what the first float deltas look like (a deltas):
|
|
As you can see the they’re very likely to be identical but even more likely to have only 1 endbit of inaccuracy. The b deltas look a bit different.
|
|
It reasonably likely that just 1 endbit is invalid but its not as good as the first one. Should be good enough tho. Because the b36 strings are always at a simmilar size (roughtly 8-12) and the whole thing is 24 characters, we know there will be 2-3 individual b36 strings. Most of the time there are 2 full ones and 1 scuffed one with about 2-6 chars. The deltas of the last one are obviously completely fucked so im not gonna show them. Instead ill tell you about another pattern i noticed when playing with the last string. I decoded the last part a bunch of times and looked at how many endbits are inaccurate. Interestingly, for every character the last string had, there were 4 accurate bits at the beginning. So to get the amount of inaccurate bits in the last part we can just do:
64 - (len(b36last) * 4)
Cool, we have figured out how much of our stuff is accurate.
This information is pretty essential because we only want to use the most
accurate parts.
To store this information, i made a structure called ApproxFloat
:
|
|
Equiped with this class we can finally fishish the rid decode function:
|
|
Phew, what an adventure.
Finally cracking the prng
We got the original randomly generated floats (kinda), now we just have to
crack the nodejs math.random
generator.
At the time of writing this, nodejs uses a variation of the xorshift128+
algorithm for random numbers as evident in their XorShift128 function:
|
|
If we can find the values of state0
and state1
, we can determine all
previous and future generated values.
In ToDouble
we can see that state0
is used as the random value (the output
we get). Because it’s converted to a double, we only get 52 bits of the 64bit
state0 int so theres even more dataloss.
Again, i made my own python version to gain a better understanding:
|
|
We can undo the xors but not the right and left shifts.
But because the output we get (first return value) is the result of the previous iteration (previous s0) we have
some data.
Ignoring the xors, it’s kinda just shaking around the bits while shaving off
some of the bits on the left and right edges leaving some in the middle we can
rotate around and piece together in multiple iterations.
Now we could do this manually as we did with the b36 decode but solving
xorshift128+ with a theorem prover has been done time and time again so why
reinvent the wheel?
There is so many articles talking about cracking math.random
so i wont go
into too much detail. The basic answer is just: use a theorem prover.
A theorem prover will receive some maffs/logic fomular and hand us a set of
inputs that satisfy some condition we set.
So for example we could do:
|
|
If it’s satisfyable we could then get a model with the values that satisfy those condititons. Whats important for us is that we need to tell the theorem prover to ignore the bits we believe to be inaccurate (the loss of the AproxxFloats). I do this by just masking the last n bits:
|
|
The theorem prover everyone is using is called z3 it’s just really solid and it has decent python bindings. So i implement eveything we have done so far in z3 (which just means using z3s datatypes and some special operators). Here is my z3 xorshift128+ implementation, it’s really not that special:
|
|
To add the conditions, i made an add_z3
function:
|
|
Then we just tell z3 to solve for a handful of foundry rids (or tokens as i called em here):
|
|
Aaaaaand we get the seed s0 and s1 :). Let’s do something with em!
Using the seed
You might think we need to run xorshift in reverse to get the previous tokens but nope. The V8 engine has a random number cache. It first generates 1000 random numbers, puts them on a stack and when someone requests a number, it gets popped off the stack until it’s empty. A side effect of this is that numbers get returned in the opposite order they were generated. Thats why you see
for d in reversed(dec_rid(t)):
in the previous code. I always request 3 tokens. 2 to get the seed and the third to double check that we got it correctly.
|
|
Then i generate the 64 last tokens and check each one for the admin flag. If a game is running and the session is associated with a user, i also dump the user database with the exploit from my previous post.
|
|
POC
Link to the poc exploit. To install the dependencies, just do:
pip install -r requirements.txt
You will need python3. The poc is super easy to use. You just tell it the foundry url and it does everything else for you.
|
|
As you can see, the admin session has a little admin!
in front of it :).
The other sessions it found are actually the sessions it generated itself.
We can just take that rid and use it as a cookie value to impersonate that
session, or feed it into the zip exploit.
Resolution
- 07.02.2021 created confidential gitlab issue with all the problems i found
- 08.02.2021 got issued 3 foundry keys as a thank you
- 17.02.2021 fixed in 0.8.0 alpha update
- 20.05.2021 fixes backported to 0.7.10 update
Comments
Im always open to talk about this stuff. Feel free to talk to me on
2021-06-03 12:56:14 +0200 +0200