Skip to content

Commit

Permalink
Add edge for shared process namespace attacks (#112)
Browse files Browse the repository at this point in the history
Add the edge attack for the Shared PS Namespace attack path.

Renamed the file to SHARE_PS_NAMESPACE to be consitent with the k8s api.

The links processing between each container is O(n^2) (n = container in a pod), which is required to make the number of hop correct (we can't just do A -> B -> C, we need to do A ->B, A->C, B->C).

The tests adds a new pods so we have 2 distinct pods to make sure we aren't linking them together.
  • Loading branch information
edznux-dd authored Sep 20, 2023
1 parent 505bab4 commit 74d542f
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 30 deletions.
2 changes: 1 addition & 1 deletion deployments/kubehound/kubegraph/kubehound-db-init.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ mgmt.addConnection(hostRead, volume, node);
hostTraverse = mgmt.makeEdgeLabel('EXPLOIT_HOST_TRAVERSE').multiplicity(MULTI).make();
mgmt.addConnection(hostTraverse, volume, volume);

sharedPs = mgmt.makeEdgeLabel('SHARED_PS_NAMESPACE').multiplicity(MULTI).make();
sharedPs = mgmt.makeEdgeLabel('SHARE_PS_NAMESPACE').multiplicity(MULTI).make();
mgmt.addConnection(sharedPs, container, container);

containerAttach = mgmt.makeEdgeLabel('CONTAINER_ATTACH').multiplicity(ONE2MANY).make();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
---
title: SHARED_PS_NAMESPACE
title: SHARE_PS_NAMESPACE
---

<!--
id: SHARED_PS_NAMESPACE
id: SHARE_PS_NAMESPACE
TODO: phrase as an attack
name: "Access container in shared process namespace"
mitreAttackTechnique: N/A - N/A
mitreAttackTactic: TA0008 - Lateral Movement
-->

# SHARED_PS_NAMESPACE
# SHARE_PS_NAMESPACE

| Source | Destination | MITRE |
| --------------------------- | ------------------------------------- |----------------------------------|
Expand All @@ -24,9 +24,9 @@ Pods represent one or more containers with shared storage and network resources.

## Prerequisites

Access to a container within a pod running other containers with shared process nanespaces
Access to a container within a pod running other containers with shared process namespaces

See the [example pod spec](https://github.com/DataDog/KubeHound/tree/main/test/setuptest-cluster/attacks/SHARED_PS_NAMESPACE.yaml).
See the [example pod spec](https://github.com/DataDog/KubeHound/tree/main/test/setuptest-cluster/attacks/SHARE_PS_NAMESPACE.yaml).

## Checks

Expand Down Expand Up @@ -87,7 +87,7 @@ Prevent the use of shared namespaces in pods, where containers have different ri

## Calculation

+ [SharedPsNamespace](https://github.com/DataDog/KubeHound/tree/main/pkg/kubehound/graph/edge/shared_ps_namespace.go)
+ [SharedPsNamespace](https://github.com/DataDog/KubeHound/tree/main/pkg/kubehound/graph/edge/share_ps_namespace.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 @@ -26,7 +26,7 @@ hide:
| [POD_EXEC](./POD_EXEC.md) | Exec into running pod | N/A | Lateral Movement |
| [POD_PATCH](./POD_PATCH.md) | Patch running pod | N/A | Lateral Movement |
| [ROLE_BIND](./ROLE_BIND.md) | Create role binding | Valid Accounts | Privilege Escalation |
| [SHARED_PS_NAMESPACE](./SHARED_PS_NAMESPACE.md) | Access container in shared process namespace | N/A | Lateral Movement |
| [SHARE_PS_NAMESPACE](./SHARE_PS_NAMESPACE.md) | Access container in shared process namespace | N/A | Lateral Movement |
| [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 |
Expand Down
116 changes: 116 additions & 0 deletions pkg/kubehound/graph/edge/share_ps_namespace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
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/storage/cache"
"github.com/DataDog/KubeHound/pkg/kubehound/storage/storedb"
"github.com/DataDog/KubeHound/pkg/kubehound/store/collections"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)

func init() {
Register(&SharePSNamespace{}, RegisterDefault)
}

type SharePSNamespace struct {
BaseEdge
}

type sharedPsNamespaceGroup struct {
Containers []primitive.ObjectID `bson:"container_ids" json:"container_ids"`
}
type sharedPsNamespaceGroupPair struct {
ContainerA primitive.ObjectID `bson:"container_a_id" json:"container_a"`
ContainerB primitive.ObjectID `bson:"container_b_id" json:"container_b"`
}

func (e *SharePSNamespace) Label() string {
return "SHARE_PS_NAMESPACE"
}

func (e *SharePSNamespace) Name() string {
return "SharePSNamespace"
}

// Processor delegates the processing tasks to to the generic containerEscapeProcessor.
func (e *SharePSNamespace) Processor(ctx context.Context, oic *converter.ObjectIDConverter, entry any) (any, error) {
typed, ok := entry.(*sharedPsNamespaceGroupPair)
if !ok {
return nil, fmt.Errorf("invalid type passed to processor: %T", entry)
}

return adapter.GremlinEdgeProcessor(ctx, oic, e.Label(), typed.ContainerA, typed.ContainerB)
}

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

coll := adapter.MongoDB(store).Collection(collections.PodName)
pipeline := bson.A{
bson.D{{"$match", bson.D{{"k8.spec.shareprocessnamespace", true}}}},
bson.D{
{"$lookup",
bson.D{
{"from", "containers"},
{"localField", "_id"},
{"foreignField", "pod_id"},
{"as", "containers_with_shared_ns"},
},
},
},
bson.D{
{"$project",
bson.D{
{"_id", 1},
{"containers_with_shared_ns", bson.D{{"_id", 1}}},
},
},
},
bson.D{
{"$project",
bson.D{
{"_id", 0},
{"container_ids", "$containers_with_shared_ns._id"},
},
},
},
}
cur, err := coll.Aggregate(ctx, pipeline)

if err != nil {
return err
}
defer cur.Close(ctx)

for cur.Next(ctx) {
var entry sharedPsNamespaceGroup
err := cur.Decode(&entry)
if err != nil {
return err
}

for _, containerSrc := range entry.Containers {
for _, containerDst := range entry.Containers {
// No need to create a link with itself
if containerSrc == containerDst {
continue
}
err = callback(ctx, &sharedPsNamespaceGroupPair{
ContainerA: containerSrc,
ContainerB: containerDst,
})
if err != nil {
return err
}
}
}
}

return complete(ctx)
}
14 changes: 0 additions & 14 deletions test/setup/test-cluster/attacks/SHARED_PS_NAMESPACE.yaml

This file was deleted.

41 changes: 41 additions & 0 deletions test/setup/test-cluster/attacks/SHARE_PS_NAMESPACE.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# SHARE_PS_NAMESPACE edge
# There's 2 pods here so we can also test we don't mix both pods shared NS together.
apiVersion: v1
kind: Pod
metadata:
name: sharedps-pod1
labels:
app: kubehound-edge-test
spec:
shareProcessNamespace: true
containers:
- name: sharedps-pod1-a
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
- name: sharedps-pod1-b
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
- name: sharedps-pod1-c
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
---
apiVersion: v1
kind: Pod
metadata:
name: sharedps-pod2
labels:
app: kubehound-edge-test
spec:
shareProcessNamespace: true
containers:
- name: sharedps-pod2-a
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
- name: sharedps-pod2-b
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
35 changes: 33 additions & 2 deletions test/system/graph_edge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ func (suite *EdgeTestSuite) TestEdge_POD_PATCH() {
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[tokenget-pod]",
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[nsenter-pod]",
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[varlog-pod]",
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[sharedps-pod]",
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[sharedps-pod1]",
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[sharedps-pod2]",
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[umh-core-pod]",
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[pod-patch-pod]",
"path[map[name:[patch-pods::pod-patch-pods]], map[], map[name:[pod-exec-pod]",
Expand Down Expand Up @@ -318,7 +319,8 @@ func (suite *EdgeTestSuite) TestEdge_POD_EXEC() {
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[tokenget-pod]",
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[nsenter-pod]",
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[varlog-pod]",
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[sharedps-pod]",
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[sharedps-pod1]",
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[sharedps-pod2]",
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[umh-core-pod]",
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[pod-patch-pod]",
"path[map[name:[exec-pods::pod-exec-pods]], map[], map[name:[pod-exec-pod]",
Expand Down Expand Up @@ -638,6 +640,35 @@ func (suite *EdgeTestSuite) TestEdge_ENDPOINT_EXPLOIT_External() {
suite.Subset(paths, expected)
}

func (suite *EdgeTestSuite) TestEdge_SHARE_PS_NAMESPACE() {
results, err := suite.g.V().
HasLabel("Container").
OutE().HasLabel("SHARE_PS_NAMESPACE").
InV().HasLabel("Container").
Path().
By(__.ValueMap("name")).
ToList()

suite.NoError(err)
suite.GreaterOrEqual(len(results), 1)

paths := suite.pathsToStringArray(results)
expected := []string{
// Pod1 a 3 containers (A,B,C) = 6 links
"path[map[name:[sharedps-pod1-a]], map[], map[name:[sharedps-pod1-b]",
"path[map[name:[sharedps-pod1-a]], map[], map[name:[sharedps-pod1-c]",
"path[map[name:[sharedps-pod1-b]], map[], map[name:[sharedps-pod1-a]",
"path[map[name:[sharedps-pod1-b]], map[], map[name:[sharedps-pod1-c]",
"path[map[name:[sharedps-pod1-c]], map[], map[name:[sharedps-pod1-b]",
"path[map[name:[sharedps-pod1-c]], map[], map[name:[sharedps-pod1-a]",

// Pod1 a 2 containers (A,B) = 2 links
"path[map[name:[sharedps-pod2-a]], map[], map[name:[sharedps-pod2-b]",
"path[map[name:[sharedps-pod2-b]], map[], map[name:[sharedps-pod2-a]",
}
suite.ElementsMatch(paths, expected)
}

func (suite *EdgeTestSuite) Test_NoEdgeCase() {
// The control pod has no interesting properties and therefore should have NO outgoing edges
results, err := suite.g.V().
Expand Down
Loading

0 comments on commit 74d542f

Please sign in to comment.