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

2022-12-07 14:40:14 浏览数 (1)

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

大家好,我是张晋涛。

上游进展

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 请求,并且按照传递的参数构造请求。

代码语言:javascript复制
 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 函数,用来返回目标位置。

代码语言:javascript复制
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 函数的部分来获取的,如下:

代码语言:javascript复制
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 获取资源的接口,这也是这个漏洞中的核心。

代码语言:javascript复制
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 版本中重点需要关注的内容,敬请期待!

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


参考资料

[1] k8s生态: https://zhuanlan.zhihu.com/container

0 人点赞