|
php.net | support | documentation | report a bug | advanced search | search howto | statistics | random bug | login |
[2018-07-05 15:47 UTC] varma dot prashanth at hotmail dot com
Description:
------------
Because of (Transfer-Encoding: Chunked) header php is echoing the body as response. This exploit doesn't need any authentication and can be exploited via POST request.
XSS tested on current versions of Chrome and Firefox Quantum.
> This vulnerability is tested on apache versions 2.4.18 and 2.4.33.
>
> Reproducing steps :
>
> 1) Intercept the request in burp suite to modify headers
>
> GET /lol.php HTTP/1.1
> Host: localhost
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:61.0) Gecko/20100101 Firefox/61.0
> Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
> Accept-Language: en-US,en;q=0.5
> Connection: close
> Upgrade-Insecure-Requests: 1
> Cache-Control: max-age=0
>
> 2) Modify the request to
>
> POST /lol.php HTTP/1.1
> Host: localhost
> User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:61.0) Gecko/20100101 Firefox/61.0
> Accept-Language: en-US,en;q=0.5
> Content-Type: application/json
> Upgrade-Insecure-Requests: 1
> Cache-Control: max-age=0
> Transfer-Encoding: chunked
> Content-Length: 25
>
> <script>alert(1)</script>
>
> 3) Response for the request
>
> HTTP/1.1 200 OK
> Date: Mon, 02 Jul 2018 05:23:16 GMT
> Server: Apache/2.4.33 (Unix) PHP/7.1.17
> X-Powered-By: PHP/7.1.17
> Content-Length: 39
> Connection: close
> Content-Type: text/html; charset=UTF-8
>
> "{'hack':'1'}"<script>alert(1)</script>
Test script:
---------------
>
> <?php
> function respond_with($header, $body) {
> header($header);
>
> die(json_encode($body));
> }
> $body = "{'hack':'1'}";
> $header = "200 Status Ok";
> respond_with($header,$body);
> ?>
>
Expected result:
----------------
> HTTP/1.1 200 OK
> Date: Mon, 02 Jul 2018 05:23:16 GMT
> Server: Apache/2.4.33 (Unix) PHP/7.1.17
> X-Powered-By: PHP/7.1.17
> Content-Length: 39
> Connection: close
> Content-Type: text/html; charset=UTF-8
>
> "{'hack':'1'}"
Actual result:
--------------
> HTTP/1.1 200 OK
> Date: Mon, 02 Jul 2018 05:23:16 GMT
> Server: Apache/2.4.33 (Unix) PHP/7.1.17
> X-Powered-By: PHP/7.1.17
> Content-Length: 39
> Connection: close
> Content-Type: text/html; charset=UTF-8
>
> "{'hack':'1'}"<script>alert(1)</script>
PatchesPull RequestsHistoryAllCommentsChangesGit/SVN commits
|
|||||||||||||||||||||||||||
Copyright © 2001-2025 The PHP GroupAll rights reserved. |
Last updated: Sat Oct 25 18:00:02 2025 UTC |
Looks like I can reproduce it now. I am not sure yet how the input ends up in the output though, it doesn't seem like PHP is sending it, but it's located in Apache's iovec buffers for output: #1 0x00007f511550494a in apr_socket_sendv (sock=sock@entry=0x7f5115c230a0, vec=vec@entry=0x7fffa5bf4f80, nvec=nvec@entry=3, len=len@entry=0x7fffa5bf4ee0) at ./network_io/unix/sendrecv.c:212 #2 0x0000557484512389 in writev_nonblocking (s=s@entry=0x7f5115c230a0, vec=0x7fffa5bf4f80, nvec=3, bb=0x7f5115c23910, cumulative_bytes_written=0x7f5115c23848, c=0x7f5115c23290) at core_filters.c:787 #3 0x0000557484512684 in send_brigade_nonblocking (s=s@entry=0x7f5115c230a0, bb=bb@entry=0x7f5115c23910, bytes_written=bytes_written@entry=0x7f5115c23848, c=c@entry=0x7f5115c23290) at core_filters.c:704 #4 0x00005574845133c1 in send_brigade_blocking (c=0x7f5115c23290, bytes_written=0x7f5115c23848, bb=0x7f5115c23910, s=0x7f5115c230a0) at core_filters.c:733 #5 ap_core_output_filter (f=0x7f5115c236e8, new_bb=0x7f5115c23910) at core_filters.c:542 #6 0x000055748452ff61 in ap_process_request (r=r@entry=0x7f5115c050a0) at http_request.c:477 (gdb) p vec[2] $4 = {iov_base = 0x7f5115c1b17b, iov_len = 27} (gdb) p (char *)0x7f5115c1b17b $5 = 0x7f5115c1b17b "<script>alert(1)</script>\r\n" So somehow this gets into Apache's "bucket brigade", even though PHP is not sending it there directly (in fact, it never sees the input since Apache doesn't let it to read it, throwing an error in ap_http_filter instead: #0 apr_brigade_write (b=0x7f5115c18810, flush=0x5574844fcef0 <ap_filter_flush>, ctx=0x7f5115c1a548, str=0x557484546fc0 "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html><head>\n<title>", nbyte=71) at ./buckets/apr_brigade.c:433 #1 0x00005574844feebc in buffer_output (r=<optimized out>, str=<optimized out>, len=<optimized out>) at protocol.c:1898 #2 0x0000557484500d9e in ap_rvputs (r=r@entry=0x7f5115c190a0) at protocol.c:2022 #3 0x000055748452dde0 in ap_send_error_response (r=0x7f5115c190a0, recursive_error=0) at http_protocol.c:1539 #4 0x0000557484532eb6 in ap_http_header_filter (f=0x7f5115c1a570, b=0x7f5115c186e0) at http_filters.c:1335 #5 0x0000557484500832 in ap_content_length_filter (f=0x7f5115c1a548, b=0x7f5115c186e0) at protocol.c:1769 #6 0x000055748453415a in ap_byterange_filter (f=0x7f5115c1a520, bb=0x7f5115c186e0) at byterange_filter.c:494 #7 0x00007f51130855f4 in deflate_out_filter (f=<optimized out>, bb=0x7f5115c186e0) at mod_deflate.c:831 #8 0x00007f511285f10a in filter_harness (f=0x7f5115c17860, bb=0x7f5115c186e0) at mod_filter.c:323 #9 0x00005574845312df in ap_http_filter (f=<optimized out>, b=0x7f5115c18540, mode=<optimized out>, block=<optimized out>, readbytes=16384) at http_filters.c:555 #10 0x00007f5111cf941f in php_apache_sapi_read_post (buf=0x7fffa5bf0500 "", count_bytes=16384) at ./sapi/apache2handler/sapi_apache2.c:198 #11 0x00007f5111c09d28 in sapi_read_post_block (buffer=buffer@entry=0x7fffa5bf0500 "", buflen=buflen@entry=16384) at ./main/SAPI.c:248 #12 0x00007f5111c0a77d in sapi_deactivate () at ./main/SAPI.c:513 #13 0x00007f5111c00ab9 in php_request_shutdown (dummy=dummy@entry=0x0) at ./main/main.c:1863 I'll have to dig more into it to see how this output ends up there.It looks like the reason for the problem is this code in sapi_apache2.c: if (!parent_req) { php_apache_request_dtor(r); ctx->request_processed = 1; bucket = apr_bucket_eos_create(r->connection->bucket_alloc); APR_BRIGADE_INSERT_TAIL(brigade, bucket); "brigade" here is one that is initialized way above in the request. But I suspect this brigade gets destroyed later by one of the Apache handlers when the input consumption fails. Applying this seems to fix the problem: diff --git a/sapi/apache2handler/sapi_apache2.c b/sapi/apache2handler/sapi_apache2.c index e7edcab6da..b2b3340826 100644 --- a/sapi/apache2handler/sapi_apache2.c +++ b/sapi/apache2handler/sapi_apache2.c @@ -724,6 +724,7 @@ zend_first_try { php_apache_request_dtor(r); ctx->request_processed = 1; bucket = apr_bucket_eos_create(r->connection->bucket_alloc); + brigade = apr_brigade_create(r->pool, r->connection->bucket_alloc); APR_BRIGADE_INSERT_TAIL(brigade, bucket); rv = ap_pass_brigade(r->output_filters, brigade); Could you please verify that it fixes the issue for you too?Just verified the patch on php 7.1.7. I am not able reproduce. Patch applied: if (!parent_req) { zend_first_try { php_apache_request_dtor(r); ctx->request_processed = 1; bucket = apr_bucket_eos_create(r->connection->bucket_alloc); brigade = apr_brigade_create(r->pool, r->connection->bucket_alloc); APR_BRIGADE_INSERT_TAIL(brigade, bucket); rv = ap_pass_brigade(r->output_filters, brigade); if (rv != APR_SUCCESS || r->connection->aborted) { zend_first_try { php_handle_aborted_connection(); } zend_end_try(); } apr_brigade_cleanup(brigade); apr_pool_cleanup_run(r->pool, (void *)&SG(server_context), php_server_context_cleanup); }zend_end_try(); }patch is working. applied apr_brigade_cleanup(brigade); if (!parent_req) { zend_first_try { php_apache_request_dtor(r); ctx->request_processed = 1; bucket = apr_bucket_eos_create(r->connection->bucket_alloc); apr_brigade_cleanup(brigade); APR_BRIGADE_INSERT_TAIL(brigade, bucket); rv = ap_pass_brigade(r->output_filters, brigade); if (rv != APR_SUCCESS || r->connection->aborted) { zend_first_try { php_handle_aborted_connection(); } zend_end_try();