php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #76067 system() function call leaks php-fpm listening sockets
Submitted: 2018-03-08 13:48 UTC Modified: 2023-08-26 14:32 UTC
Votes:1
Avg. Score:5.0 ± 0.0
Reproduced:1 of 1 (100.0%)
Same Version:0 (0.0%)
Same OS:1 (100.0%)
From: jaco at uls dot co dot za Assigned: bukka (profile)
Status: Closed Package: FPM related
PHP Version: 5.6.34 OS: Linux
Private report: No CVE-ID: None
 [2018-03-08 13:48 UTC] jaco at uls dot co dot za
Description:
------------
We've recently noticed that when some of our PHP code executes external scripts via the system() call, those scripts have access to file descriptors that it probably should not.  Specifically we are able to tap into the PHP-FPM listening socket.

Below a simple php + shell script combo to illustrate the problem.

Note that in the below, fd 0 is the listening socket, so one could conceivable start executing accept() calls on this file descriptor to steal connections that should have gone to the PHP FPM master process.

A simplistic fix is presumably to set FD_CLOEXEC on the listening sockets in the master process.  Even the accept()ed connections.  A more comprehensive fix is probably in the system() function to first close all descriptors before performing the actual system() call, and setting up stdin to come from /dev/null in the case of non-cli SAPIs.  Or at the very least one could redirect fd 0 to utilize the php://stdin stream.

This seemingly relates to bug #67383.

Test script:
---------------
<?php
header("Content-Type: text/plain");

system("ls -la /proc/self/fd/; /sbin/ss -xtap");
?>

Expected result:
----------------
I would like to see only three open file descriptors being listed in the ls statement:

0 <= /dev/null (probably)
1 => pipe()
2 => /dev/null (as per current).

(and 3 to /proc/32264/fd for the readdir cycle)

Actual result:
--------------
total 0
dr-x------ 2 nobody jkroon  0 Mar  8 15:25 .
dr-xr-xr-x 8 nobody jkroon  0 Mar  8 15:25 ..
lrwx------ 1 nobody jkroon 64 Mar  8 15:25 0 -> socket:[8376]
l-wx------ 1 nobody jkroon 64 Mar  8 15:25 1 -> pipe:[572571]
lrwx------ 1 nobody jkroon 64 Mar  8 15:25 2 -> /dev/null
lr-x------ 1 nobody jkroon 64 Mar  8 15:25 3 -> /proc/32264/fd
lrwx------ 1 nobody jkroon 64 Mar  8 15:25 4 -> socket:[572570]
Netid  State      Recv-Q Send-Q Local Address:Port                 Peer Address:Port           
...
u_str  LISTEN     0      0      /var/run/php-fpm/www 8376                  * 0                     users:(("ss",pid=32265,fd=0),("list_fds.sh",pid=32263,fd=0),("sh",pid=32262,fd=0),("php-fpm",pid=32261,fd=0))
...
u_str  ESTAB      0      0      /var/run/php-fpm/www 572570                * 0                     users:(("ss",pid=32265,fd=4),("list_fds.sh",pid=32263,fd=4),("sh",pid=32262,fd=4),("php-fpm",pid=32261,fd=4))

Patches

Pull Requests

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2018-03-08 18:22 UTC] stas@php.net
-Assigned To: +Assigned To: bukka
 [2018-03-13 18:33 UTC] bukka@php.net
I would like to first clarify why this is marked as a security issue.

I'm not really sure how this can be exploited. AFAIC you can't share the listening socket across the pools which means that it will be for a single pool and it means that the user will be the same, right? Can you please be a more specific why this is a security issue?
 [2018-03-13 19:21 UTC] jaco at uls dot co dot za
I'm not familiar with the details of the code, so:

1.  Yes, I only saw the one socket leaked, test environment only had one socket configured.  I've confirmed that you're right, only single socket for the pool is leaked.

2.  Could perhaps be utilized to bypass certain other restrictions enforced by safe mode although most likely external execution too would be disabled in safe mode.

3.  As an attacker of a web page I could utilize an exploit to run a custom script, take over the listening socket and start accepting connections (and re-originating them back into php-fpm for processing but get all traffic - MITM style - including post data potentially containing usernames and passwords, which I may not otherwise be able to get in plaintext since they'd be *hopefully* hashed in the backing DB/datastore, or using an external authenticator service like radius/ldap/whatever).

If you don't feel this is a security problem, please treat it as such and open it up for general viewing, but this is still something that should be fixed either way.
 [2018-03-18 21:37 UTC] stas@php.net
Looking at FPM code in fpm_stdio_init_child(), it looks like FD 0 (stdin) is always the listening socket. This means there's no useful way for the forked process to make any legit use of it, probably. Which means, it would probably be OK to not pass it? I am not sure though what CLOEXEC there would result in. 
@bukka, any opinion on adding CLOEXEC to listening socket somewhere in fpm_stdio_init_child() or fpm_unix_init_child()? 

That all said, since the child and the FPM process run within the same permissions framework, I am not sure there can be a real security barrier between them. If you have full remote access to the server, including system(), there's little you can't do (within the permissions of this user).
 [2018-03-22 20:55 UTC] bukka@php.net
I would be a bit careful about adding cloexec on stdin. At least I need to experiment with that first to see what possible consequences are. The thing is that the fact that stdin is used causing other issues as well (see https://bugs.php.net/bug.php?id=73342 ) so it might be better to change that but it needs a bit more thinking and mainly testing first.

I agree that this is not really a security issue. If you can't trust a program that you run using system function, then you have got bunch of other problems as well IMHO.
 [2018-10-14 15:34 UTC] bukka@php.net
-Type: Security +Type: Bug
 [2018-10-14 15:34 UTC] bukka@php.net
As discussed, I'm setting this as a not security issue for the reasons stated above. In addition I have been looking through the similar bugs and the issue is already exposed in the FPM related comment added on 2013-12-03 at 17:23 UTC to the bug about similar issue in Apache mod_php: https://bugs.php.net/bug.php?id=38915 .
 [2018-10-14 15:44 UTC] bukka@php.net
Seems like it's still set as private but I can't change it. Think it should be set as a public. Can someone with the right permission do that?
 [2018-10-14 15:54 UTC] bukka@php.net
Seems fine now and it's publicly visible.
 [2020-04-29 21:14 UTC] enyby at yandex dot ru
Same issue but with PHP 7.3.9.

As mentioned in #67383 this bug prevent restart php-fpm.
 [2020-04-29 22:00 UTC] enyby at yandex dot ru
Workaround for run $command in exec:

exec $command 3>&- 4>&- 5>&- 6>&- 7>&- 8>&- 9>&- 10>&- 11>&- 12>&- 13>&- 14>&- 15>&- 16>&- 17>&- 18>&- 19>&- 20>&- 21>&- 22>&- 23>&- 24>&- 25>&- 26>&- 27>&- 28>&- 29>&- 30>&-

In code it is look like:

$command = 'exec '.$command.' '.implode('>&- ', range(3, 30)).'>&-';

Now original command will use only fds 0, 1 and 2. With defined explicit on exec.

Require bash which understand syntax 'fd>&-' for close fd.
 [2020-08-31 08:32 UTC] jaco at uls dot co dot za
We host customer code ... so telling me a workaround is to do system("... &>/dev/null") or some variant thereof (killing off fd per fd) is just absolutely stupid insane.  More to the point, fd=0 is original listening socket.  So a remote hack can start doing accept() and intercepting posts, which could include login credentials.  With some trickery it can forward this to legitimate processors conceivably, thus effectively installing a MITM right on the hosting machine.  This is an escalation attack, so first need to exploit something else before this can be directly attacked.
 [2023-08-26 14:32 UTC] bukka@php.net
-Status: Assigned +Status: Closed
 [2023-08-26 14:32 UTC] bukka@php.net
This has been addressed by https://github.com/php/php-src/commit/418cdc0bea3d7787587964f42c309602d70232c6 and will be available in PHP 8.3.0.
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Fri Oct 11 05:01:27 2024 UTC