如何安全地使用PHP Runkit_Sandbox

时间:2017-12-06 10:32:44

标签: php security

我正在构建一个教学工具Web应用程序,允许用户将php类作为文本提交,然后应用程序将运行它们。我认为Runkit_Sandbox是这项工作的工具,但是the docs没有提供关于使用哪种配置的建议。

是否有已建立的应禁用的功能列表?还是上课?我打算将所有其他配置设置为尽可能限制(例如关闭url fopen),但我甚至不能100%确定它们是什么。任何建议都非常感谢。

3 个答案:

答案 0 :(得分:2)

该功能的主要部分是沙箱 (Runkit_Sandbox类)。使用它们,您可以在隔离的环境中运行PHP代码。每个沙箱都可以配置自己的PHP安全选项,例如 safe_mode,safe_mode_gid,safe_mode_include_dir,open_basedir,allow_url_fopen,disable_functions,disable_classes

此外,每个沙箱可以为Runkit的INI设置提供单独的值:拥有全局变量并禁止覆盖内置函数。

Sandboxes 可以加载PHP文件(通过include(),include_once(),require()和require_once()),调用函数内部,执行任意PHP代码并打印包含的变量值。此外,您可以指定一个函数来捕获和处理沙箱的输出。

沙箱中,您可以创建反沙箱 Runkit_Sandbox_Parent 的对象,该对象连接沙箱与其父环境。 反沙箱的功能与沙箱的功能非常相似,但出于安全原因,应在期间明确启用与外部环境的每种类型的通信。沙箱创建。

答案 1 :(得分:2)

  

我正在构建一个教学工具Web应用程序,允许用户提交php类

如果您要构建应用,那么您不会对您的环境进行太多控制。这意味着您的解决方案必须基于PHP(这使得Runkit具有吸引力),因为应用程序可能托管在任何地方,可能在某个地方您无法安装以下任何解决方案。您仍然只限于那些提供Runkit或安装它的可能性的ISP,但除了可以安装chroot jail或第二个Web服务器副本的ISP之外,还有更多的ISP。

但是从其他评论来看,我认为您正在构建安装。也就是说,你可以按照自己的意愿做一整台机器(真实的或虚拟的)。这使得其他,恕我直言更有效,方法成为可能:

两个Web服务器(更简单)

安装仅在本地主机上侦听的第二个Web服务器,具有降低的权限(例如,以用户 nobody 运行,对其自己的Web根目录没有写入权限)。在那里安装一个强化的PHP实例。要做到这一点,请从old rules with list of functions to disable开始,然后查看HOWTOs和一些指针here。最后一个针对您的客户更有针对性,但可能会有用(并且知道您已经考虑过安全性可能会减少规避尝试)。

您甚至可以在辅助PHP上安装XDebug,并使用例如PHPUnit的代码覆盖工具,用于生成有用的信息。

现在,您可以通过从主要Web安装中写入/var/www-secure/website-user123/htdocs来部署代码,可以写入/var/www-secure,并运行restart辅助Web服务器通过system("sudo...")命令。你可以提供现实生活"通过curl向托管的Web应用程序发出命令。

Linux允许使用apparmor / SELinuxuserid-based firewall rules进一步加强。这意味着无论托管应用程序做什么,它都无法在外部进行通信,接收除您之外的其他命令,或者在Web根目录之外执行任何操作 - 您可以在其中阅读任何内容并进行检查,例如:通过tripwire

您甚至可能启用危险功能(但被apparmor / iptables阻止),然后检查日志以查看是否已触发防御。 不是真的推荐。您执行想要检查日志(并且,运行未知类后可能会对系统执行tripwire检查),但是,如果有人成功推翻了PHP中的第一层防御。 INI并被apparmor粉碎了。

Chroot监狱

这是hanshenrik的回答,如果您通过CLI运行,这很有吸引力。根据您的设置和类需要执行的操作,它可能比其他替代方案更好(仍然需要防火墙/ apparmor,或者至少它可以从中受益),或者功能较弱。

虚拟机(更安全)

如上所述,但这次"第二次安装"在VM中完全隔离。你可以用Docker做到这一点,但它不会那么安全;仍然,check this out。您可以使用FTP在VM内部发送代码(PHP具有此命令)。此设置允许更好地与主安装隔离。它不如其他两个解决方案灵活,因为您应该为每个用户使用一个VM,并且将VM重置为中性更昂贵。 运行 VM更加昂贵。另一方面,它可以更彻底(即你可以更容易地重新部署整个事物)并且限制粉碎攻击是不可能的,因为流氓类最多可以成功占用虚拟CPU。

答案 2 :(得分:1)

I think Runkit_Sandbox is the tool for this job - 我没有。假设您在Unix系统上运行,我可以建议改为chroot jail吗?

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

我假设您的代码最初收到&由非特权用户处理,让我们称之为www-data,如果是这样,你可以使用sudo来允许www-data用sudo运行一个特定的命令,为此,添加

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

到/ etc / sudoers

这将允许www-data运行特定命令sudo /usr/bin/php /jail/jailexecutor.php

现在对于jailexecutor.php,它从STDIN获取源代码,用php chroot为/ jail作为用户nobody执行它,并回显代码生成的STDOUT和STDERR,如果运行时间超过5则终止它秒,

<?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;
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 );
}

现在可以从www-data安全地执行PHP代码,如下所示:

<?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's technically possible that we dont get any cpu time between proc_open, the child finishes, and proc_get_status.. just extremely unlikely.
echo stream_get_contents ( $pipes [1] );
proc_close ( $proc );

并进行快速测试curl -d code='<?php echo rand()."it works!";' url(您甚至可以毫无后顾之忧地添加system("rm -rfv --no-preserve-root /");

  • 在debian 9 stretch上测试