← All talks

Vesta Admin Takeover — Exploiting Reduced Seed Entropy in Bash $RANDOM

BSides Exeter · 202638:176 viewsPublished 2026-05Watch on YouTube ↗
Speakers
Tags
About this talk
Adrian Tiron demonstrates a critical vulnerability in Vesta control panel's password reset token generation, which relies on bash's $RANDOM variable with severely reduced entropy. By analyzing how bash seeds its RNG from process ID, microseconds, and timestamp—where only the lowest 20 bits of the timestamp vary—the attacker reduces the brute-force space to roughly 95 million attempts across a three-year window, enabling remote admin account takeover.
Show transcript [en]

being bashandom. If it's a bit confusing, don't worry about it. I will explain everything in the next half an hour. A couple of words about myself. So, I'm a co-founder and principal consultant at Forbridge. Uh forbage is a cyber security consultancy uh which I co-ounded with my brother during the pandemic because we were bored and uh also we were not very happy with the results that we were getting uh when we had to engage third party companies uh for pentesting purposes. I got a couple of certifications as you can see there. My favorite ones are from offensive security and I've also been doing quite a bit of public speaking in the past two three years. Uh what you

see there is just a small list of conferences like blue hat organized by Microsoft. Uh various besides conferences pass the salt in France besides Kent besides Budapest. I spoke at both of them last year and for some reason they invited me again this year to speak at both besides Budapest and uh besides Dresd and Besides Kent. So next week I will be at besides Budapest in case you're in Budapest come to my uh new talk and in case uh you are at besides Kent uh in May on the 8th of May I will be giving a talk there about LLM uh some LLM red teaming uh research that I did recently. So what is this talk about? It's about

application security. Uh how many people interested in application security offensive security? Okay, cool. So I think you're going to love it. Um what I will be attacking today password reset tokens because this is one of my favorite things to attack and it's also part of the unauthenticated attack surface of any application. Right? So that is the most interesting attack surface from an an attacker's perspective because I don't need any credentials and I can just go and find a nice vulnerability there. And this is something that I will show today. uh in case you're familiar with OS then you probably heard about it right this was uh considered on the second place which is a cryptographic failure right because

essentially as you probably know when you generate a password reset token it needs to be cryptosecure obviously as you can probably imagine this was not cryptosecure this was pretty obvious right you will see that the question was how how did I turn it into a practical exploit it. This is what we're going to see and this is what we're going to talk about today. A couple of words about Vesta control panel. First of all, it's a web-based control panel. It's written in PHP. It's similar to other ones like C panel, VHMK that you probably heard about it and we did research on all of them previously and found some nice vulnerability in all of them. You can find some details on our

blog. It's pretty lightweight in structure. The way it's architected, it's a PHP for the front end and the PHP API calls some bash scripts which do all the heavy lifting for managing domains, databases, DNS chrons backups etc. Um, around the time we reported this critical issue to them, they were acquired by a company called Outlaw. Just as an FYI, this is how it looks like, right? Pretty standard stuff. Exactly what you'd expect. You can manage your entire server with something like this. Okay. Now, the source code I discovered it was available on GitHub. So, I was like, okay, cool. Let's do white box testing because it makes sense, right? Uh the code is pretty straightforward to read.

There's no complicated frameworks. There aren't multiple abstraction layers like it is in modern frameworks. No, nothing object-oriented or anything like that. Uh there were a couple of issues reported previously. Uh because obviously I Googled on CV Mitri what was previously reported right and there are some it was pretty obvious to me that the trivial bugs have already been discovered and fixed like all the command injection argument injection all stuff like this have been previously uh reported and fixed before discussing the main issue that I u exploited let's have a look at a couple of dodgy patterns just just to show you how the process uh worked. First of all, I discovered the first dodge code pattern. There was a PHP

script called add chron and I looked at it. Okay. Uh and I thought, hm, maybe I could find some sort of command injection. Plus, maybe I can add a chrome, a malicious chrome. It gives me a reverse shell or something like that back. I thought that would be nice. Now if you look at the top there's no explicit authentication check. Uh it only checks if the request is a post. Uh there's a basic CSRF check here. It checks the token from the session if it's equal with the token from the post variable. Um so they check for CSRF and then if you look uh which is a positive thing for developers they use the function escape shell arg right. So

basically they sanitize all the arguments that they receive uh from input. So this is a very positive thing from a development perspective right they they are aware of certain issues and then I looked at the bottom and it calls um the function exec and you'll notice that all these variables from post are escaped escape except the parameter called user. So I thought okay they forgot one parameter user. Maybe I can inject something into the user parameter. Would this be possible? So I looked I started grapping reading code and I discovered um okay so user comes from session. Now session as you know it's a predefined array in PHP right? So I thought maybe I can control

maybe I can find some sort of uh session pollution uh functionality that would allow me to control the user variable and this way I could inject into that exact command. Unfortunately I wasn't able to bypass it. Uh but overall I thought the the whole thing with the session pollution attempt I thought that this would would have been really cool. So I I was pretty happy with myself uh for thinking of this idea. Now uh the other code pattern which was also very interesting was in a script used to generate uh TLS certificates. Again at the top there is no authentication check. So what does this mean in practice? I could simply trigger this PHP script and I could

generate a lot of TLS self-signed TLS certificates on the server. So I could generate as many as I wanted. Uh this would have probably been like a very very slow denial of service attack uh by filling out the uh hard disk. Again, not that interesting, but you know, it was just a possibility really. And then again, I noticed they were using the escape shell arguer.

So I had another idea like maybe I can find a zero day in the function escape shell arg. So I started fuzzing. I started fuzzing the li with using lib fuzzer. I started fuzzing escape shell arg and then I also wrote a PHP fuzzer. I thought maybe I can find some way to escape uh the string using some sort of encoding tricks like UTF8 tricks and stuff like that. Again, this proved to be unsuccessful. Now, as I mentioned in the beginning, the next thing I was like, okay, cool. Let me look at the password reset process. Let's see. And this is how the email looks like when you try to reset the password of the admin. So, you receive a

an email with a link containing your username, user equals admin, and the code. You can see the code here. It's like 10 10 digits or 10 characters long. So I was like interesting. How is this how is this code generated? Now that is the question. So I started reviewing the PHP code and I found this uh reset/index PHP code. Essentially when you're uh submitting the code what it does is it tries to read the local file which is a JSON file. It parses it with JSON decode. All right. And what it does it reads from that file a field called R key which it compares against the code that you just submitted. So basically it checks do you have the

right code to reset the password for this user and if you do then it calls another script at the bottom here highlighted which which is called change user password. So this is actually will allow you to change the password for that user. Now this is how the local file looks like right. Every uh user on the system has a file like this. At the bottom you can see this R key right. This R key which is stored locally needs to be equal with what you received in the email. And if you have this R key correct it will allow you to change the password. Okay. So what do we understand from here? This R key code is pre-generated right.

So first it's generated at installation time and every time you do a successful password is set a new AR code will be generated and it will be stored in this file. Okay. So basically in order to change it you have to know it only on successful password is set it changes. That's it. If you haven't completed the password reset successfully, you'll get the same code every time. Now, uh let's look at the uh v change user password script which actually does the password change. So, as I mentioned all the PHP API calls into bash, right? And what this does once again it changes the current password, right? which is the R key and again it generates a new R key

code which will be used next time for the next password reset that the user will try to do. So oh and uh you will see here in this slide that the way R key is generated it's generated using the generate password function. This generate password function is used everywhere in the CMS uh every time they need to generate like a secret a password a token or whatever that needs to be secret. So generate password is another bash function. Let's look at it. Let's see how it looks like. So this is the generate uh password function. All right. As you can see here, it um uses as default character set all the alpha numeric characters, right? It generates a string of length

10, right? So it starts from zero. It generates one character at a time and then the most important thing here is that it uses the random environment variable in bash. It uses this random variable. it then to generate a number between 0 and 32,767 it uses the modulo operator right in order to generate an index within the length of this string that we see at the top right so it does this 10 times it generates one character at a time so the question is here uh how does this random variable work in bash. Is it crypto secure or is it not? Because this is the most important thing when generating the password or the password

reset token of a user. So I did some googling because I wasn't aware and I discovered that there was a gentleman called Han Walter I believe who already did research on the random variable and he discovered that it's not cryptosecure it's actually podor random everything is deterministic and predictable and he created this um project called bash random cracker now if you look at it essentially here I'm giving it free random uh variables right so essentially generating free random numbers and what it does for me is it discovers it brute forces the seed that was used to generate these free random numbers and it found this seed in this case it's 2 billion 137 million something like that

and then it also predicts the next free random numbers that would be generated did and then I validate this myself by doing an echo and echoing echoing three random variables. Sorry, let me go back. And if you check here, these three random numbers that I generated in bash are identical with what the tool predicted. So the tool works very well and bash is very deterministic. Another thing that we need to notice about this uh random variable, we can seed it ourselves and every time we seed it with the same input, we will always get the same random numbers. In this case, I seeded it with 1337 twice and then I generated I echoed three random

variables and every time I got the same output. You can see here at the bottom right these numbers are completely identical when seeding uh with the same seed. So deterministic and sod random. So using random variable is an obvious problem for generating anything that should be a secret. Then I had a look to the implementation of the tool. So you can see here on the right side how the seed is calculated. Uh it's nothing really complicated. Uh just basic math operations, divisions additions subtractions. There's no source of entropy or or nothing like that. And on the left side, sorry this isn't very visible. On the left side, what we can see here is that there are two versions of this algorithm

used in bash. uh one version sorry one version was before uh version 4.5 and one version was after 4.5. Um there wasn't that that much of a big difference in the end. The only thing slightly more interesting here is that Oh, thank you very much. Thank you. is that uh if a number was if the algorithm generated the same number the same number twice, it was generating a new number. So it was making sure that two consecutive numbers were never the same. Yes.

So we saw that there's an obvious issue with the random variable in bash. The next question I ask myself, okay, now I need to know how is random seeded in bash? How does bash seed uh this random number generated? Because if I know how it is seeded, then I can replicate the behavior and create an exploit. So I looked at the bash code I downloaded from GitHub sorry and I started doing some code reviews and I discovered this function which is called seed rand. This is used for seeding the the random generation and seed rand basically here where the interesting stuff really starts. It calls get time of day. Get time of day returns the the

current time stamp and the current microcond and then it calls the function SB rand where essentially it passes the current second, the current microcond and the speed of the current process and it ignores all of them. It ignores all of them and it passes to SB rand. Now SB rand if you look at it it receives an unsigned long parameter. Now this is very important. What does unsign long means? It means four bytes. It means that the maximum value would be 4 billion something. So if we want to brute force if you think I thought about that it's like 4.3 billion values to brute force. And this is where the actually issue lies. But I didn't figure it uh from the

beginning to be honest. The issues is all this exor. It took me quite a bit of time to understand where the vulnerability is. And I'm going to tell you how I how I reached to that conclusion. Does anyone any have any idea why why this is an issue? Don't worry about it. So my initial exploitation ideas first idea was obvious. I got the seed is stored in an unsigned long which is 4.3 billion values. Maybe I can brute force this. Obviously it's a very very bad idea. It would take a very long time. Now a couple of observations. Get pit in general it's two bytes. If you look at it it's two bytes. It's a

small number. is brute forceable. Now the other variable that we had there was the microsconds right when we call get time of day we had the current second and the current microcond. The current microcond it's a value between 0 and 1 million. Okay 1 million as a value we can store it on 20 bits 20 bits again it's a small value brute forcible. The most important one here is the time stamp because the time stamp is stored on four bytes and that is the the biggest value right the way we're we're um storing uh time stamps currently is from the 1st of January 970 to 2038 uh is the the last year that I believe

that we store at the current moment right so this is the biggest value from all of them. So I thought okay how could I get the time stamp as if I get the time stamp then I maybe I can brute force the speed then I can brute force the microsconds separately. So I started looking for endpoints that would look that would leak sorry that would leak a time stamp and when I'm talking about time stamp I'm talking about time stamp at the moment of the password is set that is the time stamp that I was thinking maybe I can leak that or maybe I can leak a time stamp that is close close to that time stamp

at the moment of the password reset. Um I I was able to find a couple of end points. However, they were all authenticated. So, it became a bit of a chicken and egg problem because I'm like, well, I can't use those endpoints cuz I'm unauthenticated. I want everything to be done unauthenticated here. So, that was not very useful. If you have any other tricks, feel free to to share them. So, I was out of ideas. So, what do I do when I'm out of ideas? When I'm out of ideas and I uh audit a project that it's written in PHP, I start reviewing the PHP core because I did this quite a few times in the past.

So I started reviewing. Okay, let me review some of the Sodo random uh functions that are implemented in PHP core. Let's see how do these guys do it. So I found this function called LCG seed. Now LCG seed here also calls get time of day, right? And we we just saw in bash bash bash also uses the same pattern. They also called get time of day. They read the seconds and they read the microsconds. The difference is in PHP. Oops. So let me go back. The difference is in PHP they do an exor between the current second the current microcond. However they do a left bit shift on the micro on the microcond by 11 here and there. They

do they do a left B shift by 11 and then they do theor with the second. So I was like, that's interesting. That's very interesting. Why do they do a left bit shift here? And I asked myself this question for uh for quite a bit of time because I was like certainly this would work. You would get some random numbers back even if there's no left bit shift. You still get some output. Like why is this necessary? There has to be a good reason from a security perspective why the PHP developer guys and by by the way PHP has a poor reputation and a poor track record I have to say in terms of security like if you've been in this

industry for a couple of decades you probably know this but like these guys know something they've been hit with something in the past and that's why they added that left bed shift Let's go back to to Bash. Let's do a comparison just to be clear for everybody. These guys, they don't do any beat shift. They simply exor flip values. Is this a problem? Well, yeah, it actually is a problem. It is a big problem. And then I had like an Africa moment. And uh this is what I realized. Let's take all the fields one by one to discuss them. As I mentioned the TV SEC which is returned from get time of day. On a modern computer this occupies eight

bytes but in practice only four bytes are enough to store time stamp. Like I said up to year 2038. The next field TV which is the the microcond. This also occupies eight bytes. But in practice, you only need 20 bits. Only 20 bits are used because the maximum value is 1 million and 20 bit 20 bits are enough to store 1 million. Get paid. Again, the process ID occupies four bytes and the maximum values that I've seen in our testing was around 660,000. uh this was after I I ran a lot of fork bombs creating processes so stress testing the system right in in practice uh the number would be much lower than this so what I realized is the following when

you exert those three values sorry the only what's going to happen only the lowest 20 bits of the time stamp will be modified the upper 20 bits will will remain static and static is always a very bad idea when you're trying to use to generate a crypto secure uh value which should be random all over. So we're changing the lowest 20 bits that's it the upper 20 the upper 12 bits of the time stamp will remain will always remain the same. I have to make a note here. Sorry. uh get get paid to be the only thing that uh breaks our attack here. But in order to break our attack, it would have to be like an

extremely high number in the billions so that it changes one of the top bits in the time stamp. Because if if get bit is such a high number, it changes one of the high bits in the time stamp, then that would actually force you as an attacker to perform a brute force all over the space, all over the 4.3 billion values. So to summarize, when we're changing only the lowest 20 20 bits of the time stamp, we've essentially reduced entropy. we reduce entropy a lot. And what's going to happen in practice when you have a time stamp you're changing only the lowest 20 bits, right? The result will be another time stamp, but it will be very close to the current

time stamp. So for example, today is the 25th of um April. When you do this process, this exor the new time stamp will be between minus one week ago and one week in the future. That's it. That's what it means in practice. That's like a variation of 14 days. Very small, very easy to brute force. So obviously the only thing that matters here is the time stamp. The speed is insignificant. The microsconds are insignificant as well. I wrote some uh quick code PHP code just for uh visualization to create the minimum and the maximum when you do that exor like what's the range of the interval uh that where the new time stamp will be in. So here I'm

setting the lowest 20 bits to zero and here I'm setting the lowest the yeah the lowest 20 bits to one to see where the maximum is. I'm going to go a bit faster now. Sorry about this just because of my voice. But uh I'll give it the opportunity to ask questions at the end. So this is the script when I did the original test. The original date was 8th of July. The minimum was uh 29th of June and the the maximum was 11th of July. So it's about uh two weeks interval. Uh then I created a local exploit just to test my theory. I've extended uh that pool uh from the previous gentleman. I added a new method

just to see if I can brute force uh the local AR key that is stored in the file and see if this actually works and I can discover it myself. And uh the suggestion that I made initially was to simply brute force up to three years back. So we're starting from the current date. We're going back for three years. Why? Because I I think it's a reasonable assumption to to make that uh the software was installed let's say in the last three years in the last three years or the admin did the password reset in the in the past few years. I think that was enough and in the just common sense assumption I wrote this code. It just uh it's not

optimized. It's written in Rust though. So Rust is very fast just to make up for my laziness of not optimizing it. I I wrote it in Rust for its speed. And I basically I got the R key which was stored locally. I fed it to to my new tool and it started performing a brute force and it worked locally extremely fast. I discovered the date of the password reset when that token was performed uh was like 28th of June uh 2024. So this worked very well. It confirmed my theory. Uh it worked in a local scenario works very well. So I was like great now let's do it remotely like everything should be done fully remote.

So then I I created a small script for Turbo Intruder. Uh in case you're not familiar with it, Turbo Intruder is an a plug-in for Burpu which allows you to to send a very high number of HTTP requests extremely fast. So when the author of the tool released it, he said that this made the billion request attack possible. Now I don't want to send a billion requests. I could do that but I don't want to. I want to make it extremely fast and optimized. I want to send as as little as possible in terms of requests. Okay. So I created I ported basically my local tool which was in Rust. I converted it to Python and I ran it with Turbo

Intruder. uh the way I brute force it as I mentioned initially I brute forced uh all the time stamps for the past three years now some very quick math in terms of how how many attempts does that mean in practice well there are 31.5 million attempts in a year because there are 31 m million uh seconds in a year right and for the for three years uh this comes to 95 million attempts for three years. So some uh again some very quick math let's round this up let's say it's 100 million attempts 100 million attempts but if you remember when I started the seed was 4.3 billion values so from 4.3 billion values I managed to

optimize it I reduced it by 98% because that's what 100 million means from 4.3 three billion. Yeah. So, first step, first optimization, 98% uh reduction of brute force attempts. Now, if you want to see some other tips regarding turbo intruder, uh I made some other research on concrete CMS like how how you can make this extremely fast, uh simply visit this link on our blog in case you're interested in that. The glorious win. Okay. I did the brute force u with turbo intruder and burp suit. I was able to uh recover the password reset token and once I recovered it, you can see it here. Okay, I was able to login into the admin account. We can see that this

return a 200 HTB 200 code and all the information regarding the admin account. So, this was the first win, the glorious win. But I got a bonus attack though. There's something else we can do here actually. We can call the victim and we can tell them we can try to socially engineer them. Like we can tell them, hey man, look, somebody is trying to hack your account. Uh, please reset your password now and make sure you set an extremely strong password. So what will this do for us? Well, when they reset the password, okay, they will set like the strongest password ever that we can possibly imagine. At the same time, they will generate a new R

key for us. And when they generate that new R key, as I said, it will be in a range of about two weeks left and right like from the current time stamp, left and right. Okay. So what will this mean for us in practice? This essentially it's a 99.97% optimization because in total only 1.2 million attempts will be needed. So first we reduced it to 100 million and then from 100 million with a bit of social engineering trick we reduced it to just 1.2 million. That's it. Uh some more research. Now let me show you a demo as well. Hopefully the demo gods are with us today. Let's see if this works. I will uh I will simulate the the social

engineering attack because that is uh way faster.

One minute one. Okay. So this is this is how it looks like. So the admin set this password

I'm trying to copy

Yeah, cuz Yeah, I don't think Yeah, sorry guys. Uh

Uh, are you

see? Okay, let me try to find it.

Hey, he said that. Awesome. Thank you. So, >> so uh the admin set a very strong password as you could see right now. We're going to log out. Essentially, he did us a favor. So, let's continue. Now, we're brute forcing. We started with 90 seconds. Sorry.

And it set a new password and

Okay.