Use-after-free in php_curl related to CURLOPT_FILE/_INFILE/_WRITEHEADER
Unknown
Vulnerability Details
https://bugs.php.net/bug.php?id=69316
Description:
------------
All the relevant code mentioned below is in ext/curl/interface.c.
As far as I can see, every PHP version after (at least) 5.0 is affected; possibly older versions too.
When using CURLOPT_WRITEHEADER, CURLOPT_INFILE or CURLOPT_FILE, in _php_curl_setopt, the provided stream is cast to a stdio FILE*:
```
if (FAILURE == php_stream_cast((php_stream *) what, PHP_STREAM_AS_STDIO, (void *) &fp, REPORT_ERRORS)) {
return FAILURE;
}
```
This FILE* is then stored in the php_curl structure "ch", at the following locations, depending on which CURLOPT_ was used:
- ch->handlers->write->fp = fp;
- ch->handlers->write_header->fp = fp;
- ch->handlers->read->fp = fp;
Upon curl_exec(), _php_curl_verify_handlers() is called, which verifies if the user-set stream(s) are still open, and resets ->fp to 0 if they are not.
However, there are a number of curl callbacks we can use to close the stream after _php_curl_verify_handlers() has been called, resulting in the FILE* being free()'d. By allocating memory that ends up a the same address where the FILE structure was, its possible to achieve arbitrary code execution.
The following functions use *->fp without checking if the corresponding streams are still open (and thus if *->fp still points to a valid FILE structure or not):
- static size_t curl_write(char *data, size_t size, size_t nmemb, void *ctx)
- static size_t curl_read(char *data, size_t size, size_t nmemb, void *ctx)
- static size_t curl_write_header(char *data, size_t size, size_t nmemb, void *ctx)
- curl_exec (after curl processing is finished, there are 2 fflush() calls)
On Linux, with PHP linked against GLIBC, arbitrary code execution is trivial, since FILE structures conveniently have a "vtable" full of function pointers which we now control.
On Windows, exploitability depends on the version of the C runtime being used. Recent MS C runtimes keep a cache of FILE structures (they aren't free()'d upon fclose()), which complicates things.
Please see the test script attached, tested against:
- 64-bit PHP 5.5.9-1ubuntu4.7 (cli) (built: Mar 16 2015 20:47:39)
- 32-bit PHP 5.5.9-1ubuntu4.7 (cli) (built: Mar 16 2015 20:48:03)
- 32/64-bit PHP 5.6.7 (cli) (built: Mar 27 2015 07:04:21) (DEBUG) - custom build with ./configure --with-curl --enable-debug
Test script:
---------------
```
<?php
function hdr_callback($ch, $data) {
global $f_file;
if ($f_file) {
// close the stream, causing the FILE structure to be free()'d
fclose($f_file); $f_file = 0;
// cause an allocation of approx the same size as a FILE structure, size varies a bit depending on platform/libc
$FILE_size = (PHP_INT_SIZE == 4 ? 0x160 : 0x238);
curl_setopt($ch, CURLOPT_COOKIE, str_repeat("a", $FILE_size - 1));
}
return strlen($data);
}
$ch = curl_init('http://www.php.net/');
$f_file = fopen("body", "w") or die("failed to open file\n");
curl_setopt($ch, CURLOPT_BUFFERSIZE, 10);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, "hdr_callback");
curl_setopt($ch, CURLOPT_FILE, $f_file);
curl_exec($ch);
?>
```
Actions
View on HackerOneReport Stats
- Report ID: 73246
- State: Closed
- Substate: resolved
- Upvotes: 3