Over a few working sessions we ran a focused offensive-security exercise against a Linux VM. The goal wasn't to "hack a box" for sport — it was to pressure-test our own offensive tooling and methodology, and in doing so learn how a real attacker would assess the security posture of an application or system. This post covers the process we used, the chains we found, and the lessons each one taught us.
The framing matters: this is a methodology piece, not a trophy list. The value is in how we went about it — the repeatable process — and in the specific misconfigurations that let us move from a low-privilege shell to full control. Every finding below is a class of weakness that shows up in real production systems, and each one has a corresponding defensive fix.
The process: how we test a system's security posture
Before any exploit, there's a discipline. Our approach breaks down into five phases, and we applied it to every target we touched.
1. Reconnaissance — know what you're standing on
Every engagement starts with the same questions: What user am I? What groups am I in? What's running? What's listening? What's on disk? We enumerate the obvious first — identity, environment, running processes, network state, and the filesystem layout. The goal is a map, not a target.
2. Enumeration — find the seams
This is where the real work happens. We systematically look for the places where a system's assumptions break down:
- Privilege boundaries — what can this user do that they shouldn't be able to?
- Trusted binaries — SUID files, capabilities, sudo rules, cron jobs
- Secrets at rest — config files, environment variables, dotfiles, process memory, keyrings
- Group memberships — the quiet escalations hiding in the group file
The rule we live by: enumerate everything, assume nothing. The most valuable findings are almost never the flashy ones — they're the boring misconfigurations nobody noticed.
3. Identify the weakness — understand why it's exploitable
We don't just run tools and hope. For each seam we find, we ask what assumption is this violating? A SUID binary that reads arbitrary files violates the assumption that only root can read root-owned files. A writable directory on a root cron's PATH violates the assumption that the cron job only runs trusted code. Understanding the why is what turns a one-off exploit into a reusable technique.
4. Exploit — chain, don't spray
Real compromise is rarely a single bug. It's a chain — a series of small, individually-innocuous weaknesses that compound. We look for how findings connect: a config file leaks a password, that password decrypts a vault, the vault holds the objective. Each link is defensible on its own; the chain is what's dangerous.
5. Verify and document — prove it, then teach it
We don't claim a finding works until we've proven it end-to-end and captured the evidence. Then we document the chain, the root cause, and the fix. The documentation is the deliverable — it's what turns an exercise into an improvement in our defensive posture.
The chains we found
What follows are the specific chains we built, each one a class of real-world weakness. We've described the technique and the lesson; the specific secrets involved are omitted by design.
A trusted binary that reads anything (SUID)
The weakness: A setuid-root binary on the system had a file-reading feature that didn't validate its path argument. Because it ran as root, it could read any file on the box — including root-only secrets.
The chain: Identify the SUID binary → confirm it runs as root → use its file-read feature to pull a root-only file.
What we learned: SUID binaries are a huge attack surface. Every one is a potential privilege boundary, and a single unchecked argument turns a "helpful" tool into a root file reader. Defense: audit all SUID binaries, strip the bit where not needed, and validate inputs even in trusted tools.
Sudo with a dangerous helper (GTFOBins)
The weakness: The user had passwordless sudo access to a specific helper program. That program, when run through sudo, could execute arbitrary commands as root.
The chain: List the allowed sudo commands → recognize the helper is on the "GTFOBins" list (programs that can escape their intended use) → use its command-execution feature to get a root shell.
What we learned: Passwordless sudo to any program is a risk, but some programs are far worse than others. A tool that can run shell commands, read files, or write files is effectively root access. Defense: restrict sudo to the narrowest command set possible, and never grant sudo to a program that can execute or read arbitrarily.
Powers without the bit (Linux capabilities)
The weakness: A binary had the cap_setuid capability set — meaning it could change its own UID to root without being a full SUID binary. Capabilities are a more granular privilege model, but they're easy to misapply.
The chain: Enumerate capabilities → find a binary with cap_setuid → invoke it in a way that sets UID 0 and runs a command → root shell.
What we learned: Capabilities are a double-edged sword. They're meant to grant minimal privilege, but a single over-broad capability like cap_setuid is equivalent to full root. Defense: audit capabilities, and treat cap_setuid/cap_setgid/cap_sys_admin as root-equivalent.
The sudoedit editor injection (CVE-2023-22809)
The weakness: The user had passwordless sudoedit access to a single file. sudoedit lets a user edit a file as root — but on the affected sudo version, the EDITOR environment variable was not sanitized. An attacker could inject extra arguments into the editor invocation, causing sudoedit to also open (and let the user edit) a file they shouldn't touch — like the sudoers file.
The chain: List the sudo rules → craft an EDITOR value that appends a second file argument → sudoedit opens both the allowed file and the sudoers file → add a sudo rule granting full root → root shell.
What we learned: This is a real, published CVE (CVE-2023-22809) — a reminder that even "safe" sudo rules can be bypassed by environment-variable injection. Defense: keep sudo patched, and treat any sudoedit/sudo rule as potentially bypassable until the version is verified.
Root by membership (the docker group)
The weakness: The user was a member of the docker group. Membership in the docker group is effectively root access, because the docker daemon runs as root and will happily mount the host filesystem into a container.
The chain: Confirm docker group membership → run a container with the host filesystem mounted read-only → read a root-only file from the host.
What we learned: Group memberships are a quiet privilege boundary. docker, sudo, wheel, and similar groups grant far more than they appear to. Defense: treat docker-group membership as root-equivalent, and audit group memberships regularly.
The cron PATH hijack
The weakness: A root cron job set its PATH to include a directory writable by a low-privilege group, then ran a maintenance command by name. Because the writable directory came first in the PATH, an attacker could plant a malicious file with that name and the root cron would execute it.
The chain: Read the cron job → notice the writable directory in its PATH → plant a malicious script with the expected name → wait for the cron to fire → the script runs as root and reads the objective.
What we learned: This is a classic multi-link chain: a writable directory + a root cron + a bare command name = root. Each element is defensible; together they're a compromise. Defense: cron jobs should use absolute paths, never put writable directories in PATH, and run with the least privilege needed.
Secrets at rest (config files and encrypted vaults)
The weakness: An application's config file contained a database password in plaintext. That password, in turn, was the key to decrypting an encrypted vault file that held the real secret.
The chain: Read the app config → extract the database password → use it to decrypt the vault → recover the objective.
What we learned: Secrets at rest are the most common finding in any assessment. Plaintext credentials in config files are everywhere, and they often unlock more than they appear to. Defense: use a secrets manager, never commit credentials to config or code, and encrypt secrets with keys stored separately from the data.
Break the hash
The weakness: A user's password hash was readable from the shadow file, and the password was weak enough to crack with a wordlist. The cracked password then decrypted an encrypted backup file.
The chain: Read the shadow hash → crack it with a wordlist → use the recovered password to decrypt a backup → recover the objective.
What we learned: Password hashing only protects you if the password is strong. A weak password, even hashed, is a liability — and once cracked, it often unlocks more than the account itself. Defense: enforce strong passwords, use a slow hash (bcrypt/argon2), and never reuse a password across systems.
Only in RAM (process environment)
The weakness: A running service loaded a secret token from a file into its environment at startup. The token was meant to be ephemeral — but the seed file it was loaded from was still present on disk, readable by our user.
The chain: Identify the running service → read its startup script → find the seed file it loads → read the token directly.
What we learned: "In memory" is not the same as "gone." Secrets loaded into process environments often leave traces — the source file, the environment, or the process memory itself. Defense: delete seed files after loading, and never leave secrets in files that a low-privilege user can read.
Borrow the keyring (SSH agent hijack)
The weakness: An ssh-agent was running with a loaded SSH key, and its socket was accessible to our user. The agent is designed to hold keys so they never touch disk — but if an attacker can reach the socket, they can use the agent to authenticate as the key's owner without ever seeing the key material.
The chain: Find the running ssh-agent socket → point the agent socket variable at it → use the agent to SSH to a target as the key's owner → read the objective.
What we learned: This is the crown jewel of the exercise. SSH agent hijacking is a real, subtle technique: the key never leaves the agent, yet an attacker who can reach the socket gets full use of it. Defense: protect agent sockets (they should be owned by the user and not world-readable), use agent forwarding sparingly, and consider agent lifetime limits.
The meta-lesson: it's a process, not a hack
The single most important thing we took away isn't any one chain — it's the process. A security assessment is a discipline:
- Enumerate everything. The findings were hiding in plain sight: a config file, a group membership, a cron job, a running process. None of it was exotic.
- Understand the assumption each weakness violates. Every exploit worked because some system assumed something that wasn't true — that a SUID binary was safe, that a group was harmless, that a writable directory was never on a root PATH.
- Chain, don't spray. No single finding was catastrophic on its own. The compromise came from connecting them.
- Verify and document. We didn't claim a win until we'd proven it end-to-end and written down the root cause and the fix.
The defensive takeaway is equally clear: the same discipline that finds these weaknesses can prevent them. Audit your SUID binaries and capabilities. Restrict sudo to the narrowest set. Treat group memberships as privilege boundaries. Use absolute paths in cron. Never leave secrets in config files or on disk. Protect your agent sockets. Patch your sudo.
None of these are exotic. They're the boring, unglamorous hygiene that keeps a system from being the one where a "few sessions" of testing turns into full compromise.
Security testing isn't about finding one clever bug. It's a repeatable process — enumerate, understand the assumption, chain the weaknesses, verify, document. And the same discipline that finds the weaknesses is the discipline that prevents them.
← Back to Field Notes