
CVE-2020-13277 실습 환경: GitLab 논리적 취약점 - 임의 사용자의 비공개 저장소 무단 접근
CVE-2020-13277 랩: Gitlab 논리적 취약점 - 임의 사용자가 권한을 초과하여 개인 저장소에 접근
CVE-2020-13277
├── README.md ............... [이 README 설명]
├── imgs .................... [README 설명을 돕는 이미지]
├── gitlab .................. [Gitlab 컨테이너의 마운트 디렉터리]
│ ├── Dockerfile .......... [Gitlab의 Docker 빌드 파일]
│ ├── config .............. [Gitlab 설정 마운트 디렉터리]
│ ├── data ................ [Gitlab 데이터 마운트 디렉터리]
│ ├── logs ................ [Gitlab 로그 마운트 디렉터리]
│ ├── keys ................ [Gitlab 크랙 License 저장 디렉터리]
│ └── runner .............. [Runner 컨테이너의 마운트 디렉터리]
├── license ................. [크랙 License의 컨테이너 빌드 디렉터리]
│ ├── Dockerfile .......... [License의 Docker 빌드 파일]
│ └── license.rb .......... [크랙 License를 생성하는 Ruby 스크립트]
├── docker-compose.yml ...... [Docker 빌드 설정]
├── keygen.ps1 .............. [Windows: 원클릭으로 크랙 License 생성]
├── keygen.sh ............... [Linux: 원클릭으로 크랙 License 생성]
├── run.ps1 ................. [Windows: 원클릭으로 Gitlab 랩 실행]
├── run.sh .................. [Linux: 원클릭으로 Gitlab 랩 실행]
├── register.ps1 ............ [Windows: 원클릭으로 Runner 등록]
├── register.sh ............. [Linux: 원클릭으로 Runner 등록]
├── stop.ps1 ................ [Windows: 원클릭으로 Gitlab 랩 중지]
└── stop.sh ................. [Linux: 원클릭으로 Gitlab 랩 중지]
이 취약점의 핵심은 주로 Mirror Repository - 저장소의 미러 동기화 백업 기능을 이용하는 것입니다.
여기서 Mirror의 동기화 방향은 두 가지로 나뉩니다:
이 취약점은 Pull 방향의 Mirror Repository를 이용합니다.
아시다시피 Gitlab은 CE(커뮤니티 무료 버전)와 EE(엔터프라이즈 유료 버전) 두 가지 버전으로 나뉘며, Gitlab 공식도 이 취약점이 CE와 EE의 다음 버전에 동시에 영향을 미친다고 밝혔습니다:
>=10.6, <12.9.10>=12.10, <12.10.11>=13.0, <13.0.6하지만 이 버전들의 Gitlab Docker Image를 모두 랩 구축에 사용할 수 있다는 의미는 아닙니다. 그 이유는:
바꿔 말하면 Docker로 랩을 구축하려면 Gitlab-EE 버전을 선택하여 크랙하고(부자라면 License를 구매하는 방법도 선택할 수 있습니다) Mirror Repository - Pull 기능을 활성화해야 합니다.
하지만 Gitlab-EE를 크랙하더라도 10.x, 12.x, 13.x 모두 Mirror Repository - Pull의 URL에 로컬 경로가 포함되면 Import url is blocked: Requests to localhost are not allowed 오류가 발생합니다.
Admin area => Settings => Network => Outbound requests에서 Allow requests to the local network from hooks and services를 설정하면 로컬 URL을 구성할 수 있지만, 미러를 동기화할 때 2:Fetching remote upstream failed: fatal: unable to access http://127.0.0.1/xxxx/: The requested URL returned error: 301 오류가 발생합니다. 즉 Pull Remote Repository만 사용할 수 있습니다.
다행히도 12.x와 13.x는 로컬 URL 판별 방법이 매우 엄격하지만, 10.x 버전에는 대응 방법이 있습니다. Pull URL을 구성할 때 로컬에 설정된 DNS 서비스 이름만 구성하면 우회할 수 있습니다.
이상을 종합하면 결국 gitlab-ee:10.6.0-ee.0 버전의 Docker Image로만 이 랩을 구축할 수 있습니다.
사실 앞선 설명을 통해서도 알 수 있듯이 이 취약점의 악용 조건은 비교적 까다로워서, 기본적으로 돈 없는 사용자는 이 취약점의 영향을 받기 어렵습니다
./keygen.sh 또는 ./keygen.ps1./run.sh 또는 ./run.ps1앞서 크랙 키페어를 생성할 때 이미 공개 키를 Gitlab 컨테이너 백엔드에 기록해 두었습니다. 이제 개인 키를 프런트엔드를 통해 Gitlab에 업로드하여 크랙을 완료해야 합니다:
./gitlab/keys/ 디렉터리에 생성됩니다. 그 안의 .gitlab-license 내용(개인 키)을 복사합니다Enter license key를 선택하고 개인 키를 붙여넣은 다음 Upload license 버튼을 클릭하면 크랙이 완료됩니다이로써 Mirror Repository - Pull 기능이 활성화되었습니다

Outbound requests를 찾아 Allow requests to the local network from hooks and services를 체크하고 저장합니다이로써 Mirror Repository - Pull이 로컬 Repository를 가져올 수 있게 되었습니다

./register.sh $TOKEN 또는 ./register.ps1 $TOKEN이로써 모든 Repository에서 이 Runner를 사용하여 CI 스크립트(Pipeline Jobs)를 실행할 수 있습니다

검증 과정은 공식 Issue를 참고할 수 있습니다. 하지만 아래의 검증 과정은 이 랩에 맞추어 일부 단계를 미세 조정합니다
root 사용자로 http://127.0.0.1/admin/users 페이지를 열어 3개의 계정을 생성합니다:
victim: 피해자 계정attacker1: 공격자 계정1attacker2: 공격자 계정2계정 생성 시 초기 비밀번호를 설정할 수 없으며, Gitlab은 기본적으로 설정된 Email로 초기 비밀번호를 보냅니다. 편의상 Email은 아무거나 입력해도 되며, 먼저 계정을 생성한 후 즉시 해당 계정을 편집하면 root로 초기 비밀번호를 설정할 수 있으므로 Email을 거칠 필요가 없습니다

victim 계정으로 Gitlab에 로그인New Project 생성:
targetPrivateREADME.md 파일을 생성하고 내용을 임의로 mykey is abcxyz로 설정합니다명백히
target은victim의 개인 저장소이며, 우리의 목표는 취약점을 이용해 이 저장소의 내용을 획득하는 것입니다

attacker1 계정으로 Gitlab에 로그인New Project 생성:
pocPublic.gitlab-ci.yml 파일을 생성합니다. 내용은 다음과 같습니다:image: "ruby:2.6"
rspec:
script:
- git clone http://gitlab-ci-token:[email protected]/victim/target.git
- cd target
- ls -lah .
- cat README.md
【목적】이어지는 작업에서는 몇 가지 수법을 통해 victim이 인지하지 못하는 상황에서 자신의 권한으로 이 CI 스크립트를 실행하도록 합니다.
현재 랩에는 인증서가 설정되어 있지 않으므로
http프로토콜만 사용할 수 있습니다. 또한172.168.30.2는docker-compose.yml이 Gitlab 컨테이너에 할당한 IP입니다. 이 CI 스크립트는 결국 Runner를 통해 실행되며, 현재 랩의 Runner와 Gitlab은 같은 컨테이너가 아니므로 Docker가 할당한 IP 주소를 사용해야 합니다

attacker2 계정으로 Gitlab에 로그인New Group 생성:
testPublictest 그룹에 완전히 빈 저장소 New Project 생성:
pocPublic
미러 저장소 구성 Settings => Repository => Pull from a remote repository:
Mirror repository: 체크Git repository URL: http://GITLAB/attacker1/poc 입력Password: (비워 둠, Pull 대상 저장소의 공개 수준이 Public이므로 비밀번호가 필요 없음)Trigger pipelines for mirror updates: 체크 (미러 동기화 시 CI 스크립트 실행 트리거)구성이 완료되면 30분마다 소스 저장소의 변경 여부를 확인하고, 변경이 있으면 강제로 동기화합니다.
Git repository URL이 가리키는 것은 앞서 생성한 poc 저장소입니다. 127.0.0.1을 사용하지 않는 이유는 Pull이 로컬 저장소 가져오기를 금지하기 때문이지만, DNS를 이용해 우회할 수 있습니다.GITLAB은docker-compose.yml이 Gitlab 컨테이너에 할당한 hostname으로, 기본적으로/etc/hosts에 설정됩니다. 호스트 머신에서는GITLAB을 해석할 수 없지만, 컨테이너 내부에서는http://127.0.0.1/attacker1/poc에 접근하는 것과 같습니다

attacker2 계정 사용test 그룹의 사용자를 관리합니다victim 사용자를 그룹에 추가:
Add new member to test: victimPermissions: OwnerExpiration date: (비워 둠, 즉 무기한)=> Setting => Account => Delete account로 현재 계정(attacker2)을 삭제합니다【목적】 test 그룹에는 victim과 attacker2 두 명의 Owner만 있습니다. attacker2 사용자가 삭제되면 test 그룹과 그 아래 모든 저장소의 소유권이 강제로 victim에게 이전됩니다. 현재 test 그룹에는 attacker1/poc에서 미러 동기화된 test/poc 저장소가 있으므로, 즉 test/poc 저장소를 victim 사용자에게 강제로 양도하는 것과 같습니다.


지금까지의 상황을 먼저 정리해 보겠습니다:
victim을 위해 test/poc 저장소를 생성했습니다test/poc 저장소의 내용은 attacker1/poc 저장소에서 미러 동기화됩니다 (30분마다 변경 여부를 확인하여 동기화 필요)attacker1/poc 저장소의 내용은 공격자가 통제하며, CI 스크립트 .gitlab-ci.yml도 포함됩니다test/poc 저장소에 Trigger pipelines for mirror updates가 설정되어 있으므로 동기화할 때마다 CI 스크립트가 실행됩니다test/poc 저장소의 Owner가 victim이므로 CI 스크립트 실행 시 victim의 권한이 사용됩니다
앞서 설정한 CI 스크립트 .gitlab-ci.yml의 내용을 다시 살펴보면, 이 poc는 Runner에서 victim의 권한을 사용하여 해당 Private 저장소 target의 디렉터리 구조와 README.md에 접근했습니다:
image: "ruby:2.6"
rspec:
script:
- git clone http://gitlab-ci-token:[email protected]/victim/target.git
- cd target
- ls -lah .
- cat README.md
그 후 공격자는 attacker1/poc 저장소에서 중요하지 않은 내용만 임의로 수정하면 되며, 최악의 경우 30분만 기다리면 victim의 권한으로 설정된 CI 스크립트를 실행할 수 있습니다.
공격자는 Pipeline Jobs의 실행 결과에 직접 접근할 권한은 없지만, 스크립트의 출력 대상을 조정하기만 하면 됩니다. 예를 들어 저장소 내용을 지정된 Email이나 FTP 서버로 보내면 저장소 코드를 탈취할 수 있습니다.
