Team82 has discovered the use of a weak random number generator in Synology’s DiskStation Manager (DSM) Linux-based operating system running on the company’s network-attached storage (NAS) products
The insecure Math.random()
method was used to generate the password of the admin password for the NAS device itself.
Under some rare conditions, an attacker could leak enough information to restore the seed of the pseudorandom number generator (PRNG), reconstruct the admin password, and remotely take over the admin account.
The vulnerability, tracked as CVE-2023-2729, has been addressed by Synology. Synology’s advisory is here.
Synology DiskStation Manager (DSM) is the operating system under the hood of every Synology network-attached storage (NAS) system. DSM supports a number of functions that help the user access, manage, and share their files remotely through a centralized platform.
The DSM OS firmware comes with two pre-built Linux users: admin and guest (disabled by default).
When we install the system, the installation wizard requires us to create a new administrator-level account which is different from the built-in “admin” user. While creating the new user, the setup wizard will generate a “random” password and disable the original “admin” user account.
We found a flaw in the algorithm that generates the admin password because the password is being generated on the browser client side using the Javascript Math.Random()
function, which is not cryptographically secured.
By leaking the output of a few Math.Random() generated numbers, we were able to reconstruct the seed for the PRNG and use it to brute-force the admin password. Finally we were able to use the password to login to the admin account (after enabling it).
To execute the attack, some Math.Random values need to be leaked. One possible way to achieve this is by leaking some GUIDs (e.g. 72e14742-b0e5-4826-b7c9-eb16284fe9cd) that are also being generated using Math.Random at the first installation wizard, which means they are based on the same PRNG seed as the admin user-account password.
For example, we found that the following class name GUIDs are generated on first install and with the same Math.Random seed in the same “session” as the admin password is generated:
SYNO.SDS.PkgManApp.Instance
SYNO.SDS.AdminCenter.Application
SYNO.SDS.App.FileStation3.Instance
SYNO.SDS.HelpBrowser.Application
These GUIDs can be leaked and be used by attackers to reconstruct the seed of the PRNG of when the password was generated.
But since the GUID is generated using Math.floor(16 * Math.Random())
, additional sophistication is needed to restore the full state of the seed (see here).
In addition, since we know exactly how many Math.Random
calls we have between one of the aforementioned GUIDs and the password generation, once we have the seed state we can fully simulate the advancement of the PRNG state until we reach the password generation algorithm. This will result in a full admin password extraction.
We are starting with a factory reset to our device, so we could be faced with the installation wizard again after reboot.
Next, we are installing the DSM OS.
After a short period of time, we are faced with the new setup wizard, asking us to choose a new administrator account.
If we intercept the request, we will see that a JSON with specific user-related commands is sent to the SYNO.Core.User
API:
Set (AKA update) the admin user with new password
Create the new account and add to the administrators
As part of this procedure, the admin password is “randomly” generated by the WelcomeApp/WelcomApp.js !
password on the client side (browser). It is then sent to the NAS, and forgotten and cannot be restored.
This function is fairly simple:
First it draws a number, which will be the password length + 16
First character is a random digit
Second character is a random lower-case letter
Third character is a random uppercase letter
Forth is character is a special character
Next, it will draw the rest of the characters
And finally it will replace in place some characters, randomly too.
Here is a simplified version of the algorithm we wrote in Python:
The key issue is that the algorithm relies on the insecure Math.Random()
PRNG to generate a cryptographically random number. The PRNG is insecure because knowing the seed for Math.Random()
allows us (and others) to restore the full state of the PRNG function and predict all past/future generated numbers.
Modern browsers use the XorShift128 algorithm as their underlying pseudo-random generator to draw numbers. This algorithm is deterministic given an initial state. Therefore, based on previous research on this topic (see here), we were able to write a simple python script that uses Z3 as a symbolic execution solver, which receives some constraints and brute-forces its way to the current XorShift128
state. From there, it’s easy to just run the algorithm forward or backward to get future or past values to feed the password generation function.
The inner implementation of the XorShift128
function in each browser is a bit different, for example in how the next state is calculated, how the conversion to double works, and how the values are kept (for example, Chrome uses a cache of 64 reverse values). In our PoC, we implemented support for Chrome, Firefox, and Safari (again, most of this work was based on this research and the implementation details from V8 dev blog).
First, for testing purposes, we modified the Math.Random Javascript function to log all the generated values. We did this by overriding the Math.Random JS function to first generate the next number, print it, and only then return the value. Here is our JS code:
realrandom = Math.random
Math.random = function(){ let a1 = realrandom();console.log(a1);return a1}
This way we could debug exactly what is going on. We ran the password generation function and got the following values:
0.39728164765656415
0.05808893736455878
0.3076672729400183
0.9305761671416779
0.6532915866832478
0.178642200654346
0.4667003145378774
… (many more values) …
0.623736669967158
0.5234587422704924
0.9892502895452078
0.8339311695933369
0.11401941881097533
0.46301481070375705
0.66201529035177
Next we added a debug print for the real admin password that is being generated. We found it to be: wH~\DYx*V|AYqwgM>|01Y|WIhH|V
Now we need to see if we can reconstruct it ourselves. We generated some more math.random
values and used them as our testing point to reconstruct the seed for the PRNG:
0.2894862350439742
0.4728609250322471
0.8945401711791927
0.2303535523638034
0.9793111259581007
We used them as input seeds to our script, which is heavily based on @cyberingcc XorShift128Plus work, and generated all the past/future potential passwords.
python3 xs128p_calc_forward_backward.py chrome 200 "0.2894862350439742 0.4728609250322471 0.8945401711791927 0.2303535523638034 0.9793111259581007"
And indeed, it generated all the 200 past/future values and one of the generated values was the real password:
We enabled the admin user account and tried to login, and it worked!
Obviously, in a real life scenario the attacker will first need to leak the aforementioned GUIDs, brute force the Math.Random
state, and gain the admin password. Even after doing so, by default the builtin admin user account is disabled and most users won’t enable it (for a good reason!).
We found this vulnerability while preparing for Pwn2Own. It was a nice “aha” moment, but we quickly realized it would be almost impossible to exploit it in a real life situation. Anyway we felt it would be appropriate to write about this in order to stress the importance of using cryptographically secure Random when generating passwords.
Again, it’s important to remember that Math.random() does not provide cryptographically secure random numbers. Do not use them for anything related to security. Use the Web Crypto API instead, and more precisely the window.crypto.getRandomValues() method.
We disclosed CVE-2023-2729 to Synology, which changed the vulnerable algorithm and has pushed the fix to affected devices. DSM 7.2 is affected by the vulnerability and users are asked to upgrade to 7.2-64561 or above.
We would like to thank Synology for fixing all reported vulnerabilities.