在PHP中以root身份运行外部命令[转自linuxaid]
孤狼 发表于 2001-10-11 10:15:41 PHP ←返回版面 [快速返回]
Hunte 2001年4月15日
在PHP中运行只有root用户才可以运行的外部程序,一直是个老问题,用常规的办法很难实现。这是因为一般情况下,PHP是作为APACHE的一个模块
的,也就是说,PHP是APACHE的一部分,而APACHE除了suEXEC机制外,是不能以不同的用户ID来执行命令的,但suEXEC机制只能
CGI有效。
网上曾经有一篇文章,说用调用"su - -c COMMAND"可以实现,但经过多次试验,发现不行,因为su命令必须在STDIN上输入root的密码。
怎么办?用常规的方法难以奏效,只能再想其它的方法了。成功的关键在于能有一个可以切换用户ID但又可以在命令上输入密码(或不用输入密码)的工具。有这样的工具吗?有,它就是super。
下面就具体说说如何来做?
要注意的是,安装和配置super,都要以root身份来进行。
第一步,切换到root下
第二步,安装super
先到ftp://ftp.mdtsoft.com/pub/super下载super-3.14.0-1.i386.rpm。这是一个RPM文件,其它包括了两个工具:setuid和super,以及它们的文档和man手册。用下面的命令将它安装到系统中:
% rpm -Uvh super-3.14.0-1.i386.rpm
你还可以用这个命令来查看这个RPM中的文件:
% rpm -qpl super-3.14.0-1.i386.rpm
从结果可以看到,两个工具都将被安装到/bin目录下。
第三步,配置super
super的配置文件是/etc/super.tab。这是一个文本文件,格式也比较复杂。不过,我们这里只要很简单的加上几行就可以了。至于详细的说明,可以通过man super.tab来查看。
假设运行Apache的用户是nobody,我们欲通过super来增加系统用户(调用useradd命令),那么我们只要在super.tab文件中加入以下这行:
auser /sbin/useradd nobody,hunte
第一段是super能够识别的命令的别名;第二段是该别名所对应的系统命令的全路径;第三段是可以运行该命令的用户列表,用逗号分隔。这里除了nobody外,还一个叫hunte的普通用户,是用于下面的测试。当然,你应该用你系统中有的任意一个普通用户。
至此,super的配置就算好了。
第四步,测试
以第三步中指定的非nobody用户登录,运行:
% /bin/super auser testuser
如果前面的配置没什么错误的话,用户testuser应该是成功地创建了。可以用:
% cat /etc/passwd | grep testuser
命令来验证一下。
第五步,在PHP中调用该命令
下面是PHP代码:
<?
if ($username)
{
//应该检查新用户是否已经存在
echo '正在创建用户<$username>...';
system(escapeshellcmd("/bin/super auser $username"));
}
?>
使用super,使得在PHP中以root身份运行外部命令不再是难事。试试看吧。
测试环境:RedHat Linux 7.0 (Kernel 2.4.3) + Apache 1.3.9 + PHP 4.0.4pl1
chpasswd.cgi是一个修改linux用户密码的cgi程序,弥补更改密码需要两次输入密码的缺陷
php执行linux系统命令的几个函数:
system函数
说明:执行外部程序并显示输出资料。
语法:string system(string command, int [return_var]);
返回值: 字符串
详细介绍:
本函数就像是 C 语中的函数 system(),用来执行指令,并输出结果。若是 return_var 参数存在,则执行 command
之后的状态会填入 return_var 中。同样值得注意的是若需要处理用户输入的资料,而又要防止用户耍花招破解系统,则可以使用
EscapeShellCmd()。若 PHP 以模块式的执行,本函数会在每一行输出后自动更新 Web
服务器的输出缓冲暂存区。若需要完整的返回字符串,且不想经过不必要的其它中间的输出界面,可以使用 PassThru()。
实例代码:
< ?php
$last_line = system('ls', $retval);
echo 'Last line of the output: ' . $last_line;
echo '<hr />Return value: ' . $retval;
?>
exec函数
说明:执行外部程序。
语法:string exec(string command, string [array], int [return_var]);
返回值: 字符串
详细介绍:
本函数执行输入 command 的外部程序或外部指令。它的返回字符串只是外部程序执行后返回的最后一行;若需要完整的返回字符串,可以使用 PassThru() 这个函数。
要是参数 array 存在,command 会将 array 加到参数中执行,若不欲 array 被处理,可以在执行 exec()
之前呼叫 unset()。若是 return_var 跟 array 二个参数都存在,则执行 command 之后的状态会填入
return_var 中。
值得注意的是若需要处理使用者输入的资料,而又要防止使用者耍花招破解系统,则可以使用 EscapeShellCmd()。
实例代码:
< ?php
echo exec('whoami');
?>
popen函数
说明:打开文件。
语法:int popen(string command, string mode);
返回值: 整数
详细介绍:
本函数执行指令开档,而该文件是用管道方式处理的文件。用本函数打开的文件只能是单向的 (只能读或只能写),而且一定要用 pclose() 关闭。在文件操作上可使用 fgets()、fgetss() 与 fputs()。若是开档发生错误,返回 false 值。
实例代码:
< ?php
$fp = popen( "/bin/ls", "r" );
?>
但是执行的命令要涉及权限,举个例子:使用php执行"service network restart"命令,重启网络。apache是以一个一般用户执行的,我计算机使用apache用户,根本没有执行这个命令的权限。
解决办法:
1、绕开这个限制:
首先写个c程序,例如/usr/local/a.c
#include <stdlib.h>
main()
{
uid_t uid,euid;
uid = getuid();
euid = geteuid();
if (setreuid(euid,uid))
perror("setreuid");
system("service network restart");
}
编译文件gcc -oa -Wall a.c,这时会在当前目录下生成程序a
程序的属主chmod u+s ./a。
apache的uid 为48。调用setreuid后将有效用户id和实际用户id互换了。(必须在chmod u+s生效的情况下) 使apache当前的uid为0这样就能执行root命令了。
php程序调用a:
<?php
function RestartNetwork() {
system("/usr/local/a");
}
RestartNetwork();
?>
2、编辑/etc/sudoers文件,或者使用visudo命令
Host_Alias SERVER = localhost,你的主机名
User_Alias USERLIST = apache用户名
Cmnd_Alias COMMANDLIST = /sbin/service #添加想让apache用户可以使用的命令
#Defaults requiretty
......
修改完成后,保存/etc/sudoers只能是0440权限
然后,php就可以使用service命令
<?php
system("sudo service network restart");
?>
/etc/sudoers配置范例以及解释:
示例
下面是sudoers条目的示例.显然,一些示例有点不自然.首先,我们定义别名:
# User alias specification
User_Alias FULLTIMERS = millert, mikef, dowdy
User_Alias PARTTIMERS = bostley, jwfox, crawl
User_Alias WEBMASTERS = will, wendy, wim
# Runas alias specification
Runas_Alias OP = root, operator
Runas_Alias DB = oracle, sybase
# Host alias specification
Host_Alias SPARC = bigtime, eclipse, moet, anchor :\
SGI = grolsch, dandelion, black :\
ALPHA = widget, thalamus, foobar :\
HPPA = boa, nag, python
Host_Alias CUNETS = 128.138.0.0/255.255.0.0
Host_Alias CSNETS = 128.138.243.0, 128.138.204.0/24, 128.138.242.0
Host_Alias SERVERS = master, mail, www, ns
Host_Alias CDROM = orion, perseus, hercules
# Cmnd alias specification
Cmnd_Alias DUMPS = /usr/bin/mt, /usr/sbin/dump, /usr/sbin/rdump,\
/usr/sbin/restore, /usr/sbin/rrestore
Cmnd_Alias KILL = /usr/bin/kill
Cmnd_Alias PRINTING = /usr/sbin/lpc, /usr/bin/lprm
Cmnd_Alias SHUTDOWN = /usr/sbin/shutdown
Cmnd_Alias HALT = /usr/sbin/halt, /usr/sbin/fasthalt
Cmnd_Alias REBOOT = /usr/sbin/reboot, /usr/sbin/fastboot
Cmnd_Alias SHELLS = /usr/bin/sh, /usr/bin/csh, /usr/bin/ksh, \
/usr/local/bin/tcsh, /usr/bin/rsh, \
/usr/local/bin/zsh
Cmnd_Alias SU = /usr/bin/su
接
下来我们改写了一些编译进的默认值.我们希望sudo在所有情况下使用auth设施在系统日志中作记录.我们不想总是输出初次使用sudo的提示,并且用
户millert使用sudo不需要口令.另外,我们为SERVERS
Host_Alias中的主机维护一个附加的本地日志文件.并且由于这个日志需要保存许多年,我们要在日志的每一行中加入年份信息.
# Override built in defaults
Defaults syslog=auth
Defaults:FULLTIMERS !lecture
Defaults:millert !authenticate
Defaults@SERVERS log_year, logfile=/var/log/sudo.log
实际上决定谁能运行什么的是用户说明部分:
root ALL = (ALL) ALL
%wheel ALL = (ALL) ALL
我们让root和wheel组的成员可以在任何主机上以任何人的身份运行任何命令.
FULLTIMERS ALL = NOPASSWD: ALL
专职系统管理员(millert,mikef和dowdy)可以在任何主机上执行任何命令而不需要进行身份验证.
PARTTIMERS ALL = ALL
兼职系统管理员(bostley,jwfox和crawl)可以在任何主机上运行任何命令,但他们首先必须进行身份验证(因为这个条目没有NOPASSWD标签).
jack CSNETS = ALL
用
户jack可以在CSNETS别名(网络128.138.243.0,128.138.204.0和128.138.242.0)中的机器上运行任何命
令.在这些网络中,只有128.138.204.0明确的给出了掩码(用CIDR方式),指明这是个C类网.对CSNETS中的其他网络来说,如果本地主
机的掩码匹配的话将使用本地主机的掩码.
lisa CUNETS = ALL
用户lisa可以在CUNETS别名(B类网络128.138.0.0)中的任何主机上执行任何命令.
operator ALL = DUMPS, KILL, PRINTING, SHUTDOWN, HALT, REBOOT,\
/usr/oper/bin/
用户operator可以执行的命令被限制在一个简单的maintenance中.这里就是和备份,杀死进程,打印系统,关闭系统和任何在/usr/oper/bin/中的命令.
joe ALL = /usr/bin/su operator
用户joe只能su成operator.
pete HPPA = /usr/bin/passwd [A-z]*, !/usr/bin/passwd root
用户pete可以修改HPPA上除了root以外任何用户的口令.注意,这里人为passwd不会在命令行上使用多个用户名.
bob SPARC = (OP) ALL : SGI = (OP) ALL
用户可以在SPARC和SGI上以任何在OP Runas_Alias中列出的用户(root和operator)的身份运行任何命令.
jim +biglab = ALL
用户jim可以在biglab网络用户组中的机器上运行任何命令.因为biglab前面加上了前缀"+",所以sudo知道这是一个网络用户组.
+secretaries ALL = PRINTING, /usr/bin/adduser, /usr/bin/rmuser
网络用户组secretaries中的用户需要帮助管理打印机和进行增删用户的工作,所以需要允许他们在任何机器上执行这些命令.
fred ALL = (DB) NOPASSWD: ALL
用户fred可以在DB Runas_Alias (oracle 或 sybase)上以任何人的身份执行任何命令而不需要提供口令.
john ALPHA = /usr/bin/su [!-]*, !/usr/bin/su *root*
用户john可以在ALPHA上su成除root的任何人,但是不允许他给su加上任何标志执行.
jen ALL, !SERVERS = ALL
用户jen可以在除了SERVERS Host_Alias (master, mail, www 和 ns)以外的任何机器上执行任何命令.
jill SERVERS = /usr/bin/, !SU, !SHELLS
用户jill可以在任何SERVERS Host_Alias中的机器上运行/usr/bin/下除了属于SU和SHELLS Cmnd_Aliases外的任何命令.
steve CSNETS = (operator) /usr/local/op_commands/
用户steve可以在任何CSNETS中的机器上执行/usr/local/op_commands/中的所有命令,但是只能以operator的身份.
matt valkyrie = KILL
用户matt应该可以在他自己的工作站valkyrie上杀死挂起的进程.
WEBMASTERS www = (www) ALL, (root) /usr/bin/su www
任何在WEBMASTERS User_Alias (will, wendy,和 wim)中的用户都能够在主机www上以www(网页的所有者)的身份执行任何命令,或者简单的su成www.
ALL CDROM = NOPASSWD: /sbin/umount /CDROM,\
/sbin/mount -o nosuid\,nodev /dev/cd0a /CDROM
所
有的用户都应该能够在 CDROM Host_Alias (orion, perseus,
hercules)中的主机上mount和unmount光盘驱动器而不需要输入口令.对用户来说敲入这么长的命令有些冗长而乏味,所以把它封装在一个
shell脚本中是一个最佳的选择.
安全要点
使用"!"来从ALL中减去命令一般来说不会产生预期作用.用户可以简单的通过把希望执行的命令改名执行的法子来绕过限制.例如:
bill ALL = ALL, !SU, !SHELLS
这
并不会阻止bill执行SU和SHELLS中列出的命令.他只需要把这些命令改一个名字,或者从一个编辑器或者其他程序中escape到shell(译
注:原文是use a shell escape from an editor or other
program)就可以运行了.所以这种类型的限制至少应该经过深思熟虑(并从策略上加强它).
CAVEATS
sudoers必需总是使用visudo命令来编辑,因为它会锁定文件并且进行语法检察.这强制sudoers摆脱语法错误,因为sudoers有语法错误时sudo是不会运行的.
当使用机器的网络用户组时(与用户相反),如果您在网络用户组中存贮了完整的主机名(这经常是事实),您需要让主机名像hostname命令的输出一样是完整的或者在sudoers中使用fqdn选项.
文件
/etc/sudoers 谁能作什么的一个列表
/etc/group 本地组文件
/etc/netgroup 网络组文件
在玩C以前玩过一段时间的PHP,哪个时候需要用PHP来运行root命令,一直未果,直到有一天搜索到了super这个插件.
随着玩C的日子多了.发现可以用C语言来包裹要运行的外部命令.实验了一下.成功了.
不需要任何外部工具就可以实现用PHP执行root命令.
我下面就把方法发布给大家,有需求用php来运行root命令的朋友可以不用发愁了.
平台:Linux.实验命令iptables当前的目录是/var/www/html/http
写程序的时候用root用户
大家都知道iptables非root用户不能运行.
首先写个C程序
命名为:ipt.c
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
intmain()
{
uid_tuid,euid;
charcmd[1024];
uid=getuid();
euid=geteuid();
printf("myuid:%u\n",getuid());//这里显示的是当前的uid可以注释掉.
printf("myeuid:%u\n",geteuid());//这里显示的是当前的euid
if(setreuid(euid,uid))//交换这两个id
perror("setreuid");
printf("aftersetreuiduid:%u\n",getuid());
printf("afersertreuideuid:%u\n",geteuid());
system("/sbin/iptables-L");//执行iptables-L命令
return0;
}
[/CO
DE]
编译该文件gcc-oipt-Wallipt.c
在该路径下生成ipt这个可执行文件.
如果现在用PHP网页调用该ipt的话,即使setreuid了也是不行的.
接下来要做的是chmodu+s./ipt
ls一下
-rwsr-xr-x1rootroot5382Jul221:45ipt
s位已经设置上了.
再写一个php页面调用它.
<?php
echo'<pre>';
$last_line=system('/var/www/html/http/ipt',$retval);
echo'
</pre>
<hr/>Lastlineoftheoutput:'.$last_line.'
<hr/>Returnvalue:'.$retval;
?>
在浏览器中浏览.
[color=Red]ChainINPUT(policyACCEPT)
targetprotoptsourcedestination
ChainFORWARD(policyDROP)
targetprotoptsourcedestination
ACCEPTall--anywhereanywherestateRELATED,ESTABLISHED
ChainOUTPUT(policyACCEPT)
targetprotoptsourcedestination[/color]
[color=Blue]myuid:48
myeuid:0
aftersetreuiduid:0
afersertreuideuid:48[/color]
--------------------------------------------------------------------------------
Lastlineoftheoutput:afersertreuideuid:48
--------------------------------------------------------------------------------
Returnvalue:0
该命令执行成功..
众所周知:apache的uid为48.调用setreuid后将有效用户id和实际用户id互换了.(必须在chmodu+s生效的情况下)使apache当前的uid为0这样就能执行root命令了。
大家只需要更改C文件中的system所要执行的命令就可以实现自己的PHP执行root命令了.
评论