
CVE-2025-44203 用エクスプロイト。HotelDruid 3.0.0/3.0.7 の競合状態を標的とし、管理者認証情報を漏えいさせ、サービス拒否を引き起こします。オフラインでのパスワード復元用ブルートフォーススクリプトが含まれています。
HotelDruid 3.0.0 および 3.0.7 には、セットアップ完了前に到達可能な creadb.php セットアップエンドポイントに競合状態があります。影響は 2 つあり、どちらも同じ敗北した競合に起因します: 機密情報の開示(冗長な SQL エラーメッセージによる)とサービス拒否(DoS)です。
一度に多数の POST リクエストを送信することで、攻撃者は競合状態を引き起こし、管理者ユーザー名、パスワードハッシュ、ソルトを含む機密データをレスポンスから読み取ることができます。Debian パッケージの設定時に選択したパスワードが弱い場合、その後オフラインでワードリストにより復元できる可能性があります。
攻撃が成功すると、管理者はインストール中に設定した資格情報でログインできなくなります(サービス拒否)。
Debian パッケージは、セットアップが完了するまで creadb.php をログインなしで到達可能なままにし、パッケージデフォルトの C_UTILIZZA_SEMPRE_DEFAULTS="AUTO" により、リクエストのたびにデータベース作成全体を再実行します。このルーチンは、100 を超える SQL ステートメント(テーブルの作成とデータ投入)をロックなしで発行するため、同時に届いた複数のリクエストが同じ処理を同時に実行します。これが競合状態です。
遅い、またはビジーなマシンでは、並行実行が重なり、SQLite データベース上で衝突します。あるリクエストがテーブルと行の作成を開始すると、後続のリクエストのステートメントは、行が既に存在する、別の書き込みによってデータベースがロックされているなど、さまざまな理由で大量に失敗します。失敗のたびに、HotelDruid のクエリラッパー esegui_query() は失敗したステートメント全体を HTTP レスポンスに出力します。つまり、1 つのレスポンスに多数の SQL エラーが含まれ、そのうちの 1 つに管理アカウントの機密情報が含まれることになります。
update ...utenti set password = '<hash>', salt = '<salt>', tipo_pass = '5' where idutenti = '1'
これが、このエクスプロイトが成功を検出する仕組みです。レスポンス内で set password を検索します。これは、その正確なステートメントがエコーされた場合にのみ出現し、creadb.php の通常の出力では決して出現しません。
ロックアウトも同じ失敗に起因します。管理者行は最初に tipo_pass='n' で挿入され、上記の UPDATE だけがそれを動作するアカウント(tipo_pass='5' かつパスワード設定済み)に変えます。UPDATE の直後に実行されるコードは、それが成功したかどうかを決して確認しません。ログインを有効にする abilita_login ファイルを作成し、管理者の資格情報を保持していた ini.php を削除し、その後 ultimo_accesso を書き込んで、セットアップを完全に終了します。したがって、UPDATE が競合に負けると、アカウントは tipo_pass='n' のままになり、ログインページは正しいパスワードでも常に拒否します。一方、ログインは有効になり、資格情報ファイルは失われます。その時点で、再インストールなしに戻る方法はありません。
このため、情報漏えいの成功とロックアウトは実際には同じイベントです。どちらも、その 1 つの UPDATE が競合に負けたときに発生します。ユーザー名だけが得られた場合、それは、パスワード UPDATE がまだ成功している間に、より前のステートメントが衝突したことを意味し、したがってロックアウトは発生しません。
攻撃者マシンからターゲットに対してエクスプロイトを実行します:
python3 exploit.py 192.168.1.1
ここで、192.168.1.1 は HotelDruid が実行されているマシンの IP アドレスです。
成功した場合は、ワードリストとともに brute.py を使用して平文パスワードの復元を試みます。brute.py を開き、取得したソルトを salt に、取得したハッシュを final_hash に設定してから実行します:
python3 brute.py rockyou.txt
ここで、rockyou.txt はワードリストです。
正しいユーザー名とパスワードを使用しても、あなたはログインできず、管理者も同様です。復元したパスワードは正しいのですが、成功した競合によってアカウントが tipo_pass='n' のままになっているため、ログインページはそれを拒否します。根本原因セクションを参照してください。
この脆弱性は HotelDruid 3.0.8 で修正されました。チェンジログ はこちらです。"CVE-2025-44203" を検索するだけで見つかります。
そこで使用されるロックヘルパー(crea_lock_file()、ブロッキング flock(LOCK_EX))は、バージョン 3.0.7 ですでに存在し、コードの他の部分で使用されていました。しかし、creadb.php はそれを呼び出したことがありませんでした。パッチは次の 2 つのことを行います:
セットアップルーチンの周囲で排他ロックを取得するため、同時に到着したリクエストは競合せずに順番を待ちます。この修正は、creadb.php で最終的にそのヘルパーを呼び出すことで実装されています。管理者プロビジョニングの直前にロックを取得し、その後 distruggi_lock_file() で解放します。これを機能させているのは、ロック取得直後のチェックです。最初に進入したリクエストが、ロックを保持したまま ini.php を削除し、すべてのリクエストがプロビジョニングの前に ini.php が存在するかどうかを再確認するため、後ろに並んだリクエストはロックを取得し、ファイルが既に存在しないことを確認して、ブロック全体をスキップします。2 番目のリクエストが実行される頃にはセットアップは完了しており、何も行われません。
2 つの管理者 UPDATE クエリにサイレントフラグを渡すため、そのうちの 1 つが失敗しても、資格情報がレスポンスに出力されなくなります。この修正は、ユーザー名を設定するクエリとパスワードとソルトを設定するクエリの 2 つの esegui_query() 呼び出しに、2 番目の引数 1 を追加することで実装されています。このフラグは、クエリが失敗したときにクエリラッパーに何も出力しないよう指示します。エラーはサーバーログに記録されますが、そうでなければ失敗したステートメント(ハッシュとソルトを含む)をページに出力していたであろう echo はスキップされます。
creadb.php:
879: esegui_query("update $tableutenti set nome_utente = '".aggslashdb($admin)."' where idutenti = '1'",1);
897: esegui_query("update $tableutenti set password = '$passw', salt = '$salt', tipo_pass = '5' where idutenti = '1'",1);