The host threat model

Attack surface of a default server, and assume-breach.

Advanced14 min · lesson 1 of 15

This course assumes the attacker already got in. The hardening course (linux-hard) was about keeping them out: fewer services, tighter sudo, signed modules, a working firewall. This one starts one step later, when prevention has already failed and someone is running code on your host. The job changes from "how do I lock the door" to "what will they do next, what would I see, and what do I do about it." It builds on linux-ess and linux-hard: you should be comfortable with services, accounts, sudo, SSH and auditd rules before you start here. Everything runs on Ubuntu Server 26.04.1 LTS (kernel 7.0.0-34, aarch64 in the lab), with Rocky Linux 10.2 shown where RHEL differs. By the end of this lesson you can enumerate the real attack surface of a default 26.04 server the way an intruder does, and explain why a change to that surface, not the surface itself, is the thing to alert on.

You do not land as root

An attacker who lands on a Linux host almost never arrives as root, the account with user ID 0 that every permission check waves through. They arrive as a nobody: a web app running as its service account gets exploited, a low-privilege key is copied off a laptop, a poisoned dependency runs inside whatever account pulled it in. To see that starting position, create a stand-in service account, become it, and ask the two questions an intruder asks first: who am I, and what can I touch. The account is a throwaway made for this lesson; the terminal removes it again at the end.

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ sudo useradd --system --no-create-home --shell /usr/sbin/nologin tm-www
$ sudo -u tm-www id
uid=999(tm-www) gid=987(tm-www) groups=987(tm-www)
$ sudo -u tm-www cat /etc/shadow
cat: /etc/shadow: Permission denied
$ sudo userdel tm-www

uid=999(tm-www) is a system account (the numbers on your machine will differ; useradd --system picks a free ID from the system range below 1000) with no login shell and no membership in sudo or adm, the groups that would grant administrative power or the right to read system logs. It cannot read /etc/shadow, the file that holds password hashes, so the read is denied. What it does have is code execution and time, and a competent intruder spends that time looking, not smashing. The rest of this lesson is that look, run against a default install so the numbers are real.

The attack surface of a default server

The first thing worth mapping is every program that runs as root regardless of who starts it: the SUID-root binaries, which a normal user can launch and which run with root's authority for their lifetime. "SUID, SGID and the sticky bit" in Linux essentials explains the bit and walks through the default 26.04 set; here you read that set the way an intruder does. List it with one command.

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ find / -xdev -perm -4000 -type f 2>/dev/null
/usr/bin/fusermount3 /usr/bin/gpasswd /usr/bin/su /usr/bin/chfn /usr/bin/mount /usr/bin/sudo.ws /usr/bin/chsh /usr/bin/ntfs-3g /usr/bin/newgrp /usr/bin/umount /usr/bin/passwd /usr/lib/openssh/ssh-keysign /usr/lib/dbus-1.0/dbus-daemon-launch-helper /usr/lib/cargo/bin/su /usr/lib/cargo/bin/sudo

This is the baseline from the essentials lesson, unchanged on a fresh install. An intruder reads it for what is missing as much as for what is there: there is no pkexec and no polkit agent helper. That matters, because the most famous local-root bug of the last decade, PwnKit (CVE-2021-4034, a flaw in pkexec), lived in a SUID binary that a default 26.04 server does not even carry. PwnKit is history worth knowing, not a live finding here.

The list is normal; a new line is the signal
Every binary above is meant to be there, so nothing in this list is an alert on its own. The danger is an entry that was not here when you built the host: a SUID file an attacker dropped after reaching root, or one a careless package added. Do not eyeball the list against memory. Record it on build day, keep a copy (or at least its hash) somewhere the host cannot write to, and compare on a schedule. Finding escalation paths (linux-det/pedetect) builds that baseline-and-diff into a job.

SUID is not the only road up. File capabilities split root's power into separate keys (about forty of them), and a binary can carry one without ever being SUID, which means it never appears in the scan above. getcap is the only way to see them.

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ sudo getcap -r / 2>/dev/null
/usr/bin/ping cap_net_raw=ep /usr/bin/mtr-packet cap_net_raw=ep /usr/lib/aarch64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-ptp-helper cap_net_bind_service,cap_net_admin,cap_sys_nice=ep /usr/lib/snapd/snap-confine cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_setgid,cap_setuid,cap_sys_chroot,cap_sys_ptrace,cap_sys_admin,cap_sys_resource=p

ping holding cap_net_raw is why it no longer needs to be SUID: it gets the one capability it needs to open a raw socket and nothing more. snap-confine carrying a long list including cap_setuid and cap_sys_admin looks alarming but is expected on a snap-enabled image; the =p suffix means the caps are permitted but not switched on at launch. The lesson on local privilege escalation (linux-det/peloc) takes capabilities apart properly. For now the point is that your surface has two layers, and a scan of one is not a scan of the other.

The last part of the surface is what the machine exposes to the network. List the sockets that are accepting connections.

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=471,fd=20)) LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=882,fd=3),("systemd",pid=1,fd=260)) LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=471,fd=22)) LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=882,fd=4),("systemd",pid=1,fd=261))

A default server is quiet: sshd on port 22, and systemd-resolved's stub resolver on 127.0.0.53 and 127.0.0.54, which only listen on the loopback address and are not reachable from outside ("Networking basics" in Linux essentials reads these columns line by line). Port 22 shows two owners, sshd and systemd, because on Ubuntu SSH is socket-activated. Ask systemd how.

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ systemctl show ssh.socket -p Accept -p Triggers systemctl is-enabled ssh.socket ssh.service systemctl is-active ssh.service
Triggers=ssh.service Accept=no enabled disabled active

ssh.socket is the enabled unit and holds port 22. Accept=no means systemd does not start anything per connection: on the first connection it starts ssh.service once, the sshd listener in the ss output, and passes it the listening socket (systemd.socket(5)). That is why ssh.service reads disabled yet is active, and why both processes hold the socket. From then on sshd itself hands each connection to its own sshd-session process. For a defender this changes one habit: systemctl stop ssh.service does not close port 22, because the next connection starts the service again; taking SSH off a host means stopping and disabling ssh.socket. Every open port is an entry point to account for, and every one that appears later without a change record is a question.

The credential that is already root

Escalation is easiest when there is nothing to escalate: an account the intruder already controls that can become root through sudo. On a default cloud image the first administrative account can do exactly that, and sudo -l prints its own rights.

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ sudo -l
User deploy may run the following commands on web01: (ALL : ALL) ALL (ALL) NOPASSWD: ALL

(ALL) NOPASSWD: ALL is the line that matters to an intruder: this account runs anything as root with no password prompt, so whoever steals its key or session has an instant, gate-free path to root. On a cloud image cloud-init writes that grant for its default user (on this lab machine the lab's own drop-in grants it to deploy); "Users, groups and account lifecycle" in Linux hardening shows where such grants live and how to remove one safely. Using the grant is logged, and the next lesson, "The intrusion chain on Linux", reads that log line.

Where detection comes from, and why a change is the whole signal

On RHEL and Rocky the audit daemon (auditd), which collects the kernel's audit records and writes them to disk, is installed and enabled out of the box. On Ubuntu it is not.

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ command -v auditctl >/dev/null && echo "auditd present" || echo "auditd is not installed on this Ubuntu server"
auditd is not installed on this Ubuntu server

A default Ubuntu server has the kernel's audit subsystem but no auditd to collect from it, so auditctl is not even on the path. Adding real host detection on Ubuntu starts with sudo apt-get install auditd; on RHEL 10 (audit 4.0.3) it is already running. Later lessons install it and build the rules. The mindset to carry forward is the one every command above pointed at: the surface itself, the SUID set, the capabilities, the listeners, the sudo grants, is not the alert, because a working server is full of legitimate power. The alert is the difference from a known-good baseline. That is what makes a detection survive a careful attacker: you key on the change they cannot avoid making, not on any name or string they get to choose.

After the foothold: what an intruder does, and where this course watches
Credential abuse and escalation
SUID, sudo, capabilities
linux-det/peloc
kernel bugs and leaked secrets
linux-det/pekernel
new privileged files, rules, units
baselined in linux-det/pedetect
Lateral movement
reused SSH keys and credentials
linux-det/killchain
Actions on objectives
exfiltrate, pivot, mine
the real goal
audit, hunt, respond
every stage: auditpipe to ir
Every move leaves a trace on the host, if you watch the place the move forces the attacker to touch.

Try this

On a lab machine, record the privileged surface exactly as this lesson did: save the output of find / -xdev -perm -4000 -type f and sudo getcap -r / to a file, and note the listeners from sudo ss -tlnp. Then create a harmless SUID file with sudo install -m4755 -o root /usr/bin/true /var/tmp/.probe, re-run the find, and confirm the new line appears. Remove it with sudo rm /var/tmp/.probe and confirm it is gone. You have just done, by hand, what the baseline-and-diff job in linux-det/pedetect does on a schedule: the file you planted is the change, and the change is the finding.

Takeaway

Know the default attack surface of your platform by sight, keep a baseline the host itself cannot rewrite, and alert on the difference, not the list. Prevention has a failure rate above zero, so budget real effort for seeing the intruder who already got in.

Quick check
01You run the SUID-root scan on a fresh Ubuntu 26.04 server and it matches the set this lesson shows exactly, with no extra entries. What is the right use of that result?
Incorrect — passwd must write /etc/shadow and mount changes the mount table; a blanket strip breaks ordinary use and finds nothing.
Correct — The expected set is not news; the entry that was not there at build time is, and one extra line hides easily among the normal ones.
Incorrect — A stock install ships a whole normal set of them, so this pages someone daily for behaviour that was designed in and never changes.
Incorrect — Exposure to a CVE is judged from the installed package version against the vendor's advisory, not by searching binaries for strings, and that check says nothing about SUID files that should not be there at all.
02A teammate wants to work through a 2021 privilege-escalation guide that starts with pkexec (PwnKit). What does this lesson's SUID scan of the default install tell them?
Incorrect — Running local-root exploits on your own hosts to "check" is both unnecessary and reckless; you verify exposure from installed versions, and here the binary is not present at all.
Incorrect — There is nothing to disable for this: a default 26.04 server ships no pkexec and no polkit agent helper, so the PwnKit surface is simply absent.
Correct — The attack surface moved on; pkexec is not in the default set, so treat the CVE as a lesson in how one SUID binary became instant root, not a finding on this host.
Incorrect — Its absence is a smaller attack surface, not a defect; baselining should flag files that appear, not demand ones the platform chose not to ship.
03An exploited web application runs as a system account whose shell is /usr/sbin/nologin and which is in neither sudo nor adm. A colleague says the intruder is therefore stuck inside the application. What does the foothold view in this lesson say?
Incorrect — nologin is only the login shell; it refuses an interactive login, but code running as the account can still start programs directly.
Incorrect — A system account reads everything world-readable; only files such as /etc/shadow are closed to it, as the lesson's cat showed.
Incorrect — The kernel gives no special rights to UIDs below 1000; that range is only a convention for service accounts.
Correct — The shell field does not limit code that already runs as the account, and the enumeration in this lesson needs no privilege beyond that.

Related