Описание
[PHP-FPM] Logs from childrens may be altered
Summary
In PHP-FPM, when configured to catch workers output through catch_workers_output = yes
configuration, it may be possible to pollute the final log with up to 4 characters from the FPM_STDIO_CMD_FLUSH
macro, or remove up to 4 characters from the logs. Additionally, If PHP-FPM is configured to use a syslog, it seems that much more characters can be excluded from the logs, even though it has not been tested.
Note that this issue was found and reported as part of Quarkslab security audit on PHP-SRC with the OSTIF and PHP Foundation.
Details
The method static void fpm_stdio_child_said
defined in sapi/fpm/fpm/fpm_stdio.c
is responsible for parsing and writing the logs out from workers input.
The interesting part is the following :
For each characters of buf
, it is checked whether it is \n
or \0
. \n
is used to indicate that the current line of log is terminated and can be written. \0
is used to detect the FPM_STDIO_CMD_FLUSH
macro characters sequence, defined as \0fscf\0
and used to flush logs. It acts the same as \n
.
If a \0
character is encountered, and if the end of the buffer is reached before the full comparison with FPM_STDIO_CMD_FLUSH
can be achieved, then the comparison is done with what is currently available:
If it matches, the number of characters that have already been checked is saved in cmd_pos
and a goto
statement bring the control flow back to the top of the loop so that the buffer can be filled again. After that, the comparison continues :
As cmd_pos
is superior to 0, the comparison continues with the remaining characters to compare with. If it matches, the current buffer is written and flushed. The cursor is set to cmd_pos
and the iteration on the buffer starts again.
However, as is, the rest of the FPM_STDIO_CMD_FLUSH
characters sequence is added to the next output, including the \0
character, or, the first bytes of the next outputs are not included depending of the value of cmd_pos
.
That is because start
shouldn't be set to cmd_pos
value, which correspond to the number of characters that have already been compared, from the previous buffer. It should be set to sizeof(FPM_STDIO_CMD_FLUSH) - cmd_pos
instead.
Additionnally and more generally, if a null character is contained in the log and a syslog is configured, it appears that the final string to be written won't contains the characters after this null character. Indeed, logs are sent by calling php_syslog(syslog_priorities[zlog_level], "%s", stream->buf.data);
from zlog_stream_buf_append
defined as :
It seams pretty obvious that a null character will be treated as the end of the buffer regarding the following line : zlog_stream_buf_copy_char(stream, '\0');
which is called right beforephp_syslog
is, and that the buffer data is passed as a parameter without any information on its length.
I anyway checked how the buffer was formatted and sent to the syslog server. The buffer and arguments are processed by xbuf_format_converter
function defined in /main/spprintf.c
, where, as expected, the length of the argument is determined using strlen
function, which will stops on \0
:
Note that some prerequisites may be needed in order to exploit this vulnerability from the outside world. Auditing the communications between PHP-FPM Master and its workers was one of the asked key tasks regarding Quarkslab audit.
PoC
Using the following code, it is possible to reproduce the described behavior on log pollution, up to 4 characters.
The function fpm_stdio_child_said
has been extracted and adapted so it can read data from a buffer instead of a file descriptor:
As a first demonstration, we're filling the buffer with data and ends it with the first character of FPM_STDIO_CMD_FLUSH
so that the end of the buffer will contains \[...]AAAQuarkslab\0\0
The null character isn't showed because the shell ignores it but it is included in the output.
In order to suppress up to 4 characters, one has to write all the characters of FPM_STDIO_CMD_FLUSH
except the last one in the end of the buffer :
Impact
The impact is low.
Users of PHP-FPM that redirect stdout
and stderr
of the workers towards the master process are be affected. If the length of the messages can be controlled, logs can be polluted by including part of FPM_STDIO_CMD_FLUSH
, or stripped from up to 4 characters.
If a SYSLOG is configured and a null character can be injected in the messages sent by the workers, it seems that the rest of the log is not sent.
Пакеты
< 8.1.30
8.1.30
< 8.2.24
8.2.24
< 8.3.12
8.3.12
Связанные уязвимости
In PHP versions 8.1.* before 8.1.30, 8.2.* before 8.2.24, 8.3.* before 8.3.12, when using PHP-FPM SAPI and it is configured to catch workers output through catch_workers_output = yes, it may be possible to pollute the final log or remove up to 4 characters from the log messages by manipulating log message content. Additionally, if PHP-FPM is configured to use syslog output, it may be possible to further remove log data using the same vulnerability.
In PHP versions 8.1.* before 8.1.30, 8.2.* before 8.2.24, 8.3.* before 8.3.12, when using PHP-FPM SAPI and it is configured to catch workers output through catch_workers_output = yes, it may be possible to pollute the final log or remove up to 4 characters from the log messages by manipulating log message content. Additionally, if PHP-FPM is configured to use syslog output, it may be possible to further remove log data using the same vulnerability.
In PHP versions 8.1.* before 8.1.30, 8.2.* before 8.2.24, 8.3.* before 8.3.12, when using PHP-FPM SAPI and it is configured to catch workers output through catch_workers_output = yes, it may be possible to pollute the final log or remove up to 4 characters from the log messages by manipulating log message content. Additionally, if PHP-FPM is configured to use syslog output, it may be possible to further remove log data using the same vulnerability.
In PHP versions 8.1.* before 8.1.30, 8.2.* before 8.2.24, 8.3.* before ...