Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ATTACKS] Additional edges #68

Merged
merged 1 commit into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion deployments/kubehound/janusgraph/kubehound-db-init.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ tokenBruteforce = mgmt.makeEdgeLabel('TOKEN_BRUTEFORCE').multiplicity(MULTI).mak
mgmt.addConnection(tokenBruteforce, role, identity);

tokenList = mgmt.makeEdgeLabel('TOKEN_LIST').multiplicity(MULTI).make();
mgmt.addConnection(tokenBruteforce, role, identity);
mgmt.addConnection(tokenList, role, identity);

tokenVarLog = mgmt.makeEdgeLabel('TOKEN_VAR_LOG_SYMLINK').multiplicity(ONE2MANY).make();
mgmt.addConnection(tokenVarLog, container, volume);
Expand All @@ -80,6 +80,9 @@ mgmt.addConnection(umhCorePattern, container, node);
privMount = mgmt.makeEdgeLabel('CE_PRIV_MOUNT').multiplicity(MANY2ONE).make();
mgmt.addConnection(privMount, container, node);

sysPtrace = mgmt.makeEdgeLabel('CE_SYS_PTRACE').multiplicity(MANY2ONE).make();
mgmt.addConnection(sysPtrace, container, node);


// All properties we will index on
cls = mgmt.makePropertyKey('class').dataType(String.class).cardinality(Cardinality.SINGLE).make();
Expand Down
61 changes: 61 additions & 0 deletions pkg/kubehound/graph/edge/escape_sys_ptrace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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/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/mongo/options"
)

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

// @@DOCLINK: https://datadoghq.atlassian.net/wiki/spaces/ASE/pages/3087665799/CE+SYS+PTRACE
type EscapeSysPtrace struct {
BaseContainerEscape
}

func (e *EscapeSysPtrace) Label() string {
return "CE_SYS_PTRACE"
}

func (e *EscapeSysPtrace) Name() string {
return "ContainerEscapeSysPtrace"
}

// Processor delegates the processing tasks to to the generic containerEscapeProcessor.
func (e *EscapeSysPtrace) Processor(ctx context.Context, oic *converter.ObjectIDConverter, entry any) (any, error) {
return containerEscapeProcessor(ctx, oic, e.Label(), entry)
}

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

containers := adapter.MongoDB(store).Collection(collections.ContainerName)

// Escape is possible with shared host pid namespace and SYS_PTRACE/SYS_ADMIN capabilities
filter := bson.M{
"$and": bson.A{
bson.M{"inherited.host_pid": true},
bson.M{"k8.securitycontext.capabilities.add": "SYS_PTRACE"},
bson.M{"k8.securitycontext.capabilities.add": "SYS_ADMIN"},
}}

// We just need a 1:1 mapping of the node and container to create this edge
projection := bson.M{"_id": 1, "node_id": 1}

cur, err := containers.Find(context.Background(), filter, options.Find().SetProjection(projection))
if err != nil {
return err
}
defer cur.Close(ctx)

return adapter.MongoCursorHandler[containerEscapeGroup](ctx, cur, callback, complete)
}
145 changes: 145 additions & 0 deletions pkg/kubehound/graph/edge/token_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
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/graph/vertex"
"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"
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(&TokenList{}, RegisterGraphMutation)
}

// @@DOCLINK: https://datadoghq.atlassian.net/wiki/spaces/ASE/pages/2886795639/TOKEN_LIST
type TokenList struct {
BaseEdge
}

type tokenListGroup struct {
Role primitive.ObjectID `bson:"_id" json:"role"`
}

func (e *TokenList) Label() string {
return "TOKEN_LIST"
}

func (e *TokenList) Name() string {
return "TokenListCluster"
}

func (e *TokenList) BatchSize() int {
if e.cfg.LargeClusterOptimizations {
// Under optimization this becomes a very cheap operation
return e.cfg.BatchSize
}

return e.cfg.BatchSizeClusterImpact
}

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

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

if e.cfg.LargeClusterOptimizations {
return map[any]any{
gremlin.T.Label: vertex.RoleLabel,
gremlin.T.Id: rid,
}, nil
}

return rid, nil
}

func (e *TokenList) Traversal() types.EdgeTraversal {
return func(source *gremlin.GraphTraversalSource, inserts []any) *gremlin.GraphTraversal {
g := source.GetGraphTraversal()
if e.cfg.LargeClusterOptimizations {
// In large clusters this can explode the number of edges and we can safely assume this is a critical issue
g.
Inject(inserts).
Unfold().
As("rtl").
MergeV(__.Select("rtl")).
Option(gremlin.Merge.OnCreate, __.Fail("missing role vertex on TOKEN_LIST insert")).
Option(gremlin.Merge.OnMatch, map[any]any{
"critical": true,
}).
Barrier().Limit(0)
} else {
// In smaller clusters we can still show the (large set of) attack paths generated by this attack
g.V().
HasLabel("Identity").
Has("class", "Identity").
As("i").
V(inserts...).
Has("critical", false).
AddE(e.Label()).
To("i").
Barrier().Limit(0)
}

return g
}
}

// Stream finds all roles that are NOT namespaced and have secrets/list or equivalent wildcard permissions.
func (e *TokenList) Stream(ctx context.Context, store storedb.Provider, _ cache.CacheReader,
callback types.ProcessEntryCallback, complete types.CompleteQueryCallback) error {

roles := adapter.MongoDB(store).Collection(collections.RoleName)
pipeline := []bson.M{
{
"$match": bson.M{
"is_namespaced": false,
"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": "secrets"},
bson.M{"resources": "*"},
}},
bson.M{"$or": bson.A{
bson.M{"verbs": "list"},
bson.M{"verbs": "*"},
}},
},
},
},
},
},
{
"$project": bson.M{
"_id": 1,
},
},
}

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

return adapter.MongoCursorHandler[tokenListGroup](ctx, cur, callback, complete)
}
128 changes: 128 additions & 0 deletions pkg/kubehound/graph/edge/token_list_namespace.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/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(&TokenListNamespace{}, RegisterDefault)
}

// @@DOCLINK: https://datadoghq.atlassian.net/wiki/spaces/ASE/pages/2886795639/TOKEN_LIST
type TokenListNamespace struct {
BaseEdge
}

type tokenListNSGroup struct {
Role primitive.ObjectID `bson:"_id" json:"role"`
Identity primitive.ObjectID `bson:"identity" json:"identity"`
}

func (e *TokenListNamespace) Label() string {
return "TOKEN_LIST"
}

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

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

return adapter.GremlinEdgeProcessor(ctx, oic, e.Label(), typed.Role, typed.Identity)
}

// Stream finds all roles that are namespaced and have secrets/list or equivalent wildcard permissions and matching identities.
// Matching identities are defined as namespaced identities that share the role namespace or non-namespaced identities.
func (e *TokenListNamespace) Stream(ctx context.Context, store storedb.Provider, _ cache.CacheReader,
callback types.ProcessEntryCallback, complete types.CompleteQueryCallback) error {

roles := adapter.MongoDB(store).Collection(collections.RoleName)
pipeline := []bson.M{
{
"$match": bson.M{
"is_namespaced": true,
"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": "secrets"},
bson.M{"resources": "*"},
}},
bson.M{"$or": bson.A{
bson.M{"verbs": "list"},
bson.M{"verbs": "*"},
}},
},
},
},
},
},
{
"$lookup": bson.M{
"as": "idsInNamespace",
"from": "identities",
"let": bson.M{
"roleNamespace": "$namespace",
},
"pipeline": []bson.M{
{
"$match": bson.M{"$and": bson.A{
bson.M{"$or": bson.A{
bson.M{"$expr": bson.M{
"$eq": bson.A{
"$namespace", "$$roleNamespace",
},
}},
bson.M{"is_namespaced": false},
}},
bson.M{"$or": bson.A{
bson.M{"type": "ServiceAccount"},
bson.M{"type": "User"},
}},
}},
},
{
"$project": bson.M{
"_id": 1,
},
},
},
},
},
{
"$unwind": "$idsInNamespace",
},
{
"$project": bson.M{
"_id": 1,
"identity": "$idsInNamespace._id",
},
},
}

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

return adapter.MongoCursorHandler[tokenListNSGroup](ctx, cur, callback, complete)
}
20 changes: 20 additions & 0 deletions test/setup/test-cluster/attacks/CE_SYS_PTRACE.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# CE_SYS_PTRACE edge
# @@DOCLINK: https://datadoghq.atlassian.net/wiki/spaces/ASE/pages/3087665799/CE+SYS+PTRACE
apiVersion: v1
kind: Pod
metadata:
name: sys-ptrace-pod
labels:
app: kubehound-edge-test
spec:
hostPID: true
containers:
- name: sys-ptrace-pod
image: ubuntu
command: [ "/bin/sh", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]
securityContext:
capabilities:
add:
- SYS_PTRACE
- SYS_ADMIN
Loading