K8S 生态周报| 深入源码剖析 Kubernetes 的漏洞

「K8S 生态周报」内容主要包含我所接触到的 K8S 生态相关的每周值得推荐的一些信息。欢迎订阅知乎专栏「k8s生态」

大家好,我是张晋涛。

上游进展

Kubernetes 发布了 v1.22.16 和 1.23.14,1.24.8,1.25.4等版本,其中最重要的就是以下两个安全漏洞了。

CVE-2022-3162

当用户被授权允许在集群范围内 list 或 watch 某个 namespace 范围的自定义资源时,可以读取在同一 API 组下,不同类型的其他自定义资源。

这个漏洞影响范围是:

  • kube-apiserver v1.25.0 - v1.25.3
  • kube-apiserver v1.24.0 - v1.24.7
  • kube-apiserver v1.23.0 - v1.23.13
  • kube-apiserver v1.22.0 - v1.22.15
  • kube-apiserver < v1.21.?

CVE-2022-3294

我们平时如果想要进入 Pod 内进行操作(即 kubectl exec)的时候,它的过程是:

  • kubectl -> kube-apiserver:

kubectl 会请求 /api/v1/namespaces/<ns>/pods/<pod>/exec 到 kube-apiserver,实际的代码也很简单,可以看到是一个 POST 请求,并且按照传递的参数构造请求。

	fn := func() error {
		restClient, err := restclient.RESTClientFor(p.Config)
		if err != nil {
			return err
		}

		// TODO: consider abstracting into a client invocation or client helper
		req := restClient.Post().
			Resource("pods").
			Name(pod.Name).
			Namespace(pod.Namespace).
			SubResource("exec")
		req.VersionedParams(&corev1.PodExecOptions{
			Container: containerName,
			Command:   p.Command,
			Stdin:     p.Stdin,
			Stdout:    p.Out != nil,
			Stderr:    p.ErrOut != nil,
			TTY:       t.Raw,
		}, scheme.ParameterCodec)

		return p.Executor.Execute("POST", req.URL(), p.Config, p.In, p.Out, p.ErrOut, t.Raw, sizeQueue)
	}
  • kube-apiserver -> 目标节点的 kubelet

当 kube-apiserver 接收到来自 client 的请求后,就需要构造新的请求然后到目标节点上执行了。 这部分的最直接的代码是如下的内容,会有一个 ExecLocation 函数,用来返回目标位置。

func ExecLocation(
	ctx context.Context,
	getter ResourceGetter,
	connInfo client.ConnectionInfoGetter,
	name string,
	opts *api.PodExecOptions,
) (*url.URL, http.RoundTripper, error) {
	return streamLocation(ctx, getter, connInfo, name, opts, opts.Container, "exec")
}

当然,和节点信息有关的部分,是在 streamLocation 函数的部分来获取的,如下:

container, err = validateContainer(container, pod)
	if err != nil {
		return nil, nil, err
	}

	nodeName := types.NodeName(pod.Spec.NodeName)
	if len(nodeName) == 0 {
		// If pod has not been assigned a host, return an empty location
		return nil, nil, errors.NewBadRequest(fmt.Sprintf("pod %s does not have a host assigned", name))
	}
	nodeInfo, err := connInfo.GetConnectionInfo(ctx, nodeName)
	if err != nil {
		return nil, nil, err
	}
	params := url.Values{}
	if err := streamParams(params, opts); err != nil {
		return nil, nil, err
	}
	loc := &url.URL{
		Scheme:   nodeInfo.Scheme,
		Host:     net.JoinHostPort(nodeInfo.Hostname, nodeInfo.Port),
		Path:     fmt.Sprintf("/%s/%s/%s/%s", path, pod.Namespace, pod.Name, container),
		RawQuery: params.Encode(),
	}
	return loc, nodeInfo.Transport, nil

通过以上的步骤,kube-apiserver 就知道要跟哪个 Node 连接了。

不过这里有个需要注意的内容,上面的 ResourceGetter 是个通过 ResourceLocation 获取资源的接口,这也是这个漏洞中的核心。

func ResourceLocation(getter ResourceGetter, connection client.ConnectionInfoGetter, proxyTransport http.RoundTripper, ctx context.Context, id string) (*url.URL, http.RoundTripper, error) {
	schemeReq, name, portReq, valid := utilnet.SplitSchemeNamePort(id)
	if !valid {
		return nil, nil, errors.NewBadRequest(fmt.Sprintf("invalid node request %q", id))
	}
	info, err := connection.GetConnectionInfo(ctx, types.NodeName(name))
	if err != nil {
		return nil, nil, err
	}

+   if err := proxyutil.IsProxyableHostname(ctx, &net.Resolver{}, info.Hostname); err != nil {
+       return nil, nil, errors.NewBadRequest(err.Error())
+   }

	// We check if we want to get a default Kubelet's transport. It happens if either:
	// - no port is specified in request (Kubelet's port is default)
	// - the requested port matches the kubelet port for this node
	if portReq == "" || portReq == info.Port {
		return &url.URL{
				Scheme: info.Scheme,
				Host:   net.JoinHostPort(info.Hostname, info.Port),
			},
			info.Transport,
			nil
	}

-   if err := proxyutil.IsProxyableHostname(ctx, &net.Resolver{}, info.Hostname); err != nil {
-       return nil, nil, errors.NewBadRequest(err.Error())
-   }

	// Otherwise, return the requested scheme and port, and the proxy transport
	return &url.URL{Scheme: schemeReq, Host: net.JoinHostPort(info.Hostname, portReq)}, proxyTransport, nil
}

上面是在 v1.22.16 中的修复,可以看到实际是把 proxyutil.IsProxyableHostname 的判断逻辑移动到了前面,在之前有可能会跳过此判断。

如果跳过了这个判断,就可能会导致原本经过认证的请求被发送到 API Server 所在的私有网络(说直白点,就是有可能会篡改目标地址)。

所以,这个漏洞的触发条件也很明确,只有能篡改 Node 地址才会受到影响。

受影响的版本如下:

  • Kubernetes kube-apiserver ≤ v1.25.3
  • Kubernetes kube-apiserver ≤ v1.24.7
  • Kubernetes kube-apiserver ≤ v1.23.13
  • Kubernetes kube-apiserver ≤ v1.22.15

解决办法要么是升级 kube-apiserver,要么可以设置 egress proxy 来进行管理。 但是如果升级 kube-apiserver 也有可能会导致一些依赖于 Node/Proxy 的子资源场景下的不可用,需要注意。

本周 Kubernetes v1.26.0-rc.0 也发布了,按照之前的习惯,正式版和这个版本中差别就不会很大了。 下期我会写一篇介绍 v1.26 版本中重点需要关注的内容,敬请期待!

好了,以上就是本次的全部内容,我们下期再聊!


欢迎订阅我的文章公众号【MoeLove】

TheMoeLove

加载评论