Research by: Alexey Bukheyev and Aliaksandr Chailytko
ISPsystem panel is a well-known software with a user-friendly web interface for managing web-servers, dedicated servers, VPS (Virtual Private Servers) and billing. ISPsystem software products are utilized by hundreds of hosting providers around the world, including 1Cloud, King Servers, and Ru-Center. ISPsystem products have more than 10,000 installations:
Fig 1: Estimation of the number of ISPmanager installations.
The most famous ISPsystem products are:
ISPmanager: Web hosting & Linux server control panel (which is similar to cPanel).
BILLmanager: All-in-one web hosting billing software.
DCImanager: Dedicated server provisioning toolkit.
VMmanager: Server virtualization management software. Provided in two options: for OpenVZ and KVM virtualization type.
In this article, we describe how we found a critical security vulnerability in ISPsystem software, which allows an attacker to hijack a session of another logged-in user and take control over that user’s web-sites, virtual machines, billing data, etc. As all ISPsystem products use the same core, they are equally affected by this vulnerability.
Fortunately, ISPsystem support responded quickly to our vulnerability report, and fixed the vulnerability in version 5.178.2.
ISPsystem allows users to download and set up their software for free. To use the software, you must buy a license, but can obtain a limited trial license. We downloaded and installed an instance of ISPsystem panel for VPS management:
Fig 2: VMmanager installation.
The script sets up the necessary environment, which includes a MYSQL database server and HTTP server. By default, the HTTP server runs on the 1500 TCP port.
Fig 3: VMmanager HTTP server process.
After completing the necessary setup steps, we can access the panel via a web browser. This is the login screen:
Fig 4: VMmanager panel login interface.
Now we can experiment locally without affecting any systems of the hosting provider.
Let’s look into the authentication process. Authentication is performed using the following HTTP POST request:
Fig 5: VMmanager HTTP authentication request.
As a result of successful authentication, the server sets the session cookie, a unique string that is saved in a web-browser’s storage. The session cookie enables the system to identify the user without requesting his username and password each time. The name of the session cookie depends on the product name and consists of two parts: product name (like “vmmgr” or “vemgr”) and “ses5”. In our case, the session cookie name is “vmmgrses5”. Another cookie, “vmmgrlang5”, is used to select a template and language for the user interface.
As we can see, the value of the cookie is a HEX encoded string of 6 bytes (the string always contains 12 characters that are [0-9a-z]). The cookie expiration date is set to one year in the future.
Therefore, the attacker only needs to pick the correct 6 byte value to hijack another user’s valid session. There are only 2566 possible combinations for the session identifier.
Predictable Session Identifier Vulnerability
Actually, the number 2566is still quite huge for a remote brute force attack. Therefore, we decided to see if it is possible to decrease the range of values.
To do this, we need to know the session cookie generation algorithm.
The business logic is implemented in C++. In particular, actions such as database operations, user authentication, and user’s session management are performed by the C++ code which is executed in the context of the ihttpd process.
As we needed to find where the cookie session is generated, we looked for the text string “ses5” in the binaries, and found it in the following libraries:
After a brief review of the files mentioned above, we found that password authentication is performed by the libispapi.so library:
During the authentication process, the new object in the isp_api::Session class is created:
Fig 7: The part of decompiled authentication routine: Creating an instance of the Session class
Finally, the isp_api::Authen::Data::generate_id routine is invoked for generating the session identifier. In fact, inside this routine we can see the following code:
Fig 8: Generating the value of the session cookie.
As we mentioned above, the length of the session identifier is exactly 6 bytes. The same number is passed to the str::Random routine from the libmgr.so library. The last thing we need to find is the str::Random implementation. This function should not be confused with the library function std::rand, as they have completely different implementations.
To generate a random sequence of bytes, the rand() function is used in the str::Random method:
Fig 9: A part of str::Random implementation: Generating a random sequence.
To generate a random string of length N, the rand() function is called N times. The result of this call is assigned to a variable of “char” type. Thus, values produced by the rand() function are cropped to a length of 8 bits. At this point, we should note that the rand() function doesn’t generate truly random numbers. Instead, this function implements a pseudo-random number generation algorithm.
One important thing is that a sequence produced by a pseudo-random generator is completely determined by its initial state, also called the seed. Therefore, a sequence of pseudo-random numbers is always the same for the same seed. The picture below describes how the session cookie is produced by the str::Random method:
Figure 10: Example: session cookies generation with str::Random.
In our case, the new seed is set each time only after less than 400 pseudo-random values are generated. We should also emphasize that the seed is a 32-bit integer. Let’s look at the part of the str::Random function implementation that is responsible for setting the random seed:
Fig 11: A part of str::Random implementation: Setting the random seed.
In the pseudo-code above, the g_rnd_reset_counter variable is used to determine when the seed for a pseudo-random number generator should be updated. The initial value of g_rnd_reset_counter is set to rand()%255 + 128. That is, this variable can take values from 128 to 382. Every time the str::Random method is invoked, the length of the generated random string is subtracted from the g_rnd_reset_counter variable. When this variable becomes less or equal to 0, the seed for the generator is reset.
This implementation of str::Random makes it possible to determine the last seed value that was used for the pseudo-random generator initialization for a known session cookie. We can do that by simply looking up the session cookie in short sequences produced by the pseudo-random generator for each possible seed value.
Fig 12: Explanation of the seed lookup process.
If we know the session cookie, we can figure out the seed value of the generator, and predict the entire sequence produced by the generator for this seed.
Therefore, if another user logs in after we got the session cookie, we can get his session cookie using a brute-force attack with a maximum of 382 attempts. A similar situation arises if a user is logged in right before we got our session cookie. Both situations are shown on the next picture:
In a possible attack scenario, the attacker follows these steps:
Once in a time period “T”, log in using a valid username and password, and save the value of the assigned session cookie.
Use the rand function to generate arrays of 382 byte values for all seeds from 0 to 232 and search the saved session cookie sequences in the generated arrays (it should be emphasized that the same exact implementation of the rand function must be used as in the attacked system).
Extract all 6-byte sub-sequences from the 382-byte arrays for the seeds, where the known session cookies are found.
Try to log in with the session cookie using all extracted 6-bytes sub-sequences.
If another user logs in during the attack, his session will be hijacked.
The time period “T” should be small enough to ensure that we get at least one session cookie for each new seed used by the generator. The optimal value for “T” depends on the time of day and the number of active users. We also need to take into account that rand() is called from other places as well. Therefore, if there are many active users, the period T should be reduced.
For this case, seed lookup by the 6-bytes sequence takes at most about 20 minutes on a 16-core CPU, and this operation can easily be scaled to achieve any required speed. You can also pre-generate all 232 sequences and store them in a database. It requires about 1.5 TB of space to store all the generated data. Therefore, there are several ways to determine the seed by a known generated sequence in real time. After acquiring the seed and the sequence of bytes, all 6-byte sub-sequences should be applied as the possible session cookie.
You can also calculate offsets of known session cookies from the start of the calculated sequence. If there are no logged in users, the distance between the offsets is constant. If another user logs in, the distance increases.
Attack Proof of Concept
The first step of the attack was implemented using Python and the urllib2 library. The following options are used to log into the attacked panel:
Fig 14: Exploit PoC: Creating login request and setting up an HTTPS connection (Python)
The session identifier is acquired from the “vemgrses5” cookie:
Fig 15: Exploit PoC: Retrieving session cookie value (Python)
First, we executed the script to check how often rand() is queried when the single log in operation is performed and there are no other active users.
To figure out the seed value suitable for the session identifiers acquired by the script, the following C-code may be used:
Fig 16: Exploit PoC: generating a sequence of random numbers with specific seed (C, Linux)
The code above sets the specified seed and generates a pseudo-random sequence in which the known session cookie should be searched. The functions srandom_r() and random_r() are used instead of srand and rand to enable this code to run in parallel in several threads.
The next table shows the results we obtained:
Fig 17: Found session identifiers in the generated sequence and their offsets.
The column Session cookie contains “vmmgrses5” identifiers returned by the server for each successful log in attempt. The column Found seed contains the values of seeds that we found for the corresponding session cookie. All the session cookies belong to the pseudo-random sequence generated with the same seed. The column Offset shows the offset from the beginning of the pseudo random sequence. The last column shows the distance between the neighbor session identifiers in the random sequence.
As we can see, the distance between offsets of acquired session cookies (6-byte arrays) in a generated sequence is always 14. This means that for each successful log in attempt, rand() is normally called 14 times: 6 times to generate the session cookie string, and 8 times from other places. Therefore, if the distance becomes more than 14, there is another active user.
During the script execution we performed our log in attempt using a browser. The server assigned the vemgrses5=9e723afa5922 cookie to our session:
Fig 18: Header of HTTP response from a VMmanager server.
Next, we show that this session cookie is also predictable.
For the session cookies acquired by the script, we got the following results during the experiment:
Fig 19: Demo: Seeds lookup result for known session identifiers
The distance between the offsets in the time period from 10:57:56 to 10:58:17 has increased. This time coincides with the time of user activity. Therefore, by checking the distance, we are able to determine the other user’s activity on the website.
Let’s look at the pseudo-random sequence generated with the found seed 0x747777E4, which was used at that period:
As we can see, the user’s session cookie exists in the generated sequence. The known values acquired by the script are highlighted in green; the value we are predicting is highlighted in red.
The last thing we need to do to finalize the attack is brute-force all 6-byte subsequences of the generated sequence between the known values where the distance is more than 14:
Fig 21: Demo: Data for brute-force attack
In this case, there are only 66 values to be checked to find the correct value of another user’s session cookie.
Finally, because the server with default settings does not match the IP address of a remote host with the session cookie, we are able to invade another user’s session using the following request with the stolen session ID:
Fig 22: Demo: Request for hijacking the session
As we can see, the attack can be easily implemented by an individual with a reasonable amount of resources.
The full list of affected ISPsystem products:
If you are using any of the listed ISPsystem products with a core version under 5.178.2, we recommend that you upgrade as soon as possible.
Check Point’s IPS signature that protect against these vulnerabilities is “ISPsystem COREmanager Authentication Bypass“.