Kubernetes(K8S)集群服务器取证详解

科技   2024-11-20 23:17   山东  

推荐公众号:云淡纤尘

主要分享电子数据取证技术,坚持不易,还请大家多多支持
前言

        这一次取证其实不难,但是因为之前从来没有接触过K8S,所以对其的一些基本组成要件,运行逻辑等都没有概念。这一次取证从拿到题目开始,就是一边取证一边学习,耗时两个月也是终于成功将网站给搭建起来了,也取出数据库了。但其实对我来说最大的成就是搞懂了K8S的一些基本逻辑。同时这也是我第一次接触服务器集群的取证,我心里也大概对服务器集群取证的步骤有了大致的思路,不至于像一开始一点头绪都没有。同时,集群服务器的取证与单服务器取证最大的区别之一就是很难用取证软件一把梭梭出来,所以针对集群服务器的取证联系可以让自己不过度依赖取证软件,而是关注于个人能力的提升。这次的题目涉密,所以具体题目没有办法放出,读者朋友看一下我的取证思路,同时也请各位大佬如果有更好的取证思路欢迎交流,如果有错误也请及时指正。

关于Kubernetes(K8S)

       Kubernetes(K8S)是一个开源的容器编排平台,由Google开发,现在由云原生计算基金会(CNCF)进行维护。它旨在自动化部署、扩展和管理容器化应用程序,是云技术的核心部分,也是构建云原生的基石。

1. 主要组件

  • 主节点(Master):

    • kube-apiserver:集群的前端接口,提供RESTful API接口,用于管理集群状态、配置和操作。
    • etcd:分布式、一致性的键值存储数据库,用于存储集群的配置信息、状态和元数据。
    • kube-scheduler:调度组件,负责将新创建的Pod调度到集群中的节点上。
    • 控制器管理器:包含多个控制器,负责监控集群的状态,并根据预设的期望状态来实现集群的自愈和自动化操作。
    • 控制平面的核心部分,负责管理整个集群的状态和配置。

    • 主要组件包括:

  • 子节点(Node):

    • kubelet:负责管理节点上的Pod和容器,与主Master通信,并根据Master的指令创建、启动、监控和终止Pod。
    • kube-proxy:负责实现Kubernetes Service的负载均衡和代理转发,以及维护网络规则和iptables规则等。
    • Container Runtime:负责管理和运行容器,如Docker、containerd等。
    • Pod网络插件:负责为Pod分配IP地址,并提供Pod之间和Pod与外部网络之间的网络通信。
    • 集群的工作节点,执行Master的操作指令。

    • 主要组件包括:

2. 基本概念

Namespace

  1. 定义:Namespace是K8s中用于实现多套环境的资源隔离或多租户的资源隔离的一种机制。通过将集群内部的资源分配到不同的Namespace中,可以形成逻辑上的“组”,以方便不同的组的资源进行隔离使用和管理。

  2. 作用:

  • 资源隔离:不同Namespace中的资源是相互隔离的,一个Namespace中的资源无法直接访问另一个Namespace中的资源。
  • 多租户管理:可以将不同的Namespace交给不同的租户进行管理,实现多租户的资源隔离和权限控制。
  • 环境隔离:可以将开发、测试、生产等不同环境的资源划分到不同的Namespace中,以便于管理和维护。
  • 默认Namespace:

    • K8s在集群启动后会默认创建几个Namespace,如default、kube-node-lease、kube-public、kube-system等。
    • 所有未指定Namespace的对象都会被分配在default命名空间中。

    Pod

    1. 定义:Pod是K8s集群中进行管理的最小单元,是资源对象模型中由用户创建或部署的最小资源对象模型。程序要运行必须部署在容器中,而容器必须存在于Pod中。因此,Pod可以看作是容器的封装。

    2. 组成:

    • 一个Pod中可以存在一个或多个容器。这些容器共享存储、网络、以及生命周期等。
    • 每个Pod都有一个特殊的被称为“根容器”的Pause容器(也称为Infrastructure容器)。Pause容器负责维护整个Pod的网络空间,并对用户不可见。
  • 特点:

    • 共享资源:Pod中的容器可以共享存储卷和网络IP,实现文件共享和通信。
    • 生命周期:Pod是一个相对短暂的组件。当Pod所在节点发生故障时,Pod会被调度到其他节点上重新创建,但这是一个全新的Pod实例,与之前的Pod没有关联。
    • 单一职责:虽然一个Pod中可以包含多个容器,但通常建议每个Pod只运行一个主容器,以便于管理和维护。如果有多个紧密相关的容器需要一起运行,可以将它们放在同一个Pod中。
  • 使用和管理:

    • Pod的创建和管理通常通过K8s的控制器(如Deployment、ReplicaSet等)来完成,而不是直接创建Pod。
    • 控制器可以确保Pod的数量和状态符合预期,并在Pod出现故障时自动进行重启或重建。

    开始取证

    取证环境

            简要介绍一下本次取证的基本环境:本次题目给了三个镜像,分别是一个master节点,一个node1节点,一个node2节点。操作系统为CentOS Stream release 8。三台机器均使用静态IP配置。网站框架为WordPress,数据库使用了mysql。

    取证工具

    火眼证据分析软件

    火眼仿真取证软件

    Finalshell

    VMware Workstation Pro 17.0

    Navicat Premium 16

    取证过程

    仿真

            首先先将三个服务器镜像仿真起来,网络模式就选NAT。具体仿真过程中需要注意的问题我在前文有讲过,可翻看我第一篇文章

    image

    网络配置

            仿真起来第一件事就是查看IP,但是这三台机器是没有办法输出IP的,如下图所示

    image

            所以此时我们需要查看网络配置文件,因为集群服务器一般而言都会采用静态IP然后使用同一个NAT出口来进行联网,所以我们要去检查配置文件。这个配置文件的路径需要记住:/etc/sysconfig/network-scripts

    image

            此处可以看到,嫌疑人是把服务器重新设置了一个ens160的网卡,并使用静态IP来进行服务器配置

    划重点:

            此时会有好多人会把IP改成自动分配或者换成自己的C段。但是注意这是一个服务器集群,如果在主机名的配置文件里,嫌疑人没有给IP使用主机名,或者在其他服务的配置文件里,没有使用主机名进行通信,那么我们把服务IP改动了会造成服务器无法正常运行,更不用说重构网站什么的了。所以这里我们需要改动我们自己电脑的虚拟网卡配置

            我们进入VMware,在工具栏选中编辑,进入虚拟网络编辑器,可以看到图中这三个服务器的C段为160.所以我们把自己的虚拟网卡的子网IP的C段改成160,同时DHCP里也要同步改动,如图所示

    image

    同时,我们把三台服务器的网络配置文件里的两个ens160改成ens33,让其作为正常的ens33网卡使用,如图所示

    vim /etc/sysconfig/networl-scripts/ens160 && reboot
    image

            三台机器重复相同的操作之后就可以正常显示IP并且连接Finalshell了

    image

    重构

    通信

    首先我们要对K8S的基本结构有所了解        

    K8S的运行逻辑与Docker逻辑很类似,K8S将空间分为一个个命名空间namespaces,然后命名空间内存在很多Pods,这个PodsDocker的容器概念是一样的

    我们先查看一下这个集群各个节点能否通信,看一看各个节点的在线情况

    kubectl get node
    image

    OK,各节点状态良好,可以通信

    小坑

            由于这个命令只能在master节点上使用,所以经常会忽略两个node节点的通信情况,当我们在node节点上运行这个命令的时候,可以看到输出

    image

    注意报错信息,http://localhost:8080/api

            这说明,这个节点还是在“以自我为中心”,没有摆正自己的位置,他是一个work节点,应该是暴露自己的IP让master节点和其他work节点通信的

            从报错可以看出,kubectl 没有使用到证书与k8s api通信,在初始化集群的时候要想使用kubectl需要,申明证书变量 export KUBECONFIG=/etc/kubernetes/admin.conf ,重启之后 KUBECONFIG 变量失效了。所有需要输出如下

    echo "export KUBECONFIG=/etc/kubernetes/kubelet.conf" >> /etc/profile
    source /etc/profile
    image

    查看NamespacesPods

            注意,查看Pods的时候要指定Namespaces,不然会默认输出defauts的pods

    image

            看到这里我们可以得到,我们需要重构的网站是一个wordpress框架的网站,同时有一个redis和一个不知道是什么数据库的镜像,但其命名为wordpressdb估计就是网站后台的数据库

            也可以推测出来名为wordpress-v1-55cb456f68-vgxf2的Pod就是网站源码的容器

    我们可以查看一下这Pod的详细信息

    wordpress-v1-55cb456f68-vgxf2
    kubectl describe pod wordpress-v1-55cb456f68-27hj -n wordpress

    输出如下

    [root@master ~]# kubectl describe pod wordpress-v1-55cb456f68-vgxf2 -n wordpress
    Name:             wordpress-v1-55cb456f68-vgxf2
    Namespace:        wordpress
    Priority:         0
    Service Account:  default
    Node:             node2/192.168.160.153
    Start Time:       Sun, 30 Jun 2024 12:47:23 +0800
    Labels:           app=wordpress
                      app.kubernetes.io/name=wordpress
                      app.kubernetes.io/version=v1
                      pod-template-hash=55cb456f68
                      version=v1
    Annotations:      cni.projectcalico.org/containerID: 0183fd2c4df531208566146323ffc18ae5cae201fc9159accd40b08ea08028cf
                      cni.projectcalico.org/podIP: 10.213.96.226/32
                      cni.projectcalico.org/podIPs: 10.213.96.226/32
                      kubesphere.io/creator: admin
                      kubesphere.io/imagepullsecrets: {}
                      kubesphere.io/restartedAt: 2024-06-30T04:39:29.238Z
                      logging.kubesphere.io/logsidecar-config: {}
                      sidecar.istio.io/inject: false
    Status:           Running
    IP:               10.213.96.226
    IPs:
      IP:           10.213.96.226
    Controlled By:  ReplicaSet/wordpress-v1-55cb456f68
    Containers:
      container-arwmr6:
        Container ID:   containerd://949bab5c7fc4c79ee203e3d8d0e80c1f835b3355736d4a04f8d3611bca5201f1
        Image:          wordpress:4.8-apache
        Image ID:       docker.io/library/wordpress@sha256:6216f64ab88fc51d311e38c7f69ca3f9aaba621492b4f1fa93ddf63093768845
        Port:           80/TCP
        Host Port:      0/TCP
        State:          Running
          Started:      Wed, 20 Nov 2024 20:02:21 +0800
        Last State:     Terminated
          Reason:       Unknown
          Exit Code:    255
          Started:      Tue, 02 Jul 2024 20:22:58 +0800
          Finished:     Wed, 20 Nov 2024 19:20:45 +0800
        Ready:          True
        Restart Count:  9
        Environment:    <none>
        Mounts:
          /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-f974c (ro)
          /var/www/html from volume-onlsh6 (rw)
    Conditions:
      Type              Status
      Initialized       True 
      Ready             True 
      ContainersReady   True 
      PodScheduled      True 
    Volumes:
      volume-onlsh6:
        Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
        ClaimName:  wordpress-pvc
        ReadOnly:   false
      kube-api-access-f974c:
        Type:                    Projected (a volume that contains injected data from multiple sources)
        TokenExpirationSeconds:  3607
        ConfigMapName:           kube-root-ca.crt
        ConfigMapOptional:       <nil>
        DownwardAPI:             true
    QoS Class:                   BestEffort
    Node-Selectors:              <none>
    Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                                 node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
    Events:
      Type     Reason          Age                  From             Message
      ----     ------          ----                 ----             -------
      Normal   SandboxChanged  143d                 kubelet          Pod sandbox changed, it will be killed and re-created.
      Normal   Pulled          143d                 kubelet          Container image "wordpress:4.8-apache" already present on machine
      Normal   Created         143d                 kubelet          Created container container-arwmr6
      Normal   Started         143d                 kubelet          Started container container-arwmr6
      Normal   SandboxChanged  143d                 kubelet          Pod sandbox changed, it will be killed and re-created.
      Normal   Pulled          143d                 kubelet          Container image "wordpress:4.8-apache" already present on machine
      Normal   Created         143d                 kubelet          Created container container-arwmr6
      Normal   Started         143d                 kubelet          Started container container-arwmr6
      Normal   SandboxChanged  141d (x2 over 141d)  kubelet          Pod sandbox changed, it will be killed and re-created.
      Normal   Pulled          141d                 kubelet          Container image "wordpress:4.8-apache" already present on machine
      Normal   Created         141d                 kubelet          Created container container-arwmr6
      Normal   Started         141d                 kubelet          Started container container-arwmr6
      Normal   SandboxChanged  141d                 kubelet          Pod sandbox changed, it will be killed and re-created.
      Normal   Pulled          141d                 kubelet          Container image "wordpress:4.8-apache" already present on machine
      Normal   Created         141d                 kubelet          Created container container-arwmr6
      Normal   Started         141d                 kubelet          Started container container-arwmr6
      Normal   SandboxChanged  141d (x3 over 141d)  kubelet          Pod sandbox changed, it will be killed and re-created.
      Normal   Pulled          141d                 kubelet          Container image "wordpress:4.8-apache" already present on machine
      Normal   Created         141d                 kubelet          Created container container-arwmr6
      Normal   Started         141d                 kubelet          Started container container-arwmr6
      Normal   SandboxChanged  141d                 kubelet          Pod sandbox changed, it will be killed and re-created.
      Normal   Pulled          141d                 kubelet          Container image "wordpress:4.8-apache" already present on machine
      Normal   Created         141d                 kubelet          Created container container-arwmr6
      Normal   Started         141d                 kubelet          Started container container-arwmr6
      Warning  NodeNotReady    27m                  node-controller  Node is not ready
      Normal   SandboxChanged  26m (x2 over 26m)    kubelet          Pod sandbox changed, it will be killed and re-created.
      Normal   Pulled          26m                  kubelet          Container image "wordpress:4.8-apache" already present on machine
      Normal   Created         26m                  kubelet          Created container container-arwmr6
      Normal   Started         26m                  kubelet          Started container container-arwmr6

    可以看出来这个镜像的信息如下:

    部署在node2(192.168.160.153)节点上

    Pod内部通信IP是10.213.96.226

    Pod端口是80

    镜像sha256值是6216f64ab88fc51d311e38c7f69ca3f9aaba621492b4f1fa93ddf63093768845

    wordpressdb-v1-0
    kubectl describe pod wordpressdb-v1-0 -n wordpress

    输出如下

    [root@master ~]# kubectl describe pod wordpressdb-v1-0 -n wordpress
    Name:             wordpressdb-v1-0
    Namespace:        wordpress
    Priority:         0
    Service Account:  default
    Node:             node1/192.168.160.152
    Start Time:       Sun, 30 Jun 2024 02:29:11 +0800
    Labels:           app=wordpressdb
                      app.kubernetes.io/name=wordpress
                      app.kubernetes.io/version=v1
                      controller-revision-hash=wordpressdb-v1-5ccd5b447d
                      statefulset.kubernetes.io/pod-name=wordpressdb-v1-0
                      version=v1
    Annotations:      cni.projectcalico.org/containerID: 11cc1ff5e31eda29899f9a2655a3c7364e63f5e1c7ead3c4f5fc57e4a02dbc56
                      cni.projectcalico.org/podIP: 10.213.90.152/32
                      cni.projectcalico.org/podIPs: 10.213.90.152/32
                      kubesphere.io/creator: admin
                      kubesphere.io/imagepullsecrets: {}
                      kubesphere.io/restartedAt: 2024-06-29T18:20:30.359Z
                      logging.kubesphere.io/logsidecar-config: {}
                      sidecar.istio.io/inject: false
    Status:           Running
    IP:               10.213.90.152
    IPs:
      IP:           10.213.90.152
    Controlled By:  StatefulSet/wordpressdb-v1
    Containers:
      container-rdcw3a:
        Container ID:   containerd://b8a4887b46ef77e133400d7c6a0a3878633bd88a8c12d1eeb8b5c91d3454906f
        Image:          mysql:5.6
        Image ID:       docker.io/library/mysql@sha256:20575ecebe6216036d25dab5903808211f1e9ba63dc7825ac20cb975e34cfcae
        Port:           3306/TCP
        Host Port:      0/TCP
        State:          Running
          Started:      Wed, 20 Nov 2024 20:01:09 +0800
        Last State:     Terminated
          Reason:       Unknown
          Exit Code:    255
          Started:      Tue, 02 Jul 2024 20:23:25 +0800
          Finished:     Wed, 20 Nov 2024 19:19:16 +0800
        Ready:          True
        Restart Count:  10
        Environment:
          MYSQL_ROOT_PASSWORD:  <set to the key 'MYSQL_ROOT_PASSWORD' in secret 'mysql-secret'>  Optional: false
        Mounts:
          /var/lib/mysql from mysql (rw)
          /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-jhvxr (ro)
    Conditions:
      Type              Status
      Initialized       True 
      Ready             True 
      ContainersReady   True 
      PodScheduled      True 
    Volumes:
      mysql:
        Type:       PersistentVolumeClaim (a reference to a PersistentVolumeClaim in the same namespace)
        ClaimName:  mysql-wordpressdb-v1-0
        ReadOnly:   false
      kube-api-access-jhvxr:
        Type:                    Projected (a volume that contains injected data from multiple sources)
        TokenExpirationSeconds:  3607
        ConfigMapName:           kube-root-ca.crt
        ConfigMapOptional:       <nil>
        DownwardAPI:             true
    QoS Class:                   BestEffort
    Node-Selectors:              <none>
    Tolerations:                 node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
                                 node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
    Events:
      Type    Reason          Age                  From     Message
      ----    ------          ----                 ----     -------
      Normal  SandboxChanged  143d                 kubelet  Pod sandbox changed, it will be killed and re-created.
      Normal  Pulled          143d                 kubelet  Container image "mysql:5.6" already present on machine
      Normal  Created         143d                 kubelet  Created container container-rdcw3a
      Normal  Started         143d                 kubelet  Started container container-rdcw3a
      Normal  SandboxChanged  143d                 kubelet  Pod sandbox changed, it will be killed and re-created.
      Normal  Pulled          143d                 kubelet  Container image "mysql:5.6" already present on machine
      Normal  Created         143d                 kubelet  Created container container-rdcw3a
      Normal  Started         143d                 kubelet  Started container container-rdcw3a
      Normal  SandboxChanged  141d (x2 over 141d)  kubelet  Pod sandbox changed, it will be killed and re-created.
      Normal  Pulled          141d                 kubelet  Container image "mysql:5.6" already present on machine
      Normal  Created         141d                 kubelet  Created container container-rdcw3a
      Normal  Started         141d                 kubelet  Started container container-rdcw3a
      Normal  SandboxChanged  141d (x2 over 141d)  kubelet  Pod sandbox changed, it will be killed and re-created.
      Normal  Pulled          141d                 kubelet  Container image "mysql:5.6" already present on machine
      Normal  Created         141d                 kubelet  Created container container-rdcw3a
      Normal  Started         141d                 kubelet  Started container container-rdcw3a
      Normal  SandboxChanged  141d                 kubelet  Pod sandbox changed, it will be killed and re-created.
      Normal  Pulled          141d                 kubelet  Container image "mysql:5.6" already present on machine
      Normal  Created         141d                 kubelet  Created container container-rdcw3a
      Normal  Started         141d                 kubelet  Started container container-rdcw3a
      Normal  SandboxChanged  141d                 kubelet  Pod sandbox changed, it will be killed and re-created.
      Normal  Pulled          141d                 kubelet  Container image "mysql:5.6" already present on machine
      Normal  Created         141d                 kubelet  Created container container-rdcw3a
      Normal  Started         141d                 kubelet  Started container container-rdcw3a
      Normal  SandboxChanged  31m (x2 over 31m)    kubelet  Pod sandbox changed, it will be killed and re-created.
      Normal  Pulled          31m                  kubelet  Container image "mysql:5.6" already present on machine
      Normal  Created         31m                  kubelet  Created container container-rdcw3a
      Normal  Started         31m                  kubelet  Started container container-rdcw3a

    可以看出来这个镜像的信息如下:

    数据库类型是mysql

    部署在node1(192.168.160.152)节点上

    Pod内部通信IP是10.213.90.152

    Pod端口是3306

    镜像sha256值是20575ecebe6216036d25dab5903808211f1e9ba63dc7825ac20cb975e34cfcae

    查看映射

            需要注意的是,上面的Pod端口并不是实际端口,我们应该找到Pod映射到主机的端口

    而K8S映射端口的方式大部分是Service

    kubectl get service -o wide
    image

            这里可以发现,只有源码Pod以NodePort的方式映射到了主机31121端口,而MySQL的Pod并没有进行映射,所以我们要给他用Service创建一个映射

    删除原来的Service
    kubectl delete service wordpressdb -n wordpress
    创建一个 wordpressdb-service.yaml 文件:
    vim wordpressdb-service.yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: wordpressdb
      namespace: wordpress
    spec:
      selector:
        app: wordpressdb
      ports:
        - protocol: TCP
          port: 3306        # Service的端口
          targetPort: 3306  # Pod的内部端口
      type: NodePort        # 也可以用 LoadBalancer
    应用 YAML 文件:
    kubectl apply -f wordpressdb-service.yaml
    验证:
    kubectl get service -n wordpress
    image

    成功,进行下一步

    数据库处理

            此时我们也成功让数据库暴露在外了,但是还有一点棘手的是没有数据库的用户密码。这里也是本次取证唯一用到了取证软件的地方(仿真除外)。在全部文件中检索包含mysql的yaml配置文件

    最终在node2里找到了一个名为mysql.yaml.j2的文件,记录了MYSQL_ROOT_PASSWORD

    image

    可以看到这个密码被保存进了这个容器的环境里

    进入这个容器的命令行,输入

    env

    找到密码为my123654

    image

            此时有了数据库root用户的密码,但是因为root一般不会用于外部访问,所以我们需要在root用户下创建一个拥有所有权限的用户

    进入数据库

    mysql -u root -p

    然后输入密码

    image

            先看一下都有什么用户(别忘了加分号)

    select user,host from mysql.user;
    image

    可以看到果然root用户只能用于本地服务器访问

    先刷新一下用户权限

    flush privileges;

    然后创建一个可以在外网登录的用户

    create user 'lu'@'%' identified by '123456';

    各参数含义

    • lu:账户(用root作为名字会出问题,最好用别的名字)
    • %:允许内部和外部登录的权限
    • 123456:密码

    然后进行授权

    grant all privileges on *.* to 'lu'@'%' identified by '123456';

    再刷新权限

    flush privileges;

    用Navicat连接成功

    image

    网站登录

    这是一个wordpress架构的网站

    进去的时候需要重新配置数据库,按照之前获得的信息进行连接

    踩坑

            这里的数据库主机需要填Pod的通信IP,因为网站源码是一个Pod,MySQL数据库也是一个Pod,所以用外部IP是不好使的,需要Pod的IP

    image

            数据库用户密码的加密方式为PHPass,这是一个加盐加密的算法,如果要破解的话会很麻烦

            这里讲一个取巧的方法,因为我自己的博客网站也是使用的Wordpress搭建的,而在上一步我们已经可以拿到网站的数据库。所以我可以直接把我自己的数据库里的密码把嫌疑人网站的密码替换为我的密码

    Wordpress的用户密码存放在前缀_users表里

    这个是我的数据库

    可以直接把user_pass字段的数据复制到嫌疑人网站数据库

    image
    这个是嫌疑人的后台数据库
    image
    登录

    用原本的用户名和我替换之后的密码进行登录

    image

    成功

    image

    总结

            我这一次从零开始学习K8S的取证,算是完成了我在服务器取证领域里面对服务器集群进行取证的入门和起步。虽然说我耗时了半个月,但其实琢磨清楚之后这一套从分析到重构的流程其实并不难。所以我会继续探索。同时感谢我的师傅哥赵哥在这段时间给我的帮助。另外赵哥的独家秘笈我是不会外传的,所以文章并未体现出哈哈哈哈。


    网安杂谈
    关注电子数据取证与网络犯罪调查
     最新文章