[하루한줄] Github Private Pages XSS 취약점

URL

Breaking GitHub Private Pages for $35k

Target

  • github private pages

Explain

Github Private Pages에서 XSS 취약점이 발견되었습니다. 해당 취약점은 github private pages의 custom authentication flow 과정 중 __Host- prefix를 우회하고 page_id 파라미터를 통해 발생합니다.

github pages는 별도의 github.io 도메인에서 호스팅 되어 github.com 인증 쿠키가 private pages server로 전송되지 않습니다. 따라서 private page authentication은 github.com과의 추가적인 통합 없이는 사용자 인증할 방법이 없습니다. 이러한 이유로 github은 custom authentication flow를 만들었습니다.

취약점 제보 당시 github private pages의 custom authentication flow는 다음과 같습니다.

  1. private page에 접속하면 서버는 __Host-gh_pages_token가 존재하는지 확인합니다. 쿠키가 없거나 잘못 설정된 경우 https://github.com/login으로 리다이렉트 됩니다. initial redirect는 __Host-gh_pages_session 쿠키에 nonce를 저장합니다. 해당 쿠키는 __Host- cookie prefix를 사용하여 호스트가 아닌 도메인이 JavaScript에서 설정되는 것을 방지합니다.
  2. /login/pages/auth?nonce=&page_id=&path=로 리다이렉트 됩니다. 이 엔드포인트는 임시 인증 토큰을 생성하여 token 파라미터에 https://pages-auth.github.com/redirect에 전달합니다. nonce, page_id, path는 비슷하게 전달됩니다.
  3. /redirecthttps://repo.org.github.io/__/auth로 바로 이동합니다. 마지막 엔드포인트는 repo.org.github.io 도메인, __Host-gh_pages_token, __Host-gh_pages_id에 인증 쿠키를 설정합니다. 또한 이전에 설정된 __Host-gh_pages_sessionnonce도 확인합니다.
  4. authentication flow에서 original request path와 page id 같은 정보는 pathpage_id 쿼리 파라미터에 저장됩니다. nonce는 nonce 파라미터에서 전달됩니다.

https://repo.org.github.io/__/authpage_id 파라미터는 whitespace를 무시하여 파싱 되고 정수로 변환되어 Set-Cookie 헤더에 렌더링 됩니다. page_id=12345%0d%0a%0d%0a를 전송하여 Set-Cookie 헤더 뒤에 위치한 Location 헤더를 무시하고 body content를 렌더링할 수 있습니다. 또한 page_id에서 null byte를 만나면 integer parsing이 종료됩니다. 헤더에 null byte가 존재하면 response가 reject 되므로 body의 시작 부분에 null byte를 삽입하여 XSS를 트리거할 수 있습니다.

"?page_id" + encodeURIComponent("\r\n\r\n\x00<script>alert(origin)</script>")

하지만 nonce가 조작된 page_id의 전송을 방지하여 이를 우회하여야 합니다. 브라우저에서 쿠키의 대소문자를 다르게 처리하여 __Host-__HOST-를 다르게 처리합니다. 하지만 github private page server에서는 쿠키를 파싱할 때 대소문자를 무시하므로 __Host- prefix 우회가 가능합니다. 전체적인 POC는 다음과 같습니다.

<script>
const id = location.search.substring("?id=".length)

document.cookie = "__HOST-gh_pages_session=dea8c624-468f-4c5b-a4e6-9a32fe6b9b15; domain=.private-org.github.io";
location = "https://github.com/pages/auth?nonce=dea8c624-468f-4c5b-a4e6-9a32fe6b9b15&page_id=" + id + "%0d%0a%0d%0a%00<script>alert(origin)%3c%2fscript>&path=Lw";
</script>