Skip to content

Commit

Permalink
Add /var/log edge (#108)
Browse files Browse the repository at this point in the history
This adds the var log edge attack path
  • Loading branch information
edznux-dd authored Oct 12, 2023
1 parent f3b5206 commit 6f1b852
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 22 deletions.
6 changes: 3 additions & 3 deletions deployments/kubehound/kubegraph/kubehound-db-init.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,6 @@ mgmt.addConnection(tokenBruteforce, permissionSet, identity);
tokenList = mgmt.makeEdgeLabel('TOKEN_LIST').multiplicity(MULTI).make();
mgmt.addConnection(tokenList, permissionSet, identity);

tokenVarLog = mgmt.makeEdgeLabel('TOKEN_VAR_LOG_SYMLINK').multiplicity(ONE2MANY).make();
mgmt.addConnection(tokenVarLog, container, volume);

nsenter = mgmt.makeEdgeLabel('CE_NSENTER').multiplicity(MANY2ONE).make();
mgmt.addConnection(nsenter, container, node);

Expand All @@ -99,6 +96,9 @@ mgmt.addConnection(privMount, container, node);
sysPtrace = mgmt.makeEdgeLabel('CE_SYS_PTRACE').multiplicity(MANY2ONE).make();
mgmt.addConnection(sysPtrace, container, node);

varLogSymLink = mgmt.makeEdgeLabel('CE_VAR_LOG_SYMLINK').multiplicity(MULTI).make();
mgmt.addConnection(varLogSymLink, container, node);

endpointExploit = mgmt.makeEdgeLabel('ENDPOINT_EXPLOIT').multiplicity(MULTI).make();
mgmt.addConnection(endpointExploit, endpoint, container);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
---
title: TOKEN_VAR_LOG_SYMLINK
title: CE_VAR_LOG_SYMLINK
---

<!--
id: TOKEN_VAR_LOG_SYMLINK
name: "Steal service account token from volume"
id: CE_VAR_LOG_SYMLINK
name: "Arbitrary file reads on the host"
mitreAttackTechnique: T1552 - Unsecured Credentials
mitreAttackTactic: TA0006 - Credential Access
-->

# TOKEN_VAR_LOG_SYMLINK
# CE_VAR_LOG_SYMLINK

| Source | Destination | MITRE |
| ----------------------------------------- | ------------------------------------- |----------------------------------|
| [Container](../entities/container.md) | [Node](../entities/node.md) | [Escape to Host, T1611](https://attack.mitre.org/techniques/T1611/) |

Steal all K8s API tokens from a node via an exposed `/var/log` mount.
Arbitrary file reads on the host from a node via an exposed `/var/log` mount.

## Details

A pod running as root and with a mount point to the node’s `/var/log` directory can expose the entire contents of its host filesystem to any user who has access to its logs, enabling an attacker to steal all K8s API tokens present on the K8s node. See [Kubernetes Pod Escape Using Log Mounts](https://blog.aquasec.com/kubernetes-security-pod-escape-log-mounts) for a more detailed explanation of the technique.
A pod running as root and with a mount point to the node’s `/var/log` directory can expose the entire contents of its host filesystem to any user who has access to its logs, enabling an attacker to read arbitrary files on the host node. See [Kubernetes Pod Escape Using Log Mounts](https://blog.aquasec.com/kubernetes-security-pod-escape-log-mounts) for a more detailed explanation of the technique.

## Prerequisites

Expand Down Expand Up @@ -115,7 +115,7 @@ Avoid running containers as the `root` user. Enforce running as an unprivileged

## Calculation

+ [TokenVarLogSymlink](https://github.com/DataDog/KubeHound/tree/main/pkg/kubehound/graph/edge/token_var_log_symlink.go)
+ [EscapeVarLogSymlink](https://github.com/DataDog/KubeHound/tree/main/pkg/kubehound/graph/edge/escape_var_log_symlink.go)

## References:

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/attacks/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@ hide:
| [TOKEN_BRUTEFORCE](./TOKEN_BRUTEFORCE.md) | Brute-force secret name of service account token | Steal Application Access Token | Credential Access |
| [TOKEN_LIST](./TOKEN_LIST.md) | Access service account token secrets | Steal Application Access Token | Credential Access |
| [TOKEN_STEAL](./TOKEN_STEAL.md) | Steal service account token from volume | Unsecured Credentials | Credential Access |
| [TOKEN_VAR_LOG_SYMLINK](./TOKEN_VAR_LOG_SYMLINK.md) | Steal service account token from volume | Unsecured Credentials | Credential Access |
| [CE_VAR_LOG_SYMLINK](./CE_VAR_LOG_SYMLINK.md) | Read file from sensitive host mount | Escape to host | Privilege escalation |
| [VOLUME_ACCESS](./VOLUME_ACCESS.md) | Access host volume | Container and Resource Discovery | Discovery |
| [VOLUME_DISCOVER](./VOLUME_DISCOVER.md) | Enumerate mounted volumes | Container and Resource Discovery | Discovery |
128 changes: 128 additions & 0 deletions pkg/kubehound/graph/edge/escape_var_log_symlink.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package edge

import (
"context"
"fmt"

"github.com/DataDog/KubeHound/pkg/kubehound/graph/adapter"
"github.com/DataDog/KubeHound/pkg/kubehound/graph/types"
"github.com/DataDog/KubeHound/pkg/kubehound/models/converter"
"github.com/DataDog/KubeHound/pkg/kubehound/models/shared"
"github.com/DataDog/KubeHound/pkg/kubehound/storage/cache"
"github.com/DataDog/KubeHound/pkg/kubehound/storage/storedb"
"github.com/DataDog/KubeHound/pkg/kubehound/store/collections"
gremlin "github.com/apache/tinkerpop/gremlin-go/v3/driver"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)

func init() {
Register(&EscapeVarLogSymlink{}, RegisterGraphDependency)
}

type EscapeVarLogSymlink struct {
BaseContainerEscape
}

// The mongodb query returns a list of permissionSet
type permissionSetIDEscapeGroup struct {
PermissionSetID primitive.ObjectID `bson:"_id" json:"permission_set"`
}

func (e *EscapeVarLogSymlink) Label() string {
return "CE_VAR_LOG_SYMLINK"
}

// List of needed edges to run the traversal query
func (e *EscapeVarLogSymlink) Dependencies() []string {
return []string{"PERMISSION_DISCOVER", "IDENTITY_ASSUME", "VOLUME_DISCOVER", "VOLUME_ACCESS"}
}

func (e *EscapeVarLogSymlink) Name() string {
return "ContainerEscapeVarLogSymlink"
}

func (e *EscapeVarLogSymlink) Processor(ctx context.Context, oic *converter.ObjectIDConverter, entry any) (any, error) {
typed, ok := entry.(*permissionSetIDEscapeGroup)
if !ok {
return nil, fmt.Errorf("Invalid type passed to processor: %T", entry)
}

permissionSetVertexID, err := oic.GraphID(ctx, typed.PermissionSetID.Hex())
if err != nil {
return nil, fmt.Errorf("%s edge IN id convert: %w", e.Label(), err)
}

return permissionSetVertexID, nil
}

func (e *EscapeVarLogSymlink) Traversal() types.EdgeTraversal {
return func(source *gremlin.GraphTraversalSource, inserts []any) *gremlin.GraphTraversal {
g := source.GetGraphTraversal()
// reduce the graph to only these permission sets
g.V(inserts...).HasLabel("PermissionSet").
// get identity vertices
InE("PERMISSION_DISCOVER").OutV().
// get container vertices
InE("IDENTITY_ASSUME").OutV().
// save container vertices as "c" so we can link to it to the node via CE_VAR_LOG_SYMLINK
HasLabel("Container").As("c").
// Get all the volumes
OutE("VOLUME_DISCOVER").InV().
Has("type", shared.VolumeTypeHost).
// filter only the volumes that are "affected" by this attacks ("/", "/var", "/var/log").
Has("sourcePath", P.Within("/", "/var", "/var/log")).
// get the node related to that volume mount
InE("VOLUME_ACCESS").OutV().
HasLabel("Node").As("n").
AddE("CE_VAR_LOG_SYMLINK").From("c").To("n").
Barrier().Limit(0)

return g
}
}

func (e *EscapeVarLogSymlink) Stream(ctx context.Context, store storedb.Provider, _ cache.CacheReader,
callback types.ProcessEntryCallback, complete types.CompleteQueryCallback) error {

permissionSets := adapter.MongoDB(store).Collection(collections.PermissionSetName)
pipeline := []bson.M{
{
"$match": bson.M{
"rules": bson.M{
"$elemMatch": bson.M{
"$and": bson.A{
bson.M{"$or": bson.A{
bson.M{"apigroups": ""},
bson.M{"apigroups": "*"},
}},
bson.M{"$or": bson.A{
bson.M{"resources": "pods/log"},
bson.M{"resources": "pods/*"},
bson.M{"resources": "*"},
}},
bson.M{"$or": bson.A{
bson.M{"verbs": "get"},
bson.M{"verbs": "*"},
}},
bson.M{"resourcenames": nil}, // TODO: handle resource scope
},
},
},
},
},
{
"$project": bson.M{
"_id": 1,
},
},
}

cur, err := permissionSets.Aggregate(ctx, pipeline)
if err != nil {
return err
}
defer cur.Close(ctx)

return adapter.MongoCursorHandler[permissionSetIDEscapeGroup](ctx, cur, callback, complete)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TOKEN_VAR_LOG_SYMLINK edge
# CE_VAR_LOG_SYMLINK edge
apiVersion: v1
kind: ServiceAccount
metadata:
Expand Down Expand Up @@ -26,7 +26,7 @@ roleRef:
name: read-logs
subjects:
- kind: ServiceAccount
name: varrlog-sa
name: varlog-sa
namespace: default
---
apiVersion: v1
Expand All @@ -38,7 +38,7 @@ metadata:
app: kubehound-edge-test
spec:
containers:
- name: varlog-pod
- name: varlog-container
image: ubuntu
volumeMounts:
- mountPath: /host/var/log
Expand Down
5 changes: 3 additions & 2 deletions test/system/graph_dsl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ func (suite *DslTestSuite) TestTraversalSource_escapes() {
"path[modload-pod, CE_MODULE_LOAD, Node]",
"path[kube-proxy, CE_MODULE_LOAD, Node]",
"path[kube-proxy, CE_PRIV_MOUNT, Node]",
"path[varlog-container, CE_VAR_LOG_SYMLINK, Node]",
}

suite.ElementsMatch(escapes, expected)
Expand Down Expand Up @@ -167,7 +168,7 @@ func (suite *DslTestSuite) TestTraversalSource_hostMounts() {
func (suite *DslTestSuite) TestTraversalSource_identities() {
ids := suite.testScriptArray("kh.identities().has('namespace', 'default').values('name')")
expected := []string{
"varrlog-sa", "rolebind-sa", "tokenget-sa", "pod-create-sa",
"varlog-sa", "rolebind-sa", "tokenget-sa", "pod-create-sa",
"impersonate-sa", "pod-exec-sa", "tokenlist-sa", "pod-patch-sa",
}

Expand All @@ -177,7 +178,7 @@ func (suite *DslTestSuite) TestTraversalSource_identities() {
func (suite *DslTestSuite) TestTraversalSource_sas() {
ids := suite.testScriptArray("kh.sas().has('namespace', 'default').values('name')")
expected := []string{
"varrlog-sa", "rolebind-sa", "tokenget-sa", "pod-create-sa",
"varlog-sa", "rolebind-sa", "tokenget-sa", "pod-create-sa",
"impersonate-sa", "pod-exec-sa", "tokenlist-sa", "pod-patch-sa",
}

Expand Down
10 changes: 9 additions & 1 deletion test/system/graph_edge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ func (suite *EdgeTestSuite) TestEdge_PERMISSION_DISCOVER() {
"path[map[name:[tokenlist-sa]], map[], map[name:[list-secrets::pod-list-secrets]",
"path[map[name:[pod-exec-sa]], map[], map[name:[exec-pods::pod-exec-pods]",
"path[map[name:[impersonate-sa]], map[], map[name:[impersonate::pod-impersonate]",
"path[map[name:[varrlog-sa]], map[], map[name:[read-logs::pod-read-logs]",
"path[map[name:[varlog-sa]], map[], map[name:[read-logs::pod-read-logs]",
}

suite.Subset(paths, expected)
Expand Down Expand Up @@ -684,6 +684,14 @@ func (suite *EdgeTestSuite) Test_NoEdgeCase() {
suite.Equal(len(results), 0)
}

func (suite *EdgeTestSuite) Test_CE_VAR_LOG_SYMLINK() {
containers := map[string]bool{
"varlog-container": true,
}

suite._testContainerEscape("CE_VAR_LOG_SYMLINK", DefaultContainerEscapeNodes, containers)
}

func TestEdgeTestSuite(t *testing.T) {
suite.Run(t, new(EdgeTestSuite))
}
Expand Down
4 changes: 2 additions & 2 deletions test/system/graph_vertex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ func (suite *VertexTestSuite) TestVertexPod() {
suite.Equal(expectedPods, resultsMap)
}

func (suite *VertexTestSuite) TestVertexPerrmissionSet() {
func (suite *VertexTestSuite) TestVertexPermissionSet() {
results, err := suite.g.V().
HasLabel(vertex.PermissionSetLabel).
Has("namespace", "default").
Expand Down Expand Up @@ -297,7 +297,7 @@ func (suite *VertexTestSuite) TestVertexCritical() {
func (suite *VertexTestSuite) TestVertexVolume() {
results, err := suite.g.V().HasLabel(vertex.VolumeLabel).ElementMap().ToList()
suite.NoError(err)
suite.Equal(51, len(results))
suite.Equal(52, len(results))

results, err = suite.g.V().HasLabel(vertex.VolumeLabel).Has("sourcePath", "/proc/sys/kernel").Has("name", "nodeproc").ElementMap().ToList()
suite.NoError(err)
Expand Down
6 changes: 3 additions & 3 deletions test/system/vertex.gen.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// PLEASE DO NOT EDIT
// THIS HAS BEEN GENERATED AUTOMATICALLY on 2023-09-19 16:48
// THIS HAS BEEN GENERATED AUTOMATICALLY on 2023-10-06 15:45
//
// Generate it with "go generate ./..."
//
Expand Down Expand Up @@ -688,9 +688,9 @@ var expectedContainers = map[string]graph.Container{
// Node: "",
Compromised: 0,
},
"varlog-pod": {
"varlog-container": {
StoreID: "",
Name: "varlog-pod",
Name: "varlog-container",
Image: "ubuntu",
Command: []string{},
Args: []string{},
Expand Down

0 comments on commit 6f1b852

Please sign in to comment.