# How we broke PHP, hacked Pornhub and earned $20,000

Unserialize on user-controlled cookie param plus PHP GC use-after-free yielded remote code execution on pornhub.com via ROP to zend_eval_string.

- Payout: $20,000
- Severity: critical
- Program: PornHub
- Credited: Ruslan Habalov, cutz, Dario Weißer
- Disclosed: 2016-07-23
- CVE: CVE-2016-5771, CVE-2016-5773
- Write-up quality: 10/10
- Original: https://web.archive.org/web/20220709205125/https://www.evonide.com/how-we-broke-php-hacked-pornhub-and-earned-20000-dollar/
- Library page: https://bugbountylibrary.com/r/blog%3Aweb-archive-org-web-20220709205125-https-www-evonide-com-how-we-broke-php-hacked-pornhub-and-earned-20000-dollar
- Tags: domain:web impact:data-leak impact:rce kind:bounty platform:blog sev:critical tech:php technique:fuzzing technique:oob-exfil technique:source-review vuln:deserialization vuln:memory-corruption vuln:rce

**Target:** PHP 5.6 unserialize / garbage collector via web upload endpoints
**Root cause:** User-controlled POST 'cookie' parameter passed to unserialize; a use-after-free in PHP's garbage collection lets a freed zval be reused as an attacker-controlled string/object zval, enabling arbitrary memory read and function-pointer hijack.
**Steps:**
1. Identify endpoints like `/album_upload/create` and `/uploading/photo` where POST `cookie` is unserialized and reflected via Set-Cookie.
2. Confirm unserialize with `cookie=a:1:{i:0;O:9:"Exception":0:{}}` producing a stack trace in the Set-Cookie header.
3. Fuzz serialized strings to trigger GC UAF (CVE-2016-5771/5773); craft payload that frees a zval then reuses it as a fake string zval (type 6) with arbitrary pointer/length.
4. Leak a heap pointer by freeing an integer zval and reading its overwritten first 8 bytes via dangling pointer.
5. Force many empty arrays via `i:0;a:0:{}` repeated to leak `uninitialized_bucket` (BSS) address; page-scan backwards for `\x7fELF` to find PHP image base.
6. Walk ELF program headers to PT_DYNAMIC, then DT_STRTAB/DT_SYMTAB to resolve `zend_eval_string` and `exit` addresses; leak POST body address via `sapi_globals.request_info.request_body` chain.
7. Craft fake object zval with `zend_object_handlers` whose `add_ref` points to a stack-pivot gadget (`php_stream_bucket_split+381`: `push %rax; ... pop %rsp`), verified by leaking gadget bytes.
8. Build ROP: pivot, `pop rdi` -> php_code ptr, `pop rsi` -> 0, call `zend_eval_string`, then `pop rdi` -> 0, call `exit`.
9. Pass fake object as second arg to `setcookie` so `_zval_copy_ctor` calls `add_ref` -> pivot -> ROP -> `zend_eval_string`.
10. PHP payload sets `X-Accel-Buffering: no`, `Content-Encoding: none`, `Connection: close`, then `file_get_contents('/etc/passwd')` and flushes to read output despite abnormal termination.
**How found:** Noticed `unserialize` on the `cookie` POST parameter reflected via Set-Cookie. Standard POP-chain exploitation failed due to unknown framework classes, so they audited PHP's unserializer itself. A custom fuzzer for serialized strings found unexpected behavior on PHP 7 (not matching target), then after ~1TB of logs on PHP 5 found a GC use-after-free; deeper analysis yielded a second UAF suitable for remote exploitation.
**Exploitation:** Attacker sends POST with crafted serialized `cookie` to an upload endpoint. The unserialize triggers a GC use-after-free; the freed zval is reused as a fake object whose handlers table points to a stack-pivot gadget. `setcookie` casts the object to string, invoking `add_ref`, pivoting RSP into the POST-data ROP chain, calling `zend_eval_string` with attacker PHP code, then `exit`. Unbuffered headers let the attacker read command output (e.g. /etc/passwd) in the response.
**Escalation:** From info leak to full RCE; could dump database, source code, track users, or pivot into network/root.
**Insight:** When POP-chain gadget hunting fails on a target, pivot to fuzzing the language runtime's own unserialize/GC internals; and use repeated empty-array allocations (`i:0;a:0:{}`) to leak `uninitialized_bucket` and bootstrap an ELF base under PIE/ASLR.
