
CVE-2025-44203을 대상으로 하는 익스플로잇으로, HotelDruid 3.0.0/3.0.7의 경쟁 조건을 이용하여 관리자 자격 증명을 유출하고 서비스 거부를 유발합니다. 오프라인 비밀번호 복구를 위한 무차별 대입 스크립트를 포함합니다.
HotelDruid 3.0.0 및 3.0.7에는 설정이 완료되기 전에 접근 가능한 creadb.php 설정 엔드포인트에 경쟁 조건이 존재합니다. 이는 두 가지 영향을 미치며, 둘 다 동일한 경쟁 손실에서 비롯됩니다. 민감한 정보 노출(자세한 SQL 오류 메시지를 통해)과 서비스 거부입니다.
여러 개의 POST 요청을 한 번에 보내면 공격자가 경쟁 조건을 유발하고 응답에서 관리자 사용자 이름, 비밀번호 해시 및 솔트를 포함한 민감한 데이터를 읽을 수 있습니다. 데비안 패키지 구성 중 선택한 비밀번호가 약한 경우, 나중에 오프라인에서 워드리스트로 복구할 수 있습니다.
공격이 성공하면 관리자는 설치 중 설정한 자격 증명으로 로그인할 수 없게 됩니다(서비스 거부).
데비안 패키지는 설정이 완료될 때까지 creadb.php를 로그인 없이 접근 가능하게 유지하며, 패키지 기본값 C_UTILIZZA_SEMPRE_DEFAULTS="AUTO"를 사용하면 모든 요청 시 전체 데이터베이스 생성을 다시 실행합니다. 해당 루틴은 100개가 넘는 SQL 문을 실행하고(테이블 생성 및 데이터 채움) 주위에 잠금이 없으므로, 함께 도착하는 여러 요청이 동시에 실행됩니다. 이것이 경쟁 조건입니다.
느리거나 바쁜 머신에서는 병렬 실행이 겹쳐서 SQLite 데이터베이스에서 충돌합니다. 요청이 테이블과 행 생성을 시작한 후, 나중에 도착한 요청의 문은 행이 이미 존재하거나 데이터베이스가 다른 쓰기에 의해 잠겨 있는 등 다양한 이유로 대량으로 실패합니다. 각 실패 시 HotelDruid의 쿼리 래퍼 esegui_query()는 HTTP 응답에 실패한 전체 문을 출력합니다. 이는 단일 응답에 수십 개의 SQL 오류가 포함되어 돌아오며, 그중 하나에 관리자 계정의 민감한 정보가 포함되어 있음을 의미합니다.
update ...utenti set password = '<hash>', salt = '<salt>', tipo_pass = '5' where idutenti = '1'
익스플로잇이 성공을 감지하는 방식은 대략 이렇습니다. 응답에서 set password를 검색하는데, 이는 해당 정확한 문이 에코될 때만 나타나며, creadb.php의 정상 출력에서는 절대 발생하지 않습니다.
잠금(lockout)은 동일한 실패에서 비롯됩니다. 관리자 행은 먼저 tipo_pass='n'으로 삽입되며, 위의 UPDATE만이 이를 작동하는 계정(tipo_pass='5' 및 비밀번호 설정)으로 전환합니다. UPDATE 직후 실행되는 코드는 작동 여부를 확인하지 않습니다. 로그인을 켜는 abilita_login 파일을 생성하고, 관리자 자격 증명을 보유한 ini.php를 삭제한 후, 설정을 영구히 닫는 ultimo_accesso를 작성합니다. 따라서 UPDATE가 경쟁에서 지면 계정은 tipo_pass='n'으로 유지되며, 로그인 페이지는 올바른 비밀번호라도 항상 거부합니다. 반면 로그인은 켜져 있고 자격 증명 파일은 사라집니다. 이 시점에서 재설치 없이는 되돌릴 방법이 없습니다.
이것이 성공적인 유출과 잠금이 실제로 동일한 이벤트인 이유입니다. 둘 다 해당 하나의 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는 이를 호출하지 않았습니다. 패치는 두 가지 작업을 수행합니다:
creadb.php에서 해당 헬퍼를 호출하여 이를 구현합니다. 관리자 프로비저닝 직전에 잠금을 획득하고 이후 distruggi_lock_file()로 해제합니다. 작동하게 만드는 것은 잠금을 획득한 직후의 검사입니다. 먼저 들어온 요청은 여전히 잠금을 보유한 상태에서 ini.php를 삭제하고, 모든 요청은 프로비저닝 전에 ini.php가 존재하는지 다시 읽습니다. 따라서 뒤에 대기 중인 요청은 잠금을 획득하고 파일이 이미 사라진 것을 발견하여 전체 블록을 건너뜁니다. 두 번째 요청이 실행될 때쯤에는 설정이 이미 완료되어 아무것도 하지 않습니다.esegui_query() 호출(사용자 이름을 설정하는 호출과 비밀번호와 솔트를 설정하는 호출)에 두 번째 인수 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);