我正在构建一个教学工具Web应用程序,允许用户将php类作为文本提交,然后应用程序将运行它们。我认为Runkit_Sandbox是这项工作的工具,但是the docs没有提供关于使用哪种配置的建议。
是否有已建立的应禁用的功能列表?还是上课?我打算将所有其他配置设置为尽可能限制(例如关闭url fopen),但我甚至不能100%确定它们是什么。任何建议都非常感谢。
答案 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服务器,具有降低的权限(例如,以用户 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
/ SELinux
或userid-based firewall rules进一步加强。这意味着无论托管应用程序做什么,它都无法在外部进行通信,接收除您之外的其他命令,或者在Web根目录之外执行任何操作 - 您可以在其中阅读任何内容并进行检查,例如:通过tripwire
。
您甚至可能启用危险功能(但被apparmor / iptables阻止),然后检查日志以查看是否已触发防御。 不是真的推荐。您执行想要检查日志(并且,运行未知类后可能会对系统执行tripwire检查),但是,如果有人成功推翻了PHP中的第一层防御。 INI并被apparmor粉碎了。
这是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 /");
)