How to use PHP Runkit_Sandbox safely

I am creating a web application for a training tool that allows users to submit PHP classes as text, and then the application will run them. I think Runkit_Sandbox is the tool for this work, but docs do not offer much advice on which configurations to use.

Is there an established list of features to disable? Or classes? I plan to set all other configurations as restrictive as possible (for example, disable url fopen), but Im not even 100% sure what it is. Any advice is greatly appreciated.

+5
source share
3 answers

The main part of the functionality is sandboxes (Runkit_Sandbox class). Using them, you can run PHP code in an isolated environment. Each sandbox can be configured with its own PHP security settings, such as safe_mode, safe_mode_gid, safe_mode_include_dir, open_basedir, allow_url_fopen, disable_functions, disable_classes.

In addition, each sandbox can have individual values ​​for the Runkit INI parameters: its own global variables and the prohibition on canceling built-in functions.

Sandboxes can load PHP files (via include (), include_once (), require (), and require_once ()), call internal functions, execute arbitrary PHP code, and print the contained values ​​of variables. In addition, you can specify a function to capture and process the output of the sandbox .

In the sandbox, you can create an anti-sandbox class object of the Runkit_Sandbox_Parent class that connects the sandbox to its parent environment. The functionality of the anti-sandbox is very similar to the functionality of the sandbox , but for security reasons, each type of communication with the external environment should be explicitly enabled during the sandbox .

+2
source

I am creating a web application for a training tool that allows users to submit php classes

If you create an application , then you are not too much in control of your environment. This means that your solution must be based on PHP (which makes Runkit attractive), because the application can be hosted anywhere, perhaps somewhere you cannot install any of the solutions below. You are still limited to those ISPs that provide Runkit or the ability to install it, but more than ISPs can allow you to install a chroot jail or a second copy of the web server.

But from other comments it seems to me that you are creating the installation . That is, the whole machine (real or virtual), where you can do whatever you want. What makes others, IMHO more effective, methods are possible:

Two web servers (easier)

Install a second web server that listens only to the local host with limited rights (for example, acts as a user who does not have write access to its own web root). Install there a hard copy of PHP. To do this, start with the old rules with a list of functions to disable , then go to the HOWTOs and some pointers here . This last one is more targeted to your customers, but it can be useful (and knowing what you thought about security may reduce the number of attempts).

You can even install XDebug on secondary PHP and use, for example, PHPUnit code coverage tools to get useful information.

Now you can deploy your code by writing /var/www-secure/website-user123/htdocs from your main web installation, which can write to /var/www-secure and also run restart on the secondary web server using the commands system("sudo...") . You can provide real-life commands for a hosted web application through curl .

Linux makes it even easier to use apparmor / SELinux or user-level firewall rules . This means that no matter what the hosted application does, it will not be able to communicate outside, receive commands other than yours, or do anything outside the root of the website - where you can read and check something, eg. through tripwire .

You can even leave the dangerous function turned on (but blocked by apparmor / iptables), and then check the logs to see if the protection has been called. Not recommended . You do want to check the logs (and maybe run a test check on the system after running an unknown class), however, if someone succeeded in overthrowing the first level of protection in PHP. INI and got a broken attack.

Chrome prison

This is hanshenrik's answer, and it is attractive if you run things through the CLI. Depending on your setup and what the classes should do, it might be better than both other alternatives (firewall / apparmor still required, or at least it could benefit them) or less powerful.

Virtual machine (more secure)

As above, but this time the "second installation" is completely isolated inside the virtual machine. You can probably do this with Docker, but it will not be so safe; still check it out . You send the code inside the virtual machine using FTP (for this PHP has commands). This setting allows you to better isolate from the main installation. It is less flexible than the other two solutions, since you really have to use one virtual machine for each user, and resetting the virtual machine to neutral is more expensive. Running a virtual machine is more expensive. On the other hand, it can be more thorough (that is, you can redistribute all this more easily), and attacks with a marginal break are impossible, since the class of rogues can succeed in bullying the virtual processor.

+2
source

I think Runkit_Sandbox is the tool for this job - but I do not. assuming you work on a Unix system, can I suggest setting up a chroot jail instead?

 mkdir /jail /jail/bin /jail/lib /jail/lib64 /jail/usr /jail/etc /jail/etc/alternatives chmod -R 0711 /jail chown -R root:root /jail mount -o bind,ro /bin /jail/bin mount -o bind,ro /lib /jail/lib mount -o bind,ro /lib64 /jail/lib64 mount -o bind,ro /usr /jail/usr mount -o bind,ro /etc/alternatives /jail/etc/alternatives 

(others to consider include / dev / sys / proc)

and I assume that your code was originally received & processed by an unprivileged user, let me call it www-data , if so, you can use sudo to allow www data to run a specific command with sudo, for this add

 www-data ALL = (root) NOPASSWD: /usr/bin/php /jail/jailexecutor.php 

in / etc / sudoers

this will allow www data to run a specific command sudo /usr/bin/php /jail/jailexecutor.php

now for jailexecutor.php, it takes the source code from STDIN, executes it with php bound to / jail as the nobody user, and outputs the STDOUT and STDERR generated by the code back and terminates it if it takes longer than 5 seconds,

 <?php declare(strict_types = 1); const MAX_RUNTIME_SECONDS = 5; if (posix_geteuid () !== 0) { fprintf ( STDERR, "this script must run as root (only root can chroot)" ); die (); } $code = stream_get_contents ( STDIN ); if (! is_string ( $code )) { throw new \RuntimeException ( 'failed to read the code from stdin! (stream_get_contents failed)' ); } $file = tempnam ( __DIR__, "unsafe" ); if (! is_string ( $file )) { throw new \RuntimeException ( 'tempnam failed!' ); } register_shutdown_function ( function () use (&$file) { if (! unlink ( $file )) { throw new \RuntimeException ( 'failed to clean up the file! (unlink failed!?)' ); } } ); if (strlen ( $code ) !== file_put_contents ( $file, $code )) { throw new \RuntimeException ( 'failed to write the code to disk! (out of diskspace?)' ); } if (! chmod ( $file, 0444 )) { throw new \RuntimeException ( 'failed to chmod!' ); } $starttime = microtime ( true ); $unused = [ ]; $ph = proc_open ( 'chroot --userspec=nobody /jail /usr/bin/php ' . escapeshellarg ( basename ( $file ) ), $unused, $unused ); $terminated = false; // OPTIMIZE ME: use stream_select() instead of usleep() while ( ($status = proc_get_status ( $ph )) ['running'] ) { usleep ( 100 * 1000 ); // 100 ms if (! $terminated && microtime ( true ) - $starttime > MAX_RUNTIME_SECONDS) { $terminated = true; echo 'max runtime reached (' . MAX_RUNTIME_SECONDS . ' seconds), terminating...'; pkilltree ( ( int ) ($status ['pid']) ); // proc_terminate ( $ph, SIGKILL ); } } echo "\nexit status: " . $status ['exitcode']; proc_close ( $ph ); function pkilltree(int $pid) { system ( "kill -s STOP " . $pid ); // stop it first, so it can't make any more children $children = shell_exec ( 'pgrep -P ' . $pid ); if (is_string ( $children )) { $children = trim ( $children ); } if (! empty ( $children )) { $children = array_filter ( array_map ( 'trim', explode ( "\n", $children ) ), function ($in) { return false !== filter_var ( $in, FILTER_VALIDATE_INT ); // shouldn't be necessary, but just to be safe.. } ); foreach ( $children as $child ) { pkilltree ( ( int ) $child ); } } system ( "kill -s KILL " . $pid ); } 

PHP code can now be safely executed from www data as follows:

 <?php declare(strict_types = 1); header ( "content-type: text/plain;charset=utf8" ); $unsafeCode = ( string ) ($_POST ['code'] ?? ''); $pipes = [ ]; $proc = proc_open ( "sudo /usr/bin/php /jail/jailexecutor.php", array ( 0 => array ( "pipe", "rb" ), 1 => array ( "pipe", "wb" ), 2 => array ( "pipe", "wb" ) ), $pipes ); fwrite ( $pipes [0], $unsafeCode ); fclose ( $pipes [0] ); while ( ($status = proc_get_status ( $proc )) ['running'] ) { usleep ( 100 * 1000 ); // 100 ms echo stream_get_contents ( $pipes [2] ); echo stream_get_contents ( $pipes [1] ); } // var_dump($status); echo stream_get_contents ( $pipes [2] ); // just to be safe, it technically possible that we dont get any cpu time between proc_open, the child finishes, and proc_get_status.. just it unlikely. fclose($pipes[2]); echo stream_get_contents ( $pipes [1] ); fclose($pipes[1]); proc_close ( $proc ); 

and for a quick test, curl -d code='<?php echo rand()."it works!";' url curl -d code='<?php echo rand()."it works!";' url (you can even add system("rm -rfv --no-preserve-root /"); without worrying)

  • tested on Debian 9 Stretch
+1
source

Source: https://habr.com/ru/post/1273884/


All Articles