Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-66418-OpenClaw-Dashboard-v3.0.0-Stored-XSS-via-Failed-Login-Username-Field — 보안 권고: 관리자 계정 탈취로 이어지는 인증되지 않은 저장형 크로스 사이트 스크립팅 (openclaw-dashboard) | Kitploit
도구/GitHubGitHub/theopaid/cve-2026-66418-openclaw-dashboard-v3.0.0-stored-xss-via-failed-login-username-field
Vulnerability AnalysisWeb Application ExploitationWeb SecurityLearning & Education
GitHubtheopaid/cve-2026-66418-openclaw-dashboard-v3.0.0-stored-xss-via-failed-login-username-field

CVE-2026-66418-OpenClaw-Dashboard-v3.0.0-Stored-XSS-via-Failed-Login-Username-Field

보안 권고: 관리자 계정 탈취로 이어지는 인증되지 않은 저장형 크로스 사이트 스크립팅 (openclaw-dashboard)

저장소 보기

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
18일 전아직 검토되지 않음

보안 권고: 무인증 저장형 크로스 사이트 스크립팅으로 인한 관리자 계정 탈취 (openclaw-dashboard)

제목: OpenClaw Dashboard v3.0.0 실패한 로그인 사용자 이름 필드를 통한 저장형 XSS 할당된 CVE ID: CVE-2026-66418

대상 저장소: https://github.com/tugcantopaloglu/openclaw-dashboard

요약

로그인 엔드포인트는 제출된 사용자 이름을 검증 없이 감사 로그에 기록합니다. 알림 패널은 이후 해당 로그 항목을 읽어 innerHTML로 이스케이프 없이 페이지에 작성합니다. 로그인할 수 없는 공격자는 사용자 이름이 스크립트 페이로드인 실패한 로그인 요청을 보낼 수 있습니다. 다음에 로그인한 관리자가 알림 벨을 열면 해당 페이로드는 관리자 브라우저에서, 대시보드 오리진의 세션 토큰에 접근할 수 있는 상태로 실행됩니다.

  • CWE-79: 웹 페이지 생성 중 입력의 부적절한 중화 (크로스 사이트 스크립팅)
  • CWE-117: 로그에 대한 부적절한 출력 중화
  • CVSS 4.0: 9.3 (치명적). 벡터: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:N/SC:H/SI:H/SA:N

영향을 받는 버전

감사 로그 항목을 렌더링하는 알림 센터가 추가된 v3.0.0에서 도입되었습니다. v3.0.0 및 현재 main(d6198d0)을 포함한 이후의 모든 커밋에 존재합니다. 작성 시점 기준으로 수정되지 않았습니다.

위협 모델

공격자는 대시보드의 HTTP 포트에 도달할 수 있지만 계정이나 유효한 자격 증명이 없는 원격 당사자입니다. 이는 대시보드를 열 수 있는 모든 사람이 로그인 화면에 도달할 수 있다는 사실과 일치합니다. 유일한 전제 조건은 관리자 계정이 이미 존재한다는 것이며, 이는 최초 실행 설정 이후의 모든 배포에서 해당됩니다.

페이로드는 저장형이므로 타이밍 조정이 필요하지 않습니다. 관리자가 UI에서 제공하는 일반적인 동작인 알림 패널을 열면 실행됩니다. 일단 실행되면 관리자 브라우저와 동일한 기능을 가지며, 세션 토큰을 읽고 피해자를 대신해 모든 인증된 엔드포인트를 호출할 수 있습니다.

근본 원인

1단계: 사용자 이름이 있는 그대로 기록됩니다. 등록된 계정과 일치하지 않는 사용자 이름으로 로그인하면 다음 분기에 도달합니다:

root@kitploit:~
// server.js:1577-1583
if (username !== creds.username) {
  recordFailedAuth(ip);
  auditLog('login_failed', ip, { username });
  res.writeHead(401, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ error: 'Invalid username or password' }));
  return;
}

username은 JSON 요청 본문에서 직접 가져옵니다. 길이 제한, 문자 허용 목록, 유형 검사가 없습니다. auditLog는 이를 JSON 라인으로 디스크에 기록합니다:

root@kitploit:~
// server.js:278-282
function auditLog(event, ip, details = {}) {
  try {
    const timestamp = new Date().toISOString();
    const entry = JSON.stringify({ timestamp, event, ip, ...details }) + '\n';
    fs.appendFileSync(auditLogPath, entry, 'utf8');

JSON.stringify는 따옴표와 줄바꿈을 이스케이프하므로 페이로드는 한 줄로 유지되고 다시 깔끔하게 파싱됩니다. <, >, /는 이스케이프하지 않으므로 HTML 마크업은 그대로 유지됩니다.

2단계: 로그를 다시 읽습니다. notifications 엔드포인트는 최근 로그 라인을 브라우저로 반환합니다:

root@kitploit:~
// server.js:2042-2051
if (req.url.startsWith('/api/notifications')) {
  if (!requireAuth(req, res)) return;
  const limit = parseInt(new URL(req.url, 'http://localhost').searchParams.get('limit') || '50');
  try {
    const raw = fs.readFileSync(auditLogPath, 'utf8').trim();
    const lines = raw.split('\n').filter(Boolean).slice(-Math.min(limit, 200));
    const events = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean).reverse();
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ events }));

3단계: 사용자 이름이 이스케이프 없이 DOM에 기록됩니다. 프론트엔드는 문자열 연결로 각 알림 행을 구성하고 이를 innerHTML로 할당합니다:

root@kitploit:~
// index.html:5647-5653
body.innerHTML = data.events.map(e => {
  const icon = notifIcons[e.event] || '📋';
  const time = e.timestamp ? new Date(e.timestamp).toLocaleString() : '';
  const detail = e.username ? ' (' + e.username + ')' : '';
  const ip = e.ip ? ' from ' + e.ip : '';
  return '<div class="notif-item"><div class="notif-icon">' + icon + '</div><div class="notif-content"><div class="notif-event">' + (e.event||'').replace(/_/g, ' ') + detail + ip + '</div><div class="notif-time">' + time + '</div></div></div>';
}).join('');

e.username은 공격자의 문자열입니다. 인코딩 없이 innerHTML에 도달하므로 브라우저는 이를 HTML로 파싱합니다.

server.js:298에 설정된 Content-Security-Policy는 script-src 'self' 'unsafe-inline'을 포함하므로 onerror와 같은 인라인 이벤트 핸들러가 실행될 수 있습니다.

개념 증명

  1. 관리자 계정이 존재하는지 확인합니다(일반적인 설치라면 존재합니다). 공격자는 해당 자격 증명이 필요하지 않습니다.

  2. 인증되지 않은 공격자로서 사용자 이름이 페이로드인 실패한 로그인을 한 번 전송합니다:

    root@kitploit:~
    curl -X POST http://TARGET:7000/api/auth/login \
      -H 'Content-Type: application/json' \
      -d '{"username":"","password":"x"}'
    

    서버는 401 Invalid username or password로 응답하고 페이로드를 저장합니다.

  3. 관리자는 정상적으로 로그인하고 알림 벨을 클릭합니다.

  4. 페이로드는 관리자의 세션에서 실행됩니다. 이 예시에서는 페이지 자체의 getStoredToken()으로 세션 토큰을 읽고, 이를 사용해 POST /api/key-file을 통해 에이전트 지시 파일 AGENTS.md를 덮어씁니다. 모든 인증된 엔드포인트도 같은 방식으로 호출할 수 있습니다.

페이로드 텍스트는 단일 로그 라인으로 유지되며 /api/notifications를 통해 바이트 단위로 그대로 반환되므로, 주입과 실행을 별도로 확인할 수 있습니다.

영향

대시보드 오리진에서 관리자 권한으로 코드가 실행됩니다. 스크립트는 세션 토큰을 읽고, 에이전트의 지시 및 스킬 파일 편집과 OpenClaw 구성 변경을 포함한 모든 인증된 요청을 실행할 수 있습니다. 공격자에게 계정이 필요 없으므로, 인증되지 않은 네트워크 요청 하나가 관리자 세션에 대한 제어로 이어집니다.

수정 방안

  • innerHTML에 값을 넣기 전에 모든 값을 HTML 인코딩하거나, 문자열 연결 대신 textContent로 노드를 구성합니다. 알림의 사용자 이름, 이벤트, IP 필드 모두 이 처리가 필요합니다.
  • 로깅 전에 서버에서 username을 검증합니다. 길이를 제한하고 예상되는 문자 집합으로 제한합니다.
  • script-src에서 'unsafe-inline'을 제거합니다. 인라인 핸들러가 차단되면 이 문제는 코드 실행에서 무해한 마크업으로 낮아집니다.
도구 다운로드