Skip to content

Commit

Permalink
Merge pull request #6 from quarkslab/cloud-metadata
Browse files Browse the repository at this point in the history
Add the new CloudMetadata plugin
  • Loading branch information
mtardy authored Jul 11, 2022
2 parents 3359013 + 2209037 commit 25c6122
Show file tree
Hide file tree
Showing 4 changed files with 171 additions and 4 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions commands/dig.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
148 changes: 148 additions & 0 deletions pkg/plugins/cloudmetadata/cloudmetadata.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 25c6122

Please sign in to comment.