
Uptime Kuma의 최초 실행 Socket.IO 설정에서 발생하는 TOCTOU 경쟁 조건에 대한 개념 증명으로, 인증되지 않은 공격자가 숨겨진 두 번째 관리자를 심고 인증을 비활성화할 수 있게 한다.
abraxaslabs.tech · github.com/abraxas · @abraxas_null · [email protected] · uptime-kuma-setup-toctou
Uptime Kuma 2.5.5 - Louis Lam
최초 실행 시 Socket.IO setup은 사용자 수를 COUNT하고, cost 10으로 bcrypt 해싱한 뒤 INSERT한다. 사용자명은 UNIQUE 제약만 걸려 있다. 트랜잭션도, 뮤텍스도, 단일 행 제약도 없다. 이미 연결된 두 소켓이 모두 count === 0을 확인하면 둘 다 해싱하고, 둘 다 삽입한다. 추가된 관리자는 보이지 않는다. 사용자 목록 UI가 없기 때문이다.
운영자가 폴링하는 바로 그 시점에 갓 시작된 Kuma에 접속한 외부인이 숨겨진 두 번째 관리자를 심고, 인증을 끄고, 자동 로그인을 타고 운영자의 모니터에 올라탈 수 있다.
| ID | 아직 CVE 없음 |
| CWE | CWE-362, CWE-367 |
| CVSS | High: 7.4 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
| 제품 | Uptime Kuma |
| 영향 범위 | 2.5.5 (c98982a)까지, 최초 실행 시 빈 user 테이블 |
| 인증 | 인증 불필요 |
| 라이선스 | GNU Affero GPL v3.0 |
| 랩 | 127.0.0.1 전용 |
운영자가 /setup을 제출하는 바로 그 순간에 갓 시작된 Kuma에 접속한다. 기본 Docker 바인딩은 0.0.0.0:3001이다. 다른 사용자명으로 두 번째 user 행을 삽입한다. 그 사용자로 로그인하고, disableAuth를 설정한 뒤, R.findOne("user") 자동 로그인을 타고 운영자의 모니터, 알림, API 키, 설정에 올라탄다.
첫 번째 행이 커밋된 후에는 이후의 순차적 setup은 거부된다. 이것은 "setup이 열려 있는 상태"가 아니다. bcrypt 윈도우 동안의 경쟁 조건이다. 관리자가 이미 있는 초기화된 인스턴스는 이 버그에 해당하지 않는다.
잠재적인 API 키 활성화/비활성화 IDOR(user_id 없이 UPDATE api_key SET active=? WHERE id=?)도 두 사용자가 존재하면 활성화된다. 이는 추가 행의 후속 효과이지, 랩 오라클은 아니다.
먼저 SECURITY.md를 읽고, 그다음 제외 목록을 확인했다. 공개된 GitHub 권고 18건, 공개된 Matomo siteId XSS, 그리고 벤더가 수정하지 않겠다고 한 클라우드 메타데이터 SSRF가 있었다. 그다음 태그 2.5.5 (c98982a)에서 22건의 탐색을 진행했다. 인증/setup/2FA, 소켓 IDOR, HTTP/모니터 SSRF, SSTI 잔여물, 경로/LFI, 실제 브라우저, 플러그인/tailscale, 상태 페이지, docker.sock, 프로토타입 오염, push/prometheus, 데이터베이스, 모니터 exec, 분석 XSS, 설정, 웹소켓 CSRF, 프론트엔드 XSS, 작업, Apprise, 유지보수, master-delta. 그 기준 대부분은 미공개 결과를 내놓지 않았다. 인증은 달랐다.
최초 사용자 setup은 공개 Socket.IO 이벤트다. 로그인이 필요 없다. setup은 COUNT(user)를 수행한 뒤 await passwordHash.generate를 하고, 그다음 R.store를 한다. passwordHash.generate는 cost 10의 bcrypt.hash다. 이는 잠금이 아니라 낮잠이다. needSetup은 메모리에 있고 연결 시 "setup"을 emit하는 데만 사용된다. 쓰기 경로는 이를 무시하고 다시 카운트한다.
스키마는 사용자명 UNIQUE 제약만 있다. knex_init_db.js는 단일 행을 강제하지 않는다. 같은 사용자명은 충돌한다(SQLITE_CONSTRAINT). 다른 사용자명은 둘 다 들어간다.
v2 데이터베이스 마법사를 건너뛰기 위해 UPTIME_KUMA_DB_TYPE=sqlite로 루프백에서 louislam/uptime-kuma:2.5.5를 띄웠다. bcrypt cost 10은 첫 시도에서 보장이 아니라 경쟁이다. run.sh는 볼륨을 지우고 재시도한다. 종료 코드 2는 실패다. 다음은 빈 user 테이블이다.
성공했을 때: sqlite 1|labadmin과 2|UK-SETUP-TOCTOU-WITNESS. 순차적 지연 setup: Uptime Kuma has been initialized. 추가 사용자 로그인, setSettings disableAuth=true. 재연결 자동 로그인은 R.findOne("user")다. 가장 낮은 id, 즉 운영자다. prepare2FA otpauth 아이덴티티는 labadmin이다.
인접한 GHSA-23q2-5gf8-gjpp는 disableAuth 재활성화가 소켓을 끊지 못하는 문제다. 다른 버그다.
이미 기록된 잘못된 방향들: 동시 setup 후 사용자 행 하나(경쟁 실패이지 패치가 아님), 두 소켓이 같은 이름을 사용해서 UK-SETUP-TOCTOU-WITNESS가 없는 경우, 대시보드 HTML 200을 SUCCESS로 취급한 경우, 리버스 셸. 전부 쇼에 불과하다. 오라클은 두 개의 sqlite 행, 그다음 id 1로 disableAuth 자동 로그인이다.
cd lab
./run.sh
대상은 오직 http://127.0.0.1:18141. 공식 이미지 louislam/uptime-kuma:2.5.5, sqlite, 최초 실행. 루프백 외부로 포트를 공개하지 말 것.
user-rows [('1', 'labadmin'), ('2', 'UK-SETUP-TOCTOU-WITNESS')]
race-count=2 witness=True
sequential-late-setup initialized=True
setSettings-disableAuth ack ok
reconnect autoLogin=True loginRequired=False
bonus-autologin-identity operator=True id1=labadmin auto_is_id1=True
SUCCESS uptime-kuma-setup-toctou
setup을 직렬화하라. 트랜잭션과 단일 행 제약, 또는 COUNT+INSERT 주변의 뮤텍스. disableAuth 자동 로그인을 가장 낮은 id의 findOne으로 취급하지 말 것. 그때까지는 공유 네트워크에서 최초 실행 setup을 노출하지 말 것.
server.js setup · password-hash.js · knex_init_db.jsGNU Affero GPL v3.0. LICENSE 참조. 루프백 랩 전용. 보증 없음.