diff --git a/README.md b/README.md index 27fb6e0..fc99bed 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ pentesting process. * [Why another tool?](#why-another-tool) * [How is this tool built?](#how-is-this-tool-built) * [Areas for improvement](#areas-for-improvement) - * [How can I experience with this tool?](#how-can-i-experience-with-this-tool) + * [How to experiment with this tool?](#how-to-experiment-with-this-tool) * [Buckets](#buckets) * [Admission](#admission) * [API Resources](#api-resources) @@ -42,6 +42,7 @@ pentesting process. * [Capabilities](#capabilities) * [Cgroups](#cgroups) * [CloudMetadata](#cloudmetadata) + * [ContainerDetect](#containerdetect) * [Devices](#devices) * [Environment](#environment) * [Mount](#mount) @@ -72,7 +73,7 @@ page](https://github.com/quarkslab/kdigger/releases). ### Build from source -Just type `make` to build with the +Just type `make` to build with the [default build target](https://github.com/quarkslab/kdigger/blob/main/Makefile#L20). ```bash git clone https://github.com/quarkslab/kdigger @@ -332,11 +333,11 @@ format via array lines does not fit all the use cases perfectly but is simple to generalize without having each plugin to implement their format. The tool also proposes a JSON output format. -### How can I experience with this tool? +### How to experiment with this tool? -Good news! We created a mini Kubernetes CTF with basic steps to experience with -the tool and resolve quick challenges. For more information go to the -[minik8s-ctf repository](https://github.com/quarkslab/minik8s-ctf). +Good news! We created a mini Kubernetes CTF with basic steps to try the tool +and resolve quick challenges. For more information go to the [minik8s-ctf +repository](https://github.com/quarkslab/minik8s-ctf). ## Buckets @@ -531,6 +532,20 @@ that's the case, further research, using available endpoints for that cloud, can be conducted. You can potentially retrieve an authentication token or simply more metadata to pivot within the cloud account. +### ContainerDetect + +ContainerDetect retrieves hints that the process is running inside a typical +container. This bucket follows a discussion on Twitter about detection technics +https://twitter.com/g3rzi/status/1564594977220562945. + +For now, it's composed of six hints: +- systemd is not PID 1 +- kthreadd is not PID 2 +- inode number of root is not 2 +- root is an overlay fs +- /etc/fstab is empty +- /boot is empty + ### Devices Devices show the list of devices available in the container. This one is @@ -615,7 +630,7 @@ in the cluster. Note: [This feature was removed](https://github.com/coredns/coredns/pull/5019) starting as of CoreDNS v1.9.0 because it was mostly used by bad actors (like this tool). See the associated discussion on [the corresponding Github -issue](https://github.com/coredns/coredns/issues/4984). Kubernetes v1.24 was +issue](https://github.com/coredns/coredns/issues/4984). Kubernetes v1.24 was still using CoreDNS v1.8.6, but the v1.25 version updated CoreDNS to v1.9.3. That's why this plugin no longer works on v1.25 and above. diff --git a/commands/root.go b/commands/root.go index 44a24be..483db7e 100644 --- a/commands/root.go +++ b/commands/root.go @@ -14,6 +14,7 @@ import ( "github.com/quarkslab/kdigger/pkg/plugins/capabilities" "github.com/quarkslab/kdigger/pkg/plugins/cgroups" "github.com/quarkslab/kdigger/pkg/plugins/cloudmetadata" + "github.com/quarkslab/kdigger/pkg/plugins/containerdetect" "github.com/quarkslab/kdigger/pkg/plugins/devices" "github.com/quarkslab/kdigger/pkg/plugins/environment" "github.com/quarkslab/kdigger/pkg/plugins/mount" @@ -94,6 +95,7 @@ func registerBuckets() { node.Register(buckets) apiresources.Register(buckets) cloudmetadata.Register(buckets) + containerdetect.Register(buckets) } // printResults prints results with the output format selected by the flags diff --git a/pkg/plugins/containerdetect/containerdetect.go b/pkg/plugins/containerdetect/containerdetect.go new file mode 100644 index 0000000..3b1971c --- /dev/null +++ b/pkg/plugins/containerdetect/containerdetect.go @@ -0,0 +1,134 @@ +package containerdetect + +import ( + "bytes" + "fmt" + "os" + "syscall" + + "github.com/mitchellh/go-ps" + "github.com/quarkslab/kdigger/pkg/bucket" + "github.com/quarkslab/kdigger/pkg/plugins/mount" +) + +// this bucket follows a discussion on twitter +// https://twitter.com/g3rzi/status/1564594977220562945 + +const ( + bucketName = "containerdetect" + bucketDescription = "ContainerDetect retrieves hints that the process is running inside a typical container." +) + +var bucketAliases = []string{"container", "cdetect"} + +type Bucket struct{} + +func (n Bucket) Run() (bucket.Results, error) { + res := bucket.NewResults(bucketName) + res.SetHeaders([]string{"hint", "result"}) + + // there is a systemd with pid 1 on the host + systemdFirstPID, err := isProcessPID("systemd", 1) + if err != nil { + return *res, err + } + res.AddContent([]interface{}{"systemd is not PID 1", !systemdFirstPID}) + + // there is a kthreadd with pid 2 on the host + kthreadSecondPID, err := isProcessPID("kthreadd", 2) + if err != nil { + return *res, err + } + res.AddContent([]interface{}{"kthreadd is not PID 2", !kthreadSecondPID}) + + // the inode of root on the host should be 2 + root, err := os.Stat("/") + if err != nil { + return *res, err + } + if stat, ok := root.Sys().(*syscall.Stat_t); ok { + res.AddContent([]interface{}{"inode number of root is not 2", !(stat.Ino == 2)}) + } + + // root as an overlay filesystem might imply running in a container + mnts, err := mount.Mounts() + if err != nil { + return *res, err + } + isRootOverlayFS := false + for _, mnt := range mnts { + if mnt.Filesystem == "overlay" && mnt.Path == "/" { + isRootOverlayFS = true + } + } + res.AddContent([]interface{}{"root is an overlay fs", isRootOverlayFS}) + + // /etc/fstab might be empty in a container + isFstabEmpty, err := isFstabEmpty() + if err != nil { + return *res, err + } + res.AddContent([]interface{}{"/etc/fstab is empty", isFstabEmpty}) + + // /boot might be empty in a container + isBootEmpty, err := isBootFolderEmpty() + if err != nil { + return *res, err + } + res.AddContent([]interface{}{"/boot is empty", isBootEmpty}) + + res.AddComment("A majority of true hints might imply running in a container.") + + return *res, nil +} + +func isProcessPID(process string, pid int) (bool, error) { + p, err := ps.FindProcess(pid) + if err != nil { + return false, fmt.Errorf("failed to find process %d: %w", pid, err) + } + if p != nil && p.Executable() == process { + return true, nil + } + return false, nil +} + +func isFstabEmpty() (bool, error) { + file, err := os.ReadFile("/etc/fstab") + if err != nil { + return false, err + } + lines := bytes.Split(file, []byte("\n")) + for _, line := range lines { + if len(line) != 0 && line[0] != '#' { + return false, nil + } + } + return true, nil +} + +func isBootFolderEmpty() (bool, error) { + files, err := os.ReadDir("/boot") + if err != nil { + return false, err + } + return len(files) == 0, nil +} + +// Register registers a plugin +func Register(b *bucket.Buckets) { + b.Register(bucket.Bucket{ + Name: bucketName, + Description: bucketDescription, + Aliases: bucketAliases, + Factory: func(config bucket.Config) (bucket.Interface, error) { + return NewContainerDetectBucket(config) + }, + SideEffects: false, + RequireClient: false, + }) +} + +func NewContainerDetectBucket(config bucket.Config) (*Bucket, error) { + return &Bucket{}, nil +} diff --git a/pkg/plugins/mount/mount.go b/pkg/plugins/mount/mount.go index af0724b..c1b33a2 100644 --- a/pkg/plugins/mount/mount.go +++ b/pkg/plugins/mount/mount.go @@ -22,7 +22,7 @@ var bucketAliases = []string{"mounts", "mn"} type Bucket struct{} func (m Bucket) Run() (bucket.Results, error) { - values, err := mounts() + values, err := Mounts() if err != nil { return bucket.Results{}, err } @@ -59,7 +59,7 @@ type Mount struct { Flags string } -func mounts() ([]Mount, error) { +func Mounts() ([]Mount, error) { file, err := os.Open(mountPath) if err != nil { return nil, err