XSS-Leaks: Leaking Cross-Origin Redirects

2026. 4. 27. 21:21·Web Study
반응형

XSS-Leak

  • Cross-Site-Subdomain Leak에서 따온 이름
  • Chromium 기반 브라우저(Chrome, Edge 등)에서 Cross-Origin Redirects 목적지, fetch() 요청의 도메인 등을 누출할 수 있는 사이드 채널 공격

원래 목적이 Cross-Origin 요청의 서브도메인을 누출하는 것이 목표였기 때문에 XSS-Leak이라는 이름을 붙였다고 한다.


Background

Chrome의 커넥션 풀(Connection Pool)

브라우저는 웹 요청을 보낼 때, 내부에 동시에 처리할 수 있는 통로(소켓/커넥션)가 제한되어 있다.

  • Chrome은 전체 브라우저에서 최대 256개의 동시 네트워크 요청을 처리할 수 있음
  • 같은 오리진(Same-Origin)에서는 최대 6개까지 병렬 처리가 가능

핵심 동작 - 요청 정렬 방식

Chrome은 요청마다 우선 순위가 있고, 보통은 높은 우선순위가 먼저 소켓/커넥션을 받는다.

우선순위가 같을 때 ≠ 선입선출(FIFO) 방식

  1. Port(포트)
  2. Scheme(스킴)
  3. host(호스트) - 사전순(알파벳순)으로 더 작은 게 먼저 소켓을 받음

예를 들어, http://example.com:80 과 http://google.com:80 이 같은 우선순위 대기 중이면, example.com이 google.com보다 사전순으로 앞이라서 example.com 요청이 먼저 소켓을 받는다.

 

이러한 정렬 동작이 바로 오라클(Oracle)이 된다.

  • 내가 고른 호스트: example
  • 상대가 고른 호스트: google

이 동시에 소켓 1개를 두고 경쟁하게 만들면

  • example이 먼저면 → 내 요청이 빨리 끝남
  • google이 먼저면 → 내 요청이 대기해서 늦게 끝남

즉, 어떤 요청이 먼저 처리되는지를 시간 측정으로 알아냄으로써, 알 수 없어야 할 호스트명을 추론할 수 있다


CTF Challenge - 서브도메인 누출

문제: https://github.com/salvatore-abello/web-challenges/tree/main/X/salvatoreabello/exploit

Route /

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>challenge-01</title>
    <p id="result">Hello World!</p>
</head>
<body>
    <script nonce="<%= nonce %>">
        const DOMAIN = "<%= DOMAIN %>";
        const PORT = "<%= PORT %>";

        const result = document.getElementById("result");

        const toHex = s => [...new TextEncoder().encode(s)].map(b => b.toString(16).padStart(2,'0')).join('');

        window.onhashchange = () => {
            let flag = localStorage.getItem("flag") || "flag{fake_flag_for_testing}";
            fetch(`http://${toHex(flag)}.${DOMAIN}:${PORT}`)
            .finally(() => result.innerText = "request sent")
        }
    </script>
</body>
</html>
  • 인라인 스크립트가 포함된 페이지
  • DOMAIN = challenge-01.babelo.xyz and PORT = 80
  • location.hash가 변경되면 localStorage 에서 flag를 읽어 hex 인코딩 후 http://<hex(flag)>. challenge-01.babelo.xyz:80으로 fetch() 요청을 보냄

Route /report

try{
    ...

    page = await context.newPage();
    
    console.log(`The admin will visit ${SITE} first, and then ${url}`);

    await page.goto(`${SITE}`, { waitUntil: "domcontentloaded", timeout: 5000 });
    await sleep(100);

    await page.evaluate((flag) => {
        localStorage.setItem('flag', flag);
    }, FLAG);

    console.log(`localStorage.setItem('flag', '${FLAG}')`)

    await sleep(500);

} catch (err) {
    console.error(err);
    if (browser) await browser.close();
    return reject(new Error("Error: Setup failed, if this happens consistently on remote contact the admin"));
}

resolve("The admin will visit your URL soon");

try {
    await page.goto(url, { waitUntil: "domcontentloaded", timeout: 5000 });
    await sleep(120_000);
} catch (err) {
    console.error(err);
}
  • 헤드리스 Chrome 봇을 실행
  • 챌린지 사이트 방문 → flag를 localStorage에 저장 → 공격자가 제공한 URL로 이동 → 120초 대기

컨텐츠 보안 정책(Content Security Policy, CSP)

app.use((req, res, next) => {
    const nonce = crypto.randomBytes(16).toString('base64');
    res.locals.nonce = nonce;

    res.setHeader("Content-Security-Policy", `default-src 'none'; script-src 'nonce-${nonce}'; connect-src *.${DOMAIN}; base-uri 'none'; frame-ancestors 'none'`);
    next();
});
  • script-src 'nonce-…' : nonce 없이는 스크립트 실행 불가
  • connect-src *. challenge-01.babelo.xyz : 네트워크 요청은 해당 도메인의 서브도메인으로만 가능
  • frame-ancestors ‘none’ : iframe으로 삽입 불가

즉, XSS도 안되고, 직접 데이터도 읽을 수 없는 상황

Exploit - Leaking subdomains of cross-origin requests

const sleep = (ms) => {
    return new Promise(resolve => {
        setTimeout(resolve, ms);
    });
}

// sends a fetch request that takes 360 seconds to finish
const fetch_sleep_long = (i) => {
    controller = new AbortController();
    const signal = controller.signal;

    fetch(`http://sleep${i}.${MYSERVER}/360?q=${i}`, {
        mode: 'no-cors',
        signal: signal
    });

    return controller
}

// blocks one socket
const block_socket = async (i) => {
    let controller = fetch_sleep_long(i);
    await sleep(0);

    return controller;
}

// exhausts all sockets except one
const exhaust_sockets = async() => {
    let i = 0
    for (; i < SOCKETLIMIT; i++) {
        let controller = await block_socket(i);
        controllers.push(controller);
    }
}
  • 먼저 커넥션 풀(Connection pool)을 설정하는 함수를 만듬

1. 커넥션 풀 고갈

  • 시간이 오래 걸리는 요청을 255개를 보내서 커넥션 풀을 고갈 시킴
  • 소켓이 1개만 남은 상태가 됨

2. 피해자의 비밀 요청 트리거

  • 피해자 페이지는 hashchange 같은 이벤트로 http://<hex(flag)>. DOMAIN를 보내도록 만들어져 있음
  • 이 시점에 마지막 1개의 소켓도 차단해, 피해자의 요청을 대기 줄에 서게 함

3. 공격자의 추측 요청 보내기

  • 공격자가 자기 추측 값으로 요청을 하나 더 보냄
    Ex) http://FFFF.attacker.com
  • 이 요청도 소켓이 없으니 대기 줄에 들어감

현재 대기 줄에는 http://<hex(flag)>. DOMAIN과 http://7FFF.attacker.com 두 요청이 있음  (7FFF인 이유는 0~F 중간 값)

 

4. 소켓 1개 비우고, 기준치 요청 보내기

  • 공격자는 소켓 1개를 비우고 http://0000.attacker.com 요청을 보냄
  • 이 요청은 250ms 후에 답장하도록 설정

5. 누가 먼저 들어왔는지 시간으로 판단

  • 피해자가 6으로 시작한다고 가정
    6xxx < 7FFF : 피해자가 먼저 소켓에 들어감. 내 요청이 끝나는데 오래 걸림 (600ms 이상)
  • 피해자가 6으로 시작하고, 내 추측 값이 5FFF 였다고 가정
    6xxx < 5FFF :  내 요청이 빨리 끝남  250ms 이하
공격자의 요청 속도 의미
빠름 ( < 250ms) 추측 값이 비밀보다 사전 순 앞(=비밀이 더 큼)
느림 ( > 600ms) 추측 값이 비밀보다 사전 순 뒤(= 비밀이 더 작음)

 

공격자는 이진 탐색으로  7 → 3 → 1 이런 식으로 좁혀서 비밀 값을 알아낸다.

이것이 곧 비밀 값이 내 추측보다 큰가? 작은가? 를 알려주는 오라클!


리다이렉트 호스트 누출

리다이렉트 호스트

리다이렉트는 서버가 이 URL 말고 다른 URL로 가라고 지시하는 동작 (보통 301/302)
이때 다른 URL로 이동하라고 지시한 URL에서 호스트의 부분

  • https://admin.example.com/dashboard
  • host: admin.example.com

admin.example.com 부분이 리다이렉트 호스트이다.

시나리오

소스코드: https://gist.github.com/salvatore-abello/7d4fd69a098e1f80c900747dc1d3dae5

 

Google을 OpenID Connect 프로바이더로 사용하는 로그인 시스템:

  • 관리자(admin) → admin.test.localhost.com 으로 리다이렉트
  • 일반 사용자(user) → app.test.localhost.com 으로 리다이렉트
  • 한번 로그인하면 /login 접근 시 자동으로 해당 서브도메인으로 리다이렉트 됨

공격 원리

리다이렉트 네비게이션의 우선순위는 매우 높아서 공격자도 같은 우선순위 요청(frame navigation)을 사용한다.

사전순으로 admin < ajj < app 이므로, 공격자는 ajj를 비교 값으로 사용

사용자 유형 리다이렉트 목적지 ajj와 비교 결과
관리자 admin.test.localhost.com admin < ajj → 리다이렉트가 먼저 공격자가 요청 지연됨 ( >500ms)
일반 사용자 app.test.localhost.com app > ajj → 공격자가 먼저 공격자 요청 빠르게 완료

Exploit

1. 커넥션 풀 고갈

  • 요청 255개 보냄
  • 소켓이 1개만 남은 상태가 됨

2. auth.localhost.com/login 열기

  • 윈도우에서 auth.localhost.com/login을 염
  • 이때 1개 남은 소켓을 차단해 auth.localhost.com/login 요청을 대기로 만듦

3. 소켓 1개 열고, 즉시 다시 차단

  • 첫 요청만 처리, 리다이렉트는 대기
  • auth.localhost.com/login의 요청을 열렸을 때 들어가고, 리다이렉트 된 admin (or app).test.localhost.com은 소켓이 없으니 대기

4. ajj.attacker.com으로 iframe 요청

  • frame 네비게이션과 우선순위가 동일
  • ajj.attacker.com도 소켓이 없으니 대기로 감

현재 리다이렉트 요청과 공격자 요청(ajj)이 대기 상태

 

5. 소켓 1개 열고, 즉시 다시 차단

  • 리다이렉트 vs iframe 경쟁 유발
피해자가 관리자인 경우 피해자가 일반 사용자인 경우
대기: admin vs ajj 대기: app vs ajj
admin < ajj → 리다이렉트 먼저 ajj < app → 공격자가 먼저
공격자(ajj)는 여전히 대기 공격자(ajj)가 소켓을 먼저 차지하고 빠르게 완료

 

6. 000000.attacker.com으로 요청 전송

  • 000000은 어떤 호스트보다 사전 순으로 앞임

7. 소켓 1개 열고, 즉시 다시 차단

  • 000000은 사전순으로 1등이므로 소켓을 차지함
  • 이 요청은 500ms 후에 응답

8. 소켓 1개 열고 끝

  • 500ms 대기 후, 000000 끝나고 소켓 반환
  • 남아있는 요청 처리

ajj의 총 소요 시간을 측정한다.

ajj 소요 시간 판정
> 500ms (오래 걸림) admin이 ajj보다 앞 → 관리자
< 500ms (빠르게 끝남) ajj가 app보다 앞 → 일반 사용자

 


활용 사례

  • 크로스 오리진 요청 개수 세기 : 타겟이 몇 개의 요청을 보내는지 파악
    https://blog.babelo.xyz/posts/css-exfiltration-under-default-src-self/#swimming-in-the-connection-pool-
  • POST 요청 지연시키기 : 특정 요청의 타이밍 조작
    https://github.com/icesfont/ctf-writeups/tree/main/idekctf/2025#appendix
  • 스킴(Scheme) 누출: 요청이 http 인지 https 인지 판별
  • 포트번호 누출 : 요청의 목적이 포트 파악
  • GroupId.privacy_made_ : 누출 가능성 (미검증)

한계 및 결론

  • Chromium 기반 브라우저에서만 동작 (Firefox, Safari 불가)
  • Chrome에 보고 했지만, 기존 커넥션 풀 고갈 공격의 변형으로 간주되어 WAI (Won't fix, As Intended)로 분류

XSS(스크립트 주입)이 필요 없이, 브라우저의 커넥션 풀 내부 구현(요청 정렬 방식)이라는 사이드 채널을 이용해 크로스 오리진 요청의 목적지를 추론해 내는 공격 기법이다.

 

Connection Pool로 이용한 Timing 기법은 예전에도 있었지만, 재조명된 이유는 공격 목표가 더 구체적이고 직접적이 됐기 때문이다.

타이밍으로 뭔가 추측 가능 수준이 아니라 redirect 목적지를 뽑아낼 수 있다로 인식이 바뀌었다.


참고 문헌

xss-Leak: 교차 출처 리디렉션 유출 | 살바토레 아벨로의 블로그 --- XSS-Leak: Leaking Cross-Origin Redirects | Salvatore Abello's Blog

https://blog.babelo.xyz/posts/cross-site-subdomain-leak/

 

 

 

반응형

'Web Study' 카테고리의 다른 글

CSWSH  (0) 2026.04.28
PHP Object injection  (0) 2026.03.19
Directory Listing  (0) 2026.03.04
Host Split Attack  (0) 2026.03.04
HTTP Session Hijacking  (0) 2026.03.04
'Web Study' 카테고리의 다른 글
  • CSWSH
  • PHP Object injection
  • Directory Listing
  • Host Split Attack
y3onbug5
y3onbug5
y3onbug5 님의 블로그 입니다.
  • y3onbug5
    y3onbug5 님의 블로그
    y3onbug5
  • 전체
    오늘
    어제
    • 분류 전체보기 (170)
      • Alpacahack (19)
      • Dreamhack 워게임 (49)
        • Lv.1 (40)
        • Lv.0 (4)
        • LV.2 (3)
        • LV.3 (2)
      • [Dreamhack] Web Beginner (3)
      • [Dreamhack] Web Hacking (17)
        • 웹 기초 지식 (4)
        • Cookie & Session (2)
        • Cross-Site Scripting(XSS) (1)
        • Cross-Site Request Forgery (1)
        • SQL Injection (4)
        • NoSQL Injection (2)
        • Command Injection (1)
        • File Vulnerability (1)
        • Server-Side Request Forgery (1)
      • [Dreamhack] Web Hacking Client-Side (10)
        • XSS Filtering Bypass (2)
        • Content Security Policy (CSP) (2)
        • CSRF,CORS Bypass (2)
        • Client-Side Template Injection (CSTI) (1)
        • CSS Injection (1)
        • Relative Path Overwrite (RPO) (1)
        • DOM Vulnerability (1)
      • [Dreamhack] Web Hacking Server-Side (15)
        • SQL Injection Advanced (4)
        • SQL Injection Advanced - Fingerprinting (2)
        • NoSQL Injection Advanced (3)
        • Command Injection Advanced - Web Servers (3)
        • File Vulnerability Advanced - Web Server (3)
      • [Dreamhack]Black-Box Penetration Testing (15)
        • DreamCommunity Penetration Testing (11)
      • [Dreamhack] LLM (2)
        • [Dreamhack] LLM과 프롬프트 엔지니어링 (2)
      • Web 공부 (4)
      • Web Study (18)
      • JavaScript (17)
        • 기초 (12)
        • 중급 (4)
      • 웹 개발(Flask) (0)
      • [Security First] web 기초교육 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    webstudy
    hacking
    web hacking
    Web Study
    CSRF
    cve
    web
    드림핵
    xss
    LLM
    alpacahack
    JS
    webhacking
    JavaScript
    DreamHack
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
y3onbug5
XSS-Leaks: Leaking Cross-Origin Redirects
상단으로

티스토리툴바