From 60a727dec22e6f86a27ca43330755a8942f3ec48 Mon Sep 17 00:00:00 2001 From: mtardy Date: Mon, 11 Jul 2022 17:10:25 +0200 Subject: [PATCH 1/3] Add new plugin to scan cloud metadata endpoints --- commands/root.go | 2 + pkg/plugins/cloudmetadata/cloudmetadata.go | 148 +++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 pkg/plugins/cloudmetadata/cloudmetadata.go diff --git a/commands/root.go b/commands/root.go index ffbad90..6c0a72b 100644 --- a/commands/root.go +++ b/commands/root.go @@ -13,6 +13,7 @@ import ( "github.com/quarkslab/kdigger/pkg/plugins/authorization" "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/devices" "github.com/quarkslab/kdigger/pkg/plugins/environment" "github.com/quarkslab/kdigger/pkg/plugins/mount" @@ -92,6 +93,7 @@ func registerBuckets() { cgroups.Register(buckets) node.Register(buckets) apiresources.Register(buckets) + cloudmetadata.Register(buckets) } // printResults prints results with the output format selected by the flags diff --git a/pkg/plugins/cloudmetadata/cloudmetadata.go b/pkg/plugins/cloudmetadata/cloudmetadata.go new file mode 100644 index 0000000..08e5019 --- /dev/null +++ b/pkg/plugins/cloudmetadata/cloudmetadata.go @@ -0,0 +1,148 @@ +package cloudmetadata + +import ( + "errors" + "net/http" + "time" + + "github.com/quarkslab/kdigger/pkg/bucket" +) + +const ( + bucketName = "cloudmetadata" + bucketDescription = "Cloudmetadata scans the usual metadata endpoints in public clouds." +) + +var bucketAliases = []string{"cloud", "meta"} + +// Thanks to: +// - https://gist.github.com/jhaddix/78cece26c91c6263653f31ba453e273b +// - https://gist.github.com/BuffaloWill/fa96693af67e3a3dd3fb +// - https://github.com/Prinzhorn/cloud-metadata-services +// I selected only one endpoint for each, trying to be exclusive, maybe it would +// be more robust to have multiples, with the ones that needs headers to be +// added (a security mesure) +// +// type CloudEndpoint struct { +// URLs []string +// Headers map[string]string +// } +var endpoints map[string]string = map[string]string{ + "DigitalOcean": "http://169.254.169.254/metadata/v1.json", + "AWS": "http://169.254.169.254/latest", + "OracleCloud": "http://192.0.0.192/latest/", + "Alibaba": "http://100.100.100.200/latest/meta-data/", + "GoogleCloud": "http://metadata.google.internal/computeMetadata/", // metadata.google.internal = 169.254.169.254 + "PacketCloud": "https://metadata.packet.net/userdata", + "Azure": "http://169.254.169.254/metadata/v1/maintenance", + "OpenStack": "http://169.254.169.254/openstack", +} + +type CloudMetadataBucket struct{} + +// wait for 100ms maximum, request should be quick +const networkTimeout = 100 * time.Millisecond + +// This plugin is "slow" because it has a network timeout on scan +func (n CloudMetadataBucket) Run() (bucket.Results, error) { + res := bucket.NewResults(bucketName) + + scanResult := scanEndpoints(endpoints) + + res.SetHeaders([]string{"cloudProvider", "success", "url", "error"}) + for _, resp := range scanResult { + if resp.Error != nil { + res.AddContent([]interface{}{resp.Platform, resp.Success, resp.URL, resp.Error.Error()}) + } else { + res.AddContent([]interface{}{resp.Platform, resp.Success, resp.URL, ""}) + } + } + + return *res, nil +} + +type Response struct { + Platform string + URL string + Success bool + Error error +} + +func scanEndpoints(endpoints map[string]string) []Response { + client := http.Client{ + Timeout: networkTimeout, + } + + chResponses := make(chan Response, len(endpoints)) + + for platform, url := range endpoints { + go func(ch chan Response, platform string, url string) { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + ch <- Response{ + Platform: platform, + URL: url, + Success: false, + Error: err, + } + return + } + + resp, err := client.Do(req) + if err != nil { + // chances of timeout here + // serr, ok := err.(*urlpkg.Error) + // if ok && serr.Timeout() { + // } + ch <- Response{ + Platform: platform, + URL: url, + Success: false, + Error: err, + } + return + } + + // defer resp.Body.Close() + if resp.StatusCode == http.StatusNotFound { + ch <- Response{ + Platform: platform, + URL: url, + Success: false, + Error: errors.New("not found"), + } + return + } + ch <- Response{ + Platform: platform, + URL: url, + Success: resp.StatusCode == http.StatusOK, + } + }(chResponses, platform, url) + } + + var results []Response + for i := 0; i < cap(chResponses); i++ { + results = append(results, <-chResponses) + } + + return results +} + +// 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 NewCloudMetadataBucket(config) + }, + SideEffects: false, + RequireClient: false, + }) +} + +func NewCloudMetadataBucket(config bucket.Config) (*CloudMetadataBucket, error) { + return &CloudMetadataBucket{}, nil +} From f052d7d51db0319a621bf688812e40a435914bf3 Mon Sep 17 00:00:00 2001 From: mtardy Date: Mon, 11 Jul 2022 17:25:46 +0200 Subject: [PATCH 2/3] Add documentation in the README about cloud metadata plugin --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 628db40..3a62549 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ canal](https://i.servimg.com/u/f41/11/93/81/35/digger10.jpg) * [Authorization](#authorization) * [Capabilities](#capabilities) * [Cgroups](#cgroups) + * [CloudMetadata](#cloudmetadata) * [Devices](#devices) * [Environment](#environment) * [Mount](#mount) @@ -347,6 +348,8 @@ $ kdigger ls | cgroups | [cgroup cg] | Cgroups reads the /proc/self/cgroup | false | false | | | | files that can leak information | | | | | | under cgroups v1. | | | +| cloudmetadata | [cloud meta] | Cloudmetadata scans the usual | false | false | +| | | metadata endpoints in public clouds. | | | | devices | [device dev] | Devices shows the list of devices | false | false | | | | available in the container. | | | | environment | [environments environ env] | Environment checks the presence of | false | false | @@ -499,6 +502,21 @@ about the container ID. See [this Stackoverflow thread](https://stackoverflow.com/a/69005753) and its related threads for more information. +### CloudMetadata + +Cloudmetadata scans the usual metadata endpoints in public clouds. It is usually +quite simple to find at which service provider a VM come from, because of many +leaks in the filesystems, the environment variables, etc. But from a containers +in a VM, it can be harder, that's why this plugin performs a scan on the network +via the usual service running at `169.254.169.254` or alike. See the source code +for endpoints used and links to more endpoints. + +This plugin only gives you the information of the availability of the main +endpoints, which means that you might running in a specific public cloud. If +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. + ### Devices Devices show the list of devices available in the container. This one is From 2209037f49370f87f306adf9a9cd9e0dc3fd9844 Mon Sep 17 00:00:00 2001 From: mtardy Date: Mon, 11 Jul 2022 17:31:07 +0200 Subject: [PATCH 3/3] Remove deprecated comments and add new ones --- commands/dig.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/commands/dig.go b/commands/dig.go index 217c23f..c6b91ee 100644 --- a/commands/dig.go +++ b/commands/dig.go @@ -97,11 +97,10 @@ arguments.`, args = removeDuplicates(args) // iterate through all the specified buckets + // TODO: some plugins might be slow, for example network scanners, so it + // might be a good idea in the future to parallelize the launch of these + // plugins for _, name := range args { - // all this retry machinery is done to lazy load the client and the - // checks are in case the plugin return ErrMissingClient forever - // and we are stuck in an infinite loop. Not the best design... - // initialize the bucket if buckets.RequiresClient(name) { err := loadContext(config)