Some unusual vulnerabilities in the PHP engine

fpm_log.c memory leak and buffer overflow

A few months ago we noticed strange error messages in the access.log of the PHP FastCGI Process Manager (FPM) engine we used for running one of our sites.

anonimized-domain.name 127.0.0.1 [17/Jul/2015:19:21:37 +0200] GET /wp-admin/load-scripts.php?c=1&load%5B%5D=hoverIntent,common,admin-bar,suggest,inline-edit-post,heartbeat,svg-painter,wp-auth-check,jquery-ui-core,jquery-ui-widget,jquery&load%5B%5D=-ui-tabs,jquery-ui-mouse,jquery-ui-draggable,jquery-ui-slider,jquery-touch-punch,iris,wp-color-picker,jquery-ui-accordion,jquery&load%5B%5D=-ui-position,jquery-ui-menu,jquery-ui-autocomplete,jquery-ui-sortable,backbone,wp-util,wp-backbone,media-models,wp-plupload,medi&load%5B%5D=aelement,wp-mediaelement,media-views,media-editor,media-audiovideo,wp-playli 17109 81.10 40.55 121.64 2816 24.662 200 /foobar.info/pages/wp-admin/load-scripts.php 123.123.123.123 /wp-admin/load-scripts.php?c=1&load%5B%5D=hoverIntent,common,admin-bar,suggest,inline-edit-post,heartbeat,svg-painter,wp-auth-check,jquery-ui-core,jquery-ui-widget,jquery&load%5B%5D=-ui-tabs,jquery-ui-mouse,jquery-ui-draggable,jquery-ui-slider,jquery-touch-punch,iris,wp-color-picker,jquery-ui-ac! cordion,jquery&load%5B%5D=-ui-posi^@^@^@^@^@^@^@^@^@ è ü^?^@^@^A^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^A^@^@^@^@^@^@^@¡9©U^@^@^@^@ÕB^@^@^@^@^@^@^ B^@^@^@^@^@^@^@^F^@^@^@^@^@^@^@¯D^@^@^@^@^@l^C^B^@^@^@^@^@^@^@^@^@^@^@^@^@V`^@^@^ @^@^@^@¡9©U^@^@^@^@¯D^@^@^@^@^@Âc^B^@^@^@^@^@/wp-admin/load-scripts.php^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^ @^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^ @^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@c=1&load%5B%5D=hoverIntent,com

We were worried that our site might be under attack, so we started to investigate the issue. We were surprised what we found.

The feature that the access log contains an entry for all the requests the FastCGI Process Manager receives from the web server is quite useful, since it offers easier understanding of what is going on at the PHP level.

Moreover PHP offers customization of the access log lines based on format string variables which can be specified with the access.format option of the FPM configuration file, so we played a little bit and used the following template for access.format:

access.format = %{HTTP_HOST}e %R [%t] %m %r%Q%q %p %{user}C %{system}C %{total}C %{kilo}M %{mili}d %s %f %{REMOTE_ADDR}e %{REQUEST_URI}e

This was just an expanded version of the default access.format template, we added the REMOTE_ADDR and REQUEST_URI fields at the end – they are coming from HTTP request itself. We thought that having both the %r%Q%q sequence and %{REQUEST_URI}e in the log entries would come handy in case of SEO and search engine-friendly URLs.

Surprisingly, we had to conclude from the actual web server log entries, no binary letters were sent in the HTTP request. So what was happening then? We found the answer by reviewing the source code of php-fpm.c. The %{something}e fields were processed at line 237:

len2 = snprintf(b, FPM_LOG_BUFFER - len, "%s", env ? env : "-");

Great. Isn’t it? Let’s see what is the problem with this code; from the snprintf() manual:

"The functions snprintf() and vsnprintf() do not write more than size bytes (including the terminating null byte ('\0')). If the output was truncated due to this limit then the return value is the number of characters (excluding the terminating null byte) which would have been written to the final string if enough space had been available."

In short, even if the buffer was not large enough, the function returned the number of characters that would have been written if the buffer was the large enough. Note that in this case no closing zero is added either. The implementation used the variable len to store the full size of the string after the concatenations and then it increased the value of len by len2 at line 449:

len += len2;

After exiting the loop, a \n byte is written outside of the compiled buffer and the log text is flushed into the access log, along with subsequent memory contents:

if (!test && strlen(buffer) > 0) {
  buffer[len] = '\n';
  write(fpm_log_fd, buffer, len + 1);
}

And here we are. When the access.format template contained %{something}e at the end and a long field was referenced (like REQUEST_URI in our example), the PHP engine performed an out-of-boundaries read and also wrote a \n character outside of the allocated memory. Well, this is “just” a programming bug anyway.

But what can someone gain from it? In case access.log is readable by attackers, they might access sensitive information from PHP process memory. It depends on the actual circumstances what can those bytes contain.

We believe this vulnerability might be exploited for DoS; but as it has some strict prerequisites, the severity is low.

The fix is available with the commit: 
http://git.php.net/?p=php-src.git;a=commit;h=2721a0148649e07ed74468f097a28899741eb58f

 

LiteSpeed SAPI secret key improper disposal

The LiteSpeed Web Server – according to their official website – is allegedly the #1 commercial web server on the market. It is also advertised with benchmarks showing slightly faster processing of PHP scripts compared to the competition (Apache, Nginx). To achieve this, the LiteSpeed web server utilizes their own Server API (SAPI) module of the PHP engine. The communication between the webserver and PHP is similar to the FastCGI protocol; they call it LSAPI.

In suEXEC_Daemon mode, the LiteSpeed web server spawns one PHP master process during startup. It is running as root and accepts LSAPI requests. These requests contain the PHP specific details (which script to execute, HTTP request headers, etc.) the web server wants to be processed on behalf of the actual visitors. The web server (the LSAPI client) also sends the wanted unix user and group id along with the LSAPI request. The master process forks and calls setuid() to run the script as the specified user.

The request is authenticated with an MD5 based MAC calculation. (It’s not the industry standard HMAC construction – but hey, the MD5 hash function isn't collision resistant to begin with.) The MAC calculation is based on a pre-shared key called (LSAPI_SECRET or s_pSecret), which is generated automatically by the web server upon each startup.

While reviewing the source code, we discovered that the Litespeed PHP SAPI module does not clear this secret in its child processes. The child processes use this secret only once: in the function lsapi_suexec_auth() invoked by lsapi_changeUGid(). After successful initialization, the child saves the uid specified in the request in s_uid and never touches the secret ever again.

The secret is available in the PHP process memory space of the child processes. An attacker might be able to access and then use this information to impersonate another user on the system.

The actual exploitation might be possible by attackers being able to ptrace() to any LSAPI PHP process running as the same user. On shared web hosting environments, we don’t consider this prerequisite to be particularly strict.

After reporting this vulnerability to the PHP team, they fixed this issue with the following commit:
https://github.com/php/php-src/commit/c60d4b97707c513ee8b554eecf1c5c653cae5998#diff-19cd0c042863b5e723b785a39a866a25

 

LiteSpeed SAPI out of boundaries read due to missing input validation

Next to the previous vulnerability, we also discovered that the LiteSpeed SAPI module in PHP does not sanitize several fields of the LSAPI request correctly. In the source file sapi/litespeed/lsapilib.c, the parseRequest function calculates addresses of thesevariables in the following way:

    pReq->m_pScriptFile     = pReq->m_pReqBuf + pReq->m_pHeader->m_scriptFileOff;
    pReq->m_pScriptName     = pReq->m_pReqBuf + pReq->m_pHeader->m_scriptNameOff;
    pReq->m_pQueryString    = pReq->m_pReqBuf + pReq->m_pHeader->m_queryStringOff;
    pReq->m_pRequestMethod  = pReq->m_pReqBuf + pReq->m_pHeader->m_requestMethodOff;

These variables are then exported, so they become available in PHP code through the $_SERVER array.

As mentioned already, the offset fields (eg. m_scriptFileOff) of the header are not validated at all, so a segmentation fault occurs in the SAPI process after it receives an invalid value. This vulnerability might be exploited to carry out a DoS attack.

Reading s_pSecret (LSAPI_SECRET) might also be possible, allowing the attacker to run PHP code as any (non-root) user.

Access to the SAPI socket is a prerequisite of the attack – though this is not a particularly strict requirement in the era of process containerization.

After reporting this vulnerability to the PHP team, the following fix has been committed to the repo:
https://github.com/php/php-src/commit/08080c18f5f3700af6242a338a2698502207ed45

 

The above vulnerabilities were discovered by Imre Rad. The first fixed PHP versions are: 5.5.31, 5.6.17 and 7.0.2.

Share