CTF/DGHack/Web Operation based on PHP popchain (Unserial killer)

Hello and welcome to this third write-up focusing on the challenges of the DGHACK, 2022 edition, organized by the General Directorate of Armaments of the Ministry of the Armed Forces.

DESCRIPTION

A company has just been attacked by hackers who have recovered the configuration of one of their web servers.

Audit the web server source code and find out how they were able to access it.

Okay, well let’s go!

A quick first analysis

I connect to the website, and I download the source from the link provided for this purpose. The vulnerability is obvious in the main function of the functions.php file.

functions.php

The fact that there is a call to unserialize on a user input, without using the unserialized result afterwards, clearly makes us think that this is a pop chain type attack . I continue my investigation 

I notice that there are files that have been modified in vendor/guzzlehttp/psr7 (I base myself on the github sources for the same version 1.8.5 available here ).

By comparing these 3 files with the original files, we see that there are some functions that have been modified/added.

  • FnStream.php
FnStream->getContents

This function is more than very interesting, because if we call it with the object parameters display_content = true and _fn_getContents = “flag” , we will be able to read our file (it will be directly displayed on the page). It constitutes our arrival point.

  • Stream.php
Stream->destruct

This function is also very useful because it will be called to destroy our object. It constitutes our starting point .

You will have understood, all that remains is to pass the right attributes to the stream object so that it calls the getContents function of an FnStream object with the right attributes.

A second analysis

Now that we know where we’re starting from and where we need to get, all we have to do is solve the problem of how. The challenge now comes down to a headache.

First, you need to follow up on the __destruct method of the Stream object. To do this, the customMetadata attribute of the Stream object would need to be an object that has a closeContents method . Unfortunately, there is no object that has such a method. By learning a little more about magic methods ( on the official PHP doc ) we come across the __call magic method . It is called when we want to execute an undefined method on an object, which is our case. So let me introduce you to the third modified file that we haven’t talked about yet, I named StreamDecoratorTrait.php

StreamDecoratorTrait->__call

As we can see in the screenshot, StreamDecoratorTrait has a magic __call method . Let’s dissect it.

  • Line 71 verifies that the stream attribute of the StreamDecoratorTrait object is indeed an object and that it has a decorate method.
  • Lines 72-73 assign the value of the custom_method attribute of the StreamDecoratorTrait object to a variable named method
  • Lines 75-76 transform the method variable into an array if it is not already one.
  • Line 79 retrieves the first element from the list of arguments provided to __call , and places the value in the variable args
  • Lines 81-86 will call all methods defined in the methods array with arguments of $args. At each iteration, we remove the first element from the $args list.

To summarize : if we create a Stream object , set its size attribute to null and its customMetadata attribute to a StreamDecoratorTrait object whose custom_method attribute is a string containing “getContents” and the stream attribute is an FnStream object , SO

Stream->__destruct will call StreamDecoratorTrait->__call which will call Stream->getContents.

Except that : for getContents to display the flag.php file, you must set the display_content attribute to True and the _fn_getContents attribute to the “flag.php” string.

A third analysis

We’re almost there, we now just need to set the attributes of the FnStream object, only it’s a little more complicated than that.

fnstream.php

FnStream->__wakeup will be called when we instantiate our Stream object, and therefore all our attributes whose name is mentioned in the capture above will be removed from the object. Thin …

fnstream.php

These two functions allow us to add attributes to our FnStream object. The first ”  register  ” function allows you to add any attribute to FnStream, unless it is in the list of forbidden attributes ( self::$forbidden_attributes) . The second allows you to remove attributes from the list of prohibited attributes. Hallelujah, we’re out of the woods, let me explain:

Simply call FnStream->allow_attribute(“_fn_getContents”) , then FnStream->register(“_fn_getContents”, »flag.php”) and we will have set the value of _fn_getContents despite the restriction. All this is made possible at the level of the __call function of the StreamDecoratorTrait object by setting its customMethod attribute to an array containing the names of the methods that will be called (i.e. allow_attribute , register and getContents )

Note: I considered the StreamDecoratorTrait object to be an object, but it is actually a ” trait “. That said, it doesn’t change anything for the challenge, we simply take an object that uses this trait and that’s it.

To finish…

To facilitate the exploit, I developed 2 scripts:

<?php
class FnStream {

    public $display_content = true;

}

class LimitStream {
    public $custom_method = ["allow_attribute","allow_attribute","register","getContents"];
        function __construct($stream) {
                $this->stream = $stream;
        }
}

class Stream {
    public $size = [ ["_fn_getContents"],["getContents"],["_fn_getContents","/../../../../config"],[] ];
        function __construct($customMetadata) {
                $this->customMetadata = $customMetadata;
        }
}

$a = new Stream(new LimitStream(new FnStream));
echo serialize($a);
import subprocess
import base64
result = subprocess.run(['php', 'unserial.php'], stdout=subprocess.PIPE)
result = result.stdout.decode('utf-8')

result = result.replace('O:6:"Stream"','O:22:"GuzzleHttp\Psr7\Stream"')
result = result.replace('O:11:"LimitStream"','O:27:"GuzzleHttp\Psr7\LimitStream"')
result = result.replace('O:8:"FnStream"','O:24:"GuzzleHttp\Psr7\FnStream"')

print("[-] Serialized string:")
print(result)
print("[-] Base64 Serialized string:")
print(base64.b64encode(result.encode()).decode('utf-8'))

We place the payload in base64 in the GET data argument, and that’s it !


Posted

in

,

by

Tags: